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:
@@ -0,0 +1,39 @@
|
||||
// Adoption: the ONE time an operator's refresh token enters the mesh, and it enters already sealed.
|
||||
//
|
||||
// The refresh token is read here, on the MANAGER NODE, sealed at rest to that node's own key, and
|
||||
// only the sealed envelope leaves this process (novox/hq ADR 0050, Phase C). The control plane stores
|
||||
// that envelope via `licence set-grant` without ever seeing the refresh token in the clear — the same
|
||||
// bound every refresh keeps. This is the counterpart to `refresh/index.js`: adoption seals the first
|
||||
// envelope, refresh opens and re-seals it.
|
||||
//
|
||||
// MESH_ANTHROPIC_REFRESH_TOKEN_FILE the operator's refresh token, read once and never written out
|
||||
// MESH_NODE_SEALING_PUBLIC_FILE the manager node's public sealing key (base64 raw X25519)
|
||||
// MESH_ANTHROPIC_GRANT_OUT where the sealed envelope is written, for `licence set-grant`
|
||||
|
||||
import { readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
import { sealAtRest } from "../atrest.js";
|
||||
|
||||
function required(name: string): string {
|
||||
const v = process.env[name];
|
||||
if (!v) throw new Error(`${name} is not set — adoption needs it`);
|
||||
return v;
|
||||
}
|
||||
|
||||
const refreshToken = readFileSync(required("MESH_ANTHROPIC_REFRESH_TOKEN_FILE"), "utf8").trim();
|
||||
if (!refreshToken) throw new Error("[anthropic-manager] there is no refresh token to adopt");
|
||||
|
||||
const nodePub = readFileSync(required("MESH_NODE_SEALING_PUBLIC_FILE"), "utf8").trim();
|
||||
const envelope = sealAtRest(refreshToken, nodePub);
|
||||
|
||||
const out = required("MESH_ANTHROPIC_GRANT_OUT");
|
||||
mkdirSync(dirname(out), { recursive: true });
|
||||
const tmp = `${out}.tmp`;
|
||||
writeFileSync(
|
||||
tmp,
|
||||
JSON.stringify({ token: envelope.token, wrapped_key: envelope.wrappedKey, manager_key: envelope.managerKey }),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
renameSync(tmp, out);
|
||||
console.error("[anthropic-manager] sealed the refresh token at rest; only this node's key opens it");
|
||||
Reference in New Issue
Block a user