Work package F1 of design 57 (building config and provisioning; ADR 0349). Issue 650.
What changed
go/seal: the ADR 0206 sealed box (X25519, HKDF-SHA256, AES-256-GCM, envelope {v, eph, iv, tag, ct}, PEM keys), moved from mesh-catalog modules/claude-licence-manager/cmd/claude-licence-manager/seal.go. API: seal.Box, seal.KeyPair, seal.GenerateKeyPair(), seal.Seal(plaintext, recipientPEM), seal.Open(box, privatePEM), seal.Version.
@novox/mesh-sdk/seal (src/seal): the TypeScript twin, seal, open, generateKeyPair, SealedBox, KeyPair, VERSION; also re-exported as seal from the package root.
Standard library only, as go/merge-check.sh promises. The licence manager used crypto/hkdf, which needs Go 1.24 while this module says go 1.22; HKDF-SHA256 for one 32-byte block is written out with crypto/hmac instead of raising the module's Go version. The byte-for-byte vectors prove it equals hkdf.Key.
Two small hardenings over the original, neither changing a valid box: Open refuses an iv or tag of the wrong length (node would accept a truncated tag), and checks that eph is an X25519 key.
Why
The vault (issue 674), the provisioner client and providers (issue 675 and later) all need this box. Code shared between modules belongs in the SDK.
How it is proven
Fixtures in go/seal/testdata/:
sealed-by-licence-manager.json: made by the licence manager's own seal.go (mesh-catalog feat/57-config-and-provisioning at 8aa37e8) to a fixed test recipient key. sealed holds boxes from its unchanged Seal; vectors holds boxes from the same code with only its two randomness sources (one-time key, iv) fixed, so a seal can be compared byte for byte. The key is derived from a fixed label and guards nothing.
sealed-by-typescript.json: the agent module's TypeScript box the licence manager already keeps.
Go tests (go/seal/seal_test.go): TestABoxTheLicenceManagerSealedOpens, TestSealIsByteForByteTheLicenceManagersBox, TestABoxSealedInTypeScriptOpens, TestABoxOpensOnlyForItsRecipient, TestATamperedBoxDoesNotOpen, TestAKeyOfAnotherKindIsRefused.
TypeScript tests (test/seal.test.ts), the same six over the same fixture files.
Both failed before (package missing) and pass now.
The reverse direction (the licence manager opens what the SDK seals): the vectors show the SDK seals exactly the licence manager's bytes. Also, with the licence manager switched to this package (prepared, see below), all its existing tests pass unchanged, including TestABoxSealedInTypeScriptOpensInGo.
Deliberately left
The licence manager's switch to this package is a second pull request in mesh-catalog onto feat/57-config-and-provisioning. It can only be opened after this merges, because its require git.novox.be/novox/mesh-sdk/go must name the merged commit's pseudo-version. The change is ready: seal.go becomes aliases (type SealedBox = seal.Box, ...), newGCM moves into crypt.go; tested here against this branch.
modules/claude-code/cmd/claude-code/seal.go in mesh-catalog is a second, identical copy of the box. It is not in F1's scope; it should switch the same way.
Reviewer's checklist
Does what issue 650's Expected and design 57's F1 say: the box lives in mesh-sdk/go/seal with a TypeScript twin. Nothing outside it (the README line names the new package).
Done-when proof is tests that failed before (no package) and pass now. No existing test touched.
Composition: not applicable. This is an SDK-only addition; no module composes differently until it bumps its require.
No secret anywhere: fixture keys are test keys derived from fixed labels; plaintexts are made up. No database, no migration.
Strict parsers: no manifest or declaration field added.
sh merge-check.sh (npm ci, tsc --noEmit, npm test: 53 of 53 pass; gofmt -l empty, go vet ./..., go test -count=1 ./...: all ok) and sh go/merge-check.sh: pass.
Words: sealed box, licence manager, vault; ADR and issue numbers named with what they are.
The branch has three commits: the first two carried a placeholder README and package.json by my mistake, which the following commits replace. I checked that the branch's final tree equals my tested local tree (git diff empty). Please squash on merge.
Work package F1 of design 57 (building config and provisioning; ADR 0349). Issue 650.
## What changed
- `go/seal`: the ADR 0206 sealed box (X25519, HKDF-SHA256, AES-256-GCM, envelope `{v, eph, iv, tag, ct}`, PEM keys), moved from mesh-catalog `modules/claude-licence-manager/cmd/claude-licence-manager/seal.go`. API: `seal.Box`, `seal.KeyPair`, `seal.GenerateKeyPair()`, `seal.Seal(plaintext, recipientPEM)`, `seal.Open(box, privatePEM)`, `seal.Version`.
- `@novox/mesh-sdk/seal` (src/seal): the TypeScript twin, `seal`, `open`, `generateKeyPair`, `SealedBox`, `KeyPair`, `VERSION`; also re-exported as `seal` from the package root.
- Standard library only, as go/merge-check.sh promises. The licence manager used `crypto/hkdf`, which needs Go 1.24 while this module says `go 1.22`; HKDF-SHA256 for one 32-byte block is written out with `crypto/hmac` instead of raising the module's Go version. The byte-for-byte vectors prove it equals `hkdf.Key`.
- Two small hardenings over the original, neither changing a valid box: `Open` refuses an iv or tag of the wrong length (node would accept a truncated tag), and checks that `eph` is an X25519 key.
## Why
The vault (issue 674), the provisioner client and providers (issue 675 and later) all need this box. Code shared between modules belongs in the SDK.
## How it is proven
Fixtures in `go/seal/testdata/`:
- `sealed-by-licence-manager.json`: made by the licence manager's own seal.go (mesh-catalog feat/57-config-and-provisioning at 8aa37e8) to a fixed test recipient key. `sealed` holds boxes from its unchanged `Seal`; `vectors` holds boxes from the same code with only its two randomness sources (one-time key, iv) fixed, so a seal can be compared byte for byte. The key is derived from a fixed label and guards nothing.
- `sealed-by-typescript.json`: the agent module's TypeScript box the licence manager already keeps.
Go tests (go/seal/seal_test.go): `TestABoxTheLicenceManagerSealedOpens`, `TestSealIsByteForByteTheLicenceManagersBox`, `TestABoxSealedInTypeScriptOpens`, `TestABoxOpensOnlyForItsRecipient`, `TestATamperedBoxDoesNotOpen`, `TestAKeyOfAnotherKindIsRefused`.
TypeScript tests (test/seal.test.ts), the same six over the same fixture files.
Both failed before (package missing) and pass now.
The reverse direction (the licence manager opens what the SDK seals): the vectors show the SDK seals exactly the licence manager's bytes. Also, with the licence manager switched to this package (prepared, see below), all its existing tests pass unchanged, including `TestABoxSealedInTypeScriptOpensInGo`.
## Deliberately left
- The licence manager's switch to this package is a second pull request in mesh-catalog onto feat/57-config-and-provisioning. It can only be opened after this merges, because its `require git.novox.be/novox/mesh-sdk/go` must name the merged commit's pseudo-version. The change is ready: `seal.go` becomes aliases (`type SealedBox = seal.Box`, ...), `newGCM` moves into crypt.go; tested here against this branch.
- `modules/claude-code/cmd/claude-code/seal.go` in mesh-catalog is a second, identical copy of the box. It is not in F1's scope; it should switch the same way.
## Reviewer's checklist
1. [x] Does what issue 650's Expected and design 57's F1 say: the box lives in mesh-sdk/go/seal with a TypeScript twin. Nothing outside it (the README line names the new package).
2. [x] Done-when proof is tests that failed before (no package) and pass now. No existing test touched.
3. [x] Composition: not applicable. This is an SDK-only addition; no module composes differently until it bumps its require.
4. [x] No secret anywhere: fixture keys are test keys derived from fixed labels; plaintexts are made up. No database, no migration.
5. [x] Strict parsers: no manifest or declaration field added.
6. [x] `sh merge-check.sh` (npm ci, tsc --noEmit, npm test: 53 of 53 pass; gofmt -l empty, go vet ./..., go test -count=1 ./...: all ok) and `sh go/merge-check.sh`: pass.
7. [x] Words: sealed box, licence manager, vault; ADR and issue numbers named with what they are.
The branch has three commits: the first two carried a placeholder README and package.json by my mistake, which the following commits replace. I checked that the branch's final tree equals my tested local tree (`git diff` empty). Please squash on merge.
go/seal and its TypeScript twin @novox/mesh-sdk/seal are the ADR 0206 box
the licence manager kept to itself. Fixtures made by the licence manager's
own seal.go prove both byte for byte, in both directions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
go/seal and its TypeScript twin @novox/mesh-sdk/seal are the ADR 0206 box
the licence manager kept to itself. Fixtures made by the licence manager's
own seal.go prove both byte for byte, in both directions. (Replaces the
placeholder README the branch's first commit carried by mistake.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mesh/repo-check head a7997fc857ae merged into trunk c579f4de7511 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
Deliverynovox/mesh-sdk@a7997fc857ae — delivered since 2026-10-13T23:52:19Z
Delivery plan — builds nothing: the change touches no module of the mesh's graph
Transitions
2026-10-13 23:49 (new) → proposed (announced): novox/mesh-sdk#26's head announced
2026-10-13 23:50 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
2026-10-13 23:50 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
2026-10-13 23:52 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
2026-10-13 23:52 published → delivered (done): nothing for a walk to move
The commit's note under refs/notes/mesh-plan keeps every transition: git log --notes=mesh-plan.
<!-- mesh-delivery:view -->
**Delivery** `novox/mesh-sdk@a7997fc857ae` — **delivered** since 2026-10-13T23:52:19Z
**Delivery plan** — builds nothing: the change touches no module of the mesh's graph
**Transitions**
- 2026-10-13 23:49 (new) → proposed (announced): novox/mesh-sdk#26's head announced
- 2026-10-13 23:50 proposed → checked (checked): a verdict names this commit and the trunk's head as its trunk commit
- 2026-10-13 23:50 checked → ready (accepted): the gate passed or warned, and the repository's own check did not fail
- 2026-10-13 23:52 ready → published (merged): on the trunk its modules follow: merged there, and its walk opened — or nothing for a walk to move
- 2026-10-13 23:52 published → delivered (done): nothing for a walk to move
The commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.
Independent review of package F1 (issue 650). What I checked and ran, in my own worktree of the head:
sh merge-check.sh: exit 0 (npm ci, tsc --noEmit, 53 of 53 TypeScript tests pass including the six seal tests; gofmt, go vet and go test all pass). sh go/merge-check.sh: exit 0, go/seal ok.
HKDF: boxKey (go/seal/seal.go:186-195) follows RFC 5869 for L=32. Extract computes PRK = HMAC-SHA256(salt, IKM). Expand computes T(1) = HMAC-SHA256(PRK, info || 0x01). The salt is never empty, so the RFC's zero-salt default does not arise. I compared it with the original hkdf.Key from the licence manager's seal.go (mesh-catalog feat/57 at 8aa37e8) on 2000 random inputs: all equal. The TypeScript side uses hkdfSync with the same salt (eph SPKI DER followed by the recipient's raw key) and the same info string, "novox-mesh sealed box v1".
Fixtures: the licence manager's original code opens all 5 sealed boxes and reproduces all 5 vectors byte for byte. sealed-by-typescript.json is identical to the licence manager's copy.
Proof that the tests bite: with one byte of the HKDF expand changed in Go, 3 seal tests fail. With the info string changed in TypeScript, 3 fail.
Nonce: Seal and seal draw a fresh 12-byte iv from crypto/rand (randomBytes in TypeScript) and a fresh one-time key on every call. The fixed-randomness sealWith is unexported in Go and is not in the package's ./seal export in TypeScript. Open refuses an iv that is not 12 bytes or a tag that is not 16. No manual secret comparisons: the GCM tag check is the libraries' own. Open also no longer aliases ct when appending the tag, as the original did.
X25519: keys are checked for curve on both sides, and an all-zero shared secret is refused by Go's ecdh and by OpenSSL. Only test keys are in the fixtures.
Findings (none blocks the merge):
Must, at merge time: squash. Commit 1b36bcd replaces go/README.md with "PLACEHOLDER". Commit adfb9c8 replaces package.json with "PKGJSON", so the TypeScript build fails at that commit. The two commits also share one title. The final tree is correct; merge with method: squash.
Should, can follow: go/seal/testdata/sealed-by-licence-manager.json:29 and :85 use a plaintext shaped like an Anthropic API key (sk-ant-api03-test-...). It is fake, but secret scanners and the redactor may flag it. A neutral string would avoid that.
Note: src/seal/box.ts:280 and :285 let node's own error escape for a malformed eph, instead of "the box does not open with this key". This is harmless.
Note: issue 650's Expected also says the licence manager uses the box. That part comes in the follow-up mesh-catalog pull request, so issue 650 stays in fixing until it lands. The second copy in modules/claude-code needs the same switch.
Verdict: APPROVE at a7997fc857aea43266bc56cf096de8a4031b2c9f
Independent review of package F1 (issue 650). What I checked and ran, in my own worktree of the head:
- `sh merge-check.sh`: exit 0 (npm ci, tsc --noEmit, 53 of 53 TypeScript tests pass including the six seal tests; gofmt, go vet and go test all pass). `sh go/merge-check.sh`: exit 0, `go/seal` ok.
- HKDF: `boxKey` (go/seal/seal.go:186-195) follows RFC 5869 for L=32. Extract computes PRK = HMAC-SHA256(salt, IKM). Expand computes T(1) = HMAC-SHA256(PRK, info || 0x01). The salt is never empty, so the RFC's zero-salt default does not arise. I compared it with the original `hkdf.Key` from the licence manager's seal.go (mesh-catalog feat/57 at 8aa37e8) on 2000 random inputs: all equal. The TypeScript side uses `hkdfSync` with the same salt (eph SPKI DER followed by the recipient's raw key) and the same info string, "novox-mesh sealed box v1".
- Fixtures: the licence manager's original code opens all 5 `sealed` boxes and reproduces all 5 `vectors` byte for byte. sealed-by-typescript.json is identical to the licence manager's copy.
- Proof that the tests bite: with one byte of the HKDF expand changed in Go, 3 seal tests fail. With the info string changed in TypeScript, 3 fail.
- Nonce: `Seal` and `seal` draw a fresh 12-byte iv from crypto/rand (randomBytes in TypeScript) and a fresh one-time key on every call. The fixed-randomness `sealWith` is unexported in Go and is not in the package's `./seal` export in TypeScript. Open refuses an iv that is not 12 bytes or a tag that is not 16. No manual secret comparisons: the GCM tag check is the libraries' own. Open also no longer aliases `ct` when appending the tag, as the original did.
- X25519: keys are checked for curve on both sides, and an all-zero shared secret is refused by Go's ecdh and by OpenSSL. Only test keys are in the fixtures.
Findings (none blocks the merge):
1. Must, at merge time: squash. Commit 1b36bcd replaces go/README.md with "PLACEHOLDER". Commit adfb9c8 replaces package.json with "PKGJSON", so the TypeScript build fails at that commit. The two commits also share one title. The final tree is correct; merge with `method: squash`.
2. Should, can follow: go/seal/testdata/sealed-by-licence-manager.json:29 and :85 use a plaintext shaped like an Anthropic API key (`sk-ant-api03-test-...`). It is fake, but secret scanners and the redactor may flag it. A neutral string would avoid that.
3. Note: src/seal/box.ts:280 and :285 let node's own error escape for a malformed eph, instead of "the box does not open with this key". This is harmless.
4. Note: issue 650's Expected also says the licence manager uses the box. That part comes in the follow-up mesh-catalog pull request, so issue 650 stays in fixing until it lands. The second copy in modules/claude-code needs the same switch.
mesh-admin
merged commit c8b07242a0 into feat/57-config-and-provisioning2026-10-13 23:52:12 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Work package F1 of design 57 (building config and provisioning; ADR 0349). Issue 650.
What changed
go/seal: the ADR 0206 sealed box (X25519, HKDF-SHA256, AES-256-GCM, envelope{v, eph, iv, tag, ct}, PEM keys), moved from mesh-catalogmodules/claude-licence-manager/cmd/claude-licence-manager/seal.go. API:seal.Box,seal.KeyPair,seal.GenerateKeyPair(),seal.Seal(plaintext, recipientPEM),seal.Open(box, privatePEM),seal.Version.@novox/mesh-sdk/seal(src/seal): the TypeScript twin,seal,open,generateKeyPair,SealedBox,KeyPair,VERSION; also re-exported assealfrom the package root.crypto/hkdf, which needs Go 1.24 while this module saysgo 1.22; HKDF-SHA256 for one 32-byte block is written out withcrypto/hmacinstead of raising the module's Go version. The byte-for-byte vectors prove it equalshkdf.Key.Openrefuses an iv or tag of the wrong length (node would accept a truncated tag), and checks thatephis an X25519 key.Why
The vault (issue 674), the provisioner client and providers (issue 675 and later) all need this box. Code shared between modules belongs in the SDK.
How it is proven
Fixtures in
go/seal/testdata/:sealed-by-licence-manager.json: made by the licence manager's own seal.go (mesh-catalog feat/57-config-and-provisioning at 8aa37e8) to a fixed test recipient key.sealedholds boxes from its unchangedSeal;vectorsholds boxes from the same code with only its two randomness sources (one-time key, iv) fixed, so a seal can be compared byte for byte. The key is derived from a fixed label and guards nothing.sealed-by-typescript.json: the agent module's TypeScript box the licence manager already keeps.Go tests (go/seal/seal_test.go):
TestABoxTheLicenceManagerSealedOpens,TestSealIsByteForByteTheLicenceManagersBox,TestABoxSealedInTypeScriptOpens,TestABoxOpensOnlyForItsRecipient,TestATamperedBoxDoesNotOpen,TestAKeyOfAnotherKindIsRefused.TypeScript tests (test/seal.test.ts), the same six over the same fixture files.
Both failed before (package missing) and pass now.
The reverse direction (the licence manager opens what the SDK seals): the vectors show the SDK seals exactly the licence manager's bytes. Also, with the licence manager switched to this package (prepared, see below), all its existing tests pass unchanged, including
TestABoxSealedInTypeScriptOpensInGo.Deliberately left
require git.novox.be/novox/mesh-sdk/gomust name the merged commit's pseudo-version. The change is ready:seal.gobecomes aliases (type SealedBox = seal.Box, ...),newGCMmoves into crypt.go; tested here against this branch.modules/claude-code/cmd/claude-code/seal.goin mesh-catalog is a second, identical copy of the box. It is not in F1's scope; it should switch the same way.Reviewer's checklist
sh merge-check.sh(npm ci, tsc --noEmit, npm test: 53 of 53 pass; gofmt -l empty, go vet ./..., go test -count=1 ./...: all ok) andsh go/merge-check.sh: pass.The branch has three commits: the first two carried a placeholder README and package.json by my mistake, which the following commits replace. I checked that the branch's final tree equals my tested local tree (
git diffempty). Please squash on merge.Delivery
novox/mesh-sdk@a7997fc857ae— delivered since 2026-10-13T23:52:19ZDelivery plan — builds nothing: the change touches no module of the mesh's graph
Transitions
The commit's note under
refs/notes/mesh-plankeeps every transition:git log --notes=mesh-plan.Verdict: APPROVE at
a7997fc857Independent review of package F1 (issue 650). What I checked and ran, in my own worktree of the head:
sh merge-check.sh: exit 0 (npm ci, tsc --noEmit, 53 of 53 TypeScript tests pass including the six seal tests; gofmt, go vet and go test all pass).sh go/merge-check.sh: exit 0,go/sealok.boxKey(go/seal/seal.go:186-195) follows RFC 5869 for L=32. Extract computes PRK = HMAC-SHA256(salt, IKM). Expand computes T(1) = HMAC-SHA256(PRK, info || 0x01). The salt is never empty, so the RFC's zero-salt default does not arise. I compared it with the originalhkdf.Keyfrom the licence manager's seal.go (mesh-catalog feat/57 at 8aa37e8) on 2000 random inputs: all equal. The TypeScript side useshkdfSyncwith the same salt (eph SPKI DER followed by the recipient's raw key) and the same info string, "novox-mesh sealed box v1".sealedboxes and reproduces all 5vectorsbyte for byte. sealed-by-typescript.json is identical to the licence manager's copy.Sealandsealdraw a fresh 12-byte iv from crypto/rand (randomBytes in TypeScript) and a fresh one-time key on every call. The fixed-randomnesssealWithis unexported in Go and is not in the package's./sealexport in TypeScript. Open refuses an iv that is not 12 bytes or a tag that is not 16. No manual secret comparisons: the GCM tag check is the libraries' own. Open also no longer aliasesctwhen appending the tag, as the original did.Findings (none blocks the merge):
1b36bcdreplaces go/README.md with "PLACEHOLDER". Commitadfb9c8replaces package.json with "PKGJSON", so the TypeScript build fails at that commit. The two commits also share one title. The final tree is correct; merge withmethod: squash.sk-ant-api03-test-...). It is fake, but secret scanners and the redactor may flag it. A neutral string would avoid that.