anthropic model-access: manager + consumer modules #16

Merged
jschoubben merged 3 commits from feat/anthropic-module into main 2026-09-07 00:48:52 +00:00
Owner

The first vendor binding of model-access (ADR 0050): anthropic-manager (seals the operator refresh token to the node public key at adopt, refreshes against Anthropic's OAuth endpoint, re-seals a rotated token, emits only the access token + opaque box + licence-grain usage) and anthropic-consumer (writes ~/.claude/.credentials.json access-token-only, reports session-grain usage from the CLI transcripts per ADR 0054).

sealedbox.ts delegates to the audited tweetnacl-sealedbox-js (libsodium crypto_box_seal wire format — the Go host still opens it; cross-check proven). The manager declares tweetnacl-sealedbox-js/tweetnacl as runtime deps.

Proven end to end by the mesh-lab anthropic-bed suite (green).

The first vendor binding of model-access (ADR 0050): `anthropic-manager` (seals the operator refresh token to the node public key at adopt, refreshes against Anthropic's OAuth endpoint, re-seals a rotated token, emits only the access token + opaque box + licence-grain usage) and `anthropic-consumer` (writes `~/.claude/.credentials.json` access-token-only, reports session-grain usage from the CLI transcripts per ADR 0054). `sealedbox.ts` delegates to the audited `tweetnacl-sealedbox-js` (libsodium `crypto_box_seal` wire format — the Go host still opens it; cross-check proven). The manager declares `tweetnacl-sealedbox-js`/`tweetnacl` as runtime deps. Proven end to end by the mesh-lab `anthropic-bed` suite (green).
jschoubben added 3 commits 2026-09-07 00:48:30 +00:00
Phase C of vendor-agnostic model-access (ADR 0050/0054). Two TypeScript
runtime modules:

- anthropic-manager: the refresh token is sealed at rest to the manager
  node's own key (atrest.ts, envelope encryption over X25519) and opened
  ONLY on the manager node. adopt seals the first envelope; refresh opens
  it, calls the Anthropic OAuth token endpoint, re-seals a rotated refresh
  token, and hands the control plane only the access token plus the opaque
  envelope. Also polls licence-grain usage (ADR 0054).
- anthropic-consumer: writes the delivered access token to
  ~/.claude/.credentials.json, access-token-only, atomically (the refresh
  token is never delivered); reports session-grain usage from the CLI
  transcripts; a fail-closed identity guard (expected-uuid plumbing is a
  flagged TODO).

Both run as scheduled containers (ADR 0053). Pure logic covered by
node --test fixtures (at-rest round-trip, credential strip, transcript
sum, refresh merge).

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
The manager module drops its bespoke ECIES at-rest envelope and the node-private-key mount.
A module is never given a node's private key, so it cannot open an envelope -- the refresh
token is now delivered to it as cleartext by the host, unsealed from an ordinary sealed box.

  - sealedbox.ts: a dependency-free NaCl crypto_box_seal (node:crypto for X25519, transcribed
    XSalsa20-Poly1305 and BLAKE2b-24), byte-compatible with Go's box.SealAnonymous. It SEALS
    only -- opening is the host's job. Proven by a cross-language test in mesh-control.
  - adopt: reads the node's PUBLIC key from the delivered bound facts and seals the operator's
    refresh token to it, handing out only the box.
  - refresh: reads the refresh token as cleartext the host mounted, calls the vendor, re-seals
    a rotated token to the node's public key, submits only { access token, box }.
  - module.json: a model-access holder now -- binds the facts, binds the refresh token as a
    sealed secret; no keys dir, no MESH_NODE_SEALING_* mount.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
The manager reseals a rotated refresh token to the node key with crypto_box_seal. That
seal was a full inline transcription of TweetNaCl's XSalsa20-Poly1305 and blakejs' BLAKE2b
(dependency-free, ~440 lines). Replace the internals with the audited
tweetnacl-sealedbox-js library — the same crypto_box_seal, on the same tweetnacl and blakejs
the mesh used to validate the seal during Phase C.

The exported API is unchanged: seal(value, recipientPublicB64) -> base64. The wire format is
unchanged too — ephemeralPub(32) followed by the box, nonce = blake2b(ephemeralPub +
recipientPub, 24) — so the host's Go box.OpenAnonymous still opens it. The mesh-control
cross-check fixture is regenerated from this seal().

The library and tweetnacl are added to the module's package.json dependencies so the runtime
image bundles them (blakejs arrives transitively). A local ambient .d.ts types the untyped
CJS bundle; it is imported as a default import because Node's ESM loader cannot see a UMD
bundle's named exports.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
jschoubben merged commit bfbdf90496 into main 2026-09-07 00:48:52 +00:00
jschoubben deleted branch feat/anthropic-module 2026-09-07 00:48:52 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-catalog#16