Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion contract/testdata/taxonomy/gateway-receipt-v3.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
{"name": "action-is-not-acquisition", "change": "action"},
{"name": "unsafe-integer", "change": "number"},
{"name": "duplicate-member", "change": "duplicate"},
{"name": "oversized-envelope", "change": "oversize"}
{"name": "oversized-envelope", "change": "oversize"},
{"name": "unsigned-salts-member", "change": "salts"}
]
}
294 changes: 294 additions & 0 deletions contract/vectors/tool-result/receipt-quarantine.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
{
"description": "An external read whose receipt does not verify is withheld, and nothing of its result is kept where a context reader, a later request or a Plan step could reach it (agent#242; the amendment to ADR-0027). Written by hand. Each case makes one call of a custom connected source's read-external tool, records__read_record with the arguments {\"id\":\"e0\"}, whose executor answers the sentinel text; then the five reads a model could make: list_context; read_context, read_window and search scoped to the id the result would have been retained under ($retained: each language computes it by its own retention rule, since nothing shows it); and a search of every retained item for the sentinel. items are the six tool_result events every case gives, in that order, their callId and source record left out (source-results-current.json holds the decoration): the gate's refusal, an empty listing, three refusals of the unknown id and a search with no match. Through an API adapter the listing also names the call's own operation record (kind history, label records__read_record · failed, its result the gate's refusal), which is how the failed call stays inspectable there; the engine-level tests take it out of the listing and pin it, and at the boundary and through a native adapter, whose one invocation lists no operations, there is none. The verifier is called as verifierCalls says: a missing receipt, and one over 128 UTF-16 code units, is refused before any verifier is asked, and an expired one is a receipt whose check outlasts the source's deadline (sourceCallMilliseconds), the verifier then answering true too late. overlap runs two sources' calls in flight together: the rejected call's receipt check holds until the other source's two calls (calls, receipts) have finished and been recorded; the checkpoint written then is restored into a fresh ledger. There, and in the run, the verified result is the one entry and is read whole, its event is overlap.event, and every reader still answers the rejected call with items 1 to 5. The rejected call is recorded once, as an uncertain outcome whose failure is the gate's refusal, with no result; it is never offered or accepted as Plan evidence; no byte of any checkpoint or event the run stores carries the sentinel.",
"schema": "tool-result.json",
"source": {
"harness": {
"repository": "Judgment-Pack/judgment-pack-agent",
"package": "packages/harness"
},
"files": [
"packages/harness/toolSources.ts (SourceGate.call, ReceiptAdmission, receiptAdmission)",
"packages/harness/storage/writer.ts (RunWriter.invoke, admit)",
"packages/harness/execution/controller.ts (ExecutionController.wrapRead, admission)",
"packages/harness/receiptQuarantine.test.ts (the cases each item is checked against)"
]
},
"fixture": {
"scope": "receipt-quarantine",
"sentinel": "UNVERIFIED_E0_PAYLOAD_DO_NOT_ACCEPT_9f2",
"sourceCallMilliseconds": 400,
"declaration": {
"name": "read_record",
"description": "Read one record from the connected records source.",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
},
"taxonomy": {
"operation": "read",
"effect": "read-external"
},
"source": {
"kind": "custom",
"id": "records",
"trust": "connected",
"version": "1"
}
},
"wireName": "records__read_record",
"arguments": {
"id": "e0"
},
"cases": [
{
"name": "rejected-receipt",
"result": {
"content": [
{
"type": "text",
"text": "UNVERIFIED_E0_PAYLOAD_DO_NOT_ACCEPT_9f2"
}
]
},
"receipt": "rcpt-e0",
"verifier": "rejects",
"verifierCalls": 1
},
{
"name": "missing-receipt",
"result": {
"content": [
{
"type": "text",
"text": "UNVERIFIED_E0_PAYLOAD_DO_NOT_ACCEPT_9f2"
}
]
},
"verifier": "rejects",
"verifierCalls": 0
},
{
"name": "oversized-receipt",
"result": {
"content": [
{
"type": "text",
"text": "UNVERIFIED_E0_PAYLOAD_DO_NOT_ACCEPT_9f2"
}
]
},
"receipt": "rrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr",
"verifier": "rejects",
"verifierCalls": 0
},
{
"name": "expired-receipt",
"result": {
"content": [
{
"type": "text",
"text": "UNVERIFIED_E0_PAYLOAD_DO_NOT_ACCEPT_9f2"
}
]
},
"receipt": "rcpt-e0",
"verifier": "answers true after the deadline",
"verifierCalls": 1
},
{
"name": "error-result",
"result": {
"content": [
{
"type": "text",
"text": "UNVERIFIED_E0_PAYLOAD_DO_NOT_ACCEPT_9f2"
}
],
"isError": true
},
"receipt": "rcpt-e0",
"verifier": "rejects",
"verifierCalls": 1
}
],
"readers": [
{
"tool": "list_context",
"arguments": {}
},
{
"tool": "read_context",
"arguments": {
"id": "$retained"
}
},
{
"tool": "read_window",
"arguments": {
"id": "$retained"
}
},
{
"tool": "search",
"arguments": {
"pattern": "UNVERIFIED_E0",
"id": "$retained"
}
},
{
"tool": "search",
"arguments": {
"pattern": "UNVERIFIED_E0",
"scope": "all"
}
}
],
"overlap": {
"declaration": {
"name": "read_entry",
"description": "Read one entry from the connected registry.",
"inputSchema": {
"type": "object",
"properties": {
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
},
"taxonomy": {
"operation": "read",
"effect": "read-external"
},
"source": {
"kind": "custom",
"id": "registry",
"trust": "connected",
"version": "1"
}
},
"wireName": "registry__read_entry",
"calls": [
{
"id": "b1"
},
{
"id": "b2"
}
],
"result": {
"content": [
{
"type": "text",
"text": "VERIFIED_B1_PAYLOAD"
}
]
},
"receipt": "rcpt-b1",
"verifier": "accepts its receipt for its result",
"event": {
"type": "tool_result",
"name": "registry__read_entry",
"isError": false,
"text": "VERIFIED_B1_PAYLOAD"
}
}
},
"items": [
{
"type": "tool_result",
"name": "records__read_record",
"isError": true,
"text": "External read receipt verification failed; its result was withheld. The call ran; do not retry automatically.",
"structured": {
"failure": {
"version": 1,
"code": "tool-failed",
"source": "tool",
"outcome": "unknown",
"detail": "External read receipt verification failed; its result was withheld. The call ran; do not retry automatically."
}
}
},
{
"type": "tool_result",
"name": "list_context",
"isError": false,
"text": "{\"items\":[],\"nextOffset\":null}",
"structured": {
"items": [],
"nextOffset": null
}
},
{
"type": "tool_result",
"name": "read_context",
"isError": true,
"text": "This context reference is unavailable in the current execution. Read the current object or history again.",
"structured": {
"failure": {
"version": 1,
"code": "invalid-tool-input",
"source": "tool",
"outcome": "unknown",
"detail": "This context reference is unavailable in the current execution. Read the current object or history again."
}
}
},
{
"type": "tool_result",
"name": "read_window",
"isError": true,
"text": "This context reference is unavailable in the current execution. Read the current object or history again.",
"structured": {
"failure": {
"version": 1,
"code": "invalid-tool-input",
"source": "tool",
"outcome": "unknown",
"detail": "This context reference is unavailable in the current execution. Read the current object or history again."
}
}
},
{
"type": "tool_result",
"name": "search",
"isError": true,
"text": "This context reference is unavailable in the current execution. Read the current object or history again.",
"structured": {
"failure": {
"version": 1,
"code": "invalid-tool-input",
"source": "tool",
"outcome": "unknown",
"detail": "This context reference is unavailable in the current execution. Read the current object or history again."
}
}
},
{
"type": "tool_result",
"name": "search",
"isError": false,
"text": "{\"type\":\"search\",\"pattern\":\"UNVERIFIED_E0\",\"ignoreCase\":false,\"scope\":\"all\",\"matches\":[],\"more\":null}",
"structured": {
"type": "search",
"pattern": "UNVERIFIED_E0",
"ignoreCase": false,
"scope": "all",
"matches": [],
"more": null
}
}
]
}
10 changes: 10 additions & 0 deletions docs/adr/0027-tool-taxonomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,13 @@ rows of `contract/testdata/taxonomy/tools.json` (two admitted declarations with
approval, and five refused for their description: without the words, with the words not first,
in lower case, run into another word, and with no description). `TestTaxonomyRehearseAdmissionOnly`
and the TypeScript engine-start case hold the admission-only statement above.

