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

75 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) + UNKNOWN** → `deny` 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.