fix(mcp-wrapper): forget/delete tools no longer return fake ok receipts (BUG-18)

Root cause: two false-receipt paths in the wrapper's delete family.
- delete_by_id (removeKnowledge, deleteProcess, deleteImprint,
  dischargeWonder) FABRICATED {"ok":true,...,"note":"soft-deleted"}
  without calling the soul at all — the 'soul does not yet expose a delete
  HTTP route' note was stale (/api/neuron/node/delete exists and tombstones
  any node type).
- tool_forget forwarded the soul's response but never verified the deletion
  actually persisted before answering ok.

The change (Receipt Contract rule 1 — a tool result must reflect what
actually happened):
- delete_by_id now routes to the soul's real /api/neuron/node/delete and
  propagates its answer (honest 'node not found' for bad ids).
- Both handlers read back before answering ok: GET /api/neuron/graph?id=..
  &depth=1 must show the tombstone marker (label "tombstone:<id>"); if it
  does not, answer {"ok":false,"error":"delete_not_persisted",...} in
  the soul's not-persisted error shape (api_not_persisted).
- Soul errors and transport failures pass through unchanged.

E2E evidence (sandbox soul :7791 + wrapper :7792, elb builds):
- unpatched: removeKnowledge on a NONEXISTENT id -> {"ok":true,
  "deleted":"kn-DOES-NOT-EXIST-deadbeef","note":"soft-deleted"} (lie)
- patched:   same call -> {"error":"node not found: ..."} (soul's answer)
- happy path: remember -> forget -> {"ok":true,"tombstoned":true};
  read-back: hidden from default /list/Memory, present with
  ?include_deleted=1, node KEPT in full graph view (immutability intact)
- scripts/verify-soul-contract.sh on the soul it talks to: GATE PASS
  (27/27 presence + immutability)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Tim Lingo
2026-07-22 16:24:55 -05:00
parent b784750f69
commit f06fe1e72e
+31 -2
View File
@@ -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:<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.
@@ -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:<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)
}