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>
This commit is contained in:
Backtalk6858
2026-07-14 17:02:08 -05:00
parent 32131ade7e
commit c101167eb5
2 changed files with 79 additions and 1 deletions
+5 -1
View File
@@ -898,7 +898,11 @@ If you issue the same tool call or command twice with identical arguments, STOP
Agent-Sudo does not exist yet, so build agents that need root on server-01 use the **existing** `sudo-bridge-server01` (`http://192.168.1.90:8082`, Bearer `secret/data/sudo-bridge-server01#api_key`). Rules:
- `GET /allowlist` before any `POST /allowlist` (409 = already exists). New rules via `POST /allowlist` only (allowlist.json is root:root — cannot be edited directly).
- `sudo-bridge` uses `subprocess.run(shlex.split())`**NO shell**: no `&&`, `|`, `$()`, `>`. Each command is a separate `POST /exec`. File writes = `python3 -c "..."` with base64 (single argv).
- **⚠️ TIER-2 NTFY APPROVAL IS BROKEN** (behavior_changes id=3 — notifications never arrive, command times out at 900s). **Do NOT design a build step that blocks on a tier-2 approval.** If a required privileged command would be tier-2, STOP and record it under `actions_failed` as "needs manual authorization: <command>" so the reviewer can pre-add it as tier-1 or run it interactively. Prefer tier-1-eligible commands; batch anything needing genuine escalation into an explicit list for the user.
- **⚠️ TIER-2 NTFY APPROVAL — ROOT CAUSE CORRECTED 2026-07-14** (behavior_changes id=3, updated). Tier-2 is **NOT universally broken.** The NTFY server (`ntfy-k0oooo8cckwsok80gg8kck88`) is healthy, and **server-01's bridge reaches it fine — its tier-2 approvals DO arrive on the user's phone and were approved live during P4.** The real failure was the **OLD PRIMARY bridge configured with an unreachable NTFY endpoint** (Connection refused, topic `sudo-bridge-approvals`) — a dead send-path, not a dead NTFY. Rules now:
- **Tier-2 IS usable when (a) the target bridge can reach ntfy AND (b) the user is attended (interactive session / watching phone).** In that case, submit the tier-2 and WAIT for approval — do NOT pre-emptively batch to manual.
- **Batch to manual-auth ONLY when** the run is unattended/async, OR the target bridge's NTFY send-path is known-unreachable (currently: the old PRIMARY bridge). Then record under `actions_failed` as "needs manual authorization: <command>".
- If a tier-2 submit returns an NTFY send failure or parks/expires, that is the misconfigured-send-path case → stop waiting, record it under `actions_failed`, and continue. Never stall the whole run on it.
- Moot on primary once its Agent-Sudo daemon is live + the old bridge is decommissioned (P5).
### Mandatory wrap-up (every agent, on success OR failure, as the final message)
```
@@ -0,0 +1,74 @@
# 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.