anthropic model-access modules: manager (refreshable-grant) and consumer

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
This commit is contained in:
2026-09-07 01:00:12 +02:00
parent cd46d53464
commit c206e2e11e
19 changed files with 1293 additions and 0 deletions
@@ -0,0 +1,47 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { generateKeyPairSync } from "node:crypto";
import { sealAtRest, openAtRest, type Envelope } from "../atrest.ts";
/** A node key pair as the mesh records it: raw 32-byte X25519 keys, standard base64. */
function nodeKeys(): { pub: string; priv: string } {
const kp = generateKeyPairSync("x25519");
const pub = (kp.publicKey.export({ format: "jwk" }) as { x: string }).x;
const priv = (kp.privateKey.export({ format: "jwk" }) as { d: string }).d;
// JWK is base64url; the mesh records standard base64 of the same 32 bytes.
const std = (b64url: string) => Buffer.from(b64url, "base64url").toString("base64");
return { pub: std(pub), priv: std(priv) };
}
test("the manager seals a refresh token and reads it back with its own key", () => {
const { pub, priv } = nodeKeys();
const env = sealAtRest("rt-the-refresh-token", pub);
assert.equal(env.managerKey, pub);
// Nothing in the envelope is the refresh token in the clear.
assert.doesNotMatch(env.token, /rt-the-refresh-token/);
assert.doesNotMatch(env.wrappedKey, /rt-the-refresh-token/);
assert.equal(openAtRest(env, pub, priv), "rt-the-refresh-token");
});
test("a node that is not the manager cannot open the envelope", () => {
const manager = nodeKeys();
const other = nodeKeys();
const env = sealAtRest("rt-secret", manager.pub);
assert.throws(() => openAtRest(env, other.pub, other.priv));
});
test("two seals of the same token look nothing alike", () => {
const { pub } = nodeKeys();
const a = sealAtRest("rt-secret", pub);
const b = sealAtRest("rt-secret", pub);
assert.notEqual(a.token, b.token);
assert.notEqual(a.wrappedKey, b.wrappedKey);
});
test("a tampered envelope is refused, not silently mis-opened", () => {
const { pub, priv } = nodeKeys();
const env = sealAtRest("rt-secret", pub);
const flipped: Envelope = { ...env, token: Buffer.from(env.token, "base64").reverse().toString("base64") };
assert.throws(() => openAtRest(flipped, pub, priv));
});