disinto/docker/edge/dispatcher.sh
Agent 6be0eee20b
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
fix: dispatcher — fix clone URL and secret injection
- Use FORGE_URL/FORGE_OPS_REPO for clonable URL
- Pass -e SECRET_NAME without value (Docker inherits from env)
- Simplify logging to hide all -e flags entirely
2026-03-29 11:00:58 +00:00

164 lines
4.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# dispatcher.sh — Edge task dispatcher
#
# Polls the ops repo for approved actions and launches task-runner containers.
# Part of #24.
#
# Action JSON schema:
# {
# "id": "publish-skill-20260328",
# "formula": "clawhub-publish",
# "secrets": ["CLAWHUB_TOKEN"],
# "tools": ["clawhub"],
# "context": "SKILL.md bumped to 0.3.0",
# "model": "sonnet"
# }
set -euo pipefail
# Resolve script root (parent of lib/)
SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Source shared environment
source "${SCRIPT_ROOT}/../lib/env.sh"
# Load vault secrets after env.sh (env.sh unsets them for agent security)
# Vault secrets must be available to the dispatcher
if [ -f "$FACTORY_ROOT/.env.vault.enc" ] && command -v sops &>/dev/null; then
set -a
eval "$(sops -d --output-type dotenv "$FACTORY_ROOT/.env.vault.enc" 2>/dev/null)" \
|| echo "Warning: failed to decrypt .env.vault.enc — vault secrets not loaded" >&2
set +a
elif [ -f "$FACTORY_ROOT/.env.vault" ]; then
set -a
# shellcheck source=/dev/null
source "$FACTORY_ROOT/.env.vault"
set +a
fi
# Ops repo location (vault/actions directory)
OPS_REPO_ROOT="${OPS_REPO_ROOT:-/home/debian/disinto-ops}"
VAULT_ACTIONS_DIR="${OPS_REPO_ROOT}/vault/actions"
# Log function
log() {
printf '[%s] %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*"
}
# Clone or pull the ops repo
ensure_ops_repo() {
if [ ! -d "${OPS_REPO_ROOT}/.git" ]; then
log "Cloning ops repo from ${FORGE_URL}/${FORGE_OPS_REPO}..."
git clone "${FORGE_URL}/${FORGE_OPS_REPO}" "${OPS_REPO_ROOT}"
else
log "Pulling latest ops repo changes..."
(cd "${OPS_REPO_ROOT}" && git pull --rebase)
fi
}
# Check if an action has already been completed
is_action_completed() {
local id="$1"
[ -f "${VAULT_ACTIONS_DIR}/${id}.result.json" ]
}
# Launch a runner for the given action ID
launch_runner() {
local id="$1"
log "Launching runner for action: ${id}"
# Read action config
local action_file="${VAULT_ACTIONS_DIR}/${id}.json"
if [ ! -f "$action_file" ]; then
log "ERROR: Action file not found: ${action_file}"
return 1
fi
# Extract formula from action JSON
local formula
formula=$(jq -r '.formula // empty' "$action_file")
if [ -z "$formula" ]; then
log "ERROR: Action ${id} missing 'formula' field"
return 1
fi
# Extract secrets (array for safe handling)
local -a secrets=()
while IFS= read -r secret; do
[ -n "$secret" ] && secrets+=("$secret")
done < <(jq -r '.secrets[]? // empty' "$action_file" 2>/dev/null)
# Build command array (safe from shell injection)
local -a cmd=(docker compose run --rm runner)
# Add environment variables BEFORE service name
for secret in "${secrets[@]+"${secrets[@]}"}"; do
cmd+=(-e "${secret}") # Pass actual value to container (from env)
done
# Add formula and id as arguments (after service name)
cmd+=("$formula" "$id")
# Log command skeleton (hide all -e flags for security)
local -a log_cmd=()
local skip_next=0
for arg in "${cmd[@]}"; do
if [[ $skip_next -eq 1 ]]; then
skip_next=0
continue
fi
if [[ "$arg" == "-e" ]]; then
log_cmd+=("$arg" "<redacted>")
skip_next=1
else
log_cmd+=("$arg")
fi
done
log "Running: ${log_cmd[*]}"
# Execute with array expansion (safe from shell injection)
"${cmd[@]}"
log "Runner completed for action: ${id}"
}
# Main dispatcher loop
main() {
log "Starting dispatcher..."
log "Polling ops repo: ${VAULT_ACTIONS_DIR}"
while true; do
# Refresh ops repo at the start of each poll cycle
ensure_ops_repo
# Check if actions directory exists
if [ ! -d "${VAULT_ACTIONS_DIR}" ]; then
log "Actions directory not found: ${VAULT_ACTIONS_DIR}"
sleep 60
continue
fi
# Process each action file
for action_file in "${VAULT_ACTIONS_DIR}"/*.json; do
# Handle case where no .json files exist
[ -e "$action_file" ] || continue
local id
id=$(basename "$action_file" .json)
# Skip if already completed
if is_action_completed "$id"; then
continue
fi
# Launch runner for this action
launch_runner "$id"
done
# Wait before next poll
sleep 60
done
}
# Run main
main "$@"