Files
mesh-catalog/modules/anthropic-consumer/apply/index.ts
T
jschoubben c206e2e11e 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
2026-09-07 01:00:12 +02:00

76 lines
3.5 KiB
TypeScript

// The consumer's scheduled run: take the ACCESS token the mesh delivered and write it where the
// Claude CLI reads it, access-token-only (novox/hq ADR 0050). The refresh token is never here to
// strip — the manager holds it, and a holder's delivery has only ever been the access token.
//
// What the host delivers, per the manifest:
// secrets.model-access -> a file holding the sealed-then-unsealed ACCESS token (the host opened it
// with this node's private key; this process reads plaintext).
// binds.model-access -> a JSON file of the non-secret facts the licence serves (which licence,
// model, and — when the control plane carries them — grant expiry/scopes).
//
// Runs as `mesh-tools run` (no broker) on a schedule, so it is idempotent: same token in, same file
// out.
import { readFileSync } from "node:fs";
import { deliver, type DeliveredGrant } from "../credentials.js";
import { readAccountUuid, check } from "../identity.js";
function required(name: string): string {
const v = process.env[name];
if (!v) throw new Error(`${name} is not set — the consumer runtime was deployed without it`);
return v;
}
/** Read optional non-secret grant metadata (expiry, scopes, subscription) from the bound facts file. */
function readBoundMeta(path: string | undefined): Partial<DeliveredGrant> {
if (!path) return {};
try {
const raw = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
return {
expiresAt: typeof raw.expiresAt === "number" ? raw.expiresAt : null,
refreshTokenExpiresAt: typeof raw.refreshTokenExpiresAt === "number" ? raw.refreshTokenExpiresAt : null,
scopes: Array.isArray(raw.scopes) ? (raw.scopes as string[]) : null,
subscriptionType: typeof raw.subscriptionType === "string" ? raw.subscriptionType : null,
};
} catch {
return {};
}
}
function main(): void {
const accessToken = readFileSync(required("MESH_MODEL_ACCESS_SECRET_FILE"), "utf8").trim();
if (!accessToken) {
// Nothing was delivered — which reads exactly like a credential that never arrived, so it is
// said rather than written as an empty file the CLI would take for a login it should not do.
throw new Error("[anthropic-consumer] the delivered access token is empty; nothing was written");
}
const meta = readBoundMeta(process.env.MESH_MODEL_ACCESS_BIND_FILE);
const grant: DeliveredGrant = { accessToken, ...meta };
const target = process.env.MESH_CLAUDE_CREDENTIALS_FILE ?? `${homedir()}/.claude/.credentials.json`;
deliver(target, grant);
console.error(`[anthropic-consumer] wrote an access-token-only credential to ${target}`);
// The mis-binding guard, best-effort and fail-closed. The expected account uuid is not yet plumbed
// (identity.ts TODO), so this reports what it can see rather than acting on it — it never delivers
// to a wrong account because it never learns one to deliver to.
const identityFile = process.env.MESH_CLAUDE_IDENTITY_FILE ?? `${homedir()}/.claude.json`;
const found = readAccountUuid(identityFile);
const expected = process.env.MESH_MODEL_ACCESS_ACCOUNT_UUID ?? null;
const verdict = check(found, expected);
if (verdict.state === "wrong-account") {
throw new Error(
`[anthropic-consumer] the CLI is logged in as ${verdict.found}, not the licensed ${verdict.expected}; refusing`,
);
}
console.error(`[anthropic-consumer] identity check: ${verdict.state}`);
}
function homedir(): string {
return process.env.HOME ?? "/root";
}
main();