Skip to content

feat(gateway): accept seam for externally authored plans + end-to-end trace (R9 pure part) - #356

Merged
LamaSu merged 21 commits into
masterfrom
feat/external-plan-seam
Oct 4, 2026
Merged

LamaSu merged 21 commits into
masterfrom
feat/external-plan-seam

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Round 3 (2026-09-29): astra's findings on a16095a (pack B-356-r2b, DO-NOT-SHIP) fixed at 184a0e3

  • A (authority): R10's nested callables (loadCapabilities, loadKernels, csdForType) are captured and validated before ANY submission property is read. R10 gets frozen pinned wrappers that use the original receiver.
  • C (authority): ctx.principal and ctx.tenantId are read once, before the submission.
  • B (digest): leaf() turns a symbol into NOT_DATA and -0 into 0, so the snapshot never keeps two values that encode alike.
  • Evidence: gateway 3020 passed; mutations 6 of 6 killed. One mutant first survived, which exposed a missing test; that test is now added.
  • Round-3 pack: 70-composition-356-r3-184a0e39.md.

Reconciliation row R9, pure part (goal pcc-reconciliation; ledger item 27, "no path for an externally authored plan"): the accept seam that chains R10 → R11 → R12. It also carries the composition return contract's end-to-end trace.

Stacked on #355 (R10), which is stacked on #351 (R12).

What it does

acceptExternalPlan(submission, { principal, tenantId }, deps) injects every read, so the accept decision is a pure function of (submission, principal, live state).

What the agent supplies: only the plan's shape and what it believes each node costs.

What the server decides: every settlement term.

term source
operator and payout address R10 live re-read
gross, currency R10 live re-read
program R11, resolved server-side and checked by evidence's gate
evidence requirements the CSD tier (evidence owns this mapping)
payer the durable reservation
fee, reclaim time server policy
plan id derived from the reservation. A caller cannot pick job ids that collide with another plan's.

Refusals are typed by stage:

  • submission
  • reservation: not-found, not-issued, expired, wrong-principal, wrong-request
  • revalidation: the non-current verdicts. A stale verdict carries the re-quote.
  • currency
  • program: the claimed program doesn't match
  • evidence: no evidence contract for the tier
  • compile

The trace (external-plan-trace.test.ts)

  1. Agent DAG. The nodes are listed mail-first, although print must run first.
  2. R10 → R11 → R12 → accepted deal. The result is 2 V-next jobs, one per operator, in canonical order. Economics are bigint: g/f/n = 6.50 → 152,750 / 6,347,250 at 235 bps. The deal digest is sealed.
  3. Reservation consumed exactly once.
  4. V-next units ready. Each unit conserves exactly and carries its step id, the root and reclaimAt.

PCC_TRACE_OUT=<path> writes the happy path as JSON. The current run is at /mnt/sparkbulk/pcc-reconciliation/returns/pcc-composition-trace.json.

Charter negatives, through the whole seam:

  • forged cheaper snapshot
  • unapproved program (a claimed mismatch, or none registered)
  • expired reservation
  • principal or request A used for B
  • obligation over the reservation
  • double consume: exactly one lands
  • duplicate node: no double obligation
  • a plan altered after acceptance: digest-mismatch at consume
  • caller-chosen planId or payout: ignored
  • currency and evidence refusals
  • malformed input: typed, never a throw

/api/compose is one planner among many (f2d74b3)

submissionFromComposeResponse() restates a ComposeResponse as the claims an agent would submit:

  • nodes become step-<index>
  • dependsOn becomes edges
  • the composer's float price is passed through verbatim

Those claims then go through the same seam, so the composer gets no shortcut:

  • Drift from the live rows returns stale with a re-quote.
  • An exponent-form float such as 1e-7 is a malformed claim.
  • Refused before the seam:
    • expired or non-proposed proposals
    • dangling dependencies
    • duplicate step indices

Tests: 3 new. 4 mutation checks, each caught by a failing test.

Cross-family review (astra): SHIP-WITH-FIXES, fixed in 190bf9a

Confirmed:

  • Where each term comes from:
    • payer: the reservation
    • operator and payout: the live kernel
    • gross, currency, program, evidence, fee, reclaim time, plan id: the server
  • The reservation ID is only a lookup key. Authority is the stored record plus the authenticated principal. This satisfies the charter's "no caller reservation ID as authority".

