Events carry what happened and no secret; tokens travel on requests (design 32 §10). The manager's licence.rotated/switched events make the module ask anthropic-licence-manager.current; at start it asks once to catch up. A refresh token appearing in the credentials file is a login: it is pushed to the manager's adopt at once, sealed to the manager's key — the one time a refresh token travels. A switch replaces the old licence's grant whole, removes the API key and its helper, and rewrites oauthAccount in ~/.claude.json. New tools register and unregister MCP servers on this node, or with nodes: all / a list via an mcp.registered event every node consumes; called for one node, the answer names the other nodes running claude-code. 26 tests.
51 lines
2.2 KiB
TypeScript
51 lines
2.2 KiB
TypeScript
// Which account the agent is logged in as (novox/hq ADR 0183): not in the token, but in the agent's
|
|
// own state file beside the home, `~/.claude.json` → `oauthAccount`. Read to attribute a login; written,
|
|
// three keys and nothing else, when a licence is switched, so the file Claude Code shows the account from
|
|
// names the account whose token it now holds (as the predecessor learned: two files that disagree make
|
|
// a later login look like the wrong account).
|
|
|
|
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
|
|
export interface Identity {
|
|
readonly accountUuid: string;
|
|
readonly emailAddress?: string;
|
|
readonly organizationUuid?: string;
|
|
}
|
|
|
|
export function readIdentity(stateFile: string): Identity | null {
|
|
try {
|
|
const raw = JSON.parse(readFileSync(stateFile, "utf8")) as { oauthAccount?: Record<string, unknown> };
|
|
const a = raw.oauthAccount;
|
|
if (!a || typeof a.accountUuid !== "string") return null;
|
|
return {
|
|
accountUuid: a.accountUuid,
|
|
emailAddress: typeof a.emailAddress === "string" ? a.emailAddress : undefined,
|
|
organizationUuid: typeof a.organizationUuid === "string" ? a.organizationUuid : undefined,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Point the state file's account at `id`, keeping every other key as found. Returns whether the file
|
|
* changed; a file that cannot be read as an object is left alone rather than replaced.
|
|
*/
|
|
export function writeIdentity(stateFile: string, id: Identity): boolean {
|
|
let raw: Record<string, unknown>;
|
|
try {
|
|
raw = JSON.parse(readFileSync(stateFile, "utf8")) as Record<string, unknown>;
|
|
if (!raw || typeof raw !== "object") return false;
|
|
} catch {
|
|
raw = {};
|
|
}
|
|
const current = (raw.oauthAccount ?? {}) as Record<string, unknown>;
|
|
if (current.accountUuid === id.accountUuid && current.emailAddress === id.emailAddress
|
|
&& current.organizationUuid === id.organizationUuid) return false;
|
|
raw.oauthAccount = { ...current, accountUuid: id.accountUuid, emailAddress: id.emailAddress, organizationUuid: id.organizationUuid };
|
|
const tmp = `${stateFile}.mesh-tmp`;
|
|
writeFileSync(tmp, JSON.stringify(raw, null, 2), { mode: 0o600 });
|
|
renameSync(tmp, stateFile);
|
|
return true;
|
|
}
|