Skip to content

DAN-860: unlock's Seal identity moves to the fulfillment — main goes green - #5

Merged
Danny-Devs merged 2 commits into
mainfrom
agent-a/DAN-860-unlock-seal-canonical
Aug 16, 2026
Merged

DAN-860: unlock's Seal identity moves to the fulfillment — main goes green#5
Danny-Devs merged 2 commits into
mainfrom
agent-a/DAN-860-unlock-seal-canonical

Conversation

@Danny-Devs

@Danny-Devs Danny-Devs commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Closes the 3.5-week red main. 154 passed in Python (was 2 failed / 152 passed); TypeScript 1102 passed, unchanged.

What was actually wrong

Not "a test disagrees with a decoder." Python and TypeScript implemented disjoint requirements field sets for the same scheme — the intersection was empty.

A Seal threshold key-server model replaced the encryptionId model on 2026-07-20. It reached the conformance vectors and the TS runtime; it did not reach Python or docs/specification.md. Python was faithfully implementing a stale spec, which is why making CI green by editing a vector would have made the spec bug permanent.

The ruling, and the test that settles it

Seal is canonical. encryptionId belongs in neither requirements nor payload — it travels in the fulfillment (s402UnlockFulfillment, which already existed).

Can the server know this value at the moment it must send it?

Requirements ship in the 402, before payment exists. packageId / keyServers / threshold / contentDigest are properties of how the seller already encrypted — knowable. The Seal identity is receiptId ‖ nonce, and that receipt is minted by a transaction the buyer constructs in response to that 402.

So encryptionId in requirements was never merely stale — it was unsatisfiable by the party required to populate it. Recorded as ADR-012.

⚠️ Precision that matters for the next reader: the reason is not "the escrow doesn't exist yet." Under seal_policy.move's escrow flow the escrow_id is deterministic from signed TX1 bytes. unlock was rebuilt as a single-transaction scheme with no escrow at all; its anchor is the receipt's own object ID. REVIEW-NOTES.md contains both statements — §3 item C and §6b — and §6b is the later and correct one.

Changed — three files

File Why
python/src/s402/http.py unlock requirements + payload key sets and validate_unlock_shape brought to the vectors; payload encryptionId check deleted
docs/specification.md §4.9, the unlock payload table, and both known-key lists. The payload table also stopped describing a two-stage TX1/TX2 flow
docs/adr/012-…md the decision and the reusable test

Not touched, deliberately: the conformance vectors and the TS runtime. No vector was edited. .github/workflows/ci.yml is byte-identical to main.

Two of the ticket's premises did not survive checking

1 · types.ts needed no change. The ticket cites typescript/src/types.ts:436 as a required requirements field. That line is inside s402UnlockFulfillment — where encryptionId correctly belongs. s402UnlockExtra and s402UnlockPayload were already right. Three of five surfaces needed nothing; the real scope was two.

2 · CI was never the gap. Criterion 5 says "Nothing was merged, so nothing ran." CI runs on push: branches: [main] and it did run — run 29811549631, completed failure, 44s, on the exact commit, 2026-07-21T07:44Z. It reported red on time and nobody looked. Adding a scheduled run would produce a second unwatched red. The encoding is notification or a required check — left for @Danny-Devs, since it is a repo setting rather than code.

Criterion 4, and a correction to my own first pass

Criterion 4 asks to delete typescript/test/conformance/vectors/ as "an unread duplicate ... a trap with no upside."

