fix(engine): honor require_approval — the pause contract, implemented (PAUSE-CONTRACT + BUG-LEAK source fixes)
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>
This commit is contained in:
@@ -677,6 +677,11 @@ fn handle_session_approve(session_id: String, body: String) -> String {
|
||||
// path for all sessions created through handle_chat_agentic / agentic_loop.
|
||||
let bridge_blob: String = state_get("mcp_bridge:" + session_id)
|
||||
if !str_eq(bridge_blob, "") {
|
||||
// BUG-LEAK fix (2026-07-16): the approved tool executes below via dispatch_tool,
|
||||
// whose path/command guards read the shared workspace-root key. Re-assert THIS
|
||||
// session's own root first — an approval must never execute under whatever root
|
||||
// the last unrelated request left behind.
|
||||
state_set("agent_workspace_root", state_get("agent_workspace_root_" + session_id))
|
||||
// For "always": record tool_name in the always-allow list before resuming.
|
||||
// The tool_name is not stored in the bridge blob (only tool_use_id is).
|
||||
// Accept it from the body so the client can pass it along.
|
||||
|
||||
Reference in New Issue
Block a user