diff --git a/.env.example b/.env.example index 7ca5ba6..762acd3 100644 --- a/.env.example +++ b/.env.example @@ -49,7 +49,7 @@ WOODPECKER_DB_NAME=woodpecker # [CONFIG] Postgres database name # ── Vault-only secrets (DO NOT put these in .env) ──────────────────────── # These tokens grant access to external systems (GitHub, ClawHub, deploy targets). -# They live ONLY in .env.vault.enc and are injected into the ephemeral runner +# They live ONLY in .env.vault.enc and are injected into the ephemeral vault-runner # container at fire time (#745). lib/env.sh explicitly unsets them so agents # can never hold them directly — all external actions go through vault dispatch. # @@ -58,7 +58,7 @@ WOODPECKER_DB_NAME=woodpecker # [CONFIG] Postgres database name # (deploy keys) — SSH keys for deployment targets # # To manage vault secrets: disinto secrets edit-vault -# See also: vault/run-action.sh, vault/vault-fire.sh +# See also: vault/vault-run-action.sh, vault/vault-fire.sh # ── Project-specific secrets ────────────────────────────────────────────── # Store all project secrets here so formulas reference env vars, never hardcode. diff --git a/AGENTS.md b/AGENTS.md index 04a0ac1..ffc5561 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -164,8 +164,8 @@ Humans write these. Agents read and enforce them. | AD-002 | Single-threaded pipeline per project. | One dev issue at a time. No new work while a PR awaits CI or review. Prevents merge conflicts and keeps context clear. | | AD-003 | The runtime creates and destroys, the formula preserves. | Runtime manages worktrees/sessions/temp. Formulas commit knowledge to git before signaling done. | | AD-004 | Event-driven > polling > fixed delays. | Never `waitForTimeout` or hardcoded sleep. Use phase files, webhooks, or poll loops with backoff. | -| AD-005 | Secrets via env var indirection, never in issue bodies. | Issue bodies become code. Agent secrets go in `.env.enc`, vault secrets in `.env.vault.enc` (both SOPS-encrypted). Referenced as `$VAR_NAME`. Runner gets only vault secrets; agents get only agent secrets. | -| AD-006 | External actions go through vault dispatch, never direct. | Agents build addressables; only the vault exercises them (publishes, deploys, posts). Tokens for external systems (`GITHUB_TOKEN`, `CLAWHUB_TOKEN`, deploy keys) live only in `.env.vault.enc` and are injected into the ephemeral runner container. `lib/env.sh` unsets them so agents never hold them. PRs with direct external actions without vault dispatch get REQUEST_CHANGES. | +| AD-005 | Secrets via env var indirection, never in issue bodies. | Issue bodies become code. Agent secrets go in `.env.enc`, vault secrets in `.env.vault.enc` (both SOPS-encrypted). Referenced as `$VAR_NAME`. Vault-runner gets only vault secrets; agents get only agent secrets. | +| AD-006 | External actions go through vault dispatch, never direct. | Agents build addressables; only the vault exercises them (publishes, deploys, posts). Tokens for external systems (`GITHUB_TOKEN`, `CLAWHUB_TOKEN`, deploy keys) live only in `.env.vault.enc` and are injected into the ephemeral vault-runner container. `lib/env.sh` unsets them so agents never hold them. PRs with direct external actions without vault dispatch get REQUEST_CHANGES. | **Who enforces what:** - **Gardener** checks open backlog issues against ADs during grooming; closes violations with a comment referencing the AD number. diff --git a/bin/disinto b/bin/disinto index 772f0fa..3151c19 100755 --- a/bin/disinto +++ b/bin/disinto @@ -10,7 +10,7 @@ # disinto shell Shell into the agent container # disinto status Show factory status # disinto secrets Manage encrypted secrets -# disinto run Run action in ephemeral runner container +# disinto vault-run Run action in ephemeral vault container # # Usage: # disinto init https://github.com/user/repo @@ -39,7 +39,7 @@ Usage: disinto shell Shell into the agent container disinto status Show factory status disinto secrets Manage encrypted secrets - disinto run Run action in ephemeral runner container + disinto vault-run Run action in ephemeral vault container Init options: --branch Primary branch (default: auto-detect) @@ -242,7 +242,7 @@ services: - .env # IMPORTANT: agents get .env only (forge tokens, CI tokens, config). # Vault-only secrets (GITHUB_TOKEN, CLAWHUB_TOKEN, deploy keys) live in - # .env.vault.enc and are NEVER injected here — only the runner + # .env.vault.enc and are NEVER injected here — only the vault-runner # container receives them at fire time (AD-006, #745). depends_on: - forgejo @@ -250,7 +250,7 @@ services: networks: - disinto-net - runner: + vault-runner: build: ./docker/agents profiles: ["vault"] security_opt: @@ -263,8 +263,8 @@ services: FORGE_URL: http://forgejo:3000 DISINTO_CONTAINER: "1" PROJECT_REPO_ROOT: /home/agent/repos/\${PROJECT_NAME:-project} - # env_file set at runtime by: disinto run --env-file - entrypoint: ["bash", "/home/agent/disinto/vault/run-action.sh"] + # env_file set at runtime by: disinto vault-run --env-file + entrypoint: ["bash", "/home/agent/disinto/vault/vault-run-action.sh"] networks: - disinto-net @@ -466,8 +466,8 @@ generate_deploy_pipelines() { if [ ! -f "${wp_dir}/staging.yml" ]; then cat > "${wp_dir}/staging.yml" <<'STAGINGEOF' # .woodpecker/staging.yml — Staging deployment pipeline -# Triggered by runner via Woodpecker promote API. -# Human approves promotion in vault → runner calls promote → this runs. +# Triggered by vault-runner via Woodpecker promote API. +# Human approves promotion in vault → vault-runner calls promote → this runs. when: event: deployment @@ -498,8 +498,8 @@ STAGINGEOF if [ ! -f "${wp_dir}/production.yml" ]; then cat > "${wp_dir}/production.yml" <<'PRODUCTIONEOF' # .woodpecker/production.yml — Production deployment pipeline -# Triggered by runner via Woodpecker promote API. -# Human approves promotion in vault → runner calls promote → this runs. +# Triggered by vault-runner via Woodpecker promote API. +# Human approves promotion in vault → vault-runner calls promote → this runs. when: event: deployment @@ -2184,10 +2184,10 @@ EOF esac } -# ── run command ─────────────────────────────────────────────────────────────── +# ── vault-run command ───────────────────────────────────────────────────────── -disinto_run() { - local action_id="${1:?Usage: disinto run }" +disinto_vault_run() { + local action_id="${1:?Usage: disinto vault-run }" local compose_file="${FACTORY_ROOT}/docker-compose.yml" local vault_enc="${FACTORY_ROOT}/.env.vault.enc" @@ -2221,20 +2221,20 @@ disinto_run() { echo "Vault secrets decrypted to tmpfile" - # Run action in ephemeral runner container + # Run action in ephemeral vault-runner container local rc=0 docker compose -f "$compose_file" \ run --rm --env-file "$tmp_env" \ - runner "$action_id" || rc=$? + vault-runner "$action_id" || rc=$? # Clean up — secrets gone rm -f "$tmp_env" - echo "Run tmpfile removed" + echo "Vault tmpfile removed" if [ "$rc" -eq 0 ]; then - echo "Run action ${action_id} completed successfully" + echo "Vault action ${action_id} completed successfully" else - echo "Run action ${action_id} failed (exit ${rc})" >&2 + echo "Vault action ${action_id} failed (exit ${rc})" >&2 fi return "$rc" } @@ -2314,7 +2314,7 @@ case "${1:-}" in shell) shift; disinto_shell ;; status) shift; disinto_status "$@" ;; secrets) shift; disinto_secrets "$@" ;; - run) shift; disinto_run "$@" ;; + vault-run) shift; disinto_vault_run "$@" ;; -h|--help) usage ;; *) usage ;; esac diff --git a/formulas/review-pr.toml b/formulas/review-pr.toml index 2c02e17..b74f1e3 100644 --- a/formulas/review-pr.toml +++ b/formulas/review-pr.toml @@ -112,7 +112,7 @@ near-duplicate exists, REQUEST_CHANGES and reference the existing item. Agents must NEVER execute external actions directly. Any action that touches an external system (publish, deploy, post, push to external registry, API calls to third-party services) MUST go through vault dispatch — i.e., the -agent files a vault item (`$OPS_REPO_ROOT/vault/pending/*.json`) and the runner +agent files a vault item (`$OPS_REPO_ROOT/vault/pending/*.json`) and the vault-runner container executes it with injected secrets. Scan the diff for these patterns: @@ -129,7 +129,7 @@ Scan the diff for these patterns: If ANY of these patterns appear in agent code (scripts in `dev/`, `action/`, `planner/`, `gardener/`, `supervisor/`, `predictor/`, `review/`, `formulas/`, `lib/`) WITHOUT routing through vault dispatch (`$OPS_REPO_ROOT/vault/pending/`, `vault-fire.sh`, -`run-action.sh`), **REQUEST_CHANGES**. +`vault-run-action.sh`), **REQUEST_CHANGES**. Explain that external actions must use vault dispatch per AD-006. The agent should file a vault item instead of executing directly. @@ -137,7 +137,7 @@ should file a vault item instead of executing directly. **Exceptions** (do NOT flag these): - Code inside `vault/` — the vault system itself is allowed to handle secrets - References in comments or documentation explaining the architecture -- `bin/disinto` setup commands that manage `.env.vault.enc` and the `run` subcommand +- `bin/disinto` setup commands that manage `.env.vault.enc` - Local operations (git push to forge, forge API calls with `FORGE_TOKEN`) ## 6. Re-review (if previous review is provided) diff --git a/lib/AGENTS.md b/lib/AGENTS.md index 7bfc736..520440b 100644 --- a/lib/AGENTS.md +++ b/lib/AGENTS.md @@ -6,7 +6,7 @@ sourced as needed. | File | What it provides | Sourced by | |---|---|---| -| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`FORGE_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `forge_api()`, `forge_api_all()` (accepts optional second TOKEN parameter, defaults to `$FORGE_TOKEN`), `woodpecker_api()`, `wpdb()`, `memory_guard()` (skips agent if RAM < threshold). Auto-loads project TOML if `PROJECT_TOML` is set. Exports per-agent tokens (`FORGE_PLANNER_TOKEN`, `FORGE_GARDENER_TOKEN`, `FORGE_VAULT_TOKEN`, `FORGE_SUPERVISOR_TOKEN`, `FORGE_PREDICTOR_TOKEN`, `FORGE_ACTION_TOKEN`) — each falls back to `$FORGE_TOKEN` if not set. **Vault-only token guard (AD-006)**: `unset GITHUB_TOKEN CLAWHUB_TOKEN` so agents never hold external-action tokens — only the runner container receives them. **Container note**: when `DISINTO_CONTAINER=1`, `.env` is NOT re-sourced — compose already injects env vars (including `FORGE_URL=http://forgejo:3000`) and re-sourcing would clobber them. | Every agent | +| `lib/env.sh` | Loads `.env`, sets `FACTORY_ROOT`, exports project config (`FORGE_REPO`, `PROJECT_NAME`, etc.), defines `log()`, `forge_api()`, `forge_api_all()` (accepts optional second TOKEN parameter, defaults to `$FORGE_TOKEN`), `woodpecker_api()`, `wpdb()`, `memory_guard()` (skips agent if RAM < threshold). Auto-loads project TOML if `PROJECT_TOML` is set. Exports per-agent tokens (`FORGE_PLANNER_TOKEN`, `FORGE_GARDENER_TOKEN`, `FORGE_VAULT_TOKEN`, `FORGE_SUPERVISOR_TOKEN`, `FORGE_PREDICTOR_TOKEN`, `FORGE_ACTION_TOKEN`) — each falls back to `$FORGE_TOKEN` if not set. **Vault-only token guard (AD-006)**: `unset GITHUB_TOKEN CLAWHUB_TOKEN` so agents never hold external-action tokens — only the vault-runner container receives them. **Container note**: when `DISINTO_CONTAINER=1`, `.env` is NOT re-sourced — compose already injects env vars (including `FORGE_URL=http://forgejo:3000`) and re-sourcing would clobber them. | Every agent | | `lib/ci-helpers.sh` | `ci_passed()` — returns 0 if CI state is "success" (or no CI configured). `ci_required_for_pr()` — returns 0 if PR has code files (CI required), 1 if non-code only (CI not required). `is_infra_step()` — returns 0 if a single CI step failure matches infra heuristics (clone/git exit 128, any exit 137, log timeout patterns). `classify_pipeline_failure()` — returns "infra \" if any failed Woodpecker step matches infra heuristics via `is_infra_step()`, else "code". `ensure_priority_label()` — looks up (or creates) the `priority` label and returns its ID; caches in `_PRIORITY_LABEL_ID`. `ci_commit_status ` — queries Woodpecker directly for CI state, falls back to forge commit status API. `ci_pipeline_number ` — returns the Woodpecker pipeline number for a commit, falls back to parsing forge status `target_url`. `ci_promote ` — promotes a pipeline to a named Woodpecker environment (vault-gated deployment: vault approves, vault-fire calls this). | dev-poll, review-poll, review-pr, supervisor-poll | | `lib/ci-debug.sh` | CLI tool for Woodpecker CI: `list`, `status`, `logs`, `failures` subcommands. Not sourced — run directly. | Humans / dev-agent (tool access) | | `lib/load-project.sh` | Parses a `projects/*.toml` file into env vars (`PROJECT_NAME`, `FORGE_REPO`, `WOODPECKER_REPO_ID`, monitoring toggles, mirror config, etc.). | env.sh (when `PROJECT_TOML` is set), supervisor-poll (per-project iteration) | diff --git a/lib/env.sh b/lib/env.sh index 92eb676..d2af00e 100755 --- a/lib/env.sh +++ b/lib/env.sh @@ -112,7 +112,7 @@ export CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}" # Vault-only token guard (#745): external-action tokens (GITHUB_TOKEN, CLAWHUB_TOKEN) # must NEVER be available to agents. They live in .env.vault.enc and are injected -# only into the ephemeral runner container at fire time. Unset them here so +# only into the ephemeral vault-runner container at fire time. Unset them here so # even an accidental .env inclusion cannot leak them into agent sessions. unset GITHUB_TOKEN 2>/dev/null || true unset CLAWHUB_TOKEN 2>/dev/null || true diff --git a/vault/AGENTS.md b/vault/AGENTS.md index 879e645..6461064 100644 --- a/vault/AGENTS.md +++ b/vault/AGENTS.md @@ -28,7 +28,7 @@ needed — the human reviews and publishes directly. **Key files**: - `vault/vault-poll.sh` — Processes pending items: retry approved, auto-reject after 48h timeout, invoke vault-agent for JSON actions, notify human for procurement requests - `vault/vault-agent.sh` — Classifies and routes pending JSON actions via `claude -p`: auto-approve, auto-reject, or escalate to human -- `vault/vault-env.sh` — Shared env setup for vault sub-scripts: sources `lib/env.sh`, overrides `FORGE_TOKEN` with `FORGE_VAULT_TOKEN`, sets `VAULT_TOKEN` for runner container +- `vault/vault-env.sh` — Shared env setup for vault sub-scripts: sources `lib/env.sh`, overrides `FORGE_TOKEN` with `FORGE_VAULT_TOKEN`, sets `VAULT_TOKEN` for vault-runner container - `formulas/run-vault.toml` — Source-of-truth formula for the vault agent's classification and routing logic - `vault/vault-fire.sh` — Executes an approved action (JSON) in an **ephemeral Docker container** with vault-only secrets injected (GITHUB_TOKEN, CLAWHUB_TOKEN — never exposed to agents). For deployment actions, calls `lib/ci-helpers.sh:ci_promote()` to gate production promotes via Woodpecker environments. Writes `$OPS_REPO_ROOT/RESOURCES.md` entry for procurement MD approvals. - `vault/vault-reject.sh` — Marks a JSON action as rejected diff --git a/vault/vault-env.sh b/vault/vault-env.sh index 66b87d1..79e4176 100644 --- a/vault/vault-env.sh +++ b/vault/vault-env.sh @@ -7,6 +7,3 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/lib/env.sh" # Use vault-bot's own Forgejo identity FORGE_TOKEN="${FORGE_VAULT_TOKEN:-${FORGE_TOKEN}}" - -# Set entrypoint for runner container -export VAULT_RUNNER_ENTRYPOINT="run-action.sh" diff --git a/vault/vault-fire.sh b/vault/vault-fire.sh index 79c1d46..ad57022 100755 --- a/vault/vault-fire.sh +++ b/vault/vault-fire.sh @@ -3,8 +3,8 @@ # # Handles two pipelines: # A. Action gating (*.json): pending/ → approved/ → fired/ -# Execution delegated to ephemeral runner container via disinto run. -# The runner gets vault secrets (.env.vault.enc); this script does NOT. +# Execution delegated to ephemeral vault-runner container via disinto vault-run. +# The vault-runner gets vault secrets (.env.vault.enc); this script does NOT. # B. Procurement (*.md): approved/ → fired/ (writes RESOURCES.md entry) # # If item is in pending/, moves to approved/ first. @@ -100,7 +100,7 @@ if [ "$IS_PROCUREMENT" = true ]; then fi # ============================================================================= -# Pipeline B: Action gating — delegate to ephemeral runner container +# Pipeline B: Action gating — delegate to ephemeral vault-runner container # ============================================================================= ACTION_TYPE=$(jq -r '.type // ""' < "$ACTION_FILE") ACTION_SOURCE=$(jq -r '.source // ""' < "$ACTION_FILE") @@ -110,19 +110,19 @@ if [ -z "$ACTION_TYPE" ]; then exit 1 fi -log "$ACTION_ID: firing type=$ACTION_TYPE source=$ACTION_SOURCE via runner" +log "$ACTION_ID: firing type=$ACTION_TYPE source=$ACTION_SOURCE via vault-runner" FIRE_EXIT=0 -# Delegate execution to the ephemeral runner container. -# The runner gets vault secrets (.env.vault.enc) injected at runtime; +# Delegate execution to the ephemeral vault-runner container. +# The vault-runner gets vault secrets (.env.vault.enc) injected at runtime; # this host process never sees those secrets. if [ -f "${FACTORY_ROOT}/.env.vault.enc" ] && [ -f "${FACTORY_ROOT}/docker-compose.yml" ]; then - bash "${FACTORY_ROOT}/bin/disinto" run "$ACTION_ID" >> "$LOGFILE" 2>&1 || FIRE_EXIT=$? + bash "${FACTORY_ROOT}/bin/disinto" vault-run "$ACTION_ID" >> "$LOGFILE" 2>&1 || FIRE_EXIT=$? else # Fallback for bare-metal or pre-migration setups: run action handler directly log "$ACTION_ID: no .env.vault.enc or docker-compose.yml — running action directly" - bash "${SCRIPT_DIR}/run-action.sh" "$ACTION_ID" >> "$LOGFILE" 2>&1 || FIRE_EXIT=$? + bash "${SCRIPT_DIR}/vault-run-action.sh" "$ACTION_ID" >> "$LOGFILE" 2>&1 || FIRE_EXIT=$? fi # ============================================================================= diff --git a/vault/run-action.sh b/vault/vault-run-action.sh similarity index 89% rename from vault/run-action.sh rename to vault/vault-run-action.sh index b051511..707f3db 100755 --- a/vault/run-action.sh +++ b/vault/vault-run-action.sh @@ -1,25 +1,25 @@ #!/usr/bin/env bash -# run-action.sh — Execute an action inside the ephemeral runner container +# vault-run-action.sh — Execute an action inside the ephemeral vault-runner container # -# This script is the entrypoint for the runner container. It runs with +# This script is the entrypoint for the vault-runner container. It runs with # vault secrets injected as environment variables (GITHUB_TOKEN, CLAWHUB_TOKEN, # deploy keys, etc.) and dispatches to the appropriate action handler. # -# The runner container is ephemeral: it starts, runs the action, and is +# The vault-runner container is ephemeral: it starts, runs the action, and is # destroyed. Secrets exist only in container memory, never on disk. # -# Usage: run-action.sh +# Usage: vault-run-action.sh set -euo pipefail VAULT_SCRIPT_DIR="${DISINTO_VAULT_DIR:-/home/agent/disinto/vault}" OPS_VAULT_DIR="${DISINTO_OPS_VAULT_DIR:-${VAULT_SCRIPT_DIR}}" LOGFILE="${VAULT_SCRIPT_DIR}/vault.log" -ACTION_ID="${1:?Usage: run-action.sh }" +ACTION_ID="${1:?Usage: vault-run-action.sh }" log() { - printf '[%s] runner: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE" 2>/dev/null || \ - printf '[%s] runner: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >&2 + printf '[%s] vault-runner: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >> "$LOGFILE" 2>/dev/null || \ + printf '[%s] vault-runner: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >&2 } # Find action file in approved/