Two compounding defects made every pause->approve write_file report success
while writing NOTHING:
1. The naive json_get scanner matches "content" anywhere in the approve
body — including INSIDE tool_input, which for write_file always carries
a content field. The handler therefore treated every approved builtin
write as already-client-executed, skipped dispatch entirely, and handed
the model the file's own content as the 'tool result'. The model then
narrated 'Done, created' — a false receipt with no file. Builtin tools
now ALWAYS dispatch server-side; client content is only honored for
non-builtin (MCP/client-executed) tools. Stricter only.
2. write_file returned {"ok":true} unconditionally — fs_write's outcome
was never checked, so any failed write also reported success. The write
now verifies the file landed (fs_exists) and returns the RESOLVED path
in the ok payload; failures return a real error naming the destination.
E2E on the test brain (boot 38): approve-path write lands byte-exact and
the result carries the resolved path; auto-run writes unchanged; denied
writes execute nothing. BUG-5 (approve wire lacked tool_name) had been
masking this one — two stacked bugs on the same path.
NOTE for review: the deeper cure is a nesting-aware json reader; this fix
removes the dangerous consequence at the two spots that lie about disk.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two consent-flow fixes, gates only get stricter:
1. PAUSE-CONTRACT: the client has sent require_approval:true on every
agentic request since Phase 1c, but needs_bridge never consulted it —
builtin sub-escalate tools ran server-side unasked, making the app's
Ask autonomy silently inert for that whole class. Now the flag is
persisted per session (set/reset every request, so /approve resumes
keep it) and ask_all bridges EVERY tool turn. Absent/false = behavior
byte-identical to before. E2E: the exact probe that executed a write
unasked now returns the tool_pending envelope with nothing on disk;
full in-app circle verified (card → crash → resurrection → late
approve → fence re-fires on re-entry).
2. BUG-LEAK: agent_workspace_root lived in ONE shared state key — any
request that omitted a root inherited the previous session's folder
(proven: a rootless curl session wrote into another session's run
folder). Root is now stored per session and every request re-asserts
its own (possibly empty) root into the shared key the guards read;
same re-assert on the /approve and resume paths. Env fallback intact.
LIMITATION: assumes serialized handling; true per-call scoping means
threading session_id through dispatch — flagged for review.
Runnable C-patch for the test brain: neuron-container-build/
soul-pause-contract-20260716.patch (pause-contract only; the leak fix
needs the #23 root-write which the running C predates — source carries
both for the regen).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>