fix: [nomad-prep] P2 — dispatcher refactor: pluggable launcher + DISPATCHER_BACKEND flag (#802)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-04-16 00:22:10 +00:00
parent 7513e93d6d
commit ef40433fff

View file

@ -8,8 +8,8 @@
# 2. Scan vault/actions/ for TOML files without .result.json # 2. Scan vault/actions/ for TOML files without .result.json
# 3. Verify TOML arrived via merged PR with admin merger (Forgejo API) # 3. Verify TOML arrived via merged PR with admin merger (Forgejo API)
# 4. Validate TOML using vault-env.sh validator # 4. Validate TOML using vault-env.sh validator
# 5. Decrypt declared secrets from secrets/<NAME>.enc (age-encrypted) # 5. Decrypt declared secrets via load_secret (lib/env.sh)
# 6. Launch: docker run --rm disinto/agents:latest <action-id> # 6. Launch: delegate to _launch_runner_{docker,nomad} backend
# 7. Write <action-id>.result.json with exit code, timestamp, logs summary # 7. Write <action-id>.result.json with exit code, timestamp, logs summary
# #
# Part of #76. # Part of #76.
@ -19,7 +19,7 @@ set -euo pipefail
# Resolve script root (parent of lib/) # Resolve script root (parent of lib/)
SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SCRIPT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Source shared environment # Source shared environment (provides load_secret, log helpers, etc.)
source "${SCRIPT_ROOT}/../lib/env.sh" source "${SCRIPT_ROOT}/../lib/env.sh"
# Project TOML location: prefer mounted path, fall back to cloned path # Project TOML location: prefer mounted path, fall back to cloned path
@ -27,34 +27,11 @@ source "${SCRIPT_ROOT}/../lib/env.sh"
# the shallow clone only has .toml.example files. # the shallow clone only has .toml.example files.
PROJECTS_DIR="${PROJECTS_DIR:-${FACTORY_ROOT:-/opt/disinto}-projects}" PROJECTS_DIR="${PROJECTS_DIR:-${FACTORY_ROOT:-/opt/disinto}-projects}"
# Load granular secrets from secrets/*.enc (age-encrypted, one file per key). # -----------------------------------------------------------------------------
# These are decrypted on demand and exported so the dispatcher can pass them # Backend selection: DISPATCHER_BACKEND={docker,nomad}
# to runner containers. Replaces the old monolithic .env.vault.enc store (#777). # Default: docker. nomad lands as a pure addition during migration Step 5.
_AGE_KEY_FILE="${HOME}/.config/sops/age/keys.txt" # -----------------------------------------------------------------------------
_SECRETS_DIR="${FACTORY_ROOT}/secrets" DISPATCHER_BACKEND="${DISPATCHER_BACKEND:-docker}"
# decrypt_secret <NAME> — decrypt secrets/<NAME>.enc and print the plaintext value
decrypt_secret() {
local name="$1"
local enc_path="${_SECRETS_DIR}/${name}.enc"
if [ ! -f "$enc_path" ]; then
return 1
fi
age -d -i "$_AGE_KEY_FILE" "$enc_path" 2>/dev/null
}
# load_secrets <NAME ...> — decrypt each secret and export it
load_secrets() {
if [ ! -f "$_AGE_KEY_FILE" ]; then
echo "Warning: age key not found at ${_AGE_KEY_FILE} — secrets not loaded" >&2
return 1
fi
for name in "$@"; do
local val
val=$(decrypt_secret "$name") || continue
export "$name=$val"
done
}
# Ops repo location (vault/actions directory) # Ops repo location (vault/actions directory)
OPS_REPO_ROOT="${OPS_REPO_ROOT:-/home/debian/disinto-ops}" OPS_REPO_ROOT="${OPS_REPO_ROOT:-/home/debian/disinto-ops}"
@ -391,47 +368,21 @@ write_result() {
log "Result written: ${result_file}" log "Result written: ${result_file}"
} }
# Launch runner for the given action # -----------------------------------------------------------------------------
# Usage: launch_runner <toml_file> # Pluggable launcher backends
launch_runner() { # -----------------------------------------------------------------------------
local toml_file="$1"
local action_id
action_id=$(basename "$toml_file" .toml)
log "Launching runner for action: ${action_id}" # _launch_runner_docker ACTION_ID SECRETS_CSV MOUNTS_CSV
#
# Builds and executes a `docker run` command for the vault runner.
# Secrets are resolved via load_secret (lib/env.sh).
# Returns: exit code of the docker run. Stdout/stderr are captured to a temp
# log file whose path is printed to stdout (caller reads it).
_launch_runner_docker() {
local action_id="$1"
local secrets_csv="$2"
local mounts_csv="$3"
# Validate TOML
if ! validate_action "$toml_file"; then
log "ERROR: Action validation failed for ${action_id}"
write_result "$action_id" 1 "Validation failed: see logs above"
return 1
fi
# Check dispatch mode to determine if admin verification is needed
local dispatch_mode
dispatch_mode=$(get_dispatch_mode "$toml_file")
if [ "$dispatch_mode" = "direct" ]; then
log "Action ${action_id}: tier=${VAULT_TIER:-unknown}, dispatch_mode=${dispatch_mode} — skipping admin merge verification (direct commit)"
else
# Verify admin merge for PR-based actions
log "Action ${action_id}: tier=${VAULT_TIER:-unknown}, dispatch_mode=${dispatch_mode} — verifying admin merge"
if ! verify_admin_merged "$toml_file"; then
log "ERROR: Admin merge verification failed for ${action_id}"
write_result "$action_id" 1 "Admin merge verification failed: see logs above"
return 1
fi
log "Action ${action_id}: admin merge verified"
fi
# Extract secrets from validated action
local secrets_array
secrets_array="${VAULT_ACTION_SECRETS:-}"
# Build docker run command (self-contained, no compose context needed).
# The edge container has the Docker socket but not the host's compose project,
# so docker compose run would fail with exit 125. docker run is self-contained:
# the dispatcher knows the image, network, env vars, and entrypoint.
local -a cmd=(docker run --rm local -a cmd=(docker run --rm
--name "vault-runner-${action_id}" --name "vault-runner-${action_id}"
--network host --network host
@ -466,30 +417,26 @@ launch_runner() {
cmd+=(-v "${runtime_home}/.claude.json:/home/agent/.claude.json:ro") cmd+=(-v "${runtime_home}/.claude.json:/home/agent/.claude.json:ro")
fi fi
# Add environment variables for secrets (if any declared) # Add environment variables for secrets (resolved via load_secret)
# Secrets are decrypted per-key from secrets/<NAME>.enc (#777) if [ -n "$secrets_csv" ]; then
if [ -n "$secrets_array" ]; then local secret
for secret in $secrets_array; do for secret in $(echo "$secrets_csv" | tr ',' ' '); do
secret=$(echo "$secret" | xargs) secret=$(echo "$secret" | xargs)
if [ -n "$secret" ]; then [ -n "$secret" ] || continue
local secret_val local secret_val
secret_val=$(decrypt_secret "$secret") || { secret_val=$(load_secret "$secret") || true
log "ERROR: Secret '${secret}' not found in secrets/*.enc for action ${action_id}" if [ -z "$secret_val" ]; then
write_result "$action_id" 1 "Secret not found: ${secret} (expected secrets/${secret}.enc)" log "ERROR: Secret '${secret}' could not be resolved for action ${action_id}"
return 1 return 1
}
cmd+=(-e "${secret}=${secret_val}")
fi fi
cmd+=(-e "${secret}=${secret_val}")
done done
else
log "Action ${action_id} has no secrets declared — runner will execute without extra env vars"
fi fi
# Add volume mounts for file-based credentials (if any declared) # Add volume mounts for file-based credentials
local mounts_array if [ -n "$mounts_csv" ]; then
mounts_array="${VAULT_ACTION_MOUNTS:-}" local mount_alias
if [ -n "$mounts_array" ]; then for mount_alias in $(echo "$mounts_csv" | tr ',' ' '); do
for mount_alias in $mounts_array; do
mount_alias=$(echo "$mount_alias" | xargs) mount_alias=$(echo "$mount_alias" | xargs)
[ -n "$mount_alias" ] || continue [ -n "$mount_alias" ] || continue
case "$mount_alias" in case "$mount_alias" in
@ -504,7 +451,6 @@ launch_runner() {
;; ;;
*) *)
log "ERROR: Unknown mount alias '${mount_alias}' for action ${action_id}" log "ERROR: Unknown mount alias '${mount_alias}' for action ${action_id}"
write_result "$action_id" 1 "Unknown mount alias: ${mount_alias}"
return 1 return 1
;; ;;
esac esac
@ -517,7 +463,7 @@ launch_runner() {
# Image and entrypoint arguments: runner entrypoint + action-id # Image and entrypoint arguments: runner entrypoint + action-id
cmd+=(disinto/agents:latest /home/agent/disinto/docker/runner/entrypoint-runner.sh "$action_id") cmd+=(disinto/agents:latest /home/agent/disinto/docker/runner/entrypoint-runner.sh "$action_id")
log "Running: docker run --rm vault-runner-${action_id} (secrets: ${secrets_array:-none}, mounts: ${mounts_array:-none})" log "Running: docker run --rm vault-runner-${action_id} (secrets: ${secrets_csv:-none}, mounts: ${mounts_csv:-none})"
# Create temp file for logs # Create temp file for logs
local log_file local log_file
@ -525,7 +471,6 @@ launch_runner() {
trap 'rm -f "$log_file"' RETURN trap 'rm -f "$log_file"' RETURN
# Execute with array expansion (safe from shell injection) # Execute with array expansion (safe from shell injection)
# Capture stdout and stderr to log file
"${cmd[@]}" > "$log_file" 2>&1 "${cmd[@]}" > "$log_file" 2>&1
local exit_code=$? local exit_code=$?
@ -545,6 +490,137 @@ launch_runner() {
return $exit_code return $exit_code
} }
# _launch_runner_nomad ACTION_ID SECRETS_CSV MOUNTS_CSV
#
# Nomad backend stub — will be implemented in migration Step 5.
_launch_runner_nomad() {
echo "nomad backend not yet implemented" >&2
return 1
}
# Launch runner for the given action (backend-agnostic orchestrator)
# Usage: launch_runner <toml_file>
launch_runner() {
local toml_file="$1"
local action_id
action_id=$(basename "$toml_file" .toml)
log "Launching runner for action: ${action_id}"
# Validate TOML
if ! validate_action "$toml_file"; then
log "ERROR: Action validation failed for ${action_id}"
write_result "$action_id" 1 "Validation failed: see logs above"
return 1
fi
# Check dispatch mode to determine if admin verification is needed
local dispatch_mode
dispatch_mode=$(get_dispatch_mode "$toml_file")
if [ "$dispatch_mode" = "direct" ]; then
log "Action ${action_id}: tier=${VAULT_TIER:-unknown}, dispatch_mode=${dispatch_mode} — skipping admin merge verification (direct commit)"
else
# Verify admin merge for PR-based actions
log "Action ${action_id}: tier=${VAULT_TIER:-unknown}, dispatch_mode=${dispatch_mode} — verifying admin merge"
if ! verify_admin_merged "$toml_file"; then
log "ERROR: Admin merge verification failed for ${action_id}"
write_result "$action_id" 1 "Admin merge verification failed: see logs above"
return 1
fi
log "Action ${action_id}: admin merge verified"
fi
# Build CSV lists from validated action metadata
local secrets_csv=""
if [ -n "${VAULT_ACTION_SECRETS:-}" ]; then
# Convert space-separated to comma-separated
secrets_csv=$(echo "${VAULT_ACTION_SECRETS}" | xargs | tr ' ' ',')
fi
local mounts_csv=""
if [ -n "${VAULT_ACTION_MOUNTS:-}" ]; then
mounts_csv=$(echo "${VAULT_ACTION_MOUNTS}" | xargs | tr ' ' ',')
fi
# Delegate to the selected backend
"_launch_runner_${DISPATCHER_BACKEND}" "$action_id" "$secrets_csv" "$mounts_csv"
}
# -----------------------------------------------------------------------------
# Pluggable sidecar launcher (reproduce / triage / verify)
# -----------------------------------------------------------------------------
# _dispatch_sidecar_docker CONTAINER_NAME ISSUE_NUM PROJECT_TOML IMAGE [FORMULA]
#
# Launches a sidecar container via docker run (background, pid-tracked).
# Prints the background PID to stdout.
_dispatch_sidecar_docker() {
local container_name="$1"
local issue_number="$2"
local project_toml="$3"
local image="$4"
local formula="${5:-}"
local -a cmd=(docker run --rm
--name "${container_name}"
--network host
--security-opt apparmor=unconfined
-v /var/run/docker.sock:/var/run/docker.sock
-v agent-data:/home/agent/data
-v project-repos:/home/agent/repos
-e "FORGE_URL=${FORGE_URL}"
-e "FORGE_TOKEN=${FORGE_TOKEN}"
-e "FORGE_REPO=${FORGE_REPO}"
-e "PRIMARY_BRANCH=${PRIMARY_BRANCH:-main}"
-e DISINTO_CONTAINER=1
)
# Set formula if provided
if [ -n "$formula" ]; then
cmd+=(-e "DISINTO_FORMULA=${formula}")
fi
# Pass through ANTHROPIC_API_KEY if set
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
cmd+=(-e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}")
fi
# Mount shared Claude config dir and ~/.ssh from the runtime user's home
local runtime_home="${HOME:-/home/debian}"
if [ -d "${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}" ]; then
cmd+=(-v "${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}")
cmd+=(-e "CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR:-/var/lib/disinto/claude-shared/config}")
fi
if [ -f "${runtime_home}/.claude.json" ]; then
cmd+=(-v "${runtime_home}/.claude.json:/home/agent/.claude.json:ro")
fi
if [ -d "${runtime_home}/.ssh" ]; then
cmd+=(-v "${runtime_home}/.ssh:/home/agent/.ssh:ro")
fi
if [ -f /usr/local/bin/claude ]; then
cmd+=(-v /usr/local/bin/claude:/usr/local/bin/claude:ro)
fi
# Mount the project TOML into the container at a stable path
local container_toml="/home/agent/project.toml"
cmd+=(-v "${project_toml}:${container_toml}:ro")
cmd+=("${image}" "$container_toml" "$issue_number")
# Launch in background
"${cmd[@]}" &
echo $!
}
# _dispatch_sidecar_nomad CONTAINER_NAME ISSUE_NUM PROJECT_TOML IMAGE [FORMULA]
#
# Nomad sidecar backend stub — will be implemented in migration Step 5.
_dispatch_sidecar_nomad() {
echo "nomad backend not yet implemented" >&2
return 1
}
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Reproduce dispatch — launch sidecar for bug-report issues # Reproduce dispatch — launch sidecar for bug-report issues
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -623,52 +699,13 @@ dispatch_reproduce() {
log "Dispatching reproduce-agent for issue #${issue_number} (project: ${project_toml})" log "Dispatching reproduce-agent for issue #${issue_number} (project: ${project_toml})"
# Build docker run command using array (safe from injection) local bg_pid
local -a cmd=(docker run --rm bg_pid=$("_dispatch_sidecar_${DISPATCHER_BACKEND}" \
--name "disinto-reproduce-${issue_number}" "disinto-reproduce-${issue_number}" \
--network host "$issue_number" \
--security-opt apparmor=unconfined "$project_toml" \
-v /var/run/docker.sock:/var/run/docker.sock "disinto-reproduce:latest")
-v agent-data:/home/agent/data
-v project-repos:/home/agent/repos
-e "FORGE_URL=${FORGE_URL}"
-e "FORGE_TOKEN=${FORGE_TOKEN}"
-e "FORGE_REPO=${FORGE_REPO}"
-e "PRIMARY_BRANCH=${PRIMARY_BRANCH:-main}"
-e DISINTO_CONTAINER=1
)
# Pass through ANTHROPIC_API_KEY if set
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
cmd+=(-e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}")
fi
# Mount shared Claude config dir and ~/.ssh from the runtime user's home if available
local runtime_home="${HOME:-/home/debian}"
if [ -d "${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}" ]; then
cmd+=(-v "${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}")
cmd+=(-e "CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR:-/var/lib/disinto/claude-shared/config}")
fi
if [ -f "${runtime_home}/.claude.json" ]; then
cmd+=(-v "${runtime_home}/.claude.json:/home/agent/.claude.json:ro")
fi
if [ -d "${runtime_home}/.ssh" ]; then
cmd+=(-v "${runtime_home}/.ssh:/home/agent/.ssh:ro")
fi
# Mount claude CLI binary if present on host
if [ -f /usr/local/bin/claude ]; then
cmd+=(-v /usr/local/bin/claude:/usr/local/bin/claude:ro)
fi
# Mount the project TOML into the container at a stable path
local container_toml="/home/agent/project.toml"
cmd+=(-v "${project_toml}:${container_toml}:ro")
cmd+=(disinto-reproduce:latest "$container_toml" "$issue_number")
# Launch in background; write pid-file so we don't double-launch
"${cmd[@]}" &
local bg_pid=$!
echo "$bg_pid" > "$(_reproduce_lockfile "$issue_number")" echo "$bg_pid" > "$(_reproduce_lockfile "$issue_number")"
log "Reproduce container launched (pid ${bg_pid}) for issue #${issue_number}" log "Reproduce container launched (pid ${bg_pid}) for issue #${issue_number}"
} }
@ -748,53 +785,14 @@ dispatch_triage() {
log "Dispatching triage-agent for issue #${issue_number} (project: ${project_toml})" log "Dispatching triage-agent for issue #${issue_number} (project: ${project_toml})"
# Build docker run command using array (safe from injection) local bg_pid
local -a cmd=(docker run --rm bg_pid=$("_dispatch_sidecar_${DISPATCHER_BACKEND}" \
--name "disinto-triage-${issue_number}" "disinto-triage-${issue_number}" \
--network host "$issue_number" \
--security-opt apparmor=unconfined "$project_toml" \
-v /var/run/docker.sock:/var/run/docker.sock "disinto-reproduce:latest" \
-v agent-data:/home/agent/data "triage")
-v project-repos:/home/agent/repos
-e "FORGE_URL=${FORGE_URL}"
-e "FORGE_TOKEN=${FORGE_TOKEN}"
-e "FORGE_REPO=${FORGE_REPO}"
-e "PRIMARY_BRANCH=${PRIMARY_BRANCH:-main}"
-e DISINTO_CONTAINER=1
-e DISINTO_FORMULA=triage
)
# Pass through ANTHROPIC_API_KEY if set
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
cmd+=(-e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}")
fi
# Mount shared Claude config dir and ~/.ssh from the runtime user's home if available
local runtime_home="${HOME:-/home/debian}"
if [ -d "${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}" ]; then
cmd+=(-v "${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}")
cmd+=(-e "CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR:-/var/lib/disinto/claude-shared/config}")
fi
if [ -f "${runtime_home}/.claude.json" ]; then
cmd+=(-v "${runtime_home}/.claude.json:/home/agent/.claude.json:ro")
fi
if [ -d "${runtime_home}/.ssh" ]; then
cmd+=(-v "${runtime_home}/.ssh:/home/agent/.ssh:ro")
fi
# Mount claude CLI binary if present on host
if [ -f /usr/local/bin/claude ]; then
cmd+=(-v /usr/local/bin/claude:/usr/local/bin/claude:ro)
fi
# Mount the project TOML into the container at a stable path
local container_toml="/home/agent/project.toml"
cmd+=(-v "${project_toml}:${container_toml}:ro")
cmd+=(disinto-reproduce:latest "$container_toml" "$issue_number")
# Launch in background; write pid-file so we don't double-launch
"${cmd[@]}" &
local bg_pid=$!
echo "$bg_pid" > "$(_triage_lockfile "$issue_number")" echo "$bg_pid" > "$(_triage_lockfile "$issue_number")"
log "Triage container launched (pid ${bg_pid}) for issue #${issue_number}" log "Triage container launched (pid ${bg_pid}) for issue #${issue_number}"
} }
@ -950,53 +948,14 @@ dispatch_verify() {
log "Dispatching verification-agent for issue #${issue_number} (project: ${project_toml})" log "Dispatching verification-agent for issue #${issue_number} (project: ${project_toml})"
# Build docker run command using array (safe from injection) local bg_pid
local -a cmd=(docker run --rm bg_pid=$("_dispatch_sidecar_${DISPATCHER_BACKEND}" \
--name "disinto-verify-${issue_number}" "disinto-verify-${issue_number}" \
--network host "$issue_number" \
--security-opt apparmor=unconfined "$project_toml" \
-v /var/run/docker.sock:/var/run/docker.sock "disinto-reproduce:latest" \
-v agent-data:/home/agent/data "verify")
-v project-repos:/home/agent/repos
-e "FORGE_URL=${FORGE_URL}"
-e "FORGE_TOKEN=${FORGE_TOKEN}"
-e "FORGE_REPO=${FORGE_REPO}"
-e "PRIMARY_BRANCH=${PRIMARY_BRANCH:-main}"
-e DISINTO_CONTAINER=1
-e DISINTO_FORMULA=verify
)
# Pass through ANTHROPIC_API_KEY if set
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
cmd+=(-e "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}")
fi
# Mount shared Claude config dir and ~/.ssh from the runtime user's home if available
local runtime_home="${HOME:-/home/debian}"
if [ -d "${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}" ]; then
cmd+=(-v "${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}:${CLAUDE_SHARED_DIR:-/var/lib/disinto/claude-shared}")
cmd+=(-e "CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR:-/var/lib/disinto/claude-shared/config}")
fi
if [ -f "${runtime_home}/.claude.json" ]; then
cmd+=(-v "${runtime_home}/.claude.json:/home/agent/.claude.json:ro")
fi
if [ -d "${runtime_home}/.ssh" ]; then
cmd+=(-v "${runtime_home}/.ssh:/home/agent/.ssh:ro")
fi
# Mount claude CLI binary if present on host
if [ -f /usr/local/bin/claude ]; then
cmd+=(-v /usr/local/bin/claude:/usr/local/bin/claude:ro)
fi
# Mount the project TOML into the container at a stable path
local container_toml="/home/agent/project.toml"
cmd+=(-v "${project_toml}:${container_toml}:ro")
cmd+=(disinto-reproduce:latest "$container_toml" "$issue_number")
# Launch in background; write pid-file so we don't double-launch
"${cmd[@]}" &
local bg_pid=$!
echo "$bg_pid" > "$(_verify_lockfile "$issue_number")" echo "$bg_pid" > "$(_verify_lockfile "$issue_number")"
log "Verification container launched (pid ${bg_pid}) for issue #${issue_number}" log "Verification container launched (pid ${bg_pid}) for issue #${issue_number}"
} }
@ -1018,10 +977,25 @@ ensure_ops_repo() {
# Main dispatcher loop # Main dispatcher loop
main() { main() {
log "Starting dispatcher..." log "Starting dispatcher (backend=${DISPATCHER_BACKEND})..."
log "Polling ops repo: ${VAULT_ACTIONS_DIR}" log "Polling ops repo: ${VAULT_ACTIONS_DIR}"
log "Admin users: ${ADMIN_USERS}" log "Admin users: ${ADMIN_USERS}"
# Validate backend selection at startup
case "$DISPATCHER_BACKEND" in
docker) ;;
nomad)
log "ERROR: nomad backend not yet implemented"
echo "nomad backend not yet implemented" >&2
exit 1
;;
*)
log "ERROR: unknown DISPATCHER_BACKEND=${DISPATCHER_BACKEND}"
echo "unknown DISPATCHER_BACKEND=${DISPATCHER_BACKEND} (expected: docker, nomad)" >&2
exit 1
;;
esac
while true; do while true; do
# Refresh ops repo at the start of each poll cycle # Refresh ops repo at the start of each poll cycle
ensure_ops_repo ensure_ops_repo