Repository navigation
feat(gateway): R9 agent-plan routes — validate (live R10) and accept (seam → VCR deal binding → one R13 consume); money-path gated, 503 until wired - #391
Merged
Conversation
…seam, VCR deal binding, one R13 consume) An external agent submits the plan it composed; the server decides what it costs, who is paid, and what is sealed. - POST /api/agent-plans/validate: a read-only R10 pre-check with the live re-quote. It is wired in production today (live rows and the CSD registry). - POST /api/settlement/agent-plans/accept: the seam, then VCR's deal binding, then ONE atomic R13 consume. This is a money path, so the scope checker default-denies its writes, and WP-A will require an explicit settlement scope. It answers 503 accept-not-wired, consuming nothing, until the R13 store, #349, the evidence map, the fee policy and escrow's encoder exist. - The principal is the gate's identity, never a body field. Another principal's reservation gets the same 404 as a missing one. - One clock reading per request. A broken clock or an encoder mismatch fails closed, and a plan that cannot be bound never consumes its reservation. Only an explicit { ok: true } from the store is a seal. A server fault is a generic 500 that leaks nothing. - services/agent-plan-deal.ts (pure) builds VCR's binding (#2475, #2674): {acceptedDealDigest, all unit ids in plan order (1..64, distinct), nodeId, requires: the unit ids of the direct predecessors}. The 64-unit limit is checked before the encoder. The encoder's output is read once and checked against the deal. Tests: 16. 21 mutation checks, all killed. Full gateway suite: 3033 passed, 6 skipped; the known capture suites don't load (unbuilt verifier dist), and one load flake passes alone. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…act) into feat/agent-plans-route
…valid execution JSON is 400 and consumes nothing Merges #357 first (commit "merge: bring #357's N25 presentation..."), so this branch carries the whole N25 path. A test pins the route's view of it: - A 200 plan carries each node's canonicalPlan with the agent's inputs and its planHash. - The Layer-B presentation shows the same execution, and the store sealed exactly that digest. - A non-object inputs, or one beyond the key bound (both sendable over HTTP), gets 400 with the seam's refusal naming the field. Nothing is consumed. Route tests 17/17. Full gateway suite 3043 passed, 6 skipped. The known capture suites don't load (unbuilt verifier dist), and completion-real-tier timed out at load average ~20 but passes alone (3/3). pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
…ing) into feat/agent-plans-route
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
… rows, one CSD mapping (ChatGPT review of c48af89) The operator's cross-family review (ChatGPT, 2026-09-28) of c48af89 was DO-NOT-SHIP with four blocking findings. All four are fixed here, each with a test that fails without it. - Kernel status is an ALLOWLIST, and only "online" is available. The kernels table's status model is online, offline, maintenance and suspended, and the sweeper can set expired. - offline, maintenance and expired are unavailable/kernel-not-online; - suspended stays operator-suspended; - any other string ("retired", "", "ONLINE") is malformed-live-row. - Tenants. - The tenant option must be a well-formed id; otherwise the caller sees public rows only. An empty string is never a tenant. - A row whose tenant is not a well-formed id is visible to nobody. - Duplicate live rows. A REQUESTED id that appears twice in either loader answer makes that answer malformed. This fails closed, in either order, exactly like any other malformed answer. - A broken second copy counts too: its id is reported even when the rest of the row cannot be read. - Unrequested rows are still ignored. - One call, one mapping. csdForType is called at most once per capability type, so two nodes of one type cannot resolve against two CSDs. - The verdict's resource note: a decimal string over 100 characters is malformed before any BigInt work. Tests: 6 new, R10 53/53, and tsc is clean for the service and its tests. 12 mutation checks: 10 killed. T1 and T2 SURVIVE, and they are an equivalent pair: - validating the tenant option and validating the row tenant each close the tenant finding alone, because a visible scoped row must equal a validated option; - removing both is killed. Both are kept as defense in depth. Out of scope for this PR, and tracked: the verdict's section B/C5 asks that acceptance bind to row versions within one snapshot. That is the accept path (#356/#391), so it follows their first-round verdicts. pcc-composition 8a0f4de0, goal pcc-reconciliation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Sep 29, 2026
…rwards only public consume codes, and checks the unit bijection (astra, round 1 of #391) astra's review of 3f68ab7 (pack B-composition-391-r1) returned SHIP-WITH-FIXES. B (receiver): Object.create(seam, { now }) handed inherited methods the WRAPPER as `this`, so a seam with private fields (or a WeakMap keyed by the instance) broke. The seam's eight members are now read once. Its functions are called on the ORIGINAL seam, and only the clock is replaced. Objects (revalidation, policy, economics) pass through as they are. D (hygiene): a consume refusal's reason was forwarded verbatim. Only PUBLIC_CONSUME_REASONS now reach the client, as 409 with the code; each is a state of the caller's own reservation, named with the R13 store's ConsumeRefusal codes. wrong-principal and not-found stay one 404. Anything else (wrong-payer, a binding, digest or obligation mismatch, a raw store error, a malformed answer) is logged and answered with a generic 500, never sealed. C (defense in depth): bindDeal relied on the compiler's node-to-unit bijection. It now refuses two nodes on one unit ("binding-not-one-to-one") and a unit no node maps to ("unit-count"). B (ordering): the success body is built BEFORE the commit, so a failure while presenting or converting it leaves the reservation issued. Lost delivery after the commit is not solved here; recovering the sealed deal stays a production gate. Tests: 3 new plus one split. - A seam class with #private fields. - Both bijection violations. - Interleaved same-operator nodes (a and c by one operator, b between them) binding to their own units, derived from nodeToUnit. - The consume-answer test split: each public code gives 409 with its code; diagnostics, raw store errors and malformed answers give a generic 500 that leaks nothing. Full gateway suite: 3052 passed, 6 skipped. The capture suites do not load without a built @pcc/verifier dist. Mutations: 6 of 6 killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e, no edits; #357 landed, retarget to master) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
…oute (plain merge, no edits; fresh CI) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
LamaSu
had a problem deploying
to
trusted-checks
October 4, 2026 06:49 — with
GitHub Actions
Failure
LamaSu
marked this pull request as ready for review
October 4, 2026 13:09
LamaSu
had a problem deploying
to
trusted-checks
October 4, 2026 13:10 — with
GitHub Actions
Failure
LamaSu
added a commit
that referenced
this pull request
Oct 7, 2026
…/r13-budget-reservations (plain merge, no edits; retarget to master; fresh CI) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CZpG7S6AyDzTEJBQH8up44
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Round 2 (2026-09-29): astra's findings on 3f68ab7 (pack B-391-r1, SHIP-WITH-FIXES) fixed at 1601cae
PUBLIC_CONSUME_REASONS(the R13 store's codes for the caller's own reservation) reach the client, as 409. Anything else is logged and answered with a generic 500.71-composition-391-r2-1601cae7.md.Draft, stacked on #357 (PlanPresentation) → #356 (accept seam) → #355 (R10) → #351 (compiler). Reconciliation row R9, the HTTP half. It cannot merge before gateway's WP-A (R28, #326), and merging is the operator's call (steward #2461, #2643). Retarget the stack to master before merge (board rule 4).
What it does
An arbitrary external agent submits the plan it composed. The server decides what it costs, who is paid, and what is sealed.
POST /api/agent-plans/validaterepos.*.findByIds) and the CSD registry.POST /api/settlement/agent-plans/acceptacceptedDealDigest.accept-not-wired, listing what is missing. Consumes nothing.Accept stays 503 until every piece exists:
termsHashandacceptedPolicyDigest(a unit id derives from its escrow clone's identity).Authority
authenticatedPrincipal(req)is the API gate's identity: the key'soperatorId, else the SIWE wallet, lowercased. It is never a body field. R13's reservation-issue route must use the same function.req.tenantId. AtenantIdin the body is ignored (tested)./api/settlement/is a money-path prefix, so the scope checker default-denies its writes. After WP-A a caller needs an explicitsettlement(oradmin) scope, and a wildcard key no longer counts. This PR adds no scope rule; that is WP-A's table.wrong-principalnever appears).Order on accept, and what fails closed
asOfall use it; the seam receives it through a prototype wrapper, so method-style dependencies keep their receiver. A non-finite or negative clock is500 clock-unavailable.PlanPresentation: a stale plan showsneeds-requote, Layer C.services/agent-plan-deal.ts, pure):422 too-many-units-for-deal.500 deal-binding-failed.submissionDigest.{ ok: true }is a seal; anything else is409 reservation-conflict. A plan that cannot be bound never consumes its reservation.submissionDigest, the seal record, and aPlanPresentation(Layer B,sealed).500 internal-error. Its message is logged, never returned (tested).VCR deal binding (#2475, #2674; board N22, N25, N37)
binding(node) = { digest: acceptedDealDigest, units: every unit id of the deal in plan order (1..64, distinct), nodeId, requires: unit ids of the node's DIRECT predecessors }.[unit(print)], and print requires[].[unit(b), unit(c)], in the deal's unit order, and never the unit itself.Tests (
src/__tests__/agent-plans-route.test.ts, 17 tests)REAL on the path: the route, R10, the seam, R12 and PlanPresentation. STAND-INS, labelled as such: the live rows, evidence's gate, the evidence map, the R13 consume PROTOCOL (the trace's, unchanged) and a deterministic encoder. The real escrow compiler (#367) is proven in composition's R14 probe. Covered:
authenticatedPrincipalunit cases.inputsis 400 and consumes nothing.wrong-principalnot hidden, consume before the binding, a truthy consume counted as sealed, the seam given the live clock, a broken clock accepted, a fault message leaked, wiring called detached, tenant from the body, validate without auth. Deal binder (11): the cap off by one, the cap after the encoder,requiresdirection flipped, duplicate ids, job id or bytes32 or unit count unchecked (the phantom-unit case was added for the last), unsortedrequires, unknown or self-loop edges, the encoder before the edge check.@pcc/verifier/distis not built (as on master);completion-real-tiertimed out once at load average 27 and passes alone (3/3).Not in this PR
netSplitterForbinding. The seam takes nosplitNetyet; it needs server facts (#2755), andunitFactsstay empty until kits' resolver exists.requiresis derived from the evaluated submission today, whose edgescompositionRootcommits.Cross-family status: round 1 is queued for the 19:43 PDT codex window at 3f68ab7 (#3177). That head only merges the stack below; the route's own code is 918fefb's.
pcc-composition 8a0f4de0, goal pcc-reconciliation.
🤖 Generated with Claude Code
https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz