Files
claude-projects/agent-builder/constrained_autonomy_design_decisions.md
T
Backtalk6858 c101167eb5 docs: constrained-autonomy design approved + fix stale tier-2 guard
- constrained_autonomy_design_decisions.md: D1-D8 locked (hybrid hook
  classifier + Agent-Sudo daemon executor, structural classification with
  unknown->sandbox, fail-closed, Hermes recovery ladder) + EXECUTION ORDER
  with CA-P1a inserted as zero-dependency item 0
- agent_prompts.md shared block: replace "TIER-2 IS BROKEN" with the
  corrected root cause (old primary bridge's NTFY endpoint was unreachable;
  tier-2 works when the bridge reaches ntfy + user is attended)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:02:08 -05:00

9.9 KiB
Raw Blame History

Constrained Autonomy for Claude Code — Design Decisions

Status: DESIGN LOCKED (grill-me 2026-07-14). Not yet built. Build = phased background agents (CA-P0…P5), mirroring the Agent-Sudo build model. Do NOT implement ad hoc — follow this record.

Driving insight (user): a permission prompt the human rubber-stamps — especially a truncated command they can't read or don't understand — adds no safety. It is the same theater as the NTFY approval gate we removed from Agent-Sudo. Kill the theater; replace it with an ENFORCED code constraint layer. Governing memory: feedback_autonomous_security_constrain_not_gate (constrain capability, don't gate access), feedback_evolution_by_default, feedback_always_include_training_loop, feedback_secrets_via_proxy_only.

The core problem

Claude Code's Bash tool runs as administrator (docker group + sudo). Today, consequential commands stop for a human permission prompt. --dangerously-skip-permissions removes that gate but adds NO constraint → strictly worse. We want: remove the theater prompt, but move the real gate into deterministic enforced code that reuses the Agent-Sudo constraint+recovery stack.

D1 — Enforcement architecture: HYBRID (hook classifies+routes; daemon executes-with-constraint)

  • The hook = security-enforcement.py (existing PreToolUse Bash hook, pure-regex, <50ms, no-I/O) extended into the decision point. It runs regardless of permission mode and is code, not a rubber stamp. It emits a permissionDecision:
    • tier-0/1 (read/reversible)allow LOCALLY (no prompt, no daemon round-trip → kills the theater, stays <50ms).
    • tier-2/3 (mutating/destructive) + UNKNOWNdeny the raw command, message routes it to the Agent-Sudo daemon /exec (which already does sandbox-test-first + rollback + audit). This generalizes what the hook ALREADY does for sudo today (block → route to bridge) into "anything consequential → Agent-Sudo".
    • tier-4 (catastrophic)deny, no override.
  • The daemon = Agent-Sudo /exec (LIVE: server-01 8082, primary 8084). All heavy lifting (Incus sandbox mirror test, Timeshift/rollback, circuit-breaker, command_audit logging) stays where it's built and tested. We reuse it; we do not reimplement it in a 50ms regex hook.
  • Rejected: pure-hook (can't sandbox/rollback in 50ms), route-EVERYTHING-through-daemon (latency + hard dependency + wrong root context for harmless reads).

D2 — Classification: STRUCTURAL effect-classifier, unknown→sandbox, learned allowlist on top

  • SUDO.md only covers the sudo surface; ~90% of Claude Code commands are non-sudo (git/docker/python/curl/file ops). So the classifier judges a command by its effect class, not exact-match:
    • reads/inspection → tier-0/1 allow
    • workspace-local / reversible writes (git add/commit, edits under project dirs, scratch scripts) → tier-1 allow
    • blast-radius mutations (docker rm/restart/compose down, writes outside workspace, DB writes, net-config, mv/rm on real paths) → tier-2/3 → daemon sandbox
    • catastrophic verbs (rm -rf / patterns, mkfs, dd of=/dev/…, fork bombs, disk/partition ops) → tier-4 deny
    • sudo-prefixed → daemon's SUDO.md governs (unchanged)
    • genuinely unrecognizable structure → default to daemon sandbox (treat as tier-2) — unknown = "prove it in the mirror first," NEVER "ask the tired human." (User-confirmed.)
  • Learned allowlist layered ON TOP: commands that ran clean N times (from command_audit + human_verdict gold labels) get promoted to the local fast-path → system gets faster with use (feedback_evolution_by_default). Starts from structural analysis so day-one has zero rubber-stamp gaps.
  • Policy source of truth = a signed CLAUDE_CMDS.md tier-map file (the effect-class rules), analogous to SUDO.md.

D3 — Secrets: via secrets-proxy env_secrets (dormant until #128)

  • Any command that touches a secret runs via secrets-proxy /shell + env_secrets (vault://path#field / bitwarden://item) so values are sourced at runtime, never in stdout/history/argv. The hook's existing Category-2/3 blocks (secret exposure, hardcoded secrets) stay as the backstop that FORCES commands onto the proxy path.
  • Dependency: secrets-proxy = task #128 (pending/down). Secret-handling path is built but dormant until #128; interim fallback = feedback_secret_to_script_via_file. Decoupled from the classifier build.

D4 — Tamper-evidence: Vault transit signature (dormant until #145)

  • CLAUDE_CMDS.md gets the IDENTICAL mechanism as sudo_sign.py: Vault transit ed25519 key, sign file sha256, verify_gate on load via scoped AppRole, ..._VERIFY_ENFORCE flag. Tampered policy → refuse to honor.
  • Dependency: live signing blocked on #145 (OpenBao admin lockout; transit engine not enableable yet). SUDO.md itself runs unsigned (enforce=false) for this reason. CLAUDE_CMDS.md inherits the posture: built to verify, ships enforce=false, flips to true when #145 lands — one activation signs SUDO.md + proxy.md + CLAUDE_CMDS.md together. (proxy.md signature status UNVERIFIED — do not assert it is signed.)

D5 — Fail mode: FAIL-CLOSED

  • If the hook crashes or exceeds its 5s timeout → deny everything until healthy. A broken security control must not silently become no control. (User-confirmed.)
  • Mitigations: (a) hook stays dead-simple pure-regex (minimal crash surface); (b) an ESCAPE_HATCH env flag the USER sets to drop to prompt-mode if the hook ever bricks all Bash; (c) SessionStart self-test so a broken deploy is caught before it blocks work.

D6 — Recovery ladder (two phases)

  • Phase 1 (now, background-agent automation): fail-closed → user reachable to grant permission / co-diagnose → fix → resume autonomy.
  • Phase 2 (full autonomy): hook fail → ALL WORK PAUSES → Hermes (already monitoring) attempts diagnose+fix → success → resume; impossible → ALL WORK STOPS + high-priority NTFY ("catastrophic failure, all work halted, interactive session needed"). In that session Claude/Hermes still do the heavy lifting; user only grants permission.
  • Depends on Hermes (#138) for Phase 2.

D7 — Circuit-breaker + self-disarm (inherited from Agent-Sudo)

  • Same anomaly trip: N tier-3 failures in a window OR any tier-4 attempt → daemon self-disarms to read-only + NTFY. No new mechanism.

D8 — Training capture (inherited; zero new schema)

  • Every classify+outcome → command_audit (projects DB): assigned_tier, decision_type, matched_rule, evidence, exit_code, rollback_taken, verify_passed, human_verdict (gold label), training_signal. This is the local-model training export.

Phased build (background agents, like Agent-Sudo P0P5)

  • CA-P0 — write CLAUDE_CMDS.md tier-map + the structural classifier module (pure-regex effect-classes). No activation.
  • CA-P1 — wire the hook to emit permissionDecision allow/deny/ask; tier-0/1 local allow; tier-4 deny; tier-2/3+unknown → route to daemon /exec. Fail-closed + ESCAPE_HATCH + SessionStart self-test. Test in the mirror first.
  • CA-P2 — daemon side: accept routed Claude-Code commands, sandbox-test unknowns in Incus mirror, capture rollback, log command_audit. (Mostly exists — extend/verify.)
  • CA-P3 — secrets-proxy env_secrets path for secret-touching commands. GATED on #128.
  • CA-P4 — sign CLAUDE_CMDS.md (transit) + learned-allowlist promotion from command_audit. GATED on #145.
  • CA-P5 — Hermes recovery-ladder (Phase 2) integration. GATED on #138.

EXECUTION ORDER (user-approved 2026-07-14, revised)

User's stated order was: (1) finish Agent-Sudo P3 subsystems [command testing + recovery] + sign SUDO.md; (2) finish/test/deploy secrets-proxy + sign proxy.md; (3) design/test/deploy constrained autonomy + sign CLAUDE_CMDS.md. User's concern: "I'll have to be here to give permission for everything."

REVISED — insert item 0 first. The ordering paradox: item 3 is what frees the user, but sat last, behind the two most babysitting-heavy items. Item 3's fast-path has NO dependency on items 1/2 (only CA-P2 needs item 1's sandbox; only CA-P3 needs item 2's proxy).

  • CA-P1a — CONSERVATIVE FAST-PATH (do FIRST; zero dependencies). Hook classifies: a TIGHT, EXPLICITLY-ENUMERATED read-only set (git status/diff/log, ls, grep, find, docker ps/inspect, cat non-secret, scratch scripts) → allow locally, no prompt. Catastrophic verbs → deny (protection that does NOT exist today). Everything else falls through to the normal prompt exactly as today. Purely additive: net SAFER (adds tier-4 deny) AND less annoying. Removes ~8090% of build-time prompts since building is overwhelmingly reads. Do NOT start with the full structural classifier — that's where a mis-classification could auto-allow something real. Start tight, widen with command_audit evidence.
  • Then 1 → 2 → 3 in the user's order, each one widening the fast-path: item 1's sandbox unlocks CA-P2 routing (destructive minority stops prompting); item 2 unlocks CA-P3 secrets path; item 3 completes + signs.
  • Net: user is present only for the SHRINKING MINORITY of commands, immediately — instead of all of them until the very end.

#145 (Vault admin) — user reports RECOVERED but task not marked complete. Per feedback_verify_before_persist: VERIFY transit is actually enableable BEFORE marking done or signing. Next-session first action: verify → sign SUDO.md → close #145 → CA-P4's signing gate also falls.

Verified live state (2026-07-14)

  • Hook: /opt/appdata/docker/.claude/hooks/security-enforcement.py, registered PreToolUse matcher=Bash timeout=5 in ~/.claude/settings.json. Currently block-only (exit 0/2), no classifier, no permissions allowlist set.
  • Agent-Sudo daemon /health+/exec+/allowlist: server-01 8082 (LIVE, server_id=server-01), primary 8084 (server_id=primary; primary cutover swap still pending #146).
  • command_audit (projects DB): 15 cols as above, present.