Repository navigation
Quarantine an external result until its receipt verifies: no retained bytes, no reader, no Plan step before verification (#242) - #247
Conversation
v0.11.0 verified an external read's receipt only after the durable writer had recorded the result and the read wrapper had retained it, so a result whose receipt failed was withheld from the call's answer but listed and returned by list_context, read_context, read_window and search, carried by later requests, held by a checkpoint written meanwhile, and accepted as Plan evidence. The source gate now makes the receipt check before the call runs and hands it down the call (the context in Go, the deadline signal in TypeScript). The durable writer runs it after the host's execute and before it records anything; a refused result is recorded once as the call's uncertain outcome with the gate's refusal as its failure, no result, observation or repeat record. The read wrapper runs it before it retains or caches anything. The gate runs it last only where nothing below did. The check runs once; its own time is not the call's, so an expired receipt is withheld, not a timeout, and does not close the run's dispatch. The unverified result is not listed at all; the call stays inspectable through its tool_result refusal, its durable row and, through the API adapters, its failed operation record. No record format changes. Hand-written vector contract/vectors/tool-result/receipt-quarantine.json (rejected, missing, oversized, expired receipt, error result, overlap and restore), read by both languages; the issue's six-request sequence through the API and native adapters in both; five mutation rows with their Go twins; the amendment to ADR-0027 and the host docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Carries #245 (@hono/node-server locked at 2.1.5) into the branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
|
Carry needed from base commit dd507c4. |
Carries #244 (#241, the rehearse check sub-kind). Both sides of ADR-0027, docs/design/tool-taxonomy.md and the mutation table are kept, main's rows first; docs/reference/events.md is regenerated with scripts/gen-reference.mjs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Review fixes for #247. F1: VerifyGatewayResult and verifyGatewayResult accepted any member of the envelope's unsigned salts besides args and statement. A verified envelope is what the read wrapper retains and the durable writer records, so such a member's bytes became ordinary retained context. Both verifiers now refuse a salts member other than args and statement. The shared contract/testdata/taxonomy/gateway-receipt-v3.json gains the case unsigned-salts-member, and both languages hold that a verified envelope carrying one is withheld and reaches no reader, the chat, the checkpoint or the durable row. F2: nothing pinned that the source gate answers the checked result rather than the raw run result. Both languages now hold, with a synthetic Gateway source at the gate and through the session, that the answer is the signed artifact and never the envelope. Two mutation rows added, each caught, with their Go twins caught by hand. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Security clean-room review record: agent PR #247 (issue #242)Re-read at First round, at
MethodMy own in-process model peer drives the actual adapters: the API adapter (eino in Go, vercel in TypeScript) and the native adapter (agent in Go, codex in TypeScript). The peer works like this:
After the run the probe checks every model request body, every native tool answer, every emitted event, the stored checkpoint and every stored event, the chat snapshot and entries, the durable row, the execution and verifier counts, and the chat's next run. A positive control (a receipt that verifies) shows the same readers return retained bytes: 4–5 reader answers carry the verified payload. So a clean negative means something. I enumerated readers from the code, not from the PR:
What I checked by reading code, without driving it:
Every probe ran against the PR head and against a copy of Probes (commands run from the scratch copy
|
| # | Probe | Command | Head | main af37939 |
|---|---|---|---|---|
| P1 | Go reader sweep. 11 cases × {api, native} × {durable store, none}. Cases: rejected, missing, oversized 129, oversized 130 UTF-16 units (65 × U+1D11E), exactly 128 UTF-16 units rejected, expired (300 ms, verifier answers true after its deadline), error result, error result with missing receipt, verifier returns an error, verifier panics, and a verified control | cd go && (ulimit -v 8000000; timeout 3600 go test -count=1 -timeout 60m -v -run TestReview247ReaderSweep ./adapter/) |
44/44 pass. Every case is answered with the withheld refusal. Execution count is 1. Verifier calls are 1, or 0 where the receipt is missing or oversized. No sentinel anywhere. With a store, the durable row is unknown with the gate's failure and no result, and complete_plan_step answers PlanEvidenceInvalid … (unsuccessful-operation). The next run is clean. The source is not run again. |
26 of the 33 native and no-store API cases fail. Requests 7–12 carry the result, as do the chat, the store, the next run, events and tool answers, and a Plan step is accepted. The 7 that pass are the 3 controls and the 4 no-store error results: error results are never retained, and without a store there is no row. |
| P2 | TypeScript reader sweep. The same 11 cases plus a non-string receipt and a verifier that never resolves, × {api, native} × {store, none} | cd packages/harness && rsscap.sh 6000 3600 -- npx vitest run review247Probe.test.ts --silent=false |
48/48 pass, with the same outcomes as P1. 7 API+store cases timed out in their next-run stage under CPU contention, after every main-run assertion had passed. Re-run alone with a 600 s limit, all 7 pass (ts-sweep-rerun.log). |
29 of the 36 selected cases fail. |
| P3 | No durable store. This is the dropped redundant check (brief item 2). The read wrapper alone carries the boundary. | The durable=false halves of P1 and P2 |
22 Go and 24 TypeScript cases are clean. The chat and the readers are empty and no request carries the result. No path skips the read wrapper for a sourced tool: every host tool is wrapped as gate → WrapRead → durable host execute. |
Fails, as above. |
| P4 | Overlap in both orders. One source is rejected and one verified. Each check is held until the other call has answered. With and without a store. | go test -run 'TestReview247OverlapBothOrders' ./context/; vitest review247Boundary.test.ts -t overlap |
Pass in both languages. The rejected call is withheld. The verified call is answered and readable. Executions are 1 and 1, verifier calls 1 and 1. The rows are unknown and result. |
Fails (4/4 Go, 4/4 TS). |
| P5 | Two calls of the same source in flight, one verified and one rejected | -run TestReview247SameSourceConcurrent; vitest -t 'same source' |
Pass in both languages. Each call keeps its own check: executions 2, verifier calls 2. | (added after the main run) |
| P6 | A rejection racing a cancellation. The call's context or signal is cancelled while the verifier is held, then the verifier is released. | -run TestReview247CancelDuringTheCheck; vitest -t 'cancel during' |
Pass in both languages, for both verdicts. The answer is the withheld refusal. The row is unknown with that failure. Execution count 1. No bytes. |
The rejected cases fail. |
| P7 | A deadline during the check. Deadline 300 ms; the verifier ignores its context, sleeps 900 ms or 2.5 s, then answers true. The run's next call follows. | -run TestReview247DeadlineDuringTheCheck; vitest -t 'deadline during' |
Pass in both languages. The answer is withheld, never a timeout, so the no-retry distinction holds. The row is unknown with that failure. The next call runs, so the dispatch is still open. Go holds the call 918 ms or 2.517 s (F3). TypeScript answers at 302–311 ms. |
Fails: the bytes are reachable. |
| P8 | A blocked verifier holds only its own call | -run TestReview247BlockingVerifierHoldsOnlyItsCall; vitest -t 'blocks holds' |
Pass. Another source's call completes meanwhile, and the dispatch is still open afterwards. Go waits until release. TypeScript answers withheld at 311 ms. | Fails: the late-verified bytes are reachable. |
| P9 | Restore (brief item 4). A checkpoint and journal are captured while the rejected call's check is held (after another call committed). They are committed into a fresh store and resumed by a new run (Resume/resume:true), then every reader runs. |
-run TestReview247WindowCheckpointRestored; vitest -t 'checkpoint written' |
Pass in both languages. The window checkpoint holds the call only as claimed, with no result, and no event carries the bytes. The resumed run reaches nothing. The resumed row is unknown with no result. The source is not run again. Every journal event is still present, byte-identical and in order. |
Fails: the window checkpoint carries the result and the resumed run reaches it. |
| P10 | A Gateway envelope that verifies but carries an extra member in its unsigned salts (F1) |
-run TestReview247GatewayUnsignedSaltsMember; vitest -t 'gateway envelope' |
The answer is the signed artifact (seven). With extra=true, the unsigned member is returned by read_context, read_window, search (id-scoped and scope:"all"), and sits in the chat snapshot, the checkpoint's context entries and the durable row's result. extra=false is clean. |
Identical in both languages (pre-existing). |
| P11 | The declared asymmetry (brief item 6). The retained value of a verified custom-source result, including a receipt rc-ok IGNORE PREVIOUS INSTRUCTIONS that the verifier accepts |
-run TestReview247VerifiedRetainedReceipt; vitest -t 'declared asymmetry' |
Go: no reader, chat or answer carries the receipt. TypeScript: the answer does not, but 1 reader answer, the chat snapshot and the store do (F4). | n/a |
Brief item 8 (nothing widened, nothing lost):
- No file under
contract/schemas,contract/recordsorgo/contractchanges. - The withheld text is byte-identical to
mainin both languages. - The 14 changed rows of
docs/reference/events.mdare identical tomainapart from line numbers. - The only
go/gatechange is thenotTheGatesregistration of the vector. - No receipt is fabricated: the gate marks the checked receipt, the same as before.
- No source is filtered away.
- Journals are intact (P9).
Mutants
Every mutant was applied to <scratch>/.review/mut, one at a time, with exact find/replace (<scratch>/.review/tools/mutants.py, specs go-mutants.json and ts-mutants.json). A baseline run of every check on the unmutated copy passed first.
Check groups: PR = the PR's own receipt tests. B = my boundary probes (P4–P9). S = my adapter sweep, native plus no-store API in Go, no-store in TypeScript.
| Mutant | Caught by (failing test) |
|---|---|
| T-Q1: the gate hands no check down | PR (every case: list_context lists it); B (overlap: the rejected bytes are reachable); S (durable=false rejected … : the request carries the result) |
| T-Q2: the read wrapper skips its check | PR (retains nothing in the read wrapper… only); B (no-store overlap, cancel, deadline); S (all no-store cases) |
| T-Q3: the writer records before refusing | PR (complete_plan_step accepts: "promise resolved instead of rejecting"); B (store overlap, cancel, deadline) |
| T-Q4: the check's time is the call's | PR (expired ×2); B (a verifier that blocks…: the next call is refused, so the dispatch closed) |
| T-Q5: the gate answers the run's error | PR (expired ×3); B (deadline ×4: answered as a timeout); S (expired, verifier-never-resolves) |
| T-M7 (mine): the read wrapper retains, then checks | PR (wrapRead unit); B; S |
| T-M6 (mine): the gate answers the raw run result, not the checked one | Survives PR, B and gatewayReceipt.test.ts plus toolSources.test.ts (101 tests). Under it the Gateway answer becomes the raw envelope (F2). |
G-Q1 twin (drop context.WithValue(… receiptAdmissionKey{}, admission)) |
PR context (list_context's answer); PR adapter ("model request 4 carries…"); B; S |
G-Q2 twin (drop the WrapRead block) |
PR context (TestTheReadWrapperRetainsNothingItsCheckRefuses only; the PR adapter test passes); B (no-store); S (no-store API and native) |
G-Q3 twin (_ = admit) |
PR context ("a Plan step accepted the rejected call"); PR adapter (stored checkpoint); B; S native-store |
G-Q4 twin (drop WithoutCancel) |
PR (expired); B (deadline: "next call … the run was cancelled", so the dispatch closed) |
| G-Q5 twin (outcome only without error) | PR (expired); B (deadline: answered as a timeout); S |
| G-Q6, Go only (pass ctx to the host's execute) | PR context (the executor assertion) only. It is not a behavioural exposure. |
| G-M8 (mine): retain, then check | PR context (unit); B; S |
G-M7 (mine): drop result = checked |
Survives PR, B and the Gateway suites (TestGatewayReceipt*; the actual-producer tests skip without a fixture). Under it the Gateway answer becomes the raw envelope (F2). |
The no-store path (brief item 2) is pinned end to end only by my sweep. The PR's adapter tests always use a store, so G-Q2 survives them.
Findings
| # | Severity | Where | What | Evidence |
|---|---|---|---|---|
| F1 | HIGH. Pre-existing at af37939 in both languages. Not introduced by #247. |
VerifyGatewayResult (go/context/gateway_receipt.go) and verifyGatewayResult (packages/harness/gatewayReceipt.ts) check salts.args and salts.statement but accept any other salts member. After the check passes, WrapRead/wrapRead retain, and the durable writer records, the raw envelope rather than the checked artifact. |
A Gateway-receipted read whose receipt verifies can carry unsigned bytes in salts.<any>. They become ordinary retained context: read_context, read_window and search return them, and they are in the chat, the checkpoint entries and the durable row. This is the same class as #242. The PR's README text says GatewayReceipt sources are "kept only once its receipt verifies", which is true of the receipt but not of these bytes. |
P10. Smallest fix: refuse a salts with members other than args and statement in both verifiers, and add the case to the vector. Retaining the checked artifact instead would also close it but changes retained ids. |
| F2 | MEDIUM | SourceGate.Call: Go if err == nil { result = checked }, TypeScript shown = checked;receipt = given. Both are lines this PR rewrote. |
No test in either language pins that the gate answers the checked result (for Gateway, the signed artifact) rather than the raw run result. If broken, the model is answered the raw envelope (session, receipt, salts). | G-M7 and T-M6 survive (above). Add a synthetic Gateway case at the gate whose answer must equal the artifact. |
| F3 | LOW. Pre-existing parity. | Go verifyReceipt (tooltaxonomy.go) calls the host verifier synchronously. |
A verifier that ignores its context holds the Go call for its whole duration, past the source deadline. TypeScript abandons it at the deadline. Both still answer withheld, record unknown and keep the dispatch open, and other calls proceed. The check cannot close or wedge the run's dispatch. The callback is the host's own. |
P7 and P8. The timings on main are identical (901–918 ms, 2.50–2.52 s, 1.236 s). |
| F4 | LOW (declared) | TypeScript keeps a verified custom-source result's receipt member in the retained value and the durable row. |
A reader can return the receipt. It is exactly the string the host verifier accepted, at most 128 UTF-16 units. A missing, oversized or non-string receipt is refused before any verifier is asked. A top-level receipt on a Gateway result fails the strict outer-member check. No unverified path reaches it. It is a parity difference in retained bytes and ids. |
P11. |
| F5 | LOW (parity, code read) | TypeScript hands the host's execute the very signal that keys the call's check (receiptAdmission(signal) is exported). Go strips it (withoutReceiptAdmission). |
Host code, which is trusted, could pre-empt the call's check. A nested call of an unsourced wrapped tool made with the same signal would run the outer call's check on the inner result, which fails closed for realistic inputs. No attacker path. | session.ts executeHost, toolSources.ts. |
Not established:
- A TypeScript run resumed from the window checkpoint into a fresh chat listed no ids, where Go listed both. Possibly my fixture; it is not a boundary question.
- I did not re-run
go-race,harness-check, certification,referenceorparity(CI's). - API-adapter runs over the memory store are slow: 15–35 s for 10–13 requests in both languages. That is a fixture cost, unrelated to the change.
Re-read at 920c75c
Verdict: mergeable. HIGH count 0.
- Candidate:
920c75ce92bf7ddda9055435fb5cd27b060c37b8. It is merge commit8d6b853(a8d75a4plusorigin/mainatdd507c45, which is ADR-0027 amendment: a rehearse check sub-kind for non-binding rehearsal, with paired vectors (#241) #244) followed by the fix commit920c75c. - Where it ran: fetched into the room clone. Checks ran in fresh scratch copies of
920c75c(<scratch>/.review/head2); mutants ran in<scratch>/.review/mut. - Caps: the same as the first round. Go ran under
ulimit -v 8000000withtimeout, Node underrsscap.sh 6000. GOCACHE was inside the room and TMPDIR outside it; both were removed after. - Same-vendor statement: this is a Claude Opus re-read of a Claude Opus build and fix round. It is not a cross-vendor claim. Nothing was posted.
| Finding | Status | Evidence |
|---|---|---|
| F1 (HIGH) | Closed | Go VerifyGatewayResult now requires descriptorMembers(salts, nil, {args, statement}). TypeScript verifyGatewayResult refuses any other key of salts. The shared vector gateway-receipt-v3.json gains the case unsigned-salts-member, and the ADR and design text say so. P10, re-run at the head in both languages: with extra=true the call is answered with the withheld refusal, nothing is listed, the row is unknown, and no reader, chat, checkpoint or durable row carries the member. The extra=false control is answered seven, with one retained id and no unsigned bytes. |
| F2 (MEDIUM) | Closed | Go TestTheGateAnswersTheSignedArtifactNotTheEnvelope and the TypeScript test "answers the signed artifact, never the envelope, at the gate and through the session" pin it. Both use a synthetic Gateway source, so no fixture is needed. My G-M7 and T-M6 are now caught (below). |
| F3 (LOW) | Rides, unchanged | Pre-existing. |
| F4 (LOW) | Declared in the PR body | Parity difference only. Only a receipt the verifier accepted can be retained. |
| F5 (LOW) | Rides, unchanged | Code read only. |
Mutants at 920c75c. A baseline run of all four checks on the unmutated copy passed first.
| Mutant | Result |
|---|---|
R-G-M7: Go result = checked → _ = checked |
Caught by TestTheGateAnswersTheSignedArtifactNotTheEnvelope ("the gate answered … the envelope"). My P10 does not catch it, because F1 already refuses the extra member. |
R-T-M6: TypeScript shown = checked → the raw run result |
Caught by gatewayReceipt.test.ts › answers the signed artifact, never the envelope… |
R-G-F1: drop the salts clause (the Go twin of the new row) |
Caught by TestGatewayReceiptSharedRefusals/unsigned-salts-member ("receipt safeguard admitted"), by TestAGatewayEnvelopeWithAnUnsignedSaltsMemberIsWithheld, and by my P10, where the sinks come back. |
R-T-F1: drop the salts key check (the new TypeScript row) |
Caught by gatewayReceipt.test.ts › refuses unsigned-salts-member, and › withholds a Gateway envelope…; also by my P10 in TypeScript, where 6 sinks come back. |
Merge resolution
git merge-tree --write-tree a8d75a4 dd507c4 conflicts in exactly the four shared files. Every other file in 8d6b853 is byte-identical to git's automatic merge. That includes packages/harness/toolSources.ts, which both sides touched and which merged cleanly.
The four resolutions keep every line either side added, with no conflict markers left:
- ADR-0027: 10 lines from this PR, 53 from ADR-0027 amendment: a rehearse check sub-kind for non-binding rehearsal, with paired vectors (#241) #244.
- The design doc: 4 and 14.
mutation-rows.json: 60 and 72.
mutation-rows.json, row by row:
- At the merge it holds 499 base rows, 5 from this PR and 6 from ADR-0027 amendment: a rehearse check sub-kind for non-binding rehearsal, with paired vectors (#241) #244. The fix round adds 2, for 512.
- Every row from each side is byte-identical to its source.
mutation-check.mjs --checkat the head: 507 rows checked, 0 problems, 5 retired.
docs/reference/events.md: both sides moved the same three rows, and those rows were regenerated. After npm run build at the head, node scripts/gen-reference.mjs --check reports "13 pages match their sources".
#244 still stands
Its files are identical to main at the head: the taxonomy schema, vectors and testdata, the Go and TypeScript taxonomy code and tests, the contract tests, the record shapes and the tools reference. TestTaxonomyRehearse and TestTaxonomyRehearseAdmissionOnly pass, and toolTaxonomy.test.ts passes whole.
No new exposure
Since a8d75a4, the receipt path is byte-identical except for the two verifier lines of the F1 fix. That covers the gate, the durable writer, the read wrapper, the session wrapping, the readers and the adapters.
#244 adds a declaration-time description check to toolSources.ts and tooltaxonomy.go. A rehearse check admits only pure or read effects (RehearseEffects), so it can never reach the read-external receipt path.
At the head:
- The PR's Go receipt tests pass.
receiptQuarantine.test.ts,gatewayReceipt.test.tsandtoolTaxonomy.test.tspass whole: 54 passed, plus 2 skips gated on a fixture.
I did not re-run P1–P9, race, certification or parity. That code is unchanged since the first round, and those checks are CI's.
) parity: the rows of packages/harness/context/session.ts and packages/harness/execution/controller.ts take their new diff digests and name agent#242's change; packages/harness/receiptQuarantine.test.ts is declared as a new package file. scripts/parity.sh passes against the Desk at desk_pin. go-race: package adapter's race run was already near its 20-minute package timeout, and the adapter reproduction added about three minutes to it. The reproduction moves beside the API adapter (go/adapter/eino), and runs through the API adapter for the issue's case and the deadline case only; every case still runs through the native adapter, and go/context holds the boundary for every case. The chat's next run is checked once per adapter. Callbacks off the test goroutine report with Errorf rather than Fatalf. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
|
🎉 Thank you for your contribution to Judgment Pack! If you’d like another task, choose a path that interests you: |
…ging-243 Conflict: packages/harness/scripts/mutation-rows.json, both sides appended rows at the end; kept both groups (agent#242 rows, then agent#243 rows). scripts/parity.sh merged cleanly with both new_files entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: kikashy <35789537+kikashy@users.noreply.github.com>
Closes #242. Security boundary; milestone v0.12.0.
What was wrong
In v0.11.0 the source gate verified an external read's receipt only after the call had already passed through the durable writer and the read wrapper (
sessionhost wrapping →controllerread wrapper →chatretention →run_writerdurable host execution, thensourcesverification; the TypeScript package had the same order incontext/session.ts,execution/controller.ts,storage/writer.tsandtoolSources.ts). A result whose receipt then failed was withheld from the call's own answer, but it was already astate=resultdurable row with an operation observation and a retained chat entry:list_contextnamed it,read_context,read_windowandsearchreturned it withisError=false, later model requests carried it, a checkpoint written meanwhile held it, and its dispatch was offered and accepted as Plan evidence.The contract now
Nothing from an external result is reusable context, or successful source evidence, until its receipt verifies.
receiptAdmission), keyed by the call's deadline signal in TypeScript (ReceiptAdmission,receiptAdmission(signal)).invokeDurable/RunWriter.invoke) runs it after the host's execute and before it records anything. A refused result is recorded once as the call's uncertain outcome:state: unknown, failure = the gate's refusal (tool-failed,unknown, the existing withheld text), noresult, no observation, no repeat record. The physical execution stays once-only and is not retried; source-call accounting stays consumed.WrapRead/wrapRead) runs it before it retains in the chat or caches anything.withoutReceiptAdmission).Covered: rejected, missing, oversized and expired receipts; an error result; the actual next requests and every context reader; the next run's index; durable and restored context; the API and native adapters in both languages; two sources' calls in flight together, one rejected.
Quarantine choice: an unverified result is not listed at all. The issue asks that a failed receipt "remain inspectable as a failed/uncertain operation"; the existing records already do that without a new state: the
tool_resultrefusal event, the durable dispatch row (unknownwith that failure) and, through the API adapters (where the run lists its operations for the model), the operation recordlist_contextalready shows as<tool> · failed, whose result is the gate's refusal. A native run's single invocation lists no operations, so there the event and the durable row carry it. No listing state, record member or version is added; no frozen record format changes and no public schema changes. The new exported TypeScript names areReceiptAdmission,receiptAdmissionandRECEIPT_WITHHELD(the text is unchanged);RunWriter.invokegains an optional last parameter.ADR note: an amendment section on ADR-0027 (which owns receipts), "an external result is not context until its receipt verifies". Host docs: one paragraph each in
packages/harness/README.md(chat context) andgo/README.md(tool sources), and one indocs/design/tool-taxonomy.md.Vectors
contract/vectors/tool-result/receipt-quarantine.json, written by hand (schematool-result.json): casesrejected-receipt,missing-receipt,oversized-receipt,expired-receipt,error-result, each followed bylist_context,read_context/read_window/searchscoped to the id the result would have been retained under, and a search of every item;overlap(two sources in flight, the rejected call's check held until the other source's two calls are recorded) and the checkpoint written meanwhile restored into a fresh ledger. Read bygo/context/receipt_quarantine_test.go,go/adapter/eino/receipt_quarantine_test.goandpackages/harness/receiptQuarantine.test.ts; registered ingo/gate/vectors_test.goas not a Runtime wire decision.Tests
list_context,read_context,read_window,search, final answer; the peer learns a reference only from the listing): GoTestReceiptQuarantineThroughTheActualAdaptersingo/adapter/eino(every case through the native adapter; the issue's case and the deadline case through eino, the costly runs under the race detector; go/context holds every case at the boundary), TypeScriptreceiptQuarantine.test.ts > the issue's six requests through the {api,native} engine(vercel and codex). Each asserts 1 execution, the verifier calls the case names, 6 requests none carrying the sentinel, the five events equal to the vector, the stored checkpoint and events free of it, the durable rowunknown, and the chat's next run's first request free of the result and its reference.main(af37939; the same code as eba2ad6 for this path), with only the tests and the vector added: every Go api/native subtest fails with "model request 4 carries the unverified result", as the issue observed (error-result: "the stored checkpoint carries the unverified result"); every Go boundary case fails atlist_context's answer naming the retainedrecords__read_recordentry (error-result: its durable row is aresultcarrying the sentinel), and the overlap test at "the checkpoint written while the receipt check ran carries the result". All 18 TypeScript tests of the file fail: the 16 behavioural ones at their assertions (readers and agent answers carry the sentinel, the durable row isresult, the mid-check checkpoint carries it), the two unit tests of the new admission because it does not exist there. All pass after the fix.TestAnUnverifiedReceiptLeavesNothingAReaderCanReach(per case: events, verifier calls, chat empty, request state, durable row, Plan step refused withunsuccessful-operationand not offered, stored bytes, next run's index),TestOverlappingCallsAndARestoredCheckpointKeepOnlyTheVerifiedResult,TestTheReadWrapperRetainsNothingItsCheckRefuses,TestTheReceiptCheckRunsOnceAndRefusesEverywhere, and their TypeScript counterparts.Safeguards and mutants
Rows added to
packages/harness/scripts/mutation-rows.json(allcaught, origin agent#242); the Go twin of each was applied by hand in a scratch copy:receiptQuarantine.test.ts:list_contextlists the result, readers and agent answers return itTestAnUnverifiedReceipt…list_context's answer; overlap: checkpoint written during the check carries the resultretains nothing in the read wrapper…: the refused call answers, the chat snapshot carries the sentinelTestTheReadWrapperRetainsNothingItsCheckRefuses: "verified false answered nil"withheldRow:complete_plan_stepaccepts the rejected dispatchexpired-receipt×3: readers refused after dispatch closes, API error event, native run stops…/expired-receiptlist_context's answerexpired-receipt×3: answered as a timeout…/expired-receiptthe source's answernode packages/harness/scripts/mutation-check.mjs --row "receipt quarantine": 5 rows chosen, 5 caught.--check: 499 rows checked, 0 problems.Self-checks (AGENTS.md)
(a) boundary paths: only a
notTheGatesrow added ingo/gate/vectors_test.go, no admission or verdict change. (b) no schema member or enum value added. (c) both readers of the vector pass over everything added. (d) every row caught; twins caught by hand. (e) the refusal path is the existing withheld event, exercised with a missing (empty), oversized and rejected receipt. (f) no new checkpoint member. (g) the ADR amendment and the code were compared sentence by sentence.Checks
Candidate:
a8d75a43bc52f0f40952202760f91e7d805cdeb3(the two #242 commits,e4bc57eand60ac1b8, plus a merge oforigin/mainataf37939, asked for by the coordinator to carry #245), no uncommitted diff. Command:jpdev check --changed origin/main(merge base af37939, 18 changed paths, 24 of 30 checks selected), with TMPDIR in a directory that has nonode_modulesabove it (packed-smoke requires that).Also on this head:
node --test scripts/dependency-audit.test.mjs8 pass, 0 fail;node packages/harness/scripts/mutation-check.mjs --row "receipt quarantine"5 of 5 caught (run on the pre-merge head; the merge touches no mutated file). Not passed locally:generated-docsrefuses to run here ("inventory requires go1.26.10, found go1.26.5"; the machine has no go1.26.10), so it,parity(no Desk checkout) anddependency-audit-liveare CI's. The Go steps ran with go1.26.5, not the manifest's 1.26.10. The pre-merge head60ac1b8passed the same 23 checks.Review fix round (security clean room on a8d75a4)
Head
920c75ce92bf7ddda9055435fb5cd27b060c37b8: a merge oforigin/mainatdd507c45(#244, the rehearse sub-kind; ADR-0027,docs/design/tool-taxonomy.mdand the mutation table keep both sides, main's rows first;docs/reference/events.mdregenerated withscripts/gen-reference.mjs), then one commit:VerifyGatewayResult/verifyGatewayResultaccepted any member of the envelope's unsignedsaltsbesidesargsandstatement; a verified envelope is what the read wrapper retains and the durable writer records, so those bytes became ordinary retained context. Both verifiers now refuse any othersaltsmember (the retained value is unchanged, so retained ids do not change). The sharedcontract/testdata/taxonomy/gateway-receipt-v3.jsongainsunsigned-salts-member, answered alike byTestGatewayReceiptSharedRefusalsandgatewayReceipt.test.ts > refuses unsigned-salts-member. Boundary tests: GoTestAGatewayEnvelopeWithAnUnsignedSaltsMemberIsWithheld, TypeScriptgatewayReceipt.test.ts > withholds a Gateway envelope that carries an unsigned salts member, and no reader returns its bytes: the call is withheld, the five readers answer as the receipt-quarantine items, and the chat, request state, stored checkpoint and durable row (unknown, no result) hold none of it. Mutant (old acceptance restored): caught in both languages (the shared case is admitted; the boundary test's call is answered instead of withheld).TestTheGateAnswersTheSignedArtifactNotTheEnvelopeand TypeScriptgatewayReceipt.test.ts > answers the signed artifact, never the envelope, at the gate and through the sessionuse a synthetic Gateway source (no fixture). Mutants: Goresult = checked→_ = checkedand TypeScriptshown = checked→shown = result, both caught (the answer is the envelope).receipt quarantine Q6 the gate answers the checked resultandgateway receipt refuses an unsigned salts member;mutation-check.mjs --rowon both: 2 chosen, 2 caught;--check: 507 rows checked, 0 problems.go testof./context/ ./gate/ ./adapter/...all ok (eino under the kit's Go cap, since its scripted Node server does not start under an 8 GBulimit -v);go vetandgofmtclean;tscclean; vitestgatewayReceipt,receiptQuarantine,toolSources,toolTaxonomy,taxonomyIntegration,execution/planActions,storage/writer: 161 passed, 2 skipped;gen-reference --check13 pages match;tests.test_contractOK. The fulljpdev checkblock above is fora8d75a43.CI fix round (920c75c)
scripts/parity.shrows ofpackages/harness/context/session.tsandpackages/harness/execution/controller.tstake their new diff digests and name agent#242's change;packages/harness/receiptQuarantine.test.tsis declared new. Against the Desk atdesk_pin: "every difference is named".adapter's race run onmain(dd507c4) was already near its 20-minute package timeout, and the reproduction added about three minutes to it. It now lives ingo/adapter/einoand runs through eino for the issue's case and the deadline case only. Locally under-race:./adapter/main 865 s, before 916 s (the test 188 s), after 746 s;./adapter/eino/main 371 s, after 503 s (the test 107 s). No timeout was raised and nothing is skipped under-race.Not done, and why
Declared parity difference (F4): the TypeScript package keeps a verified custom-source result's
receiptmember inside the retained value and the durable row; Go does not (itsReceiptis never in the result JSON). Only a receipt the host's verifier accepted can be retained this way; changing it would change retained ids, so it is declared here, not changed.The deepagents engine is not driven separately; the boundary sits in
contextualSessionand the durable writer, shared by every engine.🤖 Generated with Claude Code