Skip to content

fix(gateway): N10a, /api/ip/* authorization, owner-checked and fail-closed (Gate A) - #385

Open
LamaSu wants to merge 9 commits into
masterfrom
fix/ip-route-authorization
Open

LamaSu wants to merge 9 commits into
masterfrom
fix/ip-route-authorization

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Gate A row N10a (/api/ip/* authorization). Moved to pcc-economics by steward #2641; spec: gateway's WP-E. Reviewers: gateway (the authorization rule) and coord-watch (cross-family verdict on the exact merge SHA, per board rule 1).

Why

On master, any authenticated caller could do any of these to any IP:

  • set its licensing terms (POST /api/ip/set-licensing-terms);
  • decide who its royalties go to (POST /api/ip/distribute-royalties);
  • claim its vault, recorded as 0x000…0 (POST /api/ip/:ipId/claim);
  • settle royalties for any job, with a body-supplied jobRevenue and payerAddress, silently swallowing failed payments (POST /api/ip/settle-royalties).

POST /api/csd also minted a Story IP to a body-supplied designerAddress, which defaulted to the zero address (kits #2615).

Rules

Who the caller is. Only a wallet proven by a SIWE (EIP-4361) session counts. An API key's operatorId is asserted, not proven: POST /api/auth/provision is public, so an owner check on it passes for anyone. That is why job.cancel is unregistered in mcp/operation-policy.ts. With no proven wallet, a mutation gets 401 verified_wallet_required plus the SIWE steps.

Who owns an IP (E1). There is no owner column, so ownership comes from durable records only:

IP Owner Record
capability IP the operator of the capability's kernel story_ip_registrations.capability_id
job-evidence IP the operator of the job's kernel (exactly one linked job) story_derivative_links.job_id
anything else none: every mutation is 403 ip_owner_unknown

Paths that write ownership records (3). Nothing about ownership comes from a request body.

  • register-capability: the caller must operate the capability's kernel, designerAddress must be the caller, and the capability must be recorded. The registration row is the ownership proof, so a failed write is a 500, not best-effort.
  • register-job-evidence: the caller must operate the job's kernel, operatorAddress must be the caller, and the job and parent IP must be recorded. The link row is required.
  • The settlement service's automatic job derivative now names the kernel operator (never 0x0, never a malformed address). An unrecorded link is audit-logged for repair. Job evidence must derive from the IP of the job's own capability.
  • The release-time Story royalty computed from STORY_MILESTONE_AMOUNT x STORY_ROYALTY_PERCENT, with the escrow as payer, is removed.

POST /api/csd no longer mints a Story IP (storyIpSkipped: "register_via_capability"). A CSD IP could never have a durable owner record, so IPs are registered only through register-capability.

E2. set-licensing-terms, distribute-royalties and claim require the owner. set-licensing-terms requires designerAddress to be the caller, and claim records the caller's address.

E3: settle-royalties.

  • Who: the job's buyer (the escrow's payer) or the job's kernel operator. Anyone else is 403.
  • When: only for the job's escrow milestone with status released (otherwise 409).
  • Revenue is the milestone amount in USDC base units (parseUnits(amount, 6)); any other currency is 409. The payer is the escrow's payer. A body jobRevenue or payerAddress that disagrees is 409.
  • The child IP must be this job's own registered evidence.
  • The milestone is found only in the job's own workflow's escrow. There is no fallback across workflows.
  • Once-only. A settlement of (job, child IP) is claimed by a durable event in the append-only audit log, written with no await between the check and the claim. A repeat is 409 already_settled. A settlement that paid nothing releases its claim and can be retried; one that paid anything is never repeated.
  • A failed payment fails the request (502 settlement_incomplete), and every row says paid or failed.
  • Story real mode: 501 not_executed before any Story call, so the gateway never pays from its own key on a caller's behalf (operator queue G).

Story real mode. Once #360 (N10b) lands, real-mode Story calls refuse with STORY_NOT_EXECUTED. Every /api/ip route answers that as 501 not_executed, never as a generic 500, and this holds whichever PR merges first. settle-royalties answers 501 when Story executed none of its payments.

E4.

  • pay: anyone may pay into the vault of an IP with a recorded owner, but only as themselves (payer_must_be_caller). The amount is a positive integer in base units. In real mode it is 501, as above.
  • dispute: any proven wallet may dispute an IP with a recorded owner, and is returned as raisedBy. A dispute's claimant is often not a party (for example the real author of a copied design), and Story's bond bounds abuse in real mode.

Review status

Tests

  • ip-authz.test.ts (new, 27 tests): each negative is paired with its allowed case.
    • A non-owner is refused on terms, splits and claim.
    • An unknown-owner IP is refused.
    • An API key whose operatorId is the owner's wallet is refused, and the owner's SIWE session is accepted.
    • settle-royalties: a non-party is 403; an unreleased milestone is 409; a revenue or payer mismatch is 409; the wrong child IP is 409; an unsupported currency is 409; a failed row is 502 with the row marked failed.
    • Claim records the caller; pay and dispute act as the caller.
    • STORY_NOT_EXECUTED is answered as 501, and any other Story failure stays a 500.
    • All 17 authorization tests fail against master's ip.ts.
  • ip.test.ts and story-pipeline.test.ts are updated to sign in as the seeded kernel operator. New CSD cases cover the no-session and someone-else's-designer paths, and the auto-registered derivative names the kernel operator.
  • Full gateway suite @ 67d12bec: 3001 passed, 0 failed. (completion-real-tier is flaky in full runs, and fails the same way on master's files.)
  • @ 9d5a57ba: ip-authz, ip and story-pipeline 69/69. The new gate-null test fails on the old session fallback. The change only acts when req.provenWallet is set, which nothing on master does yet.
  • tsc --noEmit on the gateway is clean.

Known gaps

  • API keys cannot change IP state until a key records that its identity was SIWE-proven. Gateway has built that as fix/key-proven-wallet @ 560a433e (#3160), a one-commit delta to fix(gateway)!: WP-A scope hardening — wildcard is not money/admin authority, fail-closed scopes, identity binding (P0 MUST-CLOSE 6-10) #326 after its review. This PR already reads it.
  • The once-only claim lives in the audit log, which is durable and append-only. A dedicated settlement table would be cleaner, but it is a schema change for the operator.
  • Kernel ownership (shop_kernels.operator_address) is the root of trust here. Its own integrity at kernel registration and upsert is gateway's row.
  • Observed, not fixed here: paid-job-flow.ts fills job.cwmId and escrow.cwmId from two independent random fallbacks when the session has no cwmId, so they differ. N10a finds the job's milestone through the job's unique stepId when the workflow ids disagree, and refuses when that is ambiguous.

Merging is the operator's decision.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K

LamaSu and others added 4 commits September 24, 2026 14:52
…losed

Gate A row N10a (steward #2641, gateway spec WP-E). Before this, anyone could set anyone's licensing terms and royalty splits, claim any vault (recorded as 0x0), and settle royalties for any job with a body-supplied revenue and payer.

Identity: only a SIWE-proven wallet. An API key's operatorId is asserted, not proven: /api/auth/provision is public, so an owner check on it passes for anyone. This is why job.cancel is unregistered.

Ownership (E1): there is no owner column, so ownership comes from durable records only.
- A capability IP is owned by its capability's kernel operator (story_ip_registrations.capability_id).
- A job-evidence IP is owned by its job's kernel operator (story_derivative_links.job_id).
- No record means no owner, and every mutation is refused (403 ip_owner_unknown).

Three paths write those records:
- register-capability: the caller must operate the capability's kernel, with designer == caller and the capability recorded.
- register-job-evidence: the caller must operate the job's kernel, with operator == caller and the job and parent recorded.
- The settlement service's automatic derivative now names the kernel operator, not 0x0.

For both register routes the record is the ownership proof, so a failed write is an error, not best-effort. POST /api/csd mints its IP to the caller's proven wallet only (never the body's designer, never 0x0), or registers no IP and says why.

E2: set-licensing-terms, distribute-royalties and claim require the owner. Claim records the caller.

E3: settle-royalties runs only for the job's buyer (the escrow payer) or its kernel operator, and only for a RELEASED milestone.
- Revenue is the milestone amount in USDC base units, and the payer is the escrow payer. A disagreeing body is 409.
- The child IP must be the job's own evidence.
- A failed payment fails the request (502), and each row reports whether it was paid or failed.

E4:
- pay: the payer is the caller, against a recorded IP.
- dispute: any proven wallet may dispute a recorded IP, and is named as the disputant.

Tests:
- ip-authz.test.ts is new, 17/17. All 17 fail against master's ip.ts.
- ip.test.ts is updated to sign in as the seeded owner.
- Full gateway run: 2999 passed, 1 failed. The failure, completion-real-tier, is pre-existing: it fails the same way on master's files in a full run (2980 passed, 1 failed) and passes alone.
- tsc --noEmit is clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
…efuses

pcc-economics N10b (#360) makes Story real mode refuse operations it cannot execute, with StoryNotExecutedError (code STORY_NOT_EXECUTED). Whichever PR merges first, the IP routes now answer that refusal as 501 not_executed, never as a generic 500 that a client could mistake for a transient fault.

settle-royalties answers 501 when Story executed none of the payments. A mix of paid and failed rows stays 502 settlement_incomplete, with the per-row outcomes.

The check is duck-typed on err.code, so it holds on master before #360 lands. ip-authz: 20/20.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
…Ps, the gateway never pays

Answers coord-watch's cross-family verdict (#2974, astra, DO-NOT-SHIP) and gateway's review (#2971, SHIP_WITH_FIXES) on ff18daa.

- settle-royalties no longer falls back across workflows. A job settles only from its own workflow's escrow milestone; another workflow's released milestone and payer never do (astra 2).
- settle-royalties is once-only (astra 3, gateway 2). A durable claim is written to the append-only audit log, with no await between the check and the claim. A second settlement is 409 already_settled. A settlement that paid nothing releases its claim so it can be retried; one that paid anything never repeats.
- In Story real mode, pay and settle answer 501 not_executed. The gateway would otherwise pay from its own key on the caller's behalf; the payer's own on-chain path is operator queue G (gateway 3).
- POST /api/csd no longer mints a Story IP. A CSD IP could never have a durable owner record (astra 1); IPs are registered through /api/ip/register-capability.
- Job evidence must derive from the IP of the job's own capability, not from any known IP. pay and dispute require an IP with a recorded owner (astra "also").
- Settlement-service auto-derivatives:
  - skip a zero-address or malformed kernel operator;
  - log an unrecorded derivative to the audit log for operator repair, instead of swallowing it.
- Release-time Story royalties computed from STORY_MILESTONE_AMOUNT x STORY_ROYALTY_PERCENT, with the escrow named as payer, are removed (astra 4, pre-existing).
- A registration minted but not recorded is audit-logged for repair (gateway 5).
- verifiedWallet accepts req.provenWallet when gateway #326's apiGate sets it (gateway 1). Until then, only the SIWE session counts.

Tests:
- ip-authz 26/26, ip 33/33, story-pipeline 9/9, csd 21/21.
- Full gateway suite: 3001 passed, 0 failed.
- tsc is clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
… final

Gateway #3160 fixed the #326 interface: apiGate sets req.provenWallet on
every request, to a proven wallet or to null. The null is deliberate in
some cases, for example a SIWE cookie riding on another identity's API key.
verifiedWallet used to fall back to the SIWE session whenever provenWallet
was not an address. After #326 it would therefore have accepted that cookie's
wallet behind the gate's back.

The field is now final once present: an address is used, and null means no
proven wallet. Only when the field is absent, as on master before #326,
is the session the proof. Nothing on master sets the field today, so
current behavior is unchanged. When #326 lands, no edit to #385 is
needed.

Test: the owner's session passes with no gate verdict, and is refused (401
verified_wallet_required) once the gate has answered null. It fails on the
old fallback. ip-authz, ip and story-pipeline 69/69; tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
LamaSu added a commit that referenced this pull request Sep 29, 2026
…IWE session or a SIWE-minted key

Economics asked for this (N8 follow-up, #2888), and it closes the seam
between #326 and #385. #326's money floor lets only an explicitly scoped
KEY move money, and a key's operatorId is only asserted on the email
path. So an owner check that needs a proven wallet (N10a, /api/ip)
refused keys outright, and after both merge the /api/ip money routes
would have needed a key AND a SIWE cookie on the same request.

- /api/auth/provision writes { siweVerified: true, provenAddress } into
  the metadata of a key it mints for a SIWE-proven wallet. Only that
  branch writes it; the email and delegated paths never do, and no route
  writes caller-supplied metadata onto a key.
- provenWalletOfKey accepts only that exact shape, naming the key's own
  (case-folded) operatorId. A custodial-quickstart walletAddress, a
  legacy row, malformed JSON or a proof for another identity gives null.
- apiGate sets req.provenWallet on every request: null first, then the
  session address or the key's proof. The key is the principal, so a
  SIWE cookie riding along on another identity's key proves nothing.
  There is no schema change.

proven-wallet.test.ts has 17 tests. Against the pre-change gate and
provision wiring (helper kept so the file imports), 7 fail. 2 are true
negatives: a SIWE-minted key and a SIWE session must yield the address.
5 are null guards that fail there only because the field was absent.
The neighbouring auth suites (provision-*, settlement-scope,
identity-binding, apigate-public-methods, api-key-mint-scopes,
siwe-hardening) pass 193/193. tsc exits 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A8smAyyypDGhf46eT71TXo
LamaSu and others added 2 commits September 29, 2026 04:28
… owner, honest outcomes

Answers astra's round-2 verdict on #385 at 9d5a57b (DO-NOT-SHIP). A
factual inventory of the root-of-trust writers is in
returns/pcc-economics-work/n10a-r3/inventory.md.

Settlement (ip.ts):
- The claim is keyed by the released escrow MILESTONE, which is the
  royalty obligation, not by (jobId, childIpId). Nothing stops one job
  having two derivative links, and the old key paid a milestone once per
  child IP.
- A per-row journal: an "intent" before each payment call, then "paid"
  with the txHash or "failed". An intent with no outcome is an uncertain
  payment, kept for reconciliation.
- The claim is released only when Story reports that it executed none of
  the rows (STORY_NOT_EXECUTED). Any other failure may have executed after
  submission, so the claim stands and the 502 says the settlement is held
  for reconciliation.
- escrows.cwm_id is not unique, and findByCwm returned the first row.
  Several escrows for one workflow are now ambiguous and settle nothing.
- Story real mode is recognized if EITHER the environment or the service
  instance says real. The instance fixes its mode when it is built, so
  the route can no longer say mock while a real signer runs.
- Still open, and stated in the code: this is not atomic across
  processes. The transactional table (royalty_settlement_events, BEGIN
  IMMEDIATE) is in the economics DDL, operator item 27.

Parent registration (ip.ts): a job/capability association is not
permission to derive. The job's capability must be on the job's kernel,
and the caller must be the parent IP's recorded owner. Job submission
does not check that pair, so a job on kernel A naming kernel B's
capability used to derive from B's IP.

Automatic derivatives (settlement-service.ts): the same parent rule
applies. A link Story minted but the store failed to record now emits
settlement_claim "failed" with outcome "unrecorded" and repairRequired,
never "completed", and no settlement.story_registered.

Tests: 7 new. Six fail on 9d5a57b: four settlement, one parent and one
pipeline. The seventh is the promised root-of-trust negative test: a
capability cannot be repointed, an owned kernel cannot be re-registered by
another actor, and the IP stays its owner's. It passes on both commits
and pins the property. Full gateway suite 3008 passed, 6 skipped, 0
failed (188 files); tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…view loop)

The review loop requires a reproduction for every change. This test shows
astra's round-2 point about the route's real-mode predicate. A
StoryIPService instance built in real mode, while STORY_MOCK now says
mock, reached payJobRoyalty with 9d5a57b's route (the request returned
200). At 6f0c763 the request is refused with 501 not_executed, and
payJobRoyalty is never called.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Sep 30, 2026
…eath it (N79 round 2)

astra's pack 116b on #462 @38cf4116: DO-NOT-SHIP, with three HIGH
findings and one MEDIUM. Each was reproduced first with a failing test at
38cf411 (n79-refund-vs-settlement.test.ts):
- F1 HIGH: a repeated failure write while /complete is in flight
  refunded ("expected 'refunded' to be 'skipped'");
- F2 HIGH: cancelling one job refunded an escrow another job shares;
- F3 HIGH: a refund landed while SettlementService.releaseMilestone
  awaited the chain ("expected 'refund_pending' to be 'skipped'");
- F4 MEDIUM: a completion that failed after a failure write left the
  failed job's escrow funded.
Two more were reproduced the same way:
- the keeper, which sweeps a findAll() snapshot across awaits, drove an
  escrow given back mid-sweep (astra's Q2 note);
- the raw chain release route let a refund land while it awaited the
  chain (the F3 class, found by extending the audit).

The fix is one mechanism: DURABLE SETTLEMENT OWNERSHIP on the escrow row.
- A settlement takes its escrow with a compare-and-set from
  funded/active to `completing` (already in @pcc/spec's vocabulary),
  synchronously at its claim: /complete (same stretch as the job claim),
  resume-settlement, SettlementService.releaseMilestone (before the
  chain await) and the raw release route (by contract address).
- A refund never lands on a `completing` escrow, however often the
  job's mutable status is rewritten (F1, F3, raw route).
- A settlement that gives up BEFORE recording evidence hands the escrow
  back (prior status). If the job ended meanwhile, the escrow goes to the
  payer then, in one transaction (F4). After evidence exists, the
  settlement keeps it, since resume-settlement continues it.
- A release that went through records its milestone `released`, and the
  escrow `completed` once every milestone is.
- The refund gives back a whole escrow only if no other job references
  it; a shared escrow is skipped as `escrow_shared` (F2).
- The keeper re-reads the row right before each drive.

Tests: the six reproductions, plus the gaps astra named:
- a failure on the LAST write rolls back every milestone and the status;
- a failure after evidence keeps ownership;
- resume takes a legacy row (the write repeated, as in F1);
- a partial release hands the escrow back;
- failed releases (service and raw route) hand it back.
All 40 mutants (both rounds) are killed. The gateway suite passes: 3053
passed, 6 skipped (six threads; the Spark is at load 37, where two /complete
tests time out at 5 s and pass alone). tsc is clean. It merges cleanly with
#326, #385, #403, #424, #441, #445, #450, #451 and #459.
LamaSu and others added 3 commits September 29, 2026 19:56
…durable journal; no legacy kernel claim; release on receipt

Cross-family review A07b (astra, DO-NOT-SHIP at 4df69fa). Each finding was reproduced at
4df69fa with a test that fails there (or, for F5, a scratch trace), then fixed.

- F1 (HIGH): the caller chose which schedule consumed the milestone. At 4df69fa all of these
  paid (200): a job with two evidence IPs, settled through the one paying a third party; two
  jobs that name one milestone; and a retry after nonexecution with a changed schedule.
  Now settle-royalties refuses as settlement_ambiguous a job with more than one evidence IP,
  or a milestone another job also settles through. The first claim binds the milestone to
  its job, evidence IP and schedule digest, so a retry that differs is refused as
  settlement_binding_changed. register-job-evidence refuses a second evidence IP for a job
  (job_evidence_already_registered), including one racing an in-flight registration: without
  the in-process guard, two concurrent requests both registered.
- F2 (MEDIUM): journal writes were swallowed. With the "paid" write failing, 4df69fa answered
  200; with the "intent" write failing, it paid anyway. Now nothing is paid without its
  intent on record: 503 journal_unavailable, and the claim is released because nothing ran.
  An outcome that cannot be recorded stops the settlement with 502 settlement_unrecorded,
  with reconciliationRequired set; the claim stands.
- F3 (HIGH): re-registering a legacy kernel (operator "" or the zero address) with any API
  key claimed it. At 4df69fa OTHER became the operator, and so owned its capabilities' IP.
  kernel.facade no longer claims such rows; they stay unowned until an admin migration. In
  /api/ip, "" and the zero address are no owner.
- F4 (HIGH): SettlementService.releaseMilestone reported "released" and settled the job on
  broadcast. Now it waits for the receipt (waitForReceipt, bounded at 90 s):
  - success: released, and the job is settled;
  - reverted: failed (release_reverted);
  - timeout, or an unreadable receipt: submitted (release_unconfirmed), with the hash.
  Three test files' escrow-client mocks gain a success receipt.
- F5 (HIGH, SIWE audience from Host): reproduced by trace (a message signed for evil.example
  opened a session with Host: evil.example). There is no change here: gateway #326 @4de265ce
  binds domain and URI to PCC_SIWE_ORIGINS, and #385 ships after #326.

The r2 test "settled once, whichever of the job's child IPs names it" encoded F1's hole. It is
replaced by the A07b F1 tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ses only on a receipt

Cross-family review A07c (astra, DO-NOT-SHIP at a31ae78). F1, F2 and F3 are CLOSED. Each open
finding was reproduced at a31ae78 with a test that fails there, then fixed:

- N1 (CRITICAL): any authenticated caller could bind a signing key it controls to a kernel with
  no recorded owner and no signer, taking the trust root its machine logs and evidence are
  verified against. At a31ae78 the attacker's re-registration answered 200 and its signer was
  stored. Now such a row refuses any signing proof (403) before anything is written. A row that
  already has a signer keeps the usual rule: that signer's proof is idempotent, and another is a
  conflict (409). A guard test pins this.
- F4 (HIGH): SettlementFacade.releaseMilestone, behind the escrow chain release route and its
  workflow activity, announced settlement_complete/completed and logged escrow.released right
  after the broadcast. Now it waits for the receipt (waitForReceipt, bounded):
  - success: announced and recorded once, with the hash;
  - reverted: a permanent failure (release_reverted), announced as failed;
  - timeout, or an unreadable receipt: status "submitted", recorded as escrow.release_submitted
    with the hash, and never announced as released.
- A07c Q6: SettlementResultDTO now declares "released" and "submitted", the statuses
  releaseMilestoneForJob returns, and the cast that hid the mismatch is gone.
- F5 (HIGH, external): no change. Gateway #326 carries the trusted SIWE origin check; its head
  85c9d5b descends from 4de265c. #385 merges after #326.

kernel-signing-ed25519's concurrent SET-ONCE test created its kernel with no owner, so it
relied on N1. Its kernel now has a recorded owner, and it still races two binds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Cross-family review A07d (astra, DO-NOT-SHIP at d5d6467): N1 and F4 CLOSED. The new N2 (HIGH)
named three pre-existing paths that announce settlement without a release receipt. Each was
reproduced at d5d6467 first.

- N2(c), fixed here. PUT /api/jobs/:jobId/complete with real settlement, and
  POST /api/jobs/:jobId/resume-settlement when the crank has not released, recorded
  evidence_submitted but emitted settlement_complete/completed. At d5d6467 the new tests
  (settlement-crank-wiring) saw ["completed"]. Now the status is "completed" only when the job
  is settled, and "started" (under way) otherwise. A released resume still reports "completed",
  and a guard test pins it. The chain-preconfirmed resume emit is already gated on a confirmed
  release and is unchanged.
- N2(a), owned elsewhere. GET /api/settlement/:jobId answered settled: true for a completed job
  whose escrow is still funded (a scratch trace at d5d6467). Readmodels' #382 (board N79;
  astra SHIP at 42ca1ea) rewrites exactly those lines, so #385 changes nothing there and
  merges after #382.
- N2(b), already fixed on master. A completed job's timeline had a "settled" event (a scratch
  trace at d5d6467). Master fixed it in 45e1b22, in a file #385 does not touch, so it reaches
  #385's head with the merge-up after #382 and #326 land.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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