Fixed:

  • Tier. The tier is the principal's purchase choice, constrained by the live offer. A reservation may carry a minTier floor, and a delegate cannot downgrade it.
  • Compose adapter. Values whose coercion throws now get a typed refusal. The validation boundary is explicit: structure is checked here, meaning in R10.
  • Seam:
    • an empty principal matches nothing
    • the clock is floored
    • a non-finite clock fails closed
  • R13 stand-in consume. It now re-checks plan id and each job's payer, and derives the obligation from the units. "A recomputed digest proves integrity, not authority."

Tests: 23, with 7 new; the fix-driven tests fail against f2d74b3. Mutation checks: seam 10/10 caught, adapter 6/6 caught.

Read-once seam (70f9582), confirmation queued

This applies the pattern that closed #351 and #355.

  • The submission is copied once by snapshotSubmission. The program cross-check, R10 and the compile use only that copy.
  • The reservation record is read once. A malformed record is refused as malformed-reservation.
  • Dependencies and policy are read once. An unreadable one is a defined TypeError. Dependencies are called with their receiver.
  • Every outcome carries submissionDigest, a type-tagged sha256 of the submission exactly as evaluated, and verdicts. The presentation (feat(gateway): PlanPresentation read model for caller-authored plans (product section 5) #357) uses the digest to bind what it displays.
  • Tests: 30 in the trace file (7 new).
  • Mutation checks: 17/17 caught. That is 7 new plus the 10 earlier ones, re-anchored.

N25: execution inputs reach the sealed deal (4f30bb1)

  • A node may carry inputs and constraints: plain JSON, the caller's content (e.g. which document and how many pages), never authority.
  • The snapshot reads each one once through @pcc/spec's copyPlanJson, as absent, an owned frozen copy, or the refusal reason.
  • submissionDigest v2 encodes them. Absent, {} and invalid all differ, and invalid evaluates to its reason.
  • Invalid execution JSON is refused as {stage:"submission", reason:"invalid-execution-json", fields}. It names EVERY bad field in (nodeId, field) order, before any reservation read.
  • The compiler (feat(spec): accepted-plan compiler — composition half of MS-01 (R12) #351) seals them in each node's canonicalPlan and planHash.
  • 5 tests; 8 mutation checks, all killed.

R15 economics binding, option (b) royalties on top (a16095a)

  • The dependency. Optional SeamDeps.economics = { unitGross, splitNet }: economics' agreementUnitGross and netSplitterFor, bound per request by the route to the agreement and the server's facts. It is read once, and both functions are called with the binding as receiver.
  • Gross vs quote. Each node's gross must exist and cover the operator's live quote from R10. Otherwise it is a typed refusal at the new economics stage before the compile: agreement-refused (with economics' code), agreement-unreadable, unit-gross-missing or quote-not-covered.
  • The compile. The compiler gets gross = the agreement's, quoteBaseUnits = the live quote, and splitNet, so the reservation must cover the grossed-up total.
  • Without an agreement nothing changes.
  • 4 tests; 8 mutation checks, all killed. The route's production wiring of economics waits on an agreement source and the server facts store.

Cross-family status: round 2 (the read-once seam + N25 + economics) is queued for the 19:43 PDT codex window at a16095a (#3177).

Stand-ins (labelled in the test)

Not in this PR

  • The HTTP route (R9). It needs R28 money scopes (Gate A). I'll ask the steward before landing it.
  • The real R13 store.
  • Escrow's R14 encoder.

Tests

  • 23 tests (seam, trace, compose adapter).
  • 7 mutation checks, each caught by a failing test:
    • principal check
    • expiry check
    • node-currency check
    • program cross-check
    • caller-chosen planId
    • missing evidence tolerated
    • request check
  • The scoped tsc is clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz

LamaSu and others added 2 commits September 24, 2026 06:00
… trace (reconciliation R9, pure part)

acceptExternalPlan(submission, { principal, tenantId }, deps) chains
R10 (live re-read) -> R11 (server-resolved programs + evidence's gate) ->
R12 (compileAcceptedPlan) with every read injected, so the accept decision
is a pure function of (submission, principal, live state). The agent
supplies only the plan's shape and what it believes each node costs; every
settlement term is the server's: operator and payout, gross, currency,
program, evidence requirements, payer (from the reservation), fee and
reclaim time (policy), and the plan id itself (derived from the reservation,
so a caller cannot pick job ids that collide with another plan's).

Refusals are typed by stage: submission, reservation (not-found / not-issued
/ expired / wrong-principal / wrong-request), revalidation (the non-current
verdicts, stale ones carrying the re-quote), currency, program (claimed
mismatch), evidence (no contract for the tier), compile.

external-plan-trace.test.ts is the return contract's end-to-end trace:
agent DAG (listed mail-first) -> accepted deal (2 V-next jobs, one per
operator, canonical order, bigint economics, sealed digest) -> reservation
consumed exactly once (a labelled R13 STAND-IN implementing the consume
protocol: re-check, RECOMPUTE the digest, consume once, seal) -> V-next
units ready (conservation, step ids, root, reclaimAt). Plus the charter's
negatives through the whole seam: forged cheaper snapshot, unapproved
program, expired reservation, principal/request A used for B, obligation
over reservation, double consume (exactly one), duplicate node, a plan
altered after acceptance (digest-mismatch at consume), caller-chosen
planId/payout ignored, currency and evidence refusals, malformed input.
PCC_TRACE_OUT=<path> writes the happy-path trace as JSON.

13 tests; 7 mutation checks (principal, expiry, node currency, program
cross-check, caller planId, missing evidence, request) each caught. Scoped
tsc clean. Stand-ins: in-memory live rows, program registry + gate (#349
draft), CSD tier -> evidence map (evidence owns it), R13 store (#2240).

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
LamaSu and others added 4 commits September 24, 2026 06:08
… agent's plan (R9)

Product pack section 5 ("SERVER COMPOSER") and ledger R9: the server
composer is one optional planner, never canonical. The new
submissionFromComposeResponse() only restates a ComposeResponse as the
claims an agent would submit (nodes step-<index>, dependsOn -> edges, the
composer's operator/kernel/type/tier, and its float estimatedPriceUSD
verbatim). R10 then re-reads and compares those claims like anyone's, so a
composer that drifted from the live rows gets `stale` + a re-quote (or
`invalid-claim` for an exponent-form float), never a deal on its own say.
Non-proposed or expired proposals, dangling dependencies and duplicate
step indices are refused before the seam.

Tests: 3 new in external-plan-trace.test.ts (16 total there): accepted
through the ordinary seam; float drift -> stale; 1e-7 -> malformed claim;
expired / over_budget / dangling dep / duplicate index refused. 4 mutation
checks (expiry, status, dangling dep, duplicate index) each caught. Scoped
tsc clean.

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
Astra's cross-family review of #356: SHIP-WITH-FIXES. It confirmed the
authority chain: payer from the reservation; operator and payout from the
live kernel; gross, currency and program from the server; the
reservationId is only a lookup key, which satisfies the charter's "no
caller reservation id as authority". Fixed:

- Tier: the tier is the principal's purchase choice, constrained by the
  live offer. A reservation may now carry `minTier`, a floor the payer set
  (invariant 11). A delegate spending it cannot downgrade assurance (new
  refusal stage `tier`). The header says so instead of "every term is
  the server's".
- Compose adapter: JSON-shaped values whose coercion throws (status,
  expiresAt or assuranceTier with a null toString) crashed it. Every field's
  type is now checked before any coercion, and a string tier "2" no longer
  becomes "tier2". The validation boundary is explicit: structure here,
  meaning in R10.
- Seam: an empty or missing principal matches nothing. The clock is floored
  (a `Date.now()/1000` clock made BigInt throw). A non-finite clock throws
  rather than compare as "not expired".
- R13 stand-in consume contract, which was overstated: it now re-checks
  plan id and EACH job's payer, and DERIVES the obligation from the units.
  It documents that a recomputed digest proves integrity, not authority
  (a resealed plan with another payer is refused).

Tests: 23 in external-plan-trace.test.ts (7 new). The fix-driven ones fail
against f2d74b3. New negatives: consume-time expiry / principal / resealed
payer / understated total / foreign plan id; a resolved program the gate
rejects; tier below the reservation minimum; nested malformed nodes and
edges; adapter coercion crashes. Mutation checks: seam 10/10 caught,
adapter 6/6 caught. Scoped tsc clean.

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
LamaSu and others added 6 commits September 24, 2026 06:35
… outcomes carry verdicts and a submission digest

The pattern that closed #351 and #355 under cross-family review, now
applied to the seam (#356) before its confirmation round:
- The submission is copied once (snapshotSubmission: requestId,
  reservationId, each node's fields, edges; lengths read once, lists
  capped, non-primitives become an owned sentinel). The program
  cross-check, R10 and the compile use only that copy. A callback that
  mutates the caller's submission changes nothing.
- The reservation record is read once. A malformed record (non-numeric
  expiresAt, or minTier outside 0..3) is a typed refusal,
  "malformed-reservation".
- Dependencies and policy are read once, before any input. An unreadable
  or non-function dependency is a defined wiring fault (TypeError). Each
  dependency is called with deps as its receiver, including the gate the
  compiler calls. The principal and tenant are read once.
- Every outcome after the snapshot carries `submissionDigest`, a
  type-tagged sha256 of the submission exactly as evaluated, so a read
  model can prove the submission it shows belongs to this outcome. It also
  carries `verdicts`, all of R10's verdicts whenever R10 ran. That field
  moves down from #357, where it belongs to the seam.

Tests: 30 in external-plan-trace.test.ts (7 new). 17 mutation checks each
caught: 7 new (fields read twice, compile edges taken from the caller,
reservation read twice, receiver dropped, unguarded wiring, unguarded
submission read, digest ignoring edges) plus the 10 earlier seam
mutations re-anchored. Scoped tsc clean.

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
LamaSu added a commit that referenced this pull request Sep 24, 2026
# Conflicts:
#	packages/gateway/src/__tests__/external-plan-trace.test.ts
#	packages/gateway/src/services/external-plan-seam.ts
LamaSu added a commit that referenced this pull request Sep 24, 2026
… be verified end to end (N19)

The oracle reads a funded tier's evidence two ways. One is event presence
under the CSD's ladder (#383). The other is a registered verifier per listed
primitive, and a stub verifier fails closed. A tier whose primitives nobody
can verify was still fundable here, as long as its committed program fit.
That sold assurance the oracle cannot check, and the tier evidence no stage
enforces weakened the promise without anyone seeing it.

assertAcceptedProgramForTier now takes the funded CSD's whole evidence map
in place of one tier. It refuses with tier-not-eligible, and the reasons,
unless tiers 0..T are eligible under computeCsdEligibility's oracle-enforcing
mode (requireImplementedVerifier). A funded tier absent from the CSD is
tier-not-in-csd. The program check runs against the CSD's own tier. The
primitive index is injectable, for tests and for when verifiers go live.

As authored, document-print-and-mail is eligible only at tier 0:
- tiers 1-3 lack ident.registered_key, which receipt.kernel_signed and
  machine.execution_log depend on;
- tier 2 has no Family-G primitive;
- six of its verifiers are stubs.
So the tier2 program (0xd229c8da) stays pinned but cannot be funded until
the CSD and its verifiers are fixed. This is N19's "or those tiers made
unfundable", derived instead of hand-listed.

API change for composition (#351/#356): pass
evidence: csd.evidence in place of tier: csd.evidence[tierKey].

Tests: committed-program.test.ts 31/31, including 8 new eligibility cases.
The existing program-leg negatives now fund a fixed CSD with live verifiers.
Spec suite green; tsc clean. 7 mutants killed: leg removed, report-only,
reasons scope, off-by-one, tier-in-CSD, missing-tier reason, caller index.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
LamaSu and others added 2 commits September 24, 2026 15:19
…nto the sealed deal (N25)

An agent's plan may now say WHAT each node runs on: `inputs` (e.g. the
document hash and page count) and `constraints` (e.g. a deadline), as plain
JSON. That is content, never authority. The compiler (#351, merged in) seals
it through each node's canonicalPlan and planHash.

- snapshotSubmission reads each node's inputs and constraints ONCE through
  @pcc/spec's copyPlanJson. The result is absent, an owned frozen copy, or
  the refusal reason.
- submissionDigest v2 encodes execution JSON by its canonical form. Absent,
  {} and invalid all differ, and invalid evaluates to its reason, just as
  a non-primitive evaluates to NOT_DATA.
- Invalid execution JSON is refused as { stage: "submission", reason:
  "invalid-execution-json", fields }. It names EVERY bad field in
  (nodeId, field) order, and it comes before any reservation read.
- The compiler receives the owned copies; absent means {}.

Tests: 5 new in the trace, 35/35 there and R10 47/47; the files typecheck.
8 mutation checks, all killed: not refused, dropped before the compiler,
the digest ignoring it, a double read, unsorted problems, a reservation
read before the refusal, absent colliding with {}, and constraints dropped.

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
LamaSu added a commit that referenced this pull request Sep 24, 2026
…hows each unit's sealed execution (N25)

This merges #356, which brings #351's agreementHash seal and the per-node
execution contract. PlanPresentation's owned plan copy now includes every
field the deal digest commits to, so its re-derived digest matches:
- agreementHash;
- each binding's planHash;
- a field-by-field copy of its canonicalPlan, with inputs and constraints
  read once through copyPlanJson. Non-JSON is invalid (malformed-outcome)
  even inside a resealed plan.

Additionally:
- The binding check requires each canonicalPlan to name its own plan, node,
  job and unit (plan-binding).
- Compiled and sealed nodes show `execution: {planHash, inputs,
  constraints}`, taken only from the intact, bound plan, never from the
  proposal. The deal shows agreementHash.
- The seam's invalid-execution-json refusal renders as refused, with one
  code per bad field ("<field>:<reason>@<nodeId>").
- The trace test's tail conflict (both sides appended a describe block) is
  resolved by keeping both.

Tests: trace 53/53 (4 new) and the files typecheck. 9 mutation checks, all
killed: planHash or agreementHash not copied, the contract not bound (and
only its node unbound), execution not shown, exec-JSON codes not rendered,
execution JSON not validated, and the deal hiding agreementHash. That last
one survived until a resealed-agreement test was added.

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
LamaSu added a commit that referenced this pull request Sep 24, 2026
LamaSu and others added 2 commits September 24, 2026 15:58
…on b: royalties on top)

SeamDeps.economics is optional and present only when an agreement applies.
It is { unitGross, splitNet }: economics' agreementUnitGross and
netSplitterFor, bound per request by the route to the agreement and the
server's facts.

- It is read once with every other dependency. Anything else is the
  defined wiring TypeError. Both functions are called with the binding as
  their receiver.
- The agreement's gross map is read once into owned data; bigints only.
- For each node, the gross must exist and must cover the operator's live
  quote from R10. Otherwise it is a typed refusal at the new "economics"
  stage: agreement-refused (with economics' code), agreement-unreadable,
  unit-gross-missing or quote-not-covered, all before the compile.
- The compiler receives gross = the agreement's, quoteBaseUnits = the live
  quote, and splitNet. So the reservation must cover the GROSSED-UP total,
  and economics can hold the operator's legs to its quote floor.
- Without an agreement nothing changes: one leg, gross = the quote.

Tests: 4 new, trace 39/39, and the files typecheck. 8 mutation checks, all
killed.

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
LamaSu added a commit that referenced this pull request Sep 24, 2026
…ntation shows the economics refusal stage

This merges #356's R15 binding, which also brings #351's quote to the
splitter. Two presentation changes:
- The refusal whitelist gains the seam's new "economics" stage. A missing
  unit gross, a gross below the quote, and an agreement refusal render as
  refused, Layer C, never a re-quote. The node is named, and economics'
  code is shown as a code.
- The `execution` doc now says its inputs are the CALLER's content, sealed
  as what was agreed, not facts PCC verified: render them as data. This is
  the doc nuance noted after the self-review of #357.

The trace's tail conflict (both sides appended a describe block) is
resolved by keeping both.

Tests: trace 58/58 (1 new) and the files typecheck. 2 mutation checks,
both killed: the stage not whitelisted, the code not shown.

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
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>
LamaSu and others added 2 commits September 29, 2026 09:27
…before reading the submission (astra, round 2 of #356)

astra's review of a16095a (pack B-composition-356-r2b) returned DO-NOT-SHIP
under the in-process getter and callback threat model.

A (authority): only the REFERENCE to deps.revalidation was captured before
the submission was read. A submission getter or a reservation callback could
then swap loadCapabilities, loadKernels or csdForType, and R10 would
authenticate the caller's own terms against fabricated rows. The three
callables are now captured and validated with every other dependency, before
any submission property is read. R10 gets only a frozen object of pinned
wrappers that call them with their original receiver.

C (authority): ctx.principal and ctx.tenantId were copied AFTER the
submission was read, so a getter could make the caller the reservation's
owner, or switch its tenant. The context is now read once, before the
submission. The checks, and the order of refusals, are unchanged.

B (digest): tag() encoded a symbol and NOT_DATA alike, and JSON encoded -0 as
0. leaf() now turns a symbol into NOT_DATA and -0 into 0, so the snapshot
never keeps two values that encode alike. Both are semantics-preserving.

Tests: 7 new.
- A: a submission getter, and a reservation callback, swapping all three R10
  dependencies both give the baseline outcome.
- A: a non-function R10 callable is a wiring fault raised before any
  submission property is read.
- A: method-style (class) loaders keep their receiver.
- C: a getter making the caller the owner is still wrong-principal.
- C: a getter switching the tenant gives the same refusal as the original
  tenant (with a control that the tenant matters).
- B: symbols and objects snapshot to one NOT_DATA, -0 to 0, and distinct
  values keep distinct digests.
Full gateway suite: 3020 passed, 6 skipped. The capture suites do not load
without a built @pcc/verifier dist.

Mutations: 6 of 6 killed. S6 (callables not validated) first survived; that
exposed a missing test, which was then added.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…code (#356, review round 3)

The seam now takes an owned copy of the submission before reading any field:
only plain objects and real arrays are walked, through their own data
properties, with the Proxy check (which invokes no trap) first. A Proxy or an
accessor anywhere refuses the submission as malformed; an object or array with
another prototype is never read. The authenticated context's principal and
tenant are read the same way. The copy is bounded in depth, value count and
list length. The header states the trust boundary: caller inputs are data,
the dependencies are trusted server wiring.

Tests: a submission getter that rewrites a method-style loader's state, a
Proxy submission, getters in node fields, execution inputs and arrays, a
class instance, a context accessor, the three bounds and a non-plain array.
The earlier read-once tests now assert that the getter never runs. 11 of 11
mutants killed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…recondition (#356, review round 4)

astra's round-4 review of 8990a11 (SHIP-WITH-FIXES) found one MEDIUM: the
no-code copy skipped symbol keys without counting them, and it bounds
nothing about an object's width before Reflect.ownKeys enumerates it. That
was reproduced first. At 8990a11, a submission with a symbol-keyed input,
or with more than 1,000,000 symbol keys, was accepted (2 failing tests).

A symbol key now refuses the submission, like an accessor or a Proxy:
JSON cannot produce one, and a skipped key goes uncounted. The header now
states the size bound as the seam's precondition. The submission is
parsed from an HTTP body the gateway caps at 1 MiB (server.ts bodyLimit),
and JavaScript cannot count an object's own keys without materializing
them, so the width bound lives with the caller. A test also pins the
string-key variant, which the value bound refuses.

Tests: external-plan-trace 60/60. Mutation: symbol keys skipped again is
killed (2 tests fail).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0171FftEAfqjwbJHowVTqAbz
LamaSu added a commit that referenced this pull request Oct 3, 2026
…ion (#357)

Pack 133 passed as #356's confirmation (steward #5847), which releases the
held merge-up of the accepted-plan stack. A merge-tree simulation of the
whole stack over master found one conflict: this one.

The conflict was in packages/gateway/src/__tests__/external-plan-trace.test.ts:
#357 and #356 each appended a describe block at the end of the file. Both are
kept, #357's block first, closed explicitly, then #356's. The seam source
(services/external-plan-seam.ts) merged without conflict.

Results: external-plan-trace 93/93. Full gateway suite: 3121 passed,
6 skipped. tsc is clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
@LamaSu
LamaSu changed the base branch from feat/plan-snapshot-revalidation to master October 3, 2026 18:34
@LamaSu LamaSu closed this Oct 3, 2026
@LamaSu LamaSu reopened this Oct 3, 2026
@LamaSu
LamaSu marked this pull request as ready for review October 3, 2026 22:39
 merged; plain merge, no edits)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
LamaSu added a commit that referenced this pull request Oct 3, 2026
…after #355 merged; plain merge, no edits)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
…am (plain merge, no edits; fresh CI on the green master)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
LamaSu added a commit that referenced this pull request Oct 4, 2026
…resentation (plain merge, no edits; fresh CI on the green master)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSBxBEX3sn9DPSqihyjuZE
@LamaSu
LamaSu merged commit df359fb into master Oct 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant