diff --git a/chat.el b/chat.el index 23ec5f0..f29ee92 100644 --- a/chat.el +++ b/chat.el @@ -1680,8 +1680,16 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String { if !path_within_root(path, root) { return json_safe("denied: path is outside the agent workspace root") } - fs_write(resolve_in_root(path, root), content) - return json_safe("{\"ok\":true}") + // BUG-6 fix (2026-07-17): never claim ok without disk truth. fs_write's result was + // never checked, so a failed write reported ok — the exact false-receipt failure + // the run guards exist to kill. Verify the file landed and return the RESOLVED + // path so callers and the model can only narrate what is really on disk. + let dest: String = resolve_in_root(path, root) + fs_write(dest, content) + if !fs_exists(dest) { + return json_safe("{\"error\":\"write failed - nothing landed at " + dest + "\"}") + } + return json_safe("{\"ok\":true,\"path\":\"" + dest + "\"}") } if str_eq(tool_name, "web_get") { let url: String = json_get(tool_input, "url") diff --git a/sessions.el b/sessions.el index 1ccc0f6..6432b81 100644 --- a/sessions.el +++ b/sessions.el @@ -713,7 +713,13 @@ fn handle_session_approve(session_id: String, body: String) -> String { // For builtin tools with no client-provided content: fall back to // dispatch_tool so those tools still execute correctly. let client_content: String = json_get(body, "content") - let use_client_content: Bool = !str_eq(client_content, "") + // BUG-6 fix (2026-07-17): the naive json_get scanner matches "content" ANYWHERE + // in the body — including INSIDE tool_input — so every approved write_file (whose + // input always carries a content field) was mistaken for client-executed, never + // dispatched, and narrated as done: a false receipt with no file on disk. Builtin + // tools now ALWAYS dispatch server-side; client content is only honored for + // non-builtin (MCP/client-executed) tools. Stricter only. + let use_client_content: Bool = !str_eq(client_content, "") && !is_builtin_tool(approve_tool_name) let use_dispatch: Bool = is_builtin_tool(approve_tool_name) && !use_client_content let raw_input: String = json_get_raw(body, "tool_input") let eff_input: String = if str_eq(raw_input, "") { "{}" } else { raw_input }