fix: feat: task dispatcher — poll ops repo and launch runners (#45)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

This commit is contained in:
Agent 2026-03-29 09:15:01 +00:00
parent fb4ffe9fb6
commit c9ef5eb98b

132
docker/edge/dispatcher.sh Normal file → Executable file
View file

@ -1,12 +1,132 @@
#!/usr/bin/env bash
# dispatcher.sh — Edge task dispatcher placeholder
# dispatcher.sh — Edge task dispatcher
#
# TODO: Implement task polling and runner launching (#45)
# Currently a no-op loop for future expansion.
# 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
while true; do
# Placeholder: no-op loop, no logic yet
# Resolve script root (parent of lib/)
SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Source shared environment
source "${SCRIPT_ROOT}/../lib/env.sh"
# 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_OPS_REPO}..."
git clone "${FORGE_WEB}" "${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 (as space-separated list for env injection)
local secrets
secrets=$(jq -r '.secrets[]? // empty' "$action_file" 2>/dev/null | tr '\n' ' ')
# Run the formula via docker compose with action ID as argument
# The runner container should be defined in docker-compose.yml
# Secrets are injected via -e flags
local compose_cmd="docker compose run --rm runner ${formula} ${id}"
if [ -n "$secrets" ]; then
# Inject secrets as environment variables
for secret in $secrets; do
compose_cmd+=" -e ${secret}=${!secret}"
done
fi
log "Running: ${compose_cmd}"
eval "${compose_cmd}"
log "Runner completed for action: ${id}"
}
# Main dispatcher loop
main() {
log "Starting dispatcher..."
log "Polling ops repo: ${VAULT_ACTIONS_DIR}"
# Ensure ops repo is available
ensure_ops_repo
while true; do
# Check if actions directory exists
if [ ! -d "${VAULT_ACTIONS_DIR}" ]; then
log "Actions directory not found: ${VAULT_ACTIONS_DIR}"
sleep 60
done
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 "$@"