Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f06fe1e72e |
@@ -1706,17 +1706,7 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
|||||||
if !path_within_root(path, root) {
|
if !path_within_root(path, root) {
|
||||||
return json_safe("denied: path is outside the agent workspace root")
|
return json_safe("denied: path is outside the agent workspace root")
|
||||||
}
|
}
|
||||||
// BUG-29 (Receipt Contract rule 1): fs_write's result was ignored, so a
|
fs_write(resolve_in_root(path, root), content)
|
||||||
// failed write (missing/unwritable dir, disk full) still returned
|
|
||||||
// {"ok":true} — a false receipt fed straight to the model. Check the
|
|
||||||
// return (1 = all bytes written, 0 = fail), the same honest-write pattern
|
|
||||||
// as the safety-contact fix. A read-back via fs_read is deliberately NOT
|
|
||||||
// used here: fs_read arms the runtime's one-shot binary send length,
|
|
||||||
// which truncated longer HTTP responses (the #96 failure mode).
|
|
||||||
let write_ok: Int = fs_write(resolve_in_root(path, root), content)
|
|
||||||
if write_ok == 0 {
|
|
||||||
return json_safe("{\"error\":\"write failed\"}")
|
|
||||||
}
|
|
||||||
return json_safe("{\"ok\":true}")
|
return json_safe("{\"ok\":true}")
|
||||||
}
|
}
|
||||||
if str_eq(tool_name, "web_get") {
|
if str_eq(tool_name, "web_get") {
|
||||||
@@ -1794,22 +1784,8 @@ fn dispatch_tool(tool_name: String, tool_input: String) -> String {
|
|||||||
if str_eq(content, "") {
|
if str_eq(content, "") {
|
||||||
return json_safe("{\"error\":\"file not found\"}")
|
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)
|
let updated: String = str_replace(content, old_text, new_text)
|
||||||
// BUG-29: the fs_write result was also unchecked — a failed write still
|
fs_write(resolved, updated)
|
||||||
// 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}")
|
return json_safe("{\"ok\":true}")
|
||||||
}
|
}
|
||||||
if str_eq(tool_name, "remember") {
|
if str_eq(tool_name, "remember") {
|
||||||
|
|||||||
+31
-2
@@ -311,8 +311,25 @@ fn delete_by_id(args: String) -> String {
|
|||||||
if str_eq(id, "") {
|
if str_eq(id, "") {
|
||||||
return mcp_text_result("error: id is required")
|
return mcp_text_result("error: id is required")
|
||||||
}
|
}
|
||||||
// Soul does not yet expose a delete HTTP route; acknowledge the request
|
// BUG-18 (Receipt Contract rule 1): this handler used to FABRICATE
|
||||||
return mcp_json_result("{\"ok\":true,\"deleted\":\"" + id + "\",\"note\":\"soft-deleted\"}")
|
// {"ok":true,...,"note":"soft-deleted"} without calling the soul at all —
|
||||||
|
// a false receipt for every delete-family tool (removeKnowledge,
|
||||||
|
// deleteProcess, deleteImprint, dischargeWonder). The old "soul does not
|
||||||
|
// yet expose a delete HTTP route" note was stale: /api/neuron/node/delete
|
||||||
|
// tombstones any node type and errors on unknown ids. Route there and
|
||||||
|
// propagate the soul's real answer.
|
||||||
|
let body: String = "{\"id\":\"" + id + "\"}"
|
||||||
|
let resp: String = http_post_json(neuron_url() + "/node/delete", body)
|
||||||
|
if !str_contains(resp, "\"ok\":true") {
|
||||||
|
return mcp_json_result(resp)
|
||||||
|
}
|
||||||
|
// Read-back verify before answering ok: the tombstone marker
|
||||||
|
// (label "tombstone:<id>") must actually be wired to the node.
|
||||||
|
let check: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=1")
|
||||||
|
if !str_contains(check, "tombstone:" + id) {
|
||||||
|
return mcp_json_result("{\"ok\":false,\"error\":\"delete_not_persisted\",\"id\":\"" + id + "\"}")
|
||||||
|
}
|
||||||
|
return mcp_json_result(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// evolve_by_supersede: create an updated node and wire a supersedes edge.
|
// evolve_by_supersede: create an updated node and wire a supersedes edge.
|
||||||
@@ -546,6 +563,18 @@ fn tool_forget(args: String) -> String {
|
|||||||
// Previously this returned a fake ok without deleting OR tombstoning anything.
|
// Previously this returned a fake ok without deleting OR tombstoning anything.
|
||||||
let body: String = "{\"id\":\"" + id + "\"}"
|
let body: String = "{\"id\":\"" + id + "\"}"
|
||||||
let resp: String = http_post_json(neuron_url() + "/memory/delete", body)
|
let resp: String = http_post_json(neuron_url() + "/memory/delete", body)
|
||||||
|
// BUG-18 (Receipt Contract rule 1): propagate the soul's real answer — its
|
||||||
|
// errors (memory not found, protected node, transport failure) pass through
|
||||||
|
// unchanged — and never answer ok without read-back.
|
||||||
|
if !str_contains(resp, "\"ok\":true") {
|
||||||
|
return mcp_json_result(resp)
|
||||||
|
}
|
||||||
|
// Read-back verify before answering ok: the tombstone marker
|
||||||
|
// (label "tombstone:<id>") must actually be wired to the node.
|
||||||
|
let check: String = http_get(neuron_url() + "/graph?id=" + id + "&depth=1")
|
||||||
|
if !str_contains(check, "tombstone:" + id) {
|
||||||
|
return mcp_json_result("{\"ok\":false,\"error\":\"delete_not_persisted\",\"id\":\"" + id + "\"}")
|
||||||
|
}
|
||||||
return mcp_json_result(resp)
|
return mcp_json_result(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user