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:
+31
-2
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user