Repository navigation
feat(sidecar): taint escalates to HIC approval (v0.5.1 HGW-W1) - #79
Conversation
Owner decision 2026-10-15 (HGW (2)). Before this change any shell_run
output, even an approved `node -v`, tainted the session, and every later
command and sidecar-hosted write was declined for the rest of the
conversation ("which this tool's host cannot ask for"), with no way to
clear it.
Now, in a session opened with hicAware: true:
- a tainted effectful sidecar call (fs_*, file_write, sheet_write,
toolchain tools, capsules) waits on a new per-session action card
(GET /sessions/:id/actions/pending, POST .../actions/decide
{id, allow, subject}); the card shows the tool, the exact canonical
arguments, a summary, the hicReason and every taint source;
- shell_run keeps its own card and still asks after taint; the card
now carries hicReason and taintSources;
- an approval releases that one call; the session stays tainted, so
the next action asks again (never global, never automatic);
- decisions are recorded (kind sidecar.tool_call, write-ahead).
Without hicAware (headless, scheduled) nobody can be asked: the call is
declined with citrate_agent_loop::cannot_ask_reason, which names the
taint source and says why.
Tests: agent-loop taint_tests (+1), action_approvals unit (+3),
action_approvals_tests session/route tests (+6), shell_run_tests (+2,
one rewritten), files_tests taint case rewritten to the card flow.
Workspace: 2241 -> 2253 passed, 0 failed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018wWZZ8GRVU9USh3kKQHsFe
…t-retry (HGW-1) Aligns the HGW-1 interface with the HGW-W1 issue (#74): - routes are GET /sessions/:id/approvals/pending and POST /sessions/:id/approvals/:aid/decide {allow, subject}; - POST /sessions/:id/taint/clear {note}: the member clears the taint from an explicit card (TaintState::clear_by_member). Recorded write-ahead (kind taint.clear); with no records log, or when the record cannot be written, nothing is cleared (503). Scheduled (unattended) runs are never cleared (409); empty note 400; not tainted 409. Approving one action still never clears the taint; - a declined card tells the model to ask the member instead of retrying (shell_run and approval cards). Test support opens one undo store per manager (a store is locked while open). Tests: action_approvals_tests (+2). Workspace: 2253 -> 2255 passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wWZZ8GRVU9USh3kKQHsFe
BerryManifold
left a comment
There was a problem hiding this comment.
Reviewed per-action taint escalation, exact-subject matching, bearer-protected decision routes, headless refusal, stop/timeout handling, and recorded member taint clearing. The 11 action-approval unit/session tests passed locally on the #81 stack head. No blocking finding in this PR. This approval does not cover later git/auto-approval changes; #85 must land to resolve the existing audit failures.
Matr0xshka
left a comment
There was a problem hiding this comment.
Reviewed per-action taint escalation, exact-subject matching, bearer-protected decision routes, headless refusal, stop/timeout handling, and recorded member taint clearing. The 11 action-approval unit/session tests passed locally on the #81 stack head. No blocking finding in this PR. This approval does not cover later git/auto-approval changes; #85 must land to resolve the existing audit failures.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wWZZ8GRVU9USh3kKQHsFe
The squash of #79 on main is identical to hgw/w1-taint-hic@00264e5, which this branch already contains, so the resolution keeps this branch's tree. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wWZZ8GRVU9USh3kKQHsFe
main is the squash of #79, identical to hgw/w1-taint-hic@00264e5, which this branch already contains; the resolution keeps this branch's tree. Lands #80-#84 as one squash (owner decision 2026-10-15). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wWZZ8GRVU9USh3kKQHsFe
HGW Week 1, runtime, 1 of 5. Stack: this PR (base
main) → #80 → #81 → #82 → #83. Each PR is green on its own (fmt on touched files, clippy-D warningson agent-sidecar / agent-loop / agent-shell,cargo test --workspace --locked); merge in order. Refs #74 (HGW-W1). Owner decision 2026-10-15, HGW (2). Needs the owner's security sign-off (W1 adds a privileged path, per the sprint file).What changes
Before: any
shell_runoutput (even an approvednode -v) tainted the session, and every later command and sidecar-hosted write was declined for the rest of the conversation ("…which this tool's host cannot ask for"). Nothing could clear it.Now, in a session opened with
hicAware: true:fs_*,file_write,sheet_write, the toolchain tools, capsules) waits on a new per-session approval card. The card shows the tool, the exact canonical arguments (subject), a one-line summary,hicReasonand every taint source. An allow runs that one call with those arguments.shell_runstill asks after taint, on its own card, which now also carrieshicReasonandtaintSources.POST /sessions/:id/taint/clear {note}callsTaintState::clear_by_member. The clear is recorded first (taint.clear). If no records log is configured, or the record fails, nothing is cleared (503). A scheduled (unattended) session is never cleared (409).hicAware(headless or scheduled), nobody can be asked. The call is declined withcitrate_agent_loop::cannot_ask_reason, which names the taint source.sidecar.tool_call), like the shell and MCP cards.Interface for the core lane
GET /sessions/:id/approvals/pending{pending: [{id: "act-<n>", callId, tool, hic: "required", subject, summary, hicReason, taintSources, expiresInSecs}]}POST /sessions/:id/approvals/:aid/decide{allow, subject}. The subject must equal the card's; otherwise 409. A missing session is 404.POST /sessions/:id/taint/clear{note}: 200{ok, cleared: {source, reason}, sources}; 400 for an empty note; 409 when unattended or not tainted; 503 when no records log is configuredGET /sessions/:id/shell/pendinghicReasonandtaintSourceshicAware: trueand poll/approvals/pendingnext to/shell/pending(sidecarProvider.ts). The card renderssummary,subject,hicReasonandtaintSources.CITRATE_HERMES_RECORDS_DIRset.Tests (red first)
taint_tests: +1 (the headless refusal names its source)action_approvalsunit: +3action_approvals_tests: +8. Covers AT-1.1 (≥3 consecutiveshell_runs plusfs_writeafter taint), one approval per action, exact subject, the headless decline, routes and bearer, records, the explicit clear, and ask-not-retry.shell_run_tests: +2 (one test rewritten for the new semantics)files_tests: the taint case is rewritten to the card flow (a decline writes nothing)5b337ff).Not here
The ADR-HGW-1 write-up, which the sprint file says lands first (Mac lane, core repo).
🤖 Generated with Claude Code
https://claude.ai/code/session_018wWZZ8GRVU9USh3kKQHsFe