## Amendment: an external result is not context until its receipt verifies (agent#242)

Status: accepted for v0.12.0. Amends the receipt paragraph above; both languages, held to the hand-written `contract/vectors/tool-result/receipt-quarantine.json`.

v0.11.0 verified an external read's receipt only after the call had passed through the durable writer and the read wrapper. A result whose receipt then failed was already recorded as a successful durable result and retained in the chat's ledger: the source event said it was withheld while `list_context`, `read_context`, `read_window` and `search` returned it, later model requests carried it, a checkpoint written meanwhile held it, and its durable row was offered and accepted as Plan evidence.

The boundary is now: nothing from an external result is reusable context, or successful source evidence, until its receipt verifies. The source gate makes the receipt check before the call runs and hands it down the call (on the call's context in Go, by its deadline signal in TypeScript). The durable writer runs it after the host's execute and before it records anything of the result; the read wrapper runs it before it retains or caches anything; the gate runs it last only where nothing below did (a result served without a call). It runs once, and every later caller is given its first outcome. A result whose receipt is rejected, missing or not verified within the source's deadline (expired), including an error result, is recorded once as the call's uncertain outcome: state `unknown`, its failure the gate's refusal (`tool-failed`, `unknown`, the withheld text above), with no result bytes, observation or repeat record. It is never retained, so no reader lists or returns it, no later request or index carries it, and no Plan step is offered it or accepts it. The physical execution stays once-only and is not retried; its source-call accounting stays consumed. The check's time is the rest of the source's deadline, as before: when it runs out the result is withheld, not reported as a timeout, and since the execution itself ended in time its outcome is still recorded without closing the run's dispatch.

Quarantine choice: an unverified result is not listed at all. The failed call stays inspectable through what already records it: its `tool_result` event (`isError`, `tool-failed`, `unknown`, the withheld text), its durable dispatch row (`unknown`, with that failure) and, where the run lists its operations for the model (the API adapters; a native run's single invocation lists none), the operation record the context tools list as `<tool> · failed`, whose result is the gate's refusal. No listing state, record member or version is added, and no frozen record format changes. A checkpoint written while a receipt check runs (by another call's commit) holds only the call's claim, so a run restored from it holds no part of the result. Calls in flight together are checked each on its own; a verified result is retained as before. A verified Gateway envelope is what is retained, whole, so its unsigned `salts` may carry only the two members its commitments open (`args`, `statement`); any other member refuses the receipt in both languages (the shared `contract/testdata/taxonomy/gateway-receipt-v3.json` case `unsigned-salts-member`).
6 changes: 6 additions & 0 deletions docs/design/tool-taxonomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

Runtime must publish `{operation,effect,checkKind?,experimental?}` directly inside MCP `_meta`. Gateway snapshot-backed registrations require current V2 owner taxonomy and independent full approval context; historical V1 remains readable but cannot authorize current admission. An external read's host verifier must authenticate the receipt and bind its source, descriptor pin, tool, canonical arguments and canonical result. Returning true merely because a receipt string exists violates the host contract.

The verifier is asked before anything of the result is kept: an unverified result is never retained
for the context readers, recorded as a durable result or offered as Plan evidence, and the call
stays inspectable only as a failed operation (ADR-0027's amendment, agent#242).

Check subkinds for hosts: `validate`, `check`, `verify`, `test`, `analyze`, `advise` and, since the
ADR-0027 amendment of 2026-10-29 (#241), `rehearse`: a non-binding dry evaluation of an artifact
the host declares (a saved graph or pack identity), whose result carries no authority and whose
Expand All @@ -29,6 +33,8 @@ pin. The executor obtains `gatewayExecutionContext(signal)` / `GatewayInvocation
its receiptSession to Gateway's existing tools/call `_meta["io.judgment-pack/session"]`, separately
from arguments. It returns the actual complete MCP envelope. Agent authenticates and unwraps
the signed artifact before publication. No custom verifier runs after Gateway verification fails.
The envelope's unsigned salts may carry only `args` and `statement`; any other member refuses it,
since a verified envelope is what the run retains (agent#242).
The raw tool key comes from approved owner metadata; a frontend bridge calls the explicit
approved platform plus "." plus that key. No source identity is guessed from model text.

Expand Down
Loading
Loading