From b4c9e149b0d07e486d510d8ce9adf1bee7e44141 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 24 May 2026 23:10:30 -0400 Subject: [PATCH 01/12] =?UTF-8?q?docs:=20add=20PRD=200012=20=E2=80=94=20st?= =?UTF-8?q?uck-agent=20recovery=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/prds/0012-stuck-agent-recovery-flow.md | 81 +++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/prds/0012-stuck-agent-recovery-flow.md diff --git a/docs/prds/0012-stuck-agent-recovery-flow.md b/docs/prds/0012-stuck-agent-recovery-flow.md new file mode 100644 index 0000000..dd3867b --- /dev/null +++ b/docs/prds/0012-stuck-agent-recovery-flow.md @@ -0,0 +1,81 @@ +# PRD 0012: Stuck-agent recovery flow + +- **Status:** Draft +- **Author:** didericis +- **Created:** 2026-05-24 + +## Summary + +When an agent running inside a claude-bottle container gets blocked by a missing permission, tool, or skill, it asks for help via a PR comment; the user approves a manifest change in a TUI dashboard; the orchestrator rebuilds the container from the new manifest and resumes work on the same branch — without ever opening a live channel into the running bottle. + +## Problem + +Running parallel agents in isolated bottles makes it cheap to spin up work in parallel, but expensive to recover when an agent gets stuck. Today, if a bottle is missing a permission or a tool the agent needs to make progress, the only options are to kill the container and start over (losing work) or open a live channel into the bottle to fix it in place (breaking the sandbox property that makes bottles trustworthy in the first place). The user feels this directly whenever a parallel run blocks on something the manifest didn't anticipate. + +## Goals / Success Criteria + +A real stuck agent recovers end-to-end through the flow: the agent hits a missing permission, posts a PR comment describing the ask, the user reviews the request in a dashboard, approves a manifest diff, and a fresh bottle picks up on the same branch and continues. The whole loop completes without anyone running `docker attach` or opening any live channel into the original container. + +## Non-goals + +- Live attach or in-place mutation of running containers. The whole design exists to avoid this. +- Agent-to-agent communication. Re-stated from the project's existing non-goals; the recovery flow is human→agent only. +- Auditing or forensic replay of agent runs. Git/forge history is the audit log; this PRD does not add a separate run log. +- Reducing time-to-unstuck below some target. Faster than kill-and-restart is implicit, but no specific SLO is in scope. + +## Scope + +### In scope + +- A `/request-bottle-change` slash command the agent invokes when it knows it's blocked. +- A TUI dashboard that lists running bottles and pending change requests, and takes approve/reject input from the user. +- A rebuild orchestrator that tears down the old bottle, applies the approved manifest change, and starts a replacement bottle on the same branch. +- A state-preservation helper that carries forward what it can across the rebuild (working tree is mandatory; transcript / reasoning context is best-effort). +- A stuck-signal mechanism that does not require a forge token inside the bottle: the agent's slash command sends the request to the existing cred-proxy endpoint, which (with a host-mounted volume) writes the sentinel artifact on the host side. The orchestrator polls that artifact and posts the PR comment from outside the bottle. + +### Out of scope + +- A tool-denial hook that auto-detects "stuck" without the agent's involvement. Deferred to a follow-up; v1 is opt-in via the slash command. +- A web dashboard. TUI only in v1. +- Live channel into running containers (see Non-goals). +- Agent-to-agent communication (see Non-goals). +- Auditing / forensic replay (see Non-goals). + +## Proposed Design + +### New services / components + +- **`/request-bottle-change` slash command.** Shipped as a skill mounted into bottles. When the agent invokes it, the command POSTs a structured request (what's needed, why, what was tried) to the cred-proxy endpoint and halts the agent. The agent never touches the host filesystem. +- **TUI dashboard.** A `claude-bottle dashboard` (or similarly named) command that lists running bottles, surfaces pending change requests, shows the proposed manifest diff, and accepts approve/reject input. Targets stdlib only; a TUI library is added only if the experience truly demands it. +- **Rebuild orchestrator.** The plumbing that, on approval, tears down the existing bottle, applies the approved manifest change, snapshots state via the state-preservation helper, and starts a fresh bottle on the same branch. +- **State-preservation helper.** Mandatory: ensures the working tree is pushed before teardown. Best-effort: carries forward the agent's transcript / reasoning context into the replacement container so the new agent starts warm rather than cold. + +### Existing code touched + +- **cred-proxy** (PRD 0010) — extended with an endpoint that accepts stuck-requests from inside a bottle and writes the sentinel artifact to a host-mounted volume. +- **`cli.py`** — gains the dashboard subcommand and the rebuild path. +- **Bottle lifecycle scripts** — extended for orchestrated teardown + rebuild with state hand-off, distinct from a fresh-spawn. +- **Bottle manifest schema** — may need to record the originating manifest version / change history per agent run, so the dashboard can show "what changed" rather than "what is." + +### Data model changes + +- A new stuck-request artifact (probably JSON) written by the cred-proxy on behalf of the agent, with whatever fields the dashboard needs to render the ask. +- A per-agent-run record sufficient to map a running bottle back to its PR / branch, so the orchestrator knows where to post the comment and which branch to resume on. + +### External dependencies + +- The Gitea API / `tea` CLI is already in the toolbox (the project is on Gitea); no new auth surface beyond what the orchestrator already needs to read/post on PRs. +- A TUI library is a *maybe* — only if stdlib can't carry the dashboard experience. Default to no new dependency. + +## Open questions + +- What exactly does best-effort transcript preservation look like? Mount the agent's state directory, snapshot on teardown, remount in the replacement? How much fidelity is "good enough" for the new agent to pick up? +- Should v1 also ship the tool-denial hook (auto-detect stuck), or strictly the agent-initiated slash command? Currently deferred, but the line is worth confirming during implementation. +- How does the dashboard handle rejection? Does the agent get a comment back saying "denied, here's why," or does the bottle just stay torn down? +- How does the orchestrator know which PR / branch a given bottle maps to — recorded at bottle-spawn time, derived from the working tree, or specified in the manifest? +- Concurrency: if multiple bottles request changes simultaneously, what does the dashboard surface and in what order? + +## References + +- PRD 0010 — cred-proxy (the endpoint extended to carry stuck-requests). +- `CLAUDE.md` — project non-goal on agent-to-agent communication; this PRD stays on the human→agent side of that line. -- 2.52.0 From 83756fa8c9d8b5d7554fe19b744c6f2fe2d87383 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 24 May 2026 23:12:55 -0400 Subject: [PATCH 02/12] docs(prd-0012): open question for gitlock/pipelock exception flow --- docs/prds/0012-stuck-agent-recovery-flow.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/prds/0012-stuck-agent-recovery-flow.md b/docs/prds/0012-stuck-agent-recovery-flow.md index dd3867b..7d69ef4 100644 --- a/docs/prds/0012-stuck-agent-recovery-flow.md +++ b/docs/prds/0012-stuck-agent-recovery-flow.md @@ -74,6 +74,7 @@ A real stuck agent recovers end-to-end through the flow: the agent hits a missin - How does the dashboard handle rejection? Does the agent get a comment back saying "denied, here's why," or does the bottle just stay torn down? - How does the orchestrator know which PR / branch a given bottle maps to — recorded at bottle-spawn time, derived from the working tree, or specified in the manifest? - Concurrency: if multiple bottles request changes simultaneously, what does the dashboard surface and in what order? +- How does the flow handle one-off exceptions to gitlock / pipelock denials — e.g. a commit that includes docs with intentionally-bogus tokens that the secret scanner correctly flags? The shape (agent blocked → ask via PR comment → user approves → continue) is the same as a manifest-change request, but the *resolution* is different: a per-operation override or a scoped allowlist entry, not a new manifest. Does this fold into the same `/request-bottle-change` slash command with a different request type, or is it a separate slash command (e.g. `/request-gate-exception`)? And how is an "exception" expressed safely — by commit SHA, by content hash, by a narrow allowlist rule? Either way, the approval must be auditable so a future reader can see what was waived and why. ## References -- 2.52.0 From a74dd2b97f34a1a48e1f2b90954d753358c1ab97 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 24 May 2026 23:39:17 -0400 Subject: [PATCH 03/12] docs: research on git-gate commit approval; link from PRD 0012 --- docs/prds/0012-stuck-agent-recovery-flow.md | 2 +- docs/research/git-gate-commit-approval.md | 229 ++++++++++++++++++++ 2 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 docs/research/git-gate-commit-approval.md diff --git a/docs/prds/0012-stuck-agent-recovery-flow.md b/docs/prds/0012-stuck-agent-recovery-flow.md index 7d69ef4..41a142a 100644 --- a/docs/prds/0012-stuck-agent-recovery-flow.md +++ b/docs/prds/0012-stuck-agent-recovery-flow.md @@ -74,7 +74,7 @@ A real stuck agent recovers end-to-end through the flow: the agent hits a missin - How does the dashboard handle rejection? Does the agent get a comment back saying "denied, here's why," or does the bottle just stay torn down? - How does the orchestrator know which PR / branch a given bottle maps to — recorded at bottle-spawn time, derived from the working tree, or specified in the manifest? - Concurrency: if multiple bottles request changes simultaneously, what does the dashboard surface and in what order? -- How does the flow handle one-off exceptions to gitlock / pipelock denials — e.g. a commit that includes docs with intentionally-bogus tokens that the secret scanner correctly flags? The shape (agent blocked → ask via PR comment → user approves → continue) is the same as a manifest-change request, but the *resolution* is different: a per-operation override or a scoped allowlist entry, not a new manifest. Does this fold into the same `/request-bottle-change` slash command with a different request type, or is it a separate slash command (e.g. `/request-gate-exception`)? And how is an "exception" expressed safely — by commit SHA, by content hash, by a narrow allowlist rule? Either way, the approval must be auditable so a future reader can see what was waived and why. +- How does the flow handle one-off exceptions to gitlock / pipelock denials — e.g. a commit that includes docs with intentionally-bogus tokens that the secret scanner correctly flags? The shape (agent blocked → ask via PR comment → user approves → continue) is the same as a manifest-change request, but the *resolution* is different: a per-operation override or a scoped allowlist entry, not a new manifest. Does this fold into the same `/request-bottle-change` slash command with a different request type, or is it a separate slash command (e.g. `/request-gate-exception`)? And how is an "exception" expressed safely — by commit SHA, by content hash, by a narrow allowlist rule? Either way, the approval must be auditable so a future reader can see what was waived and why. See `docs/research/git-gate-commit-approval.md` for a survey of gitleaks's native allowlist primitives and a recommendation. ## References diff --git a/docs/research/git-gate-commit-approval.md b/docs/research/git-gate-commit-approval.md new file mode 100644 index 0000000..463d7bc --- /dev/null +++ b/docs/research/git-gate-commit-approval.md @@ -0,0 +1,229 @@ +# Approving specific commits past git-gate + +Research into (1) whether a dashboard or operator surface for the +git-gate (a.k.a. "gitlock", PRD 0008) already exists, and (2) what a +narrowly-scoped approval flow for false-positive gitleaks rejections +could look like without compromising the gate's "if it's bypassable it +isn't a gate" property. + +Motivated by PRD 0012's open question: when an agent commits docs +containing intentionally-bogus tokens that the secret scanner +correctly flags, the rejection is correct in the literal sense and +wrong in the user-intent sense, and there is no way to say so. + +## Summary + +There is no dashboard for the git-gate today. The CLI ships +`init / list / info / start / edit / cleanup` for bottles; the gate is +visible only as a sidecar in `bottle_plan.py`'s preflight rendering. +No `gate` subcommand exists. + +There is also no exception mechanism. The pre-receive hook calls +`gitleaks git --log-opts="$range" --no-banner --redact` with no +config path and no allowlist surface. PRD 0008 explicitly rejects +exceptions ("Bypass for trusted commits. No `[skip gitleaks]` +trailer, no allowlist by commit hash. If the gate is bypassable it +isn't a gate."). + +That non-goal is correct under its own framing — *any path the agent +can take* invalidates the gate — but it conflates two distinct +questions: can the *agent* bypass the gate (must be no), and can the +*user* approve a narrowly-scoped exception (could be yes, under +constraints). PRD 0012's recovery flow is exactly the seam where a +user-side, out-of-band approval could live without giving the agent +any in-band bypass. + +The design problem is therefore not "should there be exceptions" but +"how narrow does an exception have to be before the gate is still a +gate." This note surveys gitleaks's native allowlist primitives, +sketches three approval-scope designs, and recommends a direction. + +## Question 1: Is there a dashboard / operator surface for git-gate? + +No, in three senses: + +- **No CLI subcommand.** `claude_bottle/cli/` has `_common, cleanup, + edit, info, init, list, start` and nothing gate-specific. + `claude-bottle list` shows bottles, not their gates' state or + recent rejections. +- **No gate-side log surface.** Rejections are written to the + pre-receive hook's stderr (`echo "git-gate: gitleaks rejected push + to $ref" >&2`); the agent sees the rejection in its `git push` + output, but nothing persists outside the container's logs. +- **No upstream UI for git-gate.** gitleaks itself is a CLI; it has + no built-in dashboard. The hosted secret-scanning UIs surveyed in + `git-secret-scanning-hardening.md` (ggshield, TruffleHog Enterprise) + are SaaS products that ship repo content to a vendor — explicitly + the wrong shape for a project whose premise is sandbox isolation. + +The PRD 0012 dashboard, when it exists, is the natural place for +git-gate operator surface to live: list pending change requests, +show recent rejections per bottle, render the diff of any +exception-approval request. There is no reason to build a separate +gate dashboard. + +## Question 2: How could specific commits be approved? + +### What gitleaks gives you natively + +Gitleaks's TOML config supports an `[allowlist]` block (or +`[[rules.allowlists]]` per-rule) with four selectors that can be +combined inside a single entry. The selectors observed in current +gitleaks (v8) are: + +- `paths` — list of regex against file paths. +- `regexes` — list of regex matched against the *finding's* matched + bytes; on match, suppress the finding. `regexTarget` chooses + whether the regex applies to the matched bytes, the surrounding + line, or the secret group only. +- `stopwords` — substrings that, if present in the finding, suppress + it. Cheaper than `regexes` for literal matches. +- `commits` — explicit commit SHAs to skip entirely. +- `condition` — `AND` (default) or `OR` across the above selectors, + letting an entry require, e.g., both a path match *and* a content + match before suppressing. + +`commits` is the bluntest tool and the easiest to misuse: a single +SHA can hide arbitrary content. `paths + regexes` (with AND) is the +narrowest scope: a finding is only suppressed if it lives at a +specific path *and* matches a specific byte pattern. That's the +shape that makes a per-finding exception still defensible. + +The hook today does not pass `--config` to gitleaks. Adding it would +mean baking a config file into the gate image *or* mounting one in +at `start` time. The image is built per `DockerGitGate.start`, so +either is mechanically straightforward. + +### The design tension + +PRD 0008's "no bypass for trusted commits" non-goal is load-bearing +*against the agent*. It is not load-bearing against the user, who +already has every privilege the gate is trying to deny the agent. +The risk of letting the user approve exceptions is not direct (the +user can already do whatever they want); it is indirect: + +- **Prompt-injection laundering.** An attacker who has captured the + agent's prompt-stream can ask the agent to *request* an exception + that looks plausible ("I just need to commit the test fixture for + the new auth flow"). If the user rubber-stamps the request, the + attacker has used the user as a bypass channel. This is the same + risk as any human-in-the-loop control: it degrades to "no control" + if the human always says yes. +- **Scope creep of a granted exception.** A commit-SHA allowlist + approved for one commit could, in principle, be re-targeted at a + different commit if the allowlist isn't tied to the content. This + is why `commits` alone is unsafe; `paths + regexes` is the form + that survives content-substitution. +- **Persistence past intent.** An exception granted "just for this + commit" that stays in the gate's config indefinitely is no longer + a per-commit exception; it's a permanent allowlist entry. Without + TTL or a clean teardown, exceptions accrete. + +These three risks shape the design constraints below. + +### Three design options + +**Option A — Reject and rotate.** Treat every gitleaks hit as +"rewrite the commit to not contain the literal token, then re-push." +For docs with fake tokens, use a sentinel string the repo's +gitleaks config recognizes as obviously not a real secret (e.g. +`AKIAIOSFODNN7EXAMPLE`, AWS's documented example key, or a project- +specific placeholder like ``). + +- *Cost:* zero. No new code. +- *Property:* gate stays unbypassable in both senses. +- *Friction:* every author must know the placeholder convention. The + first time someone pastes a realistic-looking fake into a doc, + they get rejected and have to redo the commit. Probably fine for + the host repo; less fine for bottles authoring third-party content. +- *Verdict:* this should be the *default*. The exception flow exists + only for cases where Option A genuinely fails (e.g. the example is + specifically about a real-looking token format, or the upstream + doc requires the literal pattern). + +**Option B — Per-finding narrow allowlist via PRD 0012 flow.** When +the agent's push is rejected, the agent invokes +`/request-gate-exception` (or `/request-bottle-change` with an +exception variant). The slash command POSTs to the cred-proxy +endpoint, carrying: + +- the file path that triggered the finding +- the finding's matched-byte hash (not the bytes themselves, to keep + the request artifact non-secret on its own) +- the gitleaks rule ID +- a free-text justification ("docs example for AWS auth flow") + +The user reviews the request in the dashboard, sees the file and the +diff, and approves an entry of shape `{ paths: [], +regexes: [], condition: AND }`. +The gate restarts with that config entry merged into its +`.gitleaks.toml`. A future commit on the same path with a *different* +finding still hits the gate and rejects. + +- *Property:* approved exceptions are content-locked, not commit- + locked. Substituting bytes on the same path triggers a fresh + rejection. +- *Auditability:* the approval is a manifest diff; it lives in git + history and in the PR conversation thread per PRD 0012. +- *Open: TTL.* Should the entry expire? Plausible defaults: never + (it's content-locked anyway), or "until the next manifest version + bump." Lean "never" for v1; revisit if exception lists balloon. + +**Option C — Pre-flight scan with author signoff.** Run gitleaks +client-side inside the bottle (as a non-gating advisory check) so +the agent sees findings *before* attempting the push. The slash +command then includes the pre-known findings; the dashboard shows +the user the finding inline rather than having to go look at the +rejection log. On approval, same Option-B-style allowlist entry +gets added. + +- *Property:* identical end-state to Option B; better UX because + the agent stops before the rejected push, not after. +- *Cost:* one more place that needs gitleaks installed (the bottle + image), and an in-bottle advisory check that the agent can in + principle ignore. That's fine because it's *advisory* — the gate + still rejects; the in-bottle check just avoids one round-trip. +- *Verdict:* nice-to-have over Option B, not a substitute. + +### Recommendation + +Default to Option A as the canonical answer ("rewrite to use a +placeholder"). Build Option B as the PRD 0012 exception path, scoped +narrowly: `paths + regexes` with AND, no `commits` selector exposed +to the approval flow. Defer Option C to a follow-up; it's an +ergonomic win, not a security property. + +This puts the answer to PRD 0012's open question as: + +- Same recovery shape (`/request-bottle-change`), distinguishable + request type. The dashboard renders an exception request + differently from a manifest-change request because the *diff* + being approved is to the gate's allowlist, not to the manifest. +- Exceptions are expressed as `(path, content-pattern)` pairs, not + commit SHAs. Re-pushing different bytes on the same path + re-triggers the gate. +- The approval is recorded twice for audit: in the PR thread (free- + text), and as a versioned diff to the gate's allowlist config (or + the manifest field that materializes into it). + +## Cross-references + +- PRD 0008 — git-gate design and "no bypass" non-goal. +- PRD 0010 — cred-proxy; the inbound endpoint PRD 0012 reuses for + exception requests. +- PRD 0012 — stuck-agent recovery flow; the open question this note + informs. +- `docs/research/git-secret-scanning-hardening.md` — prior research + on the secret-scanning tool landscape and why gitleaks is the fit. + +## Sources + +- [gitleaks configuration documentation](https://github.com/gitleaks/gitleaks#configuration) + — `[allowlist]` selectors (`paths`, `regexes`, `stopwords`, + `commits`, `regexTarget`, `condition`). +- [AWS example access key (`AKIAIOSFODNN7EXAMPLE`)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html) + — documented placeholder safe to use in examples without + triggering most secret scanners. +- `claude_bottle/git_gate.py` — pre-receive hook implementation + (`gitleaks git --log-opts="$log_opts" --no-banner --redact`, no + `--config` argument today). -- 2.52.0 From c33930290fa7160ba7c5d31de92cc0a7a93f883c Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 24 May 2026 23:54:46 -0400 Subject: [PATCH 04/12] docs(research): survey gitleaks dashboards + add baseline-file primitive --- docs/research/git-gate-commit-approval.md | 348 +++++++++++++++------- 1 file changed, 244 insertions(+), 104 deletions(-) diff --git a/docs/research/git-gate-commit-approval.md b/docs/research/git-gate-commit-approval.md index 463d7bc..a81464e 100644 --- a/docs/research/git-gate-commit-approval.md +++ b/docs/research/git-gate-commit-approval.md @@ -13,86 +13,200 @@ wrong in the user-intent sense, and there is no way to say so. ## Summary -There is no dashboard for the git-gate today. The CLI ships -`init / list / info / start / edit / cleanup` for bottles; the gate is -visible only as a sidecar in `bottle_plan.py`'s preflight rendering. -No `gate` subcommand exists. +No off-the-shelf dashboard fits the shape claude-bottle needs +(per-bottle, host-local, integrated into a pre-receive rejection +with approval feeding back into the gate's own decision). Gitleaks +itself is a CLI with no UI and was declared **feature-complete** in +early 2026; the author's successor project **Betterleaks** is +explicitly "for the agentic era" but is also CLI-shaped and still +young. The closest open-source dashboard is **DefectDojo**, which +ingests gitleaks JSON but is post-hoc and org-scale — its "marked +as accepted" state does not feed back into the scanner. SaaS +dashboards (GitGuardian, TruffleHog Enterprise) ship repo content +to a vendor and were already disqualified by +`git-secret-scanning-hardening.md`. -There is also no exception mechanism. The pre-receive hook calls -`gitleaks git --log-opts="$range" --no-banner --redact` with no -config path and no allowlist surface. PRD 0008 explicitly rejects -exceptions ("Bypass for trusted commits. No `[skip gitleaks]` -trailer, no allowlist by commit hash. If the gate is bypassable it -isn't a gate."). +The git-gate ships no exception mechanism today: the pre-receive +hook calls `gitleaks git --log-opts="$range" --no-banner --redact` +with no `--config` and no `--baseline-path`, and PRD 0008 +explicitly rejects exceptions ("Bypass for trusted commits. No +`[skip gitleaks]` trailer, no allowlist by commit hash. If the +gate is bypassable it isn't a gate."). -That non-goal is correct under its own framing — *any path the agent -can take* invalidates the gate — but it conflates two distinct -questions: can the *agent* bypass the gate (must be no), and can the -*user* approve a narrowly-scoped exception (could be yes, under -constraints). PRD 0012's recovery flow is exactly the seam where a -user-side, out-of-band approval could live without giving the agent -any in-band bypass. +That non-goal is correct against the *agent* but conflates two +questions: can the *agent* bypass the gate (must be no), and can +the *user* approve a narrowly-scoped exception out-of-band (could +be yes). PRD 0012's recovery flow is exactly the seam where the +user-side approval can live without giving the agent any in-band +bypass. -The design problem is therefore not "should there be exceptions" but -"how narrow does an exception have to be before the gate is still a -gate." This note surveys gitleaks's native allowlist primitives, -sketches three approval-scope designs, and recommends a direction. +Gitleaks does ship one native primitive that maps well to "approve +this specific finding" — the **baseline file** — which is +semantically a better fit for per-finding approval than the +allowlist config (a suppression *rule*). This note surveys the +dashboard landscape, the two native primitives (allowlist and +baseline), and recommends a direction. -## Question 1: Is there a dashboard / operator surface for git-gate? +## Question 1: Existing dashboards and control surfaces -No, in three senses: +### Inside claude-bottle today -- **No CLI subcommand.** `claude_bottle/cli/` has `_common, cleanup, - edit, info, init, list, start` and nothing gate-specific. - `claude-bottle list` shows bottles, not their gates' state or - recent rejections. -- **No gate-side log surface.** Rejections are written to the - pre-receive hook's stderr (`echo "git-gate: gitleaks rejected push - to $ref" >&2`); the agent sees the rejection in its `git push` - output, but nothing persists outside the container's logs. -- **No upstream UI for git-gate.** gitleaks itself is a CLI; it has - no built-in dashboard. The hosted secret-scanning UIs surveyed in - `git-secret-scanning-hardening.md` (ggshield, TruffleHog Enterprise) - are SaaS products that ship repo content to a vendor — explicitly - the wrong shape for a project whose premise is sandbox isolation. +`claude_bottle/cli/` has `_common, cleanup, edit, info, init, list, +start` — nothing gate-specific. The gate appears only as a sidecar +in `bottle_plan.py`'s preflight rendering. Rejections are written +to the pre-receive hook's stderr (`echo "git-gate: gitleaks +rejected push to $ref" >&2`) and surface only in the agent's +`git push` output — nothing persists outside the container's logs. -The PRD 0012 dashboard, when it exists, is the natural place for -git-gate operator surface to live: list pending change requests, -show recent rejections per bottle, render the diff of any -exception-approval request. There is no reason to build a separate -gate dashboard. +### Native gitleaks: CLI-only, and now feature-complete + +Gitleaks has no built-in dashboard or web UI. As of early 2026 the +project has been declared **feature complete** — only security +patches will be merged going forward. The original maintainer +(Zachary Rice) has moved active work to Betterleaks (below), so +any dashboard built directly against gitleaks should treat the +gitleaks surface as frozen rather than evolving. + +### Betterleaks: the same author's "agentic era" successor + +Started February 2026 and explicitly framed for AI agents driving +the scanner: flag-based output for low-token-overhead consumption, +parallelized Git scanning, CEL-based filtering in place of the +TOML allowlist, and a roadmap that includes LLM-assisted +classification and automatic secret revocation via provider APIs. +Still CLI-shaped — no dashboard either. + +Relevant to claude-bottle in two ways: + +- The upstream direction of travel is *toward* agent-driven + scanners, which makes "the bottle invokes a scanner and reports + findings up" a supported pattern rather than a hack. +- CEL is a richer expression language for filter entries than + gitleaks's selector struct, which loosens the design space for + Option B (below). If claude-bottle ever swaps gitleaks for + Betterleaks, the approval-flow design should be expressible in + both. + +### Output formats: SARIF + viewers + +Both gitleaks and Betterleaks can emit SARIF. That plugs into +GitHub Advanced Security's Code Scanning tab (read-only viewer +with a dismiss-as-not-a-problem state) and assorted open-source +SARIF viewers (`sarif-web-component`, Microsoft's VS Code +extension). These render findings; they do not handle approval +state or feed back into the scanner. Useful for *seeing* findings; +not useful as the approval surface. + +### Findings aggregators + +[**DefectDojo**](https://defectdojo.com/integrations/gitleaks) is +the closest open-source thing to "a dashboard for gitleaks." It +ingests gitleaks JSON (and ~200 other scanners), aggregates and +deduplicates, lets you triage and mark findings as accepted or +false-positive in its UI, and tracks remediation state. Designed +for org-scale: one DefectDojo instance covers many repos and +scanners. + +Shape mismatch for claude-bottle: + +- DefectDojo's review state is *informational* — marking a finding + as accepted in DefectDojo does not write to gitleaks's allowlist + or baseline and does not change what the gate decides on the + next push. +- It expects findings as artifacts of CI runs, not as the + rejection-cause of an in-flight push. +- A single shared instance violates the one-sidecar-per-bottle + posture; per-bottle DefectDojo instances are absurd overhead. + +Useful to know it exists, especially for long-term post-hoc +finding tracking. Not the v1 answer for the in-flight approval +flow PRD 0012 needs. + +A separate [JupiterOne integration](https://github.com/gitleaks-findings/gitleaks) +exists but ships findings to JupiterOne's commercial platform and +has effectively zero public adoption (0 stars, 0 forks). Mentioned +only because its repo name suggests "the dashboard" and isn't. + +### SaaS dashboards (disqualified by sandbox premise) + +GitGuardian / ggshield and TruffleHog Enterprise both offer +incident-triage UIs with finding-level approval state. Both ship +repo content to a vendor; already disqualified in +`git-secret-scanning-hardening.md` for a project whose entire +premise is sandbox isolation. + +### Bottom line + +No off-the-shelf dashboard fits claude-bottle's shape: per-bottle, +host-local, integrated into a pre-receive rejection with the +approval feeding back into the gate's own decision-making. The +nearest open-source analogue (DefectDojo) is post-hoc and +org-scale; the nearest UX (GitGuardian) is SaaS. The PRD 0012 +dashboard — sharing surface with the broader stuck-agent recovery +flow — remains the right place to build this. ## Question 2: How could specific commits be approved? ### What gitleaks gives you natively -Gitleaks's TOML config supports an `[allowlist]` block (or -`[[rules.allowlists]]` per-rule) with four selectors that can be -combined inside a single entry. The selectors observed in current -gitleaks (v8) are: +Two distinct primitives, and the distinction matters for designing +an approval flow. + +**Allowlists** are *suppression rules* — config-level patterns that +say "ignore findings matching X." Gitleaks's TOML config supports +an `[allowlist]` block (or `[[rules.allowlists]]` per-rule) with +four selectors: - `paths` — list of regex against file paths. -- `regexes` — list of regex matched against the *finding's* matched - bytes; on match, suppress the finding. `regexTarget` chooses - whether the regex applies to the matched bytes, the surrounding - line, or the secret group only. -- `stopwords` — substrings that, if present in the finding, suppress - it. Cheaper than `regexes` for literal matches. +- `regexes` — list of regex matched against the finding bytes; + `regexTarget` directs the regex at the extracted secret + (default), the entire regex match, or the whole line. +- `stopwords` — substrings that, if present, suppress the finding. - `commits` — explicit commit SHAs to skip entirely. -- `condition` — `AND` (default) or `OR` across the above selectors, - letting an entry require, e.g., both a path match *and* a content - match before suppressing. -`commits` is the bluntest tool and the easiest to misuse: a single -SHA can hide arbitrary content. `paths + regexes` (with AND) is the -narrowest scope: a finding is only suppressed if it lives at a -specific path *and* matches a specific byte pattern. That's the -shape that makes a per-finding exception still defensible. +Selectors combine with `condition = "OR"` (default; suppress if any +selector matches) or `condition = "AND"` (suppress only if all +match). `commits` is the bluntest tool and the easiest to misuse: +a single SHA can hide arbitrary content. `paths + regexes` with +AND is the narrowest scope, and the form that makes a per-finding +exception still defensible. -The hook today does not pass `--config` to gitleaks. Adding it would -mean baking a config file into the gate image *or* mounting one in -at `start` time. The image is built per `DockerGitGate.start`, so -either is mechanically straightforward. +**Baselines** are a *known-findings list* — a JSON file of +previously detected findings that gitleaks's `IsNew` function +compares against on the next scan, so only new findings get +reported. The file is generated by saving a scan's JSON output and +fed back in via `--baseline-path`. The comparison checks RuleID, +description, file path, line numbers, secret content, commit, and +author/timestamp. When `--redact` is enabled, redacted Secret and +Match fields are ignored in the comparison so the baseline still +functions with redacted reports. + +Detection flow is: global allowlist → rule-specific allowlist → +baseline → reported finding. Allowlist suppressions therefore win +over baseline; baseline is the last gate before report. + +The hook today passes neither `--config` nor `--baseline-path`. +Wiring either in is mechanically straightforward: the gate image +is built per `DockerGitGate.start`, so the config / baseline can be +baked into the image *or* mounted in at start. + +**Allowlist vs baseline for approval storage.** Both can express +"don't reject this finding," but they imply different things about +intent: + +- An *allowlist* entry says "any future finding that matches this + pattern is fine." Generative: it covers findings that don't + exist yet on commits that haven't been made. +- A *baseline* entry says "this exact finding I've already seen is + fine." Specific: it pins to the bytes / location / rule of one + observed finding; a different finding on the same path on a + later commit re-triggers. + +For a per-commit user approval, baseline is the better semantic +match: each approval is an attestation about one observed finding, +not a rule that pre-approves a pattern. Baseline entries can also +be diffed in PRs trivially (it's a JSON list) — they double as the +audit record. ### The design tension @@ -141,40 +255,41 @@ specific placeholder like ``). specifically about a real-looking token format, or the upstream doc requires the literal pattern). -**Option B — Per-finding narrow allowlist via PRD 0012 flow.** When -the agent's push is rejected, the agent invokes +**Option B — Per-finding approval via PRD 0012 flow.** When the +agent's push is rejected, the agent invokes `/request-gate-exception` (or `/request-bottle-change` with an exception variant). The slash command POSTs to the cred-proxy -endpoint, carrying: +endpoint, carrying the gitleaks finding record (rule ID, file path, +line, redacted match) and a free-text justification ("docs example +for AWS auth flow"). -- the file path that triggered the finding -- the finding's matched-byte hash (not the bytes themselves, to keep - the request artifact non-secret on its own) -- the gitleaks rule ID -- a free-text justification ("docs example for AWS auth flow") +The user reviews the request in the dashboard, sees the file and +the diff, and approves. The approval gets written into the gate's +**baseline file** — the JSON list of known-OK findings the gate +passes as `--baseline-path` to gitleaks. The gate restarts with +the new baseline. -The user reviews the request in the dashboard, sees the file and the -diff, and approves an entry of shape `{ paths: [], -regexes: [], condition: AND }`. -The gate restarts with that config entry merged into its -`.gitleaks.toml`. A future commit on the same path with a *different* -finding still hits the gate and rejects. - -- *Property:* approved exceptions are content-locked, not commit- - locked. Substituting bytes on the same path triggers a fresh - rejection. -- *Auditability:* the approval is a manifest diff; it lives in git - history and in the PR conversation thread per PRD 0012. -- *Open: TTL.* Should the entry expire? Plausible defaults: never - (it's content-locked anyway), or "until the next manifest version - bump." Lean "never" for v1; revisit if exception lists balloon. +- *Property:* approved findings are pinned to the specific + observed bytes / path / rule. A different secret on the same + path on a later commit re-triggers the gate. +- *Auditability:* baseline file is JSON in git history; each PR + approval becomes a diff to that file. The free-text + justification lives in the PR thread per PRD 0012. +- *Fallback to allowlist for canonical cases.* If a particular + fixture file should be permanently understood as "examples only," + the user can promote a baseline entry to an `[allowlist]` rule + with `paths + regexes` AND — explicit generalization, opt-in by + the user, never by the agent. +- *Open: TTL.* Should baseline entries expire? Baseline is specific + by construction, so the case for expiration is weaker than for + allowlist. Lean "never" for v1; revisit if baselines balloon. **Option C — Pre-flight scan with author signoff.** Run gitleaks client-side inside the bottle (as a non-gating advisory check) so the agent sees findings *before* attempting the push. The slash command then includes the pre-known findings; the dashboard shows the user the finding inline rather than having to go look at the -rejection log. On approval, same Option-B-style allowlist entry +rejection log. On approval, same Option-B-style baseline entry gets added. - *Property:* identical end-state to Option B; better UX because @@ -188,23 +303,28 @@ gets added. ### Recommendation Default to Option A as the canonical answer ("rewrite to use a -placeholder"). Build Option B as the PRD 0012 exception path, scoped -narrowly: `paths + regexes` with AND, no `commits` selector exposed -to the approval flow. Defer Option C to a follow-up; it's an -ergonomic win, not a security property. +placeholder"). Build Option B as the PRD 0012 exception path, +storing approvals in the gate's **baseline file** (not in an +allowlist rule). Baseline is the right primitive because each +approval is an attestation about one observed finding, not a +generative pattern. Allowlist promotion is a separate, user- +initiated escalation for cases that genuinely deserve patterning. +The `commits` selector is never exposed to the approval flow under +either path — it hides arbitrary content. Defer Option C to a +follow-up; it's an ergonomic win, not a security property. This puts the answer to PRD 0012's open question as: - Same recovery shape (`/request-bottle-change`), distinguishable request type. The dashboard renders an exception request differently from a manifest-change request because the *diff* - being approved is to the gate's allowlist, not to the manifest. -- Exceptions are expressed as `(path, content-pattern)` pairs, not - commit SHAs. Re-pushing different bytes on the same path - re-triggers the gate. -- The approval is recorded twice for audit: in the PR thread (free- - text), and as a versioned diff to the gate's allowlist config (or - the manifest field that materializes into it). + being approved is to the gate's baseline file, not to the + manifest. +- Exceptions are expressed as baseline-file entries — finding- + specific JSON records — not commit SHAs or regex patterns. +- The approval is recorded twice for audit: in the PR thread + (free-text justification), and as a versioned diff to the + baseline file (which is committed alongside the manifest). ## Cross-references @@ -218,12 +338,32 @@ This puts the answer to PRD 0012's open question as: ## Sources -- [gitleaks configuration documentation](https://github.com/gitleaks/gitleaks#configuration) - — `[allowlist]` selectors (`paths`, `regexes`, `stopwords`, - `commits`, `regexTarget`, `condition`). +- [gitleaks repository](https://github.com/gitleaks/gitleaks) — + `[allowlist]` selectors (`paths`, `regexes`, `stopwords`, + `commits`, `regexTarget`, `condition`); also home of the + feature-complete notice. +- [Gitleaks allowlists & baselines (DeepWiki)](https://deepwiki.com/gitleaks/gitleaks/4.4-allowlists-and-baselines) + — detailed walk-through of the allowlist selector struct, the + baseline file format, the `IsNew` comparison logic, and the + global→rule→baseline detection order. Primary source for the + allowlist-vs-baseline distinction this note rests on. +- [Betterleaks (GitHub)](https://github.com/betterleaks/betterleaks) + — Zachary Rice's successor project; CEL filtering, agent-driven + output design, roadmap for LLM-assisted classification. +- [Help Net Security on Betterleaks](https://www.helpnetsecurity.com/2026/03/19/betterleaks-open-source-secrets-scanner/) + and [The New Stack](https://thenewstack.io/betterleaks-open-source-secret-scanner/) + — context on the "agentic era" framing and why gitleaks froze. +- [DefectDojo gitleaks parser](https://defectdojo.com/integrations/gitleaks) + — JSON ingest, finding triage UI, accept/false-positive state. + Open-source, generic, post-hoc; informational state only — + marking a finding as accepted does not feed back into the + scanner. Shape mismatch for in-flight per-bottle approval. +- [gitleaks-findings/gitleaks](https://github.com/gitleaks-findings/gitleaks) + — JupiterOne integration, not a dashboard. Listed because the + repo name is misleading. - [AWS example access key (`AKIAIOSFODNN7EXAMPLE`)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_iam-quotas.html) — documented placeholder safe to use in examples without triggering most secret scanners. -- `claude_bottle/git_gate.py` — pre-receive hook implementation - (`gitleaks git --log-opts="$log_opts" --no-banner --redact`, no - `--config` argument today). +- `claude_bottle/git_gate.py` — pre-receive hook implementation. + Today: `gitleaks git --log-opts="$log_opts" --no-banner + --redact`; no `--config`, no `--baseline-path`. -- 2.52.0 From 1f9722ae276bffcee31faf8e01c97162e9e20c10 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 24 May 2026 23:59:42 -0400 Subject: [PATCH 05/12] docs(research): add Betterleaks switching analysis --- docs/research/git-gate-commit-approval.md | 70 +++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/research/git-gate-commit-approval.md b/docs/research/git-gate-commit-approval.md index a81464e..0d94bc7 100644 --- a/docs/research/git-gate-commit-approval.md +++ b/docs/research/git-gate-commit-approval.md @@ -326,6 +326,76 @@ This puts the answer to PRD 0012's open question as: (free-text justification), and as a versioned diff to the baseline file (which is committed alongside the manifest). +## Should we switch to Betterleaks? + +Not yet. Worth tracking. The wins are real on paper, but a +security gate is the wrong place to bet on a three-month-old +project, and the vendor-side benchmark numbers (98.6% recall vs +gitleaks's 70.4% on CredData) have not been independently +reproduced in published sources. + +### What Betterleaks would add for claude-bottle + +- **Detection coverage on encoded secrets.** Native handling of + doubly- and triply-encoded matches. This matters in the + threat model: an agent coaxed into base64-wrapping a token to + slip past the gate is a plausible attack, and gitleaks's + entropy-based approach misses many encodings. +- **CEL filters instead of the TOML allowlist struct.** More + expressive than `paths + regexes + condition`. Doesn't unlock + anything fundamental, but cleaner if exception rules ever need + conjunctive logic ("allow if path matches X *and* line contains + a documented placeholder string"). +- **Agent-aware output.** Flag-based, low-token-overhead CLI + output designed for an AI agent (like one running inside a + bottle) to consume. Useful for the `/request-gate-exception` + slash command's parsing path; ergonomic win, not security- + load-bearing. +- **Avoids the frozen-upstream problem.** Gitleaks is feature- + complete, so a migration is eventually forced; the question is + whether to pay the cost now or later. + +### What it would cost + +- The existing pre-receive hook calls `gitleaks git + --log-opts= --no-banner --redact`. Betterleaks's CLI + surface is similar but not identical and was not designed as a + drop-in for that specific invocation. Some hook rewrite is + likely. +- Whether Betterleaks has a baseline-file equivalent (the + storage format Option B recommends) is unconfirmed at the time + of writing. If it does not, Option B's storage format would + have to be re-derived against whatever Betterleaks offers. +- A three-month-old project has fewer security audits, fewer + third-party integrations, and a smaller community than + gitleaks has accumulated since 2018. The gate is exactly where + that asymmetry matters most. + +### Criteria to revisit + +Revisit when at least two of the following are true: + +- Betterleaks has accumulated ~12 months of stable releases and + at least one external security audit. +- The CredData benchmark numbers have been independently + reproduced. +- A baseline-file equivalent (or a clearly better primitive for + per-finding approval storage) is shipped and documented. +- Gitleaks releases a security patch we cannot apply because the + underlying issue is a design choice rather than a bug — i.e. + the frozen status starts to bite. + +### Forward-compatibility for the approval flow + +Independent of the switching decision, Option B should treat the +choice of scanner as substitutable. Practically: the approval- +flow contract is "an approval is a finding-specific JSON record +stored alongside the manifest"; the *format* of that record +(gitleaks baseline schema today, something else later) is a +serialization concern downstream of the contract. Swapping +scanners then becomes a serialization migration, not a flow +redesign. + ## Cross-references - PRD 0008 — git-gate design and "no bypass" non-goal. -- 2.52.0 From 02647917b2b5ccc455a65649d32c1df220deb4fd Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 00:15:18 -0400 Subject: [PATCH 06/12] docs(research): built-in supervisor design (TUI + PR feedback) --- docs/research/built-in-supervisor-design.md | 98 +++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 docs/research/built-in-supervisor-design.md diff --git a/docs/research/built-in-supervisor-design.md b/docs/research/built-in-supervisor-design.md new file mode 100644 index 0000000..9d3313d --- /dev/null +++ b/docs/research/built-in-supervisor-design.md @@ -0,0 +1,98 @@ +# Built-in Supervisor Design + +## Question + +Can claude-bottle grow a built-in supervisor — TUI inventory plus PR-feedback routing — without breaking the per-bottle isolation model, and without departing from the bash-first, low-dependency posture? + +## Context + +claude-bottle today is a fleet *executor*: `./cli.py start ` brings up one bottle (agent container + pipelock + optional git-gate + optional cred-proxy on a per-bottle internal network), and `cli.py` tears it down when the session ends. There is no inventory view, no idle-detection, no automated reaction to PR or CI events. In parallel use, a human is the supervisor — opening one terminal per bottle, switching between them, and watching upstream PR state by hand. + +A separate survey of the broader ecosystem ([agent control dashboards research, mid-2026](https://gitea.dideric.is/didericis/consilium-research/src/branch/main/developer-workflow/agent-control-dashboards-2026-05-24.md)) sorts dashboards into five tiers (session managers, parallel runners, Kanban boards, mission-control SPAs, observability backends). The earlier first-pass conclusion was that a full SPA tier conflicts with claude-bottle's isolation model. This doc reconsiders the smaller question: a TUI supervisor in the existing Python CLI. + +## What I got wrong the first time + +The earlier framing treated "add a supervisor" as synonymous with "adopt something Composio-AO-shaped" — a Next.js SPA with plugins, dashboards, and a long-running web server. On that framing, the answer is correctly "no, that's too heavy and breaks isolation." + +But the framing collapses two different costs that aren't actually coupled: + +1. The runtime cost of *each bottle* (already paid: container + 1–3 sidecars + 2 networks). +2. The runtime cost of a *supervisor* that watches and controls bottles. + +A supervisor doesn't have to be heavy. A TUI built into the existing Python CLI, reading `docker ps` and host-side log files, is closer in spirit to `tmux-agent-status` than to Mission Control. The trust analysis below is what actually matters. + +## Proposed design + +Three layers, each independently useful, in order of ambition: + +### 1. `./cli.py status` — read-only inventory + +Reads `docker ps` filtered by a bottle label and tails each bottle's session log. Reports per bottle: name, agent, uptime, last-activity timestamp, token spend if available, associated PR/branch if recorded. + +No new daemons. No new ports. No new credentials. ~100 lines. + +### 2. `./cli.py watch` — TUI over the same data + +Same data as `status`, rendered with auto-refresh and keyboard shortcuts that shell out to the existing `cli.py attach / stop / start` commands. + +Library choice: prefer the stdlib `curses` module to stay bash-first-adjacent; fall back to `rich` or `textual` only if the curses path proves painful. Both `rich` and `textual` are single-purpose, pure-Python deps with no transitive bloat, but they are still new deps and per the project conventions warrant a deliberate decision. + +This is the Claude Squad / tmux-agent-status pattern, applied to bottles instead of tmux sessions. The whole category exists *because* a TUI is the lightweight shape that doesn't require what the SPA tier requires. + +### 3. `./cli.py supervise` — PR feedback router + +The optional, more ambitious layer. The bottle manifest gains an optional field: + +```yaml +pr_watch: + upstream: gitea.dideric.is/didericis/myproject + branch: agent/task-42 +auto_respawn: false # opt-in; default notify-only +``` + +`./cli.py supervise` polls the named upstream for new review comments and CI failures on `branch`. When one fires, default behavior is a desktop notification (or a flash in the TUI). With `auto_respawn: true`, the supervisor tears down the bottle and re-runs it with the feedback prepended to the next prompt. + +The polling token is a **host** token (the same `GH_PAT` / Gitea token the host already keeps in shell env), not a bottle credential. The supervisor never holds bottle secrets. + +## Why this doesn't break the trust model + +The load-bearing question is whether the supervisor introduces the privileged-channel-into-every-bottle problem that disqualifies the SPA tier. It does not, for four reasons: + +| Concern | Mitigation | +|---|---| +| Reaching into running bottles | Supervisor reads `docker ps` and host-side log files. The host already sees both — Docker is the trust boundary, the supervisor is on the host side of it. | +| Holding bottle credentials | The polling token is a host token. The supervisor never receives `bottle.cred_proxy.routes` entries; it has no path to them. | +| Bridging between bottles | The supervisor does not relay state from bottle A to bottle B. It relays *upstream PR state* to a bottle's next prompt — and only if the manifest opts in. | +| New attack surface | All "control" actions go through `./cli.py start `, which already enforces the manifest. The supervisor is an automated caller of the existing CLI, not a parallel control plane. | + +The boundary stays at the bottle wall. The supervisor looks outward at git/PR state and downward at Docker; it does not look *inward* through pipelock. + +This also doesn't conflict with the "lean on git history for auditing" non-goal. The supervisor is using git/PR state as the *input* to its loop, not constructing a separate audit log. Git history remains the source of truth for what happened. + +## Where to be conservative + +A few design defaults worth holding: + +- **Default to notify, not auto-respawn.** Review-comment → re-run is a fully autonomous flow. Make it opt-in per bottle. The notify-only mode is safe enough to be the default; the auto-respawn mode crosses into agent-autonomy territory that deserves an explicit decision per agent. +- **Idle detection is harder than it looks.** Last-log-line-age works ~80% of the time. Codeman's Ralph Loop tracker (watching for `` tags) is more accurate but adds complexity and tooling-coupling. Start with the dumb version; add heuristics only when actual confusion arises. +- **No web UI.** A browser UI reintroduces the privileged-channel problem — the browser talks to a server that talks to all bottles. TUI sidesteps it because the supervisor runs in the user's own shell context, not as a long-running daemon serving multiple consumers. +- **State file in `~/.claude-bottle/`, not inside any bottle.** The mapping of bottle → PR → status lives next to the manifest. Nothing about the supervisor's bookkeeping enters a bottle. +- **No new credentials on bottles.** PR-watch is a host-side concern. A bottle's manifest *names* the upstream/branch to watch; it does not grant the bottle the ability to read PR state itself. + +## Trust-model edge cases worth flagging + +- **Cross-host supervisor.** If the supervisor ever grows to coordinate bottles on multiple hosts, the trust analysis changes — the polling token now has to travel, and the "host" boundary is no longer one machine. Out of scope for v1; flag in any future design doc that contemplates it. +- **Auto-respawn with attacker-controlled review content.** If `auto_respawn: true` is on and the upstream is one where untrusted parties can post review comments, those comments become a prompt-injection vector. Mitigations: limit auto-respawn to private repos by default; document the threat; consider a per-route allowlist of trusted commenters. +- **Native Claude Code OpenTelemetry as an alternative observability path.** Setting `CLAUDE_CODE_ENABLE_TELEMETRY=1` inside a bottle would emit OTel data — but the bottle's pipelock allowlist must then include the OTel collector's host, and the trace data is generated by the (untrusted) agent rather than by the (trusted) host-side perimeter. The supervisor-via-Docker-plus-pipelock-logs path is the cleaner observability spine for this project's threat model; in-bottle OTel is an extra signal worth weighing case-by-case, not the default. + +## Scope estimate + +The full `status` / `watch` / `supervise` trio is plausibly ~500 lines of Python on top of the existing CLI, no new runtimes, no new daemons, no new ports, and (with `curses`) no new deps. That fits "Low dependencies by default. The project is bash-first; ask before adding new tools, runtimes, or package managers" without requiring an exception. + +Phased: `status` first (purely additive, no design decisions), then `watch` (the design decisions are mostly UX, not architecture), then `supervise` (the only layer that introduces a new behavioral default and warrants a PRD of its own). + +## Conclusion + +A supervisor that respects the bottle wall is a small natural extension of what claude-bottle already is, not a category shift toward Mission Control / Codeman / Composio AO. The mistake in earlier framing was treating "supervisor" as synonymous with "dashboard SPA." The trust-model question that disqualifies the SPA tier (privileged channel into every bottle) does not apply to a TUI that reads host-side signals and shells out to the existing CLI. + +Recommendation: build `status` and `watch` opportunistically when the pain is felt; treat `supervise` as a separate PRD before implementation, with `auto_respawn` defaulting to off. -- 2.52.0 From 95a4433d39ca452a4b30e80b064ca0e94bdd8236 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 00:50:41 -0400 Subject: [PATCH 07/12] docs(research): drop auto-respawn from the supervisor design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The autonomous "review comment → respawn bottle with comment as next prompt" loop is the one feature that opens a prompt-injection vector the bottle wall can't close (a public commenter would get to issue instructions inside the agent's perimeter on every launch). The available mitigations — commenter allowlists, prompt-injection regex screens, private-repo defaults — are all soft. The durable defense is to keep the human between the review comment and any next agent prompt. So `supervise` is now strictly notify-only. The `auto_respawn` manifest field, the "with auto_respawn: true" behavior paragraph, and the matching trust-model edge case all go. The reasoning stays in the "Where to be conservative" bullet so the decision isn't re-litigated later. --- docs/research/built-in-supervisor-design.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/research/built-in-supervisor-design.md b/docs/research/built-in-supervisor-design.md index 9d3313d..724ef41 100644 --- a/docs/research/built-in-supervisor-design.md +++ b/docs/research/built-in-supervisor-design.md @@ -47,10 +47,9 @@ The optional, more ambitious layer. The bottle manifest gains an optional field: pr_watch: upstream: gitea.dideric.is/didericis/myproject branch: agent/task-42 -auto_respawn: false # opt-in; default notify-only ``` -`./cli.py supervise` polls the named upstream for new review comments and CI failures on `branch`. When one fires, default behavior is a desktop notification (or a flash in the TUI). With `auto_respawn: true`, the supervisor tears down the bottle and re-runs it with the feedback prepended to the next prompt. +`./cli.py supervise` polls the named upstream for new review comments and CI failures on `branch`. When one fires, it surfaces as a desktop notification or a flash in the TUI. The human decides what to do with the feedback — there is no autonomous loop that feeds the comment back into a bottle's next prompt (see "Where to be conservative" for why). The polling token is a **host** token (the same `GH_PAT` / Gitea token the host already keeps in shell env), not a bottle credential. The supervisor never holds bottle secrets. @@ -73,7 +72,7 @@ This also doesn't conflict with the "lean on git history for auditing" non-goal. A few design defaults worth holding: -- **Default to notify, not auto-respawn.** Review-comment → re-run is a fully autonomous flow. Make it opt-in per bottle. The notify-only mode is safe enough to be the default; the auto-respawn mode crosses into agent-autonomy territory that deserves an explicit decision per agent. +- **No auto-respawn.** The supervisor surfaces PR feedback to a human, never to the bottle's next prompt. The autonomous flow (review-comment → tear down → relaunch with the comment prepended) was considered and rejected: in a public-ish repo, any commenter could inject content that the next launch would treat as system instructions, with the agent's full bottle privileges. Available mitigations — commenter allowlists, prompt-injection regex screens, private-repo defaults — are all soft. The load-bearing defense is to keep the human between the review comment and any agent prompt. Notify-only is the only mode. - **Idle detection is harder than it looks.** Last-log-line-age works ~80% of the time. Codeman's Ralph Loop tracker (watching for `` tags) is more accurate but adds complexity and tooling-coupling. Start with the dumb version; add heuristics only when actual confusion arises. - **No web UI.** A browser UI reintroduces the privileged-channel problem — the browser talks to a server that talks to all bottles. TUI sidesteps it because the supervisor runs in the user's own shell context, not as a long-running daemon serving multiple consumers. - **State file in `~/.claude-bottle/`, not inside any bottle.** The mapping of bottle → PR → status lives next to the manifest. Nothing about the supervisor's bookkeeping enters a bottle. @@ -82,7 +81,6 @@ A few design defaults worth holding: ## Trust-model edge cases worth flagging - **Cross-host supervisor.** If the supervisor ever grows to coordinate bottles on multiple hosts, the trust analysis changes — the polling token now has to travel, and the "host" boundary is no longer one machine. Out of scope for v1; flag in any future design doc that contemplates it. -- **Auto-respawn with attacker-controlled review content.** If `auto_respawn: true` is on and the upstream is one where untrusted parties can post review comments, those comments become a prompt-injection vector. Mitigations: limit auto-respawn to private repos by default; document the threat; consider a per-route allowlist of trusted commenters. - **Native Claude Code OpenTelemetry as an alternative observability path.** Setting `CLAUDE_CODE_ENABLE_TELEMETRY=1` inside a bottle would emit OTel data — but the bottle's pipelock allowlist must then include the OTel collector's host, and the trace data is generated by the (untrusted) agent rather than by the (trusted) host-side perimeter. The supervisor-via-Docker-plus-pipelock-logs path is the cleaner observability spine for this project's threat model; in-bottle OTel is an extra signal worth weighing case-by-case, not the default. ## Scope estimate @@ -95,4 +93,4 @@ Phased: `status` first (purely additive, no design decisions), then `watch` (the A supervisor that respects the bottle wall is a small natural extension of what claude-bottle already is, not a category shift toward Mission Control / Codeman / Composio AO. The mistake in earlier framing was treating "supervisor" as synonymous with "dashboard SPA." The trust-model question that disqualifies the SPA tier (privileged channel into every bottle) does not apply to a TUI that reads host-side signals and shells out to the existing CLI. -Recommendation: build `status` and `watch` opportunistically when the pain is felt; treat `supervise` as a separate PRD before implementation, with `auto_respawn` defaulting to off. +Recommendation: build `status` and `watch` opportunistically when the pain is felt; treat `supervise` as a separate PRD before implementation, scoped to notify-only (no autonomous loop from review comment to next agent prompt — see "Where to be conservative"). -- 2.52.0 From 49082dfadff26d4d699a52ff548ba7cd522351f3 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 01:36:29 -0400 Subject: [PATCH 08/12] docs(prd-0012): adopt text-only notify protocol + SIGHUP routes reload Rewrites Scope, Proposed Design, Data model, and Open questions to match the model where /supervise/notify is text-in/text-out, routes edits + SIGHUP reload are supervisor-side tooling, and manifest rebuilds are the heavy path. Adds the per-bottle routes-edit audit log. Co-Authored-By: Claude Opus 4.7 --- docs/prds/0012-stuck-agent-recovery-flow.md | 48 +++++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/docs/prds/0012-stuck-agent-recovery-flow.md b/docs/prds/0012-stuck-agent-recovery-flow.md index 41a142a..db493a4 100644 --- a/docs/prds/0012-stuck-agent-recovery-flow.md +++ b/docs/prds/0012-stuck-agent-recovery-flow.md @@ -6,7 +6,7 @@ ## Summary -When an agent running inside a claude-bottle container gets blocked by a missing permission, tool, or skill, it asks for help via a PR comment; the user approves a manifest change in a TUI dashboard; the orchestrator rebuilds the container from the new manifest and resumes work on the same branch — without ever opening a live channel into the running bottle. +When an agent running inside a claude-bottle container gets blocked, it signals via the per-bottle cred-proxy sidecar's `/supervise/notify` endpoint. The supervisor sees the message in a host-side TUI and responds with either a text hint (resolves the block in-place, the agent continues), a cred-proxy config swap (supervisor edits `routes.json`, SIGHUP-reloads cred-proxy, replies with a "try again" hint), or — for the heavier case where the bottle's manifest itself needs to change — an approved manifest diff that triggers a rebuild of the bottle on the same branch. The supervisor never opens a live channel into a running bottle; all signal flow goes through the existing internal-network endpoint that cred-proxy already terminates. ## Problem @@ -27,11 +27,15 @@ A real stuck agent recovers end-to-end through the flow: the agent hits a missin ### In scope -- A `/request-bottle-change` slash command the agent invokes when it knows it's blocked. -- A TUI dashboard that lists running bottles and pending change requests, and takes approve/reject input from the user. -- A rebuild orchestrator that tears down the old bottle, applies the approved manifest change, and starts a replacement bottle on the same branch. -- A state-preservation helper that carries forward what it can across the rebuild (working tree is mandatory; transcript / reasoning context is best-effort). -- A stuck-signal mechanism that does not require a forge token inside the bottle: the agent's slash command sends the request to the existing cred-proxy endpoint, which (with a host-mounted volume) writes the sentinel artifact on the host side. The orchestrator polls that artifact and posts the PR comment from outside the bottle. +- A `/stuck` slash command the agent invokes when blocked. POSTs free-text to cred-proxy's `/supervise/notify` and blocks awaiting a text reply. +- A `/supervise/notify` endpoint on cred-proxy that persists the agent's message host-side and holds the agent's connection open until the supervisor responds. Wire protocol is text-only: request is the agent's message; response is `{text: "..."}`. +- SIGHUP-based hot reload of `routes.json` on cred-proxy, so the supervisor can change the agent's credential surface without restarting the proxy or dropping in-flight calls. +- A TUI dashboard that lists running bottles and pending stuck-notifications. Two verbs: + - `r ` — reply to a pending stuck message (delivers text to the waiting agent). + - `routes edit ` — open the bottle's `routes.json` in `$EDITOR`, SIGHUP cred-proxy on save. Not gated on a pending message; the supervisor can edit routes anytime. +- A host-side audit log at `~/.claude-bottle/audit/cred-proxy-.log` that records every `routes.json` edit: timestamp, diff before/after, the operator's reply text if the edit was tied to a `/stuck` reply. Logs route shape, never secret values. +- A rebuild path for the heavier case where the bottle's *manifest* (not just routes) must change. Orchestrator tears down the bottle, applies the approved manifest diff, and starts a replacement bottle on the same branch. +- A state-preservation helper for the rebuild path: working tree push is mandatory; transcript / reasoning context is best-effort. ### Out of scope @@ -45,22 +49,26 @@ A real stuck agent recovers end-to-end through the flow: the agent hits a missin ### New services / components -- **`/request-bottle-change` slash command.** Shipped as a skill mounted into bottles. When the agent invokes it, the command POSTs a structured request (what's needed, why, what was tried) to the cred-proxy endpoint and halts the agent. The agent never touches the host filesystem. -- **TUI dashboard.** A `claude-bottle dashboard` (or similarly named) command that lists running bottles, surfaces pending change requests, shows the proposed manifest diff, and accepts approve/reject input. Targets stdlib only; a TUI library is added only if the experience truly demands it. -- **Rebuild orchestrator.** The plumbing that, on approval, tears down the existing bottle, applies the approved manifest change, snapshots state via the state-preservation helper, and starts a fresh bottle on the same branch. +- **`/stuck` slash command.** Shipped as a skill mounted into bottles. POSTs the agent's free-text message to cred-proxy's `/supervise/notify` and blocks awaiting a text reply. Reply text is handed back to the agent verbatim — the agent doesn't need to know whether the supervisor edited routes, opened an editor, or did anything else before composing the reply. +- **cred-proxy `/supervise/notify` endpoint.** Receives the agent's message, persists it to a host-mounted queue, and holds the agent's connection open until the supervisor responds. The wire protocol is text-only in both directions; the supervisor's side-effects (routes edit, manifest diff, no-op) are invisible to the agent. +- **cred-proxy SIGHUP reload.** New behavior on the existing process: SIGHUP re-reads `routes.json` without dropping connections or breaking in-flight calls. ~30 lines added to the server. +- **TUI dashboard.** A `claude-bottle dashboard` (or similarly named) command. Lists running bottles, surfaces pending stuck-notifications, exposes the `r ` and `routes edit ` verbs, and (for the rebuild path) shows proposed manifest diffs with approve/reject input. Targets stdlib only; a TUI library is added only if the experience truly demands it. +- **Routes-edit audit log.** `~/.claude-bottle/audit/cred-proxy-.log`. Every `routes.json` edit appends: timestamp, diff of routes before/after, operator's reply text if tied to a `/stuck` reply. Records what the bottle's credential surface looked like at time T without storing the secret values themselves. +- **Rebuild orchestrator (heavy path).** Used when the manifest itself must change, not just routes. On approval, tears down the existing bottle, applies the approved manifest diff, snapshots state via the state-preservation helper, and starts a fresh bottle on the same branch. - **State-preservation helper.** Mandatory: ensures the working tree is pushed before teardown. Best-effort: carries forward the agent's transcript / reasoning context into the replacement container so the new agent starts warm rather than cold. ### Existing code touched -- **cred-proxy** (PRD 0010) — extended with an endpoint that accepts stuck-requests from inside a bottle and writes the sentinel artifact to a host-mounted volume. -- **`cli.py`** — gains the dashboard subcommand and the rebuild path. +- **cred-proxy** (PRD 0010) — gains the `/supervise/notify` endpoint, the host-mounted notification queue, and SIGHUP reload of `routes.json`. +- **`cli.py`** — gains the dashboard subcommand (with `r` and `routes edit` verbs) and the rebuild path. - **Bottle lifecycle scripts** — extended for orchestrated teardown + rebuild with state hand-off, distinct from a fresh-spawn. - **Bottle manifest schema** — may need to record the originating manifest version / change history per agent run, so the dashboard can show "what changed" rather than "what is." ### Data model changes -- A new stuck-request artifact (probably JSON) written by the cred-proxy on behalf of the agent, with whatever fields the dashboard needs to render the ask. -- A per-agent-run record sufficient to map a running bottle back to its PR / branch, so the orchestrator knows where to post the comment and which branch to resume on. +- A per-bottle pending-notification queue: cred-proxy holds the agent's open connection; the queue holds the metadata (id, bottle slug, message body, arrival timestamp) the TUI needs to render the ask. +- A per-bottle `routes.json` audit log file at `~/.claude-bottle/audit/cred-proxy-.log`, append-only. +- A per-agent-run record sufficient to map a running bottle back to its PR / branch, so the rebuild orchestrator knows where to post the comment and which branch to resume on. ### External dependencies @@ -69,12 +77,14 @@ A real stuck agent recovers end-to-end through the flow: the agent hits a missin ## Open questions -- What exactly does best-effort transcript preservation look like? Mount the agent's state directory, snapshot on teardown, remount in the replacement? How much fidelity is "good enough" for the new agent to pick up? -- Should v1 also ship the tool-denial hook (auto-detect stuck), or strictly the agent-initiated slash command? Currently deferred, but the line is worth confirming during implementation. -- How does the dashboard handle rejection? Does the agent get a comment back saying "denied, here's why," or does the bottle just stay torn down? -- How does the orchestrator know which PR / branch a given bottle maps to — recorded at bottle-spawn time, derived from the working tree, or specified in the manifest? -- Concurrency: if multiple bottles request changes simultaneously, what does the dashboard surface and in what order? -- How does the flow handle one-off exceptions to gitlock / pipelock denials — e.g. a commit that includes docs with intentionally-bogus tokens that the secret scanner correctly flags? The shape (agent blocked → ask via PR comment → user approves → continue) is the same as a manifest-change request, but the *resolution* is different: a per-operation override or a scoped allowlist entry, not a new manifest. Does this fold into the same `/request-bottle-change` slash command with a different request type, or is it a separate slash command (e.g. `/request-gate-exception`)? And how is an "exception" expressed safely — by commit SHA, by content hash, by a narrow allowlist rule? Either way, the approval must be auditable so a future reader can see what was waived and why. See `docs/research/git-gate-commit-approval.md` for a survey of gitleaks's native allowlist primitives and a recommendation. +- SIGHUP race window. An agent that retries within msec of the SIGHUP may hit old routes once before the reload completes, fail, and retry against the new routes. Assumption is that normal HTTP retry semantics absorb this; worth confirming under real usage rather than designing around it preemptively. +- Multiple pending notifications from the same bottle. If the agent calls `/stuck` again before the prior message is answered, what does the queue do — replace, append, or refuse? Append feels safest; replace is wrong (loses context); refuse forces the agent to handle a new error mode. +- Verb naming under load. `r ` optimizes for muscle memory mid-incident; `reply ` reads better cold. Worth picking once and committing. +- Best-effort transcript preservation on the rebuild path. Mount the agent's state directory, snapshot on teardown, remount in the replacement? How much fidelity is "good enough" for the new agent to pick up? +- Tool-denial auto-detection. Should v1 also ship a tool-denial hook that auto-invokes `/stuck` without the agent's involvement, or strictly the agent-initiated form? Currently deferred; line worth confirming during implementation. +- Rejection semantics on the rebuild path. Does the agent receive a `/stuck` reply explaining the rejection, or does the bottle just stay torn down? +- Bottle → PR/branch mapping. Recorded at bottle-spawn time, derived from the working tree, or specified in the manifest? +- How does the flow handle one-off exceptions to gitlock / pipelock denials — e.g. a commit that includes docs with intentionally-bogus tokens that the secret scanner correctly flags? The shape (agent blocked → `/stuck` → operator decides → reply) is the same, but the *resolution* differs: a per-operation override or a scoped allowlist entry, not a routes edit or a manifest change. Does the operator express the exception by commit SHA, by content hash, or by a narrow allowlist rule? Either way, the approval must be auditable so a future reader can see what was waived and why. See `docs/research/git-gate-commit-approval.md` for a survey of gitleaks's native allowlist primitives and a recommendation. ## References -- 2.52.0 From e5a4c324a0ba36b78f314fd7ba821e7ebabf7b88 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 01:47:24 -0400 Subject: [PATCH 09/12] docs(prd-0012): name the three stuck categories and add pipelock path Introduces cred-proxy block, pipelock block, and capability gap as the three named categories of stuck. Adds pipelock-edit support (restart- based for v1) parallel to the existing cred-proxy routes-edit path, plus a pipelock audit log. Broadens Goals to cover all three paths. Co-Authored-By: Claude Opus 4.7 --- docs/prds/0012-stuck-agent-recovery-flow.md | 34 +++++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/docs/prds/0012-stuck-agent-recovery-flow.md b/docs/prds/0012-stuck-agent-recovery-flow.md index db493a4..96a50de 100644 --- a/docs/prds/0012-stuck-agent-recovery-flow.md +++ b/docs/prds/0012-stuck-agent-recovery-flow.md @@ -6,7 +6,7 @@ ## Summary -When an agent running inside a claude-bottle container gets blocked, it signals via the per-bottle cred-proxy sidecar's `/supervise/notify` endpoint. The supervisor sees the message in a host-side TUI and responds with either a text hint (resolves the block in-place, the agent continues), a cred-proxy config swap (supervisor edits `routes.json`, SIGHUP-reloads cred-proxy, replies with a "try again" hint), or — for the heavier case where the bottle's manifest itself needs to change — an approved manifest diff that triggers a rebuild of the bottle on the same branch. The supervisor never opens a live channel into a running bottle; all signal flow goes through the existing internal-network endpoint that cred-proxy already terminates. +When an agent running inside a claude-bottle container gets blocked, it signals via the per-bottle cred-proxy sidecar's `/supervise/notify` endpoint. The supervisor sees the message in a host-side TUI and responds with one of four shapes: a text hint (no infrastructure change, the agent continues); a cred-proxy routes edit (SIGHUP-reload of cred-proxy, agent retries); a pipelock allowlist edit (restart pipelock, agent retries); or an approved manifest diff that triggers a full rebuild of the bottle on the same branch. These map to three categories of stuck — **cred-proxy block**, **pipelock block**, and **capability gap** — described below. The supervisor never opens a live channel into a running bottle; all signal flow goes through the existing internal-network endpoint that cred-proxy already terminates. ## Problem @@ -14,7 +14,7 @@ Running parallel agents in isolated bottles makes it cheap to spin up work in pa ## Goals / Success Criteria -A real stuck agent recovers end-to-end through the flow: the agent hits a missing permission, posts a PR comment describing the ask, the user reviews the request in a dashboard, approves a manifest diff, and a fresh bottle picks up on the same branch and continues. The whole loop completes without anyone running `docker attach` or opening any live channel into the original container. +A real stuck agent recovers end-to-end in each of the three categories: a **cred-proxy block** is fixed by a `routes edit` + SIGHUP and a "retry now" reply without restarting anything; a **pipelock block** is fixed by an allowlist edit + pipelock restart and a "retry now" reply; a **capability gap** triggers a manifest-diff approval and a bottle rebuild that picks up on the same branch. All three complete without anyone running `docker attach` or opening any live channel into the original container. ## Non-goals @@ -33,8 +33,9 @@ A real stuck agent recovers end-to-end through the flow: the agent hits a missin - A TUI dashboard that lists running bottles and pending stuck-notifications. Two verbs: - `r ` — reply to a pending stuck message (delivers text to the waiting agent). - `routes edit ` — open the bottle's `routes.json` in `$EDITOR`, SIGHUP cred-proxy on save. Not gated on a pending message; the supervisor can edit routes anytime. -- A host-side audit log at `~/.claude-bottle/audit/cred-proxy-.log` that records every `routes.json` edit: timestamp, diff before/after, the operator's reply text if the edit was tied to a `/stuck` reply. Logs route shape, never secret values. -- A rebuild path for the heavier case where the bottle's *manifest* (not just routes) must change. Orchestrator tears down the bottle, applies the approved manifest diff, and starts a replacement bottle on the same branch. +- Equivalent support for pipelock: a `pipelock edit ` TUI verb that opens pipelock's allowlist in `$EDITOR` and restarts pipelock on save. (v1 uses restart, not SIGHUP — see Open questions.) +- Host-side audit logs at `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log` that record every config edit: timestamp, diff before/after, the operator's reply text if the edit was tied to a `/stuck` reply. Records config shape, never secret values. +- A rebuild path for the **capability gap** case where the bottle's *manifest* (not just routes or pipelock allowlist) must change. Orchestrator tears down the bottle, applies the approved manifest diff, and starts a replacement bottle on the same branch. - A state-preservation helper for the rebuild path: working tree push is mandatory; transcript / reasoning context is best-effort. ### Out of scope @@ -47,27 +48,39 @@ A real stuck agent recovers end-to-end through the flow: the agent hits a missin ## Proposed Design +### Stuck categories + +Three named categories, ordered by remediation cost: + +- **cred-proxy block.** The agent's request was refused by cred-proxy — missing route, expired token, wrong scope. The bottle is otherwise healthy. *Remediation:* operator runs `routes edit `, edits `routes.json`, saves. cred-proxy SIGHUP-reloads; in-flight connections are not dropped. Operator replies to the `/stuck` message with a "retry now" hint. The agent retries against the (now-reloaded) cred-proxy and proceeds. +- **pipelock block.** The agent's outbound request was refused by pipelock — host not in the allowlist, protocol not permitted, etc. The bottle is otherwise healthy, but the egress perimeter is wrong. *Remediation:* operator runs `pipelock edit `, edits the allowlist, saves. pipelock restarts; the agent's in-flight outbound calls may drop and need retry. Operator replies to the `/stuck` message with a "retry now" hint. (v1 uses restart; SIGHUP reload for pipelock is an Open question.) +- **capability gap.** The bottle is missing something the agent needs that lives in the manifest itself — a tool, a skill, a permission grant, an env var. Routes and pipelock are correct; the agent container just doesn't have the capability. *Remediation:* operator approves a manifest diff in the TUI. The rebuild orchestrator tears down the bottle, applies the diff, and starts a replacement bottle on the same branch via the state-preservation helper. The replacement agent picks up where the original was, now with the missing capability. + +The wire protocol does not change between categories: the agent POSTs free text to `/supervise/notify` and receives `{text: "..."}`. The category is the operator's mental model for triage, not a field on the request. The agent does not need to know which category its message will fall into. + ### New services / components - **`/stuck` slash command.** Shipped as a skill mounted into bottles. POSTs the agent's free-text message to cred-proxy's `/supervise/notify` and blocks awaiting a text reply. Reply text is handed back to the agent verbatim — the agent doesn't need to know whether the supervisor edited routes, opened an editor, or did anything else before composing the reply. - **cred-proxy `/supervise/notify` endpoint.** Receives the agent's message, persists it to a host-mounted queue, and holds the agent's connection open until the supervisor responds. The wire protocol is text-only in both directions; the supervisor's side-effects (routes edit, manifest diff, no-op) are invisible to the agent. -- **cred-proxy SIGHUP reload.** New behavior on the existing process: SIGHUP re-reads `routes.json` without dropping connections or breaking in-flight calls. ~30 lines added to the server. -- **TUI dashboard.** A `claude-bottle dashboard` (or similarly named) command. Lists running bottles, surfaces pending stuck-notifications, exposes the `r ` and `routes edit ` verbs, and (for the rebuild path) shows proposed manifest diffs with approve/reject input. Targets stdlib only; a TUI library is added only if the experience truly demands it. -- **Routes-edit audit log.** `~/.claude-bottle/audit/cred-proxy-.log`. Every `routes.json` edit appends: timestamp, diff of routes before/after, operator's reply text if tied to a `/stuck` reply. Records what the bottle's credential surface looked like at time T without storing the secret values themselves. -- **Rebuild orchestrator (heavy path).** Used when the manifest itself must change, not just routes. On approval, tears down the existing bottle, applies the approved manifest diff, snapshots state via the state-preservation helper, and starts a fresh bottle on the same branch. +- **cred-proxy SIGHUP reload.** New behavior on the existing process: SIGHUP re-reads `routes.json` without dropping connections or breaking in-flight calls. ~30 lines added to the server. Used by the **cred-proxy block** category. +- **pipelock edit + restart.** v1 ships restart-based reload for pipelock: on `pipelock edit ` save, the supervisor writes the new allowlist and restarts the pipelock container. The agent's in-flight outbound calls drop and rely on retry. Used by the **pipelock block** category. +- **TUI dashboard.** A `claude-bottle dashboard` (or similarly named) command. Lists running bottles, surfaces pending stuck-notifications, exposes the `r `, `routes edit `, and `pipelock edit ` verbs, and (for the **capability gap** category) shows proposed manifest diffs with approve/reject input. Targets stdlib only; a TUI library is added only if the experience truly demands it. +- **Config-edit audit logs.** `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`. Every edit appends: timestamp, diff before/after, operator's reply text if tied to a `/stuck` reply. Records what the bottle's credential surface and egress perimeter looked like at time T without storing secret values. +- **Rebuild orchestrator (capability-gap path).** Used when the manifest itself must change, not just routes or the pipelock allowlist. On approval, tears down the existing bottle, applies the approved manifest diff, snapshots state via the state-preservation helper, and starts a fresh bottle on the same branch. - **State-preservation helper.** Mandatory: ensures the working tree is pushed before teardown. Best-effort: carries forward the agent's transcript / reasoning context into the replacement container so the new agent starts warm rather than cold. ### Existing code touched - **cred-proxy** (PRD 0010) — gains the `/supervise/notify` endpoint, the host-mounted notification queue, and SIGHUP reload of `routes.json`. -- **`cli.py`** — gains the dashboard subcommand (with `r` and `routes edit` verbs) and the rebuild path. +- **pipelock** — gains a clean restart path that picks up the new allowlist on container restart. No code changes likely needed if pipelock already reads its config on startup; the orchestration is supervisor-side. +- **`cli.py`** — gains the dashboard subcommand (with `r`, `routes edit`, and `pipelock edit` verbs) and the rebuild path. - **Bottle lifecycle scripts** — extended for orchestrated teardown + rebuild with state hand-off, distinct from a fresh-spawn. - **Bottle manifest schema** — may need to record the originating manifest version / change history per agent run, so the dashboard can show "what changed" rather than "what is." ### Data model changes - A per-bottle pending-notification queue: cred-proxy holds the agent's open connection; the queue holds the metadata (id, bottle slug, message body, arrival timestamp) the TUI needs to render the ask. -- A per-bottle `routes.json` audit log file at `~/.claude-bottle/audit/cred-proxy-.log`, append-only. +- Per-bottle config audit log files at `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`, append-only. - A per-agent-run record sufficient to map a running bottle back to its PR / branch, so the rebuild orchestrator knows where to post the comment and which branch to resume on. ### External dependencies @@ -78,6 +91,7 @@ A real stuck agent recovers end-to-end through the flow: the agent hits a missin ## Open questions - SIGHUP race window. An agent that retries within msec of the SIGHUP may hit old routes once before the reload completes, fail, and retry against the new routes. Assumption is that normal HTTP retry semantics absorb this; worth confirming under real usage rather than designing around it preemptively. +- SIGHUP reload for pipelock. v1 ships restart-based reload, which drops in-flight outbound calls. Should pipelock gain SIGHUP support so **pipelock block** is as cheap as **cred-proxy block**? Depends on how often the operator edits the allowlist mid-task and how disruptive a pipelock bounce actually is. - Multiple pending notifications from the same bottle. If the agent calls `/stuck` again before the prior message is answered, what does the queue do — replace, append, or refuse? Append feels safest; replace is wrong (loses context); refuse forces the agent to handle a new error mode. - Verb naming under load. `r ` optimizes for muscle memory mid-incident; `reply ` reads better cold. Worth picking once and committing. - Best-effort transcript preservation on the rebuild path. Mount the agent's state directory, snapshot on teardown, remount in the replacement? How much fidelity is "good enough" for the new agent to pick up? -- 2.52.0 From c71713e7d3bbff8edbc8565ba91abfa72cf4cf56 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 02:53:26 -0400 Subject: [PATCH 10/12] docs(prd-0012): switch /stuck to three structured MCP tool calls Replaces the text-only /supervise/notify protocol with three MCP tools the agent calls directly: cred-proxy-block, pipelock-block, and capability-block. Each tool carries the agent's proposed config file (routes.json, pipelock allowlist, or Dockerfile) plus a justification. Adds a new MCP sidecar, a read-only current-config mount in the agent container, and renames "capability gap" to "capability block" to match the tool name. The text-only-vs-structured tradeoff is captured as an Open question with pros/cons on both sides. Co-Authored-By: Claude Opus 4.7 --- docs/prds/0012-stuck-agent-recovery-flow.md | 77 +++++++++++---------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/docs/prds/0012-stuck-agent-recovery-flow.md b/docs/prds/0012-stuck-agent-recovery-flow.md index 96a50de..9162f63 100644 --- a/docs/prds/0012-stuck-agent-recovery-flow.md +++ b/docs/prds/0012-stuck-agent-recovery-flow.md @@ -6,7 +6,7 @@ ## Summary -When an agent running inside a claude-bottle container gets blocked, it signals via the per-bottle cred-proxy sidecar's `/supervise/notify` endpoint. The supervisor sees the message in a host-side TUI and responds with one of four shapes: a text hint (no infrastructure change, the agent continues); a cred-proxy routes edit (SIGHUP-reload of cred-proxy, agent retries); a pipelock allowlist edit (restart pipelock, agent retries); or an approved manifest diff that triggers a full rebuild of the bottle on the same branch. These map to three categories of stuck — **cred-proxy block**, **pipelock block**, and **capability gap** — described below. The supervisor never opens a live channel into a running bottle; all signal flow goes through the existing internal-network endpoint that cred-proxy already terminates. +When an agent running inside a claude-bottle container gets blocked, it invokes one of three MCP tool calls — `cred-proxy-block`, `pipelock-block`, or `capability-block` — passing a *proposed* config change (modified `routes.json`, modified pipelock allowlist, or modified agent Dockerfile) plus text describing why the change is justified. The supervisor sees the proposal in a host-side TUI, approves / modifies / rejects it, and the corresponding remediation runs: SIGHUP-reload cred-proxy with the new routes; restart pipelock with the new allowlist; rebuild the bottle from the new Dockerfile on the same branch. The agent's tool call blocks until the operator acts. The supervisor never opens a live channel into a running bottle; all signal flow goes through a per-bottle MCP sidecar on the existing internal network. ## Problem @@ -14,7 +14,7 @@ Running parallel agents in isolated bottles makes it cheap to spin up work in pa ## Goals / Success Criteria -A real stuck agent recovers end-to-end in each of the three categories: a **cred-proxy block** is fixed by a `routes edit` + SIGHUP and a "retry now" reply without restarting anything; a **pipelock block** is fixed by an allowlist edit + pipelock restart and a "retry now" reply; a **capability gap** triggers a manifest-diff approval and a bottle rebuild that picks up on the same branch. All three complete without anyone running `docker attach` or opening any live channel into the original container. +A real stuck agent recovers end-to-end in each of the three categories: a **cred-proxy block** is fixed by the operator approving the agent's proposed `routes.json`, SIGHUP-reloading cred-proxy, and the tool returning "approved, retry now"; a **pipelock block** is fixed by the operator approving the proposed allowlist, restarting pipelock, and the tool returning "approved, retry now"; a **capability block** triggers a bottle rebuild from the proposed Dockerfile, with the replacement agent picking up on the same branch. All three complete without anyone running `docker attach` or opening any live channel into the original container. ## Non-goals @@ -27,20 +27,19 @@ A real stuck agent recovers end-to-end in each of the three categories: a **cred ### In scope -- A `/stuck` slash command the agent invokes when blocked. POSTs free-text to cred-proxy's `/supervise/notify` and blocks awaiting a text reply. -- A `/supervise/notify` endpoint on cred-proxy that persists the agent's message host-side and holds the agent's connection open until the supervisor responds. Wire protocol is text-only: request is the agent's message; response is `{text: "..."}`. -- SIGHUP-based hot reload of `routes.json` on cred-proxy, so the supervisor can change the agent's credential surface without restarting the proxy or dropping in-flight calls. -- A TUI dashboard that lists running bottles and pending stuck-notifications. Two verbs: - - `r ` — reply to a pending stuck message (delivers text to the waiting agent). - - `routes edit ` — open the bottle's `routes.json` in `$EDITOR`, SIGHUP cred-proxy on save. Not gated on a pending message; the supervisor can edit routes anytime. -- Equivalent support for pipelock: a `pipelock edit ` TUI verb that opens pipelock's allowlist in `$EDITOR` and restarts pipelock on save. (v1 uses restart, not SIGHUP — see Open questions.) -- Host-side audit logs at `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log` that record every config edit: timestamp, diff before/after, the operator's reply text if the edit was tied to a `/stuck` reply. Records config shape, never secret values. -- A rebuild path for the **capability gap** case where the bottle's *manifest* (not just routes or pipelock allowlist) must change. Orchestrator tears down the bottle, applies the approved manifest diff, and starts a replacement bottle on the same branch. +- A per-bottle MCP sidecar on the bottle's internal network that exposes three tools to the agent: `cred-proxy-block`, `pipelock-block`, `capability-block`. Each tool blocks until the operator acts. +- Tool input schemas: each tool takes the full proposed file (a new `routes.json`, a new pipelock allowlist, or a new Dockerfile, respectively) plus a `justification` text field. Tool output: `{status: "approved"|"modified"|"rejected", notes: "..."}`. +- A read-only mount at `/etc/claude-bottle/current-config/` in the agent container that exposes the current `routes.json`, pipelock allowlist, and Dockerfile, so the agent can read them before composing a proposed change. +- SIGHUP-based hot reload of `routes.json` on cred-proxy, so an approved **cred-proxy block** proposal takes effect without restarting the proxy or dropping in-flight calls. +- A clean restart path on pipelock that picks up a new allowlist on container restart; v1 does not ship SIGHUP reload for pipelock (see Open questions). +- A TUI dashboard that lists running bottles and pending tool-call proposals. For each pending proposal: shows the diff (current → proposed) and the agent's justification. Operator actions: approve, modify-then-approve (edit the proposed diff before applying), reject with reason. Also exposes proactive operator-initiated edits: `routes edit `, `pipelock edit ` — useful when the operator wants to change config without waiting for an agent prompt. +- Host-side audit logs at `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`. Each entry: timestamp, diff before/after, the agent's justification text (if the edit came from a tool call), and the operator's action (approve / modify / reject) with notes. Records config shape, never secret values. +- A rebuild path for the **capability block** case where the agent Dockerfile must change. Orchestrator tears down the bottle, applies the approved Dockerfile, and starts a replacement bottle on the same branch. - A state-preservation helper for the rebuild path: working tree push is mandatory; transcript / reasoning context is best-effort. ### Out of scope -- A tool-denial hook that auto-detects "stuck" without the agent's involvement. Deferred to a follow-up; v1 is opt-in via the slash command. +- A tool-denial hook that auto-detects "stuck" without the agent's involvement. Deferred to a follow-up; v1 is opt-in via the agent calling one of the three MCP tools. - A web dashboard. TUI only in v1. - Live channel into running containers (see Non-goals). - Agent-to-agent communication (see Non-goals). @@ -50,57 +49,63 @@ A real stuck agent recovers end-to-end in each of the three categories: a **cred ### Stuck categories -Three named categories, ordered by remediation cost: +Three named categories, each with its own MCP tool. Ordered by remediation cost: -- **cred-proxy block.** The agent's request was refused by cred-proxy — missing route, expired token, wrong scope. The bottle is otherwise healthy. *Remediation:* operator runs `routes edit `, edits `routes.json`, saves. cred-proxy SIGHUP-reloads; in-flight connections are not dropped. Operator replies to the `/stuck` message with a "retry now" hint. The agent retries against the (now-reloaded) cred-proxy and proceeds. -- **pipelock block.** The agent's outbound request was refused by pipelock — host not in the allowlist, protocol not permitted, etc. The bottle is otherwise healthy, but the egress perimeter is wrong. *Remediation:* operator runs `pipelock edit `, edits the allowlist, saves. pipelock restarts; the agent's in-flight outbound calls may drop and need retry. Operator replies to the `/stuck` message with a "retry now" hint. (v1 uses restart; SIGHUP reload for pipelock is an Open question.) -- **capability gap.** The bottle is missing something the agent needs that lives in the manifest itself — a tool, a skill, a permission grant, an env var. Routes and pipelock are correct; the agent container just doesn't have the capability. *Remediation:* operator approves a manifest diff in the TUI. The rebuild orchestrator tears down the bottle, applies the diff, and starts a replacement bottle on the same branch via the state-preservation helper. The replacement agent picks up where the original was, now with the missing capability. +- **cred-proxy block.** Tool: `cred-proxy-block`. The agent's request was refused by cred-proxy — missing route, expired token, wrong scope. The agent reads the current `routes.json` from `/etc/claude-bottle/current-config/`, composes a modified version, and calls the tool with `{routes: , justification: "..."}`. The operator reviews the diff in the TUI; on approval, the supervisor writes the new `routes.json` and cred-proxy SIGHUP-reloads. In-flight connections are not dropped. The tool returns `{status: "approved", notes: "..."}` and the agent retries. +- **pipelock block.** Tool: `pipelock-block`. The agent's outbound request was refused by pipelock — host not in the allowlist, protocol not permitted. The agent reads the current allowlist, composes a modified version, and calls the tool with `{allowlist: , justification: "..."}`. On approval, the supervisor writes the new allowlist and restarts pipelock; in-flight outbound calls may drop and rely on retry. The tool returns the same approve/reject shape. +- **capability block.** Tool: `capability-block`. The bottle is missing a tool, skill, permission, or env var the agent needs — something that lives in the agent Dockerfile rather than in routes or the pipelock allowlist. The agent reads the current Dockerfile, composes a modified version, and calls the tool with `{dockerfile: , justification: "..."}`. On approval, the rebuild orchestrator tears down the bottle, builds from the new Dockerfile, and starts a replacement bottle on the same branch via the state-preservation helper. Because the current agent is about to be replaced, the tool's return is best-effort — the replacement agent inherits the approval record via the preserved transcript. -The wire protocol does not change between categories: the agent POSTs free text to `/supervise/notify` and receives `{text: "..."}`. The category is the operator's mental model for triage, not a field on the request. The agent does not need to know which category its message will fall into. +The three tools are dispatched by name, so the operator's TUI knows which remediation engine to wire to which proposal. The agent must choose the right tool for what failed: a 403 from a credentialed request is a `cred-proxy-block`; a connection refused at the egress is a `pipelock-block`; a "command not found" or missing-skill error is a `capability-block`. ### New services / components -- **`/stuck` slash command.** Shipped as a skill mounted into bottles. POSTs the agent's free-text message to cred-proxy's `/supervise/notify` and blocks awaiting a text reply. Reply text is handed back to the agent verbatim — the agent doesn't need to know whether the supervisor edited routes, opened an editor, or did anything else before composing the reply. -- **cred-proxy `/supervise/notify` endpoint.** Receives the agent's message, persists it to a host-mounted queue, and holds the agent's connection open until the supervisor responds. The wire protocol is text-only in both directions; the supervisor's side-effects (routes edit, manifest diff, no-op) are invisible to the agent. -- **cred-proxy SIGHUP reload.** New behavior on the existing process: SIGHUP re-reads `routes.json` without dropping connections or breaking in-flight calls. ~30 lines added to the server. Used by the **cred-proxy block** category. -- **pipelock edit + restart.** v1 ships restart-based reload for pipelock: on `pipelock edit ` save, the supervisor writes the new allowlist and restarts the pipelock container. The agent's in-flight outbound calls drop and rely on retry. Used by the **pipelock block** category. -- **TUI dashboard.** A `claude-bottle dashboard` (or similarly named) command. Lists running bottles, surfaces pending stuck-notifications, exposes the `r `, `routes edit `, and `pipelock edit ` verbs, and (for the **capability gap** category) shows proposed manifest diffs with approve/reject input. Targets stdlib only; a TUI library is added only if the experience truly demands it. -- **Config-edit audit logs.** `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`. Every edit appends: timestamp, diff before/after, operator's reply text if tied to a `/stuck` reply. Records what the bottle's credential surface and egress perimeter looked like at time T without storing secret values. -- **Rebuild orchestrator (capability-gap path).** Used when the manifest itself must change, not just routes or the pipelock allowlist. On approval, tears down the existing bottle, applies the approved manifest diff, snapshots state via the state-preservation helper, and starts a fresh bottle on the same branch. -- **State-preservation helper.** Mandatory: ensures the working tree is pushed before teardown. Best-effort: carries forward the agent's transcript / reasoning context into the replacement container so the new agent starts warm rather than cold. +- **MCP sidecar.** New per-bottle container on the bottle's internal network. Exposes the three tools (`cred-proxy-block`, `pipelock-block`, `capability-block`) to the agent. On a tool call: validates the proposed file syntactically (valid JSON for `routes.json`, parseable Dockerfile, etc.), persists the proposal to a host-mounted queue, and holds the tool-call connection open until the supervisor acts. On a response from the supervisor, returns `{status, notes}` to the agent. Whether this lives as its own container or as a mode of cred-proxy is an Open question; v1 plan is its own container to keep cred-proxy focused on credentials. +- **`cred-proxy-block` MCP tool.** Input: `{routes: , justification: }`. Output: `{status: "approved"|"modified"|"rejected", notes: }`. On approval, the supervisor writes the new `routes.json` and SIGHUPs cred-proxy. +- **`pipelock-block` MCP tool.** Input: `{allowlist: , justification: }`. Output: same approve/reject shape. On approval, the supervisor writes the new allowlist and restarts pipelock. +- **`capability-block` MCP tool.** Input: `{dockerfile: , justification: }`. Output: best-effort approval acknowledgement (the agent is about to be torn down). On approval, the rebuild orchestrator runs. +- **Read-only current-config mount.** `/etc/claude-bottle/current-config/` in the agent container exposes `routes.json`, the pipelock allowlist, and the agent Dockerfile from the host. Read-only — the agent proposes changes via the tool call, never by writing the file directly. +- **cred-proxy SIGHUP reload.** New behavior on the existing process: SIGHUP re-reads `routes.json` without dropping connections or breaking in-flight calls. ~30 lines added to the server. Used by the **cred-proxy block** path. +- **pipelock restart path.** v1 uses restart, not SIGHUP: the supervisor writes the new allowlist and restarts the pipelock container. Agent's in-flight outbound calls drop and rely on retry. Used by the **pipelock block** path. +- **TUI dashboard.** A `claude-bottle dashboard` (or similarly named) command. Lists running bottles and pending tool-call proposals. For each proposal: shows current vs. proposed diff and the agent's justification; operator actions are approve, modify-then-approve, or reject-with-reason. Also exposes proactive operator-initiated edits: `routes edit ` and `pipelock edit ` — useful when the operator wants to change config without waiting for an agent prompt. Targets stdlib only; a TUI library is added only if the experience truly demands it. +- **Config-edit audit logs.** `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`. Every edit appends: timestamp, diff before/after, the agent's justification (if the edit came from a tool call), and the operator's action with notes. Records config shape, never the secret values themselves. +- **Rebuild orchestrator (capability-block path).** Used when the Dockerfile must change. On approval, tears down the existing bottle, builds from the new Dockerfile, snapshots state via the state-preservation helper, and starts a fresh bottle on the same branch. +- **State-preservation helper.** Mandatory: ensures the working tree is pushed before teardown. Best-effort: carries forward the agent's transcript / reasoning context — including the approved `capability-block` proposal — into the replacement container so the new agent starts warm rather than cold. ### Existing code touched -- **cred-proxy** (PRD 0010) — gains the `/supervise/notify` endpoint, the host-mounted notification queue, and SIGHUP reload of `routes.json`. +- **cred-proxy** (PRD 0010) — gains SIGHUP reload of `routes.json`. The supervise-notify responsibility moves out of cred-proxy and into the new MCP sidecar, keeping cred-proxy focused on credentials. - **pipelock** — gains a clean restart path that picks up the new allowlist on container restart. No code changes likely needed if pipelock already reads its config on startup; the orchestration is supervisor-side. -- **`cli.py`** — gains the dashboard subcommand (with `r`, `routes edit`, and `pipelock edit` verbs) and the rebuild path. -- **Bottle lifecycle scripts** — extended for orchestrated teardown + rebuild with state hand-off, distinct from a fresh-spawn. +- **`cli.py`** — gains the dashboard subcommand (approve/modify/reject pending tool-call proposals; `routes edit` and `pipelock edit` for proactive operator-initiated changes) and the rebuild path. +- **Bottle lifecycle scripts** — extended to launch the MCP sidecar alongside the other sidecars; mount the read-only current-config directory into the agent container; orchestrate teardown + rebuild with state hand-off. - **Bottle manifest schema** — may need to record the originating manifest version / change history per agent run, so the dashboard can show "what changed" rather than "what is." ### Data model changes -- A per-bottle pending-notification queue: cred-proxy holds the agent's open connection; the queue holds the metadata (id, bottle slug, message body, arrival timestamp) the TUI needs to render the ask. -- Per-bottle config audit log files at `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`, append-only. +- A per-bottle pending-proposal queue: the MCP sidecar holds the agent's open tool-call connection; the queue holds the proposal payload (id, bottle slug, tool name, current file hash, proposed file content, agent's justification text, arrival timestamp) the TUI needs to render the ask. +- Per-bottle config audit log files at `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`, append-only. Each entry includes the agent's justification and the operator's action. - A per-agent-run record sufficient to map a running bottle back to its PR / branch, so the rebuild orchestrator knows where to post the comment and which branch to resume on. ### External dependencies - The Gitea API / `tea` CLI is already in the toolbox (the project is on Gitea); no new auth surface beyond what the orchestrator already needs to read/post on PRs. - A TUI library is a *maybe* — only if stdlib can't carry the dashboard experience. Default to no new dependency. +- An MCP server library / framework. The MCP sidecar implements the MCP protocol so the agent can call the three tools natively. Pick the lightest option that lets the sidecar advertise three tools with structured input/output schemas; do not adopt a heavier MCP framework than the three tools justify. ## Open questions +- Text-only vs. structured tools. Earlier drafts of this PRD used a text-only protocol (`/supervise/notify` returning `{text}`); this revision uses three structured MCP tools that carry the agent's proposed file. **Structured wins on:** richer triage signal (operator sees the diff up front, not just a description of it), cleaner audit (the agent's proposed shape is captured alongside the operator's action), and the agent does diff-authoring work the operator would otherwise have to do. **Structured costs:** larger wire surface, the agent has to know the file formats (`routes.json` schema, Dockerfile syntax, pipelock allowlist format), miscategorization is possible (e.g. a 403 the agent reads as a `cred-proxy-block` might actually be a pipelock issue at a different layer). **Text-only wins on:** smallest possible protocol, no schema burden on the agent, easy to extend (every new category is just another reason in prose). **Text-only costs:** operator does all the diff authoring, audit log loses the agent's proposed shape, no opportunity for the agent's understanding of the fix to be inspected. Worth re-litigating if the MCP sidecar grows complex relative to the value it produces. +- MCP sidecar placement. v1 plan is its own container. Alternative is folding the supervise plane back into cred-proxy as a second tool surface. Own container keeps separation clean; folded saves one sidecar per bottle. Worth deciding once the sidecar's actual line count is known. +- `capability-block` return semantics. The current agent is torn down on approval, so the tool's return value never reaches it. Options: (a) fire-and-forget, the tool returns immediately with "queued" and the agent halts; (b) block the tool, let the rebuild orchestrator's teardown kill the connection, replacement agent gets the approval record via state-preservation; (c) the tool blocks, returns "approved" right before teardown, the agent has milliseconds to log it. (b) seems cleanest but is worth confirming during implementation. - SIGHUP race window. An agent that retries within msec of the SIGHUP may hit old routes once before the reload completes, fail, and retry against the new routes. Assumption is that normal HTTP retry semantics absorb this; worth confirming under real usage rather than designing around it preemptively. - SIGHUP reload for pipelock. v1 ships restart-based reload, which drops in-flight outbound calls. Should pipelock gain SIGHUP support so **pipelock block** is as cheap as **cred-proxy block**? Depends on how often the operator edits the allowlist mid-task and how disruptive a pipelock bounce actually is. -- Multiple pending notifications from the same bottle. If the agent calls `/stuck` again before the prior message is answered, what does the queue do — replace, append, or refuse? Append feels safest; replace is wrong (loses context); refuse forces the agent to handle a new error mode. -- Verb naming under load. `r ` optimizes for muscle memory mid-incident; `reply ` reads better cold. Worth picking once and committing. +- Multiple pending proposals from the same bottle. If the agent calls a second tool before the first is answered, what does the queue do — replace, append, or refuse? Append feels safest; replace is wrong (loses context); refuse forces the agent to handle a new error mode. Also: can different tools from the same bottle be pending simultaneously (e.g. a `cred-proxy-block` and a `pipelock-block` in flight at once)? +- Proposal validation strictness. The MCP sidecar validates the proposed file syntactically. Should it also do a deeper check — e.g. does the proposed `routes.json` introduce a route the operator already rejected this session? Probably no for v1; the operator is the gate. - Best-effort transcript preservation on the rebuild path. Mount the agent's state directory, snapshot on teardown, remount in the replacement? How much fidelity is "good enough" for the new agent to pick up? -- Tool-denial auto-detection. Should v1 also ship a tool-denial hook that auto-invokes `/stuck` without the agent's involvement, or strictly the agent-initiated form? Currently deferred; line worth confirming during implementation. -- Rejection semantics on the rebuild path. Does the agent receive a `/stuck` reply explaining the rejection, or does the bottle just stay torn down? +- Tool-denial auto-detection. Should v1 also ship a denial hook that auto-invokes one of the three tools without the agent's reasoning step, or strictly the agent-initiated form? Currently deferred; agent-initiated is safer (the agent has the most context about *why* it needed the call that was denied). - Bottle → PR/branch mapping. Recorded at bottle-spawn time, derived from the working tree, or specified in the manifest? -- How does the flow handle one-off exceptions to gitlock / pipelock denials — e.g. a commit that includes docs with intentionally-bogus tokens that the secret scanner correctly flags? The shape (agent blocked → `/stuck` → operator decides → reply) is the same, but the *resolution* differs: a per-operation override or a scoped allowlist entry, not a routes edit or a manifest change. Does the operator express the exception by commit SHA, by content hash, or by a narrow allowlist rule? Either way, the approval must be auditable so a future reader can see what was waived and why. See `docs/research/git-gate-commit-approval.md` for a survey of gitleaks's native allowlist primitives and a recommendation. +- How does the flow handle one-off exceptions to gitlock / pipelock denials — e.g. a commit that includes docs with intentionally-bogus tokens that the secret scanner correctly flags? The shape (agent blocked → tool call → operator decides → result) is the same, but the *resolution* differs: a per-operation override or a scoped allowlist entry, not a routes edit, allowlist edit, or Dockerfile change. Is this a fourth tool (`exception-block`?), or does it fold into `pipelock-block` with a scoped one-shot allowlist entry? Either way, the approval must be auditable so a future reader can see what was waived and why. See `docs/research/git-gate-commit-approval.md` for a survey of gitleaks's native allowlist primitives and a recommendation. ## References -- PRD 0010 — cred-proxy (the endpoint extended to carry stuck-requests). +- PRD 0010 — cred-proxy (gains SIGHUP reload of `routes.json`; the supervise plane lives in a separate MCP sidecar). - `CLAUDE.md` — project non-goal on agent-to-agent communication; this PRD stays on the human→agent side of that line. -- 2.52.0 From 5c3c60cff48a4aa9d4ba49a0d6e094abc8967109 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 03:05:55 -0400 Subject: [PATCH 11/12] docs(prd-0012): explain why the MCP server is a sidecar, not in-container Captures the rationale for placing the MCP server outside the agent container. The bottle wall doesn't strictly require it (the operator TUI is the actual gate), but pattern consistency, audit metadata trust, connection lifecycle, future enforcement headroom, and pipelock cleanliness all argue for sidecar placement. Co-Authored-By: Claude Opus 4.7 --- docs/prds/0012-stuck-agent-recovery-flow.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/prds/0012-stuck-agent-recovery-flow.md b/docs/prds/0012-stuck-agent-recovery-flow.md index 9162f63..2409efc 100644 --- a/docs/prds/0012-stuck-agent-recovery-flow.md +++ b/docs/prds/0012-stuck-agent-recovery-flow.md @@ -57,6 +57,20 @@ Three named categories, each with its own MCP tool. Ordered by remediation cost: The three tools are dispatched by name, so the operator's TUI knows which remediation engine to wire to which proposal. The agent must choose the right tool for what failed: a 403 from a credentialed request is a `cred-proxy-block`; a connection refused at the egress is a `pipelock-block`; a "command not found" or missing-skill error is a `capability-block`. +### Why the MCP server is a sidecar, not in-container + +The MCP server could in principle run inside the agent container. It doesn't, for reasons that are individually soft but together argue for the sidecar shape. + +The security boundary is the operator approving in the TUI plus the host-side config write, neither of which depends on where the MCP server runs. Even if a compromised agent forged a proposal end-to-end, a human still has to approve before cred-proxy, pipelock, or the Dockerfile change. So in-container isn't a hard violation of the bottle wall. + +It's still the wrong placement for five reasons: + +1. **Pattern consistency.** cred-proxy and pipelock are sidecars because boundary services don't live in the agent's process space. An in-container MCP server would be the first exception, and the next reader has to ask why this one is different. +2. **Audit metadata trust.** Sidecar-generated timestamps and bottle metadata are trusted; in-container metadata is whatever the agent wrote. Same outcome (the operator is still the gate) but weaker provenance. +3. **Connection lifecycle.** A sidecar holds the tool-call connection independently of the agent process — agent crash or restart doesn't orphan a pending operator response. +4. **Future enforcement headroom.** If the MCP server ever needs to *enforce* something (rate limits, dedup, schema-strict rejection), it has to be a trusted process. Building it in-container now means re-architecting later. +5. **Pipelock cleanliness.** Sidecar-on-internal-network is the same egress shape pipelock already permits. In-container would need a loopback exception in the allowlist. + ### New services / components - **MCP sidecar.** New per-bottle container on the bottle's internal network. Exposes the three tools (`cred-proxy-block`, `pipelock-block`, `capability-block`) to the agent. On a tool call: validates the proposed file syntactically (valid JSON for `routes.json`, parseable Dockerfile, etc.), persists the proposal to a host-mounted queue, and holds the tool-call connection open until the supervisor acts. On a response from the supervisor, returns `{status, notes}` to the agent. Whether this lives as its own container or as a mode of cred-proxy is an Open question; v1 plan is its own container to keep cred-proxy focused on credentials. -- 2.52.0 From e10634abe7c637cafb9f627020c3e30226bac722 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 03:40:02 -0400 Subject: [PATCH 12/12] docs(prd-0012): split into overview + 4 implementation PRDs PRD 0012 becomes the cross-cutting overview (stuck categories taxonomy, sidecar-vs-in-container rationale, implementation chunk pointers). Implementation detail moves into four follow-on PRDs that 0012 references: 0013 (supervise plane foundation), 0014 (cred-proxy block remediation), 0015 (pipelock block remediation), 0016 (capability block remediation). Co-Authored-By: Claude Opus 4.7 --- docs/prds/0012-stuck-agent-recovery-flow.md | 92 +++++---------------- 1 file changed, 20 insertions(+), 72 deletions(-) diff --git a/docs/prds/0012-stuck-agent-recovery-flow.md b/docs/prds/0012-stuck-agent-recovery-flow.md index 2409efc..3639ed2 100644 --- a/docs/prds/0012-stuck-agent-recovery-flow.md +++ b/docs/prds/0012-stuck-agent-recovery-flow.md @@ -8,6 +8,8 @@ When an agent running inside a claude-bottle container gets blocked, it invokes one of three MCP tool calls — `cred-proxy-block`, `pipelock-block`, or `capability-block` — passing a *proposed* config change (modified `routes.json`, modified pipelock allowlist, or modified agent Dockerfile) plus text describing why the change is justified. The supervisor sees the proposal in a host-side TUI, approves / modifies / rejects it, and the corresponding remediation runs: SIGHUP-reload cred-proxy with the new routes; restart pipelock with the new allowlist; rebuild the bottle from the new Dockerfile on the same branch. The agent's tool call blocks until the operator acts. The supervisor never opens a live channel into a running bottle; all signal flow goes through a per-bottle MCP sidecar on the existing internal network. +This PRD is the overview. Implementation is split across four follow-on PRDs (0013–0016); see *Implementation chunks* below. + ## Problem Running parallel agents in isolated bottles makes it cheap to spin up work in parallel, but expensive to recover when an agent gets stuck. Today, if a bottle is missing a permission or a tool the agent needs to make progress, the only options are to kill the container and start over (losing work) or open a live channel into the bottle to fix it in place (breaking the sandbox property that makes bottles trustworthy in the first place). The user feels this directly whenever a parallel run blocks on something the manifest didn't anticipate. @@ -23,41 +25,17 @@ A real stuck agent recovers end-to-end in each of the three categories: a **cred - Auditing or forensic replay of agent runs. Git/forge history is the audit log; this PRD does not add a separate run log. - Reducing time-to-unstuck below some target. Faster than kill-and-restart is implicit, but no specific SLO is in scope. -## Scope - -### In scope - -- A per-bottle MCP sidecar on the bottle's internal network that exposes three tools to the agent: `cred-proxy-block`, `pipelock-block`, `capability-block`. Each tool blocks until the operator acts. -- Tool input schemas: each tool takes the full proposed file (a new `routes.json`, a new pipelock allowlist, or a new Dockerfile, respectively) plus a `justification` text field. Tool output: `{status: "approved"|"modified"|"rejected", notes: "..."}`. -- A read-only mount at `/etc/claude-bottle/current-config/` in the agent container that exposes the current `routes.json`, pipelock allowlist, and Dockerfile, so the agent can read them before composing a proposed change. -- SIGHUP-based hot reload of `routes.json` on cred-proxy, so an approved **cred-proxy block** proposal takes effect without restarting the proxy or dropping in-flight calls. -- A clean restart path on pipelock that picks up a new allowlist on container restart; v1 does not ship SIGHUP reload for pipelock (see Open questions). -- A TUI dashboard that lists running bottles and pending tool-call proposals. For each pending proposal: shows the diff (current → proposed) and the agent's justification. Operator actions: approve, modify-then-approve (edit the proposed diff before applying), reject with reason. Also exposes proactive operator-initiated edits: `routes edit `, `pipelock edit ` — useful when the operator wants to change config without waiting for an agent prompt. -- Host-side audit logs at `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`. Each entry: timestamp, diff before/after, the agent's justification text (if the edit came from a tool call), and the operator's action (approve / modify / reject) with notes. Records config shape, never secret values. -- A rebuild path for the **capability block** case where the agent Dockerfile must change. Orchestrator tears down the bottle, applies the approved Dockerfile, and starts a replacement bottle on the same branch. -- A state-preservation helper for the rebuild path: working tree push is mandatory; transcript / reasoning context is best-effort. - -### Out of scope - -- A tool-denial hook that auto-detects "stuck" without the agent's involvement. Deferred to a follow-up; v1 is opt-in via the agent calling one of the three MCP tools. -- A web dashboard. TUI only in v1. -- Live channel into running containers (see Non-goals). -- Agent-to-agent communication (see Non-goals). -- Auditing / forensic replay (see Non-goals). - -## Proposed Design - -### Stuck categories +## Stuck categories Three named categories, each with its own MCP tool. Ordered by remediation cost: -- **cred-proxy block.** Tool: `cred-proxy-block`. The agent's request was refused by cred-proxy — missing route, expired token, wrong scope. The agent reads the current `routes.json` from `/etc/claude-bottle/current-config/`, composes a modified version, and calls the tool with `{routes: , justification: "..."}`. The operator reviews the diff in the TUI; on approval, the supervisor writes the new `routes.json` and cred-proxy SIGHUP-reloads. In-flight connections are not dropped. The tool returns `{status: "approved", notes: "..."}` and the agent retries. -- **pipelock block.** Tool: `pipelock-block`. The agent's outbound request was refused by pipelock — host not in the allowlist, protocol not permitted. The agent reads the current allowlist, composes a modified version, and calls the tool with `{allowlist: , justification: "..."}`. On approval, the supervisor writes the new allowlist and restarts pipelock; in-flight outbound calls may drop and rely on retry. The tool returns the same approve/reject shape. -- **capability block.** Tool: `capability-block`. The bottle is missing a tool, skill, permission, or env var the agent needs — something that lives in the agent Dockerfile rather than in routes or the pipelock allowlist. The agent reads the current Dockerfile, composes a modified version, and calls the tool with `{dockerfile: , justification: "..."}`. On approval, the rebuild orchestrator tears down the bottle, builds from the new Dockerfile, and starts a replacement bottle on the same branch via the state-preservation helper. Because the current agent is about to be replaced, the tool's return is best-effort — the replacement agent inherits the approval record via the preserved transcript. +- **cred-proxy block.** Tool: `cred-proxy-block`. The agent's request was refused by cred-proxy — missing route, expired token, wrong scope. The agent reads the current `routes.json` from `/etc/claude-bottle/current-config/`, composes a modified version, and calls the tool with `{routes: , justification: "..."}`. The operator reviews the diff in the TUI; on approval, the supervisor writes the new `routes.json` and cred-proxy SIGHUP-reloads. In-flight connections are not dropped. The tool returns `{status: "approved", notes: "..."}` and the agent retries. Implementation: PRD 0014. +- **pipelock block.** Tool: `pipelock-block`. The agent's outbound request was refused by pipelock — host not in the allowlist, protocol not permitted. The agent reads the current allowlist, composes a modified version, and calls the tool with `{allowlist: , justification: "..."}`. On approval, the supervisor writes the new allowlist and restarts pipelock; in-flight outbound calls may drop and rely on retry. The tool returns the same approve/reject shape. Implementation: PRD 0015. +- **capability block.** Tool: `capability-block`. The bottle is missing a tool, skill, permission, or env var the agent needs — something that lives in the agent Dockerfile rather than in routes or the pipelock allowlist. The agent reads the current Dockerfile, composes a modified version, and calls the tool with `{dockerfile: , justification: "..."}`. On approval, the rebuild orchestrator tears down the bottle, builds from the new Dockerfile, and starts a replacement bottle on the same branch via the state-preservation helper. Because the current agent is about to be replaced, the tool's return is best-effort — the replacement agent inherits the approval record via the preserved transcript. Implementation: PRD 0016. The three tools are dispatched by name, so the operator's TUI knows which remediation engine to wire to which proposal. The agent must choose the right tool for what failed: a 403 from a credentialed request is a `cred-proxy-block`; a connection refused at the egress is a `pipelock-block`; a "command not found" or missing-skill error is a `capability-block`. -### Why the MCP server is a sidecar, not in-container +## Why the MCP server is a sidecar, not in-container The MCP server could in principle run inside the agent container. It doesn't, for reasons that are individually soft but together argue for the sidecar shape. @@ -71,55 +49,25 @@ It's still the wrong placement for five reasons: 4. **Future enforcement headroom.** If the MCP server ever needs to *enforce* something (rate limits, dedup, schema-strict rejection), it has to be a trusted process. Building it in-container now means re-architecting later. 5. **Pipelock cleanliness.** Sidecar-on-internal-network is the same egress shape pipelock already permits. In-container would need a loopback exception in the allowlist. -### New services / components +## Implementation chunks -- **MCP sidecar.** New per-bottle container on the bottle's internal network. Exposes the three tools (`cred-proxy-block`, `pipelock-block`, `capability-block`) to the agent. On a tool call: validates the proposed file syntactically (valid JSON for `routes.json`, parseable Dockerfile, etc.), persists the proposal to a host-mounted queue, and holds the tool-call connection open until the supervisor acts. On a response from the supervisor, returns `{status, notes}` to the agent. Whether this lives as its own container or as a mode of cred-proxy is an Open question; v1 plan is its own container to keep cred-proxy focused on credentials. -- **`cred-proxy-block` MCP tool.** Input: `{routes: , justification: }`. Output: `{status: "approved"|"modified"|"rejected", notes: }`. On approval, the supervisor writes the new `routes.json` and SIGHUPs cred-proxy. -- **`pipelock-block` MCP tool.** Input: `{allowlist: , justification: }`. Output: same approve/reject shape. On approval, the supervisor writes the new allowlist and restarts pipelock. -- **`capability-block` MCP tool.** Input: `{dockerfile: , justification: }`. Output: best-effort approval acknowledgement (the agent is about to be torn down). On approval, the rebuild orchestrator runs. -- **Read-only current-config mount.** `/etc/claude-bottle/current-config/` in the agent container exposes `routes.json`, the pipelock allowlist, and the agent Dockerfile from the host. Read-only — the agent proposes changes via the tool call, never by writing the file directly. -- **cred-proxy SIGHUP reload.** New behavior on the existing process: SIGHUP re-reads `routes.json` without dropping connections or breaking in-flight calls. ~30 lines added to the server. Used by the **cred-proxy block** path. -- **pipelock restart path.** v1 uses restart, not SIGHUP: the supervisor writes the new allowlist and restarts the pipelock container. Agent's in-flight outbound calls drop and rely on retry. Used by the **pipelock block** path. -- **TUI dashboard.** A `claude-bottle dashboard` (or similarly named) command. Lists running bottles and pending tool-call proposals. For each proposal: shows current vs. proposed diff and the agent's justification; operator actions are approve, modify-then-approve, or reject-with-reason. Also exposes proactive operator-initiated edits: `routes edit ` and `pipelock edit ` — useful when the operator wants to change config without waiting for an agent prompt. Targets stdlib only; a TUI library is added only if the experience truly demands it. -- **Config-edit audit logs.** `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`. Every edit appends: timestamp, diff before/after, the agent's justification (if the edit came from a tool call), and the operator's action with notes. Records config shape, never the secret values themselves. -- **Rebuild orchestrator (capability-block path).** Used when the Dockerfile must change. On approval, tears down the existing bottle, builds from the new Dockerfile, snapshots state via the state-preservation helper, and starts a fresh bottle on the same branch. -- **State-preservation helper.** Mandatory: ensures the working tree is pushed before teardown. Best-effort: carries forward the agent's transcript / reasoning context — including the approved `capability-block` proposal — into the replacement container so the new agent starts warm rather than cold. +- **PRD 0013 — Supervise plane foundation.** MCP sidecar shell, three tool definitions, proposal queue, read-only current-config mount, minimal TUI, audit log format. After 0013, an operator can see proposals and approve/reject them but no remediation actually runs (the approval handlers are no-ops). +- **PRD 0014 — cred-proxy block remediation.** cred-proxy SIGHUP reload, host-side write on approval, `routes edit ` TUI verb, cred-proxy audit log filled in. First end-to-end useful category. +- **PRD 0015 — pipelock block remediation.** pipelock restart wiring, host-side write on approval, `pipelock edit ` TUI verb, pipelock audit log filled in. Same shape as 0014 for a different sidecar. +- **PRD 0016 — capability block remediation.** Rebuild orchestrator, state-preservation helper, `capability-block` end-to-end wiring, bottle-lifecycle changes for orchestrated teardown + rebuild. Heaviest chunk, lands last. -### Existing code touched - -- **cred-proxy** (PRD 0010) — gains SIGHUP reload of `routes.json`. The supervise-notify responsibility moves out of cred-proxy and into the new MCP sidecar, keeping cred-proxy focused on credentials. -- **pipelock** — gains a clean restart path that picks up the new allowlist on container restart. No code changes likely needed if pipelock already reads its config on startup; the orchestration is supervisor-side. -- **`cli.py`** — gains the dashboard subcommand (approve/modify/reject pending tool-call proposals; `routes edit` and `pipelock edit` for proactive operator-initiated changes) and the rebuild path. -- **Bottle lifecycle scripts** — extended to launch the MCP sidecar alongside the other sidecars; mount the read-only current-config directory into the agent container; orchestrate teardown + rebuild with state hand-off. -- **Bottle manifest schema** — may need to record the originating manifest version / change history per agent run, so the dashboard can show "what changed" rather than "what is." - -### Data model changes - -- A per-bottle pending-proposal queue: the MCP sidecar holds the agent's open tool-call connection; the queue holds the proposal payload (id, bottle slug, tool name, current file hash, proposed file content, agent's justification text, arrival timestamp) the TUI needs to render the ask. -- Per-bottle config audit log files at `~/.claude-bottle/audit/cred-proxy-.log` and `~/.claude-bottle/audit/pipelock-.log`, append-only. Each entry includes the agent's justification and the operator's action. -- A per-agent-run record sufficient to map a running bottle back to its PR / branch, so the rebuild orchestrator knows where to post the comment and which branch to resume on. - -### External dependencies - -- The Gitea API / `tea` CLI is already in the toolbox (the project is on Gitea); no new auth surface beyond what the orchestrator already needs to read/post on PRs. -- A TUI library is a *maybe* — only if stdlib can't carry the dashboard experience. Default to no new dependency. -- An MCP server library / framework. The MCP sidecar implements the MCP protocol so the agent can call the three tools natively. Pick the lightest option that lets the sidecar advertise three tools with structured input/output schemas; do not adopt a heavier MCP framework than the three tools justify. +0013 is a hard prerequisite for 0014–0016. The other three can in principle ship in any order, but the recommended sequence is cheapest-blast-radius first (0014 → 0015 → 0016) so cheaper wins land while the rebuild path is being designed. ## Open questions -- Text-only vs. structured tools. Earlier drafts of this PRD used a text-only protocol (`/supervise/notify` returning `{text}`); this revision uses three structured MCP tools that carry the agent's proposed file. **Structured wins on:** richer triage signal (operator sees the diff up front, not just a description of it), cleaner audit (the agent's proposed shape is captured alongside the operator's action), and the agent does diff-authoring work the operator would otherwise have to do. **Structured costs:** larger wire surface, the agent has to know the file formats (`routes.json` schema, Dockerfile syntax, pipelock allowlist format), miscategorization is possible (e.g. a 403 the agent reads as a `cred-proxy-block` might actually be a pipelock issue at a different layer). **Text-only wins on:** smallest possible protocol, no schema burden on the agent, easy to extend (every new category is just another reason in prose). **Text-only costs:** operator does all the diff authoring, audit log loses the agent's proposed shape, no opportunity for the agent's understanding of the fix to be inspected. Worth re-litigating if the MCP sidecar grows complex relative to the value it produces. -- MCP sidecar placement. v1 plan is its own container. Alternative is folding the supervise plane back into cred-proxy as a second tool surface. Own container keeps separation clean; folded saves one sidecar per bottle. Worth deciding once the sidecar's actual line count is known. -- `capability-block` return semantics. The current agent is torn down on approval, so the tool's return value never reaches it. Options: (a) fire-and-forget, the tool returns immediately with "queued" and the agent halts; (b) block the tool, let the rebuild orchestrator's teardown kill the connection, replacement agent gets the approval record via state-preservation; (c) the tool blocks, returns "approved" right before teardown, the agent has milliseconds to log it. (b) seems cleanest but is worth confirming during implementation. -- SIGHUP race window. An agent that retries within msec of the SIGHUP may hit old routes once before the reload completes, fail, and retry against the new routes. Assumption is that normal HTTP retry semantics absorb this; worth confirming under real usage rather than designing around it preemptively. -- SIGHUP reload for pipelock. v1 ships restart-based reload, which drops in-flight outbound calls. Should pipelock gain SIGHUP support so **pipelock block** is as cheap as **cred-proxy block**? Depends on how often the operator edits the allowlist mid-task and how disruptive a pipelock bounce actually is. -- Multiple pending proposals from the same bottle. If the agent calls a second tool before the first is answered, what does the queue do — replace, append, or refuse? Append feels safest; replace is wrong (loses context); refuse forces the agent to handle a new error mode. Also: can different tools from the same bottle be pending simultaneously (e.g. a `cred-proxy-block` and a `pipelock-block` in flight at once)? -- Proposal validation strictness. The MCP sidecar validates the proposed file syntactically. Should it also do a deeper check — e.g. does the proposed `routes.json` introduce a route the operator already rejected this session? Probably no for v1; the operator is the gate. -- Best-effort transcript preservation on the rebuild path. Mount the agent's state directory, snapshot on teardown, remount in the replacement? How much fidelity is "good enough" for the new agent to pick up? -- Tool-denial auto-detection. Should v1 also ship a denial hook that auto-invokes one of the three tools without the agent's reasoning step, or strictly the agent-initiated form? Currently deferred; agent-initiated is safer (the agent has the most context about *why* it needed the call that was denied). -- Bottle → PR/branch mapping. Recorded at bottle-spawn time, derived from the working tree, or specified in the manifest? -- How does the flow handle one-off exceptions to gitlock / pipelock denials — e.g. a commit that includes docs with intentionally-bogus tokens that the secret scanner correctly flags? The shape (agent blocked → tool call → operator decides → result) is the same, but the *resolution* differs: a per-operation override or a scoped allowlist entry, not a routes edit, allowlist edit, or Dockerfile change. Is this a fourth tool (`exception-block`?), or does it fold into `pipelock-block` with a scoped one-shot allowlist entry? Either way, the approval must be auditable so a future reader can see what was waived and why. See `docs/research/git-gate-commit-approval.md` for a survey of gitleaks's native allowlist primitives and a recommendation. +- **Text-only vs. structured tools.** An earlier draft of this PRD used a text-only protocol (`/supervise/notify` returning `{text}`); this revision uses three structured MCP tools that carry the agent's proposed file. **Structured wins on:** richer triage signal (operator sees the diff up front, not just a description of it), cleaner audit (the agent's proposed shape is captured alongside the operator's action), and the agent does diff-authoring work the operator would otherwise have to do. **Structured costs:** larger wire surface, the agent has to know the file formats (`routes.json` schema, Dockerfile syntax, pipelock allowlist format), miscategorization is possible (e.g. a 403 the agent reads as a `cred-proxy-block` might actually be a pipelock issue at a different layer). **Text-only wins on:** smallest possible protocol, no schema burden on the agent, easy to extend (every new category is just another reason in prose). **Text-only costs:** operator does all the diff authoring, audit log loses the agent's proposed shape, no opportunity for the agent's understanding of the fix to be inspected. Worth re-litigating if the MCP sidecar grows complex relative to the value it produces. +- **Tool-denial auto-detection.** Should v1 also ship a denial hook that auto-invokes one of the three tools without the agent's reasoning step, or strictly the agent-initiated form? Currently deferred; agent-initiated is safer (the agent has the most context about *why* it needed the call that was denied). ## References -- PRD 0010 — cred-proxy (gains SIGHUP reload of `routes.json`; the supervise plane lives in a separate MCP sidecar). +- PRD 0010 — cred-proxy (gains SIGHUP reload of `routes.json` in 0014). +- PRD 0013 — supervise plane foundation. +- PRD 0014 — cred-proxy block remediation. +- PRD 0015 — pipelock block remediation. +- PRD 0016 — capability block remediation. - `CLAUDE.md` — project non-goal on agent-to-agent communication; this PRD stays on the human→agent side of that line. -- 2.52.0