One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
158 lines
7.3 KiB
TypeScript
158 lines
7.3 KiB
TypeScript
// The manager's scheduled run (novox/hq ADR 0050/0053). It is the whole of the carve-out in one
|
||
// place, and it runs on the MANAGER NODE, never in the control plane:
|
||
//
|
||
// 1. read the refresh token as CLEARTEXT — the host unsealed the stored box with THIS node's private
|
||
// key and mounted it at the module's bound secret path, exactly as it delivers any credential.
|
||
// This module holds no node key and opens nothing itself;
|
||
// 2. call the vendor's OAuth token endpoint to mint a fresh access token (and maybe a rotated
|
||
// refresh token);
|
||
// 3. if the vendor rotated the refresh token, SEAL the new one to this node's PUBLIC sealing key
|
||
// (delivered in the bound facts) with the same anonymous box the mesh seals every credential with;
|
||
// 4. hand the control plane back ONLY the access token in the clear + the opaque re-sealed box —
|
||
// never the refresh token — which it seals per consumer holder and stores;
|
||
// 5. poll usage with the fresh access token and record the licence-grain reading.
|
||
//
|
||
// mesh-controller receives the products of steps 3–4 through `licence submit-refresh` (access token +
|
||
// sealed box). The refresh token never leaves this process except as ciphertext, and it never had to
|
||
// be opened here at all — the host did that.
|
||
//
|
||
// This runs as `mesh-tools run`, which connects no broker, so the outputs are written to files the
|
||
// host mounts; the submit itself (the transport to mesh-controller) is done by the caller invoking
|
||
// `mesh-controller licence submit-refresh`. In the lab that caller is the scenario; in production it is
|
||
// an authenticated call the manager node makes. The transport is the one part stubbed here — FLAGGED
|
||
// — because a cross-node authenticated command surface is out of this module's scope.
|
||
|
||
import { readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
||
import { dirname } from "node:path";
|
||
|
||
import { readEnv } from "@novox/mesh-sdk/primitives";
|
||
|
||
import { seal } from "../sealedbox.js";
|
||
import { managerPublicKey, writeSealedGrant } from "../grantfile.js";
|
||
import { refreshGrant, grantFromRefresh, readUsage, flattenUsage, type UsageReading } from "../client.js";
|
||
|
||
/** The vendor-neutral usage row ADR 0054 fixes — the shape the model-usage store upserts. Kept local
|
||
* to the producer (the normalisation lives in the adapter), so nothing here couples to the SDK. */
|
||
interface UsageRow {
|
||
licence: string;
|
||
consumer: string;
|
||
period: string;
|
||
metric: string;
|
||
value: number;
|
||
}
|
||
|
||
/** Normalise a licence-grain reading into ADR 0054 rows: one utilization row per window, a row
|
||
* omitted when its percentage is absent. `consumer` is the holding module — the licence grain. */
|
||
function licenceRows(licence: string, consumer: string, reading: UsageReading): UsageRow[] {
|
||
const rows: UsageRow[] = [];
|
||
const add = (period: string, pct: number | null): void => {
|
||
if (pct !== null && pct !== undefined && Number.isFinite(pct)) {
|
||
rows.push({ licence, consumer, period, metric: "utilization", value: pct });
|
||
}
|
||
};
|
||
add("5h", reading.sessionPct);
|
||
add("7d", reading.weeklyPct);
|
||
add("extra", reading.extraPct);
|
||
return rows;
|
||
}
|
||
|
||
function required(name: string): string {
|
||
const v = process.env[name];
|
||
if (!v) throw new Error(`${name} is not set — the manager runtime was deployed without it`);
|
||
return v;
|
||
}
|
||
|
||
function atomicWrite(path: string, content: string): void {
|
||
mkdirSync(dirname(path), { recursive: true });
|
||
const tmp = `${path}.tmp`;
|
||
writeFileSync(tmp, content, { mode: 0o600 });
|
||
renameSync(tmp, path);
|
||
}
|
||
|
||
async function main(): Promise<void> {
|
||
const licence = process.env.MESH_ANTHROPIC_LICENCE ?? "unknown";
|
||
|
||
// Step 1: the refresh token as cleartext, unsealed and mounted by the HOST. No open here.
|
||
const refreshToken = readFileSync(required("MESH_MODEL_ACCESS_SECRET_FILE"), "utf8").trim();
|
||
if (!refreshToken) {
|
||
// Nothing was delivered — the manager has not adopted a refresh token yet, or the push has not
|
||
// landed. Said rather than treated as an empty token the vendor would reject obscurely.
|
||
throw new Error("[anthropic-manager] no refresh token was delivered; adopt one first");
|
||
}
|
||
|
||
// The node's PUBLIC sealing key, to re-seal a rotated refresh token. Public, delivered in the facts.
|
||
const nodePub = managerPublicKey(required("MESH_MODEL_ACCESS_BIND_FILE"));
|
||
|
||
// Step 2: the vendor call.
|
||
const refreshed = await refreshGrant(refreshToken);
|
||
if (!refreshed) {
|
||
// A dead endpoint or a rejected token: nothing to publish, and we do not clobber a good grant.
|
||
throw new Error(`[anthropic-manager] the refresh of ${licence} produced no grant`);
|
||
}
|
||
const grant = grantFromRefresh(refreshed, Date.now());
|
||
if (!grant) {
|
||
throw new Error(`[anthropic-manager] the refresh of ${licence} returned no access token`);
|
||
}
|
||
|
||
// Step 3: re-seal the rotated refresh token, if the vendor rotated it. Nothing to store otherwise.
|
||
if (grant.rotatedRefresh) {
|
||
const sealed = seal(new Uint8Array(Buffer.from(grant.rotatedRefresh, "utf8")), nodePub);
|
||
if (process.env.MESH_ANTHROPIC_GRANT_OUT) {
|
||
writeSealedGrant(process.env.MESH_ANTHROPIC_GRANT_OUT, sealed, nodePub);
|
||
}
|
||
}
|
||
|
||
// Step 4: the access token in the clear, for the control plane to seal per consumer holder. This is
|
||
// all it ever receives that is not ciphertext.
|
||
atomicWrite(required("MESH_ANTHROPIC_ACCESS_OUT"), grant.access.accessToken);
|
||
|
||
console.error(
|
||
`[anthropic-manager] refreshed ${licence}: access token minted` +
|
||
(grant.rotatedRefresh ? ", refresh token rotated and re-sealed" : ", refresh token unchanged"),
|
||
);
|
||
|
||
// Step 5: licence-grain usage, best-effort — a usage read failing must not fail the refresh.
|
||
try {
|
||
const usage = await readUsage(grant.access.accessToken);
|
||
if (usage) {
|
||
const reading = flattenUsage(usage);
|
||
if (process.env.MESH_ANTHROPIC_USAGE_OUT) {
|
||
atomicWrite(
|
||
process.env.MESH_ANTHROPIC_USAGE_OUT,
|
||
JSON.stringify({ licence, grain: "licence", ...reading }),
|
||
);
|
||
}
|
||
// Emit ADR-0054 rows, not a vendor-shaped body: the consumer of module.*.usage.* is the
|
||
// vendor-neutral model-usage store, so the normalisation is done HERE. The node names the
|
||
// holding module; with MESH_NODE unset the consumer is the module alone.
|
||
const node = readEnv("MESH_NODE", "");
|
||
const consumer = node ? `${node}/anthropic-manager` : "anthropic-manager";
|
||
await emitUsage({ rows: licenceRows(licence, consumer, reading), raw: usage });
|
||
}
|
||
} catch (err) {
|
||
console.error(`[anthropic-manager] usage poll for ${licence} failed: ${err}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Emit a usage event best-effort by shelling out to the sibling mesh-tools `emit` primitive, which
|
||
* is the one path that wires a broker from a run-once/scheduled step (which itself connects none).
|
||
* A broker hiccup must never fail a refresh that already happened.
|
||
*/
|
||
async function emitUsage(body: Record<string, unknown>): Promise<void> {
|
||
const main = process.env.MESH_TOOLS_MAIN ?? "/app/dist/main.js";
|
||
const { spawn } = await import("node:child_process");
|
||
await new Promise<void>((resolve) => {
|
||
const child = spawn(process.execPath, [main, "emit", "module.anthropic-manager.usage.read", JSON.stringify(body)], {
|
||
stdio: "inherit",
|
||
});
|
||
child.on("exit", () => resolve());
|
||
child.on("error", (err) => {
|
||
console.error(`[anthropic-manager] could not emit usage: ${err}`);
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
await main();
|