It is not stale, not a duplicate, and not hand-maintained. It is gitignored build output: typescript/scripts/prepare-publish.sh copies spec/vectors/*.json into it during prepublishOnly, and hard-fails if the source is missing. It is in package.json's files array so npm consumers receive the contract. It cannot drift — cp has no opinions — and it is correctly absent from a clean checkout. Deleting it would strip the vectors from the published package.

An earlier revision of this PR added a drift check on it. That was wrong and has been removed. I had read the two directories as copies kept in sync by discipline. They are not; one is generated from the other at publish time. Worse, the check failed on the CI runner with published dir missing — because a clean checkout correctly does not have it — which would have made CI red on every commit. That is precisely how a correct guard gets switched off (DAN-719), and I built the thing I had flagged as the hazard.

The exit-2 "cannot check" path is what surfaced this, so the failure was loud rather than silent — but the right outcome is no check here at all.

The real uncovered gap, recorded in ADR-012 rather than patched: spec/vectors/ is itself generated, by test/conformance/generate-vecters.ts running the live encode/decode functions. Nothing asserts the tracked vectors still match what the current code produces. That check is worth building; it is not built here because tsx is not an installed dependency, so it could not be watched green — and an unverified gate is worse than an absent one.

Verify: cd python && uv run --extra dev pytest -v → 154 passed on this branch.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
s402-docs Ready Ready Preview Aug 16, 2026 7:12am

…green

main has been red since 2026-07-21. Python and TypeScript implemented disjoint
requirements field sets for the same scheme; the intersection was empty.

A Seal threshold key-server model replaced the encryptionId model on 2026-07-20
and reached the conformance vectors and the TS runtime, but not Python or the
written spec. Python was faithfully implementing a stale spec.

Seal is canonical. The settling test: can the server know this value at the
moment it must send it? Requirements ship in the 402, before payment exists.
packageId/keyServers/threshold/contentDigest are properties of how the seller
already encrypted, so the server knows them. The Seal identity is
receiptId||nonce, and that receipt is minted by a transaction the BUYER
constructs in response to the 402 — so encryptionId in requirements was not
stale, it was unsatisfiable by the party required to populate it.

- python/src/s402/http.py: unlock requirements and payload key sets, and
  validate_unlock_shape, brought to the vectors. Payload encryptionId check
  deleted.
- docs/specification.md: 4.9, the unlock payload table, and both known-key
  lists. The payload table also stopped describing a two-stage TX1/TX2 flow;
  unlock has been single-transaction pay_and_mint since 2026-07-20.
- docs/adr/012: records the decision and the reusable test.

NOT changed, deliberately: the conformance vectors and the TS runtime were
already correct. TypeScript types were too — the ticket cited types.ts:436 as a
required requirements field, but that line is inside s402UnlockFulfillment,
where encryptionId belongs. Three of five surfaces needed nothing.

No CI job is added. DAN-860's criterion 4 calls
typescript/test/conformance/vectors/ a stale duplicate; it is gitignored build
output that scripts/prepare-publish.sh copies from spec/vectors/ at publish
time, failing closed if the source is absent. It cannot drift and is correctly
missing from a clean checkout. A drift check on it is not just unnecessary, it
is red on every clean checkout — which is how a correct guard gets switched off
(DAN-719). The real uncovered gap is that spec/vectors/ is generated from live
code by generate-vectors.ts and nothing asserts the tracked vectors still match
what the code produces. Not built here: tsx is not an installed dependency, so
it could not be watched green, and an unverified gate is worse than none.
Recorded in ADR-012.

Verified: python 154 passed (was 2 failed/152 passed) · typescript 1102 passed,
unchanged · ci.yml byte-identical to main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… deleted

Records the check I added and removed inside DAN-860. spec/vectors/ and
typescript/test/conformance/vectors/ are byte-identical, and I read that as two
hand-maintained copies needing a drift guard. The second is gitignored build
output that prepare-publish.sh copies from the first at publish time, so the
guard was red on every clean checkout — the shape DAN-719 already named as how a
correct guard gets switched off.

Identical contents tell you what is true, never why. Read .gitignore and the
packaging manifest before concluding two directories are peers, and run any new
gate against a fresh clone rather than a working tree full of build output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Danny-Devs
Danny-Devs merged commit 1693d1d into main Aug 16, 2026
10 checks passed
@Danny-Devs
Danny-Devs deleted the agent-a/DAN-860-unlock-seal-canonical branch August 16, 2026 07:41
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