From 62c562a3f18b9859500560e3a980f29d121bc30d Mon Sep 17 00:00:00 2001 From: Tim Lingo <1timlingo@gmail.com> Date: Wed, 22 Jul 2026 16:19:09 -0500 Subject: [PATCH] fix(chat): agent write_file/edit_file no longer return false success receipts (BUG-29) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: dispatch_tool's write_file returned {"ok":true} without checking fs_write's result, and edit_file returned ok:true even when old_text was absent (str_replace silently no-ops) and its fs_write was also unchecked. Any failed or no-op write fed the model a false success receipt, which it then repeated to the user as fact. The change (Receipt Contract rule 1 — a tool result must reflect what actually happened): - write_file: check fs_write's return (1 = all bytes written, 0 = fail); on failure return {"error":"write failed"} in the handler's existing error-JSON shape. - edit_file: reject empty old_text, verify old_text is actually present (str_contains) before replacing, and check the fs_write result the same way. - Verification is by operation result, NOT an fs_read read-back: fs_read arms the runtime's one-shot binary send length, the exact mechanism that truncated the safety-contact response (#96). Same honest-write pattern as that fix. E2E evidence (sandboxed elb build, dispatch_tool driven directly): - unpatched: write_file into a chmod-000 dir -> {"ok":true} (lie); edit_file with absent old_text -> {"ok":true} (lie, file untouched) - patched: same calls -> {"error":"write failed"} / {"error":"old_text not found in file"}; happy paths still ok:true - scripts/verify-soul-contract.sh on the patched soul: GATE PASS (27/27 presence + immutability) Co-Authored-By: Claude Fable 5 --- chat.el | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/chat.el b/chat.el index f29ee92..4de2f22 100644 --- a/chat.el +++ b/chat.el @@ -1680,13 +1680,18 @@ 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") } - // 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. + // BUG-29 (Receipt Contract rule 1) + BUG-6 (disk truth): fs_write's result + // was ignored, so a failed write (missing/unwritable dir, disk full) still + // returned {"ok":true} — a false receipt fed straight to the model. Check + // fs_write's return (1 = all bytes written, 0 = fail): an operation-result + // check, stronger than a post-hoc fs_exists, which false-passes when an + // overwrite fails but the stale file remains. A read-back via fs_read is + // deliberately NOT used: fs_read arms the runtime's one-shot binary send + // length, which truncated longer HTTP responses (the #96 failure mode). + // Still return the RESOLVED path so callers/model narrate only disk truth. let dest: String = resolve_in_root(path, root) - fs_write(dest, content) - if !fs_exists(dest) { + let write_ok: Int = fs_write(dest, content) + if write_ok == 0 { return json_safe("{\"error\":\"write failed - nothing landed at " + dest + "\"}") } return json_safe("{\"ok\":true,\"path\":\"" + dest + "\"}") @@ -1766,8 +1771,22 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String { if str_eq(content, "") { return json_safe("{\"error\":\"file not found\"}") } + // BUG-29 (Receipt Contract rule 1): when old_text was absent (or empty), + // str_replace was a silent no-op and the handler still claimed ok:true — + // a false receipt. Verify the text is actually present before replacing. + if str_eq(old_text, "") { + return json_safe("{\"error\":\"old_text is required\"}") + } + if !str_contains(content, old_text) { + return json_safe("{\"error\":\"old_text not found in file\"}") + } let updated: String = str_replace(content, old_text, new_text) - fs_write(resolved, updated) + // BUG-29: the fs_write result was also unchecked — a failed write still + // returned ok:true. Same honest-write check as write_file above. + let write_ok: Int = fs_write(resolved, updated) + if write_ok == 0 { + return json_safe("{\"error\":\"write failed\"}") + } return json_safe("{\"ok\":true}") } if str_eq(tool_name, "remember") {