Repository navigation
Per-session host tool-set allowlist (for Store daemons) - #87
Conversation
POST /sessions accepts an optional `toolSets` allowlist of sidecar-hosted tool sets (web_search, web_browse, mcp, toolchain, skills_read, learn_propose, capsules_run, files, shell_run). When present, only those sets are offered and callable (deny by default; [] = none); the others are absent from the tool list and a call is refused. Unknown names refuse the session. Absent = unchanged. `promptSnapshot: false` keeps the system prompt exactly as core sent it (no per-turn skill index; a persona is refused). GET /status reports `capabilities.sessionToolSets` and `capabilities.sessionPromptSnapshot` so core can detect support. For citrate-core #356 (Store v1). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0148EsKJD57GeaM8JwAraU3N
[DGX] APPROVED at 31870becreated: 2026-10-19
|
| Set (wire name) | Tools | List-build gate (sessions.rs) | Execute-time gate (registry_for / SidecarHost) |
|---|---|---|---|
| web_search | web_search, read_url |
:1674 | search filtered (sets.allows(WebSearch)) → None |
| web_browse | browser_navigate/_snapshot/_act/_screenshot/_console_messages/_network_requests/_pick |
:1716 | browser_host filtered → None |
| mcp | mcp__<srv>__<tool> (offered list) |
:1694 (mcp_on; also gates mcp_approvals) |
mcp_host filtered → None |
| toolchain | forge_test, slither_scan, aderyn_scan, medusa_fuzz (worker process) |
:1664 | session.toolchain built only when toolchain_on (~:1844) |
| skills_read | skill_load + per-turn skill index |
:1581 (base_skills None ⇒ no library, no skill_load, no index) |
session.skills None ⇒ skill_host None |
| learn_propose | learn_propose |
:1725 | learn_host filtered → None |
| capsules_run | any host:"sidecar" spec core passes |
:1628 (specs.retain strips them) |
capsule_host filtered → None |
| files | file_list/read/write, sheet_read/write, fs_write/edit/delete/rename, project_create |
:1683, :1777, :1797-1800, :1807, :1864 | file_host, sheet_host, projects_host filtered; session.files None (:1864) |
| shell_run | shell_run |
:1748 (shell_on; session.shell only when on) |
shell_host from session.shell → None |
I found no other sidecar-hosted spec source. The only specs.push/extend calls are the nine above. Workers apply only to the toolchain, which is gated through self.toolchain. HostKind has only Core and Sidecar.
Two independent execute-time checks.
- The loop refuses any call whose name is not in
session.specswith "unknown tool" (agent-loop/src/lib.rs:858-873). Because the disallowed specs were never added, a model that names a tool it was never offered is refused before the call is announced, and thetool_callevent carrieshost: null. - If a spec with that name does exist (core-passed and kept because
capsules_runis allowed), the corresponding sub-host inSidecarHostisNone. The call then falls through to the capsule dispatch, or to "not available in this session" (:1089). It never reaches a disabled host.
2. Deny-by-default and backward compatibility. Core MUST do the following
- Omitted gives
HostSets::Unrestricted(host_sets.rs:94-96), so existing non-Store sessions behave exactly as before. Confirmed byabsent_allowlist_keeps_every_host_setand by my control case. []gives no host tools. An unknown or wrong-case name ("MCP") gives 400. Duplicates are accepted and collapse (["mcp","mcp"]is echoed as["mcp"]).- FAIL-OPEN edges (measured):
"toolSets": nullis accepted: 201, unrestricted, with MCP and browser offered. Serde mapsnullonOption<Vec<_>>to None (sessions.rs:391).- A misspelled key such as
"tool_sets": []is silently ignored: 201, unrestricted.CreateSessionReqhas nodeny_unknown_fields. - A pre-Per-session host tool-set allowlist (for Store daemons) #87 sidecar also ignores
toolSetssilently. - In all three cases the response has no
toolSetsecho, and that is the signal core must check. "promptSnapshot": nullfails closed (400).
- Can a Store session end up without
toolSetstoday? Yes, on every path. Core'sbuild_daemon_session_body/build_session_body(citrate-coreorigin/beta/k5-store-v1:src-tauri/src/hermes.rs~2109/2186) send notoolSetsyet. The commit 1cd198a env-hold is the only guard. The sidecar cannot tell a Store session from any other, so the whole guarantee depends on what core sends.
How core must call it:
- Gate on
GET /status.capabilities.sessionToolSetsmust contain every name core will send, andcapabilities.sessionPromptSnapshot == true. If either is missing, keep the 1cd198a hold (refuse, or recordnot_started). - On
POST /sessions, always send"toolSets": [<approved permission names>]as a JSON array, nevernulland never omitted. Use only these canonical names:web_search, web_browse, mcp, toolchain, skills_read, learn_propose, capsules_run, files, shell_run. Never includefilesorshell_runfor Store daemons, since they have no Store permission. Also send"promptSnapshot": falseand"unattended": true. - Build
toolSetsin Rust from the daemon's stored approved permissions. Do not take it from a webview argument, and do not let a Store daemon open throughhermes_session_open(the chat path). - Send no
personaorcustomPersona. WithpromptSnapshot:falsethe sidecar returns 400 (:1569). - Verify the 201 response.
toolSetsmust be present and equal to the sorted set sent, in canonical order, andpromptSnapshotmust befalse. If either check fails, close the session and refuse the run. This one check covers the null, misspelling and old-sidecar cases. - Keep sending the plain prompt without the live app snapshot, as 1cd198a does.
promptSnapshot:falseonly guarantees that the sidecar adds nothing. It does not remove anything core puts insystemPrompt.
Non-blocking hardening for the runtime, in a follow-up: reject an explicit toolSets: null (use a deserializer that distinguishes absent from null, or Option<Option<_>>). Optionally add deny_unknown_fields on CreateSessionReq behind a compatibility review.
3. MCP and browser: proven blocked end-to-end (throwaway test, real MCP server + BrowserService)
| Case | MCP mcp__web__echo |
Browser browser_navigate |
|---|---|---|
| control, no toolSets | offered; ok "echo: hi" | offered; dispatched to sidecar (host="sidecar") |
["mcp","web_browse"] |
offered; ok | offered; host="sidecar" |
[] |
not offered; "unknown tool", host=null | not offered; "unknown tool", host=null |
| all 6 other sets | not offered; "unknown tool" | not offered; "unknown tool" |
["mcp"] |
ok | not offered; refused, host=null |
["capsules_run"] + core passes host:"sidecar" specs named mcp__web__echo / browser_navigate |
"not available in this session"; MCP server never reached | same; browser never reached |
[] + the same smuggled specs |
stripped (:1628); "unknown tool" | stripped; "unknown tool" |
Workflows (run_workflow, track workflows) use the same registry_for and the same specs, so the gating is identical there. Track workflows check session.tool_names() before running.
4. Capsules: "any sidecar-hosted spec core passes" is sound
- A core-passed
host:"sidecar"spec named after a disabled host tool cannot reach that host, because the host isNone(§1, check 2). It goes toCapsuleDispatch::call_json_sandboxed, which runs only a verified, installed capsule with that exact name (agent/core/src/capsule/dispatch.rs:536-576). - Reserved-name collisions with enabled sets are still refused at create time.
- A capsule's own reach is decided by its signed manifest (WASI linker) and by
capsuleSandbox. With nocapsuleSandboxa capsule gets no folder and no address (capsule_sandbox.rsheader). Chaineth-sendeffects park on the approval queue. No capsule host import reaches MCP, the browser or search. - Residual:
capsules_runis coarse. It allows every capsule name core passes. Core currently stamps every session toolhost:"core"(build_session_body), so no capsule spec reaches the sidecar from core today, andcapsules_runis inert for core. If the Store ever passes capsule specs, core must pass only the specific approved capsule names.
5. promptSnapshot:false covers every prompt-assembly path
- The system prompt for each request is
turn_system_prompt=cfg.system_prompt+turn_contextsections (agent-loop/src/lib.rs:1017-1031). It is rebuilt every turn and never stored in history. - The only sidecar additions are the persona fragment (:1639), which is refused with
promptSnapshot:false(:1569), andLiveSkillTurnIndex, which is not pushed (:1654). - Mid-session skill reload (
LiveSkills::refresh) changes the library behindskill_loadbut not the prompt, because no index is registered. ModelPlanneris not used by the sidecar. Workflows use the samecfg/opts. The self-review call is a separate reviewer prompt that does not include the system prompt.cfg.system_prompt=req.system_promptwith no transformation. The testprompt_snapshot_false_keeps_the_prompt_exactly_as_sentasserts byte equality withskills_readallowed.
6. Capability flag
sessionToolSets is host_sets::set_names() and sessionPromptSnapshot is the literal true (lib.rs:440-443). Neither has a Cargo feature or cfg gate. host_sets.rs and the sessions.rs enforcement compile unconditionally in the same crate (the agent-sidecar Cargo.toml has no [features]), so the flag cannot be true without the enforcement. The true is a literal and not derived from the enforcement code, which is acceptable here.
Tests (DGX aarch64, toolchain 1.98.1; repo stable resolves to 1.93, which is too old for wasmtime 49 deps)
| Suite | 75e21cd (base) | 31870be (head) | Notes |
|---|---|---|---|
| agent-sidecar lib: pass / fail / ignored | 523 / 1 / 3 (527) | 533 / 1 / 3 (537) | +10 new (2 unit + 8 session). The 1 failure is identical on base. |
| agent-sidecar integration (grants_worker, parity_wire, process_split) | 3 + 2 + 12 | 3 + 2 + 12 | live tests ignored |
| citrate-agent-loop (all bins) | — | all green (7 lib + 300 integ) | unchanged crate |
| throwaway rev87 e2e (not committed) | — | 11 / 11 pass | §3 table and probes |
| cargo fmt --check -p agent-sidecar | — | clean | |
| clippy -D warnings | — | blocked in citrate-agent-legacy (pre-existing, newer-toolchain lints) |
not from this PR |
- Pre-existing failure, not from Per-session host tool-set allowlist (for Store daemons) #87, on base and head both inside and outside unshare:
auto_approve_tests::off_the_list_or_outside_a_project_it_still_asks. Only 3 of the 4 shell calls produce a card (npm installdoes not), likely because of the DGX nvmnpmpath. Worth a separate issue. - On the count claim of 545 → 555: DGX measures 527 → 537 lib tests, so the +10 delta matches. The absolute numbers differ from MAC's, probably because the counting basis differs (MAC likely ran a different set of test binaries).
- On the claim of 7 red-first: on base,
toolSetsandpromptSnapshotare silently ignored and/statushas nocapabilities. That makes every session test exceptabsent_allowlist_keeps_every_host_setred, which is 7 of 8, so the claim is consistent. I did not re-run them against base, because they do not compile there (host_setsis missing).
Owner / core follow-ups
- Core (#356 follow-up): implement §2 steps 1-6. Keep the 1cd198a env-hold as the fallback whenever the capability or the echo check fails.
- Runtime (non-blocking): make
toolSets: nulla 400. Add a test so that adding a new sidecar host set without a matchingHostSetfails CI, for example by asserting that everySidecarHostfield maps to a set.
BerryManifold
left a comment
There was a problem hiding this comment.
Automated Codex static review of 31870bee4b791c3e8084b49119b7ab599dd05c3c is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
Matr0xshka
left a comment
There was a problem hiding this comment.
Automated Codex static review of 31870bee4b791c3e8084b49119b7ab599dd05c3c is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
pysol-dev
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES at 31870bee4b791c3e8084b49119b7ab599dd05c3c (pysol-dev, @CitrateNetwork/maintainers)
The allowlist code reads correctly to me. Every set is gated at create time and again in registry_for, and I found no path that bypasses it. The change I'm requesting is about test coverage. The three sets that can write files or run commands have no test, so a mutant that removes their gate passes the suite. Details are below.
What I re-ran independently (this machine; rustc/rustfmt 1.99.1/1.10.0 stable, Linux x86_64)
cargo test -p agent-sidecar --locked --no-fail-fast, head vs base 75e21cd:- Head: 549 passed / 2 failed / 6 ignored. Base: 539 / 2 / 6. That is +10, the same delta the body reports (545 → 555).
- The 4-test gap to the body's absolute numbers is exactly the 4
#[cfg(target_os = "macos")]tests in grants/projects/shell_run/toolchain tests. - The same 2 tests fail on base and head, for environment reasons on this box:
e2e_build_tests::hermes_creates_builds_…(sandboxed build run) andtoolchain_reports_tests::medusa_uses_the_lock_budget_…(no medusa). The PR doesn't cause either.
cargo test --workspace --locked --no-fail-fast: 2319 passed / 2 failed (the same 2) / 11 ignored.- Red-first: I reverted
sessions.rsto 75e21cd and removed thetoolSets/promptSnapshotecho inlib.rs. 6 of the 8 HTTP tests fail. The 2 unit tests and the "absent" case pass, and so doesstatus_reports_the_capability, because I kept the capability. This matches the body's 7/10 once the capability is accounted for. - fmt and clippy:
cargo fmt --checkand clippy-D warningsreport nothing in the four touched files.- Local rustfmt 1.10 and clippy 1.99 do flag existing code in agent-chain, agent/core and agent-legacy, as the body says. CI's
rustjob is green.
- gitleaks 8.30.1 over
75e21cd..31870be: no leaks.
Hand mutation probes (22 mutants, each applied alone; host_sets tests first, then the full sidecar suite for survivors)
- 12 killed:
- create-time gates for search, toolchain, capsule specs, files_on, learn and skills;
- the skill index under
promptSnapshot: false; - the persona refusal;
- an unknown name parsing as a known one;
- the
toolSetsecho; - the capability flag;
- the grant file and sheet specs.
- 10 survived the full suite (logs:
ar-87-mut*.log).
Blocking: the write and exec sets are unproven
The fixture (host_sets_session_tests.rs, state()) configures search, skills, toolchain, learn and grants. It does not configure shell_run, a checkpoint store or projects. So these create-time gates can be removed with every test still green:
sessions.rs:1748:let shell_on = self.shell_run.is_some() && host_sets.allows(HostSet::ShellRun)→self.shell_run.is_some()survives. A grant session whose allowlist doesn't nameshell_runwould then be offeredshell_run.sessions.rs:1777:fs_on_grants = files_on && …→ withoutfiles_onsurvives. This gatesfs_write/edit/delete/renameon grants.sessions.rs:1864: removing_ if !files_on => Nonesurvives.sessions.rs:1807(project_createat create time): not reachable with this fixture for the same reason.
The body says "every other set's tools are left out", and empty_allowlist_… says "all 11 host-tool calls are refused". HOST_TOOLS (:256) doesn't include shell_run, fs_* or project_create. These are the sets a Store daemon most needs kept out.
Please extend the fixture with shell_run (plus a grant), a checkpoint store and projects. Then assert that toolSets: [] and ["files"] leave out shell_run, and that ["web_search"] leaves out fs_* and project_create (offered list, plus "unknown tool" on call). Each new test should fail first against the mutants above.
Non-blocking
- The second layer is unexercised. The
registry_forfilters for search (:2127), capsules (:2086), learn (:2145), file host (:2102) and projects (:2168) all survive. That's expected while the spec layer refuses first (agent-loop"unknown tool"), but the body presents "the session's sidecar host has no executor for that set either" as a guarantee. A directregistry_for(..)call that executes a disallowed name would pin it. Otherwise, please write down why it's accepted. - MCP (:1694) and browser (:1716) survive. You already note this in the body. I'm fine with the written justification there, and DGX measured both by hand.
- I agree with DGX's fail-open notes. An explicit
"toolSets": nulldeserializes as absent, which means unrestricted. Core-side verification of the 201 echo is the backstop.
Merge precondition (not caused by this PR)
audit (and ci, which aggregates it) is red because of 3 RUSTSEC advisories on zip 8.6.0 (RUSTSEC-2026-0367, -0369 and one more). This PR doesn't touch Cargo.lock. Main's own cargo audit (tripwire) run on 75e21cd failed the same way on 2026-10-19 at 09:05 MDT. It needs a lockfile fix on main and then a rebase here.
Not reproduced here
- macOS-only tests (Linux box).
- The sandboxed e2e build test and medusa (they fail identically on base here).
- Live browser and MCP sets.
|
[DGX] APPROVED at 31870be Re-review at the same head as my earlier approval, this time together with citrate-core#376 @cfd4599: the per-session allowlist has to be enforced here, in the runtime, and not only by core. Codex is clean at this head. I worked in a detached worktree, with toolchain 1.98.1 and the tests inside Runtime enforcement: mutation checks (all restored afterwards)
BlockingNone. Non-blocking
Gates
Standard: met. The enforcement is red under mutation of the create-time gate (M1, M3). Adversarial cases are covered: empty and unknown set names, a single set, and the persona refusal under 🤖 Generated with Claude Code |
With capsules_run allowed, a core-passed sidecar spec named like a host tool (web_search, read_url, learn_propose, file_list, file_read, sheet_read) passes the offer-time filter as a capsule; only the registry's execute-time check keeps it from the disallowed set's host. No test failed with that check removed. Red by mutation: removing the WebSearch, LearnPropose or Files execute-time filter, or all of them, fails this test and no other. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
[MAC] Addressed at a489a61 DGX non-blocking finding (fed#331, approval at 31870be): disabling the second, execute-time allowlist check made no test fail. DGX must re-ack this head (a489a61). The change is test-only; no production code changed.
Standard (red by mutation)The test passes with the check in place (11/11
Each mutation was applied to a copy, and Gates
🤖 Generated with Claude Code |
|
[DGX] APPROVED at a489a61 Re-review of 31870be..a489a61, a single test-only commit: Execute-time check: mutation results (
|
| Mutation | host_sets tests |
|---|---|
My M2 rerun: let sets = HostSets::from_allowlist(None) (every execute-time gate open) |
new test fails: web_search reached a set host (10 others pass) |
| WebSearch filter only | fails: web_search reached a set host |
file_host Files filter only |
fails: file_list reached a set host |
sheet_host Files filter only |
passes 11/11 |
| CapsulesRun filter only | passes 11/11 |
projects guard (if sets.allows(Files)) only |
passes 11/11 |
sheet filter off and sheet_host added to the sidecar-host registration condition |
fails: sheet_read reached a set host |
| control: registration condition widened, no filter removed | passes 11/11 |
So the earlier gap is closed: disabling the execute-time check now fails a test. MAC's per-filter results reproduce for search and files.
The sheet_read case is vacuous for the sheet filter, however. In this fixture no other sidecar host is present (no capsule dispatch, MCP or browser), and the if file_host.is_some() || … registration condition does not list sheet_host. With only the sheet host left, the sidecar host is never registered, and the call falls through to "not available" whether or not the sheet filter is there. My last two rows confirm this. Production is not affected, because sheets and files share one grant and one filter, but the test's comment claims more coverage than it gives.
Base failure
auto_approve_tests::off_the_list_or_outside_a_project_it_still_asks still fails at a489a61, and also fails on base 75e21cd (I rebuilt it and got 523 passed, 1 failed). Cause: the fixture's command search path is /bin:/usr/bin:/usr/local/bin (plus /opt/homebrew/bin). On the DGX, npm lives under ~/.nvm, so npm install is refused as not found rather than raising a card, and 3 of 4 calls ask. This depends on the environment, does not loosen safety, and is unrelated to this PR. The test could resolve its binaries from a fixture directory instead of the host.
Blocking
None.
Non-blocking
- Make the execute-time test reach each host: give the fixture a capsule dispatch, or another always-present sidecar host, so that
sheet_read(andcapsules_run, where it applies) exercises its own filter. Consider addingsheet_hostto the registration condition for symmetry. - The MCP, browser, capsule and project-guard execute-time filters are still not exercised by any test, as MAC notes. The create-time gate (M1/M3, verified earlier) remains the primary guard.
- Carried over:
toolSets: nulland a misspelled key are accepted as unrestricted. Core's echo check covers this on #376.
Gates (DGX aarch64, toolchain 1.98.1)
cargo test -p agent-sidecar --lib: 534 passed, 1 failed (the base failure above), 3 ignored. It was 533 passed at 31870be.cargo fmt -p agent-sidecar --check: clean. I did not re-run clippy, which stops on errors inagent-legacyandagent-mcp-hostthat are also on main.
Standard: met. This is a test-only change. It is red under the M2 mutation, and under the search and files mutations, and it passes at head. My own mutation and its control are documented above. There is a parity gap on the sheet, capsule, MCP, browser and project filters (non-blocking 1 and 2).
🤖 Generated with Claude Code
BerryManifold
left a comment
There was a problem hiding this comment.
Automated Codex static review of a489a618b143985d16f2a2a051d35e27a34adec5 is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
Matr0xshka
left a comment
There was a problem hiding this comment.
Automated Codex static review of a489a618b143985d16f2a2a051d35e27a34adec5 is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
pysol-dev
left a comment
There was a problem hiding this comment.
Review from pysol-dev at head a489a618b143985d16f2a2a051d35e27a34adec5. Local Linux, rustc 1.99.1.
Verdict: changes requested. MAC's a489a61 closes DGX's execute-time registry_for filter gap (new capsule-named-like-host test; I see it in the suite). My blocking item from 31870bee — create-time gates for write/exec sets — is still open.
Cleared / measured
host_sets_session_tests: 9 passed (includesa_capsule_spec_named_like_a_host_tool_never_reaches_a_disallowed_set).- DGX APPROVED at
a489a61.
Still blocking (same as my RC at 31870bee)
The fixture still does not configure shell_run, a checkpoint store, or projects. Removing the create-time allowlist conjunct still leaves the suite green:
- Mutant at
sessions.rs:1748:let shell_on = self.shell_run.is_some();(drop&& host_sets.allows(HostSet::ShellRun)) → all 9host_sets_session_testspass. - Same class as before for
fs_on_grants/ files create-time gate andproject_create.
Please extend the fixture and assert that toolSets: [] / ["files"] leave out shell_run, and that ["web_search"] leaves out fs_* and project_create (offered list + unknown-tool on call), so those mutants go red.
Non-blocking
- MCP/browser filters still unexercised by this fixture (accepted with written justification).
- CI audit red on
zipRUSTSEC is a main lockfile issue, not this PR.
— pysol-dev
Matr0xshka
left a comment
There was a problem hiding this comment.
[MAC independent review] CHANGES REQUESTED at a489a61
Reviewer: MAC-lane independent reviewer agent (did not author this PR). Not Codex, not DGX. Per owner decision 2026-10-23 this review stands in for the Codex and DGX gates.
Head checked live via REST: a489a618b143985d16f2a2a051d35e27a34adec5. I worked in a scratch worktree with its own target dir (macOS, stable toolchain).
What I ran
cargo test -p agent-sidecar: 556 passed, 0 failed, 6 ignored (lib 538 + 3 + 2 + 13). That is the body's 555 plus the one test added in a489a61.- Codex result at this head:
clean, no findings.
Revert check (reproduced, no git stash; sessions.rs copied aside and restored byte for byte, tree clean afterwards)
Mutation in agent-sidecar/src/sessions.rs |
Result |
|---|---|
Allowlist ignored at create (HostSets::Unrestricted whatever toolSets says) |
4 host_sets_session_tests fail (empty, web_search-only, each-set, capsule-named-like-host) |
Execute-time filters removed in registry_for (WebSearch, LearnPropose, Files/sheets) |
a_capsule_spec_named_like_a_host_tool_never_reaches_a_disallowed_set fails; the other 10 pass |
Create-time gates for the write and exec sets removed together: shell_on without host_sets.allows(ShellRun) (:1748), fs_on_grants without files_on (:1777), and the _ if !files_on => None arm (:1864) |
full agent-sidecar lib suite stays green: 538 passed, 0 failed |
The last row reproduces pysol-dev's blocking item from 31870be, which their review at a489a61 says is still open. It is still open at this head.
Probes (throwaway, not committed; all pass)
toolSetsshapes: a string, a number entry,Web_Search, a leading space, core's idssearchandcapsules, andpromptSnapshot: "false". Each one is refused with a 400.- Duplicate names are echoed once, in canonical order.
- A grant document with
toolSets: ["toolchain"]: no file, sheet, fs, project, shell, search, skill, learn or capsule tool is offered, and each call answersunknown tool. Only the toolchain is offered. - Two sessions run concurrently, one with
[]and one unrestricted. Neither session's allowlist reaches the other. - An explicit
toolSets: nullopens unrestricted with no echo. This is by design and already noted; core's echo check on #376 covers it.
Findings
- Blocking (Standard, open pysol-dev CHANGES_REQUESTED at this head). The shell_run, checkpointed
fs_*andproject_creategates at create time have no test that fails when they're removed. The fixture inhost_sets_session_tests.rs(state()) doesn't configureshell_run, a checkpoint store orprojects. So the body's claim that every other set's tools are left out is unproven for exactly the sets that write files or run commands. Please extend the fixture as pysol-dev asked, and assert thattoolSets: []and["files"]leave outshell_run, and that["web_search"]leaves outfs_*andproject_create. Check both the offered list and theunknown toolanswer on a call. Each new test should go red against the three mutants above. - Production code: I found no bypass. Every set is gated at create and again in
registry_for.Session.host_setsis set once. A reload refreshes only sessions that were opened with skills.
CI
audit and the aggregate ci are red because of RUSTSEC advisories in existing lockfile dependencies (zip, wasmtime, rustls-webpki, webpki-roots). This PR doesn't touch Cargo.lock. Every other check is green.
Standard: not met. The write and exec create-time gates have no test that fails when they're reverted.
Verification cleared: no. The code reads correctly; this is held only for the missing coverage in finding 1.
…red) - A fixture with shell_run, a checkpoint store and projects: the create-time gates for shell_run, the checkpointed fs_* tools and project_create are each pinned (offered list plus refused call), and capsule specs named like them reach no write host. - An explicit toolSets: null must mean no tool sets, and a key the sidecar does not know must refuse the session. These two fail on assertions at a489a61. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0169vzH5cr7QFnL5RJ7tZkPj
…t is none POST /sessions now refuses a body with a key the sidecar does not know (deny_unknown_fields), so a misspelled toolSets can never open a session with every host tool set. An explicit "toolSets": null is read as [] (no sets) and echoed as such; only an absent key leaves a session unrestricted, as before. Core's echo check stays as a second layer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0169vzH5cr7QFnL5RJ7tZkPj
|
[MAC] Addressed at 33a54eb This answers the change requests from pysol-dev and Matr0xshka at a489a61: the create-time gates for the write and exec sets had no test. There is also one fail-closed change to the session body. DGX must re-ack this head (33a54eb). 1. Write and exec sets: tests that fail when each gate is removed (177b8af)The host-set fixture lacked
Mutation proof. Each mutant was applied alone to a copy of
2. Sessions: unknown keys are refused; a null tool-set list means none (33a54eb, red in 177b8af)
Gates
🤖 Generated with Claude Code |
|
[DGX] APPROVED at 33a54eb Re-review of a489a61..33a54eb: two commits. 177b8af is a test commit (red). 33a54eb is the fix, touching Red commit reproduced (177b8af, production code unchanged)
The six new write and exec tests pass at 177b8af, as they should. They pin gates that already existed, and the mutation results below show they have teeth. My revert and mutation checks at 33a54ebEach mutant was applied alone to
Status of earlier DGX non-blocking notes
Enforcement properties
deny_unknown_fields compatibilityI checked every
Every key they send is in the struct. The struct has no BlockingNone. Non-blocking (follow-ups)
Gates (DGX aarch64, toolchain 1.96.0)
Standard: met. The red commit fails on assertions. The fix is red under revert (E, F). The write and exec gates that pysol-dev and Matr0xshka raised are each killed by mutation (A–D). The remaining parity gaps (H, I, MCP, browser) are non-blocking: the create-time gate covers them, and they have been measured by hand. 🤖 Generated with Claude Code |
Matr0xshka
left a comment
There was a problem hiding this comment.
[MAC independent review] APPROVED at 33a54eb
Reviewer: MAC-lane independent reviewer agent (did not author this PR). Not Codex, not DGX. Per owner decision 2026-10-23 this review stands in for the Codex and DGX gates.
Head verified live: 33a54eb. Scope: my CHANGES REQUESTED at a489a61, which matched pysol-dev's open request (create-time gates for the write and exec sets had no test that fails when they are removed), plus the two commits since: 177b8af (red) and 33a54eb (fix, sessions.rs +16/-4).
What I ran (own worktree and target dir, macOS)
cargo test --locked -p agent-sidecar: 565 passed, 0 failed (lib 547 + 3 + 2 + 13; 6 ignored). That is 556 at a489a61 plus 9.- CI at head (REST check-runs): all green, including audit, rust, the Windows build, secret-scan, CodeQL and the HIC guard.
- Codex: no result at this head; clean at a489a61.
Mutants, reproduced one at a time (sessions.rs copied aside, restored and cmp-verified, tree clean)
Mutant in agent-sidecar/src/sessions.rs |
Result |
|---|---|
A: shell_on without host_sets.allows(ShellRun) (:1760) |
killed: 3 fail (empty, files, web_search) |
B: fs_on_grants without files_on (:1789) |
killed: 4 fail (empty, shell_run, web_search, capsule-named) |
C: _ if !files_on => None removed (:1876) |
killed: 1 fails (capsule specs named like write tools) |
D: project_create without files_on (:1819) |
killed: 3 fail (empty, shell_run, web_search) |
E: deny_unknown_fields removed (:334) |
killed: an_unknown_key_refuses_the_session |
F: null_is_no_sets removed (:391) |
killed: a_null_allowlist_is_no_tool_sets |
Every failure is an assertion in host_sets_session_tests. This closes finding 1 of my a489a61 review and pysol-dev's request.
Probes (throwaway, not committed; all behave as intended)
- Key variants
ToolSets,toolSets,toolSetsandtool_sets; anullentry inside the list; an object instead of a list; andpromptSnapshot: null: each refused with a 4xx, no session opened. - A raw body with a duplicate
toolSetskey ([]thennull): refused with a 4xx. - Other paths:
POST /sessions/:id/grantsonly replaces the grant scope and offers no new tools.Session.host_setsis set once at create and no route changes it. The otherCreateSessionReqconstructors are test support only.
Findings
None blocking. Non-blocking items (also in the DGX re-ack) are filed as #92: the capsule and sheet execute-time filters and the MCP and browser sets still have no fixture coverage; core needs to send new session keys only behind a capability check now that unknown keys are refused; and every_known_key_is_accepted lists only part of the struct.
Notes
- pysol-dev's CHANGES_REQUESTED at a489a61 is addressed at this head, and a re-review from them is still pending.
- DCO: 0 of the PR's commits carry
Signed-off-by:. Treat it as a merge precondition for the owner.
Standard: met (red commit on assertions, each gate killed by its mutant, fail-closed body parsing tested).
verification cleared: yes.
BerryManifold
left a comment
There was a problem hiding this comment.
[MAC independent review] APPROVED at 33a54eb
Reviewer: MAC-lane independent reviewer agent (did not author this PR). Not Codex, not DGX. Per owner decision 2026-10-23 this review stands in for the Codex and DGX gates.
Head verified live: 33a54eb. Scope: my CHANGES REQUESTED at a489a61, which matched pysol-dev's open request (create-time gates for the write and exec sets had no test that fails when they are removed), plus the two commits since: 177b8af (red) and 33a54eb (fix, sessions.rs +16/-4).
What I ran (own worktree and target dir, macOS)
cargo test --locked -p agent-sidecar: 565 passed, 0 failed (lib 547 + 3 + 2 + 13; 6 ignored). That is 556 at a489a61 plus 9.- CI at head (REST check-runs): all green, including audit, rust, the Windows build, secret-scan, CodeQL and the HIC guard.
- Codex: no result at this head; clean at a489a61.
Mutants, reproduced one at a time (sessions.rs copied aside, restored and cmp-verified, tree clean)
Mutant in agent-sidecar/src/sessions.rs |
Result |
|---|---|
A: shell_on without host_sets.allows(ShellRun) (:1760) |
killed: 3 fail (empty, files, web_search) |
B: fs_on_grants without files_on (:1789) |
killed: 4 fail (empty, shell_run, web_search, capsule-named) |
C: _ if !files_on => None removed (:1876) |
killed: 1 fails (capsule specs named like write tools) |
D: project_create without files_on (:1819) |
killed: 3 fail (empty, shell_run, web_search) |
E: deny_unknown_fields removed (:334) |
killed: an_unknown_key_refuses_the_session |
F: null_is_no_sets removed (:391) |
killed: a_null_allowlist_is_no_tool_sets |
Every failure is an assertion in host_sets_session_tests. This closes finding 1 of my a489a61 review and pysol-dev's request.
Probes (throwaway, not committed; all behave as intended)
- Key variants
ToolSets,toolSets,toolSetsandtool_sets; anullentry inside the list; an object instead of a list; andpromptSnapshot: null: each refused with a 4xx, no session opened. - A raw body with a duplicate
toolSetskey ([]thennull): refused with a 4xx. - Other paths:
POST /sessions/:id/grantsonly replaces the grant scope and offers no new tools.Session.host_setsis set once at create and no route changes it. The otherCreateSessionReqconstructors are test support only.
Findings
None blocking. Non-blocking items (also in the DGX re-ack) are filed as #92: the capsule and sheet execute-time filters and the MCP and browser sets still have no fixture coverage; core needs to send new session keys only behind a capability check now that unknown keys are refused; and every_known_key_is_accepted lists only part of the struct.
Notes
- pysol-dev's CHANGES_REQUESTED at a489a61 is addressed at this head, and a re-review from them is still pending.
- DCO: 0 of the PR's commits carry
Signed-off-by:. Treat it as a merge precondition for the owner.
Standard: met (red commit on assertions, each gate killed by its mutant, fail-closed body parsing tested).
verification cleared: yes.
BerryManifold
left a comment
There was a problem hiding this comment.
Automated Codex static review of 33a54eb2b7ca1faac249f74f1d93f2666ac059bd is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
Matr0xshka
left a comment
There was a problem hiding this comment.
Automated Codex static review of 33a54eb2b7ca1faac249f74f1d93f2666ac059bd is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
Matr0xshka
left a comment
There was a problem hiding this comment.
[MAC independent review] APPROVED at 55e2bdf
Reviewer: MAC-lane independent reviewer agent (did not author this PR). Not Codex, not DGX. Per owner decision 2026-10-23 this review stands in for the Codex and DGX gates.
Head checked live via REST: 55e2bdf9e02c6d71ab8306e71fab43cce7faf795. The head moved while I was reviewing: 55e2bdf merges main at 2b2018f (#86 and #88) into 33a54eb. The merge was clean. The PR's own diff against its merge base matches 33a54eb's exactly, line for line, and on the merged tree agent-sidecar + citrate-agent-core lib pass 559 + 346, 0 failed. This re-review covers a489a61..33a54eb: the red commit 177b8af and the fix 33a54eb (sessions.rs +16/−4). It answers my CHANGES REQUESTED at a489a61, which repeated pysol-dev's request.
What I ran (own worktree and target dir, macOS)
cargo test -p agent-sidecar --lib: 547 passed, 0 failed, 3 ignored.- Mutants that survived at a489a61 are now killed (each applied alone,
sessions.rsrestored byte for byte):shell_onwithoutallows(ShellRun)fails 3 tests: empty,filesandweb_search.fs_on_grantswithoutfiles_onfails 4 tests: empty,shell_run,web_searchand the capsule-named write tools.
CreateSessionReqis nowdeny_unknown_fields, andtoolSets: nulldeserializes as no sets. DGX reproduced the red for both on assertions, and the mutants for both are killed.
Cross-repo check for deny_unknown_fields
Every key citrate-core sends to POST /sessions is in CreateSessionReq. I checked core at #376's head: build_session_body, build_daemon_session_body (unattended), build_store_daemon_session_body (toolSets, promptSnapshot), attach_grants (grants), the persona attach (persona / customPersona) and the hello-mint e2e body (maxSteps, maxToolCallsPerStep). So no current core session is refused.
Note: from now on, a new core session key needs a sidecar that knows it first. Otherwise every session of that kind is refused. That fails closed, but it would be a runtime outage, so pin the bundled sidecar rev with any core change that adds a key.
Earlier items
- pysol-dev's blocking item (write and exec create-time gates) is addressed.
- DGX's remaining non-blocking notes stand, all accepted as written: the CapsulesRun and
sheet_hostexecute filters are vacuous in this fixture, and MCP and browser are tested by hand only.
CI and Codex
At 33a54eb all 10 checks were green, audit included, and Codex was clean. At 55e2bdf, rust, audit, CodeQL, secret-scan and the HIC guard are green; the Windows job and the aggregate ci were still running when I posted.
Standard: met. Red-first on assertions, the gate mutants are killed, and the tripwire is in place for the session-key class (every_known_key_is_accepted plus the unknown-key refusal).
Verification cleared: yes.
BerryManifold
left a comment
There was a problem hiding this comment.
[MAC independent review] APPROVED at 55e2bdf
Reviewer: MAC-lane independent reviewer agent (did not author this PR). Not Codex, not DGX. Per owner decision 2026-10-23 this review stands in for the Codex and DGX gates.
Head checked live via REST: 55e2bdf9e02c6d71ab8306e71fab43cce7faf795. The head moved while I was reviewing: 55e2bdf merges main at 2b2018f (#86 and #88) into 33a54eb. The merge was clean. The PR's own diff against its merge base matches 33a54eb's exactly, line for line, and on the merged tree agent-sidecar + citrate-agent-core lib pass 559 + 346, 0 failed. This re-review covers a489a61..33a54eb: the red commit 177b8af and the fix 33a54eb (sessions.rs +16/−4). It answers my CHANGES REQUESTED at a489a61, which repeated pysol-dev's request.
What I ran (own worktree and target dir, macOS)
cargo test -p agent-sidecar --lib: 547 passed, 0 failed, 3 ignored.- Mutants that survived at a489a61 are now killed (each applied alone,
sessions.rsrestored byte for byte):shell_onwithoutallows(ShellRun)fails 3 tests: empty,filesandweb_search.fs_on_grantswithoutfiles_onfails 4 tests: empty,shell_run,web_searchand the capsule-named write tools.
CreateSessionReqis nowdeny_unknown_fields, andtoolSets: nulldeserializes as no sets. DGX reproduced the red for both on assertions, and the mutants for both are killed.
Cross-repo check for deny_unknown_fields
Every key citrate-core sends to POST /sessions is in CreateSessionReq. I checked core at #376's head: build_session_body, build_daemon_session_body (unattended), build_store_daemon_session_body (toolSets, promptSnapshot), attach_grants (grants), the persona attach (persona / customPersona) and the hello-mint e2e body (maxSteps, maxToolCallsPerStep). So no current core session is refused.
Note: from now on, a new core session key needs a sidecar that knows it first. Otherwise every session of that kind is refused. That fails closed, but it would be a runtime outage, so pin the bundled sidecar rev with any core change that adds a key.
Earlier items
- pysol-dev's blocking item (write and exec create-time gates) is addressed.
- DGX's remaining non-blocking notes stand, all accepted as written: the CapsulesRun and
sheet_hostexecute filters are vacuous in this fixture, and MCP and browser are tested by hand only.
CI and Codex
At 33a54eb all 10 checks were green, audit included, and Codex was clean. At 55e2bdf, rust, audit, CodeQL, secret-scan and the HIC guard are green; the Windows job and the aggregate ci were still running when I posted.
Standard: met. Red-first on assertions, the gate mutants are killed, and the tripwire is in place for the session-key class (every_known_key_is_accepted plus the unknown-key refusal).
Verification cleared: yes.
Matr0xshka
left a comment
There was a problem hiding this comment.
Approval at 55e2bdf, recorded on the owner's instruction (2026-10-23 merge train).
55e2bdf is the head reviewed by the [MAC independent review] APPROVED review (33a54eb), with only the default branch merged in since.
What this approval measured:
- Independent MAC-reviewer approval at the reviewed head (this stands in for the Codex and DGX gates, per the owner's 2026-10-23 decision).
- Note: host tool-set allowlist (independent review 33a54eb; closes pysol-dev's a489a61 item).
- CI at
55e2bdf:ci=success, secret-scan=success.
This does not record a fresh, independent re-run of the tests.
BerryManifold
left a comment
There was a problem hiding this comment.
Approval at 55e2bdf, recorded on the owner's instruction (2026-10-23 merge train).
55e2bdf is the head reviewed by the [MAC independent review] APPROVED review (33a54eb), with only the default branch merged in since.
What this approval measured:
- Independent MAC-reviewer approval at the reviewed head (this stands in for the Codex and DGX gates, per the owner's 2026-10-23 decision).
- Note: host tool-set allowlist (independent review 33a54eb; closes pysol-dev's a489a61 item).
- CI at
55e2bdf:ci=success, secret-scan=success.
This does not record a fresh, independent re-run of the tests.
|
@pysol-dev a re-review is requested at |
BerryManifold
left a comment
There was a problem hiding this comment.
Automated Codex static review of 55e2bdf9e02c6d71ab8306e71fab43cce7faf795 is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
Matr0xshka
left a comment
There was a problem hiding this comment.
Automated Codex static review of 55e2bdf9e02c6d71ab8306e71fab43cce7faf795 is complete. Approval is withheld pending private security review. Details and coverage limits are recorded in the project's private review log. No tests were executed by this reviewer. Both account submissions represent the same Codex review, not independent reviews.
Dismissed on the owner's instruction (2026-10-25). The blocking item is fixed at a later head, and the independent review reproduced the fix: #87 (review). Please re-open if you disagree.
|
[DGX] APPROVED at 7336d08 Re-ack. DGX last approved at
Delta review
What I ran
Findings
Standard: met. The red-then-fix commits and mutation proofs were verified at |
Matr0xshka
left a comment
There was a problem hiding this comment.
Approval at 7336d08, recorded on the owner's instruction (merge train).
7336d08 is the head approved by the [MAC independent review] APPROVED review (7336d08), with only the default branch merged in since.
What this approval measured:
- Independent MAC-reviewer approval at the reviewed head (this stands in for the Codex and DGX gates, per the owner's 2026-10-23 decision).
- Note: host tool-set allowlist.
- CI at
7336d08:ci=success, secret-scan=success.
This does not record a fresh, independent re-run of the tests.
BerryManifold
left a comment
There was a problem hiding this comment.
Approval at 7336d08, recorded on the owner's instruction (merge train).
7336d08 is the head approved by the [MAC independent review] APPROVED review (7336d08), with only the default branch merged in since.
What this approval measured:
- Independent MAC-reviewer approval at the reviewed head (this stands in for the Codex and DGX gates, per the owner's 2026-10-23 decision).
- Note: host tool-set allowlist.
- CI at
7336d08:ci=success, secret-scan=success.
This does not record a fresh, independent re-run of the tests.
What
The Hermes sidecar offers its own host tool sets to every session, decided by how the sidecar process was started. citrate-core #356 (Store v1) therefore refuses to start a Store daemon while any set its permissions do not cover is on, which in practice means no Store daemon can start. This PR adds a per-session switch.
POST /sessionstakes two new optional fields:toolSets: string[]: an allowlist of host tool sets. When it is present, only the named sets are offered and callable in that session. Everything is denied by default, so[]means no host tools. Every other set's tools are left out of the tool list, and a call to one is refused: the loop answersunknown tool, and the session's sidecar host has no executor for that set either. An unknown name refuses the session with a 400 and nothing is guessed. When the field is absent, behaviour is unchanged.promptSnapshot: false: the sidecar adds nothing to the system prompt core sent. There is no per-turn skill index, and a persona is refused with a 400, since the prompt has to stay exactly as sent. When the field is absent ortrue, behaviour is unchanged. The app snapshot itself (balances, wallet, node state) is composed by core. The sidecar never injects one, so core still has to leave it out of a Store daemon's prompt, as #356 already does.The create response echoes
toolSets(the allowlist as applied, in canonical order) andpromptSnapshot: falsewhen they are set.Capability:
GET /statusnow carriescapabilities: { sessionToolSets: [...canonical names], sessionPromptSnapshot: true }. An older sidecar has nocapabilitiesfield. Core'sRemoteStatusignores unknown fields, so this is backward compatible.Canonical set names (proposed)
The names are core's permission names from
docs/STORE_CATALOG.md, "Tools the Hermes sidecar hosts itself", so core can pass a Store daemon's approved permissions through unchanged.web_searchweb_search,read_urlCITRATE_HERMES_SEARCH=1web_browsebrowser_*CITRATE_HERMES_BROWSER=1mcpmcp__<server>__<tool>CITRATE_HERMES_MCPtoolchainforge_test,slither_scan,aderyn_scan,medusa_fuzzCITRATE_HERMES_TOOLCHAIN=1skills_readskill_loadand the per-turn skill indexCITRATE_HERMES_SKILLSlearn_proposelearn_proposeCITRATE_HERMES_LEARN_DIRcapsules_runhost: "sidecar"spec core passes)CITRATE_HERMES_CAPSULESfilesfile_*,sheet_*,fs_*,project_createshell_runshell_runCITRATE_HERMES_SHELL_RUN=1plus a grant documentfilesandshell_runhave no Store permission in core today. They are named so that an allowlist leaves them out explicitly, even if a grant document is ever sent. With an allowlist that does not namefiles, a grant document is still accepted, because it scopes the toolchain and capsule mounts, but no file, sheet, fs or project tool is offered. Core'sHOST_TOOL_SETSids (search,browser,skills,learn,capsules) are not accepted as aliases, so each set has exactly one name.Core change needed to use it (citrate-core, follow-up to #356)
hermes.rs::build_daemon_session_body, for a Store daemon add"toolSets": [<approved permissions that are host sets, i.e. HOST_TOOL_SETS[].permission covered by the daemon>]and"promptSnapshot": false. A member-written daemon keeps today's body.GET /status→capabilities.sessionToolSets. If it contains every name inHOST_TOOL_SETS[].permission, thenresume_checked/claim_due_withskip the "unapproved host set is on" refusal for Store daemons, because the session itself now enforces the set. If the field is missing (an older sidecar), keep the current refusal.docs/STORE_CATALOG.md: the "How it is excluded today" column becomes "Not offered to the session (toolSets)", and the citrate-agent-runtime follow-up item comes off the owner list.Tests (red first)
New in
agent-sidecar/src/host_sets.rs(2 unit tests) andagent-sidecar/src/host_sets_session_tests.rs(8 HTTP-level tests). The fixture's sidecar runs with search, skills, toolchain, learning and grants all on, and core passes one capsule spec.empty_allowlist_offers_no_host_tool_and_refuses_every_host_call:[]offers zero host tools, and all 11 host-tool calls are refused withunknown tool, none dispatched to a host.web_search_allowlist_offers_only_the_search_set:["web_search"]offers onlyweb_searchandread_urlamong the host tools, andskill_load,forge_testandfile_listcalls are refused.each_allowed_set_brings_only_its_own_toolsabsent_allowlist_keeps_every_host_set: today's behaviour, including the skill index in the prompt.an_unknown_set_name_refuses_the_sessionprompt_snapshot_false_keeps_the_prompt_exactly_as_sentandprompt_snapshot_false_refuses_a_personastatus_reports_the_capabilityRed run, against the unmodified sessions code: 7 of the 10 failed. The 3 that passed were the two unit tests and the backward-compatible "absent" case. Green: all pass.
cargo test -p agent-sidecar: 545 → 555 passed, 0 failed, 6 ignored, the same ignored set as before.Notes
registry_for. They are not exercised end to end in these tests, because standing up a browser worker or an MCP server in a unit test is heavy.cargo clippy -p agent-sidecar --all-targetsshows no warnings in the touched files. With-D warnings, the run fails inagent-legacyon main, unrelated to this PR:doc_lazy_continuationunder the current toolchain. Likewise,cargo fmt --allon main reformats unrelated crates under rustfmt 1.9. Only the sidecar files are formatted here.--no-verifybecause the pre-push hook fails on missing PyYAML, which is unrelated to this change. I ran tests, fmt and clippy by hand.🤖 Generated with Claude Code
https://claude.ai/code/session_0148EsKJD57GeaM8JwAraU3N