diff --git a/mcp-wrapper/src/main.el b/mcp-wrapper/src/main.el index 5c8c46a..aba5c55 100644 --- a/mcp-wrapper/src/main.el +++ b/mcp-wrapper/src/main.el @@ -311,8 +311,25 @@ fn delete_by_id(args: String) -> String { if str_eq(id, "") { return mcp_text_result("error: id is required") } - // Soul does not yet expose a delete HTTP route; acknowledge the request - return mcp_json_result("{\"ok\":true,\"deleted\":\"" + id + "\",\"note\":\"soft-deleted\"}") + // BUG-18 (Receipt Contract rule 1): this handler used to FABRICATE + // {"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:") 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. @@ -546,6 +563,18 @@ fn tool_forget(args: String) -> String { // Previously this returned a fake ok without deleting OR tombstoning anything. let body: String = "{\"id\":\"" + id + "\"}" 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:") 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) }