consortium
Version:
Remote control and session sharing CLI for AI coding agents
13,366 lines • 481 kB
JavaScript
'use strict';
var axios = require('axios');
var chalk = require('chalk');
var fs$1 = require('fs');
var fs = require('node:fs');
var os = require('node:os');
var path = require('node:path');
var node_events = require('node:events');
var socket_ioClient = require('socket.io-client');
var z = require('zod');
var node_crypto = require('node:crypto');
var tweetnacl = require('tweetnacl');
var node_module = require('node:module');
var child_process = require('child_process');
var util = require('util');
var fs$2 = require('fs/promises');
var os$1 = require('os');
var crypto = require('crypto');
var path$1 = require('path');
var url = require('url');
var node_child_process = require('node:child_process');
var fs$3 = require('node:fs/promises');
var node_util = require('node:util');
var http = require('http');
var net = require('net');
var expoServerSdk = require('expo-server-sdk');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
var name = "consortium";
var version = "0.8.12";
var description = "Remote control and session sharing CLI for AI coding agents";
var author = "ConsortiumAI";
var license = "BUSL-1.1";
var type = "module";
var homepage = "https://github.com/ConsortiumAI/consortium-cli";
var bugs = "https://github.com/ConsortiumAI/consortium-cli/issues";
var repository = "ConsortiumAI/consortium-cli";
var bin = {
consortium: "./bin/consortium.mjs",
"consortium-local": "./bin/consortium-local.mjs",
"consortium-mcp": "./bin/consortium-mcp.mjs"
};
var main = "./dist/index.cjs";
var module$1 = "./dist/index.mjs";
var types = "./dist/index.d.cts";
var exports$1 = {
".": {
require: {
types: "./dist/index.d.cts",
"default": "./dist/index.cjs"
},
"import": {
types: "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"./lib": {
require: {
types: "./dist/lib.d.cts",
"default": "./dist/lib.cjs"
},
"import": {
types: "./dist/lib.d.mts",
"default": "./dist/lib.mjs"
}
},
"./codex/consortiumMcpStdioBridge": {
require: {
types: "./dist/codex/consortiumMcpStdioBridge.d.cts",
"default": "./dist/codex/consortiumMcpStdioBridge.cjs"
},
"import": {
types: "./dist/codex/consortiumMcpStdioBridge.d.mts",
"default": "./dist/codex/consortiumMcpStdioBridge.mjs"
}
},
"./pi-ext": {
require: {
types: "./dist/pi/ext/consortium-permission/index.d.cts",
"default": "./dist/pi/ext/consortium-permission/index.cjs"
},
"import": {
types: "./dist/pi/ext/consortium-permission/index.d.mts",
"default": "./dist/pi/ext/consortium-permission/index.mjs"
}
}
};
var files = [
"dist",
"bin",
"scripts",
"tools/archives",
"tools/licenses",
"package.json"
];
var scripts = {
"why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started consortium",
typecheck: "tsc --noEmit",
build: "npx shx rm -rf dist && node -e \"process.argv=process.argv.concat(['--noEmit']);require('typescript/lib/tsc.js')\" && node scripts/build-stamp.cjs && node scripts/copy-pi-ext-pkg.cjs",
"build:tests": "node -e \"process.argv=process.argv.concat(['--noEmit']);require('typescript/lib/tsc.js')\" && node scripts/build-stamp.cjs && node scripts/copy-pi-ext-pkg.cjs",
"check:no-placeholder-key": "node -e \"const fs=require('fs');const s=fs.readFileSync('src/harness/verifySignature.ts','utf8');const m=s.match(/-----BEGIN PUBLIC KEY-----[\\s\\S]*?-----END PUBLIC KEY-----/);if(!m||m[0].includes('PLACEHOLDER')){console.error('placeholder pubkey in verifySignature.ts');process.exit(1)}\"",
"check:harness-key": "node scripts/check-harness-key.cjs",
test: "$npm_execpath run check:no-placeholder-key && $npm_execpath run build && vitest run",
start: "$npm_execpath run build && node ./bin/consortium.mjs",
dev: "tsx src/index.ts",
"dev:local-server": "$npm_execpath run build && tsx --env-file .env.dev-local-server src/index.ts",
"dev:cluster": "$npm_execpath run build && tsx --env-file .env.dev-cluster src/index.ts",
"dev:integration-test-env": "$npm_execpath run build && tsx --env-file .env.integration-test src/index.ts",
prepublishOnly: "$npm_execpath run build && $npm_execpath test && node scripts/check-dist-version.cjs",
release: "$npm_execpath install && release-it",
postinstall: "node scripts/unpack-tools.cjs && node scripts/fix-node-pty-perms.cjs && node scripts/fix-libsodium-esm.cjs && node scripts/verify-platform.cjs",
"// ==== Dev/Stable Variant Management ====": "",
stable: "node scripts/env-wrapper.cjs stable",
"dev:variant": "node scripts/env-wrapper.cjs dev",
"// ==== Stable Version Quick Commands ====": "",
"stable:daemon:start": "node scripts/env-wrapper.cjs stable daemon start",
"stable:daemon:stop": "node scripts/env-wrapper.cjs stable daemon stop",
"stable:daemon:status": "node scripts/env-wrapper.cjs stable daemon status",
"stable:auth": "node scripts/env-wrapper.cjs stable auth",
"// ==== Development Version Quick Commands ====": "",
"dev:daemon:start": "node scripts/env-wrapper.cjs dev daemon start",
"dev:daemon:stop": "node scripts/env-wrapper.cjs dev daemon stop",
"dev:daemon:status": "node scripts/env-wrapper.cjs dev daemon status",
"dev:auth": "node scripts/env-wrapper.cjs dev auth",
"// ==== Setup ====": "",
"setup:dev": "node scripts/setup-dev.cjs",
doctor: "node scripts/env-wrapper.cjs stable doctor",
"// ==== Development Linking ====": "",
"link:dev": "node scripts/link-dev.cjs",
"unlink:dev": "node scripts/link-dev.cjs unlink"
};
var dependencies = {
"@agentclientprotocol/sdk": "^0.8.0",
"@modelcontextprotocol/sdk": "^1.25.3",
"@sentry/node": "^8.46.0",
"@stablelib/base64": "^2.0.1",
"@stablelib/hex": "^2.0.1",
ai: "^5.0.107",
axios: "^1.13.2",
"better-sqlite3": "^12.9.0",
chalk: "^5.6.2",
"cross-spawn": "^7.0.6",
"expo-server-sdk": "^3.15.0",
fastify: "^5.6.2",
"fastify-type-provider-zod": "4.0.2",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^3.0.5",
imapflow: "^1.7.8",
ink: "^6.5.1",
"libsodium-wrappers": "^0.7.13",
mailparser: "^3.9.20",
nodemailer: "^9.1.1",
open: "^10.2.0",
"playwright-core": "1.58.2",
"ps-list": "^8.1.1",
"qrcode-terminal": "^0.12.0",
react: "^19.2.0",
"socket.io-client": "^4.8.1",
tar: "^7.5.2",
tmp: "^0.2.5",
tweetnacl: "^1.0.3",
ws: "^8.18.0",
zod: "3.25.76",
yaml: "^2.8.2",
pixelmatch: "^6.0.0",
pngjs: "^7.0.0"
};
var devDependencies = {
"@eslint/compat": "^1",
"@types/better-sqlite3": "^7.6.13",
"@types/cross-spawn": "^6.0.6",
"@types/http-proxy": "^1.17.17",
"@types/libsodium-wrappers": "^0.7.14",
"@types/mailparser": "^3.4.6",
"@types/node": ">=20",
"@types/ps-list": "^6.2.1",
"@types/qrcode-terminal": "^0.12.2",
"@types/react": "^19.2.7",
"@types/tmp": "^0.2.6",
"@types/ws": "^8.5.12",
"cross-env": "^10.1.0",
dotenv: "^16.6.1",
eslint: "^9",
"eslint-config-prettier": "^10",
"ink-testing-library": "^4.0.0",
pkgroll: "^2.14.2",
"release-it": "^19.0.6",
shx: "^0.3.3",
"ts-node": "^10",
tsx: "^4.20.6",
typescript: "5.9.3",
vitest: "^3.2.4",
"@consortium/crypto": "*",
"@consortium/atlas-core": "*",
"@consortium/browser-core": "*",
"@consortium/browser-protocol": "*",
"@consortium/device-protocol": "*",
"@yume-chan/adb": "^2.1.0",
"@yume-chan/adb-scrcpy": "^2.1.0",
"@yume-chan/scrcpy": "^2.1.0",
"@yume-chan/adb-server-node-tcp": "^2.1.0",
"@yume-chan/stream-extra": "^2.1.0",
"@consortium/extension-sdk": "*",
"@consortium/mail-core": "*",
"consortium-bench": "*"
};
var resolutions = {
"whatwg-url": "14.2.0",
"parse-path": "7.0.3",
"@types/parse-path": "7.0.3"
};
var optionalDependencies = {
"node-pty": "^1.0.0",
"consortium-code-darwin-arm64": "0.2.0-canary.20260417210700",
"consortium-code-darwin-x64": "0.2.0-canary.20260417210700",
"consortium-code-linux-arm64": "0.2.0-canary.20260417210700",
"consortium-code-linux-x64": "0.2.0-canary.20260417210700"
};
var publishConfig = {
registry: "https://registry.npmjs.org"
};
var packageManager = "yarn@1.22.22";
var engines = {
node: ">=20"
};
var packageJson = {
name: name,
version: version,
description: description,
author: author,
license: license,
type: type,
homepage: homepage,
bugs: bugs,
repository: repository,
bin: bin,
main: main,
module: module$1,
types: types,
exports: exports$1,
files: files,
scripts: scripts,
dependencies: dependencies,
devDependencies: devDependencies,
resolutions: resolutions,
optionalDependencies: optionalDependencies,
publishConfig: publishConfig,
packageManager: packageManager,
engines: engines
};
function isLocalhostUrl(url) {
try {
const host = new URL(url).hostname;
return host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1";
} catch {
return false;
}
}
class Configuration {
serverUrl;
webappUrl;
isDaemonProcess;
// Directories and paths (from persistence)
consortiumHomeDir;
logsDir;
settingsFile;
privateKeyFile;
daemonStateFile;
daemonLockFile;
currentCliVersion;
isExperimentalEnabled;
disableCaffeinate;
authToken = process.env.CONSORTIUM_TOKEN;
constructor() {
if (process.env.CONSORTIUM_HOME_DIR) {
const expandedPath = process.env.CONSORTIUM_HOME_DIR.replace(/^~/, os.homedir());
this.consortiumHomeDir = expandedPath;
} else {
this.consortiumHomeDir = path.join(os.homedir(), ".consortium");
}
this.settingsFile = path.join(this.consortiumHomeDir, "settings.json");
let persistedServerUrl;
try {
const rawSettings = fs.readFileSync(this.settingsFile, "utf-8");
const parsed = JSON.parse(rawSettings);
if (typeof parsed.serverUrl === "string" && /^https?:\/\//.test(parsed.serverUrl)) {
persistedServerUrl = parsed.serverUrl;
}
} catch {
}
this.serverUrl = process.env.CONSORTIUM_SERVER_URL || persistedServerUrl || "https://api.consortium.dev";
this.webappUrl = process.env.CONSORTIUM_WEBAPP_URL || (isLocalhostUrl(this.serverUrl) ? `http://localhost:${process.env.CONSORTIUM_WEBAPP_PORT || "8081"}` : "https://consortium.dev");
const args = process.argv.slice(2);
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
this.logsDir = path.join(this.consortiumHomeDir, "logs");
this.privateKeyFile = path.join(this.consortiumHomeDir, "access.key");
this.daemonStateFile = path.join(this.consortiumHomeDir, "daemon.state.json");
this.daemonLockFile = path.join(this.consortiumHomeDir, "daemon.state.json.lock");
this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.CONSORTIUM_EXPERIMENTAL?.toLowerCase() || "");
this.disableCaffeinate = ["true", "1", "yes"].includes(process.env.CONSORTIUM_DISABLE_CAFFEINATE?.toLowerCase() || "");
this.currentCliVersion = packageJson.version;
const variant = process.env.CONSORTIUM_VARIANT || "stable";
if (variant === "dev" && !this.consortiumHomeDir.includes("dev")) {
console.warn('\u26A0\uFE0F WARNING: CONSORTIUM_VARIANT=dev but CONSORTIUM_HOME_DIR does not contain "dev"');
console.warn(` Current: ${this.consortiumHomeDir}`);
console.warn(` Expected: Should contain "dev" (e.g., ~/.consortium-dev)`);
}
if (!this.isDaemonProcess && variant === "dev") {
console.log("\x1B[33m\u{1F527} DEV MODE\x1B[0m - Data: " + this.consortiumHomeDir);
}
if (!fs.existsSync(this.consortiumHomeDir)) {
fs.mkdirSync(this.consortiumHomeDir, { recursive: true, mode: 448 });
}
if (!fs.existsSync(this.logsDir)) {
fs.mkdirSync(this.logsDir, { recursive: true, mode: 448 });
}
}
}
const configuration = new Configuration();
const MAX_CAUSE_DEPTH = 4;
function serializeError(err, depth = 0) {
if (err instanceof Error) {
const out = {
name: err.name || "Error",
message: err.message || ""
};
if (err.stack) {
out.stack = err.stack;
}
const code = err.code;
if (typeof code === "string") {
out.code = code;
}
const cause = err.cause;
if (cause !== void 0 && depth < MAX_CAUSE_DEPTH) {
out.cause = serializeError(cause, depth + 1);
}
return out;
}
if (typeof err === "object" && err !== null) {
try {
return { name: "NonError", message: JSON.stringify(err) };
} catch {
return { name: "NonError", message: String(err) };
}
}
return { name: "NonError", message: String(err) };
}
const RECENT_LINES_MAX = 100;
function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
return date.toLocaleString("sv-SE", {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
}).replace(/[: ]/g, "-").replace(/,/g, "") + "-pid-" + process.pid;
}
function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
return date.toLocaleTimeString("en-US", {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
hour12: false,
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3
});
}
function getSessionLogPath() {
const timestamp = createTimestampForFilename();
const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
return path.join(configuration.logsDir, filename);
}
class Logger {
constructor(logFilePath = getSessionLogPath()) {
this.logFilePath = logFilePath;
this.structuredLogFilePath = logFilePath.replace(/\.log$/, ".jsonl");
if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && process.env.CONSORTIUM_SERVER_URL) {
this.dangerouslyUnencryptedServerLoggingUrl = process.env.CONSORTIUM_SERVER_URL;
console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
}
}
dangerouslyUnencryptedServerLoggingUrl;
/** Sibling structured log: same path with .jsonl extension */
structuredLogFilePath;
recentLines = [];
subscribers = [];
/** Correlation ID inherited from the spawning daemon, if any */
inheritedReqId = process.env.CONSORTIUM_REQUEST_ID || void 0;
/**
* Subscribe to every structured entry (in-process). Used by the daemon log
* watchdog. Subscribers must never throw — exceptions are swallowed so a
* broken subscriber can't take down logging.
*/
subscribe(fn) {
this.subscribers.push(fn);
return () => {
const idx = this.subscribers.indexOf(fn);
if (idx >= 0) this.subscribers.splice(idx, 1);
};
}
/** Last N plaintext log lines, oldest first. Feeds last-exit.json forensics. */
getRecentLines() {
return [...this.recentLines];
}
/**
* Structured logging entry point: writes both the plaintext line (for
* humans) and the JSONL line (for tooling), with an explicit level and
* optional error / context / correlation ID.
*/
event(level, message, opts) {
this.write(level, message, [], opts);
}
/**
* Unified sink: one call produces exactly one plaintext line and one JSONL
* line, updates the ring buffer, and notifies subscribers. All public log
* methods route through here.
*/
write(level, message, args, extra) {
const entry = {
ts: (/* @__PURE__ */ new Date()).toISOString(),
level,
msg: message
};
const errSource = extra?.err !== void 0 ? extra.err : args.find((a) => a instanceof Error);
if (errSource !== void 0) entry.err = serializeError(errSource);
if (extra?.ctx) entry.ctx = extra.ctx;
const reqId = extra?.reqId ?? this.inheritedReqId;
if (reqId) entry.reqId = reqId;
const plaintextParts = [...args];
if (extra?.err !== void 0) plaintextParts.push(serializeError(extra.err));
if (extra?.ctx) plaintextParts.push(extra.ctx);
const levelPrefix = level === "debug" ? "" : `[${level.toUpperCase()}] `;
this.logToFile(`[${this.localTimezoneTimestamp()}]`, `${levelPrefix}${message}`, ...plaintextParts);
try {
fs$1.appendFileSync(this.structuredLogFilePath, JSON.stringify(entry) + "\n");
} catch {
}
for (const fn of this.subscribers) {
try {
fn(entry);
} catch {
}
}
}
// Use local timezone for simplicity of locating the logs,
// in practice you will not need absolute timestamps
localTimezoneTimestamp() {
return createTimestampForLogEntry();
}
debug(message, ...args) {
this.write("debug", message, args);
}
debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
if (!process.env.DEBUG) {
this.debug(`In production, skipping message inspection`);
}
const truncateStrings = (obj) => {
if (typeof obj === "string") {
return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
}
if (Array.isArray(obj)) {
const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
if (obj.length > maxArrayLength) {
truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
}
return truncatedArray;
}
if (obj && typeof obj === "object") {
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (key === "usage") {
continue;
}
result[key] = truncateStrings(value);
}
return result;
}
return obj;
};
const truncatedObject = truncateStrings(object);
const json = JSON.stringify(truncatedObject, null, 2);
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, "\n", json);
}
info(message, ...args) {
this.logToConsole("info", "", message, ...args);
this.write("info", message, args);
}
infoDeveloper(message, ...args) {
this.debug(message, ...args);
if (process.env.DEBUG) {
this.logToConsole("info", "[DEV]", message, ...args);
}
}
warn(message, ...args) {
this.logToConsole("warn", "", message, ...args);
this.write("warn", message, args);
}
error(message, ...args) {
this.logToConsole("error", "", message, ...args);
this.write("error", message, args);
}
getLogPath() {
return this.logFilePath;
}
logToConsole(level, prefix, message, ...args) {
switch (level) {
case "debug": {
console.log(chalk.gray(prefix), message, ...args);
break;
}
case "error": {
console.error(chalk.red(prefix), message, ...args);
break;
}
case "info": {
console.log(chalk.blue(prefix), message, ...args);
break;
}
case "warn": {
console.log(chalk.yellow(prefix), message, ...args);
break;
}
default: {
this.debug("Unknown log level:", level);
console.log(chalk.blue(prefix), message, ...args);
break;
}
}
}
async sendToRemoteServer(level, message, ...args) {
if (!this.dangerouslyUnencryptedServerLoggingUrl) return;
try {
await fetch(this.dangerouslyUnencryptedServerLoggingUrl + "/logs-combined-from-cli-and-mobile-for-simple-ai-debugging", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
level,
message: `${message} ${args.map(
(a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)
).join(" ")}`,
source: "cli",
platform: process.platform
})
});
} catch (error) {
}
}
logToFile(prefix, message, ...args) {
const errorAwareReplacer = (_key, value) => value instanceof Error ? serializeError(value) : value;
const logLine = `${prefix} ${message} ${args.map(
(arg) => typeof arg === "string" ? arg : arg instanceof Error ? JSON.stringify(serializeError(arg)) : JSON.stringify(arg, errorAwareReplacer)
).join(" ")}
`;
this.recentLines.push(logLine.trimEnd());
if (this.recentLines.length > RECENT_LINES_MAX) {
this.recentLines.splice(0, this.recentLines.length - RECENT_LINES_MAX);
}
if (this.dangerouslyUnencryptedServerLoggingUrl) {
let level = "info";
if (prefix.includes(this.localTimezoneTimestamp())) {
level = "debug";
}
this.sendToRemoteServer(level, message, ...args).catch(() => {
});
}
try {
fs$1.appendFileSync(this.logFilePath, logLine);
} catch (appendError) {
if (process.env.DEBUG) {
console.error("[DEV MODE ONLY THROWING] Failed to append to log file:", appendError);
throw appendError;
}
}
}
}
let logger = new Logger();
async function listDaemonLogFiles(limit = 50) {
try {
const logsDir = configuration.logsDir;
if (!fs.existsSync(logsDir)) {
return [];
}
const logs = fs.readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
const fullPath = path.join(logsDir, file);
const stats = fs.statSync(fullPath);
return { file, path: fullPath, modified: stats.mtime };
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
try {
const { readDaemonState } = await Promise.resolve().then(function () { return persistence; });
const state = await readDaemonState();
if (!state) {
return logs;
}
if (state.daemonLogPath && fs.existsSync(state.daemonLogPath)) {
const stats = fs.statSync(state.daemonLogPath);
const persisted = {
file: path.basename(state.daemonLogPath),
path: state.daemonLogPath,
modified: stats.mtime
};
const idx = logs.findIndex((l) => l.path === persisted.path);
if (idx >= 0) {
const [found] = logs.splice(idx, 1);
logs.unshift(found);
} else {
logs.unshift(persisted);
}
}
} catch {
}
return logs.slice(0, Math.max(0, limit));
} catch {
return [];
}
}
async function getLatestDaemonLog() {
const [latest] = await listDaemonLogFiles(1);
return latest || null;
}
const SessionMessageContentSchema = z.z.object({
c: z.z.string(),
// Base64 encoded encrypted content
t: z.z.literal("encrypted")
});
const UpdateBodySchema = z.z.object({
message: z.z.object({
id: z.z.string(),
seq: z.z.number(),
content: SessionMessageContentSchema
}),
sid: z.z.string(),
// Session ID
t: z.z.literal("new-message")
});
const UpdateSessionBodySchema = z.z.object({
t: z.z.literal("update-session"),
sid: z.z.string(),
metadata: z.z.object({
version: z.z.number(),
value: z.z.string()
}).nullish(),
agentState: z.z.object({
version: z.z.number(),
value: z.z.string()
}).nullish()
});
const UpdateMachineBodySchema = z.z.object({
t: z.z.literal("update-machine"),
machineId: z.z.string(),
metadata: z.z.object({
version: z.z.number(),
value: z.z.string()
}).nullish(),
daemonState: z.z.object({
version: z.z.number(),
value: z.z.string()
}).nullish()
});
z.z.object({
id: z.z.string(),
seq: z.z.number(),
body: z.z.union([
UpdateBodySchema,
UpdateSessionBodySchema,
UpdateMachineBodySchema
]),
createdAt: z.z.number()
});
const HardwareProfileSchema = z.z.object({
schemaVersion: z.z.literal(1),
platform: z.z.string(),
arch: z.z.string(),
cpuModel: z.z.string(),
cpuCoresPhysical: z.z.number(),
cpuThreads: z.z.number(),
ramTotalBytes: z.z.number(),
gpus: z.z.array(z.z.object({
vendor: z.z.enum(["apple", "nvidia", "amd", "intel", "unknown"]),
name: z.z.string(),
vramBytes: z.z.number().optional(),
driver: z.z.string().optional(),
computeCapability: z.z.string().optional()
})),
unifiedMemory: z.z.boolean(),
usableModelBytes: z.z.number(),
memBandwidthGBs: z.z.number().optional(),
bandwidthSource: z.z.enum(["measured", "chip-table", "unknown"]),
diskFreeBytes: z.z.number(),
detectedAt: z.z.number(),
warnings: z.z.array(z.z.string())
});
z.z.object({
host: z.z.string(),
platform: z.z.string(),
consortiumCliVersion: z.z.string(),
homeDir: z.z.string(),
consortiumHomeDir: z.z.string(),
consortiumLibDir: z.z.string(),
// Hardware specs — collected once at daemon startup from os module
// (no syscall, libuv-cached). Optional so older daemons stay valid.
cpuCount: z.z.number().optional(),
memoryTotalMb: z.z.number().optional(),
// Full hardware profile (GPU, VRAM, unified-memory budget, bandwidth) used
// by the local-model fit engine. Populated asynchronously after daemon boot,
// so it is absent on a freshly-started daemon and on older CLI versions.
hardware: HardwareProfileSchema.optional(),
// OpenClaw detection fields (populated by daemon heartbeat)
openclawInstalled: z.z.boolean().optional(),
openclawVersion: z.z.string().nullable().optional(),
openclawWorkspaceExists: z.z.boolean().optional(),
openclawGatewayRunning: z.z.boolean().optional(),
openclawGatewayPort: z.z.number().optional(),
openclawChannels: z.z.array(z.z.string()).optional(),
openclawAgentCount: z.z.number().optional(),
openclawAgents: z.z.array(z.z.object({
name: z.z.string(),
workspace: z.z.string(),
agentDir: z.z.string(),
model: z.z.string(),
routingRules: z.z.number(),
isDefault: z.z.boolean()
})).optional(),
// Generic per-harness runtime detection, keyed by harnessType. Populated by
// the daemon heartbeat for every registered harness (openclaw, hermes, …).
harnessInstances: z.z.record(z.z.string(), z.z.object({
type: z.z.string(),
family: z.z.string().optional(),
installed: z.z.boolean(),
version: z.z.string().nullable().optional(),
workspaceExists: z.z.boolean().optional(),
gatewayRunning: z.z.boolean().optional(),
gatewayPort: z.z.number().optional(),
channels: z.z.array(z.z.string()).optional(),
agentCount: z.z.number().optional(),
agents: z.z.array(z.z.object({
// Canonical agent id (deriveAgentId for managed agents). The detection
// layer has always written it; the schema used to strip it, so the app
// could only match agents by display name — which drifts on rename.
id: z.z.string().optional(),
name: z.z.string(),
// Runtime liveness as the harness reports it ('active' | 'stopped' | …).
status: z.z.string().optional(),
port: z.z.number().optional(),
workspace: z.z.string().optional(),
agentDir: z.z.string().optional(),
model: z.z.string().optional(),
routingRules: z.z.number().optional(),
isDefault: z.z.boolean().optional()
})).optional()
})).optional(),
// Terminal multiplexer capabilities — populated at daemon startup.
// Tells the mobile app whether "New persistent terminal" should be enabled.
terminalCapabilities: z.z.object({
tmux: z.z.boolean(),
zellij: z.z.boolean()
}).optional()
});
z.z.object({
status: z.z.union([
z.z.enum(["running", "shutting-down"]),
z.z.string()
// Forward compatibility
]),
pid: z.z.number().optional(),
httpPort: z.z.number().optional(),
startedAt: z.z.number().optional(),
shutdownRequestedAt: z.z.number().optional(),
shutdownSource: z.z.union([
z.z.enum(["mobile-app", "cli", "os-signal", "unknown"]),
z.z.string()
// Forward compatibility
]).optional()
});
z.z.object({
content: SessionMessageContentSchema,
createdAt: z.z.number(),
id: z.z.string(),
seq: z.z.number(),
updatedAt: z.z.number()
});
const MessageMetaSchema = z.z.object({
sentFrom: z.z.string().optional(),
// Source identifier
permissionMode: z.z.enum(["default", "acceptEdits", "bypassPermissions", "plan", "read-only", "safe-yolo", "yolo"]).optional(),
// Permission mode for this message
model: z.z.string().nullable().optional(),
// Model name for this message (null = reset)
fallbackModel: z.z.string().nullable().optional(),
// Fallback model for this message (null = reset)
customSystemPrompt: z.z.string().nullable().optional(),
// Custom system prompt for this message (null = reset)
appendSystemPrompt: z.z.string().nullable().optional(),
// Append to system prompt for this message (null = reset)
allowedTools: z.z.array(z.z.string()).nullable().optional(),
// Allowed tools for this message (null = reset)
disallowedTools: z.z.array(z.z.string()).nullable().optional()
// Disallowed tools for this message (null = reset)
});
z.z.object({
session: z.z.object({
id: z.z.string(),
tag: z.z.string(),
seq: z.z.number(),
createdAt: z.z.number(),
updatedAt: z.z.number(),
metadata: z.z.string(),
metadataVersion: z.z.number(),
agentState: z.z.string().nullable(),
agentStateVersion: z.z.number()
})
});
const UserMessageSchema = z.z.object({
role: z.z.literal("user"),
content: z.z.object({
type: z.z.literal("text"),
text: z.z.string(),
// Session-attachments (PR 5): Drive node ids and the client-generated
// local id ride alongside `text` inside the encrypted body. Optional /
// additive — older messages without these fields must continue to
// parse. PR 6 will consume them when forwarding to ACP.
attachmentIds: z.z.array(z.z.string()).optional(),
localId: z.z.string().optional(),
// PR 5 inline-envelope path: when the message carries its own
// driveId + per-message DEK, the resolver can hit Drive directly
// without consulting the server's binding rows.
driveId: z.z.string().optional(),
driveNodeIds: z.z.array(z.z.string()).optional(),
driveDek: z.z.string().optional(),
// Per-attachment records (successor wire shape): each attachment
// carries its OWN driveId + per-file key + name/mime, so the sender
// no longer ships a whole-drive DEK and mixed-drive sends work.
attachments: z.z.array(z.z.object({
nodeId: z.z.string(),
driveId: z.z.string(),
kind: z.z.string(),
key: z.z.string().optional(),
name: z.z.string().optional(),
mime: z.z.string().optional(),
size: z.z.number().optional(),
linkUri: z.z.string().optional()
})).optional()
}),
localKey: z.z.string().optional(),
// Mobile messages include this
meta: MessageMetaSchema.optional(),
// Convenience copy — apiSession also surfaces attachmentIds at the top
// level (mirroring `content.attachmentIds`) so consumers don't have to
// reach through `content`. PR 6 owns the forwarding path that reads
// this; PR 5 just makes sure the field is preserved end-to-end.
attachmentIds: z.z.array(z.z.string()).optional(),
localId: z.z.string().optional(),
driveId: z.z.string().optional(),
driveDek: z.z.string().optional()
});
const AgentMessageSchema = z.z.object({
role: z.z.literal("agent"),
content: z.z.object({
type: z.z.literal("output"),
data: z.z.any()
}),
meta: MessageMetaSchema.optional()
});
z.z.union([UserMessageSchema, AgentMessageSchema]);
function encryptBox(env, data, recipientPublicKey) {
const { sodium, getRandomBytes } = env;
const ephemeralKeyPair = sodium.crypto_box_keypair();
const nonce = getRandomBytes(sodium.crypto_box_NONCEBYTES);
const encrypted = sodium.crypto_box_easy(data, nonce, recipientPublicKey, ephemeralKeyPair.privateKey);
const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
result.set(ephemeralKeyPair.publicKey, 0);
result.set(nonce, ephemeralKeyPair.publicKey.length);
result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
return result;
}
function decryptBox(env, encryptedBundle, recipientSecretKey) {
const { sodium } = env;
const ephemeralPublicKey = encryptedBundle.slice(0, sodium.crypto_box_PUBLICKEYBYTES);
const nonce = encryptedBundle.slice(
sodium.crypto_box_PUBLICKEYBYTES,
sodium.crypto_box_PUBLICKEYBYTES + sodium.crypto_box_NONCEBYTES
);
const encrypted = encryptedBundle.slice(sodium.crypto_box_PUBLICKEYBYTES + sodium.crypto_box_NONCEBYTES);
try {
return sodium.crypto_box_open_easy(encrypted, nonce, ephemeralPublicKey, recipientSecretKey);
} catch {
return null;
}
}
const SIGN_BYTES = 64;
const SIGN_PUBLICKEYBYTES = 32;
const DEK_WRAP_VERSION_LEGACY = 0;
const DEK_WRAP_VERSION_SIGNED = 1;
function concatBytes(...parts) {
let total = 0;
for (const p of parts) total += p.length;
const out = new Uint8Array(total);
let off = 0;
for (const p of parts) {
out.set(p, off);
off += p.length;
}
return out;
}
function bytesEqual(a, b) {
if (a.length !== b.length) return false;
let diff = 0;
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
return diff === 0;
}
function signDetached(env, message, signSecretKey) {
const { sodium } = env;
if (typeof sodium.crypto_sign_detached !== "function") {
throw new Error(
"signed DEK wrap requested but this libsodium binding lacks crypto_sign_detached (Ed25519 unavailable)"
);
}
return sodium.crypto_sign_detached(message, signSecretKey);
}
function verifyDetached(env, signature, message, signPublicKey) {
const { sodium } = env;
if (typeof sodium.crypto_sign_verify_detached !== "function") return false;
try {
return sodium.crypto_sign_verify_detached(signature, message, signPublicKey) === true;
} catch {
return false;
}
}
function deriveSignPublicKey(env, signSecretKey) {
const { sodium } = env;
if (typeof sodium.crypto_sign_ed25519_sk_to_pk === "function") {
try {
return sodium.crypto_sign_ed25519_sk_to_pk(signSecretKey);
} catch {
}
}
if (signSecretKey.length === 64) return signSecretKey.slice(32, 64);
return null;
}
function decodeBase64$1(base64, encoding = "base64") {
let normalizedBase64 = base64;
if (encoding === "base64url") {
normalizedBase64 = base64.replace(/-/g, "+").replace(/_/g, "/");
const padding = normalizedBase64.length % 4;
if (padding) {
normalizedBase64 += "=".repeat(4 - padding);
}
}
const binaryString = atob(normalizedBase64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
function encodeBase64$1(buffer, encoding = "base64") {
const CHUNK_SIZE = 32768;
let binaryString = "";
for (let i = 0; i < buffer.length; i += CHUNK_SIZE) {
const chunk = buffer.subarray(i, i + CHUNK_SIZE);
binaryString += String.fromCharCode.apply(null, Array.from(chunk));
}
const base64 = btoa(binaryString);
if (encoding === "base64url") {
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
return base64;
}
const DRIVE_DEK_SIGN_DOMAIN = new TextEncoder().encode("consortium/dek-wrap/v1");
function generateDriveDek(env) {
return env.getRandomBytes(32);
}
function encryptDriveDek(env, dek, publicKey, signSecretKey, senderSignPublicKey) {
const sealed = encryptBox(env, dek, publicKey);
{
const result = new Uint8Array(sealed.length + 1);
result[0] = DEK_WRAP_VERSION_LEGACY;
result.set(sealed, 1);
return result;
}
}
function decryptDriveDek(env, encrypted, privateKey, expectedSenderSignPublicKey) {
const version = encrypted[0];
if (version === DEK_WRAP_VERSION_LEGACY) {
return decryptBox(env, encrypted.slice(1), privateKey);
}
if (version === DEK_WRAP_VERSION_SIGNED) {
const senderPub = encrypted.slice(1, 1 + SIGN_PUBLICKEYBYTES);
const signature = encrypted.slice(1 + SIGN_PUBLICKEYBYTES, 1 + SIGN_PUBLICKEYBYTES + SIGN_BYTES);
const sealed = encrypted.slice(1 + SIGN_PUBLICKEYBYTES + SIGN_BYTES);
if (senderPub.length !== SIGN_PUBLICKEYBYTES || signature.length !== SIGN_BYTES) return null;
if (expectedSenderSignPublicKey && !bytesEqual(senderPub, expectedSenderSignPublicKey)) return null;
if (!verifyDetached(env, signature, concatBytes(DRIVE_DEK_SIGN_DOMAIN, sealed), senderPub)) return null;
return decryptBox(env, sealed, privateKey);
}
return null;
}
function encryptDriveContent(env, plaintext, dek) {
const { sodium, getRandomBytes } = env;
const nonce = getRandomBytes(sodium.crypto_secretbox_NONCEBYTES);
const ciphertext = sodium.crypto_secretbox_easy(plaintext, nonce, dek);
const result = new Uint8Array(nonce.length + ciphertext.length);
result.set(nonce);
result.set(ciphertext, nonce.length);
return result;
}
function decryptDriveContent(env, encrypted, dek) {
const { sodium } = env;
const nonce = encrypted.slice(0, sodium.crypto_secretbox_NONCEBYTES);
const ciphertext = encrypted.slice(sodium.crypto_secretbox_NONCEBYTES);
try {
return sodium.crypto_secretbox_open_easy(ciphertext, nonce, dek);
} catch {
return null;
}
}
function encryptDriveMetadata(env, metadata, dek) {
const plaintext = new TextEncoder().encode(JSON.stringify(metadata));
const encrypted = encryptDriveContent(env, plaintext, dek);
return encodeBase64$1(encrypted, "base64");
}
function decryptDriveMetadata(env, encrypted, dek) {
try {
const data = decodeBase64$1(encrypted, "base64");
const decrypted = decryptDriveContent(env, data, dek);
if (!decrypted) return null;
return JSON.parse(new TextDecoder().decode(decrypted));
} catch {
return null;
}
}
const FIELD_VERSION = 0;
const GCM_IV_BYTES = 12;
const ARTIFACT_DEK_SIGN_DOMAIN = new TextEncoder().encode("consortium/artifact-dek-wrap/v1");
function subtle$1() {
const c = globalThis.crypto;
if (!c?.subtle) {
throw new Error("artifact crypto requires Web Crypto (crypto.subtle) \u2014 unavailable in this runtime");
}
return c.subtle;
}
function toArrayBuffer$1(arr) {
return arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
}
function generateArtifactDek(env) {
return env.getRandomBytes(32);
}
function wrapArtifactDek(env, dek, userPublicKey, signSecretKey, senderSignPublicKey) {
const sealed = encryptBox(env, dek, userPublicKey);
if (!signSecretKey) {
const out2 = new Uint8Array(sealed.length + 1);
out2[0] = DEK_WRAP_VERSION_LEGACY;
out2.set(sealed, 1);
return encodeBase64$1(out2, "base64");
}
const senderPub = senderSignPublicKey ?? deriveSignPublicKey(env, signSecretKey);
if (!senderPub) {
throw new Error(
"wrapArtifactDek: cannot derive Ed25519 sender public key; pass senderSignPublicKey explicitly"
);
}
const signature = signDetached(env, concatBytes(ARTIFACT_DEK_SIGN_DOMAIN, sealed), signSecretKey);
const out = new Uint8Array(1 + senderPub.length + signature.length + sealed.length);
out[0] = DEK_WRAP_VERSION_SIGNED;
out.set(senderPub, 1);
out.set(signature, 1 + senderPub.length);
out.set(sealed, 1 + senderPub.length + signature.length);
return encodeBase64$1(out, "base64");
}
async function encryptArtifactField(value, dek) {
const key = await subtle$1().importKey("raw", toArrayBuffer$1(dek), { name: "AES-GCM" }, false, ["encrypt"]);
const iv = new Uint8Array(GCM_IV_BYTES);
const c = globalThis.crypto;
c.getRandomValues(iv);
const plaintext = new TextEncoder().encode(JSON.stringify(value));
const ct = new Uint8Array(await subtle$1().encrypt({ name: "AES-GCM", iv }, key, toArrayBuffer$1(plaintext)));
const out = new Uint8Array(1 + iv.length + ct.length);
out[0] = FIELD_VERSION;
out.set(iv, 1);
out.set(ct, 1 + iv.length);
return encodeBase64$1(out, "base64");
}
const SHA512_BYTES = 64;
const SHA512_BLOCK_BYTES = 128;
const DERIVED_SEED_BYTES = 32;
async function sha512(env, data) {
const s = env.sodium;
if (typeof s.crypto_hash_sha512 === "function") return s.crypto_hash_sha512(data);
if (typeof s.crypto_hash === "function") return s.crypto_hash(data);
const subtle = globalThis.crypto?.subtle;
if (!subtle) {
throw new Error(
"deriveKeyV2 requires SHA-512: binding lacks crypto_hash_sha512/crypto_hash and no Web Crypto subtle is available"
);
}
const buf = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
return new Uint8Array(await subtle.digest("SHA-512", buf));
}
async function hmacSha512(env, key, message) {
let k = key;
if (k.length > SHA512_BLOCK_BYTES) k = await sha512(env, k);
const padded = new Uint8Array(SHA512_BLOCK_BYTES);
padded.set(k);
const inner = new Uint8Array(SHA512_BLOCK_BYTES + message.length);
const outerPrefix = new Uint8Array(SHA512_BLOCK_BYTES);
for (let i = 0; i < SHA512_BLOCK_BYTES; i++) {
inner[i] = padded[i] ^ 54;
outerPrefix[i] = padded[i] ^ 92;
}
inner.set(message, SHA512_BLOCK_BYTES);
const innerHash = await sha512(env, inner);
const outer = new Uint8Array(SHA512_BLOCK_BYTES + SHA512_BYTES);
outer.set(outerPrefix);
outer.set(innerHash, SHA512_BLOCK_BYTES);
return sha512(env, outer);
}
async function deriveSeedV2(env, master, label) {
const tag = await hmacSha512(env, master, new TextEncoder().encode(label));
return tag.slice(0, DERIVED_SEED_BYTES);
}
const V2_CONTENT_DEK_WRAP_LABEL = "consortium/v2/dek-wrap";
const ENVELOPE_MAGIC = 226;
const FRAMED_HEADER_VERSION = 1;
const SCHEME_V4 = 4;
const FRAMED_HEADER_BYTES = 8;
const CONTENT_DEK_WRAP_VERSION = 220;
const GCM_NONCE_BYTES = 12;
const GCM_TAG_BYTES = 16;
const DEK_BYTES = 32;
const SHARED_CONTENT_DOMAIN = "consortium/content/v1";
const CONTENT_DOMAIN_AAD$1 = new TextEncoder().encode(SHARED_CONTENT_DOMAIN);
const SEALED_DEK_MARKER_HEADER = new Uint8Array([
ENVELOPE_MAGIC,
FRAMED_HEADER_VERSION,
SCHEME_V4,
0,
0,
0,
0,
0
]);
async function deriveContentDekWrapKeypair(env, master) {
const seed = await deriveSeedV2(env, master, V2_CONTENT_DEK_WRAP_LABEL);
if (seed.length !== DERIVED_SEED_BYTES) {
throw new Error(`deriveContentDekWrapKeypair: unexpected seed length ${seed.length}`);
}
try {
const kp = env.sodium.crypto_box_seed_keypair(seed);
const secretKey = kp.secretKey ?? kp.privateKey;
if (!secretKey) {
throw new Error("deriveContentDekWrapKeypair: binding returned no box secret key");
}
return { publicKey: kp.publicKey, secretKey };
} finally {
seed.fill(0);
}
}
function unwrapContentDek(env, frame, dekWrapSecretKey) {
if (frame.length < 1 || frame[0] !== CONTENT_DEK_WRAP_VERSION) {
return null;
}
const dek = decryptBox(env, frame.subarray(1), dekWrapSecretKey);
if (!dek || dek.length !== DEK_BYTES) {
return null;
}
return dek;
}
function parseSealedDekContent(payload) {
if (payload.length < 2) {
return null;
}
const wrappedLen = payload[0] << 8 | payload[1];
let off = 2;
if (payload.length < off + wrappedLen + GCM_NONCE_BYTES + GCM_TAG_BYTES) {
return null;
}
const wrappedDek = payload.slice(off, off + wrappedLen);
off += wrappedLen;
const nonce = payload.slice(off, off + GCM_NONCE_BYTES);
off += GCM_NONCE_BYTES;
const ct = payload.slice(off);
return { wrappedDek, nonce, ct };
}
function isSealedDekContent(blob) {
if (blob.length < FRAMED_HEADER_BYTES + 2) {
return false;
}
for (let i = 0; i < FRAMED_HEADER_BYTES; i++) {
if (blob[i] !== SEALED_DEK_MARKER_HEADER[i]) {
return false;
}
}
return true;
}
function readSealedDekContent(blob) {
if (!isSealedDekContent(blob)) {
return null;
}
return parseSealedDekContent(blob.subarray(FRAMED_HEADER_BYTES));
}
function subtle() {
const c = globalThis.crypto;
if (!c?.subtle) {
throw new Error("aesgcm-dek.v1 requires Web Crypto (crypto.subtle) \u2014 unavailable in this runtime");
}
return c.subtle;
}
function toArrayBuffer(arr) {
return arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength);
}
async function aesGcmOpenBody(dek, nonce, body) {
try {
const key = await subtle().importKey("raw", toArrayBuffer(dek), { name: "AES-GCM" }, false, ["decrypt"]);
const pt = await subtle().decrypt(
{ name: "AES-GCM", iv: toArrayBuffer(nonce), additionalData: toArrayBuffer(CONTENT_DOMAIN_AAD$1), tagLength: 128 },
key,
toArrayBuffer(body)
);
return new Uint8Array(pt);
} catch {
return null;
}
}
async function openSealedDekBytesWithDek(blob, dek) {
if (dek.length !== DEK_BYTES) {
return null;
}
const parsed = readSealedDekContent(blob);
if (!parsed) {
return null;
}
return aesGcmOpenBody(dek, parsed.nonce, parsed.ct);
}
new TextEncoder().encode("consortium/identity-rotation/v1");
new TextEncoder();
class MessageDecodeError extends Error {
constructor(message) {
super(message);
this.name = "MessageDecodeError";
}
}
new TextEncoder();
const strictTextDecoder = new TextDecoder("utf-8", { fatal: true });
function isPlainObject(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function coerceAttachmentIds(raw) {
if (!Array.isArray(raw)) return [];
const out = [];
for (const entry of raw) {
if (typeof entry !== "string") return [];
out.push(entry);
}
return out;
}
function decodeMessageBody(plaintext) {
let text;
try {
text = strictTextDecoder.decode(plaintext);
} catch (e) {
throw new MessageDecodeError(
`Message body is not valid UTF-8: ${e.message}`
);
}
const trimmed = text.trimStart();
if (!trimmed.startsWith("{")) {
return { v: 1, text };
}
let parsed;
try {
parsed = JSON.parse(text);
} catch {
return { v: 1, text };
}
if (!isPlainObject(parsed)) {
return { v: 1, text };
}
if (typeof parsed.v !== "number" || parsed.v !== 2) {
return { v: 1, text };
}
const innerText = typeof parsed.text === "string" ? parsed.text : "";
const attachmentIds = coerceAttachmentIds(parsed.attachmentIds);
const result = {
v: 2,
text: innerText,
attachmentIds
};
if (typeof parsed.localId === "string") {
result.localId = parsed.localId;
}
return result;
}
const require$2 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-BYXHizYk.cjs', document.baseURI).href)));
const sodium = require$2("libsodium-wrappers");
let ENV = null;
let READY = null;
async function getCryptoEnv() {
if (ENV) return ENV;
if (!READY) {
READY = (async () => {
await sodium.ready;
const env = {
// libsodium-wrappers exposes the required surface.
// Cast: the .d.ts types are broader than SodiumLike but
// structurally compatible.
sodium,
getRandomBytes: (size) => new Uint8Array(node_crypto.randomBytes(size))
};
ENV = env;
return env;
})();
}
return READY;
}
const CONTENT_DOMAIN_AAD = Buffer.from(SHARED_CONTENT_DOMAIN, "utf8");
let dekWrapKeypair = null;
let configuring = null;
let cachedEnv = null;
async function primeSealedDekEnv() {
if (cachedEnv) return;
try {
cachedEnv = await getCryptoEnv();
} catch {
cachedEnv = null;
}
}
async function configureSealedDekContent(credentials) {
const enc = credentials?.encryption;
if (!enc || enc.type !== "legacy" || enc.secret.length !== 32) {
return;
}
if (dekWrapKeypair) {
return;
}
if (!configuring) {
configuring = (async () => {
try {
await primeSealedDekEnv();
if (cachedEnv) {
dekWrapKeypair = await deriveContentDekWrapKeypair(cachedEnv, enc.secret);
}
} catch {
dekWrapKeypair = null;
} finally {
configuring = null;
}
})();
}
await configuring;
}
function isSealedDekReadable() {
return dekWrapKeypair !== null;
}
function openSealedDekBytesSync(blob) {
if (!dekWrapKeypair) {
return null;
}
const parsed = readSealedDekContent(blob);
if (!parsed) {
return null;
}
const env = cachedEnv;
if (!env) {
return null;
}
const dek = unwrapContentDek(env, parsed.wrappedDek, dekWrapKeypair.secretKey);
if (!dek) {
return null;
}
try {
const tag = parsed.ct.subarray(parsed.ct.length - 16);
const ct = parsed.ct.subarray(0, parsed.ct.length - 16);
const decipher = node_crypto.createDecipheriv("aes-256-gcm", dek, parsed.nonce);
decipher.setAAD(CONTENT_DOMAIN_AAD);
decipher.setAuthTag(tag);
return new Uint8Array(Buffer.concat([decipher.update(ct), decipher.final()]));
} catch {
return null;
} finally {
dek.fill(0);
}
}
function openSealedDekContentSync(blob) {
const bytes = openSealedDekBytesSync(blob);
if (!bytes) {
return null;
}
try {
return JSON.parse(new TextDecoder().decode(bytes));
} catch {
return null;
}
}
function encodeBase64(buffer, variant = "base64") {
if (variant === "base64url") {
return encodeBase64Url(buffer);
}
return Buffer.from(buffer).toString("base64");
}
function encodeBase64Url(buffer) {
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
}
function decodeBase64(base64, variant = "base64") {
if (variant === "base64url") {
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
return new Uint8Array(Buffer.from(base64Standard, "base64"));
}
return new Uint8Array(Buffer.from(base64, "base64"));
}
function getRandomBytes(size) {
return new Uint8Array(node_crypto.randomBytes(size));
}
function libsodiumEncryptForPublicKey(data, recipientPublicKey) {
const ephemeralKeyPair = tweetnacl.box.keyPair();
const nonce = getRandomBytes(tweetnacl.box.nonceLength);
const encrypted = tweetnacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
const result = new Uint8Array(ephemeralKeyPair.publicKey.length + nonce.length + encrypted.length);
result.set(ephemeralKeyPair.publicKey, 0);
result.set(nonce, ephemeralKeyPair.publicKey.length);
result.set(encrypted, ephemeralKeyPair.publicKey.length + nonce.length);
return result;
}
function encryptLegacy(data, secret) {
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
const result = new Uint8Array(nonce.length + encrypted.length);
result.set(nonce);
result.set(encrypted, nonce.length);
return result;
}
function decryptLegacyWithReason(data, secret) {
const minLength = tweetnacl.secretbox.nonceLength + tweetnacl.secretbox.overheadLength;
if (data.length < minLength) {
return { value: null, reason: `payload too short: ${data.length} bytes (need >=${minLength})` };
}
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
if (!decrypted) {
return { value: null, reason: "secretbox authentication failed (wrong key or corrupted ciphertext)" };
}
try {
return { value: JSON.parse(new TextDecoder().decode(decrypted)) };
} catch (err) {
return { value: null, reason: `decrypted plaintext is not valid JSON: ${err instanceof Error ? err.message : String(err)}` };
}
}
function encryptWithDataKey(data, dataKey) {
const nonce = getRandomBytes(12);
const cipher = node_crypto.createCipheriv("aes-256-gcm", dataKey, nonce);
const plaintext = new TextEncoder().encode(JSON.stringify(data));
const encrypted = Buffer.concat([
cipher.update(plaintext),
cipher.final()
]);
const authTag = cipher.getAuthTag();
const bundle = new Uint8Array(12 + encrypted.length + 16 + 1);
bundle.set([0], 0);
bundle.set(nonce, 1);
bundle.set(new Uint8Array(encrypted), 13);
bundle.set(new Uint8Array(authTag), 13 + encrypted.length);
return bundle;
}
function decryptWithDataKeyWithReason(bundle, dataKey) {
if (bundle.length < 1) {
return { value: null, reason: "empty bundle" };
}
if (bundle[0] !== 0) {
return { value: null, reason: `unsupported bundle version ${bundle[0]} (expected 0) \u2014 CLI/app version skew?` };
}
if (bundle.length < 12 + 16 + 1) {
return { value: null, reason: `bundle too short: ${bundle.length} bytes (need >=${12 + 16 + 1})` };
}
const nonce = bundle.slice(1, 13);
const authTag = bundle.slice(bundle.length - 16);
const ciphertext = bundle.slice(13, bundle.length - 16);
let decrypted;
try {
const decipher = node_crypto.createDecipheriv("aes-256-gcm", dataKey, nonce);
decipher.setAuthTag(authTag);
decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
} catch (error) {
return { value: null, reason: `AES-GCM authentication failed (wrong key or corrupted ciphertext): ${error instanceof Error ? error.message : String(error)}` };
}
try {
return { value: JSON.parse(new TextDecoder().decode(decrypted)) };
} catch (err) {
return { value: null, reason: `decrypted plaintext is not valid JSON: ${err instanceof Error ? err.message : String(err)}` };
}
}
function decryptWithDataKey(bundle, dataKey) {
return decryptWithDataKeyWithReason(bundle, dataKey).value;
}
function encrypt(key, variant, data) {
if (variant === "legacy") {
return encryptLegacy(data, key);
} else {
return encryptWithDataKey(data, key);
}
}
function decrypt(key, variant, data) {
return decryptWithReason(key, variant, data).value;
}
function decryptWithReason(key, variant, data) {
const primary = variant === "legacy" ? decryptLegacyWithReason(data, key) : decryptWithDataKeyWithReason(data, key);
if (primary.reason === void 0) {
return primary;
}
if (!isSealedDekContent(data)) {
return primary;
}
if (!isSealedDekReadable()) {
return {
value: null,
reason: `aesgcm-dek.v1 content requires the account seed to derive the content-DEK-wrap key; these credentials carry none (dataKey tier) \u2014 fail closed`
};
}
const opened = openSealedDekContentSync(data);
if (opened === null) {
return { value: null, reason: `aesgcm-dek.v1 content did not open (wrong account, tampered frame, or truncated payload)` };
}
return { value: opened };
}
function signChallenge(secret, challenge) {
const keypair = tweetnacl.sign.keyPair.fromSeed(secret);
const signature = tweetnacl.sign.detached(challenge, keypair.secretKey);
return {
challenge,
publicKey: keypair.publicKey,
signature
};
}
function authChallenge(secret) {
return signChallenge(secret, getRandomBytes(32));
}
async function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
return Math.round(Math.random() * maxDelayRet);
}
function createBackoff(opts) {
return async (callback) => {
let currentFailureCount = 0;
const minDelay = 250;
const maxDelay = 1e3;
const maxFailureCount = 50;
while (true) {
try {
return await callback();
} catch (e) {
if (currentFailureCount < maxFailureCount) {
currentFailureCount++;
}
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
await delay(waitForRequest);
}
}
};
}
let backoff = createBackoff();
class AsyncLock {
permits = 1;
promiseResolverQueue = [];
async inLock(func) {
try {
await this.lock();
return await func();
} finally {
this.unlock();
}
}
async lock() {
if (this.permits > 0) {
this.permits = this.permits - 1;
return;
}
await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
}
unlock() {
this.permits += 1;
if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
throw new Error("this.permits should never be > 0 when there is someone waiting.");
} else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
this.permits -= 1;
const nextResolver = this.promiseResolverQueue.shift();
if (nextResolver) {
setTimeout(() => {
nextResolver(true);
}, 0);
}
}
}
}
const ERROR_SIGNATURES = [
{
key: "crypto-auth-failure",
pattern: /AES-GCM authentication failed|secretbox authentication failed|unable to authenticate data/i,
cause: "E2EE key mismatch between client and daemon, or corrupted ciphertext",
hint: "The app and this machine likely hold different encryption keys. Re-pair the machine (or re-run `consortium auth login`) and restart the daemon."
},
{
key: "crypto-version-skew",
pattern: /unsupported bundle version/i,
cause: "Encrypted payload format is newer/older than this CLI understands",
hint: "CLI and app versions have drifted. Update the older side (`npm upgrade consortium` or update the app)."
},
{
key: "crypto-payload-truncated",
pattern: /payload too short|bundle too short|empty bundle/i,
cause: "Encrypted payload arrived truncated or empty",
hint: "Usually transient network corruption; if it recurs, check the relay/server logs for body-size limits."
},
{
key: "binary-missing",
pattern: /spawn \S+ ENOENT/,
cause: "A required binary is not installed or not on PATH",
hint: "Install the missing binary or remove the integration that requires it. The daemon inherits PATH from its launcher, not your shell profile."
},
{
key: "connection-refused",
pattern: /ECONNREFUSED/,
cause: "Target service is not listening on the expected address/port",
hint: "Check CONSORTIUM_SERVER_URL and that the server is running (`consortium daemon status`)."
},
{
key: "port-in-use",
pattern: /EADDRINUSE/,
cause: "Another process already occupies the port",
hint: "Find the conflicting process (`lsof -i :<port>`) or let the daemon pick a fresh random port by restarting it."
},
{
key: "tls-verification",
pattern: /CERT_|SELF_SIGNED|unable to verify the first certificate|certificate has expired/i,
cause: "TLS certificate verification failed against the server",
hint: "Check the server certificate (self-hosted instances often need the CA added to the trust store)."
},
{
key: "missing-required-param",
pattern: /is required/,
cause: "RPC params missing a required field \u2014 when params decrypted to null, the real failure is the decryption upstream",
hint: 'Look for a "Param decryption failed" log entry just before this one; fix that root cause first.'
}
];
function matchErrorSignature(text) {
for (const sig of ERROR_SIGNATURES) {
if (sig.pattern.test(text)) {
return sig;
}
}
return null;
}
class RpcHandlerManager {
handlers = /* @__PURE__ */ new Map();
scopePrefix;
encryptionKey;
encryptionVariant;
logger;
onError;
socket = null;
constructor(config) {
this.scopePrefix = config.scopePrefix;
this.encryptionKey = config.encryptionKey;
this.encryptionVariant = config.encryptionVariant;
this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
this.onError = config.onError;
}
/**
* Register an RPC handler for a specific method
* @param method - The method name (without prefix)
* @param handler - The handler function
*/
registerHandler(method, handler) {
const prefixedMethod = this.getPrefixedMethod(method);
this.handlers.set(prefixedMethod, handler);
if (this.socket) {
this.socket.emitWithAck("rpc-register", { method: prefixedMethod }).catch((err) => {
this.logger(`[RPC] Failed to register ${prefixedMethod}: ${err}`);
});
}
}
/**
* Handle an incoming RPC request
* @param request - The RPC request data
* @param callback - The response callback
*/
async handleRequest(request) {
let reqId;
try {
const handler = this.handlers.get(request.method);
if (!handler) {
this.logger("[RPC] [ERROR] Method not found", { method: request.method });
const errorResponse = { error: "Method not found", method: request.method };
const encryptedError = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
return encryptedError;
}
const payload = decodeBase64(request.params);
const decryption = decryptWithReason(this.encryptionKey, this.encryptionVariant, payload);
if (decryption.value === null && decryption.reason !== void 0) {
const signature = matchErrorSignature(decryption.reason);
this.logger("[RPC] [ERROR] Param decryption failed", {
method: request.method,
reason: decryption.reason,
payloadBytes: payload.length,
...signature ? { hint: signature.hint } : {}
});
this.onError?.(new Error(`RPC param decryption failed: ${decryption.reason}`), {
method: request.method,
payloadBytes: payload.length
});
const errorResponse = {
error: `Param decryption failed: ${decryption.reason}`,
method: request.method,
...signature ? { hint: signature.hint } : {}
};
return encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
}
const decryptedParams = decryption.value;
if (decryptedParams && typeof decryptedParams === "object" && typeof decryptedParams._reqId === "string") {
reqId = decryptedParams._reqId;
}
this.logger("[RPC] Calling handler", { method: request.method, ...reqId ? { reqId } : {} });
const result = await handler(decryptedParams);
this.logger("[RPC] Handler returned", { method: request.method, hasResult: result !== void 0, ...reqId ? { reqId } : {} });
const encryptedResponse = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, result));
this.logger("[RPC] Sending encrypted response", { method: request.method, responseLength: encryptedResponse.length, ...reqId ? { reqId } : {} });
return encryptedResponse;
} catch (error) {
const serialized = serializeError(error);
const signature = matchErrorSignature(`${serialized.message} ${serialized.code ?? ""}`);
this.logger("[RPC] [ERROR] Error handling request", {
method: request.method,
error: serialized,
...reqId ? { reqId } : {},
...signature ? { hint: signature.hint } : {}
});
this.onError?.(error, { method: request.method, ...reqId ? { reqId } : {} });
const errorResponse = {
error: serialized.message || "Unknown error",
errorName: serialized.name,
...serialized.code ? { errorCode: serialized.code } : {},
method: request.method,
...reqId ? { reqId } : {},
...signature ? { hint: signature.hint } : {}
};
return encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, errorResponse));
}
}
async onSocketConnect(socket) {
this.socket = socket;
const registrations = Array.from(this.handlers.keys()).map(
(prefixedMethod) => socket.emitWithAck("rpc-register", { method: prefixedMethod }).catch((err) => {
this.logger(`[RPC] Failed to register ${prefixedMethod}: ${err}`);
})
);
await Promise.all(registrations);
}
onSocketDisconnect() {
this.socket = null;
}
/**
* Get the number of registered handlers
*/
getHandlerCount() {
return this.handlers.size;
}
/**
* Check if a handler is registered
* @param method - The method name (without prefix)
*/
hasHandler(method) {
const prefixedMethod = this.getPrefixedMethod(method);
return this.handlers.has(prefixedMethod);
}
/**
* Invoke a registered handler in-process with already-decrypted params.
* Used by the generic harness dispatcher to fall through to a legacy
* per-harness RPC (e.g. `openclaw-agent-add`) when the harness provider
* doesn't expose the dispatcher-facing method directly.
*/
async callLocal(method, params) {
const prefixedMethod = this.getPrefixedMethod(method);
const handler = this.handlers.get(prefixedMethod);
if (!handler) return void 0;
return await handler(params);
}
/**
* Clear all handlers
*/
clearHandlers() {
this.handlers.clear();
this.logger("Cleared all RPC handlers");
}
/**
* Get the prefixed method name
* @param method - The method name
*/
getPrefixedMethod(method) {
return `${this.scopePrefix}:${method}`;
}
}
const __dirname$1 = path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-BYXHizYk.cjs', document.baseURI).href))));
function projectPath() {
const path = path$1.resolve(__dirname$1, "..");
return path;
}
function run$1(args, options) {
const RUNNER_PATH = path$1.resolve(path$1.join(projectPath(), "scripts", "ripgrep_launcher.cjs"));
return new Promise((resolve2, reject) => {
const child = child_process.spawn(process.execPath, [RUNNER_PATH, JSON.stringify(args)], {
stdio: ["pipe", "pipe", "pipe"],
cwd: options?.cwd
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (data) => {
stdout += data.toString();
});
child.stderr.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
resolve2({
exitCode: code || 0,
stdout,
stderr
});
});
child.on("error", (err) => {
reject(err);
});
});
}
function getBinaryPath() {
const platformName = os$1.platform();
const binaryName = platformName === "win32" ? "difft.exe" : "difft";
return path$1.resolve(path$1.join(projectPath(), "tools", "unpacked", binaryName));
}
function run(args, options) {
const binaryPath = getBinaryPath();
return new Promise((resolve2, reject) => {
const child = child_process.spawn(binaryPath, args, {
stdio: ["pipe", "pipe", "pipe"],
cwd: options?.cwd,
env: {
...process.env,
// Force color output when needed
FORCE_COLOR: "1"
}
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (data) => {
stdout += data.toString();
});
child.stderr.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (code) => {
resolve2({
exitCode: code || 0,
stdout,
stderr
});
});
child.on("error", (err) => {
reject(err);
});
});
}
function validatePath(targetPath, workingDirectory) {
const resolvedTarget = path$1.resolve(workingDirectory, targetPath);
const resolvedWorkingDir = path$1.resolve(workingDirectory);
if (!resolvedTarget.startsWith(resolvedWorkingDir + "/") && resolvedTarget !== resolvedWorkingDir) {
return {
valid: false,
error: `Access denied: Path '${targetPath}' is outside the working directory`
};
}
return { valid: true };
}
const execFileAsync$6 = util.promisify(child_process.execFile);
const SAFE_GIT_REF = /^[A-Za-z0-9._/-]+$/;
function assertSafeGitRef(value, label) {
if (!value || !SAFE_GIT_REF.test(value) || value.includes("..")) {
throw new Error(`Unsafe git ${label}: ${JSON.stringify(value)}`);
}
}
async function runCommand(file, args, cwd, timeout = 3e4) {
const options = { cwd, timeout };
const result = await execFileAsync$6(file, args, options);
return {
stdout: result.stdout?.toString() || "",
stderr: result.stderr?.toString() || ""
};
}
async function gitExec(args, cwd, timeout = 3e4) {
return runCommand("git", args, cwd, timeout);
}
async function ensureDevGitignore(repoPath) {
const gitignorePath = path$1.join(repoPath, ".gitignore");
try {
const content = await fs$2.readFile(gitignorePath, "utf8");
if (!content.includes(".dev/") && !content.includes(".dev\n")) {
await fs$2.appendFile(gitignorePath, "\n# Worktree directory\n.dev/\n");
}
} catch {
await fs$2.appendFile(gitignorePath, "# Worktree directory\n.dev/\n");
}
}
function slugify(text) {
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").substring(0, 40);
}
async function createCardWorktree(repoPath, cardId, cardSlug, baseBranch = "main") {
assertSafeGitRef(baseBranch, "baseBranch");
const slug = slugify(cardSlug);
const cardIdShort = cardId.substring(0, 7);
const branchName = `card/${cardIdShort}/${slug}`;
const worktreeDir = `.dev/worktrees/${slug}`;
const worktreePath = path$1.join(repoPath, worktreeDir);
assertSafeGitRef(branchName, "branchName");
await ensureDevGitignore(repoPath);
await fs$2.mkdir(path$1.join(repoPath, ".dev/worktrees"), { recursive: true });
try {
await gitExec(["fetch", "origin"], repoPath, 6e4);
} catch (e) {
logger.debug(`[Worktree] Fetch failed (may be offline): ${e}`);
}
const startPoint = `origin/${baseBranch}`;
try {
await gitExec(["worktree", "add", "-b", branchName, worktreeDir, startPoint], repoPath);
} catch (error) {
if (error.stderr?.includes("already exists")) {
try {
await gitExec(["worktree", "add", worktreeDir, branchName], repoPath);
} catch (e2) {
if (e2.stderr?.includes("already registered")) {
return { worktreePath, branchName };
}
throw new Error(`Failed to create worktree: ${e2.stderr || e2.message}`);
}
} else {
throw new Error(`Failed to create worktree: ${error.stderr || error.message}`);
}
}
return { worktreePath, branchName };
}
async function listWorktrees(repoPath, baseBranch = "main") {
assertSafeGitRef(baseBranch, "baseBranch");
const { stdout } = await gitExec(["worktree", "list", "--porcelain"], repoPath);
const worktrees = [];
let current = {};
for (const line of stdout.split("\n")) {
if (line.startsWith("worktree ")) {
if (current.path) {
worktrees.push(current);
}
current = { path: line.substring(9), changedFiles: [] };
} else if (line.startsWith("HEAD ")) {
current.head = line.substring(5);
} else if (line.startsWith("branch ")) {
const branch = line.substring(7).replace("refs/heads/", "");
current.branch = branch;
const cardMatch = branch.match(/^card\/([^/]+)\//);
if (cardMatch) {
current.cardId = cardMatch[1];
}
}
}
if (current.path) {
worktrees.push(current);
}
const cardWorktrees = worktrees.filter((w) => w.path.includes(".dev/worktrees"));
for (const wt of cardWorktrees) {
wt.changedFiles = await getWorktreeChangedFiles(wt.path, repoPath, baseBranch);
}
return cardWorktrees;
}
async function getWorktreeChangedFiles(worktreePath, repoPath, baseBranch = "main") {
assertSafeGitRef(baseBranch, "baseBranch");
try {
const { stdout } = await gitExec(["diff", "--name-only", `origin/${baseBranch}...HEAD`], worktreePath);
return stdout.trim().split("\n").filter(Boolean);
} catch {
try {
const { stdout } = await gitExec(["diff", "--name-only", "HEAD"], worktreePath);
return stdout.trim().split("\n").filter(Boolean);
} catch {
return [];
}
}
}
async function cleanupWorktree(repoPath, worktreePath, deleteBranch = false) {
let branchName;
if (deleteBranch) {
try {
const { stdout } = await gitExec(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
branchName = stdout.trim();
} catch {
}
}
try {
await gitExec(["worktree", "remove", worktreePath, "--force"], repoPath);
} catch (error) {
await gitExec(["worktree", "prune"], repoPath);
}
if (deleteBranch && branchName && branchName !== "main" && branchName !== "master") {
try {
assertSafeGitRef(branchName, "branchName");
await gitExec(["branch", "-D", branchName], repoPath);
} catch {
}
}
}
async function pushWorktreeBranch(worktreePath, title) {
try {
await gitExec(["add", "-A"], worktreePath);
await gitExec(["commit", "-m", title, "--allow-empty"], worktreePath);
} catch {
}
const { stdout: branchStdout } = await gitExec(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath);
const branchName = branchStdout.trim();
assertSafeGitRef(branchName, "branchName");
await gitExec(["push", "-u", "origin", branchName], worktreePath, 6e4);
return { branchName };
}
const ALWAYS_CONFLICT_FILES = [
"package.json",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"Cargo.lock",
"Gemfile.lock",
"go.sum",
"poetry.lock",
"composer.lock"
];
function detectConflicts(activeWorktrees, proposedFiles) {
const fileToWorktrees = /* @__PURE__ */ new Map();
for (const wt of activeWorktrees) {
for (const file of wt.changedFiles) {
const existing = fileToWorktrees.get(file) || [];
existing.push(wt.branch);
fileToWorktrees.set(file, existing);
}
}
const conflicts = [];
for (const [file, worktrees] of fileToWorktrees) {
if (worktrees.length > 1) {
const basename = file.split("/").pop() || file;
conflicts.push({
file,
worktrees,
isLockFile: ALWAYS_CONFLICT_FILES.includes(basename)
});
}
}
return {
hasConflicts: conflicts.length > 0,
hasLockFileConflicts: conflicts.some((c) => c.isLockFile),
conflicts
};
}
function formatConflictReport(report) {
if (!report.hasConflicts) {
return "No file conflicts detected between active worktrees.";
}
const lines = ["## File Conflict Warning\n"];
if (report.hasLockFileConflicts) {
lines.push("**CRITICAL: Lock file conflicts detected.** These cards should NOT run in parallel.\n");
}
lines.push("Conflicting files:");
for (const conflict of report.conflicts) {
const marker = conflict.isLockFile ? " \u26A0\uFE0F LOCK FILE" : "";
lines.push(`- \`${conflict.file}\`${marker} \u2192 modified by: ${conflict.worktrees.join(", ")}`);
}
return lines.join("\n");
}
const execFileAsync$5 = util.promisify(child_process.execFile);
const MAX_BUFFER = 32 * 1024 * 1024;
async function runGit(repoPath, args, timeout = 6e4) {
try {
const { stdout, stderr } = await execFileAsync$5("git", args, { cwd: repoPath, timeout, maxBuffer: MAX_BUFFER });
return { stdout: stdout.toString(), stderr: stderr.toString(), code: 0 };
} catch (err) {
return {
stdout: (err?.stdout ?? "").toString(),
stderr: (err?.stderr ?? err?.message ?? "").toString(),
code: typeof err?.code === "number" ? err.code : 1
};
}
}
function ensureOk(result, what) {
if (result.code !== 0) {
throw new Error(`${what} failed: ${(result.stderr || result.stdout).trim()}`);
}
return result;
}
const CONFLICT_CODES = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
async function gitStatus(repoPath) {
const res = ensureOk(await runGit(repoPath, ["status", "--porcelain=v1", "--branch", "--untracked-files=all"]), "git status");
const lines = res.stdout.split("\n").filter(Boolean);
let branch = "HEAD";
let upstream = null;
let ahead = 0;
let behind = 0;
const staged = [];
const unstaged = [];
const untracked = [];
const conflicts = [];
for (const line of lines) {
if (line.startsWith("##")) {
const info = line.slice(3);
const branchPart = info.split(" ")[0];
const [local, up] = branchPart.split("...");
branch = local.replace(/^No commits yet on /, "") || "HEAD";
upstream = up ? up.split(" ")[0] : null;
const aheadM = info.match(/ahead (\d+)/);
const behindM = info.match(/behind (\d+)/);
if (aheadM) ahead = parseInt(aheadM[1], 10);
if (behindM) behind = parseInt(behindM[1], 10);
continue;
}
const xy = line.slice(0, 2);
const path = line.slice(3);
if (xy === "??") {
untracked.push(path);
continue;
}
if (CONFLICT_CODES.has(xy)) {
conflicts.push(path);
continue;
}
const x = xy[0];
const y = xy[1];
if (x !== " " && x !== "?") staged.push(path);
if (y !== " " && y !== "?") unstaged.push(path);
}
const clean = staged.length === 0 && unstaged.length === 0 && untracked.length === 0 && conflicts.length === 0;
return { branch, upstream, ahead, behind, staged, unstaged, untracked, conflicts, clean };
}
async function gitStage(repoPath, paths) {
ensureOk(await runGit(repoPath, ["add", "--", ...paths]), "git add");
}
async function gitStageAll(repoPath) {
ensureOk(await runGit(repoPath, ["add", "-A"]), "git add -A");
}
async function gitUnstage(repoPath, paths) {
ensureOk(await runGit(repoPath, ["restore", "--staged", "--", ...paths]), "git unstage");
}
async function gitCommit(repoPath, message) {
ensureOk(await runGit(repoPath, ["commit", "-m", message]), "git commit");
const sha = ensureOk(await runGit(repoPath, ["rev-parse", "HEAD"]), "git rev-parse").stdout.trim();
return { sha };
}
async function gitFetch(repoPath) {
ensureOk(await runGit(repoPath, ["fetch", "--all", "--prune"], 12e4), "git fetch");
}
async function gitPush(repoPath, opts = {}) {
const branch = ensureOk(await runGit(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"]), "git branch").stdout.trim();
const args = ["push", "--set-upstream", "origin", branch];
if (opts.force) args.splice(1, 0, "--force-with-lease");
const res = await runGit(repoPath, args, 12e4);
return res;
}
async function gitPull(repoPath) {
return runGit(repoPath, ["pull", "--ff-only"], 12e4);
}
async function gitBranchList(repoPath) {
const res = ensureOk(await runGit(repoPath, ["branch", "--format=%(refname:short)"]), "git branch");
const branches = res.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
const current = ensureOk(await runGit(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"]), "git branch").stdout.trim();
return { branches, current };
}
async function gitCreateBranch(repoPath, name, checkout = true) {
ensureOk(await runGit(repoPath, checkout ? ["checkout", "-b", name] : ["branch", name]), "git create branch");
}
async function gitCheckout(repoPath, ref) {
ensureOk(await runGit(repoPath, ["checkout", ref]), "git checkout");
}
async function gitDeleteBranch(repoPath, name, force = false) {
ensureOk(await runGit(repoPath, ["branch", force ? "-D" : "-d", name]), "git delete branch");
}
async function asOpResult(repoPath, res, what) {
if (res.code === 0) {
return { ok: true, conflicted: false, conflicts: [], message: `${what} completed` };
}
const status = await gitStatus(repoPath);
if (status.conflicts.length > 0) {
return { ok: false, conflicted: true, conflicts: status.conflicts, message: `${what} hit conflicts` };
}
return { ok: false, conflicted: false, conflicts: [], message: (res.stderr || res.stdout).trim() || `${what} failed` };
}
async function gitMerge(repoPath, ref, noFf = false) {
const args = ["merge"];
if (noFf) args.push("--no-ff");
args.push(ref);
return asOpResult(repoPath, await runGit(repoPath, args), `merge ${ref}`);
}
async function gitRebase(repoPath, onto) {
return asOpResult(repoPath, await runGit(repoPath, ["rebase", onto]), `rebase onto ${onto}`);
}
async function gitCherryPick(repoPath, sha) {
return asOpResult(repoPath, await runGit(repoPath, ["cherry-pick", sha]), `cherry-pick ${sha}`);
}
async function gitRevert(repoPath, sha) {
return asOpResult(repoPath, await runGit(repoPath, ["revert", "--no-edit", sha]), `revert ${sha}`);
}
async function gitOpContinue(repoPath, kind) {
const cmd = kind === "cherry-pick" ? ["cherry-pick", "--continue"] : kind === "rebase" ? ["rebase", "--continue"] : kind === "revert" ? ["revert", "--continue"] : ["merge", "--continue"];
return asOpResult(repoPath, await runGitNoEditor(repoPath, cmd), `${kind} --continue`);
}
async function gitOpAbort(repoPath, kind) {
const cmd = kind === "cherry-pick" ? ["cherry-pick", "--abort"] : kind === "rebase" ? ["rebase", "--abort"] : kind === "revert" ? ["revert", "--abort"] : ["merge", "--abort"];
ensureOk(await runGit(repoPath, cmd), `${kind} --abort`);
}
async function runGitNoEditor(repoPath, args) {
try {
const { stdout, stderr } = await execFileAsync$5("git", args, { cwd: repoPath, timeout: 6e4, maxBuffer: MAX_BUFFER, env: { ...process.env, GIT_EDITOR: "true" } });
return { stdout: stdout.toString(), stderr: stderr.toString(), code: 0 };
} catch (err) {
return { stdout: (err?.stdout ?? "").toString(), stderr: (err?.stderr ?? err?.message ?? "").toString(), code: typeof err?.code === "number" ? err.code : 1 };
}
}
function looksBinary(buf) {
const n = Math.min(buf.length, 8192);
for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
return false;
}
async function gitListConflicts(repoPath) {
const status = await gitStatus(repoPath);
const out = [];
for (const path of status.conflicts) {
let buf = null;
try {
buf = await fs$2.readFile(path$1.join(repoPath, path));
} catch {
buf = null;
}
const binary = buf ? looksBinary(buf) : true;
out.push({ path, binary, merged: binary || !buf ? null : buf.toString("utf-8") });
}
return out;
}
async function gitResolveSide(repoPath, path, side) {
ensureOk(await runGit(repoPath, ["checkout", `--${side}`, "--", path]), `resolve --${side}`);
ensureOk(await runGit(repoPath, ["add", "--", path]), "git add");
}
async function gitResolveContent(repoPath, path, content) {
await fs$2.writeFile(path$1.join(repoPath, path), content, "utf-8");
ensureOk(await runGit(repoPath, ["add", "--", path]), "git add");
}
async function gitDiff(repoPath, opts = {}) {
const args = ["diff"];
if (opts.staged) args.push("--staged");
if (opts.path) args.push("--", opts.path);
return ensureOk(await runGit(repoPath, args), "git diff").stdout;
}
async function gitLog(repoPath, limit = 50) {
const fmt = "%H%x1f%an%x1f%aI%x1f%s";
const res = ensureOk(await runGit(repoPath, ["log", `-${limit}`, `--pretty=format:${fmt}`]), "git log");
return res.stdout.split("\n").filter(Boolean).map((line) => {
const [sha, author, date, subject] = line.split("");
return { sha, author, date, subject };
});
}
logger.debug?.("gitManager loaded");
const execAsync = util.promisify(child_process.exec);
const execFileAsync$4 = util.promisify(child_process.execFile);
function registerCommonHandlers(rpcHandlerManager, workingDirectory) {
rpcHandlerManager.registerHandler("bash", async (data) => {
logger.debug("Shell command request:", data.command);
if (data.cwd && data.cwd !== "/") {
const validation = validatePath(data.cwd, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
}
try {
const options = {
cwd: data.cwd === "/" ? void 0 : data.cwd,
timeout: data.timeout || 3e4
// Default 30 seconds timeout
};
logger.debug("Shell command executing...", { cwd: options.cwd, timeout: options.timeout });
const { stdout, stderr } = await execAsync(data.command, options);
logger.debug("Shell command executed, processing result...");
const result = {
success: true,
stdout: stdout ? stdout.toString() : "",
stderr: stderr ? stderr.toString() : "",
exitCode: 0
};
logger.debug("Shell command result:", {
success: true,
exitCode: 0,
stdoutLen: result.stdout.length,
stderrLen: result.stderr.length
});
return result;
} catch (error) {
const execError = error;
if (execError.code === "ETIMEDOUT" || execError.killed) {
const result2 = {
success: false,
stdout: execError.stdout || "",
stderr: execError.stderr || "",
exitCode: typeof execError.code === "number" ? execError.code : -1,
error: "Command timed out"
};
logger.debug("Shell command timed out:", {
success: false,
exitCode: result2.exitCode,
error: "Command timed out"
});
return result2;
}
const result = {
success: false,
stdout: execError.stdout ? execError.stdout.toString() : "",
stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
exitCode: typeof execError.code === "number" ? execError.code : 1,
error: execError.message || "Command failed"
};
logger.debug("Shell command failed:", {
success: false,
exitCode: result.exitCode,
error: result.error,
stdoutLen: result.stdout.length,
stderrLen: result.stderr.length
});
return result;
}
});
rpcHandlerManager.registerHandler("readFile", async (data) => {
logger.debug("Read file request:", data.path);
const validation = validatePath(data.path, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
try {
const buffer = await fs$2.readFile(data.path);
const content = buffer.toString("base64");
return { success: true, content };
} catch (error) {
logger.debug("Failed to read file:", error);
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
}
});
rpcHandlerManager.registerHandler("writeFile", async (data) => {
logger.debug("Write file request:", data.path);
const validation = validatePath(data.path, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
try {
if (data.expectedHash !== null && data.expectedHash !== void 0) {
try {
const existingBuffer = await fs$2.readFile(data.path);
const existingHash = crypto.createHash("sha256").update(existingBuffer).digest("hex");
if (existingHash !== data.expectedHash) {
return {
success: false,
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
};
}
} catch (error) {
const nodeError = error;
if (nodeError.code !== "ENOENT") {
throw error;
}
return {
success: false,
error: "File does not exist but hash was provided"
};
}
} else {
try {
await fs$2.stat(data.path);
return {
success: false,
error: "File already exists but was expected to be new"
};
} catch (error) {
const nodeError = error;
if (nodeError.code !== "ENOENT") {
throw error;
}
}
}
const buffer = Buffer.from(data.content, "base64");
await fs$2.writeFile(data.path, buffer);
const hash = crypto.createHash("sha256").update(buffer).digest("hex");
return { success: true, hash };
} catch (error) {
logger.debug("Failed to write file:", error);
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
}
});
rpcHandlerManager.registerHandler("listDirectory", async (data) => {
logger.debug("List directory request:", data.path);
const validation = validatePath(data.path, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
try {
const entries = await fs$2.readdir(data.path, { withFileTypes: true });
const directoryEntries = await Promise.all(
entries.map(async (entry) => {
const fullPath = path$1.join(data.path, entry.name);
let type = "other";
let size;
let modified;
if (entry.isDirectory()) {
type = "directory";
} else if (entry.isFile()) {
type = "file";
}
try {
const stats = await fs$2.stat(fullPath);
size = stats.size;
modified = stats.mtime.getTime();
} catch (error) {
logger.debug(`Failed to stat ${fullPath}:`, error);
}
return {
name: entry.name,
type,
size,
modified
};
})
);
directoryEntries.sort((a, b) => {
if (a.type === "directory" && b.type !== "directory") return -1;
if (a.type !== "directory" && b.type === "directory") return 1;
return a.name.localeCompare(b.name);
});
return { success: true, entries: directoryEntries };
} catch (error) {
logger.debug("Failed to list directory:", error);
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
}
});
rpcHandlerManager.registerHandler("getDirectoryTree", async (data) => {
logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
const validation = validatePath(data.path, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
async function buildTree(path, name, currentDepth) {
try {
const stats = await fs$2.stat(path);
const node = {
name,
path,
type: stats.isDirectory() ? "directory" : "file",
size: stats.size,
modified: stats.mtime.getTime()
};
if (stats.isDirectory() && currentDepth < data.maxDepth) {
const entries = await fs$2.readdir(path, { withFileTypes: true });
const children = [];
await Promise.all(
entries.map(async (entry) => {
if (entry.isSymbolicLink()) {
logger.debug(`Skipping symlink: ${path$1.join(path, entry.name)}`);
return;
}
const childPath = path$1.join(path, entry.name);
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
if (childNode) {
children.push(childNode);
}
})
);
children.sort((a, b) => {
if (a.type === "directory" && b.type !== "directory") return -1;
if (a.type !== "directory" && b.type === "directory") return 1;
return a.name.localeCompare(b.name);
});
node.children = children;
}
return node;
} catch (error) {
logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
return null;
}
}
try {
if (data.maxDepth < 0) {
return { success: false, error: "maxDepth must be non-negative" };
}
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
const tree = await buildTree(data.path, baseName, 0);
if (!tree) {
return { success: false, error: "Failed to access the specified path" };
}
return { success: true, tree };
} catch (error) {
logger.debug("Failed to get directory tree:", error);
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
}
});
rpcHandlerManager.registerHandler("ripgrep", async (data) => {
logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
if (data.cwd) {
const validation = validatePath(data.cwd, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
}
try {
const result = await run$1(data.args, { cwd: data.cwd });
return {
success: true,
exitCode: result.exitCode,
stdout: result.stdout.toString(),
stderr: result.stderr.toString()
};
} catch (error) {
logger.debug("Failed to run ripgrep:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Failed to run ripgrep"
};
}
});
rpcHandlerManager.registerHandler("difftastic", async (data) => {
logger.debug("Difftastic request with args:", data.args, "cwd:", data.cwd);
if (data.cwd) {
const validation = validatePath(data.cwd, workingDirectory);
if (!validation.valid) {
return { success: false, error: validation.error };
}
}
try {
const result = await run(data.args, { cwd: data.cwd });
return {
success: true,
exitCode: result.exitCode,
stdout: result.stdout.toString(),
stderr: result.stderr.toString()
};
} catch (error) {
logger.debug("Failed to run difftastic:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Failed to run difftastic"
};
}
});
rpcHandlerManager.registerHandler("create-worktree", async (data) => {
logger.debug("Create worktree request:", data);
try {
const result = await createCardWorktree(
data.repoPath,
data.cardId,
data.cardSlug,
data.baseBranch
);
return {
success: true,
worktreePath: result.worktreePath,
branchName: result.branchName
};
} catch (error) {
logger.debug("Failed to create worktree:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Failed to create worktree"
};
}
});
rpcHandlerManager.registerHandler("list-worktrees", async (data) => {
logger.debug("List worktrees request:", data);
try {
const worktrees = await listWorktrees(data.repoPath, data.baseBranch);
const report = detectConflicts(worktrees);
return {
success: true,
worktrees: worktrees.map((w) => ({
path: w.path,
branch: w.branch,
head: w.head,
cardId: w.cardId,
changedFiles: w.changedFiles
})),
conflictReport: formatConflictReport(report)
};
} catch (error) {
logger.debug("Failed to list worktrees:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Failed to list worktrees"
};
}
});
rpcHandlerManager.registerHandler("cleanup-worktree", async (data) => {
logger.debug("Cleanup worktree request:", data);
try {
await cleanupWorktree(data.repoPath, data.worktreePath, data.deleteBranch);
return { success: true };
} catch (error) {
logger.debug("Failed to cleanup worktree:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Failed to cleanup worktree"
};
}
});
rpcHandlerManager.registerHandler("clone-project-repo", async (data) => {
logger.debug(`Clone project repo request for project ${data.projectId}`);
try {
if (!/^[A-Za-z0-9_-]{1,128}$/.test(data.projectId)) {
return { success: false, error: "Invalid project id" };
}
if (!/^https:\/\//i.test(data.cloneUrl)) {
return { success: false, error: "Only https clone URLs are supported" };
}
const projectsRoot = process.env.CONSORTIUM_PROJECTS_DIR || path$1.join(os$1.homedir(), "consortium", "projects");
const target = path$1.join(projectsRoot, data.projectId, "repo");
if (fs$1.existsSync(path$1.join(target, ".git"))) {
return { success: true, path: target, alreadyPresent: true };
}
await fs$2.mkdir(path$1.join(projectsRoot, data.projectId), { recursive: true });
const args = ["clone"];
if (data.ref) args.push("-b", data.ref);
args.push(data.cloneUrl, target);
await execFileAsync$4("git", args, { cwd: projectsRoot, timeout: 3e5, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
if (data.scrubUrl && data.scrubUrl !== data.cloneUrl) {
await execFileAsync$4("git", ["remote", "set-url", "origin", data.scrubUrl], { cwd: target, timeout: 3e4 });
}
return { success: true, path: target, alreadyPresent: false };
} catch (error) {
const raw = error instanceof Error ? error.message : String(error);
const safe = raw.replace(/https:\/\/[^@\s]*@/gi, "https://");
logger.debug(`Failed to clone project repo: ${safe}`);
return { success: false, error: safe };
}
});
rpcHandlerManager.registerHandler("push-worktree-branch", async (data) => {
logger.debug("Push worktree branch request:", data);
try {
const result = await pushWorktreeBranch(data.worktreePath, data.title);
return { success: true, branchName: result.branchName };
} catch (error) {
logger.debug("Failed to push worktree branch:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Failed to push branch"
};
}
});
const wrapGit = (name, fn) => fn().then((r) => ({ success: true, ...r })).catch((error) => {
logger.debug(`[git] ${name} failed:`, error);
return { success: false, error: error instanceof Error ? error.message : `git ${name} failed` };
});
rpcHandlerManager.registerHandler("git-status", (d) => wrapGit("status", async () => ({ status: await gitStatus(d.repoPath) })));
rpcHandlerManager.registerHandler("git-stage", (d) => wrapGit("stage", async () => {
d.paths && d.paths.length ? await gitStage(d.repoPath, d.paths) : await gitStageAll(d.repoPath);
return {};
}));
rpcHandlerManager.registerHandler("git-unstage", (d) => wrapGit("unstage", async () => {
await gitUnstage(d.repoPath, d.paths);
return {};
}));
rpcHandlerManager.registerHandler("git-commit", (d) => wrapGit("commit", () => gitCommit(d.repoPath, d.message)));
rpcHandlerManager.registerHandler("git-fetch", (d) => wrapGit("fetch", async () => {
await gitFetch(d.repoPath);
return {};
}));
rpcHandlerManager.registerHandler("git-push", (d) => wrapGit("push", async () => {
const r = await gitPush(d.repoPath, { force: d.force });
return { pushed: r.code === 0, output: (r.stderr || r.stdout).trim() };
}));
rpcHandlerManager.registerHandler("git-pull", (d) => wrapGit("pull", async () => {
const r = await gitPull(d.repoPath);
return { ok: r.code === 0, output: (r.stderr || r.stdout).trim() };
}));
rpcHandlerManager.registerHandler("git-branch-list", (d) => wrapGit("branch-list", () => gitBranchList(d.repoPath)));
rpcHandlerManager.registerHandler("git-create-branch", (d) => wrapGit("create-branch", async () => {
await gitCreateBranch(d.repoPath, d.name, d.checkout !== false);
return {};
}));
rpcHandlerManager.registerHandler("git-checkout", (d) => wrapGit("checkout", async () => {
await gitCheckout(d.repoPath, d.ref);
return {};
}));
rpcHandlerManager.registerHandler("git-delete-branch", (d) => wrapGit("delete-branch", async () => {
await gitDeleteBranch(d.repoPath, d.name, !!d.force);
return {};
}));
rpcHandlerManager.registerHandler("git-merge", (d) => wrapGit("merge", async () => ({ result: await gitMerge(d.repoPath, d.ref, !!d.noFf) })));
rpcHandlerManager.registerHandler("git-rebase", (d) => wrapGit("rebase", async () => ({ result: await gitRebase(d.repoPath, d.onto) })));
rpcHandlerManager.registerHandler("git-cherry-pick", (d) => wrapGit("cherry-pick", async () => ({ result: await gitCherryPick(d.repoPath, d.sha) })));
rpcHandlerManager.registerHandler("git-revert", (d) => wrapGit("revert", async () => ({ result: await gitRevert(d.repoPath, d.sha) })));
rpcHandlerManager.registerHandler("git-op-continue", (d) => wrapGit("op-continue", async () => ({ result: await gitOpContinue(d.repoPath, d.kind) })));
rpcHandlerManager.registerHandler("git-op-abort", (d) => wrapGit("op-abort", async () => {
await gitOpAbort(d.repoPath, d.kind);
return {};
}));
rpcHandlerManager.registerHandler("git-list-conflicts", (d) => wrapGit("list-conflicts", async () => ({ conflicts: await gitListConflicts(d.repoPath) })));
rpcHandlerManager.registerHandler("git-resolve-side", (d) => wrapGit("resolve-side", async () => {
await gitResolveSide(d.repoPath, d.path, d.side);
return {};
}));
rpcHandlerManager.registerHandler("git-resolve-content", (d) => wrapGit("resolve-content", async () => {
await gitResolveContent(d.repoPath, d.path, d.content);
return {};
}));
rpcHandlerManager.registerHandler("git-diff", (d) => wrapGit("diff", async () => ({ diff: await gitDiff(d.repoPath, { staged: d.staged, path: d.path }) })));
rpcHandlerManager.registerHandler("git-log", (d) => wrapGit("log", async () => ({ log: await gitLog(d.repoPath, d.limit) })));
}
const PRICING = {
// --- Claude 4 & Future Models ---
"claude-4.5-opus": {
input: 5,
output: 25,
cache_write: 6.25,
cache_read: 0.5
},
"claude-4.1-opus": {
input: 15,
output: 75,
cache_write: 18.75,
cache_read: 1.5
},
"claude-4-opus": {
input: 15,
output: 75,
cache_write: 18.75,
cache_read: 1.5
},
"claude-4.5-sonnet": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
"claude-4-sonnet": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
"claude-4.5-haiku": {
input: 1,
output: 5,
cache_write: 1.25,
cache_read: 0.1
},
// --- Legacy / Claude 3 ---
"claude-3-opus-20240229": {
input: 15,
output: 75,
cache_write: 18.75,
cache_read: 1.5
},
"claude-3-sonnet-20240229": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
"claude-3-5-sonnet-20240620": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
// New Sonnet 3.5 updated model
"claude-3-5-sonnet-20241022": {
input: 3,
output: 15,
cache_write: 3.75,
cache_read: 0.3
},
"claude-3-haiku-20240307": {
input: 0.25,
output: 1.25,
cache_write: 0.3125,
cache_read: 0.025
},
"claude-3-5-haiku-20241022": {
input: 0.8,
output: 4,
cache_write: 1,
// Approx based on 1.25x rule usually or custom
cache_read: 0.08
}
};
const DEFAULT_MODEL = "claude-3-5-sonnet-20241022";
function calculateCost(usage, modelId) {
let pricing = PRICING[modelId];
if (!pricing) {
if (modelId?.includes("opus")) {
if (modelId.includes("4.5")) pricing = PRICING["claude-4.5-opus"];
else if (modelId.includes("4.1")) pricing = PRICING["claude-4.1-opus"];
else if (modelId.includes("4")) pricing = PRICING["claude-4-opus"];
else pricing = PRICING["claude-3-opus-20240229"];
} else if (modelId?.includes("sonnet")) {
if (modelId.includes("4.5")) pricing = PRICING["claude-4.5-sonnet"];
else if (modelId.includes("4")) pricing = PRICING["claude-4-sonnet"];
else pricing = PRICING["claude-3-5-sonnet-20241022"];
} else if (modelId?.includes("haiku")) {
if (modelId.includes("4.5")) pricing = PRICING["claude-4.5-haiku"];
else if (modelId.includes("3.5")) pricing = PRICING["claude-3-5-haiku-20241022"];
else pricing = PRICING["claude-3-haiku-20240307"];
} else pricing = PRICING[DEFAULT_MODEL];
}
const inputCost = usage.input_tokens / 1e6 * pricing.input;
const outputCost = usage.output_tokens / 1e6 * pricing.output;
const cacheWriteCost = (usage.cache_creation_input_tokens || 0) / 1e6 * pricing.cache_write;
const cacheReadCost = (usage.cache_read_input_tokens || 0) / 1e6 * pricing.cache_read;
const totalInputCost = inputCost + cacheWriteCost + cacheReadCost;
return {
total: totalInputCost + outputCost,
input: totalInputCost,
output: outputCost
};
}
const DEFAULT_TIMEOUT_MS$1 = 3e4;
const MAX_TIMEOUT_MS = 5 * 6e4;
const MAX_BUFFER_BYTES = 1024 * 1024;
const SIGKILL_GRACE_MS = 2e3;
function clampTimeout(requested) {
if (typeof requested !== "number" || !Number.isFinite(requested) || requested <= 0) {
return DEFAULT_TIMEOUT_MS$1;
}
return Math.min(Math.floor(requested), MAX_TIMEOUT_MS);
}
function executeCommand(options) {
const { command, args = [], cwd, timeoutMs } = options;
const resolvedTimeoutMs = clampTimeout(timeoutMs);
const startedAt = Date.now();
return new Promise((resolve) => {
let stdout = "";
let stderr = "";
let stdoutTruncated = false;
let stderrTruncated = false;
let timedOut = false;
let settled = false;
let killTimer = null;
const finish = (exitCode) => {
if (settled) return;
settled = true;
if (killTimer) {
clearTimeout(killTimer);
killTimer = null;
}
clearTimeout(timeoutHandle);
const truncated = stdoutTruncated || stderrTruncated;
const result = {
stdout,
stderr,
exitCode,
durationMs: Date.now() - startedAt
};
if (truncated) result.truncated = true;
if (timedOut) result.timedOut = true;
resolve(result);
};
let child;
try {
child = node_child_process.execFile(command, args, {
cwd,
shell: false,
windowsHide: true,
// We manage our own truncation; set high buffer so execFile doesn't kill us.
maxBuffer: MAX_BUFFER_BYTES * 4
});
} catch (err) {
stderr = err instanceof Error ? err.message : String(err);
finish(null);
return;
}
const timeoutHandle = setTimeout(() => {
timedOut = true;
if (!child.killed) {
try {
child.kill("SIGTERM");
} catch {
}
}
killTimer = setTimeout(() => {
if (!child.killed && child.exitCode === null) {
try {
child.kill("SIGKILL");
} catch {
}
}
}, SIGKILL_GRACE_MS);
}, resolvedTimeoutMs);
child.stdout?.on("data", (chunk) => {
if (stdoutTruncated) return;
const piece = typeof chunk === "string" ? chunk : chunk.toString("utf8");
const remaining = MAX_BUFFER_BYTES - Buffer.byteLength(stdout, "utf8");
if (Buffer.byteLength(piece, "utf8") <= remaining) {
stdout += piece;
} else {
stdout += Buffer.from(piece, "utf8").slice(0, Math.max(0, remaining)).toString("utf8");
stdoutTruncated = true;
}
});
child.stderr?.on("data", (chunk) => {
if (stderrTruncated) return;
const piece = typeof chunk === "string" ? chunk : chunk.toString("utf8");
const remaining = MAX_BUFFER_BYTES - Buffer.byteLength(stderr, "utf8");
if (Buffer.byteLength(piece, "utf8") <= remaining) {
stderr += piece;
} else {
stderr += Buffer.from(piece, "utf8").slice(0, Math.max(0, remaining)).toString("utf8");
stderrTruncated = true;
}
});
child.on("error", (err) => {
if (!stderr) stderr = err instanceof Error ? err.message : String(err);
finish(null);
});
child.on("close", (code, _signal) => {
finish(code ?? null);
});
});
}
function isUsableForFailover(entry, now) {
if (entry.status === "invalid") return false;
if (entry.status === "cooldown" && typeof entry.cooldownUntil === "number" && entry.cooldownUntil > now) return false;
return true;
}
function daemonAccountCandidates(index, providerId, pinnedAccountKeyId, now) {
const out = [];
if (pinnedAccountKeyId) {
const entry = index.accounts[pinnedAccountKeyId];
if (entry && entry.providerId === providerId) {
out.push({ accountKeyId: pinnedAccountKeyId, reason: "pin", allowInvalid: true });
}
}
const policy = index.failover[providerId];
if (policy?.enabled) {
for (const id of policy.order) {
const entry = index.accounts[id];
if (!entry || entry.providerId !== providerId) continue;
if (!isUsableForFailover(entry, now)) continue;
out.push({ accountKeyId: id, reason: "failover", allowInvalid: false });
}
}
const defaultId = index.providerDefaults[providerId];
if (defaultId) {
const entry = index.accounts[defaultId];
if (entry && entry.providerId === providerId && entry.status !== "invalid") {
out.push({ accountKeyId: defaultId, reason: "default", allowInvalid: false });
}
}
const newest = Object.entries(index.accounts).filter(([, e]) => e.providerId === providerId && e.status !== "invalid").sort(([, a], [, b]) => b.updatedAt - a.updatedAt)[0];
if (newest) {
out.push({ accountKeyId: newest[0], reason: "most-recent", allowInvalid: false });
}
return out;
}
const SCRYPT_N = 1 << 15;
const SCRYPT_r = 8;
const SCRYPT_p = 1;
const SCRYPT_DKLEN = 32;
const SCRYPT_MAXMEM = 256 * 1024 * 1024;
const SALT_LEN = 16;
const IV_LEN = 12;
const LEGACY_ID_PREFIX = "legacy:";
const ACCOUNT_BUNDLE_VERSION = 2;
function getKeystorePath() {
return path.join(configuration.consortiumHomeDir, "byok.json");
}
function readAccessKeyMaterial() {
try {
if (fs.existsSync(configuration.privateKeyFile)) {
return fs.readFileSync(configuration.privateKeyFile);
}
} catch (err) {
logger.debug("[byokKeystore] could not read access.key:", err);
}
throw new Error(
`BYOK keystore unavailable: no access key. Authenticate the daemon so access.key exists at ${configuration.privateKeyFile} before storing or reading BYOK keys.`
);
}
function deriveAesKey(salt) {
const material = readAccessKeyMaterial();
return node_crypto.scryptSync(material, salt, SCRYPT_DKLEN, {
N: SCRYPT_N,
r: SCRYPT_r,
p: SCRYPT_p,
maxmem: SCRYPT_MAXMEM
});
}
function deriveLegacyAesKey() {
const raw = readAccessKeyMaterial();
return node_crypto.createHash("sha256").update(raw).update("consortium-byok-v1").digest();
}
function sealWithMaterial(plaintext, material) {
const salt = node_crypto.randomBytes(SALT_LEN);
const key = node_crypto.scryptSync(material, salt, SCRYPT_DKLEN, {
N: SCRYPT_N,
r: SCRYPT_r,
p: SCRYPT_p,
maxmem: SCRYPT_MAXMEM
});
const iv = node_crypto.randomBytes(IV_LEN);
const cipher = node_crypto.createCipheriv("aes-256-gcm", key, iv);
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
return {
salt: salt.toString("base64"),
iv: iv.toString("base64"),
ciphertext: ct.toString("base64"),
authTag: cipher.getAuthTag().toString("base64")
};
}
function openWithMaterial(blob, material) {
try {
const key = node_crypto.scryptSync(material, Buffer.from(blob.salt, "base64"), SCRYPT_DKLEN, {
N: SCRYPT_N,
r: SCRYPT_r,
p: SCRYPT_p,
maxmem: SCRYPT_MAXMEM
});
const decipher = node_crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(blob.iv, "base64"));
decipher.setAuthTag(Buffer.from(blob.authTag, "base64"));
const pt = Buffer.concat([
decipher.update(Buffer.from(blob.ciphertext, "base64")),
decipher.final()
]);
return pt.toString("utf8");
} catch (err) {
logger.debug("[byokKeystore] openWithMaterial failed:", err);
return null;
}
}
function encryptRecord(plaintext, key) {
const iv = node_crypto.randomBytes(IV_LEN);
const cipher = node_crypto.createCipheriv("aes-256-gcm", key, iv);
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return {
iv: iv.toString("base64"),
ciphertext: ct.toString("base64"),
authTag: tag.toString("base64"),
createdAt: Date.now()
};
}
function decryptRecord(rec, key) {
try {
const decipher = node_crypto.createDecipheriv(
"aes-256-gcm",
key,
Buffer.from(rec.iv, "base64")
);
decipher.setAuthTag(Buffer.from(rec.authTag, "base64"));
const pt = Buffer.concat([
decipher.update(Buffer.from(rec.ciphertext, "base64")),
decipher.final()
]);
return pt.toString("utf8");
} catch (err) {
logger.debug("[byokKeystore] decrypt failed (likely KDF mismatch after re-auth):", err);
return null;
}
}
function decryptLegacy(entry, key) {
try {
const decipher = node_crypto.createDecipheriv(
"aes-256-gcm",
key,
Buffer.from(entry.iv, "base64")
);
decipher.setAuthTag(Buffer.from(entry.tag, "base64"));
const pt = Buffer.concat([
decipher.update(Buffer.from(entry.ct, "base64")),
decipher.final()
]);
return pt.toString("utf8");
} catch (err) {
logger.debug("[byokKeystore] legacy decrypt failed:", err);
return null;
}
}
function writeFileAtomic(data) {
const path$1 = getKeystorePath();
try {
fs.mkdirSync(path.dirname(path$1), { recursive: true });
} catch {
}
const tmp = `${path$1}.tmp.${process.pid}.${Date.now()}`;
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { encoding: "utf8", mode: 384 });
try {
fs.chmodSync(tmp, 384);
} catch {
}
fs.renameSync(tmp, path$1);
try {
fs.chmodSync(path$1, 384);
} catch {
}
}
function emptyV3() {
return {
version: 3,
salt: node_crypto.randomBytes(SALT_LEN).toString("base64"),
records: {},
index: { accounts: {}, providerDefaults: {}, failover: {} }
};
}
function legacyAccountId(providerId) {
return `${LEGACY_ID_PREFIX}${providerId}`;
}
function parseBundle(plaintext) {
try {
const parsed = JSON.parse(plaintext);
if (parsed && typeof parsed === "object" && (parsed.kind === "oauth" || parsed.kind === "api-key")) {
return {
...parsed,
bundleVersion: typeof parsed.bundleVersion === "number" ? parsed.bundleVersion : 1
};
}
} catch {
}
return null;
}
function serializeBundle(bundle) {
return JSON.stringify({ ...bundle, bundleVersion: ACCOUNT_BUNDLE_VERSION });
}
function wrapRawKey(file, aesKey, providerId, rawKey, createdAt) {
const id = legacyAccountId(providerId);
const bundle = { kind: "api-key", apiKey: rawKey, raw: rawKey };
file.records[id] = { ...encryptRecord(serializeBundle(bundle), aesKey), createdAt };
file.index.accounts[id] = {
providerId,
label: "Imported key",
kind: "api-key",
updatedAt: createdAt
};
if (!file.index.providerDefaults[providerId]) {
file.index.providerDefaults[providerId] = id;
}
}
function readFileMigrating() {
const path = getKeystorePath();
if (!fs.existsSync(path)) return emptyV3();
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(path, "utf8"));
} catch (err) {
logger.debug("[byokKeystore] failed to parse byok.json; treating as empty:", err);
return emptyV3();
}
if (parsed && typeof parsed === "object" && parsed.version === 3 && typeof parsed.salt === "string" && parsed.records && parsed.index) {
return parsed;
}
if (parsed && typeof parsed === "object" && parsed.version === 2 && typeof parsed.salt === "string" && parsed.records) {
const v2 = parsed;
logger.debug("[byokKeystore] migrating v2 byok.json \u2192 v3 (multi-account)");
const oldKey = deriveAesKey(Buffer.from(v2.salt, "base64"));
const fresh = emptyV3();
const newKey = deriveAesKey(Buffer.from(fresh.salt, "base64"));
let migrated = 0;
let dropped = 0;
for (const [providerId, rec] of Object.entries(v2.records)) {
const pt = decryptRecord(rec, oldKey);
if (pt === null) {
dropped++;
continue;
}
wrapRawKey(fresh, newKey, providerId, pt, rec.createdAt ?? Date.now());
migrated++;
}
try {
writeFileAtomic(fresh);
} catch (err) {
logger.debug("[byokKeystore] failed to persist migrated v3 file:", err);
}
logger.debug(`[byokKeystore] v2\u2192v3 migration complete: migrated=${migrated} dropped=${dropped}`);
return fresh;
}
if (parsed && typeof parsed === "object" && parsed.entries) {
const v1 = parsed;
logger.debug("[byokKeystore] migrating v1 byok.json \u2192 v3 (scrypt KDF + multi-account)");
const legacyKey = deriveLegacyAesKey();
const fresh = emptyV3();
const newKey = deriveAesKey(Buffer.from(fresh.salt, "base64"));
let migrated = 0;
let dropped = 0;
for (const [providerId, entry] of Object.entries(v1.entries ?? {})) {
const pt = decryptLegacy(entry, legacyKey);
if (pt === null) {
dropped++;
continue;
}
wrapRawKey(fresh, newKey, providerId, pt, Date.now());
migrated++;
}
try {
writeFileAtomic(fresh);
} catch (err) {
logger.debug("[byokKeystore] failed to persist migrated v3 file:", err);
}
logger.debug(`[byokKeystore] v1\u2192v3 migration complete: migrated=${migrated} dropped=${dropped}`);
return fresh;
}
logger.debug("[byokKeystore] unknown byok.json shape; treating as empty");
return emptyV3();
}
function runStartupMigration() {
try {
readFileMigrating();
} catch (err) {
logger.debug("[byokKeystore] runStartupMigration error (non-fatal):", err);
}
}
function registerAccount(input) {
const file = readFileMigrating();
const aesKey = deriveAesKey(Buffer.from(file.salt, "base64"));
file.records[input.accountKeyId] = encryptRecord(serializeBundle(input.bundle), aesKey);
file.index.accounts[input.accountKeyId] = {
providerId: input.providerId,
vendor: input.vendor,
label: input.label ?? "",
kind: input.kind,
envVar: input.envVar,
status: "ok",
expiresAt: input.bundle.expiresAt,
updatedAt: Date.now()
};
if (input.isDefault || !file.index.providerDefaults[input.providerId]) {
file.index.providerDefaults[input.providerId] = input.accountKeyId;
}
writeFileAtomic(file);
}
function maskedTailFromBundle(bundle) {
if (!bundle) return "";
if (bundle.kind === "api-key") {
return bundle.apiKey ? bundle.apiKey.slice(-4) : "";
}
if (bundle.raw) {
try {
const parsed = JSON.parse(bundle.raw);
const email = parsed?.account?.email_address ?? parsed?.account?.email ?? parsed?.email;
if (typeof email === "string" && email.length > 0) return email;
} catch {
}
}
return bundle.accessToken ? bundle.accessToken.slice(-4) : "";
}
function getAccountBundle(accountKeyId) {
const file = readFileMigrating();
const rec = file.records[accountKeyId];
if (!rec) return null;
const aesKey = deriveAesKey(Buffer.from(file.salt, "base64"));
const pt = decryptRecord(rec, aesKey);
return pt === null ? null : parseBundle(pt);
}
function updateAccountBundle(accountKeyId, bundle) {
const file = readFileMigrating();
if (!file.records[accountKeyId] || !file.index.accounts[accountKeyId]) return false;
const aesKey = deriveAesKey(Buffer.from(file.salt, "base64"));
file.records[accountKeyId] = encryptRecord(serializeBundle(bundle), aesKey);
const entry = file.index.accounts[accountKeyId];
file.index.accounts[accountKeyId] = {
...entry,
expiresAt: bundle.expiresAt,
status: "ok",
updatedAt: Date.now()
};
writeFileAtomic(file);
return true;
}
function removeAccount(accountKeyId) {
const file = readFileMigrating();
if (!file.records[accountKeyId] && !file.index.accounts[accountKeyId]) return;
const providerId = file.index.accounts[accountKeyId]?.providerId;
delete file.records[accountKeyId];
delete file.index.accounts[accountKeyId];
if (providerId && file.index.providerDefaults[providerId] === accountKeyId) {
const remaining = Object.entries(file.index.accounts).filter(([, e]) => e.providerId === providerId).sort(([, a], [, b]) => b.updatedAt - a.updatedAt);
if (remaining[0]) file.index.providerDefaults[providerId] = remaining[0][0];
else delete file.index.providerDefaults[providerId];
}
for (const [pid, policy] of Object.entries(file.index.failover)) {
file.index.failover[pid] = { ...policy, order: policy.order.filter((id) => id !== accountKeyId) };
}
if (Object.keys(file.records).length === 0 && Object.keys(file.index.accounts).length === 0) {
try {
fs.unlinkSync(getKeystorePath());
return;
} catch {
}
}
writeFileAtomic(file);
}
function listAccounts() {
const file = readFileMigrating();
return Object.entries(file.index.accounts).map(([accountKeyId, entry]) => ({ accountKeyId, ...entry })).sort((a, b) => b.updatedAt - a.updatedAt);
}
function setProviderDefault(providerId, accountKeyId) {
const file = readFileMigrating();
if (accountKeyId === null) {
delete file.index.providerDefaults[providerId];
writeFileAtomic(file);
return true;
}
const entry = file.index.accounts[accountKeyId];
if (!entry || entry.providerId !== providerId) return false;
file.index.providerDefaults[providerId] = accountKeyId;
writeFileAtomic(file);
return true;
}
function setProviderFailover(providerId, policy) {
const file = readFileMigrating();
file.index.failover[providerId] = {
enabled: policy.enabled,
order: policy.order.filter((id) => !!file.index.accounts[id])
};
writeFileAtomic(file);
}
function markAccountStatus(accountKeyId, status, cooldownUntil) {
const file = readFileMigrating();
const entry = file.index.accounts[accountKeyId];
if (!entry) return;
file.index.accounts[accountKeyId] = {
...entry,
status,
cooldownUntil: status === "cooldown" ? cooldownUntil : void 0
};
writeFileAtomic(file);
}
function resolveAccountForProvider(providerId, pinnedAccountKeyId) {
const file = readFileMigrating();
const candidates = daemonAccountCandidates(
file.index,
providerId,
pinnedAccountKeyId,
Date.now()
);
let aesKey = null;
for (const candidate of candidates) {
const rec = file.records[candidate.accountKeyId];
if (!rec) continue;
aesKey ??= deriveAesKey(Buffer.from(file.salt, "base64"));
const pt = decryptRecord(rec, aesKey);
const bundle = pt === null ? null : parseBundle(pt);
if (!bundle) continue;
return {
accountKeyId: candidate.accountKeyId,
entry: file.index.accounts[candidate.accountKeyId],
bundle,
reason: candidate.reason
};
}
return null;
}
function registerProviderKey(providerId, key) {
const file = readFileMigrating();
const aesKey = deriveAesKey(Buffer.from(file.salt, "base64"));
const id = legacyAccountId(providerId);
const bundle = { kind: "api-key", apiKey: key, raw: key };
file.records[id] = encryptRecord(serializeBundle(bundle), aesKey);
file.index.accounts[id] = {
providerId,
label: "Imported key",
kind: "api-key",
updatedAt: Date.now()
};
file.index.providerDefaults[providerId] = id;
writeFileAtomic(file);
return { byokKeyRef: `byok:${providerId}` };
}
function getProviderKey(providerId) {
const resolved = resolveAccountForProvider(providerId);
if (!resolved) return null;
return resolved.bundle.apiKey ?? resolved.bundle.accessToken ?? resolved.bundle.raw ?? null;
}
function removeProviderKey(providerId) {
removeAccount(legacyAccountId(providerId));
}
const KEYCHAIN_SERVICE$1 = "consortium-cli";
const EXEC_TIMEOUT_MS = 5e3;
const KEYCHAIN_KEK_ENV = "CONSORTIUM_KEK_KEYCHAIN";
function isKeychainKekEnabled() {
const v = (process.env[KEYCHAIN_KEK_ENV] ?? "").trim().toLowerCase();
return v === "1" || v === "true";
}
function currentAccount() {
try {
return os.userInfo().username || "consortium";
} catch {
return "consortium";
}
}
function decodeKek(raw) {
const trimmed = raw.trim();
if (!trimmed) return null;
const buf = Buffer.from(trimmed, "base64");
return buf.length >= 32 ? buf : null;
}
function readMac(account) {
try {
const out = node_child_process.execFileSync(
"security",
["find-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE$1, "-w"],
{ encoding: "utf8", timeout: EXEC_TIMEOUT_MS, stdio: ["ignore", "pipe", "ignore"] }
);
return decodeKek(out);
} catch {
return null;
}
}
function storeMac(account, b64) {
try {
node_child_process.execFileSync(
"security",
["add-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE$1, "-U", "-w", b64],
{ timeout: EXEC_TIMEOUT_MS, stdio: "ignore" }
);
return true;
} catch {
return false;
}
}
function deleteMac(account) {
try {
node_child_process.execFileSync(
"security",
["delete-generic-password", "-a", account, "-s", KEYCHAIN_SERVICE$1],
{ timeout: EXEC_TIMEOUT_MS, stdio: "ignore" }
);
} catch {
}
}
function readLinux(account) {
try {
const out = node_child_process.execFileSync(
"secret-tool",
["lookup", "service", KEYCHAIN_SERVICE$1, "account", account],
{ encoding: "utf8", timeout: EXEC_TIMEOUT_MS, stdio: ["ignore", "pipe", "ignore"] }
);
return decodeKek(out);
} catch {
return null;
}
}
function storeLinux(account, b64) {
try {
node_child_process.execFileSync(
"secret-tool",
["store", "--label=consortium-cli KEK", "service", KEYCHAIN_SERVICE$1, "account", account],
{ input: b64, timeout: EXEC_TIMEOUT_MS, stdio: ["pipe", "ignore", "ignore"] }
);
return true;
} catch {
return false;
}
}
function deleteLinux(account) {
try {
node_child_process.execFileSync(
"secret-tool",
["clear", "service", KEYCHAIN_SERVICE$1, "account", account],
{ timeout: EXEC_TIMEOUT_MS, stdio: "ignore" }
);
} catch {
}
}
function winTarget(account) {
return `${KEYCHAIN_SERVICE$1}:${account}`;
}
const WIN_ADDTYPE = `
$ErrorActionPreference='Stop'
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
public class ConsortiumCredMan {
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct CREDENTIAL {
public uint Flags; public uint Type; public string TargetName; public string Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public uint CredentialBlobSize; public IntPtr CredentialBlob; public uint Persist;
public uint AttributeCount; public IntPtr Attributes; public string TargetAlias; public string UserName;
}
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern bool CredWrite(ref CREDENTIAL c, uint flags);
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern bool CredRead(string target, uint type, uint flags, out IntPtr credential);
[DllImport("advapi32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern bool CredDelete(string target, uint type, uint flags);
[DllImport("advapi32.dll")] public static extern void CredFree(IntPtr credential);
}
"@
`;
function runPowerShell(script, env) {
try {
return node_child_process.execFileSync(
"powershell.exe",
["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", WIN_ADDTYPE + script],
{
encoding: "utf8",
timeout: EXEC_TIMEOUT_MS,
stdio: ["ignore", "pipe", "ignore"],
env: { ...process.env, ...env }
}
);
} catch {
return null;
}
}
function readWindows(account) {
const out = runPowerShell(
`
$t=$env:CONSORTIUM_KEK_TARGET
$p=[IntPtr]::Zero
if(-not [ConsortiumCredMan]::CredRead($t,1,0,[ref]$p)){ exit 1 }
$c=[System.Runtime.InteropServices.Marshal]::PtrToStructure($p,[Type][ConsortiumCredMan+CREDENTIAL])
$bytes=New-Object byte[] $c.CredentialBlobSize
[System.Runtime.InteropServices.Marshal]::Copy($c.CredentialBlob,$bytes,0,$c.CredentialBlobSize)
[ConsortiumCredMan]::CredFree($p)
[System.Text.Encoding]::UTF8.GetString($bytes)
`,
{ CONSORTIUM_KEK_TARGET: winTarget(account) }
);
return out === null ? null : decodeKek(out);
}
function storeWindows(account, b64) {
const out = runPowerShell(
`
$t=$env:CONSORTIUM_KEK_TARGET
$v=$env:CONSORTIUM_KEK_VALUE
$bytes=[System.Text.Encoding]::UTF8.GetBytes($v)
$blob=[System.Runtime.InteropServices.Marshal]::AllocHGlobal($bytes.Length)
[System.Runtime.InteropServices.Marshal]::Copy($bytes,0,$blob,$bytes.Length)
$cred=New-Object ConsortiumCredMan+CREDENTIAL
$cred.Type=1; $cred.TargetName=$t; $cred.Persist=2
$cred.CredentialBlobSize=$bytes.Length; $cred.CredentialBlob=$blob; $cred.UserName=$t
$ok=[ConsortiumCredMan]::CredWrite([ref]$cred,0)
[System.Runtime.InteropServices.Marshal]::FreeHGlobal($blob)
if($ok){ 'OK' } else { exit 1 }
`,
{ CONSORTIUM_KEK_TARGET: winTarget(account), CONSORTIUM_KEK_VALUE: b64 }
);
return out !== null && out.includes("OK");
}
function deleteWindows(account) {
runPowerShell(
`
$t=$env:CONSORTIUM_KEK_TARGET
[ConsortiumCredMan]::CredDelete($t,1,0) | Out-Null
`,
{ CONSORTIUM_KEK_TARGET: winTarget(account) }
);
}
function readKeychainKek() {
const account = currentAccount();
switch (process.platform) {
case "darwin":
return readMac(account);
case "linux":
return readLinux(account);
case "win32":
return readWindows(account);
default:
return null;
}
}
function writeKeychainKek(kek) {
const account = currentAccount();
const b64 = kek.toString("base64");
let stored;
switch (process.platform) {
case "darwin":
stored = storeMac(account, b64);
break;
case "linux":
stored = storeLinux(account, b64);
break;
case "win32":
stored = storeWindows(account, b64);
break;
default:
return false;
}
if (!stored) return false;
const readBack = readKeychainKek();
return readBack !== null && readBack.equals(kek);
}
function deleteKeychainKek() {
const account = currentAccount();
switch (process.platform) {
case "darwin":
deleteMac(account);
break;
case "linux":
deleteLinux(account);
break;
case "win32":
deleteWindows(account);
break;
}
}
const systemKeychainBackend = {
readKeychainKek,
writeKeychainKek
};
function resolveCredKek(deps) {
const { keychainEnabled, backend, file } = deps;
if (!keychainEnabled) {
const existing2 = file.read();
if (existing2) return { kek: existing2, source: "file" };
const kek2 = file.mint();
file.write(kek2);
return { kek: kek2, source: "file" };
}
const fromKeychain = backend.readKeychainKek();
if (fromKeychain) return { kek: fromKeychain, source: "keychain-hit" };
const existing = file.read();
const kek = existing ?? file.mint();
if (backend.writeKeychainKek(kek)) {
file.remove();
return { kek, source: "keychain-stored" };
}
if (!existing) file.write(kek);
return { kek, source: "file-fallback" };
}
const AnthropicConfigSchema = z__namespace.object({
baseUrl: z__namespace.string().url().optional(),
authToken: z__namespace.string().optional(),
model: z__namespace.string().optional()
});
const OpenAIConfigSchema = z__namespace.object({
apiKey: z__namespace.string().optional(),
baseUrl: z__namespace.string().url().optional(),
model: z__namespace.string().optional()
});
const AzureOpenAIConfigSchema = z__namespace.object({
apiKey: z__namespace.string().optional(),
endpoint: z__namespace.string().url().optional(),
apiVersion: z__namespace.string().optional(),
deploymentName: z__namespace.string().optional()
});
const TogetherAIConfigSchema = z__namespace.object({
apiKey: z__namespace.string().optional(),
model: z__namespace.string().optional()
});
const TmuxConfigSchema = z__namespace.object({
sessionName: z__namespace.string().optional(),
tmpDir: z__namespace.string().optional(),
updateEnvironment: z__namespace.boolean().optional()
});
const EnvironmentVariableSchema = z__namespace.object({
name: z__namespace.string().regex(/^[A-Z_][A-Z0-9_]*$/, "Invalid environment variable name"),
value: z__namespace.string()
});
const ProfileCompatibilitySchema = z__namespace.object({
claude: z__namespace.boolean().default(true),
codex: z__namespace.boolean().default(true),
gemini: z__namespace.boolean().default(true),
// Renamed from 'opencode' to 'consortium-code'. Old profiles with 'opencode'
// field are handled by passthrough — the default(true) ensures new profiles
// get the correct value.
"consortium-code": z__namespace.boolean().default(true)
}).passthrough();
const AIBackendProfileSchema = z__namespace.object({
id: z__namespace.string().uuid(),
name: z__namespace.string().min(1).max(100),
description: z__namespace.string().max(500).optional(),
// Agent-specific configurations
anthropicConfig: AnthropicConfigSchema.optional(),
openaiConfig: OpenAIConfigSchema.optional(),
azureOpenAIConfig: AzureOpenAIConfigSchema.optional(),
togetherAIConfig: TogetherAIConfigSchema.optional(),
// Tmux configuration
tmuxConfig: TmuxConfigSchema.optional(),
// Environment variables (validated)
environmentVariables: z__namespace.array(EnvironmentVariableSchema).default([]),
// Default session type for this profile
defaultSessionType: z__namespace.enum(["simple", "worktree"]).optional(),
// Default permission mode for this profile (supports both Claude and Codex modes)
defaultPermissionMode: z__namespace.enum([
"default",
"acceptEdits",
"bypassPermissions",
"plan",
// Claude modes
"read-only",
"safe-yolo",
"yolo"
// Codex modes
]).optional(),
// Default model mode for this profile
defaultModelMode: z__namespace.string().optional(),
// Compatibility metadata
compatibility: ProfileCompatibilitySchema.default({ claude: true, codex: true, gemini: true, "consortium-code": true }),
// Built-in profile indicator
isBuiltIn: z__namespace.boolean().default(false),
// Metadata
createdAt: z__namespace.number().default(() => Date.now()),
updatedAt: z__namespace.number().default(() => Date.now()),
version: z__namespace.string().default("1.0.0")
});
function validateProfileForAgent(profile, agent) {
return profile.compatibility[agent];
}
function getProfileEnvironmentVariables(profile) {
const envVars = {};
profile.environmentVariables.forEach((envVar) => {
envVars[envVar.name] = envVar.value;
});
if (profile.anthropicConfig) {
if (profile.anthropicConfig.baseUrl) envVars.ANTHROPIC_BASE_URL = profile.anthropicConfig.baseUrl;
if (profile.anthropicConfig.authToken) envVars.ANTHROPIC_AUTH_TOKEN = profile.anthropicConfig.authToken;
if (profile.anthropicConfig.model) envVars.ANTHROPIC_MODEL = profile.anthropicConfig.model;
}
if (profile.openaiConfig) {
if (profile.openaiConfig.apiKey) envVars.OPENAI_API_KEY = profile.openaiConfig.apiKey;
if (profile.openaiConfig.baseUrl) envVars.OPENAI_BASE_URL = profile.openaiConfig.baseUrl;
if (profile.openaiConfig.model) envVars.OPENAI_MODEL = profile.openaiConfig.model;
}
if (profile.azureOpenAIConfig) {
if (profile.azureOpenAIConfig.apiKey) envVars.AZURE_OPENAI_API_KEY = profile.azureOpenAIConfig.apiKey;
if (profile.azureOpenAIConfig.endpoint) envVars.AZURE_OPENAI_ENDPOINT = profile.azureOpenAIConfig.endpoint;
if (profile.azureOpenAIConfig.apiVersion) envVars.AZURE_OPENAI_API_VERSION = profile.azureOpenAIConfig.apiVersion;
if (profile.azureOpenAIConfig.deploymentName) envVars.AZURE_OPENAI_DEPLOYMENT_NAME = profile.azureOpenAIConfig.deploymentName;
}
if (profile.togetherAIConfig) {
if (profile.togetherAIConfig.apiKey) envVars.TOGETHER_API_KEY = profile.togetherAIConfig.apiKey;
if (profile.togetherAIConfig.model) envVars.TOGETHER_MODEL = profile.togetherAIConfig.model;
}
if (profile.tmuxConfig) {
if (profile.tmuxConfig.sessionName !== void 0) envVars.TMUX_SESSION_NAME = profile.tmuxConfig.sessionName;
if (profile.tmuxConfig.tmpDir) envVars.TMUX_TMPDIR = profile.tmuxConfig.tmpDir;
if (profile.tmuxConfig.updateEnvironment !== void 0) {
envVars.TMUX_UPDATE_ENVIRONMENT = profile.tmuxConfig.updateEnvironment.toString();
}
}
return envVars;
}
const SUPPORTED_SCHEMA_VERSION = 3;
const DEFAULT_RELAY_TRUST_POLICY = {
mode: "prompt-per-app",
perApp: {}
};
const defaultSettings = {
schemaVersion: SUPPORTED_SCHEMA_VERSION,
onboardingCompleted: false,
profiles: [],
localEnvironmentVariables: {},
relayTrustPolicy: { ...DEFAULT_RELAY_TRUST_POLICY, perApp: {} }
};
function migrateSettings(raw, fromVersion) {
let migrated = { ...raw };
if (fromVersion < 2) {
if (!migrated.profiles) {
migrated.profiles = [];
}
if (!migrated.localEnvironmentVariables) {
migrated.localEnvironmentVariables = {};
}
migrated.schemaVersion = 2;
}
if (fromVersion < 3) {
if (!migrated.relayTrustPolicy || typeof migrated.relayTrustPolicy !== "object") {
migrated.relayTrustPolicy = { ...DEFAULT_RELAY_TRUST_POLICY, perApp: {} };
} else {
if (!migrated.relayTrustPolicy.mode) {
migrated.relayTrustPolicy.mode = DEFAULT_RELAY_TRUST_POLICY.mode;
}
if (!migrated.relayTrustPolicy.perApp || typeof migrated.relayTrustPolicy.perApp !== "object") {
migrated.relayTrustPolicy.perApp = {};
}
}
migrated.schemaVersion = 3;
}
return migrated;
}
async function readSettings() {
if (!fs.existsSync(configuration.settingsFile)) {
return { ...defaultSettings };
}
try {
const content = await fs$3.readFile(configuration.settingsFile, "utf8");
const raw = JSON.parse(content);
const schemaVersion = raw.schemaVersion ?? 1;
if (schemaVersion > SUPPORTED_SCHEMA_VERSION) {
logger.warn(
`\u26A0\uFE0F Settings schema v${schemaVersion} > supported v${SUPPORTED_SCHEMA_VERSION}. Update consortium-cli for full functionality.`
);
}
const migrated = migrateSettings(raw, schemaVersion);
if (migrated.profiles && Array.isArray(migrated.profiles)) {
const validProfiles = [];
for (const profile of migrated.profiles) {
try {
const validated = AIBackendProfileSchema.parse(profile);
validProfiles.push(validated);
} catch (error) {
logger.warn(
`\u26A0\uFE0F Invalid profile "${profile?.name || profile?.id || "unknown"}" - skipping. Error: ${error.message}`
);
}
}
migrated.profiles = validProfiles;
}
const merged = { ...defaultSettings, ...migrated };
if (!merged.relayTrustPolicy) {
merged.relayTrustPolicy = { ...DEFAULT_RELAY_TRUST_POLICY, perApp: {} };
}
return merged;
} catch (error) {
logger.warn(`Failed to read settings: ${error.message}`);
return { ...defaultSettings };
}
}
async function updateSettings(updater) {
const LOCK_RETRY_INTERVAL_MS = 100;
const MAX_LOCK_ATTEMPTS = 50;
const STALE_LOCK_TIMEOUT_MS = 1e4;
const lockFile = configuration.settingsFile + ".lock";
const tmpFile = configuration.settingsFile + ".tmp";
let fileHandle;
let attempts = 0;
while (attempts < MAX_LOCK_ATTEMPTS) {
try {
fileHandle = await fs$3.open(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
break;
} catch (err) {
if (err.code === "EEXIST") {
attempts++;
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
try {
const stats = await fs$3.stat(lockFile);
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
await fs$3.unlink(lockFile).catch(() => {
});
}
} catch {
}
} else {
throw err;
}
}
}
if (!fileHandle) {
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
}
try {
const current = await readSettings() || { ...defaultSettings };
const updated = await updater(current);
if (!fs.existsSync(configuration.consortiumHomeDir)) {
await fs$3.mkdir(configuration.consortiumHomeDir, { recursive: true, mode: 448 });
}
await fs$3.writeFile(tmpFile, JSON.stringify(updated, null, 2), { mode: 384 });
await fs$3.rename(tmpFile, configuration.settingsFile);
return updated;
} finally {
await fileHandle.close();
await fs$3.unlink(lockFile).catch(() => {
});
}
}
const credentialsSchema$1 = z__namespace.object({
token: z__namespace.string(),
secret: z__namespace.string().base64().nullish(),
// Legacy
encryption: z__namespace.object({
publicKey: z__namespace.string().base64(),
machineKey: z__namespace.string().base64()
}).nullish()
}).passthrough();
const CRED_ENVELOPE_TYPE = "consortium-cred-v1";
function credKekPath() {
return path.join(configuration.consortiumHomeDir, "access.kek");
}
function credKekFileOps() {
const p = credKekPath();
return {
read: () => {
try {
if (fs.existsSync(p)) {
const buf = fs.readFileSync(p);
if (buf.length >= 32) return buf;
}
} catch {
}
return null;
},
write: (kek) => {
if (!fs.existsSync(configuration.consortiumHomeDir)) {
fs.mkdirSync(configuration.consortiumHomeDir, { recursive: true, mode: 448 });
}
fs.writeFileSync(p, kek, { mode: 384 });
try {
fs.chmodSync(p, 384);
} catch {
}
},
remove: () => {
try {
if (fs.existsSync(p)) fs.unlinkSync(p);
} catch {
}
},
mint: () => node_crypto.randomBytes(32)
};
}
function readOrCreateCredKek() {
return resolveCredKek({
keychainEnabled: isKeychainKekEnabled(),
backend: systemKeychainBackend,
file: credKekFileOps()
}).kek;
}
async function readCredentialPayload() {
const raw = await fs$3.readFile(configuration.privateKeyFile, "utf8");
const outer = JSON.parse(raw);
if (outer && typeof outer === "object" && outer.__consortiumCred === CRED_ENVELOPE_TYPE) {
const kek = readOrCreateCredKek();
const pt = openWithMaterial(outer, kek);
if (pt === null) throw new Error("access.key decryption failed");
return { obj: JSON.parse(pt), wasEncrypted: true };
}
return { obj: outer, wasEncrypted: false };
}
async function persistCredentials(payload) {
if (!fs.existsSync(configuration.consortiumHomeDir)) {
await fs$3.mkdir(configuration.consortiumHomeDir, { recursive: true, mode: 448 });
}
const canonical = JSON.stringify(payload);
if (fs.existsSync(configuration.privateKeyFile)) {
try {
const existing = await readCredentialPayload();
if (existing.wasEncrypted && JSON.stringify(existing.obj) === canonical) {
try {
await fs$3.chmod(configuration.privateKeyFile, 384);
} catch {
}
return;
}
} catch {
}
}
const kek = readOrCreateCredKek();
const sealed = sealWithMaterial(JSON.stringify(payload, null, 2), kek);
const envelope = { __consortiumCred: CRED_ENVELOPE_TYPE, ...sealed };
writeFileAtomicSync(configuration.privateKeyFile, JSON.stringify(envelope, null, 2));
}
async function readCredentials() {
if (!fs.existsSync(configuration.privateKeyFile)) {
return null;
}
try {
const payload = await readCredentialPayload();
const credentials = credentialsSchema$1.parse(payload.obj);
if (!payload.wasEncrypted) {
try {
await persistCredentials(payload.obj);
} catch (err) {
logger.debug("[PERSISTENCE] access.key encrypt-at-rest migration deferred:", err);
}
}
if (credentials.secret) {
const resolved = {
token: credentials.token,
encryption: {
type: "legacy",
secret: new Uint8Array(Buffer.from(credentials.secret, "base64"))
}
};
void configureSealedDekContent(resolved);
return resolved;
} else if (credentials.encryption) {
return {
token: credentials.token,
encryption: {
type: "dataKey",
publicKey: new Uint8Array(Buffer.from(credentials.encryption.publicKey, "base64")),
machineKey: new Uint8Array(Buffer.from(credentials.encryption.machineKey, "base64"))
}
};
} else {
return {
token: credentials.token,
encryption: null
};
}
} catch {
return null;
}
return null;
}
async function writeCredentialsLegacy(credentials) {
await persistCredentials({
secret: encodeBase64(credentials.secret),
token: credentials.token
});
}
async function writeCredentialsDataKey(credentials) {
await persistCredentials({
encryption: { publicKey: encodeBase64(credentials.publicKey), machineKey: encodeBase64(credentials.machineKey) },
token: credentials.token
});
}
async function writeCredentialsTokenOnly(token) {
await persistCredentials({ token });
}
async function clearCredentials() {
if (fs.existsSync(configuration.privateKeyFile)) {
await fs$3.unlink(configuration.privateKeyFile);
}
try {
if (fs.existsSync(credKekPath())) await fs$3.unlink(credKekPath());
} catch {
}
if (isKeychainKekEnabled()) {
try {
deleteKeychainKek();
} catch {
}
}
}
async function clearMachineId() {
await updateSettings((settings) => ({
...settings,
machineId: void 0
}));
}
async function readDaemonState() {
try {
if (!fs.existsSync(configuration.daemonStateFile)) {
return null;
}
const content = await fs$3.readFile(configuration.daemonStateFile, "utf-8");
return JSON.parse(content);
} catch (error) {
console.error(`[PERSISTENCE] Daemon state file corrupted: ${configuration.daemonStateFile}`, error);
return null;
}
}
function readDaemonStateSync() {
try {
if (!fs.existsSync(configuration.daemonStateFile)) {
return null;
}
const content = fs.readFileSync(configuration.daemonStateFile, "utf-8");
return JSON.parse(content);
} catch (error) {
logger.debug(`[PERSISTENCE] Daemon state file corrupted (sync read): ${configuration.daemonStateFile}`, error);
return null;
}
}
function writeDaemonState(state) {
writeFileAtomicSync(configuration.daemonStateFile, JSON.stringify(state, null, 2));
}
function writeFileAtomicSync(target, payload) {
const tmp = `${target}.tmp.${process.pid}`;
const fd = fs.openSync(tmp, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY, 384);
try {
fs.writeSync(fd, payload);
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
try {
fs.renameSync(tmp, target);
} catch (err) {
try {
fs.unlinkSync(tmp);
} catch {
}
throw err;
}
try {
fs.chmodSync(target, 384);
} catch {
}
}
async function clearDaemonState() {
if (fs.existsSync(configuration.daemonStateFile)) {
try {
await fs$3.unlink(configuration.daemonStateFile);
} catch (err) {
if (err.code !== "ENOENT") throw err;
}
}
}
async function forceCleanupStaleLock() {
if (fs.existsSync(configuration.daemonLockFile)) {
try {
await fs$3.unlink(configuration.daemonLockFile);
} catch {
}
}
}
const STALE_LOCK_MTIME_MS = 15 * 60 * 1e3;
function touchDaemonLock() {
try {
const now = /* @__PURE__ */ new Date();
fs.utimesSync(configuration.daemonLockFile, now, now);
} catch {
}
}
async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const fileHandle = await fs$3.open(
configuration.daemonLockFile,
fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY,
384
);
await fileHandle.writeFile(String(process.pid));
return fileHandle;
} catch (error) {
if (error.code === "EEXIST") {
try {
const lockPid = fs.readFileSync(configuration.daemonLockFile, "utf-8").trim();
if (lockPid && !isNaN(Number(lockPid))) {
const pid = Number(lockPid);
let processAlive = false;
try {
process.kill(pid, 0);
processAlive = true;
} catch {
}
if (!processAlive) {
const tmpLockFile = configuration.daemonLockFile + `.stale.${process.pid}`;
try {
fs.renameSync(configuration.daemonLockFile, tmpLockFile);
const confirmedPid = fs.readFileSync(tmpLockFile, "utf-8").trim();
if (confirmedPid === lockPid) {
fs.unlinkSync(tmpLockFile);
} else {
try {
fs.renameSync(tmpLockFile, configuration.daemonLockFile);
} catch {
try {
fs.unlinkSync(tmpLockFile);
} catch {
}
}
}
} catch (renameErr) {
if (renameErr.code === "ENOENT") {
} else {
throw renameErr;
}
}
continue;
}
}
} catch (readErr) {
if (readErr.code !== "ENOENT") {
try {
const ageMs = Date.now() - fs.statSync(configuration.daemonLockFile).mtimeMs;
if (ageMs > STALE_LOCK_MTIME_MS) {
fs.unlinkSync(configuration.daemonLockFile);
}
} catch {
}
}
}
}
if (attempt === maxAttempts) {
return null;
}
const delayMs = attempt * delayIncrementMs;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
return null;
}
async function releaseDaemonLock(lockHandle) {
try {
await lockHandle.close();
} catch {
}
try {
if (fs.existsSync(configuration.daemonLockFile)) {
fs.unlinkSync(configuration.daemonLockFile);
}
} catch {
}
}
function activeOrgKey(serverUrl, token) {
const material = token ? `${serverUrl}\0${token}` : `${serverUrl}\0__nocreds__`;
return node_crypto.createHash("sha256").update(material).digest("hex").slice(0, 16);
}
async function currentActiveOrgKey() {
let token = null;
try {
const creds = await readCredentials();
token = creds?.token ?? null;
} catch {
}
return activeOrgKey(configuration.serverUrl, token);
}
async function getActiveOrgId() {
const settings = await readSettings();
const key = await currentActiveOrgKey();
const keyed = settings.activeOrgByKey?.[key];
if (keyed) return keyed;
if (settings.activeOrgId) {
await setActiveOrgId(settings.activeOrgId);
return settings.activeOrgId;
}
return null;
}
async function setActiveOrgId(orgId) {
const key = await currentActiveOrgKey();
await updateSettings((settings) => {
const byKey = { ...settings.activeOrgByKey ?? {} };
if (orgId) {
byKey[key] = orgId;
} else {
delete byKey[key];
}
return {
...settings,
activeOrgByKey: byKey,
// Drop the legacy global field — keyed storage is authoritative now.
activeOrgId: void 0
};
});
}
async function clearActiveOrgId() {
await setActiveOrgId(void 0);
}
async function currentBrainScopeKey() {
let token = null;
try {
token = (await readCredentials())?.token ?? null;
} catch {
}
return activeOrgKey(configuration.serverUrl, token);
}
async function getActiveBrainKey() {
const settings = await readSettings();
const scope = await currentBrainScopeKey();
return settings.activeBrainByKey?.[scope] ?? null;
}
async function setActiveBrainKey(brainKey) {
const scope = await currentBrainScopeKey();
await updateSettings((settings) => {
const byKey = { ...settings.activeBrainByKey ?? {} };
if (brainKey) byKey[scope] = brainKey;
else delete byKey[scope];
return { ...settings, activeBrainByKey: byKey };
});
}
async function listBrains() {
const settings = await readSettings();
return Object.values(settings.brains ?? {});
}
async function getBrain(brainKey) {
const settings = await readSettings();
return settings.brains?.[brainKey] ?? null;
}
async function getActiveBrain() {
const key = await getActiveBrainKey();
if (!key) return null;
return getBrain(key);
}
async function upsertBrain(entry) {
await updateSettings((settings) => ({
...settings,
brains: { ...settings.brains ?? {}, [entry.key]: entry }
}));
}
async function setBrainSeq(brainKey, lastSeq) {
await updateSettings((settings) => {
const existing = settings.brains?.[brainKey];
if (!existing) return settings;
return {
...settings,
brains: { ...settings.brains ?? {}, [brainKey]: { ...existing, lastSeq } }
};
});
}
async function readRelayTrustPolicy() {
const settings = await readSettings();
const policy = settings.relayTrustPolicy;
if (!policy) {
return { ...DEFAULT_RELAY_TRUST_POLICY, perApp: {} };
}
return {
mode: policy.mode ?? DEFAULT_RELAY_TRUST_POLICY.mode,
perApp: policy.perApp ?? {}
};
}
async function writeRelayTrustPolicy(updater) {
const updated = await updateSettings((settings) => {
const current = settings.relayTrustPolicy ?? { ...DEFAULT_RELAY_TRUST_POLICY, perApp: {} };
const next = updater(current);
return {
...settings,
relayTrustPolicy: {
mode: next.mode ?? DEFAULT_RELAY_TRUST_POLICY.mode,
perApp: next.perApp ?? {}
}
};
});
return updated.relayTrustPolicy ?? { ...DEFAULT_RELAY_TRUST_POLICY, perApp: {} };
}
var persistence = /*#__PURE__*/Object.freeze({
__proto__: null,
AIBackendProfileSchema: AIBackendProfileSchema,
STALE_LOCK_MTIME_MS: STALE_LOCK_MTIME_MS,
SUPPORTED_SCHEMA_VERSION: SUPPORTED_SCHEMA_VERSION,
acquireDaemonLock: acquireDaemonLock,
clearActiveOrgId: clearActiveOrgId,
clearCredentials: clearCredentials,
clearDaemonState: clearDaemonState,
clearMachineId: clearMachineId,
forceCleanupStaleLock: forceCleanupStaleLock,
getActiveBrain: getActiveBrain,
getActiveBrainKey: getActiveBrainKey,
getActiveOrgId: getActiveOrgId,
getBrain: getBrain,
getProfileEnvironmentVariables: getProfileEnvironmentVariables,
listBrains: listBrains,
readCredentials: readCredentials,
readDaemonState: readDaemonState,
readDaemonStateSync: readDaemonStateSync,
readRelayTrustPolicy: readRelayTrustPolicy,
readSettings: readSettings,
releaseDaemonLock: releaseDaemonLock,
setActiveBrainKey: setActiveBrainKey,
setActiveOrgId: setActiveOrgId,
setBrainSeq: setBrainSeq,
touchDaemonLock: touchDaemonLock,
updateSettings: updateSettings,
upsertBrain: upsertBrain,
validateProfileForAgent: validateProfileForAgent,
writeCredentialsDataKey: writeCredentialsDataKey,
writeCredentialsLegacy: writeCredentialsLegacy,
writeCredentialsTokenOnly: writeCredentialsTokenOnly,
writeDaemonState: writeDaemonState,
writeRelayTrustPolicy: writeRelayTrustPolicy
});
const DEFAULT_TIMEOUT_MS = 1e4;
const REQUEST_EVENT = "external-relay-trust-request";
const RESPONSE_EVENT = "external-relay-trust-response";
const KNOWN_DECISIONS = /* @__PURE__ */ new Set([
"allow",
"allow-always",
"deny",
"deny-always"
]);
function isTrustResponsePayload(payload) {
if (!payload || typeof payload !== "object") return false;
const candidate = payload;
return typeof candidate.promptId === "string" && typeof candidate.decision === "string" && KNOWN_DECISIONS.has(candidate.decision);
}
async function requestTrustFromMobile(args) {
const { socket, appId, kind, sessionId, machineId } = args;
const timeoutMs = args.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const promptId = node_crypto.randomUUID();
return await new Promise((resolve) => {
let settled = false;
let timer = null;
const cleanup = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
try {
socket.off(RESPONSE_EVENT, listener);
} catch (err) {
logger.debug("[relay/trust] Failed to detach trust-response listener", { err, promptId });
}
};
const finish = (outcome) => {
if (settled) return;
settled = true;
cleanup();
resolve(outcome);
};
const listener = (payload) => {
if (!isTrustResponsePayload(payload)) return;
if (payload.promptId !== promptId) return;
finish(payload.decision);
};
try {
socket.on(RESPONSE_EVENT, listener);
} catch (err) {
logger.debug("[relay/trust] Failed to attach trust-response listener", { err, promptId });
resolve("timeout");
return;
}
timer = setTimeout(() => finish("timeout"), timeoutMs);
const maybeUnref = timer.unref;
if (typeof maybeUnref === "function") {
try {
maybeUnref.call(timer);
} catch {
}
}
try {
socket.emit(REQUEST_EVENT, {
promptId,
appId,
kind,
sessionId,
machineId,
requestedAt: Date.now()
});
} catch (err) {
logger.debug("[relay/trust] Failed to emit trust-request event", { err, promptId });
finish("timeout");
}
});
}
const PAT_SYNTHETIC_APP_ID = "__pat__";
let cached = null;
async function loadPolicy() {
if (cached) return cached;
cached = await readRelayTrustPolicy();
return cached;
}
function invalidateCache() {
cached = null;
}
async function readPerAppPolicy(appId) {
const policy = await loadPolicy();
const entry = policy.perApp[appId];
return entry ?? null;
}
async function writePerAppPolicy(appId, policy) {
await writeRelayTrustPolicy((current) => ({
mode: current.mode,
perApp: { ...current.perApp, [appId]: policy }
}));
invalidateCache();
}
async function persistIfAlways(appId, outcome) {
if (outcome === "allow-always") {
await writePerAppPolicy(appId, "allow");
} else if (outcome === "deny-always") {
await writePerAppPolicy(appId, "deny");
}
}
function outcomeToDecision(outcome) {
if (outcome === "allow" || outcome === "allow-always") return "allow";
return "deny";
}
async function promptAndPersist(appId, kind, socket, sessionId, machineId, promptTimeoutMs) {
if (!socket) {
logger.debug("[relay/trust] No socket available for mobile prompt \u2014 denying", { appId, kind });
return "deny";
}
const outcome = await requestTrustFromMobile({
socket,
appId,
kind,
sessionId,
machineId,
timeoutMs: promptTimeoutMs
});
await persistIfAlways(appId, outcome);
return outcomeToDecision(outcome);
}
async function evaluateTrust(input) {
const { appId, kind, sessionId, machineId, socket, promptTimeoutMs } = input;
if (!appId) {
if (kind !== "machine.invoke") {
return "allow";
}
const existing2 = await readPerAppPolicy(PAT_SYNTHETIC_APP_ID);
if (existing2) return existing2;
return await promptAndPersist(PAT_SYNTHETIC_APP_ID, kind, socket, sessionId, machineId, promptTimeoutMs);
}
const policy = await loadPolicy();
const existing = policy.perApp[appId];
if (existing === "allow" || existing === "deny") {
return existing;
}
if (policy.mode === "auto-accept" && kind !== "machine.invoke") {
return "allow";
}
return await promptAndPersist(appId, kind, socket, sessionId, machineId, promptTimeoutMs);
}
function isRelayRequest(event) {
if (!event || typeof event !== "object") return false;
const candidate = event;
return candidate.type === "external-relay-request" && typeof candidate.requestId === "string" && typeof candidate.kind === "string" && typeof candidate.payload === "object" && candidate.payload !== null;
}
function isRelayEnabled() {
return process.env.CONSORTIUM_RELAY_ENABLED === "1";
}
function createRelayHandler(options) {
const { socket, session } = options;
const reply = (response) => {
try {
;
socket.emit("external-relay-response", response);
} catch (err) {
logger.debug("[relay] Failed to emit relay response", { err, requestId: response.requestId });
}
};
const handleSessionSendMessage = (event) => {
const payload = event.payload;
if (typeof payload.content !== "string") {
reply({
requestId: event.requestId,
status: "error",
errorCode: "invalid_payload",
errorMessage: "session.send-message requires payload.content: string"
});
return;
}
try {
session.sendClaudeSessionMessage({
type: "user",
uuid: node_crypto.randomUUID(),
message: { content: payload.content }
});
} catch (err) {
logger.debug("[relay] sendClaudeSessionMessage threw", { err, requestId: event.requestId });
reply({
requestId: event.requestId,
status: "error",
errorCode: "internal",
errorMessage: err instanceof Error ? err.message : String(err)
});
return;
}
reply({
requestId: event.requestId,
status: "ok",
result: { enqueued: true, timestamp: Date.now() }
});
};
const handleMachineInvoke = async (event) => {
const payload = event.payload;
if (typeof payload.command !== "string" || payload.command.length === 0) {
reply({
requestId: event.requestId,
status: "error",
errorCode: "invalid_payload",
errorMessage: "machine.invoke requires payload.command: string"
});
return;
}
const args = Array.isArray(payload.args) ? payload.args.filter((a) => typeof a === "string") : void 0;
const cwd = typeof payload.cwd === "string" ? payload.cwd : void 0;
const timeoutMs = typeof payload.timeoutMs === "number" ? payload.timeoutMs : void 0;
const result = await executeCommand({
command: payload.command,
args,
cwd,
timeoutMs
});
reply({
requestId: event.requestId,
status: "ok",
result
});
};
const dispatch = (event) => {
try {
switch (event.kind) {
case "session.send-message":
handleSessionSendMessage(event);
return;
case "machine.invoke":
handleMachineInvoke(event).catch((err) => {
logger.debug("[relay] machine.invoke handler rejected", { err, requestId: event.requestId });
reply({
requestId: event.requestId,
status: "error",
errorCode: "internal",
errorMessage: err instanceof Error ? err.message : String(err)
});
});
return;
default:
reply({
requestId: event.requestId,
status: "error",
errorCode: "unknown_kind"
});
return;
}
} catch (err) {
reply({
requestId: event.requestId,
status: "error",
errorCode: "internal",
errorMessage: err instanceof Error ? err.message : String(err)
});
}
};
const onEphemeral = (event) => {
if (!isRelayRequest(event)) {
return;
}
if (!isRelayEnabled()) {
reply({
requestId: event.requestId,
status: "rejected",
errorCode: "disabled"
});
return;
}
if (event.kind !== "session.send-message" && event.kind !== "machine.invoke") {
reply({
requestId: event.requestId,
status: "error",
errorCode: "unknown_kind"
});
return;
}
if (!event.appId && event.kind === "session.send-message") {
dispatch(event);
return;
}
evaluateTrust({
appId: event.appId,
kind: event.kind,
sessionId: event.sessionId,
machineId: event.machineId,
socket
}).then((decision) => {
if (decision === "deny") {
reply({
requestId: event.requestId,
status: "rejected",
errorCode: "user_declined"
});
return;
}
dispatch(event);
}).catch((err) => {
logger.debug("[relay] Trust evaluation threw \u2014 failing closed", { err, requestId: event.requestId });
reply({
requestId: event.requestId,
status: "rejected",
errorCode: "user_declined"
});
});
};
return { onEphemeral };
}
const WRAPPED_KEY_NONCE_BYTES = tweetnacl.box.nonceLength;
tweetnacl.box.publicKeyLength;
function wrapDataEncryptionKey(dataEncryptionKey, appBoxPublicKey) {
const ephemeral = tweetnacl.box.keyPair();
const nonce = getRandomBytes(WRAPPED_KEY_NONCE_BYTES);
const ciphertext = tweetnacl.box(
dataEncryptionKey,
nonce,
appBoxPublicKey,
ephemeral.secretKey
);
const bundle = new Uint8Array(
ephemeral.publicKey.length + nonce.length + ciphertext.length
);
bundle.set(ephemeral.publicKey, 0);
bundle.set(nonce, ephemeral.publicKey.length);
bundle.set(ciphertext, ephemeral.publicKey.length + nonce.length);
return bundle;
}
const SESSION_DEK_BYTES = tweetnacl.secretbox.keyLength;
async function rotateSessionKey(input) {
const {
serverBaseUrl,
deviceToken,
sessionId,
currentKeyVersion,
authorizedGrants
} = input;
if (currentKeyVersion < 1 || !Number.isInteger(currentKeyVersion)) {
throw new Error(
`sessionKeyRotate: currentKeyVersion must be a positive integer, got ${currentKeyVersion}`
);
}
const seen = /* @__PURE__ */ new Set();
for (const g of authorizedGrants) {
if (g.appBoxPublicKey.length !== tweetnacl.box.publicKeyLength) {
throw new Error(
`sessionKeyRotate: appBoxPublicKey for ${g.appId} must be ${tweetnacl.box.publicKeyLength} bytes, got ${g.appBoxPublicKey.length}`
);
}
if (seen.has(g.appId)) {
throw new Error(`sessionKeyRotate: duplicate appId in authorizedGrants: ${g.appId}`);
}
seen.add(g.appId);
}
const newDek = getRandomBytes(SESSION_DEK_BYTES);
const wrappedKeys = authorizedGrants.map((g) => ({
appId: g.appId,
wrappedKey: encodeBase64(wrapDataEncryptionKey(newDek, g.appBoxPublicKey))
}));
const newKeyVersion = currentKeyVersion + 1;
const url = `${serverBaseUrl.replace(/\/+$/, "")}/v1/sessions/${encodeURIComponent(sessionId)}/rotate-key`;
try {
const response = await axios.post(
url,
{ newKeyVersion, wrappedKeys },
{
headers: {
"Authorization": `Bearer ${deviceToken}`,
"Content-Type": "application/json"
},
timeout: 15e3,
validateStatus: () => true
}
);
if (response.status === 409) {
throw new SessionKeyRotateVersionConflictError(
extractServerErrorMessage(response.status, response.data)
);
}
if (response.status < 200 || response.status >= 300) {
throw new Error(extractServerErrorMessage(response.status, response.data));
}
const body = response.data;
if (!body || typeof body.keyVersion !== "number" || typeof body.rotatedAt !== "number" || typeof body.appliedGrants !== "number" || typeof body.skippedRevokedGrants !== "number") {
throw new Error(
`sessionKeyRotate: malformed success response from ${url} \u2014 expected { keyVersion, rotatedAt, appliedGrants, skippedRevokedGrants }`
);
}
return {
newDek,
newKeyVersion: body.keyVersion,
rotatedAt: body.rotatedAt,
appliedGrants: body.appliedGrants,
skippedRevokedGrants: body.skippedRevokedGrants
};
} catch (error) {
if (error instanceof SessionKeyRotateVersionConflictError) {
throw error;
}
if (error instanceof Error && error.message.startsWith("sessionKeyRotate:")) {
throw error;
}
if (axios.isAxiosError(error)) {
const code = error.code ? ` (${error.code})` : "";
throw new Error(`sessionKeyRotate: request to ${url} failed${code}: ${error.message}`);
}
const message = error instanceof Error ? error.message : String(error);
throw new Error(`sessionKeyRotate: ${message}`);
}
}
class SessionKeyRotateVersionConflictError extends Error {
constructor(message) {
super(message);
this.name = "SessionKeyRotateVersionConflictError";
}
}
function extractServerErrorMessage(status, body) {
if (body && typeof body === "object") {
const record = body;
const error = typeof record.error === "string" ? record.error : void 0;
const message = typeof record.message === "string" ? record.message : void 0;
if (error && message) return `sessionKeyRotate: ${error}: ${message}`;
if (error) return `sessionKeyRotate: ${error}`;
if (message) return `sessionKeyRotate: ${message}`;
}
if (typeof body === "string" && body.length > 0) {
return `sessionKeyRotate: HTTP ${status}: ${body}`;
}
return `sessionKeyRotate: HTTP ${status}`;
}
function isRotateRequest(event) {
if (!event || typeof event !== "object") return false;
const candidate = event;
return candidate.type === "session-rotate-requested" && typeof candidate.sessionId === "string" && typeof candidate.reason === "string";
}
function createRotateHandler(options) {
const onEphemeral = (event) => {
if (!isRotateRequest(event)) return;
(async () => {
const sessionId = event.sessionId;
try {
const state = await options.lookupSession(sessionId);
if (!state) {
logger.debug("[rotate] Unknown session \u2014 dropping rotate request", { sessionId });
return;
}
const result = await rotateSessionKey({
serverBaseUrl: options.serverBaseUrl,
deviceToken: options.deviceToken,
sessionId,
currentKeyVersion: state.currentKeyVersion,
authorizedGrants: state.authorizedGrants
});
await options.onRotated(sessionId, result);
logger.debug("[rotate] Session DEK rotated", {
sessionId,
newKeyVersion: result.newKeyVersion,
appliedGrants: result.appliedGrants
});
} catch (err) {
logger.debug("[rotate] Rotation failed", {
sessionId,
err: err instanceof Error ? err.message : String(err)
});
}
})();
};
return { onEphemeral };
}
const MAX_ARTIFACT_FILE_BYTES = 512 * 1024;
class ApiSessionClient extends node_events.EventEmitter {
token;
sessionId;
metadata;
metadataVersion;
agentState;
agentStateVersion;
socket;
pendingMessages = [];
pendingMessageCallback = null;
// EPHEMERAL-1: optional subscriber for server-broadcast model-change
// events (Path A set/clear). Runners register this to reconcile the
// active model + repaint their terminal UI without waiting for the next
// request. Null when no runner is interested (e.g. headless tooling).
modelChangeCallback = null;
rpcHandlerManager;
agentStateLock = new AsyncLock();
metadataLock = new AsyncLock();
encryptionKey;
encryptionVariant;
// User content public key for wrapping library-artifact DEKs (dataKey accounts only).
userContentPublicKey;
// In-memory state for library artifacts created in this session (id -> state).
artifactStates = /* @__PURE__ */ new Map();
// Latest model id observed on this session's stream. Claude Code's SDK
// result messages carry the canonical token totals (including cache_read
// and cache_creation) but no model id — the model lives on assistant
// and system-init messages. We cache it here so the result-message
// usage report can attribute tokens to the right model.
lastClaudeModel;
// Buffer for session messages when the socket is disconnected. Drained on
// the next `connect` event (which socket.io fires on both initial connect
// and every reconnect). Without this, a brief network blip during an
// agent's reply was silently dropping messages on the floor, which is why
// sessions appeared to "only update when the user typed" — the typing
// triggered a reconnect that caused the client to refetch missed state.
pendingEmits = [];
static MAX_PENDING_EMITS = 1e3;
// Manual reconnect state. Socket.IO v4 does not auto-retry middleware-
// level handshake rejections (the path the server takes for "Invalid
// authentication token"), so we schedule reconnects ourselves with
// exponential backoff.
reconnectTimer = null;
manualReconnectAttempts = 0;
destroyed = false;
// Watchdog armed when we buffer a payload while disconnected. If the
// socket has not reconnected within FLUSH_WATCHDOG_MS we fall back to
// scheduleManualReconnect() instead of waiting for Socket.IO's idle
// backoff — otherwise scheduled-wake assistant output sits in
// pendingEmits until the next user-typed message nudges the
// connection alive, producing an n+1 visual lag.
flushWatchdog = null;
static FLUSH_WATCHDOG_MS = 2e3;
/** Get encryption details for passing to child processes. */
getEncryptionDetails() {
return { key: this.encryptionKey, variant: this.encryptionVariant };
}
/**
* Upload an agent-produced media blob to the server as a SessionArtifact.
*
* Phase 1.5 (E2EE): when the session content key is available (it always
* is for a live ApiSessionClient — both 'legacy' and 'dataKey' variants
* carry a 32-byte symmetric key that the app derives for the same
* session), the bytes are encrypted client-side with libsodium secretbox
* as `nonce || ciphertext` — the exact Drive-content primitive, so the
* app decrypts with the code path it already ships — and the POST carries
* `encVersion: 1`. If the key is missing/malformed (defensive: should not
* happen), we fall back to the Phase-1 plaintext upload with no
* `encVersion`, which the private-prefix + presigned-URL model still
* gates. Server-side size caps apply to the CIPHERTEXT (secretbox adds
* only 24-byte nonce + 16-byte MAC).
*
* Returns the artifact id and metadata so the caller can immediately
* reference it in an outgoing agent message
* (`{ type: 'artifact', artifactId, mime }`).
*/
async uploadArtifact(args) {
let payload = args.bytes;
let encVersion;
if (this.encryptionKey && this.encryptionKey.length === 32) {
const env = await getCryptoEnv();
payload = encryptDriveContent(env, args.bytes, this.encryptionKey);
encVersion = 1;
}
const dataBase64 = encodeBase64(payload);
const axiosMod = await import('axios');
const axios = axiosMod.default ?? axiosMod;
const url = `${configuration.serverUrl}/v1/sessions/${this.sessionId}/artifacts`;
let result;
try {
const { data } = await axios.post(
url,
{ mime: args.mime, dataBase64, ...encVersion !== void 0 ? { encVersion } : {} },
{ headers: { Authorization: `Bearer ${this.token}` } }
);
result = data;
} catch (err) {
const status = err?.response?.status;
const serverBody = err?.response?.data;
const serverMsg = typeof serverBody === "string" ? serverBody : serverBody?.error ?? serverBody?.message ?? JSON.stringify(serverBody ?? {});
const msg = `uploadArtifact failed: HTTP ${status ?? "?"} ${serverMsg} (mime=${args.mime}, bytes=${args.bytes.length}, base64=${dataBase64.length})`;
throw new Error(msg);
}
if (encVersion === 1 && result.encVersion !== 1) {
throw new Error(
`uploadArtifact aborted: server did not acknowledge encVersion=1 (got ${result.encVersion ?? "none"}). The server is older than this CLI; encrypted artifact bytes would be stored as legacy plaintext and permanently corrupt the artifact. Upgrade the server before uploading encrypted artifacts.`
);
}
return result;
}
/** True when this session can create interactive library artifacts. */
canCreateLibraryArtifacts() {
return !!this.userContentPublicKey;
}
/**
* Create a versioned interactive artifact (the /v1/artifacts entity),
* linked to this session. Encrypts header+body with a fresh per-artifact
* DEK wrapped to the user's content public key — byte-compatible with the
* mobile app's ArtifactEncryption, so the artifact opens in the app's
* slide-in panel and library. Returns the new artifact id.
*/
async createLibraryArtifact(input) {
if (!this.userContentPublicKey) {
throw new Error("This account uses legacy encryption and cannot create interactive artifacts.");
}
const env = await getCryptoEnv();
const dek = generateArtifactDek(env);
const wrappedDek = wrapArtifactDek(env, dek, this.userContentPublicKey);
const header = await encryptArtifactField(
{ title: input.title, sessions: [this.sessionId], kind: input.kind, language: input.language, mime: input.mime },
dek
);
const body = await encryptArtifactField({ body: input.content, versions: [] }, dek);
const id = node_crypto.randomUUID();
const axiosMod = await import('axios');
const axios = axiosMod.default ?? axiosMod;
const url = `${configuration.serverUrl}/v1/artifacts`;
await axios.post(
url,
{ id, header, body, dataEncryptionKey: wrappedDek },
{ headers: { Authorization: `Bearer ${this.token}` } }
);
this.artifactStates.set(id, {
dek,
title: input.title,
kind: input.kind,
language: input.language,
mime: input.mime,
content: input.content,
bodyVersion: 1,
versions: []
});
this.autoArtifactSeen.add(this.artifactDedupeKey(input.kind, input.content));
return { id };
}
/**
* Update an artifact previously created in this session, producing a new
* body version and appending the prior body to the inline version history
* (capped). Requires the artifact to exist in this session's in-memory
* state (i.e. created in the same run) — otherwise the DEK is unavailable
* and the caller should create a new artifact instead.
*/
async updateLibraryArtifact(input) {
const state = this.artifactStates.get(input.id);
if (!state) {
throw new Error("Artifact not found in this session \u2014 create a new one instead of updating.");
}
await getCryptoEnv();
const nextVersions = [
...state.versions,
{ n: state.bodyVersion, body: state.content, createdAt: Date.now(), summary: input.summary }
].slice(-10);
const newTitle = input.title ?? state.title;
const updateRequest = {};
const encryptedBody = await encryptArtifactField({ body: input.content, versions: nextVersions }, state.dek);
updateRequest.body = encryptedBody;
updateRequest.expectedBodyVersion = state.bodyVersion;
if (input.title !== void 0 && input.title !== state.title) {
updateRequest.header = await encryptArtifactField(
{ title: newTitle, sessions: [this.sessionId], kind: state.kind, language: state.language, mime: state.mime },
state.dek
);
updateRequest.expectedHeaderVersion = state.bodyVersion;
}
const axiosMod = await import('axios');
const axios = axiosMod.default ?? axiosMod;
const url = `${configuration.serverUrl}/v1/artifacts/${input.id}`;
const { data } = await axios.post(url, updateRequest, {
headers: { Authorization: `Bearer ${this.token}` }
});
if (data && data.success === false && data.error === "version-mismatch") {
const retryBodyVersion = typeof data.currentBodyVersion === "number" ? data.currentBodyVersion : state.bodyVersion + 1;
await axios.post(
url,
{ body: encryptedBody, expectedBodyVersion: retryBodyVersion },
{ headers: { Authorization: `Bearer ${this.token}` } }
);
state.bodyVersion = retryBodyVersion + 1;
} else {
state.bodyVersion = typeof data?.bodyVersion === "number" ? data.bodyVersion : state.bodyVersion + 1;
}
state.content = input.content;
state.title = newTitle;
state.versions = nextVersions;
return { bodyVersion: state.bodyVersion, title: state.title, kind: state.kind };
}
constructor(token, session) {
super();
this.token = token;
this.sessionId = session.id;
this.metadata = session.metadata;
this.metadataVersion = session.metadataVersion;
this.agentState = session.agentState;
this.agentStateVersion = session.agentStateVersion;
this.encryptionKey = session.encryptionKey;
this.encryptionVariant = session.encryptionVariant;
this.userContentPublicKey = session.userContentPublicKey;
this.rpcHandlerManager = new RpcHandlerManager({
scopePrefix: this.sessionId,
encryptionKey: this.encryptionKey,
encryptionVariant: this.encryptionVariant,
logger: (msg, data) => logger.debug(msg, data)
});
registerCommonHandlers(this.rpcHandlerManager, this.metadata.path);
this.socket = socket_ioClient.io(configuration.serverUrl, {
auth: {
token: this.token,
clientType: "session-scoped",
sessionId: this.sessionId
},
path: "/v1/updates",
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 1e3,
reconnectionDelayMax: 5e3,
transports: ["websocket"],
withCredentials: true,
autoConnect: false,
// Bump RPC acknowledgement timeout from the 60s default. The
// bare "operation has timed out" string users were seeing on
// intermittent Grok session-create was Socket.IO's ack
// timeout firing during a slow WS handshake or first RPC.
// 120s tolerates a brief network blip without the agent
// appearing to "just fail to start".
ackTimeout: 12e4,
timeout: 3e4
// initial connect timeout
});
this.socket.on("connect", () => {
logger.debug("Socket connected successfully");
this.manualReconnectAttempts = 0;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.flushWatchdog) {
clearTimeout(this.flushWatchdog);
this.flushWatchdog = null;
}
this.rpcHandlerManager.onSocketConnect(this.socket);
this.flushPendingEmits();
});
this.socket.on("rpc-request", async (data, callback) => {
callback(await this.rpcHandlerManager.handleRequest(data));
});
this.socket.on("disconnect", (reason) => {
logger.debug("[API] Socket disconnected:", reason);
this.rpcHandlerManager.onSocketDisconnect();
});
this.socket.on("server-shutting-down", (data) => {
logger.debug(`[API] Server shutting down (reason: ${data.reason}), forcing transport close to trigger reconnect`);
this.socket.io.engine?.close();
});
this.socket.on("connect_error", (error) => {
logger.debug("[API] Socket connection error:", error);
this.rpcHandlerManager.onSocketDisconnect();
this.scheduleManualReconnect();
});
this.socket.on("update", (data) => {
try {
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
if (!data.body) {
logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
return;
}
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
const body = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.message.content.c));
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
const userResult = UserMessageSchema.safeParse(body);
if (userResult.success) {
const transportLocalId = data.body.message.localId;
if (userResult.data.content.localId === void 0 && typeof transportLocalId === "string") {
userResult.data.content.localId = transportLocalId;
}
try {
const decoded = decodeMessageBody(
new TextEncoder().encode(userResult.data.content.text)
);
if (decoded.v === 2) {
userResult.data.content.text = decoded.text;
if (userResult.data.content.attachmentIds === void 0) {
userResult.data.content.attachmentIds = decoded.attachmentIds;
}
if (userResult.data.content.localId === void 0 && decoded.localId) {
userResult.data.content.localId = decoded.localId;
}
}
} catch (e) {
if (!(e instanceof MessageDecodeError)) throw e;
}
if (userResult.data.content.attachmentIds !== void 0) {
userResult.data.attachmentIds = userResult.data.content.attachmentIds;
}
if (userResult.data.content.driveId !== void 0) {
userResult.data.driveId = userResult.data.content.driveId;
}
if (this.pendingMessageCallback) {
this.pendingMessageCallback(userResult.data);
} else {
this.pendingMessages.push(userResult.data);
}
} else {
this.emit("message", body);
}
} else if (data.body.t === "update-session") {
if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.metadata.value));
this.metadataVersion = data.body.metadata.version;
}
if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
this.agentState = data.body.agentState.value ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(data.body.agentState.value)) : null;
this.agentStateVersion = data.body.agentState.version;
}
} else if (data.body.t === "update-machine") {
logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
} else {
this.emit("message", data.body);
}
} catch (error) {
logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
}
});
const relayHandler = createRelayHandler({ socket: this.socket, session: this });
let rotationNoOpWarned = false;
const rotateHandler = createRotateHandler({
serverBaseUrl: configuration.serverUrl,
deviceToken: this.token,
lookupSession: async (sessionId) => {
if (!rotationNoOpWarned) {
rotationNoOpWarned = true;
logger.info(
"[rotate] secure_rotate requested but forward-secure DEK rotation is not yet implemented on this device; the server version bump and delivery supersession still apply, but the local DEK is NOT rotated",
{ sessionId }
);
} else {
logger.debug("[rotate] secure_rotate no-op (rotation unimplemented on this device)", { sessionId });
}
return null;
},
onRotated: () => {
}
});
this.socket.on("ephemeral", (event) => {
relayHandler.onEphemeral(event);
rotateHandler.onEphemeral(event);
this.onModelChangeEphemeral(event);
});
this.socket.on("error", (error) => {
logger.debug("[API] Socket error:", error);
});
this.socket.connect();
}
onUserMessage(callback) {
this.pendingMessageCallback = callback;
while (this.pendingMessages.length > 0) {
callback(this.pendingMessages.shift());
}
}
/**
* EPHEMERAL-1: subscribe to server-broadcast model-change events
* (emitted on Path A set AND clear — CONTRACT A). The server fans these
* out so every live client/session reconciles the active model
* immediately instead of only learning on the next request. Runners use
* this to update their in-memory model + repaint the model pill.
*
* `modelId === null` means the override was cleared (reset to default).
* `sessionId` is present for session-scoped changes and absent for
* account-level (default-for-new-sessions) changes.
*/
onModelChange(callback) {
this.modelChangeCallback = callback;
}
/**
* Parse + dispatch a model-change ephemeral. Defensive: tolerates the
* broadcast arriving as either `model-change` or `model-changed` and
* silently ignores anything that isn't a well-formed model-change event
* (the same socket carries relay + rotate ephemerals).
*/
onModelChangeEphemeral(event) {
if (!event || typeof event !== "object") return;
const e = event;
if (e.type !== "model-change" && e.type !== "model-changed") return;
const providerId = typeof e.providerId === "string" ? e.providerId : void 0;
if (!providerId) return;
const modelId = e.modelId === null ? null : typeof e.modelId === "string" ? e.modelId : void 0;
if (modelId === void 0) return;
const sessionId = typeof e.sessionId === "string" ? e.sessionId : void 0;
if (sessionId && sessionId !== this.sessionId) {
logger.debug(`[API] Ignoring model-change for other session ${sessionId} (ours=${this.sessionId})`);
return;
}
logger.debug(`[API] model-change ephemeral: provider=${providerId} model=${modelId ?? "(cleared)"} sessionId=${sessionId ?? "(account)"}`);
if (this.modelChangeCallback) {
try {
this.modelChangeCallback({ providerId, modelId, sessionId });
} catch (err) {
logger.debug("[API] model-change callback threw", { err });
}
}
}
/**
* Emit a 'message' payload, buffering it if the socket is currently
* disconnected. Drained on the next 'connect' event.
*/
reliableEmitMessage(payload, context) {
if (this.socket.connected) {
this.socket.emit("message", payload);
return;
}
if (this.pendingEmits.length >= ApiSessionClient.MAX_PENDING_EMITS) {
this.pendingEmits.shift();
logger.debug("[API] Reconnect buffer at cap, dropping oldest", { context });
}
this.pendingEmits.push(payload);
logger.debug("[API] Socket disconnected, queued for reconnect", { context, queueSize: this.pendingEmits.length });
if (!this.destroyed && !this.reconnectTimer) {
try {
this.socket.connect();
} catch (err) {
logger.debug("[API] socket.connect() while buffering threw", { err });
}
}
if (!this.destroyed && !this.flushWatchdog) {
this.flushWatchdog = setTimeout(() => {
this.flushWatchdog = null;
if (this.destroyed) return;
if (this.socket.connected) return;
if (this.pendingEmits.length === 0) return;
logger.debug("[API] Flush watchdog firing manual reconnect");
this.scheduleManualReconnect();
}, ApiSessionClient.FLUSH_WATCHDOG_MS);
}
}
flushPendingEmits() {
if (this.pendingEmits.length === 0) return;
const toSend = this.pendingEmits;
this.pendingEmits = [];
logger.debug(`[API] Flushing ${toSend.length} pending messages after reconnect`);
for (const payload of toSend) {
this.socket.emit("message", payload);
}
}
/**
* Send message to session
* @param body - Message body (can be MessageContent or raw content for agent messages)
*/
sendClaudeSessionMessage(body) {
let content;
if (body.type === "user" && typeof body.message.content === "string" && body.isSidechain !== true && body.isMeta !== true) {
content = {
role: "user",
content: {
type: "text",
text: body.message.content
},
meta: {
sentFrom: "cli"
}
};
} else {
content = {
role: "agent",
content: {
type: "output",
data: body
// This wraps the entire Claude message
},
meta: {
sentFrom: "cli"
}
};
}
logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
this.reliableEmitMessage({
sid: this.sessionId,
message: encrypted,
localId: body.type === "summary" ? `summary:${body.leafUuid}` : body.uuid
}, `claude-session:${body.type}`);
if (body.type === "summary" && "summary" in body && "leafUuid" in body) {
this.updateMetadata((metadata) => ({
...metadata,
summary: {
text: body.summary,
updatedAt: Date.now()
}
}));
}
}
sendCodexMessage(body) {
let content = {
role: "agent",
content: {
type: "codex",
data: body
},
meta: {
sentFrom: "cli"
}
};
const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
this.reliableEmitMessage({
sid: this.sessionId,
message: encrypted,
localId: node_crypto.randomUUID()
}, `codex:${body?.type ?? "unknown"}`);
}
/**
* Send a generic agent message to the session using ACP (Agent Communication Protocol) format.
* Works for any agent type (Gemini, Codex, Claude, etc.) - CLI normalizes to unified ACP format.
*
* @param provider - The agent provider sending the message (e.g., 'gemini', 'codex', 'claude')
* @param body - The message payload (type: 'message' | 'reasoning' | 'tool-call' | 'tool-result')
*/
/**
* Send a user message to the session, rendered as a chat bubble on the web client.
* Used to mirror terminal TUI input to the web session.
*/
sendUserChatMessage(text) {
const content = {
role: "user",
content: { type: "text", text },
meta: { sentFrom: "cli" }
};
const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
this.reliableEmitMessage({
sid: this.sessionId,
message: encrypted,
localId: node_crypto.randomUUID()
}, "user-chat");
}
sendAgentMessage(provider, body) {
let content = {
role: "agent",
content: {
type: "acp",
provider,
data: body
},
meta: {
sentFrom: "cli"
}
};
logger.debug(`[SOCKET] Sending ACP message from ${provider}:`, { type: body.type, hasMessage: "message" in body });
const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
this.reliableEmitMessage({
sid: this.sessionId,
message: encrypted,
localId: node_crypto.randomUUID()
}, `acp:${provider}:${body.type}`);
if (provider !== "consortium-code" && body.type === "message" && typeof body.message === "string") {
void this.autoDetectArtifactsFromText(provider, body.message);
}
if (body.type === "tool-call") {
const path = this.extractWritePath(body);
if (path) this.pendingWriteCalls.set(body.callId, path);
} else if (body.type === "tool-result") {
const callId = body.callId;
const path = this.pendingWriteCalls.get(callId);
if (path) {
this.pendingWriteCalls.delete(callId);
if (!body.isError) {
void this.promoteFileWriteArtifact(provider, path);
}
}
}
}
// callId -> written file path, captured on a write tool-call and consumed
// on the matching tool-result (when the file is on disk).
pendingWriteCalls = /* @__PURE__ */ new Map();
// Written file path -> artifact id, so re-writing the same file produces a
// new VERSION of one artifact instead of a fresh card each time.
fileArtifactIds = /* @__PURE__ */ new Map();
// Content hashes of blocks already promoted to artifacts this session, so a
// re-sent / streamed-then-finalized message doesn't create duplicates.
autoArtifactSeen = /* @__PURE__ */ new Set();
// Dedupe key for a promoted block: kind + sha256 of the trailing-trimmed
// content. Hashing the FULL normalized content (not length + first 64 chars)
// avoids both false-positive collisions between distinct same-length blocks
// and false-negatives between the explicit tool (raw arg) and the fallback
// (scanForArtifacts already strips trailing whitespace).
artifactDedupeKey(kind, content) {
const normalized = content.replace(/\s+$/, "");
return `${kind}:${node_crypto.createHash("sha256").update(normalized).digest("hex")}`;
}
/**
* Scan agent text for substantial fenced blocks and promote them to
* interactive artifacts — the safety net for agents that print a code
* block instead of calling create_artifact. Works for ALL agents: full-
* message agents (gemini/grok/pi/claude) are scanned per outgoing message
* from sendAgentMessage; streaming agents (consortium-code) call this once
* with the whole turn's text. Best-effort and fire-and-forget.
*
* Default ON; set CONSORTIUM_ARTIFACTS_AUTODETECT=0 to disable.
*/
async autoDetectArtifactsFromText(provider, text) {
if (process.env.CONSORTIUM_ARTIFACTS_AUTODETECT === "0") return;
if (!this.userContentPublicKey) return;
try {
const { scanForArtifacts } = await Promise.resolve().then(function () { return require('./scanForArtifacts-DBtmjd28.cjs'); });
const candidates = scanForArtifacts(text);
for (const c of candidates) {
const key = this.artifactDedupeKey(c.kind, c.content);
if (this.autoArtifactSeen.has(key)) continue;
this.autoArtifactSeen.add(key);
const { id } = await this.createLibraryArtifact({
title: c.title,
kind: c.kind,
content: c.content,
language: c.language
});
this.sendAgentMessage(provider, { type: "artifact-ref", artifactId: id, title: c.title, artifactKind: c.kind, versionHint: 1, id: node_crypto.randomUUID() });
}
} catch (err) {
logger.debug("[apiSession] auto artifact detection failed (non-fatal):", err);
}
}
// Extract a written file path from a write-style tool-call, or null if this
// isn't a file write. Gated on both a write-y tool name and a path arg so
// non-file tools (bash, search, etc.) are ignored.
extractWritePath(body) {
const name = (body.name ?? "").toLowerCase();
if (!/write|edit|create|patch|save|replace|notebook/.test(name)) return null;
const input = body.input;
if (!input || typeof input !== "object") return null;
const p = input.filePath ?? input.file_path ?? input.path ?? input.target_file ?? input.absolute_path;
return typeof p === "string" && p.length > 0 ? p : null;
}
// Extension -> artifact kind. Only PREVIEWABLE / deliverable kinds promote
// on file write (html/svg/mermaid/markdown/react) — promoting every .ts/.js
// a coding agent writes would spam cards during ordinary development.
fileArtifactKind(path$1) {
const ext = path.extname(path$1).slice(1).toLowerCase();
switch (ext) {
case "html":
case "htm":
return { kind: "html" };
case "svg":
return { kind: "svg" };
case "jsx":
return { kind: "react", language: "jsx" };
case "tsx":
return { kind: "react", language: "tsx" };
case "md":
case "markdown":
return { kind: "markdown" };
case "mmd":
case "mermaid":
return { kind: "mermaid" };
default:
return null;
}
}
/**
* Promote a written file to an interactive artifact: read it, infer the
* kind from its extension, and create (or, for a re-write of the same path,
* version) the artifact + drop a chat card. This is what makes coding
* agents behave like Claude artifacts — they write files, not fences.
*
* `content` may be passed when the caller already has it in hand (e.g.
* Claude's Write tool input, Codex add patches); otherwise the file is read
* from disk (it exists by the time the write tool-result fires). Best-effort
* and fire-and-forget. Honours CONSORTIUM_ARTIFACTS_AUTODETECT.
*/
async promoteFileWriteArtifact(provider, filePath, content) {
if (process.env.CONSORTIUM_ARTIFACTS_AUTODETECT === "0") return;
if (!this.userContentPublicKey) return;
try {
const kindInfo = this.fileArtifactKind(filePath);
if (!kindInfo) return;
logger.debug(`[apiSession] promoting file-write artifact: ${filePath} (kind=${kindInfo.kind}, provider=${provider})`);
let text = content;
if (text === void 0) {
const abs = path.isAbsolute(filePath) ? filePath : path.join(this.metadata?.path ?? "", filePath);
const { readFile } = await import('node:fs/promises');
text = await readFile(abs, "utf8");
}
if (!text || !text.trim()) return;
if (text.length > MAX_ARTIFACT_FILE_BYTES) return;
const title = path.basename(filePath);
const key = this.artifactDedupeKey(kindInfo.kind, text);
if (this.autoArtifactSeen.has(key)) return;
const existingId = this.fileArtifactIds.get(filePath);
if (existingId && this.artifactStates.has(existingId)) {
const res = await this.updateLibraryArtifact({ id: existingId, content: text });
this.autoArtifactSeen.add(key);
this.sendAgentMessage(provider, { type: "artifact-ref", artifactId: existingId, title, artifactKind: kindInfo.kind, versionHint: res.bodyVersion, id: node_crypto.randomUUID() });
} else {
const { id } = await this.createLibraryArtifact({ title, kind: kindInfo.kind, content: text, language: kindInfo.language });
this.fileArtifactIds.set(filePath, id);
this.sendAgentMessage(provider, { type: "artifact-ref", artifactId: id, title, artifactKind: kindInfo.kind, versionHint: 1, id: node_crypto.randomUUID() });
}
} catch (err) {
logger.debug("[apiSession] file-write artifact promotion failed (non-fatal):", err);
}
}
sendSessionEvent(event, id) {
let content = {
role: "agent",
content: {
id: id ?? node_crypto.randomUUID(),
type: "event",
data: event
}
};
const encrypted = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, content));
this.socket.emit("message", {
sid: this.sessionId,
message: encrypted
});
}
/**
* Emit a live token-stream delta over the ephemeral `session-stream`
* channel. Unlike `sendAgentMessage`, this is NOT persisted — the
* server relays it to clients currently viewing the session and drops
* it otherwise. The durable, authoritative message still flows through
* `sendAgentMessage` at turn end; this only powers the live "typing"
* feed. Deltas are best-effort (`volatile`): if the socket is
* congested a dropped delta is harmless because the final message
* reconciles the text.
*
* The `textDelta` is encrypted with the session key exactly like a
* durable message body, so the relay server never sees plaintext.
*
* @param provider agent id ('grok' | 'gemini' | 'codex' | 'claude' | 'pi' | …)
* @param streamId stable id for one assistant turn (answer+reasoning share it)
* @param channel 'answer' for response text, 'reasoning' for thinking
* @param seq monotonically increasing index within the stream (ordering)
* @param textDelta the incremental chunk (omit on the terminal frame)
* @param done true to signal the stream is finished (clears the buffer)
*/
sendStreamDelta(provider, streamId, channel, seq, textDelta, done = false) {
let data;
if (textDelta) {
data = encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, { textDelta }));
}
this.socket.volatile.emit("session-stream", {
sid: this.sessionId,
provider,
streamId,
channel,
seq,
done,
data
});
}
/**
* Optional provider for message-queue observability. When set, every
* keepAlive heartbeat carries `queued`/`queuedIds` so clients can render
* exact queue state (level-triggered: re-sent every ~2s on a volatile
* emit, so a dropped frame self-heals on the next tick — no ack contract).
* Old servers/clients simply ignore the extra fields.
*/
queueStatsProvider = null;
setQueueStatsProvider(provider) {
this.queueStatsProvider = provider;
}
/**
* Send a ping message to keep the connection alive
*/
keepAlive(thinking, mode) {
if (process.env.DEBUG) {
logger.debug(`[API] Sending keep alive message: ${thinking}`);
}
let stats = null;
try {
stats = this.queueStatsProvider?.() ?? null;
} catch {
}
this.socket.volatile.emit("session-alive", {
sid: this.sessionId,
time: Date.now(),
thinking,
mode,
...stats && { queued: stats.queued, queuedIds: stats.queuedIds.slice(0, 32) }
});
}
/**
* Send session death message
*/
sendSessionDeath() {
this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
}
/**
* Infers the LLM provider ID from a model name string.
*/
inferProvider(model) {
if (!model) return void 0;
if (/^claude-/i.test(model)) return "anthropic";
if (/^(gpt-|o[134]-|chatgpt-)/i.test(model)) return "openai";
if (/^gemini-/i.test(model)) return "google";
if (/^deepseek-/i.test(model)) return "deepseek";
if (/^grok/i.test(model)) return "xai";
return void 0;
}
/**
* Detects the usage mode based on environment variables (B-8).
*
* · 'managed' — the agent's traffic is rewritten through the Consortium
* proxy, so the SERVER already metered and billed this call and the
* report is supplementary analytics. This used to check only
* `ANTHROPIC_BASE_URL`, so a managed OpenAI or Google session was
* mislabeled 'wrapped' and got billed a SECOND time from the
* client-reported cost. All three vendor base-URL vars are checked now.
* · 'byok' — the daemon injected the user's OWN credential for this spawn
* (custom base URL and/or a vendor API-key env var it set). The user
* pays their vendor directly, so the server must not bill the wallet.
* · 'wrapped' — everything else; the server bills the client-reported cost.
*
* The BYOK signals are deliberately restricted to variables the DAEMON
* sets (`daemon/run.ts` — `CONSORTIUM_CODE_BYOK_BASE_URL` /
* `CONSORTIUM_CODE_BYOK_ENV_VAR` / `CONSORTIUM_CODE_BYOK_PROVIDER`). A bare
* `ANTHROPIC_API_KEY` inherited from the user's shell is NOT treated as
* BYOK: that would silently stop billing existing wrapped sessions, which
* is a revenue change, not an accounting fix.
*/
detectUsageMode() {
const managedBaseUrls = [
process.env.ANTHROPIC_BASE_URL,
process.env.OPENAI_BASE_URL,
process.env.GOOGLE_API_ENDPOINT
];
if (managedBaseUrls.some((u) => (u || "").includes("/v1/proxy/"))) return "managed";
if (process.env.CONSORTIUM_CODE_BYOK_BASE_URL) return "byok";
const byokEnvVar = process.env.CONSORTIUM_CODE_BYOK_ENV_VAR;
if (byokEnvVar && process.env[byokEnvVar]) return "byok";
return "wrapped";
}
/**
* Send usage data to the server
*
* `opts` (L4.2 — non-claude agents + sub-agent rollup):
* · `keyPrefix` — report-key namespace. Defaults to the legacy
* 'claude-session' so existing claude rows keep upserting under their
* historical keys; codex/gemini/grok pass their agent name so the
* ledger can tell who reported.
* · `providerId` — explicit provider when the model id alone can't infer
* it (e.g. codex running its default model: the CLI never learns the
* model string, but the vendor is definitionally openai).
* · `zeroCost` — report tokens only, cost 0. REQUIRED for the non-claude
* agent self-reports: `calculateCost` falls back to Anthropic Sonnet
* pricing for unknown model ids, and the server debits the wallet for
* any wrapped-mode report with cost > 0 — a codex/gemini/grok session
* running on the user's OWN vendor account must never create a wallet
* charge. Vendor-true cost adoption is lane L4.4's scope, not ours.
*/
sendUsageData(usage, model, turnKey, opts) {
const totalTokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
const costs = opts?.zeroCost ? { total: 0, input: 0, output: 0 } : calculateCost(usage, model);
const usageMode = this.detectUsageMode();
const usageReport = {
key: `${opts?.keyPrefix ?? "claude-session"}:${turnKey ?? node_crypto.randomUUID()}`,
sessionId: this.sessionId,
tokens: {
total: totalTokens,
input: usage.input_tokens,
output: usage.output_tokens,
// The server aggregator at /v1/sessions/:id/usage reads
// these as camelCase (matching what proxyBilling writes for
// managed-mode rows). Snake-case keys here would silently
// sum to zero in the cumulative cache totals.
cacheWrite: usage.cache_creation_input_tokens || 0,
cacheRead: usage.cache_read_input_tokens || 0,
// Snake-case duplicates kept for any older readers that
// expect them. Cheap to ship; the server picks one.
cache_creation: usage.cache_creation_input_tokens || 0,
cache_read: usage.cache_read_input_tokens || 0
},
cost: {
total: costs.total,
input: costs.input,
output: costs.output
},
providerId: this.inferProvider(model) ?? opts?.providerId,
usageMode,
modelId: model,
// D0 provenance: these counts are the AGENT's own report (Claude
// Code's `result.usage`), i.e. vendor-computed but relayed by the
// agent — not measured by our proxy. The server stores this on
// `UsageReport.data.source` so the UI can label it honestly
// instead of presenting every number as proxy-measured truth.
source: "agent-reported"
};
logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
this.socket.emit("usage-report", usageReport);
}
/**
* Update session metadata
* @param handler - Handler function that returns the updated metadata
*/
updateMetadata(handler) {
this.metadataLock.inLock(async () => {
await backoff(async () => {
let updated = handler(this.metadata);
const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) });
if (answer.result === "success") {
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
this.metadataVersion = answer.version;
} else if (answer.result === "version-mismatch") {
if (answer.version > this.metadataVersion) {
this.metadataVersion = answer.version;
this.metadata = decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.metadata));
}
throw new Error("Metadata version mismatch");
} else if (answer.result === "error") ;
});
});
}
/**
* Update session agent state
* @param handler - Handler function that returns the updated agent state
*/
updateAgentState(handler) {
logger.debugLargeJson("Updating agent state", this.agentState);
this.agentStateLock.inLock(async () => {
await backoff(async () => {
let updated = handler(this.agentState || {});
const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(this.encryptionKey, this.encryptionVariant, updated)) : null });
if (answer.result === "success") {
this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
this.agentStateVersion = answer.version;
logger.debug("Agent state updated", this.agentState);
} else if (answer.result === "version-mismatch") {
if (answer.version > this.agentStateVersion) {
this.agentStateVersion = answer.version;
this.agentState = answer.agentState ? decrypt(this.encryptionKey, this.encryptionVariant, decodeBase64(answer.agentState)) : null;
}
throw new Error("Agent state version mismatch");
} else if (answer.result === "error") ;
});
});
}
/**
* Wait for socket buffer to flush
*/
async flush() {
if (!this.socket.connected) {
return;
}
return new Promise((resolve) => {
this.socket.emit("ping", () => {
resolve();
});
setTimeout(() => {
resolve();
}, 1e4);
});
}
async close() {
logger.debug("[API] socket.close() called");
this.destroyed = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.flushWatchdog) {
clearTimeout(this.flushWatchdog);
this.flushWatchdog = null;
}
this.socket.close();
}
/**
* Schedule a manual reconnect after a connect_error. Mirrors
* ApiMachineClient.scheduleManualReconnect; see comment there for the
* Socket.IO middleware-rejection rationale. Session sockets don't
* carry their own refresh path — the session token comes from the
* parent process, so the only thing we can do here is retry with
* the existing token until it works (or until close() is called).
*/
scheduleManualReconnect() {
if (this.destroyed) return;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.manualReconnectAttempts++;
const delay = Math.min(1e3 * Math.pow(2, this.manualReconnectAttempts - 1), 3e4) + Math.floor(Math.random() * 1e3);
logger.debug(`[API] Scheduling manual reconnect attempt #${this.manualReconnectAttempts} in ${delay}ms`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
if (this.destroyed) return;
if (this.socket.connected) return;
logger.debug(`[API] Manual reconnect attempt #${this.manualReconnectAttempts} firing`);
this.socket.connect();
}, delay);
}
}
function finiteNumber(v) {
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
}
function usageFromCodexLastTokenUsage(last) {
if (!last || typeof last !== "object") return null;
const o = last;
const input = finiteNumber(o.input_tokens);
const output = finiteNumber(o.output_tokens);
if (input === void 0 || output === void 0) return null;
const cached = finiteNumber(o.cached_input_tokens) ?? 0;
return {
input_tokens: Math.max(0, input - cached),
output_tokens: output,
cache_creation_input_tokens: 0,
cache_read_input_tokens: Math.max(0, cached)
};
}
function usageFromAgentTokenCount(raw) {
if (!raw || typeof raw !== "object") return null;
const o = raw;
const info = o.info;
if (info && typeof info === "object") {
const fromCodex = usageFromCodexLastTokenUsage(info.last_token_usage);
if (fromCodex) return fromCodex;
}
{
const input = finiteNumber(o.input_tokens);
const output = finiteNumber(o.output_tokens);
if (input !== void 0 && output !== void 0) {
const cacheRead = finiteNumber(o.cache_read_input_tokens);
const cacheWrite = finiteNumber(o.cache_creation_input_tokens);
if (cacheRead !== void 0 || cacheWrite !== void 0) {
return {
input_tokens: input,
output_tokens: output,
cache_creation_input_tokens: cacheWrite ?? 0,
cache_read_input_tokens: cacheRead ?? 0
};
}
const cached = finiteNumber(o.cached_input_tokens) ?? 0;
return {
input_tokens: Math.max(0, input - cached),
output_tokens: output,
cache_creation_input_tokens: 0,
cache_read_input_tokens: Math.max(0, cached)
};
}
}
for (const candidate of [o, o.usage, o.usageMetadata]) {
if (!candidate || typeof candidate !== "object") continue;
const m = candidate;
const prompt = finiteNumber(m.promptTokenCount);
const output = finiteNumber(m.candidatesTokenCount);
if (prompt === void 0 || output === void 0) continue;
const cached = finiteNumber(m.cachedContentTokenCount) ?? 0;
const thoughts = finiteNumber(m.thoughtsTokenCount) ?? 0;
return {
input_tokens: Math.max(0, prompt - cached),
output_tokens: output + thoughts,
cache_creation_input_tokens: 0,
cache_read_input_tokens: Math.max(0, cached)
};
}
return null;
}
const require$1 = node_module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('types-BYXHizYk.cjs', document.baseURI).href)));
function collectDiagnostics(binary, args) {
const resolvedPath = fs.existsSync(binary) ? binary : null;
return {
binary,
resolvedPath,
args,
platform: process.platform,
arch: process.arch,
path: process.env.PATH || "(unset)",
execPath: process.execPath
};
}
class SpawnDiagnosticsError extends Error {
info;
cause;
constructor(original, info) {
const origMsg = original instanceof Error ? original.message : String(original);
super(
`Failed to spawn process.
binary: ${info.binary}
absolute path: ${info.resolvedPath ?? "<not found on disk>"}
args: ${JSON.stringify(info.args)}
platform: ${info.platform} (${info.arch})
execPath: ${info.execPath}
PATH: ${info.path}
underlying error: ${origMsg}`
);
this.name = "SpawnDiagnosticsError";
this.info = info;
this.cause = original;
}
}
function spawnPtyWithDiagnostics(binary, args, options) {
const info = collectDiagnostics(binary, args);
logger.debug(`[spawnPty] ${binary} ${args.join(" ")} (path=${info.resolvedPath ?? "NOT FOUND"})`);
try {
const nodePty = require$1("node-pty");
return nodePty.spawn(binary, args, options);
} catch (err) {
throw new SpawnDiagnosticsError(err, info);
}
}
function isPosixSpawnpError(err) {
let cur = err;
for (let i = 0; i < 5 && cur; i++) {
if (cur instanceof Error && typeof cur.message === "string" && /posix_spawnp failed/i.test(cur.message)) {
return true;
}
if (cur instanceof SpawnDiagnosticsError) return true;
cur = cur?.cause;
}
return false;
}
function formatPosixSpawnpGuidance(err, packageName) {
let info;
if (err instanceof SpawnDiagnosticsError) info = err.info;
const lines = [];
lines.push("");
lines.push("A subprocess spawn failed. This usually means a binary is missing,");
lines.push("the architecture of an installed native module does not match your");
lines.push("Node runtime, or your CLI install is incomplete.");
lines.push("");
if (info) {
lines.push(` binary tried: ${info.binary}`);
lines.push(` absolute path: ${info.resolvedPath ?? "<not found>"}`);
lines.push(` platform/arch: ${info.platform} (${info.arch})`);
lines.push(` node: ${info.execPath}`);
lines.push("");
}
lines.push("Try, in order:");
lines.push(` 1. consortium doctor --fix (clean launchagents, stale daemons, caches)`);
lines.push(` 2. npm uninstall -g ${packageName} && npm install -g ${packageName}@latest`);
lines.push(` 3. Re-open your terminal (so you inherit a fresh PATH)`);
lines.push("");
return lines.join("\n");
}
var spawnDiagnostics = /*#__PURE__*/Object.freeze({
__proto__: null,
SpawnDiagnosticsError: SpawnDiagnosticsError,
formatPosixSpawnpGuidance: formatPosixSpawnpGuidance,
isPosixSpawnpError: isPosixSpawnpError,
spawnPtyWithDiagnostics: spawnPtyWithDiagnostics
});
function platformKey() {
const plat = process.platform;
const arch = process.arch;
if (plat === "linux" && arch === "x64") return "linux-x64";
if (plat === "linux" && arch === "arm64") return "linux-arm64";
if (plat === "darwin" && arch === "x64") return "darwin-x64";
if (plat === "darwin" && arch === "arm64") return "darwin-arm64";
if (plat === "win32" && arch === "x64") return "win32-x64";
if (plat === "win32" && arch === "arm64") return "win32-arm64";
return `${plat}-${arch}`;
}
function resolveTmuxBinary() {
if (process.env.CONSORTIUM_MUX_PATH) {
return process.env.CONSORTIUM_MUX_PATH;
}
try {
const bundled = require.resolve(
`consortium-mux-tmux-${platformKey()}/bin/tmux`
);
return bundled;
} catch {
}
return "tmux";
}
function resolveZellijBinary() {
const exe = process.platform === "win32" ? "zellij.exe" : "zellij";
const env = process.env.CONSORTIUM_MUX_PATH;
if (env && (env.endsWith("zellij") || env.endsWith("zellij.exe"))) {
return env;
}
try {
const bundled = require.resolve(
`consortium-mux-zellij-${platformKey()}/bin/${exe}`
);
return bundled;
} catch {
}
return "zellij";
}
const SENSITIVE_SUBSTRINGS = [
"TOKEN",
"SECRET",
"PASSWORD",
"API_KEY",
"APIKEY",
"PRIVATE_KEY",
"CREDENTIAL",
"AUTH_TOKEN"
];
const SENSITIVE_EXACT = /* @__PURE__ */ new Set(["DATABASE_URL", "REDIS_URL", "CODEX_HOME"]);
function filterEnv(env) {
const out = {};
for (const [key, value] of Object.entries(env)) {
if (value === void 0) continue;
const upper = key.toUpperCase();
if (SENSITIVE_EXACT.has(upper)) continue;
if (SENSITIVE_SUBSTRINGS.some((p) => upper.includes(p))) continue;
out[key] = value;
}
return out;
}
function terminalEnv(base = process.env) {
const env = filterEnv(base);
if (!env.COLORTERM) env.COLORTERM = "truecolor";
if (!env.LANG && !env.LC_ALL && !env.LC_CTYPE && process.platform !== "win32") {
env.LANG = process.platform === "darwin" ? "en_US.UTF-8" : "C.UTF-8";
}
return env;
}
const COALESCE_MS = 16;
const IDLE_GAP_MS = 16;
function createOutputBatcher(onFlush) {
let buffer = [];
let timer = null;
let lastFlush = 0;
const flush = () => {
timer = null;
if (buffer.length === 0) return;
const combined = buffer.length === 1 ? buffer[0] : Buffer.concat(buffer);
buffer = [];
lastFlush = Date.now();
onFlush(combined);
};
return {
push(chunk) {
buffer.push(chunk);
if (timer) return;
const delay = Date.now() - lastFlush >= IDLE_GAP_MS ? 0 : COALESCE_MS;
timer = setTimeout(flush, delay);
},
flushNow() {
if (timer) {
clearTimeout(timer);
timer = null;
}
flush();
},
dispose() {
if (timer) {
clearTimeout(timer);
timer = null;
}
buffer = [];
}
};
}
const execFile$1 = node_util.promisify(node_child_process.execFile);
const SESSION_PREFIX$1 = "consortium-";
function sessionName$1(id) {
return SESSION_PREFIX$1 + id.replace(/[^a-zA-Z0-9_-]/g, "_");
}
function buildHostedNewSessionArgs(sessionName2, opts) {
const args = [
"new-session",
"-d",
"-P",
"-F",
"#{pane_pid}",
"-s",
sessionName2,
"-x",
String(opts.cols),
"-y",
String(opts.rows),
"-c",
opts.cwd
];
for (const [key, value] of Object.entries(opts.env)) {
if (value === void 0 || value === null) continue;
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
args.push("-e", `${key}=${value}`);
}
args.push("--", ...opts.command);
return args;
}
class TmuxBackend {
mode = "tmux";
/** tmux binary path (resolved once at construction). */
bin;
/** Whether the binary probe succeeded. */
available;
/** Map of terminal-id → map of viewer-id → ViewerState */
viewers = /* @__PURE__ */ new Map();
/** Weak set tracking known ids (for has()). Populated on create()/attach(). */
knownIds = /* @__PURE__ */ new Set();
constructor(bin, available) {
this.bin = bin;
this.available = available;
}
/** Factory: probes the binary and returns a ready (or unavailable) backend. */
static async init() {
const bin = resolveTmuxBinary();
try {
await execFile$1(bin, ["-V"], { timeout: 4e3 });
logger.debug(`[TMUX] Backend initialised with binary: ${bin}`);
await execFile$1(bin, ["set-option", "-g", "aggressive-resize", "on"], { timeout: 4e3 }).catch(() => {
});
return new TmuxBackend(bin, true);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.debug(`[TMUX] Binary probe failed (${bin}): ${msg} \u2014 backend marked unavailable`);
return new TmuxBackend(bin, false);
}
}
async create(opts) {
if (!this.available) return { ok: false, error: "tmux not available" };
const name = sessionName$1(opts.id);
const shell = opts.shell ?? process.env.SHELL ?? (process.platform === "win32" ? "cmd.exe" : "/bin/bash");
const cwd = opts.cwd ?? process.env.HOME ?? "/tmp";
const env = terminalEnv({ ...process.env, ...opts.env ?? {} });
const envArgs = [];
for (const [k, v] of Object.entries(env)) {
envArgs.push("-e", `${k}=${v}`);
}
try {
await execFile$1(
this.bin,
[
"new-session",
"-d",
"-s",
name,
"-x",
String(opts.cols),
"-y",
String(opts.rows),
...envArgs,
shell
],
{ cwd, timeout: 1e4 }
);
await execFile$1(this.bin, ["set-option", "-t", name, "aggressive-resize", "on"], { timeout: 4e3 }).catch(() => {
});
this.knownIds.add(opts.id);
this.viewers.set(opts.id, /* @__PURE__ */ new Map());
logger.debug(`[TMUX] Created session ${name} (${opts.cols}x${opts.rows})`);
return { ok: true };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.debug(`[TMUX] create failed for ${name}: ${msg}`);
return { ok: false, error: msg };
}
}
/**
* Host a program (an agent session) as the pane program of a new tmux
* session. The program is exec'd directly — no shell — so the pane dies
* with it and its exit is never masked by a lingering prompt.
*
* Tuned for a TUI that is watched from the app, not for a shell:
* - `status off`: the raw view must be the agent's screen, nothing else.
* - `escape-time 0` (server option): tmux's default 500 ms ESC delay makes
* every Esc keypress — Claude's interrupt — feel broken.
* - `window-size latest` + `aggressive-resize on`: the pane follows the
* device that last touched it instead of the smallest attached client.
* - Env is passed UNFILTERED via `-e`: the hosted program is the agent and
* needs its credentials (viewers only see the rendered screen).
*/
async createHosted(opts) {
if (!this.available) return { ok: false, error: "tmux not available" };
if (opts.command.length === 0) return { ok: false, error: "empty command" };
const name = sessionName$1(opts.id);
const args = buildHostedNewSessionArgs(name, opts);
try {
const { stdout } = await execFile$1(this.bin, args, { cwd: opts.cwd, timeout: 1e4 });
const pid = parseInt(stdout.trim(), 10);
if (!Number.isFinite(pid)) {
return { ok: false, error: `tmux did not report a pane pid (got "${stdout.trim()}")` };
}
const opt = (scope, key, value) => execFile$1(this.bin, ["set-option", ...scope, key, value], { timeout: 4e3 }).catch(() => {
});
await Promise.all([
opt(["-t", name], "status", "off"),
opt(["-t", name], "aggressive-resize", "on"),
opt(["-t", name], "window-size", "latest"),
opt(["-t", name], "history-limit", "50000"),
opt(["-s"], "escape-time", "0")
]);
this.knownIds.add(opts.id);
this.viewers.set(opts.id, /* @__PURE__ */ new Map());
logger.debug(`[TMUX] Hosted ${name} pid=${pid} (${opts.cols}x${opts.rows}) cmd=${opts.command[0]}`);
return { ok: true, pid };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.debug(`[TMUX] createHosted failed for ${name}: ${msg}`);
return { ok: false, error: msg };
}
}
async attach(id, viewerId, cols, rows, onData, onExit) {
if (!this.available) return { ok: false, error: "tmux not available" };
const name = sessionName$1(id);
try {
await execFile$1(this.bin, ["has-session", "-t", name], { timeout: 4e3 });
} catch {
return { ok: false, error: `No tmux session: ${name}` };
}
let replay;
try {
const { stdout } = await execFile$1(
this.bin,
["capture-pane", "-peJ", "-S", "-10000", "-t", name],
{ timeout: 5e3, maxBuffer: 4 * 1024 * 1024 }
);
if (stdout) replay = Buffer.from(stdout, "utf-8");
} catch (err) {
logger.debug(`[TMUX] capture-pane failed for ${name}: ${err instanceof Error ? err.message : err}`);
}
let pty;
try {
const nodePty = await import('node-pty');
pty = nodePty.spawn(this.bin, ["attach", "-t", name], {
name: "xterm-256color",
cols,
rows,
cwd: process.env.HOME ?? "/tmp",
env: terminalEnv(process.env),
// Raw byte stream — see terminals/index.ts openPty for rationale.
encoding: null
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { ok: false, error: `node-pty spawn failed: ${msg}` };
}
this.knownIds.add(id);
const viewerMap = this.viewers.get(id) ?? /* @__PURE__ */ new Map();
this.viewers.set(id, viewerMap);
const existing = viewerMap.get(viewerId);
if (existing) {
existing.replacing = true;
existing.batcher.dispose();
try {
existing.pty.kill();
} catch {
}
viewerMap.delete(viewerId);
logger.debug(`[TMUX] Replaced existing viewer ${viewerId} on session ${name}`);
}
const state = { pty, batcher: createOutputBatcher(onData), replacing: false };
viewerMap.set(viewerId, state);
pty.onData((raw) => {
state.batcher.push(Buffer.isBuffer(raw) ? raw : Buffer.from(raw, "utf-8"));
});
pty.onExit(({ exitCode }) => {
if (viewerMap.get(viewerId) === state) {
viewerMap.delete(viewerId);
}
if (state.replacing) {
state.batcher.dispose();
logger.debug(`[TMUX] Replaced viewer ${viewerId} pty exited (suppressed)`);
return;
}
state.batcher.flushNow();
logger.debug(`[TMUX] Viewer ${viewerId} detached from ${name} (exit ${exitCode})`);
onExit(exitCode ?? 0);
});
logger.debug(`[TMUX] Attached viewer ${viewerId} to ${name} (${cols}x${rows})`);
return { ok: true, replay };
}
detach(id, viewerId) {
const viewerMap = this.viewers.get(id);
if (!viewerMap) return;
const state = viewerMap.get(viewerId);
if (!state) return;
state.batcher.dispose();
try {
state.pty.kill();
} catch {
}
viewerMap.delete(viewerId);
logger.debug(`[TMUX] Detached viewer ${viewerId} from session ${id}`);
}
write(id, data) {
const viewerMap = this.viewers.get(id);
if (!viewerMap || viewerMap.size === 0) return false;
const [state] = viewerMap.values();
state.pty.write(data);
return true;
}
resize(id, viewerId, cols, rows) {
const viewerMap = this.viewers.get(id);
if (!viewerMap) return false;
const state = viewerMap.get(viewerId);
if (!state) return false;
state.pty.resize(cols, rows);
execFile$1(this.bin, ["refresh-client", "-t", sessionName$1(id), "-C", `${cols}x${rows}`], { timeout: 2e3 }).catch(() => {
});
return true;
}
async destroy(id) {
const name = sessionName$1(id);
const viewerMap = this.viewers.get(id);
if (viewerMap) {
for (const [vid] of viewerMap) this.detach(id, vid);
this.viewers.delete(id);
}
this.knownIds.delete(id);
try {
await execFile$1(this.bin, ["kill-session", "-t", name], { timeout: 5e3 });
logger.debug(`[TMUX] Destroyed session ${name}`);
} catch (err) {
logger.debug(`[TMUX] kill-session ${name}: ${err instanceof Error ? err.message : err}`);
}
}
async list() {
if (!this.available) return [];
try {
const { stdout } = await execFile$1(
this.bin,
["list-sessions", "-F", "#{session_name}|#{session_created}|#{?session_attached,1,0}"],
{ timeout: 5e3 }
);
const descriptors = [];
for (const line of stdout.split("\n")) {
const trimmed = line.trim();
if (!trimmed.startsWith(SESSION_PREFIX$1)) continue;
const parts = trimmed.split("|");
if (parts.length < 3) continue;
const rawName = parts[0];
const createdAt = parseInt(parts[1] ?? "0", 10) * 1e3;
const id = rawName.slice(SESSION_PREFIX$1.length);
descriptors.push({
id,
mode: "tmux",
status: "running",
createdAt: isNaN(createdAt) ? 0 : createdAt
});
}
return descriptors;
} catch {
return [];
}
}
has(id) {
return this.knownIds.has(id);
}
}
const execFile = node_util.promisify(node_child_process.execFile);
const SESSION_PREFIX = "consortium-";
const SESSION_INIT_GRACE_MS = 800;
function sessionName(id) {
return SESSION_PREFIX + id.replace(/[^a-zA-Z0-9_-]/g, "_");
}
class ZellijBackend {
mode = "zellij";
bin;
available;
sessions = /* @__PURE__ */ new Map();
knownIds = /* @__PURE__ */ new Set();
constructor(bin, available) {
this.bin = bin;
this.available = available;
}
static async init() {
const bin = resolveZellijBinary();
try {
await execFile(bin, ["--version"], { timeout: 4e3 });
logger.debug(`[ZELLIJ] Backend initialised with binary: ${bin}`);
return new ZellijBackend(bin, true);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.debug(`[ZELLIJ] Binary probe failed (${bin}): ${msg} \u2014 backend marked unavailable`);
return new ZellijBackend(bin, false);
}
}
async create(opts) {
if (!this.available) return { ok: false, error: "zellij not available" };
if (this.sessions.has(opts.id)) return { ok: false, error: "already exists" };
const name = sessionName(opts.id);
const cwd = opts.cwd ?? process.env.HOME ?? process.env.USERPROFILE ?? os.tmpdir();
const env = terminalEnv({ ...process.env, ...opts.env ?? {} });
let host;
try {
const nodePty = await import('node-pty');
host = nodePty.spawn(this.bin, ["--session", name], {
name: "xterm-256color",
cols: opts.cols,
rows: opts.rows,
cwd,
env
});
host.onData(() => {
});
host.onExit(() => {
logger.debug(`[ZELLIJ] Host pty exited for session ${name}`);
this.sessions.delete(opts.id);
this.knownIds.delete(opts.id);
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { ok: false, error: `node-pty spawn failed: ${msg}` };
}
await new Promise((resolve) => setTimeout(resolve, SESSION_INIT_GRACE_MS));
this.sessions.set(opts.id, { host, viewers: /* @__PURE__ */ new Map() });
this.knownIds.add(opts.id);
logger.debug(`[ZELLIJ] Created session ${name} (${opts.cols}x${opts.rows})`);
return { ok: true };
}
async attach(id, viewerId, cols, rows, onData, onExit) {
if (!this.available) return { ok: false, error: "zellij not available" };
const session = this.sessions.get(id);
if (!session) return { ok: false, error: `No zellij session: ${sessionName(id)}` };
const name = sessionName(id);
let replay;
let dumpDir;
try {
dumpDir = await fs$3.mkdtemp(path.join(os.tmpdir(), "consortium-zellij-"));
const dumpPath = path.join(dumpDir, "screen.txt");
await execFile(
this.bin,
["--session", name, "action", "dump-screen", dumpPath],
{ timeout: 4e3 }
);
replay = await fs$3.readFile(dumpPath);
await fs$3.unlink(dumpPath).catch(() => {
});
await fs$3.rmdir(dumpDir).catch(() => {
});
} catch (err) {
logger.debug(`[ZELLIJ] dump-screen failed for ${name}: ${err instanceof Error ? err.message : err}`);
if (dumpDir) await fs$3.rmdir(dumpDir).catch(() => {
});
}
let pty;
try {
const nodePty = await import('node-pty');
pty = nodePty.spawn(this.bin, ["--session", name, "attach"], {
name: "xterm-256color",
cols,
rows,
cwd: process.env.HOME ?? process.env.USERPROFILE ?? os.tmpdir(),
env: terminalEnv(process.env),
// Raw byte stream — see terminals/index.ts openPty for rationale.
encoding: null
});
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { ok: false, error: `node-pty spawn failed: ${msg}` };
}
this.knownIds.add(id);
const state = { pty, batcher: createOutputBatcher(onData) };
session.viewers.set(viewerId, state);
pty.onData((raw) => {
state.batcher.push(Buffer.isBuffer(raw) ? raw : Buffer.from(raw, "utf-8"));
});
pty.onExit(({ exitCode }) => {
state.batcher.flushNow();
session.viewers.delete(viewerId);
logger.debug(`[ZELLIJ] Viewer ${viewerId} detached from ${name} (exit ${exitCode})`);
onExit(exitCode ?? 0);
});
logger.debug(`[ZELLIJ] Attached viewer ${viewerId} to ${name} (${cols}x${rows})`);
return { ok: true, replay };
}
detach(id, viewerId) {
const session = this.sessions.get(id);
if (!session) return;
const state = session.viewers.get(viewerId);
if (!state) return;
state.batcher.dispose();
try {
state.pty.kill();
} catch {
}
session.viewers.delete(viewerId);
logger.debug(`[ZELLIJ] Detached viewer ${viewerId} from session ${id}`);
}
write(id, data) {
const session = this.sessions.get(id);
if (!session || session.viewers.size === 0) return false;
const [state] = session.viewers.values();
state.pty.write(data);
return true;
}
resize(id, viewerId, cols, rows) {
const session = this.sessions.get(id);
if (!session) return false;
const state = session.viewers.get(viewerId);
if (!state) return false;
state.pty.resize(cols, rows);
return true;
}
async destroy(id) {
const session = this.sessions.get(id);
const name = sessionName(id);
if (session) {
for (const [vid] of session.viewers) this.detach(id, vid);
try {
session.host.kill();
} catch {
}
this.sessions.delete(id);
}
this.knownIds.delete(id);
try {
await execFile(this.bin, ["kill-session", name], { timeout: 5e3 });
logger.debug(`[ZELLIJ] Destroyed session ${name}`);
} catch (err) {
logger.debug(`[ZELLIJ] kill-session ${name}: ${err instanceof Error ? err.message : err}`);
}
}
async list() {
if (!this.available) return [];
try {
const { stdout } = await execFile(
this.bin,
["list-sessions", "--no-formatting"],
{ timeout: 5e3 }
);
const descriptors = [];
for (const line of stdout.split("\n")) {
const trimmed = line.trim();
if (!trimmed.startsWith(SESSION_PREFIX)) continue;
const nameMatch = trimmed.match(/^(\S+)/);
if (!nameMatch) continue;
const rawName = nameMatch[1];
const id = rawName.slice(SESSION_PREFIX.length);
const exited = /EXITED/i.test(trimmed);
descriptors.push({
id,
mode: "zellij",
status: exited ? "exited" : "running",
createdAt: 0
// zellij doesn't expose creation epoch in list-sessions
});
}
return descriptors;
} catch {
return [];
}
}
has(id) {
return this.knownIds.has(id);
}
}
const REPLAY_LIMIT_BYTES = 256 * 1024;
class ExternalBackend {
mode = "external";
terminals = /* @__PURE__ */ new Map();
/**
* A session process has connected and is mirroring its PTY.
* Returns a handle the socket layer drives; calling it again for the same
* id replaces the registration (the session reconnected).
*/
register(id, write) {
const existing = this.terminals.get(id);
const terminal = {
write,
replay: existing?.replay ?? Buffer.alloc(0),
createdAt: existing?.createdAt ?? Date.now(),
// Keep viewers across a reconnect: the app is attached to the
// session, not to this particular socket.
viewers: existing?.viewers ?? /* @__PURE__ */ new Map()
};
this.terminals.set(id, terminal);
logger.debug(`[EXTERNAL] Registered mirrored terminal ${id} (${terminal.viewers.size} viewer(s) waiting)`);
return {
pushOutput: (chunk) => {
const current = this.terminals.get(id);
if (current !== terminal) return;
const combined = terminal.replay.length === 0 ? Buffer.from(chunk) : Buffer.concat([terminal.replay, chunk]);
terminal.replay = combined.length > REPLAY_LIMIT_BYTES ? combined.subarray(combined.length - REPLAY_LIMIT_BYTES) : combined;
for (const viewer of terminal.viewers.values()) {
viewer.onData(chunk);
}
},
close: (code) => {
const current = this.terminals.get(id);
if (current !== terminal) return;
for (const viewer of terminal.viewers.values()) {
viewer.onExit(code);
}
this.terminals.delete(id);
logger.debug(`[EXTERNAL] Mirrored terminal ${id} ended (code ${code})`);
}
};
}
async create() {
return { ok: false, error: "External terminals cannot be created by the daemon" };
}
async attach(id, viewerId, _cols, _rows, onData, onExit) {
const terminal = this.terminals.get(id);
if (!terminal) return { ok: false, error: `No mirrored terminal: ${id}` };
terminal.viewers.set(viewerId, { onData, onExit });
logger.debug(`[EXTERNAL] Viewer ${viewerId} attached to ${id}`);
return { ok: true, replay: terminal.replay.length > 0 ? terminal.replay : void 0 };
}
/**
* No createHosted here on purpose: TerminalManager selects the hosting
* backend by which one implements it, so advertising a version that always
* fails would shadow tmux and break daemon-spawned session hosting.
*/
detach(id, viewerId) {
const terminal = this.terminals.get(id);
if (!terminal) return;
terminal.viewers.delete(viewerId);
logger.debug(`[EXTERNAL] Viewer ${viewerId} detached from ${id}`);
}
write(id, data) {
const terminal = this.terminals.get(id);
if (!terminal) return false;
terminal.write(data);
return true;
}
/**
* Deliberately a no-op — see the class comment. Returns true so callers
* treat the terminal as found and do not fall through to the ephemeral
* pty path with the same id.
*/
resize(id, _viewerId, _cols, _rows) {
return this.terminals.has(id);
}
async destroy(id) {
const terminal = this.terminals.get(id);
if (!terminal) return;
terminal.viewers.clear();
logger.debug(`[EXTERNAL] Dropped viewers for mirrored terminal ${id} (session keeps running)`);
}
async list() {
return [...this.terminals.entries()].map(([id, t]) => ({
id,
mode: "external",
status: "running",
createdAt: t.createdAt
}));
}
has(id) {
return this.terminals.has(id);
}
}
const MAX_TERMINALS = 20;
const HOSTED_SESSION_TERMINAL_PREFIX = "session-";
function isHostedSessionTerminalId(id) {
return id.startsWith(HOSTED_SESSION_TERMINAL_PREFIX);
}
class TerminalManager {
backends = {};
/** Ephemeral sessions: back-compat with existing pty:open flow */
ephemeralPtys = /* @__PURE__ */ new Map();
/** Tracks which ids are managed by a persistent backend */
persistentIds = /* @__PURE__ */ new Set();
/** @internal — call init() instead */
constructor() {
}
/** Async factory: initialises all available backends. */
static async init() {
const mgr = new TerminalManager();
const [tmux, zellij] = await Promise.all([TmuxBackend.init(), ZellijBackend.init()]);
mgr.backends.tmux = tmux;
mgr.backends.zellij = zellij;
mgr.backends.external = new ExternalBackend();
logger.debug("[TerminalManager] Initialised. Capabilities: " + JSON.stringify(mgr.getCapabilities()));
return mgr;
}
/** Returns which persistent backends are functional (binary present and probe succeeded). */
getCapabilities() {
const tmux = this.backends.tmux;
const zellij = this.backends.zellij;
return {
tmux: !!tmux,
zellij: !!zellij
};
}
/** Pick a sensible default backend for the host platform. */
defaultMode() {
const caps = this.getCapabilities();
if (process.platform === "win32") {
if (caps.zellij) return "zellij";
if (caps.tmux) return "tmux";
} else {
if (caps.tmux) return "tmux";
if (caps.zellij) return "zellij";
}
return "ephemeral";
}
totalCount() {
return this.ephemeralPtys.size + this.persistentIds.size;
}
pickBackend(requestedMode) {
if (requestedMode === "ephemeral") return null;
return this.backends[requestedMode] ?? null;
}
// -------------------------------------------------------------------------
// Hosted agent sessions
// -------------------------------------------------------------------------
/** Registry for terminals mirrored by other processes (see ExternalBackend). */
externalBackend() {
return this.backends.external;
}
/** Which backend can host agent sessions on this machine, if any. */
hostingMode() {
for (const backend of Object.values(this.backends)) {
if (backend?.createHosted) return backend.mode;
}
return null;
}
/**
* Run a program inside a multiplexer session so its TUI is attachable
* through the ordinary pty:open { persistentTerminalId } flow. Hosted
* sessions do NOT count against MAX_TERMINALS — that cap protects against
* runaway user terminals, and an agent session is bounded by the session
* spawner instead.
*/
async hostSession(opts) {
if (!isHostedSessionTerminalId(opts.id)) {
return { ok: false, error: `hosted terminal id must start with "${HOSTED_SESSION_TERMINAL_PREFIX}"` };
}
for (const backend of Object.values(this.backends)) {
if (!backend?.createHosted) continue;
const result = await backend.createHosted(opts);
return { ...result, mode: backend.mode };
}
return { ok: false, error: "No multiplexer backend can host sessions on this machine" };
}
// -------------------------------------------------------------------------
// Persistent terminal API
// -------------------------------------------------------------------------
async createTerminal(mode, opts) {
if (mode === "ephemeral") {
return { ok: false, mode, error: "Use openPty() for ephemeral terminals" };
}
if (this.totalCount() >= MAX_TERMINALS) {
return { ok: false, mode, error: `Maximum terminal limit (${MAX_TERMINALS}) reached` };
}
const backend = this.pickBackend(mode);
if (!backend) {
return { ok: false, mode, error: `Backend '${mode}' not available` };
}
const result = await backend.create(opts);
if (result.ok) {
this.persistentIds.add(opts.id);
}
return { ok: result.ok, mode, error: result.error };
}
async attachTerminal(id, viewerId, cols, rows, onData, onExit) {
for (const backend of Object.values(this.backends)) {
if (!backend) continue;
if (backend.has(id)) {
return backend.attach(id, viewerId, cols, rows, onData, onExit);
}
}
for (const backend of Object.values(this.backends)) {
if (!backend) continue;
const list = await backend.list();
if (list.some((d) => d.id === id)) {
this.persistentIds.add(id);
return backend.attach(id, viewerId, cols, rows, onData, onExit);
}
}
return { ok: false, error: `No persistent terminal found with id: ${id}` };
}
detachTerminal(id, viewerId) {
for (const backend of Object.values(this.backends)) {
if (backend?.has(id)) {
backend.detach(id, viewerId);
return;
}
}
}
writeTerminal(id, data) {
for (const backend of Object.values(this.backends)) {
if (backend?.has(id)) {
return backend.write(id, data);
}
}
return false;
}
resizeTerminal(id, viewerId, cols, rows) {
for (const backend of Object.values(this.backends)) {
if (backend?.has(id)) {
return backend.resize(id, viewerId, cols, rows);
}
}
return false;
}
async destroyTerminal(id) {
for (const backend of Object.values(this.backends)) {
if (backend?.has(id)) {
await backend.destroy(id);
this.persistentIds.delete(id);
return;
}
}
await Promise.all(
Object.values(this.backends).map(
(b) => b?.destroy(id).catch(() => {
})
)
);
this.persistentIds.delete(id);
}
async listTerminals() {
const results = [];
for (const backend of Object.values(this.backends)) {
if (!backend) continue;
const list = await backend.list().catch(() => []);
results.push(...list);
}
return results.filter((d) => !isHostedSessionTerminalId(d.id));
}
// -------------------------------------------------------------------------
// Ephemeral PTY API — preserved exactly from original ptyManager
// -------------------------------------------------------------------------
openPty(terminalId, cols, rows, onData, onExit) {
if (this.ephemeralPtys.has(terminalId)) {
logger.debug(`[PTY] Re-attach to existing ephemeral session ${terminalId}`);
return { ok: true };
}
if (this.totalCount() >= MAX_TERMINALS) {
return { ok: false, error: `Maximum terminal limit (${MAX_TERMINALS}) reached` };
}
try {
const shell = process.env.SHELL ?? (process.platform === "win32" ? "cmd.exe" : "/bin/bash");
const cwd = os.homedir();
const filteredEnv = terminalEnv(process.env);
const pty = spawnPtyWithDiagnostics(shell, [], {
name: "xterm-256color",
cols,
rows,
cwd,
env: filteredEnv,
// encoding:null → node-pty emits raw Buffers on onData and
// accepts Buffers on write(). Keeps the byte stream exact so
// multi-byte UTF-8 / split ANSI escape sequences reach xterm
// intact (VS-Code-grade TUI rendering).
encoding: null
});
const batcher = createOutputBatcher(onData);
pty.onData((data) => {
batcher.push(Buffer.isBuffer(data) ? data : Buffer.from(data, "utf-8"));
});
pty.onExit(() => {
batcher.flushNow();
this.ephemeralPtys.delete(terminalId);
logger.debug(`[PTY] Session ${terminalId} exited`);
onExit();
});
this.ephemeralPtys.set(terminalId, { pty, cols, rows });
logger.debug(`[PTY] Opened session ${terminalId} (${cols}x${rows}, shell: ${shell})`);
return { ok: true };
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to spawn PTY";
logger.debug(`[PTY] Failed to open session ${terminalId}: ${message}`);
return { ok: false, error: message };
}
}
writePty(terminalId, data) {
const session = this.ephemeralPtys.get(terminalId);
if (!session) return;
session.pty.write(data);
}
resizePty(terminalId, cols, rows) {
const session = this.ephemeralPtys.get(terminalId);
if (!session) return;
session.pty.resize(cols, rows);
session.cols = cols;
session.rows = rows;
}
closePty(terminalId) {
const session = this.ephemeralPtys.get(terminalId);
if (!session) return;
try {
session.pty.kill();
} catch {
}
this.ephemeralPtys.delete(terminalId);
logger.debug(`[PTY] Closed session ${terminalId}`);
}
closeAllPty() {
for (const [terminalId, session] of this.ephemeralPtys) {
try {
session.pty.kill();
} catch {
}
logger.debug(`[PTY] Closed session ${terminalId} (shutdown)`);
}
this.ephemeralPtys.clear();
}
}
let _instance = null;
async function getTerminalManager() {
if (!_instance) {
_instance = await TerminalManager.init();
}
return _instance;
}
function expandHome$1(value, home) {
if (value === "~") return home;
if (value.startsWith("~/")) return path.join(home, value.slice(2));
return value;
}
function resolveOverride(raw, fallback, home) {
if (typeof raw !== "string") return fallback;
const trimmed = raw.trim();
if (trimmed.length === 0) return fallback;
return expandHome$1(trimmed, home);
}
function resolveClaudeConfigDir(opts = {}) {
const env = opts.env ?? process.env;
const home = opts.homeDir ?? os.homedir();
return resolveOverride(env.CLAUDE_CONFIG_DIR, path.join(home, ".claude"), home);
}
function resolveCodexHome(opts = {}) {
const env = opts.env ?? process.env;
const home = opts.homeDir ?? os.homedir();
return resolveOverride(env.CODEX_HOME, path.join(home, ".codex"), home);
}
const BINARY_CHECK_TIMEOUT_MS = 4e3;
const defaultProbeRunCommand = (cmd, args) => new Promise((resolve) => {
node_child_process.execFile(cmd, args, { timeout: BINARY_CHECK_TIMEOUT_MS }, (err, stdout, stderr) => {
const so = stdout == null ? "" : String(stdout);
const se = stderr == null ? "" : String(stderr);
if (!err) {
resolve({ stdout: so, stderr: se, code: 0 });
return;
}
const code = err.code;
resolve({ stdout: so, stderr: se, code: typeof code === "number" ? code : 1 });
});
});
async function checkAgentBinary(run, binary, args = ["--version"]) {
try {
const result = await run(binary, args);
if (result.code === 0) {
const version = result.stdout.trim() || void 0;
logger.debug(`[auth-probe] ${binary} binary present${version ? ` (${version})` : ""}`);
return { present: true, version };
}
logger.debug(`[auth-probe] ${binary} binary check exit=${result.code} \u2014 treating as absent`);
return { present: false };
} catch (err) {
logger.debug(
`[auth-probe] ${binary} binary check threw (${err.message}) \u2014 treating as absent`
);
return { present: false };
}
}
const AGENT_AUTH_KINDS = [
"claude",
"codex",
"gemini",
"grok",
"pi",
"opencode",
"consortium"
];
const AGENT_AUTH_CAPABILITIES = {
claude: {
agent: "claude",
binary: "claude",
modes: ["native-local", "paste-code", "api-key"],
localMode: "native-local",
// Anthropic has no device-code grant; `code=true` on the authorize URL
// makes claude.ai display an OOB code instead, which is the only
// remotely-completable path.
remoteMode: "paste-code",
// Emits JSON: loggedIn / authMethod / email / orgName / subscriptionType.
statusCommand: ["auth", "status"],
loginCommand: ["auth", "login", "--claudeai"],
remoteLoginCommand: null,
credentialPaths: [
"$CLAUDE_CONFIG_DIR/.credentials.json",
"~/.claude/.credentials.json",
"keychain:Claude Code-credentials"
],
apiKeyEnvVars: ["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN"],
verified: { cliVersion: "2.1.219", at: "2026-08-05" },
notes: "`claude setup-token` also mints a long-lived token interactively; not used because it needs a TTY and paste-code already covers the remote case."
},
codex: {
agent: "codex",
binary: "codex",
modes: ["native-local", "device-code", "api-key"],
localMode: "native-local",
// VERIFIED 2026-08-05: `codex login --device-auth` prints
// https://auth.openai.com/codex/device + a XXXX-XXXXX code (15 min).
// The CLI polls and writes its own auth.json — we neither hold the
// device_code nor synthesize the credential file.
remoteMode: "device-code",
statusCommand: ["login", "status"],
loginCommand: ["login"],
remoteLoginCommand: ["login", "--device-auth"],
credentialPaths: ["$CODEX_HOME/auth.json", "~/.codex/auth.json"],
apiKeyEnvVars: ["OPENAI_API_KEY"],
verified: { cliVersion: "0.144.4", at: "2026-08-05" },
notes: "Also accepts `codex login --with-api-key` / `--with-access-token` on stdin, which avoids hand-writing auth.json (id_token required, last_refresh must be recent or codex rotates the refresh token \u2014 WAVE0_SPIKE_RESULTS \xA7S4\u2032)."
},
gemini: {
agent: "gemini",
binary: "gemini",
// VERIFIED 2026-08-05 (0.53.1): there is NO `gemini auth`/`login`
// subcommand. OAuth happens inside the interactive TUI only; headless
// invocations refuse with "run the Gemini CLI in an interactive
// terminal to log in, provide a GEMINI_API_KEY, or configure ADC".
modes: ["api-key"],
localMode: null,
remoteMode: null,
statusCommand: null,
loginCommand: null,
remoteLoginCommand: null,
credentialPaths: [
"~/.gemini/oauth_creds.json",
"~/.gemini/google_accounts.json",
"~/.config/gcloud/application_default_credentials.json"
],
apiKeyEnvVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
verified: { cliVersion: "0.53.1", at: "2026-08-05" },
notes: "A user-code path DOES exist in the bundle (NO_BROWSER=true \u2192 authWithUserCode \u2192 https://codeassist.google.com/authcode) but it requires an interactive TTY and alternate-screen handling, so it is a cli-spawn candidate for a later wave, not a shipped mode. The previous gcloud device-code adapter captured Application Default Credentials \u2014 a DIFFERENT credential than the one gemini-cli reads \u2014 and was removed."
},
grok: {
agent: "grok",
binary: "grok",
modes: ["native-local", "device-code", "api-key"],
localMode: "native-local",
// VERIFIED 2026-08-05: `grok login --device-auth` prints, on STDERR,
// https://accounts.x.ai/oauth2/device?user_code=XXXX-XXXX plus the
// bare code. The URL embeds the code (RFC 8628
// verification_uri_complete), so the app can offer a single tap.
remoteMode: "device-code",
statusCommand: null,
loginCommand: ["login", "--oauth"],
remoteLoginCommand: ["login", "--device-auth"],
credentialPaths: ["~/.grok/auth.json"],
apiKeyEnvVars: ["XAI_API_KEY", "GROK_API_KEY", "GROK_CODE_XAI_API_KEY"],
verified: { cliVersion: "0.2.118", at: "2026-08-05" },
notes: "Device-auth prints to stderr, not stdout \u2014 a stdout-only parser sees nothing. Requires a SuperGrok / X Premium+ subscription; without one the CLI errors and the flow fails loud."
},
pi: {
agent: "pi",
binary: "pi",
// VERIFIED 2026-08-05 (0.83.0): `pi auth` only PRINTS credentials for
// external clients (print-api-key / print-bearer-token) — it is a
// reader, not a login. Credentials come from --api-key or env.
modes: ["api-key"],
localMode: null,
remoteMode: null,
// Deliberately null: `pi auth print-api-key` writes the secret to
// stdout, so it must never be used as a status probe.
statusCommand: null,
loginCommand: null,
remoteLoginCommand: null,
credentialPaths: ["~/.pi/settings.json"],
apiKeyEnvVars: ["GOOGLE_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"],
verified: { cliVersion: "0.83.0", at: "2026-08-05" },
notes: "BYOK only; provider is selected per-invocation with --provider/--model."
},
opencode: {
agent: "opencode",
binary: "opencode",
// VERIFIED 2026-08-05 (1.18.14): `opencode auth login` is an
// interactive provider picker (clack TUI) covering ~75 providers
// including Anthropic OAuth; `opencode auth list` reports stored
// credentials WITHOUT printing them.
modes: ["api-key", "cli-spawn"],
localMode: "cli-spawn",
remoteMode: null,
statusCommand: ["auth", "list"],
loginCommand: ["auth", "login"],
remoteLoginCommand: null,
credentialPaths: ["~/.local/share/opencode/auth.json"],
apiKeyEnvVars: ["OPENCODE_API_KEY"],
verified: { cliVersion: "1.18.14", at: "2026-08-05" },
notes: "cli-spawn is listed but not yet driven by a daemon flow; until that lands the UI offers api-key only. Provider selection is part of the TUI, so a spawn adapter must relay the picker, not just a URL."
},
consortium: {
agent: "consortium",
binary: "consortium",
// The managed proxy: credentials are the user's Consortium account,
// established at app login. There is nothing per-machine to connect.
modes: [],
localMode: null,
remoteMode: null,
statusCommand: null,
loginCommand: null,
remoteLoginCommand: null,
credentialPaths: ["$CONSORTIUM_HOME_DIR/access.key"],
apiKeyEnvVars: ["CONSORTIUM_TOKEN"],
verified: null,
notes: "Managed proxy \u2014 no per-machine provider login."
}
};
function getAgentAuthCapability(agent) {
return AGENT_AUTH_CAPABILITIES[agent];
}
function connectableAgents() {
return AGENT_AUTH_KINDS.map((a) => AGENT_AUTH_CAPABILITIES[a]).filter((c) => c.modes.length > 0);
}
function parseClaudeStatus(stdout) {
const start = stdout.indexOf("{");
const end = stdout.lastIndexOf("}");
if (start < 0 || end <= start) return { status: "unknown", reason: "no JSON in output" };
let parsed;
try {
parsed = JSON.parse(stdout.slice(start, end + 1));
} catch {
return { status: "unknown", reason: "unparseable JSON" };
}
if (parsed.loggedIn !== true) return { status: "missing" };
const email = typeof parsed.email === "string" ? parsed.email : void 0;
const account = typeof parsed.orgName === "string" ? parsed.orgName : void 0;
const tier = typeof parsed.subscriptionType === "string" ? parsed.subscriptionType : void 0;
const method = typeof parsed.authMethod === "string" ? parsed.authMethod : void 0;
return {
status: "present",
identity: email || account ? { email, account } : void 0,
// e.g. "claude.ai · pro" — shown as a subtitle, never a credential.
detail: [method, tier].filter(Boolean).join(" \xB7 ") || void 0
};
}
function parseCodexStatus(stdout) {
const text = stdout.toLowerCase();
if (text.includes("not logged in")) return { status: "missing" };
if (text.includes("logged in")) {
const method = /logged in using ([^\n.]+)/i.exec(stdout)?.[1]?.trim();
return { status: "present", detail: method };
}
return { status: "unknown", reason: "unrecognised codex status output" };
}
function parseOpencodeStatus(stdout) {
const match = /(\d+)\s+credentials?/i.exec(stdout);
if (!match) return { status: "unknown", reason: "unrecognised opencode auth output" };
const count = Number(match[1]);
if (count <= 0) return { status: "missing" };
return { status: "present", detail: `${count} provider${count === 1 ? "" : "s"}` };
}
const PARSERS = {
claude: parseClaudeStatus,
codex: parseCodexStatus,
opencode: parseOpencodeStatus
};
async function probeCliStatus(agent, run) {
const cap = getAgentAuthCapability(agent);
const parser = PARSERS[agent];
if (!cap?.statusCommand || !parser) {
return { status: "unknown", reason: "no status command for this agent" };
}
let result;
try {
result = await run(cap.binary, [...cap.statusCommand]);
} catch (err) {
return { status: "unknown", reason: err instanceof Error ? err.message : String(err) };
}
const verdict = parser(`${result.stdout}
${result.stderr}`);
logger.debug(`[auth-probe] ${agent} status command \u2192 ${verdict.status}`);
return verdict;
}
const KEYCHAIN_SERVICE = "Claude Code-credentials";
function parseCredentialsBlob(blob) {
if (!blob || typeof blob !== "object") return null;
let obj = blob;
if (obj.claudeAiOauth && typeof obj.claudeAiOauth === "object") {
obj = obj.claudeAiOauth;
}
const token = typeof obj.access_token === "string" && obj.access_token || typeof obj.accessToken === "string" && obj.accessToken || typeof obj.token === "string" && obj.token || void 0;
const identity = {};
const expiresAtMs = typeof obj.expiresAt === "number" ? obj.expiresAt : void 0;
const expiresAtSec = typeof obj.expires_at === "number" ? obj.expires_at : void 0;
if (expiresAtMs !== void 0) {
identity.expiresAt = expiresAtMs;
} else if (expiresAtSec !== void 0) {
identity.expiresAt = expiresAtSec * 1e3;
}
if (typeof obj.email === "string") {
identity.email = obj.email;
} else if (obj.account && typeof obj.account === "object") {
const acct = obj.account;
if (typeof acct.email === "string") identity.email = acct.email;
else if (typeof acct.email_address === "string") identity.email = acct.email_address;
}
return { token: token || void 0, identity };
}
function readCredentialFile(path) {
if (!fs.existsSync(path)) return null;
let raw;
try {
raw = fs.readFileSync(path, "utf8");
} catch (err) {
logger.debug(
`claude auth probe: unable to read ${path} (${err.message})`
);
return { path, state: "invalid" };
}
if (!raw.trim()) {
logger.debug(`claude auth probe: ${path} exists but is empty`);
return { path, state: "invalid" };
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch (err) {
logger.debug(
`claude auth probe: ${path} is not valid JSON (${err.message})`
);
return { path, state: "invalid" };
}
const result = parseCredentialsBlob(parsed);
if (!result || !result.token) {
logger.debug(`claude auth probe: ${path} parsed but no token field present`);
return { path, state: "invalid" };
}
return { path, state: "present", identity: result.identity };
}
class ClaudeAuthProbe {
env;
home;
platform;
run;
nowFn;
constructor(options = {}) {
this.env = options.env ?? process.env;
this.home = options.homeDir ?? os.homedir();
this.platform = options.platform ?? process.platform;
this.run = options.runCommand ?? defaultProbeRunCommand;
this.nowFn = options.now ?? Date.now;
}
/**
* Ask Claude Code itself, and inspect the machine, at the same time.
*
* `claude auth status` is the authoritative answer — it reflects whatever
* precedence Claude Code applies internally, and it returns the account
* email and subscription tier, which no amount of file inspection can
* produce. But it is only authoritative when it says YES: "not logged in"
* coexists perfectly well with an `ANTHROPIC_API_KEY` in the environment
* that the CLI would happily use, so a negative falls through to the
* credential discovery below.
*
* Both run concurrently. Serialising them would stack a second exec
* timeout in front of the macOS Keychain lookup and could blow the probe
* registry's 5 s budget — the same reason the binary check is started
* before, not after, credential discovery.
*/
async probe() {
const cliStatus = probeCliStatus("claude", this.run);
const [discovered, verdict] = await Promise.all([this.discoverCredentials(), cliStatus]);
if (verdict.status !== "present") return discovered;
logger.debug(`claude auth probe: CLI reports signed in (${verdict.detail ?? "no detail"})`);
return {
...discovered,
status: "present",
source: "cli",
identity: verdict.identity ?? discovered.identity
};
}
async discoverCredentials() {
const lastCheckedAt = this.nowFn();
let warning;
const binaryCheck = checkAgentBinary(this.run, "claude");
const base = async (partial) => {
const binary = await binaryCheck;
const out = {
agent: "claude",
lastCheckedAt,
...partial,
binaryPresent: binary.present
};
if (!binary.present && out.status !== "missing") {
warning = warning ?? "binary-missing";
}
if (warning && !out.warning) out.warning = warning;
return out;
};
const oauthEnv = this.env.CLAUDE_CODE_OAUTH_TOKEN;
if (oauthEnv && oauthEnv.length > 0) {
logger.debug(
`claude auth probe: env CLAUDE_CODE_OAUTH_TOKEN present <${oauthEnv.length} chars>`
);
return base({ status: "present", source: "env" });
}
const apiKeyEnv = this.env.ANTHROPIC_API_KEY;
if (apiKeyEnv && apiKeyEnv.length > 0) {
logger.debug(
`claude auth probe: env ANTHROPIC_API_KEY present <${apiKeyEnv.length} chars>`
);
return base({ status: "present", source: "env" });
}
if (this.platform === "darwin") {
try {
const result = await this.run("security", [
"find-generic-password",
"-s",
KEYCHAIN_SERVICE,
"-w"
]);
if (result.code === 0 && result.stdout.trim().length > 0) {
logger.debug(
`claude auth probe: keychain hit <${result.stdout.trim().length} chars>`
);
return base({ status: "present", source: "keychain" });
}
const stderrLower = (result.stderr || "").toLowerCase();
const isLocked = result.code === 51 || stderrLower.includes("user interaction is not allowed") || stderrLower.includes("keychain access locked") || stderrLower.includes("keychain") && stderrLower.includes("locked");
if (isLocked) {
logger.debug(
`claude auth probe: keychain locked (exit=${result.code}); falling through with warning`
);
warning = "keychain-locked";
}
logger.debug(
`claude auth probe: keychain miss (exit=${result.code})`
);
} catch (err) {
logger.debug(
`claude auth probe: keychain lookup threw (${err.message})`
);
}
}
if (this.platform === "linux") {
logger.debug(
"claude auth probe: linux libsecret lookup not yet implemented; falling through to file probe"
);
}
const candidates = [
path.join(resolveClaudeConfigDir({ env: this.env, homeDir: this.home }), ".credentials.json"),
path.join(this.home, ".config", "claude", "auth.json")
];
let fileOutcome = null;
for (const path of candidates) {
const outcome = readCredentialFile(path);
if (outcome) {
fileOutcome = outcome;
break;
}
}
if (fileOutcome) {
if (fileOutcome.state === "present") {
logger.debug(
`claude auth probe: present via file ${fileOutcome.path}` + (fileOutcome.identity?.email ? ` email=${fileOutcome.identity.email}` : "") + (fileOutcome.identity?.expiresAt ? ` expiresAt=${fileOutcome.identity.expiresAt}` : "")
);
return base({
status: "present",
source: "file",
identity: fileOutcome.identity
});
}
logger.debug(`claude auth probe: invalid via file ${fileOutcome.path}`);
return base({ status: "invalid", source: "file" });
}
logger.debug("claude auth probe: no credentials found, returning missing");
return base({ status: "missing" });
}
}
function describeKey$4(key) {
const trimmed = key.trim();
const tail = trimmed.length >= 4 ? trimmed.slice(-4) : trimmed;
return `<${trimmed.length} chars, \u2026${tail}>`;
}
function decodeJwtEmail(token) {
try {
const segments = token.split(".");
if (segments.length < 2) return void 0;
const payloadSeg = segments[1];
const b64 = payloadSeg.replace(/-/g, "+").replace(/_/g, "/");
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
const json = Buffer.from(padded, "base64").toString("utf8");
const parsed = JSON.parse(json);
if (parsed && typeof parsed === "object" && "email" in parsed) {
const email = parsed.email;
if (typeof email === "string" && email.length > 0) {
return email;
}
}
return void 0;
} catch {
return void 0;
}
}
async function readAuthFile(path, now) {
let raw;
try {
raw = await fs.promises.readFile(path, "utf8");
} catch (err) {
const code = err.code;
if (code === "ENOENT" || code === "ENOTDIR") {
return { state: null };
}
logger.debug(`[auth/codex] failed to read ${path}: ${code ?? "unknown"}`);
return {
state: {
agent: "codex",
status: "invalid",
source: "file",
lastCheckedAt: now,
error: `read failed: ${code ?? "unknown"}`
}
};
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
logger.debug(`[auth/codex] malformed JSON at ${path}`);
return {
state: {
agent: "codex",
status: "invalid",
source: "file",
lastCheckedAt: now,
error: "malformed JSON"
}
};
}
if (!parsed || typeof parsed !== "object") {
return {
state: {
agent: "codex",
status: "invalid",
source: "file",
lastCheckedAt: now,
error: "not a JSON object"
}
};
}
const apiKey = typeof parsed.OPENAI_API_KEY === "string" ? parsed.OPENAI_API_KEY : void 0;
const tokensObj = parsed.tokens && typeof parsed.tokens === "object" ? parsed.tokens : void 0;
const idToken = tokensObj && typeof tokensObj.id_token === "string" ? tokensObj.id_token : void 0;
const accessToken = tokensObj && typeof tokensObj.access_token === "string" ? tokensObj.access_token : void 0;
if (!apiKey && !idToken && !accessToken) {
return {
state: {
agent: "codex",
status: "invalid",
source: "file",
lastCheckedAt: now,
error: "no recognized credential fields"
}
};
}
let identity;
if (idToken) {
const email = decodeJwtEmail(idToken);
if (email) {
identity = { email };
} else {
logger.debug("[auth/codex] id_token present but email claim could not be decoded");
}
}
if (apiKey) {
logger.debug(`[auth/codex] file contains OPENAI_API_KEY ${describeKey$4(apiKey)}`);
}
if (idToken) {
logger.debug(`[auth/codex] file contains tokens.id_token <${idToken.length} chars>`);
}
if (accessToken) {
logger.debug(`[auth/codex] file contains tokens.access_token <${accessToken.length} chars>`);
}
return {
state: {
agent: "codex",
status: "present",
source: "file",
identity,
lastCheckedAt: now
}
};
}
class CodexAuthProbe {
now;
env;
homeDir;
runCommand;
constructor(options = {}) {
this.now = options.now ?? Date.now;
this.env = options.env ?? process.env;
this.homeDir = options.homeDir ?? os.homedir();
this.runCommand = options.runCommand ?? defaultProbeRunCommand;
}
async probe() {
const now = this.now();
const binaryCheck = checkAgentBinary(this.runCommand, "codex");
const [credentialState, cliVerdict] = await Promise.all([
this.probeCredentials(now),
probeCliStatus("codex", this.runCommand)
]);
const state = cliVerdict.status === "present" ? { ...credentialState, status: "present", source: "cli" } : credentialState;
const binary = await binaryCheck;
const warning = !binary.present && state.status !== "missing" ? state.warning ?? "binary-missing" : state.warning;
return { ...state, binaryPresent: binary.present, ...warning ? { warning } : {} };
}
async probeCredentials(now) {
const envKey = this.env.OPENAI_API_KEY;
if (typeof envKey === "string" && envKey.length > 0) {
if (envKey.startsWith("sk-")) {
logger.debug(`[auth/codex] OpenAI key shape ${describeKey$4(envKey)}`);
} else {
logger.debug(`[auth/codex] non-standard key shape but accepting ${describeKey$4(envKey)}`);
}
return {
agent: "codex",
status: "present",
source: "env",
lastCheckedAt: now
};
}
const primaryPath = path.join(
resolveCodexHome({ env: this.env, homeDir: this.homeDir }),
"auth.json"
);
const primary = await readAuthFile(primaryPath, now);
if (primary.state) {
return primary.state;
}
const fallbackPath = path.join(this.homeDir, ".config", "openai", "auth.json");
const fallback = await readAuthFile(fallbackPath, now);
if (fallback.state) {
return fallback.state;
}
return {
agent: "codex",
status: "missing",
lastCheckedAt: now
};
}
}
const GEMINI_AGENT = "gemini";
const ENV_VAR_NAMES$1 = [
"GEMINI_API_KEY",
"GOOGLE_API_KEY",
"GOOGLE_AI_STUDIO_API_KEY"
];
const CREDENTIAL_KEY_FIELDS = ["api_key", "apiKey", "key", "token"];
function shapeOnly$1(secret) {
const last4 = secret.length >= 4 ? secret.slice(-4) : secret;
return `<${secret.length} chars, ...${last4}>`;
}
function makePresent$1(now, source, identity) {
return {
agent: GEMINI_AGENT,
status: "present",
source,
identity,
lastCheckedAt: now
};
}
class GeminiAuthProbe {
now;
env;
homeDir;
runCommand;
constructor(options = {}) {
this.now = options.now ?? Date.now;
this.env = options.env ?? process.env;
this.homeDir = options.homeDir ?? os.homedir();
this.runCommand = options.runCommand ?? defaultProbeRunCommand;
}
async probe() {
const binaryCheck = checkAgentBinary(this.runCommand, "gemini");
const state = await this.probeCredentials();
const binary = await binaryCheck;
const warning = !binary.present && state.status !== "missing" ? state.warning ?? "binary-missing" : state.warning;
return { ...state, binaryPresent: binary.present, ...warning ? { warning } : {} };
}
async probeCredentials() {
const envHit = this.probeEnv();
if (envHit) return envHit;
const fileHit = await this.probeCredentialsFile();
if (fileHit) return fileHit;
const adcHit = await this.probeAdc();
if (adcHit) return adcHit;
return {
agent: GEMINI_AGENT,
status: "missing",
lastCheckedAt: this.now()
};
}
/**
* Best-effort `gemini --version` check the daemon may invoke when
* it wants to know whether the CLI is on $PATH. Never used to
* downgrade auth status; safe to skip.
*/
async livenessCheck() {
try {
const result = await this.runCommand("gemini", ["--version"]);
if (result.code === 0) {
const version = result.stdout.trim() || void 0;
return { alive: true, version };
}
return { alive: false };
} catch {
return { alive: false };
}
}
probeEnv() {
for (const name of ENV_VAR_NAMES$1) {
const raw = this.env[name];
if (typeof raw === "string" && raw.length > 0) {
logger.debug(`[auth-probe gemini] env hit: ${name}=${shapeOnly$1(raw)}`);
return makePresent$1(this.now(), "env");
}
}
return void 0;
}
async probeCredentialsFile() {
const filePath = path.join(this.homeDir, ".config", "gemini", "credentials.json");
let raw;
try {
raw = await fs.promises.readFile(filePath, "utf8");
} catch (err) {
const code = err?.code;
if (code === "ENOENT") return void 0;
logger.debug(`[auth-probe gemini] credentials.json read error: ${code ?? "unknown"}`);
return void 0;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
logger.debug("[auth-probe gemini] credentials.json malformed JSON");
return {
agent: GEMINI_AGENT,
status: "invalid",
source: "file",
lastCheckedAt: this.now(),
error: "credentials.json is not valid JSON"
};
}
if (parsed === null || typeof parsed !== "object") {
return {
agent: GEMINI_AGENT,
status: "invalid",
source: "file",
lastCheckedAt: this.now(),
error: "credentials.json root is not an object"
};
}
const obj = parsed;
for (const field of CREDENTIAL_KEY_FIELDS) {
const v = obj[field];
if (typeof v === "string" && v.length > 0) {
logger.debug(`[auth-probe gemini] file hit: field=${field} ${shapeOnly$1(v)}`);
return makePresent$1(this.now(), "file");
}
}
return void 0;
}
async probeAdc() {
const filePath = path.join(
this.homeDir,
".config",
"gcloud",
"application_default_credentials.json"
);
let raw;
try {
raw = await fs.promises.readFile(filePath, "utf8");
} catch (err) {
const code = err?.code;
if (code === "ENOENT") return void 0;
logger.debug(`[auth-probe gemini] ADC read error: ${code ?? "unknown"}`);
return void 0;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
logger.debug("[auth-probe gemini] ADC malformed JSON");
return void 0;
}
let identity;
if (parsed !== null && typeof parsed === "object") {
const email = parsed.client_email;
if (typeof email === "string" && email.length > 0) {
identity = { email };
}
}
logger.debug("[auth-probe gemini] ADC hit");
return makePresent$1(this.now(), "file", identity);
}
}
const GROK_AGENT = "grok";
const ENV_VAR_NAMES = [
"GROK_CODE_XAI_API_KEY",
"XAI_API_KEY",
"GROK_API_KEY"
];
const AUTH_JSON_SCOPES = [
"https://auth.x.ai::b1a00492-073a-47ea-816f-4c329264a828",
"https://accounts.x.ai/sign-in"
];
function shapeOnly(secret) {
const last4 = secret.length >= 4 ? secret.slice(-4) : secret;
return `<${secret.length} chars, ...${last4}>`;
}
function makePresent(now, source, identity) {
return {
agent: GROK_AGENT,
status: "present",
source,
identity,
lastCheckedAt: now
};
}
class GrokAuthProbe {
now;
env;
homeDir;
runCommand;
constructor(options = {}) {
this.now = options.now ?? Date.now;
this.env = options.env ?? process.env;
this.homeDir = options.homeDir ?? os.homedir();
this.runCommand = options.runCommand ?? defaultProbeRunCommand;
}
async probe() {
const binaryCheck = checkAgentBinary(this.runCommand, "grok");
const state = await this.probeCredentials();
const binary = await binaryCheck;
const warning = !binary.present && state.status !== "missing" ? state.warning ?? "binary-missing" : state.warning;
return { ...state, binaryPresent: binary.present, ...warning ? { warning } : {} };
}
async probeCredentials() {
const envHit = this.probeEnv();
if (envHit) return envHit;
const fileHit = await this.probeCredentialsFile();
if (fileHit) return fileHit;
return {
agent: GROK_AGENT,
status: "missing",
lastCheckedAt: this.now()
};
}
/**
* Best-effort `grok --version` check the daemon may invoke when
* it wants to know whether the CLI is on $PATH. Never used to
* downgrade auth status; safe to skip.
*/
async livenessCheck() {
try {
const result = await this.runCommand("grok", ["--version"]);
if (result.code === 0) {
const version = result.stdout.trim() || void 0;
return { alive: true, version };
}
return { alive: false };
} catch {
return { alive: false };
}
}
probeEnv() {
for (const name of ENV_VAR_NAMES) {
const raw = this.env[name];
if (typeof raw === "string" && raw.length > 0) {
logger.debug(`[auth-probe grok] env hit: ${name}=${shapeOnly(raw)}`);
return makePresent(this.now(), "env");
}
}
return void 0;
}
async probeCredentialsFile() {
const filePath = path.join(this.homeDir, ".grok", "auth.json");
let raw;
try {
raw = await fs.promises.readFile(filePath, "utf8");
} catch (err) {
const code = err?.code;
if (code === "ENOENT") return void 0;
logger.debug(`[auth-probe grok] auth.json read error: ${code ?? "unknown"}`);
return void 0;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
logger.debug("[auth-probe grok] auth.json malformed JSON");
return {
agent: GROK_AGENT,
status: "invalid",
source: "file",
lastCheckedAt: this.now(),
error: "auth.json is not valid JSON"
};
}
if (parsed === null || typeof parsed !== "object") {
return {
agent: GROK_AGENT,
status: "invalid",
source: "file",
lastCheckedAt: this.now(),
error: "auth.json root is not an object"
};
}
const root = parsed;
for (const scope of AUTH_JSON_SCOPES) {
const scoped = root[scope];
if (scoped === null || typeof scoped !== "object") continue;
const key = scoped.key;
if (typeof key === "string" && key.length > 0) {
logger.debug(`[auth-probe grok] auth.json hit: scope=${scope} ${shapeOnly(key)}`);
return makePresent(this.now(), "file");
}
}
return void 0;
}
}
const credentialsSchema = z.z.object({
token: z.z.string().min(1),
secret: z.z.string().base64().nullish(),
encryption: z.z.object({
publicKey: z.z.string().base64(),
machineKey: z.z.string().base64()
}).nullish()
}).passthrough();
function resolveCredentialsPath() {
const home = process.env.CONSORTIUM_HOME_DIR ? process.env.CONSORTIUM_HOME_DIR.replace(/^~/, os.homedir()) : path.join(os.homedir(), ".consortium");
return path.join(home, "access.key");
}
function envHasToken() {
const tok = process.env.CONSORTIUM_TOKEN;
return typeof tok === "string" && tok.trim().length > 0;
}
class ConsortiumAuthProbe {
now;
constructor(options = {}) {
this.now = options.now ?? Date.now;
}
async probe() {
const lastCheckedAt = this.now();
if (envHasToken()) {
return {
agent: "consortium",
status: "present",
source: "env",
lastCheckedAt
};
}
const credPath = resolveCredentialsPath();
try {
if (!fs.existsSync(credPath)) {
return {
agent: "consortium",
status: "missing",
lastCheckedAt
};
}
} catch (err) {
return {
agent: "consortium",
status: "unknown",
lastCheckedAt,
error: err instanceof Error ? err.message : String(err)
};
}
try {
const raw = await fs$3.readFile(credPath, "utf8");
const parsed = credentialsSchema.safeParse(JSON.parse(raw));
if (!parsed.success) {
return {
agent: "consortium",
status: "invalid",
source: "file",
lastCheckedAt,
error: "credentials file failed structural validation"
};
}
return {
agent: "consortium",
status: "present",
source: "file",
lastCheckedAt
};
} catch (err) {
return {
agent: "consortium",
status: "invalid",
source: "file",
lastCheckedAt,
error: err instanceof Error ? err.message : String(err)
};
}
}
}
function expandHome(p, home) {
return p.startsWith("~/") ? path.join(home, p.slice(2)) : p;
}
class ByokAgentProbe {
agent;
env;
homeDir;
now;
runCommand;
constructor(agent, options = {}) {
this.agent = agent;
this.env = options.env ?? process.env;
this.homeDir = options.homeDir ?? os.homedir();
this.now = options.now ?? Date.now;
this.runCommand = options.runCommand ?? defaultProbeRunCommand;
}
async probe() {
const lastCheckedAt = this.now();
const cap = getAgentAuthCapability(this.agent);
if (!cap) {
return { agent: this.agent, status: "unknown", lastCheckedAt, error: "no capability row" };
}
const binaryCheck = checkAgentBinary(this.runCommand, cap.binary);
const statusCheck = probeCliStatus(this.agent, this.runCommand);
const envVar = cap.apiKeyEnvVars.find((name) => {
const value = this.env[name];
return typeof value === "string" && value.trim().length > 0;
});
const [binary, verdict] = await Promise.all([binaryCheck, statusCheck]);
const base = (partial) => {
const state = {
agent: this.agent,
lastCheckedAt,
...partial,
binaryPresent: binary.present
};
if (!binary.present && state.status !== "missing") state.warning = "binary-missing";
return state;
};
if (verdict.status === "present") return base({ status: "present", source: "cli" });
if (envVar) return base({ status: "present", source: "env" });
const credentialFile = cap.credentialPaths.map((p) => expandHome(p, this.homeDir)).find((p) => !p.includes("$") && fs.existsSync(p));
if (credentialFile) return base({ status: "present", source: "file" });
return base({ status: "missing" });
}
}
const AGENT_KINDS = ["claude", "codex", "gemini", "grok", "pi", "opencode", "consortium"];
const PROBE_TIMEOUT_MS = 5e3;
const DEFAULT_CACHE_TTL_MS = 3e4;
const ENV_SIGNATURE_KEYS = [
"CLAUDE_CODE_OAUTH_TOKEN",
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"CODEX_API_KEY",
"GEMINI_API_KEY",
"GOOGLE_API_KEY",
"GROK_CODE_XAI_API_KEY",
"XAI_API_KEY",
"GROK_API_KEY",
"OPENCODE_API_KEY",
"CONSORTIUM_TOKEN",
"CONSORTIUM_HOME_DIR",
// Config-dir overrides relocate the credential files the probes read
// (see `auth/paths.ts`), so they belong in the cache signature.
"CLAUDE_CONFIG_DIR",
"CODEX_HOME",
"HOME"
];
const DEFAULT_FACTORIES = {
claude: (opts) => new ClaudeAuthProbe(opts),
codex: (opts) => new CodexAuthProbe(opts),
gemini: (opts) => new GeminiAuthProbe(opts),
grok: (opts) => new GrokAuthProbe(opts),
// BYOK-only agents (no login flow of their own) share one probe.
pi: (opts) => new ByokAgentProbe("pi", opts),
opencode: (opts) => new ByokAgentProbe("opencode", opts),
consortium: (opts) => new ConsortiumAuthProbe(opts)
};
let factories = { ...DEFAULT_FACTORIES };
const cache$1 = /* @__PURE__ */ new Map();
function invalidateProbeCache() {
cache$1.clear();
}
function buildCacheKey(opts) {
const env = opts.env ?? process.env;
const envSignatureKeys = ENV_SIGNATURE_KEYS.filter((k) => typeof env[k] === "string").sort();
const agents = (opts.agents ?? AGENT_KINDS).slice().sort();
return JSON.stringify({
homeDir: opts.homeDir ?? null,
envSignature: envSignatureKeys,
agents
});
}
async function runOneProbe(agent, options, timeoutMs, now) {
const factory = factories[agent];
let timer;
try {
const probe = factory(options);
const result = await Promise.race([
probe.probe(),
new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error(`probe timed out after ${timeoutMs}ms`)),
timeoutMs
);
})
]);
return result;
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.debug(`[auth-probe] ${agent} probe failed: ${message}`);
return {
agent,
status: "unknown",
lastCheckedAt: now(),
error: message
};
} finally {
if (timer) clearTimeout(timer);
}
}
async function runAllProbes(opts = {}) {
const now = opts.now ?? Date.now;
const ttlMs = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
const timeoutMs = opts.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
const agents = opts.agents ?? AGENT_KINDS.slice();
const cacheKey = buildCacheKey(opts);
if (ttlMs > 0) {
const hit = cache$1.get(cacheKey);
if (hit && hit.expiresAt > now()) {
return hit.result;
}
}
const probeOptions = {
now,
env: opts.env,
homeDir: opts.homeDir,
runCommand: opts.runCommand
};
const settled = await Promise.allSettled(
agents.map((agent) => runOneProbe(agent, probeOptions, timeoutMs, now))
);
const states = settled.map((entry, idx) => {
const agent = agents[idx];
if (entry.status === "fulfilled") return entry.value;
const message = entry.reason instanceof Error ? entry.reason.message : String(entry.reason);
return { agent, status: "unknown", lastCheckedAt: now(), error: message };
});
const result = { states, probedAt: now() };
if (ttlMs > 0) {
cache$1.set(cacheKey, { result, expiresAt: now() + ttlMs });
}
return result;
}
const APPLE_SILICON_GBS = {
"m1": 68,
"m1 pro": 200,
"m1 max": 400,
"m1 ultra": 800,
"m2": 100,
"m2 pro": 200,
"m2 max": 400,
"m2 ultra": 800,
"m3": 100,
"m3 pro": 150,
// M3 Max ships as 300 (14-core CPU) and 400 (16-core) — take the lower.
"m3 max": 300,
"m3 ultra": 800,
"m4": 120,
"m4 pro": 273,
// M4 Max ships as 410 (14-core CPU) and 546 (16-core) — take the lower.
"m4 max": 410
};
const DISCRETE_GPU_GBS = {
"rtx 5090": 1792,
"rtx 4090": 1008,
"rtx 4080": 717,
"rtx 4070 ti": 504,
"rtx 4070": 504,
"rtx 3090 ti": 1008,
"rtx 3090": 936,
"rtx 3080": 760,
"rtx 3070": 448,
"rtx 3060": 360,
"rtx a6000": 768,
"a100": 1555,
"h100": 3350,
"l40s": 864
};
function appleSiliconBandwidthGBs(brandString) {
const m = brandString.toLowerCase().match(/apple\s+(m\d+)(\s+(pro|max|ultra))?/);
if (!m) return void 0;
const key = m[3] ? `${m[1]} ${m[3]}` : m[1];
return APPLE_SILICON_GBS[key];
}
function discreteGpuBandwidthGBs(gpuName) {
const name = gpuName.toLowerCase();
let best;
for (const [key, gbs] of Object.entries(DISCRETE_GPU_GBS)) {
if (!name.includes(key)) continue;
if (!best || key.length > best.key.length) best = { key, gbs };
}
return best?.gbs;
}
const execFileAsync$3 = util.promisify(child_process.execFile);
const SYSCTL_TIMEOUT_MS = 1e3;
const PROFILER_TIMEOUT_MS = 5e3;
async function sysctl(keys) {
const out = {};
try {
const { stdout } = await execFileAsync$3("sysctl", keys, { timeout: SYSCTL_TIMEOUT_MS });
for (const line of stdout.split("\n")) {
const idx = line.indexOf(":");
if (idx === -1) continue;
out[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
}
} catch {
}
return out;
}
function darwinUsableModelBytes(ramTotalBytes, wiredLimitMb) {
if (wiredLimitMb > 0) return wiredLimitMb * 1024 * 1024;
const GB = 1024 ** 3;
return ramTotalBytes <= 36 * GB ? Math.floor(ramTotalBytes * 0.7) : ramTotalBytes - 8 * GB;
}
function parseDarwinDisplays(json, appleSilicon) {
let parsed;
try {
parsed = JSON.parse(json);
} catch {
return [];
}
const gpus = [];
for (const entry of parsed.SPDisplaysDataType ?? []) {
const name = entry.sppci_model;
if (!name) continue;
const lower = name.toLowerCase();
const vendor = lower.includes("apple") ? "apple" : lower.includes("nvidia") || lower.includes("geforce") ? "nvidia" : lower.includes("amd") || lower.includes("radeon") ? "amd" : lower.includes("intel") ? "intel" : "unknown";
const vramBytes = appleSilicon ? void 0 : parseVramString(entry.spdisplays_vram);
gpus.push({ vendor, name, vramBytes });
}
return gpus;
}
function parseVramString(raw) {
if (!raw) return void 0;
const m = raw.trim().match(/^([\d.]+)\s*(GB|MB)$/i);
if (!m) return void 0;
const value = parseFloat(m[1]);
if (!Number.isFinite(value)) return void 0;
return Math.round(value * (m[2].toUpperCase() === "GB" ? 1024 ** 3 : 1024 ** 2));
}
async function detectDarwin() {
const warnings = [];
const keys = await sysctl([
"machdep.cpu.brand_string",
"hw.memsize",
"hw.physicalcpu",
"hw.logicalcpu",
"iogpu.wired_limit_mb"
]);
const cpuModel = keys["machdep.cpu.brand_string"] || os$1.cpus()[0]?.model || "unknown";
const ramTotalBytes = Number(keys["hw.memsize"]) || os$1.totalmem();
const cpuCoresPhysical = Number(keys["hw.physicalcpu"]) || os$1.cpus().length;
const cpuThreads = Number(keys["hw.logicalcpu"]) || os$1.cpus().length;
const wiredLimitMb = Number(keys["iogpu.wired_limit_mb"]) || 0;
const memBandwidthGBs = appleSiliconBandwidthGBs(cpuModel);
const unifiedMemory = memBandwidthGBs !== void 0 || /apple m\d/i.test(cpuModel);
let gpus = [];
try {
const { stdout } = await execFileAsync$3(
"system_profiler",
["SPDisplaysDataType", "-json"],
{ timeout: PROFILER_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 }
);
gpus = parseDarwinDisplays(stdout, unifiedMemory);
} catch {
warnings.push("system_profiler did not respond; GPU details unavailable");
}
const usableModelBytes = unifiedMemory ? darwinUsableModelBytes(ramTotalBytes, wiredLimitMb) : gpus.reduce((sum, g) => sum + (g.vramBytes ?? 0), 0) || Math.floor(ramTotalBytes * 0.5);
return {
cpuModel,
cpuCoresPhysical,
cpuThreads,
ramTotalBytes,
gpus,
unifiedMemory,
usableModelBytes,
memBandwidthGBs,
warnings
};
}
const execFileAsync$2 = util.promisify(child_process.execFile);
const SMI_TIMEOUT_MS = 3e3;
function parseMemTotalBytes(meminfo) {
const m = meminfo.match(/^MemTotal:\s+(\d+)\s+kB/m);
return m ? parseInt(m[1], 10) * 1024 : void 0;
}
function parsePhysicalCores(cpuinfo) {
const pairs = /* @__PURE__ */ new Set();
let physicalId = "0";
for (const line of cpuinfo.split("\n")) {
const [rawKey, rawVal] = line.split(":");
if (!rawVal) continue;
const key = rawKey.trim();
const val = rawVal.trim();
if (key === "physical id") physicalId = val;
else if (key === "core id") pairs.add(`${physicalId}:${val}`);
}
return pairs.size > 0 ? pairs.size : void 0;
}
function parseCpuModel(cpuinfo) {
return cpuinfo.match(/^model name\s*:\s*(.+)$/m)?.[1]?.trim();
}
function parseNvidiaSmi(csv) {
const gpus = [];
for (const line of csv.split("\n")) {
if (!line.trim()) continue;
const parts = line.split(",").map((p) => p.trim());
if (parts.length < 2) continue;
const mib = parseFloat(parts[1]);
gpus.push({
vendor: "nvidia",
name: parts[0],
vramBytes: Number.isFinite(mib) ? Math.round(mib * 1024 * 1024) : void 0,
driver: parts[2] || void 0,
computeCapability: parts[3] || void 0
});
}
return gpus;
}
async function detectNvidia(warnings) {
try {
const { stdout } = await execFileAsync$2("nvidia-smi", [
"--query-gpu=name,memory.total,driver_version,compute_cap",
"--format=csv,noheader,nounits"
], { timeout: SMI_TIMEOUT_MS });
return parseNvidiaSmi(stdout);
} catch (err) {
if (err?.code !== "ENOENT") warnings.push("nvidia-smi present but did not respond");
return [];
}
}
async function detectAmd(warnings) {
try {
const { stdout } = await execFileAsync$2(
"rocm-smi",
["--showproductname", "--showmeminfo", "vram", "--json"],
{ timeout: SMI_TIMEOUT_MS }
);
const parsed = JSON.parse(stdout);
const gpus = [];
for (const card of Object.values(parsed)) {
const total = Object.entries(card).find(([k]) => /vram.*total|total.*vram/i.test(k))?.[1];
const bytes = total ? parseInt(total, 10) : NaN;
gpus.push({
vendor: "amd",
name: card["Card series"] || card["Card model"] || "AMD GPU",
vramBytes: Number.isFinite(bytes) ? bytes : void 0
});
}
return gpus;
} catch (err) {
if (err?.code !== "ENOENT") warnings.push("rocm-smi present but did not respond");
return [];
}
}
async function detectSysfsDrm() {
const gpus = [];
try {
for (const card of await fs$1.promises.readdir("/sys/class/drm")) {
if (!/^card\d+$/.test(card)) continue;
try {
const raw = await fs$1.promises.readFile(
`/sys/class/drm/${card}/device/mem_info_vram_total`,
"utf8"
);
const bytes = parseInt(raw.trim(), 10);
if (Number.isFinite(bytes) && bytes > 0) {
gpus.push({ vendor: "unknown", name: card, vramBytes: bytes });
}
} catch {
}
}
} catch {
}
return gpus;
}
async function detectLinux() {
const warnings = [];
const [meminfo, cpuinfo] = await Promise.all([
fs$1.promises.readFile("/proc/meminfo", "utf8").catch(() => ""),
fs$1.promises.readFile("/proc/cpuinfo", "utf8").catch(() => "")
]);
const ramTotalBytes = parseMemTotalBytes(meminfo) ?? os$1.totalmem();
const cpuModel = parseCpuModel(cpuinfo) ?? os$1.cpus()[0]?.model ?? "unknown";
const cpuThreads = os$1.cpus().length;
const cpuCoresPhysical = parsePhysicalCores(cpuinfo) ?? cpuThreads;
let gpus = await detectNvidia(warnings);
if (gpus.length === 0) gpus = await detectAmd(warnings);
if (gpus.length === 0) gpus = await detectSysfsDrm();
const vramTotal = gpus.reduce((sum, g) => sum + (g.vramBytes ?? 0), 0);
const usableModelBytes = vramTotal > 0 ? vramTotal : Math.floor(ramTotalBytes * 0.5);
const memBandwidthGBs = gpus.length > 0 ? discreteGpuBandwidthGBs(gpus[0].name) : void 0;
return {
cpuModel,
cpuCoresPhysical,
cpuThreads,
ramTotalBytes,
gpus,
unifiedMemory: false,
usableModelBytes,
memBandwidthGBs,
warnings
};
}
const execFileAsync$1 = util.promisify(child_process.execFile);
const PS_TIMEOUT_MS = 8e3;
async function powershell(script) {
const { stdout } = await execFileAsync$1(
"powershell.exe",
["-NoProfile", "-NonInteractive", "-Command", script],
{ timeout: PS_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 }
);
return stdout;
}
function parseWindowsGpus(json) {
let parsed;
try {
parsed = JSON.parse(json);
} catch {
return [];
}
const rows = Array.isArray(parsed) ? parsed : [parsed];
const gpus = [];
for (const row of rows) {
if (!row?.Name) continue;
const raw = typeof row.VramBytes === "string" ? parseInt(row.VramBytes, 10) : row.VramBytes;
const lower = row.Name.toLowerCase();
gpus.push({
vendor: lower.includes("nvidia") || lower.includes("geforce") ? "nvidia" : lower.includes("amd") || lower.includes("radeon") ? "amd" : lower.includes("intel") ? "intel" : "unknown",
name: row.Name,
// Guard the uint32 saturation value even here, in case a driver key is
// missing and something upstream fell back to AdapterRAM.
vramBytes: typeof raw === "number" && Number.isFinite(raw) && raw > 0 && raw !== 4294967295 ? raw : void 0,
driver: row.Driver || void 0
});
}
return gpus;
}
const GPU_QUERY = `
$ErrorActionPreference='SilentlyContinue'
$base='HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}'
Get-ChildItem $base | Where-Object { $_.PSChildName -match '^\\d{4}$' } | ForEach-Object {
$p = Get-ItemProperty $_.PSPath
if ($p.DriverDesc) {
[pscustomobject]@{
Name = $p.DriverDesc
VramBytes = $p.'HardwareInformation.qwMemorySize'
Driver = $p.DriverVersion
}
}
} | ConvertTo-Json -Compress
`.trim();
async function detectWindows() {
const warnings = [];
const cpus = os$1.cpus();
const cpuModel = cpus[0]?.model ?? "unknown";
const cpuThreads = cpus.length;
const ramTotalBytes = os$1.totalmem();
let cpuCoresPhysical = cpuThreads;
try {
const out = await powershell(
"(Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfCores -Sum).Sum"
);
const cores = parseInt(out.trim(), 10);
if (Number.isFinite(cores) && cores > 0) cpuCoresPhysical = cores;
} catch {
warnings.push("could not read physical core count; using logical count");
}
let gpus = [];
try {
const { stdout } = await execFileAsync$1("nvidia-smi", [
"--query-gpu=name,memory.total,driver_version,compute_cap",
"--format=csv,noheader,nounits"
], { timeout: PS_TIMEOUT_MS });
gpus = parseNvidiaSmi(stdout);
} catch {
}
if (gpus.length === 0) {
try {
gpus = parseWindowsGpus(await powershell(GPU_QUERY));
} catch {
warnings.push("could not enumerate display adapters");
}
}
const vramTotal = gpus.reduce((sum, g) => sum + (g.vramBytes ?? 0), 0);
const usableModelBytes = vramTotal > 0 ? vramTotal : Math.floor(ramTotalBytes * 0.5);
const memBandwidthGBs = gpus.length > 0 ? discreteGpuBandwidthGBs(gpus[0].name) : void 0;
return {
cpuModel,
cpuCoresPhysical,
cpuThreads,
ramTotalBytes,
gpus,
unifiedMemory: false,
usableModelBytes,
memBandwidthGBs,
warnings
};
}
const CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
let cache;
let inFlight;
async function diskFreeBytes(dir) {
try {
const stats = await fs$1.promises.statfs(dir);
return Number(stats.bavail) * Number(stats.bsize);
} catch {
return 0;
}
}
async function detectUncached() {
const platform = process.platform;
const base = platform === "darwin" ? await detectDarwin() : platform === "linux" ? await detectLinux() : platform === "win32" ? await detectWindows() : null;
if (!base) {
const ramTotalBytes = os$1.totalmem();
return {
schemaVersion: 1,
platform,
arch: process.arch,
cpuModel: os$1.cpus()[0]?.model ?? "unknown",
cpuCoresPhysical: os$1.cpus().length,
cpuThreads: os$1.cpus().length,
ramTotalBytes,
gpus: [],
unifiedMemory: false,
usableModelBytes: Math.floor(ramTotalBytes * 0.5),
bandwidthSource: "unknown",
diskFreeBytes: await diskFreeBytes(os$1.homedir()),
detectedAt: Date.now(),
warnings: [`unsupported platform "${platform}"; reporting CPU/RAM only`]
};
}
return {
schemaVersion: 1,
platform,
arch: process.arch,
cpuModel: base.cpuModel,
cpuCoresPhysical: base.cpuCoresPhysical,
cpuThreads: base.cpuThreads,
ramTotalBytes: base.ramTotalBytes,
gpus: base.gpus,
unifiedMemory: base.unifiedMemory,
usableModelBytes: base.usableModelBytes,
memBandwidthGBs: base.memBandwidthGBs,
bandwidthSource: base.memBandwidthGBs !== void 0 ? "chip-table" : "unknown",
diskFreeBytes: await diskFreeBytes(os$1.homedir()),
detectedAt: Date.now(),
warnings: base.warnings
};
}
async function detectHardware(opts) {
if (!opts?.force && cache && Date.now() - cache.at < CACHE_TTL_MS) {
return cache.profile;
}
if (inFlight) return inFlight;
inFlight = detectUncached().then((profile) => {
cache = { at: Date.now(), profile };
const gpu = profile.gpus[0]?.name ?? "no GPU";
logger.debug(
`[HARDWARE] ${profile.cpuModel} | ${fmtGb(profile.ramTotalBytes)} RAM | ${gpu} | usable ${fmtGb(profile.usableModelBytes)} | ${profile.memBandwidthGBs ?? "?"} GB/s (${profile.bandwidthSource})` + (profile.warnings.length ? ` | warnings: ${profile.warnings.join("; ")}` : "")
);
return profile;
}).finally(() => {
inFlight = void 0;
});
return inFlight;
}
function fmtGb(bytes) {
return `${(bytes / 1024 ** 3).toFixed(1)}GB`;
}
function recognizeClaude(parsed) {
if (!parsed || typeof parsed !== "object") return false;
let obj = parsed;
if (obj.claudeAiOauth && typeof obj.claudeAiOauth === "object") {
obj = obj.claudeAiOauth;
}
return typeof obj.access_token === "string" || typeof obj.accessToken === "string" || typeof obj.token === "string";
}
function recognizeCodex(parsed) {
if (!parsed || typeof parsed !== "object") return false;
const obj = parsed;
if (typeof obj.OPENAI_API_KEY === "string") return true;
if (obj.tokens && typeof obj.tokens === "object") {
const t = obj.tokens;
return typeof t.access_token === "string" || typeof t.id_token === "string" || typeof t.refresh_token === "string";
}
return false;
}
function recognizeGemini(parsed) {
if (!parsed || typeof parsed !== "object") return false;
const obj = parsed;
return typeof obj.access_token === "string" || typeof obj.refresh_token === "string" || typeof obj.client_id === "string" || typeof obj.api_key === "string";
}
function recognizeConsortium(parsed) {
if (!parsed || typeof parsed !== "object") return false;
const obj = parsed;
return typeof obj.token === "string" || typeof obj.secret === "string";
}
function recognizeGrok(parsed) {
if (!parsed || typeof parsed !== "object") return false;
return Object.values(parsed).some((v) => {
if (!v || typeof v !== "object") return false;
const key = v.key;
return typeof key === "string" && key.length > 0;
});
}
function candidatesFor(agent, opts) {
const home = opts.homeDir ?? os.homedir();
const env = opts.env ?? process.env;
switch (agent) {
case "claude":
return [
{
path: path.join(
resolveClaudeConfigDir({ env, homeDir: home }),
".credentials.json"
),
recognize: recognizeClaude
},
{ path: path.join(home, ".config", "claude", "auth.json"), recognize: recognizeClaude }
];
case "codex":
return [
{
path: path.join(resolveCodexHome({ env, homeDir: home }), "auth.json"),
recognize: recognizeCodex
},
{ path: path.join(home, ".config", "openai", "auth.json"), recognize: recognizeCodex }
];
case "gemini":
return [
{ path: path.join(home, ".config", "gemini", "credentials.json"), recognize: recognizeGemini }
];
case "grok":
return [
{ path: path.join(home, ".grok", "auth.json"), recognize: recognizeGrok }
];
case "consortium": {
const consortiumHome = env.CONSORTIUM_HOME_DIR || path.join(home, ".consortium");
return [
{ path: path.join(consortiumHome, "access.key"), recognize: recognizeConsortium }
];
}
default:
return [];
}
}
function clearAgentCredentials(agent, opts = {}) {
const candidates = candidatesFor(agent, opts);
const cleared = [];
const skipped = [];
for (const cand of candidates) {
if (!fs.existsSync(cand.path)) continue;
let raw;
try {
raw = fs.readFileSync(cand.path, "utf8");
} catch (err) {
skipped.push({ path: cand.path, reason: `read failed: ${err.message}` });
logger.info(`[auth-clear-creds] skip ${cand.path}: read failed`);
continue;
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
skipped.push({ path: cand.path, reason: "unrecognized shape (not JSON)" });
logger.info(`[auth-clear-creds] refuse ${cand.path}: not JSON`);
continue;
}
if (!cand.recognize(parsed)) {
skipped.push({ path: cand.path, reason: "unrecognized shape" });
logger.info(`[auth-clear-creds] refuse ${cand.path}: schema not recognized`);
continue;
}
try {
fs.unlinkSync(cand.path);
cleared.push(cand.path);
logger.info(`[auth-clear-creds] cleared ${cand.path} for agent=${agent}`);
} catch (err) {
skipped.push({ path: cand.path, reason: `unlink failed: ${err.message}` });
logger.info(`[auth-clear-creds] unlink ${cand.path} failed: ${err.message}`);
}
}
return { ok: cleared.length > 0 || skipped.length === 0, cleared, skipped };
}
const TERMINAL_RETENTION_MS$3 = 5 * 60 * 1e3;
class DeviceCodeFlow {
records = /* @__PURE__ */ new Map();
now;
setTimer;
clearTimer;
constructor(opts = {}) {
this.now = opts.now ?? (() => Date.now());
this.setTimer = opts.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
this.clearTimer = opts.clearTimeout ?? ((h) => clearTimeout(h));
}
/** Begin a new device-code flow against `adapter`. */
async start(adapter) {
const init = await adapter.initiate();
const intervalSec = Math.max(1, Math.floor(init.interval));
const expiresInSec = Math.max(1, Math.floor(init.expiresIn));
const id = node_crypto.randomUUID();
const startedAt = this.now();
const expiresAtMs = startedAt + expiresInSec * 1e3;
const handle = {
id,
agent: adapter.agent,
kind: "device-code",
startedAt
};
const record = {
handle,
status: {
status: "awaiting-user",
handle,
payload: {
kind: "device-code",
verificationUrl: init.verificationUrl,
userCode: init.userCode,
expiresAt: expiresAtMs
}
},
adapter,
deviceCode: init.deviceCode,
expiresAtMs,
intervalMs: intervalSec * 1e3,
pollTimer: null,
gcTimer: null,
cancelled: false
};
this.records.set(id, record);
logger.debug(`[auth-flow ${adapter.agent}] device-code started id=${id} expiresIn=${expiresInSec}s interval=${intervalSec}s`);
this.schedulePoll(record);
return handle;
}
/** Snapshot of the current flow status. */
async getStatus(id) {
const record = this.records.get(id);
if (!record) {
throw new Error(`device-code flow not found: ${id}`);
}
return record.status;
}
/** Stop the poller, mark `cancelled`, run adapter.cleanup. */
async cancel(id) {
const record = this.records.get(id);
if (!record) return;
if (this.isTerminal(record.status)) return;
record.cancelled = true;
this.stopPoll(record);
const finishedAt = this.now();
record.status = {
status: "cancelled",
handle: record.handle,
finishedAt
};
logger.debug(`[auth-flow ${record.adapter.agent}] cancelled id=${id}`);
if (record.adapter.cleanup) {
try {
await record.adapter.cleanup();
} catch (err) {
logger.debug(`[auth-flow ${record.adapter.agent}] cleanup error: ${shapeError(err)}`);
}
}
this.scheduleGc(record);
}
/**
* Test/admin hook: drop all in-memory state and clear timers. Idempotent.
*/
dispose() {
for (const record of this.records.values()) {
this.stopPoll(record);
this.cancelGc(record);
}
this.records.clear();
}
// ---- internals -------------------------------------------------------
schedulePoll(record) {
if (record.cancelled || this.isTerminal(record.status)) return;
const remaining = record.expiresAtMs - this.now();
if (remaining <= 0) {
this.transitionExpired(record);
return;
}
const delay = Math.min(record.intervalMs, remaining);
record.pollTimer = this.setTimer(() => {
void this.runPollOnce(record);
}, delay);
}
stopPoll(record) {
if (record.pollTimer != null) {
this.clearTimer(record.pollTimer);
record.pollTimer = null;
}
}
async runPollOnce(record) {
record.pollTimer = null;
if (record.cancelled || this.isTerminal(record.status)) return;
if (this.now() >= record.expiresAtMs) {
this.transitionExpired(record);
return;
}
let result;
try {
result = await record.adapter.poll(record.deviceCode);
} catch (err) {
logger.debug(`[auth-flow ${record.adapter.agent}] poll threw: ${shapeError(err)} \u2014 treating as pending`);
this.schedulePoll(record);
return;
}
if (record.cancelled || this.isTerminal(record.status)) return;
switch (result.state) {
case "pending":
this.schedulePoll(record);
return;
case "success": {
try {
await record.adapter.persist(result.credentials);
} catch (err) {
this.transitionFailed(record, `persist failed: ${shapeError(err)}`);
return;
}
if (record.cancelled) return;
const identity = record.adapter.extractIdentity?.(result.credentials);
record.status = {
status: "succeeded",
handle: record.handle,
finishedAt: this.now(),
identity
};
logger.debug(`[auth-flow ${record.adapter.agent}] succeeded id=${record.handle.id}`);
invalidateProbeCache();
this.scheduleGc(record);
return;
}
case "denied":
case "expired":
case "error":
if (result.state === "expired") {
this.transitionExpired(record);
} else {
this.transitionFailed(record, result.error);
}
return;
}
}
transitionFailed(record, error) {
this.stopPoll(record);
record.status = {
status: "failed",
handle: record.handle,
finishedAt: this.now(),
error
};
logger.debug(`[auth-flow ${record.adapter.agent}] failed id=${record.handle.id}: ${error}`);
this.scheduleGc(record);
}
transitionExpired(record) {
this.stopPoll(record);
record.status = {
status: "expired",
handle: record.handle,
finishedAt: this.now()
};
logger.debug(`[auth-flow ${record.adapter.agent}] expired id=${record.handle.id}`);
this.scheduleGc(record);
}
scheduleGc(record) {
this.cancelGc(record);
record.gcTimer = this.setTimer(() => {
this.records.delete(record.handle.id);
}, TERMINAL_RETENTION_MS$3);
}
cancelGc(record) {
if (record.gcTimer != null) {
this.clearTimer(record.gcTimer);
record.gcTimer = null;
}
}
isTerminal(status) {
return status.status === "succeeded" || status.status === "failed" || status.status === "cancelled" || status.status === "expired";
}
}
function shapeError(err) {
if (err instanceof Error) return err.message;
return String(err);
}
const DEFAULT_GATEWAY_PORT = 18789;
const GATEWAY_CHECK_TIMEOUT_MS = 2e3;
const COMMON_BINARY_PATHS = [
"/usr/local/bin/openclaw",
"/usr/bin/openclaw",
"/opt/homebrew/bin/openclaw",
path$1.join(os$1.homedir(), ".nvm/versions/node"),
// handled specially below
path$1.join(os$1.homedir(), ".local/share/fnm/node-versions"),
// handled specially below
path$1.join(os$1.homedir(), ".local/bin/openclaw"),
path$1.join(os$1.homedir(), ".npm-global/bin/openclaw")
];
function whichBinary(name) {
return new Promise((resolve) => {
const extraPaths = [
"/usr/local/bin",
"/opt/homebrew/bin",
path$1.join(os$1.homedir(), ".local/bin"),
path$1.join(os$1.homedir(), ".npm-global/bin")
];
const extendedPath = [process.env.PATH, ...extraPaths].filter(Boolean).join(":");
child_process.execFile("which", [name], { timeout: 5e3, env: { ...process.env, PATH: extendedPath } }, (error, stdout) => {
if (!error && stdout.trim()) {
resolve(stdout.trim());
return;
}
(async () => {
for (const p of COMMON_BINARY_PATHS) {
if (p.includes(".nvm") || p.includes("fnm")) continue;
try {
await fs$2.access(p);
logger.debug(`[OPENCLAW DETECT] Found binary via fallback path: ${p}`);
resolve(p);
return;
} catch {
}
}
const nvmDir = path$1.join(os$1.homedir(), ".nvm/versions/node");
try {
const versions = await fs$2.readdir(nvmDir);
for (const ver of versions.sort().reverse()) {
const binPath = path$1.join(nvmDir, ver, "bin", name);
try {
await fs$2.access(binPath);
logger.debug(`[OPENCLAW DETECT] Found binary in nvm: ${binPath}`);
resolve(binPath);
return;
} catch {
}
}
} catch {
}
const fnmDir = path$1.join(os$1.homedir(), ".local/share/fnm/node-versions");
try {
const versions = await fs$2.readdir(fnmDir);
for (const ver of versions.sort().reverse()) {
const binPath = path$1.join(fnmDir, ver, "installation/bin", name);
try {
await fs$2.access(binPath);
logger.debug(`[OPENCLAW DETECT] Found binary in fnm: ${binPath}`);
resolve(binPath);
return;
} catch {
}
}
} catch {
}
resolve(null);
})();
});
});
}
function getOpenClawVersion(binaryPath) {
return new Promise((resolve) => {
child_process.execFile(binaryPath, ["--version"], { timeout: 5e3 }, (error, stdout) => {
if (error || !stdout.trim()) {
resolve(null);
return;
}
const match = stdout.trim().match(/(\d+\.\d+\.\d+[\w.-]*)/);
resolve(match ? match[1] : stdout.trim());
});
});
}
async function readOpenClawConfig(workspaceDir) {
try {
const configPath = path$1.join(workspaceDir, "openclaw.json");
const raw = await fs$2.readFile(configPath, "utf-8");
const config = JSON.parse(raw);
const channels = [];
if (config.channels && typeof config.channels === "object") {
for (const [name, channelConfig] of Object.entries(config.channels)) {
if (channelConfig && typeof channelConfig === "object" && channelConfig.enabled !== false) {
channels.push(name);
}
}
}
const gatewayPort = typeof config.gateway?.port === "number" ? config.gateway.port : DEFAULT_GATEWAY_PORT;
return { channels, gatewayPort };
} catch {
return { channels: [], gatewayPort: DEFAULT_GATEWAY_PORT };
}
}
async function detectAgents(workspaceDir, binaryPath) {
const bin = binaryPath || "openclaw";
let cliFailed = false;
try {
const cliResult = await new Promise((resolve) => {
child_process.execFile(bin, ["agents", "list", "--json"], { timeout: 1e4 }, (error, stdout) => {
if (error || !stdout.trim()) {
cliFailed = true;
resolve({ count: 0, agents: [] });
return;
}
try {
const parsed = JSON.parse(stdout);
const list = Array.isArray(parsed) ? parsed : parsed.agents || [];
const agents = list.map((a) => ({
id: a.id || a.name || "unknown",
name: a.identityName || a.name || a.id || "unknown",
status: "unknown",
...typeof a.model === "string" && a.model ? { model: a.model } : {}
}));
resolve({ count: agents.length, agents });
} catch {
cliFailed = true;
resolve({ count: 0, agents: [] });
}
});
});
if (!cliFailed) return { ...cliResult, degraded: false };
} catch {
cliFailed = true;
}
try {
const files = await fs$2.readdir(workspaceDir);
const agents = [];
if (files.includes("AGENTS.md")) {
agents.push({ id: "default", name: "Default Agent", status: "unknown" });
}
try {
const agentsDir = path$1.join(workspaceDir, "agents");
const agentDirs = await fs$2.readdir(agentsDir);
for (const dir of agentDirs) {
agents.push({ id: dir, name: dir, status: "unknown" });
}
} catch {
}
return { count: agents.length, agents, degraded: cliFailed };
} catch {
return { count: 0, agents: [], degraded: cliFailed };
}
}
function checkGatewayHealth(port) {
return new Promise((resolve) => {
const req = http.get({
hostname: "127.0.0.1",
port,
path: "/health",
timeout: GATEWAY_CHECK_TIMEOUT_MS
}, (res) => {
resolve(res.statusCode !== void 0 && res.statusCode >= 200 && res.statusCode < 300);
res.resume();
});
const fallback = () => {
child_process.execFile("pgrep", ["-f", "openclaw-gateway"], { timeout: 3e3 }, (err, stdout) => {
if (err || stdout.trim().length === 0) {
resolve(false);
return;
}
const probe = net.connect({ host: "127.0.0.1", port });
const done = (ok) => {
probe.removeAllListeners();
probe.destroy();
resolve(ok);
};
probe.setTimeout(GATEWAY_CHECK_TIMEOUT_MS);
probe.once("connect", () => done(true));
probe.once("error", () => done(false));
probe.once("timeout", () => done(false));
});
};
req.on("error", fallback);
req.on("timeout", () => {
req.destroy();
fallback();
});
});
}
function detectRunningSandboxes() {
return new Promise((resolve) => {
child_process.execFile("/bin/ps", ["aux"], { timeout: 5e3 }, (error, stdout) => {
if (error || !stdout.trim()) {
resolve([]);
return;
}
const seen = /* @__PURE__ */ new Set();
const agents = [];
for (const line of stdout.split("\n")) {
const match = line.match(/openclaw-sbx-agent-([a-zA-Z0-9_-]+)-[0-9a-f]+/);
if (match && !seen.has(match[1])) {
seen.add(match[1]);
agents.push({ id: match[1], name: match[1], status: "active" });
}
}
resolve(agents);
});
});
}
async function detectOpenClaw(customWorkspaceDir) {
const workspaceDir = customWorkspaceDir || process.env.OPENCLAW_WORKSPACE_DIR || path$1.join(os$1.homedir(), ".openclaw");
const [binaryPath, workspaceExists] = await Promise.all([
whichBinary("openclaw"),
fs$2.access(workspaceDir).then(() => true).catch(() => false)
]);
const installed = binaryPath !== null;
if (!installed && !workspaceExists) {
return {
installed: false,
binaryPath: null,
version: null,
workspaceExists: false,
workspaceDir,
gatewayRunning: false,
gatewayPort: DEFAULT_GATEWAY_PORT,
channels: [],
agentCount: 0,
agents: []
};
}
const [version, config, agentInfo, sandboxAgents] = await Promise.all([
binaryPath ? getOpenClawVersion(binaryPath) : Promise.resolve(null),
workspaceExists ? readOpenClawConfig(workspaceDir) : Promise.resolve({ channels: [], gatewayPort: DEFAULT_GATEWAY_PORT }),
detectAgents(workspaceDir, binaryPath),
detectRunningSandboxes()
]);
const gatewayRunning = await checkGatewayHealth(config.gatewayPort);
const mergedAgents = [...agentInfo.agents];
const knownIds = new Set(mergedAgents.map((a) => a.id));
for (const sbx of sandboxAgents) {
if (!knownIds.has(sbx.id)) {
mergedAgents.push(sbx);
knownIds.add(sbx.id);
} else {
const existing = mergedAgents.find((a) => a.id === sbx.id);
if (existing) existing.status = "active";
}
}
const result = {
installed,
binaryPath,
version,
workspaceExists,
workspaceDir,
gatewayRunning,
gatewayPort: config.gatewayPort,
channels: config.channels,
agentCount: mergedAgents.length,
agents: mergedAgents,
detectionDegraded: agentInfo.degraded
};
logger.debug(`[OPENCLAW DETECT] Detection result: installed=${installed}, workspace=${workspaceExists}, gateway=${gatewayRunning}, version=${version}, channels=[${config.channels.join(",")}], agents=${mergedAgents.length} (configured=${agentInfo.count}, sandboxes=${sandboxAgents.length})`);
return result;
}
function toMetadataFields(result) {
return {
openclawInstalled: result.installed,
openclawVersion: result.version,
openclawWorkspaceExists: result.workspaceExists,
openclawGatewayRunning: result.gatewayRunning,
openclawGatewayPort: result.gatewayPort,
openclawChannels: result.channels,
openclawAgentCount: result.agentCount
};
}
const URL_RE$1 = /(https?:\/\/[^\s'"<>]+)/;
const URL_CODE_RE = /[?&]user_code=([A-Za-z0-9._-]+)/;
const BARE_CODE_RE = /\b([A-Z0-9]{3,}-[A-Z0-9]{3,})\b/;
const EXPIRY_RE = /expires? in (\d+)\s*(minute|min|second|sec)/i;
const DEFAULT_EXPIRES_IN = 600;
const DEFAULT_POLL_INTERVAL = 3;
const PARSE_TIMEOUT_MS = 45e3;
function extendedPathEnv$2() {
const extra = [
"/usr/local/bin",
"/opt/homebrew/bin",
path.join(os.homedir(), ".local/bin"),
path.join(os.homedir(), ".npm-global/bin")
];
return { ...process.env, PATH: [process.env.PATH, ...extra].filter(Boolean).join(":") };
}
function parseDeviceAuthOutput(buffer) {
const urlMatch = buffer.match(URL_RE$1);
if (!urlMatch) return null;
const url = urlMatch[1].replace(/[.,)\]]+$/, "");
const fromUrl = url.match(URL_CODE_RE)?.[1];
const code = fromUrl ?? buffer.match(BARE_CODE_RE)?.[1] ?? "";
const expiryMatch = buffer.match(EXPIRY_RE);
const expiresIn = expiryMatch ? Number(expiryMatch[1]) * (expiryMatch[2].startsWith("min") ? 60 : 1) : null;
return { url, code, urlIncludesCode: fromUrl !== void 0, expiresIn };
}
class NativeDeviceAuthAdapter {
agent;
/** Set once initiate() parses the CLI's output; read by the RPC layer. */
urlIncludesCode = false;
binary;
argv;
spawnImpl;
resolveBinary;
parseTimeoutMs;
defaultExpiresIn;
child = null;
exitCode = void 0;
buffer = "";
constructor(agent, opts = {}) {
const cap = getAgentAuthCapability(agent);
if (!cap || cap.remoteMode !== "device-code" || !cap.remoteLoginCommand) {
throw new Error(`native device-auth is not available for agent '${agent}'`);
}
this.agent = agent;
this.binary = cap.binary;
this.argv = cap.remoteLoginCommand;
this.spawnImpl = opts.spawnImpl ?? node_child_process.spawn;
this.resolveBinary = opts.resolveBinary ?? whichBinary;
this.parseTimeoutMs = opts.parseTimeoutMs ?? PARSE_TIMEOUT_MS;
this.defaultExpiresIn = opts.defaultExpiresIn ?? DEFAULT_EXPIRES_IN;
}
async initiate() {
const binaryPath = await this.resolveBinary(this.binary);
if (!binaryPath) {
throw new Error(`${this.binary} CLI not found on PATH \u2014 install it first`);
}
const spawnOptions = { env: extendedPathEnv$2(), stdio: ["ignore", "pipe", "pipe"] };
const child = this.spawnImpl(binaryPath, [...this.argv], spawnOptions);
this.child = child;
child.stdout?.on("data", (c) => {
this.buffer += c.toString();
});
child.stderr?.on("data", (c) => {
this.buffer += c.toString();
});
child.on("exit", (code) => {
this.exitCode = code;
});
child.on("error", (err) => {
this.exitCode = this.exitCode ?? -1;
this.buffer += `
${err.message}`;
});
const parsed = await this.waitForPrompt();
this.urlIncludesCode = parsed.urlIncludesCode;
logger.debug(
`[auth-flow ${this.agent}] device-auth prompt parsed (codeInUrl=${parsed.urlIncludesCode}, hasCode=${parsed.code.length > 0})`
);
return {
verificationUrl: parsed.url,
userCode: parsed.code,
// The CLI owns the real device_code; the runner only echoes this
// back to poll(), which ignores it.
deviceCode: "native",
expiresIn: parsed.expiresIn ?? this.defaultExpiresIn,
interval: DEFAULT_POLL_INTERVAL
};
}
async poll(_deviceCode) {
if (this.exitCode === void 0) return { state: "pending" };
if (this.exitCode === 0) {
logger.debug(`[auth-flow ${this.agent}] CLI exited 0 \u2014 credentials written by the CLI`);
return { state: "success", credentials: { native: true, agent: this.agent } };
}
const tail = this.buffer.split("\n").map((l) => l.trim()).filter(Boolean).slice(-3).join(" | ");
return {
state: "error",
error: `${this.binary} ${this.argv.join(" ")} exited ${this.exitCode}${tail ? `: ${tail}` : ""}`
};
}
/** No-op: the agent CLI already wrote its own credential file. */
async persist() {
}
async cleanup() {
if (this.child && this.exitCode === void 0) {
try {
this.child.kill("SIGTERM");
} catch {
}
}
this.buffer = "";
}
waitForPrompt() {
const started = Date.now();
return new Promise((resolve, reject) => {
const tick = () => {
const parsed = parseDeviceAuthOutput(this.buffer);
if (parsed) {
resolve(parsed);
return;
}
if (this.exitCode !== void 0) {
const tail = this.buffer.split("\n").map((l) => l.trim()).filter(Boolean).slice(-3).join(" | ");
reject(new Error(
`${this.binary} exited ${this.exitCode} before printing a verification URL${tail ? `: ${tail}` : ""}`
));
return;
}
if (Date.now() - started > this.parseTimeoutMs) {
void this.cleanup();
reject(new Error(`${this.binary} did not print a verification URL in time`));
return;
}
setTimeout(tick, 250);
};
tick();
});
}
}
const CODE_PROMPT_RE = /paste code|enter the code|authorization code[^\n]*>|code here/i;
const URL_RE = /(https?:\/\/[^\s'"<>]+)/;
const DEFAULT_TTL_MS = 10 * 60 * 1e3;
const TERMINAL_RETENTION_MS$2 = 5 * 60 * 1e3;
const PROMPT_TIMEOUT_MS = 3e4;
function extendedPathEnv$1() {
const extra = [
"/usr/local/bin",
"/opt/homebrew/bin",
path.join(os.homedir(), ".local/bin"),
path.join(os.homedir(), ".npm-global/bin")
];
return { ...process.env, PATH: [process.env.PATH, ...extra].filter(Boolean).join(":") };
}
class CliLoginFlow {
records = /* @__PURE__ */ new Map();
spawnImpl;
resolveBinary;
now;
ttlMs;
promptTimeoutMs;
constructor(opts = {}) {
this.spawnImpl = opts.spawnImpl ?? node_child_process.spawn;
this.resolveBinary = opts.resolveBinary ?? whichBinary;
this.now = opts.now ?? (() => Date.now());
this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
this.promptTimeoutMs = opts.promptTimeoutMs ?? PROMPT_TIMEOUT_MS;
}
/** True when this agent's own CLI can run the login. */
static supports(agent) {
const cap = getAgentAuthCapability(agent);
return Boolean(cap?.loginCommand);
}
async start(agent) {
const cap = getAgentAuthCapability(agent);
if (!cap?.loginCommand) {
throw new Error(`auth-flow: ${agent} has no CLI login command`);
}
const binaryPath = await this.resolveBinary(cap.binary);
if (!binaryPath) {
throw new Error(`${cap.binary} is not installed on this machine \u2014 install it first`);
}
const startedAt = this.now();
const handle = { id: node_crypto.randomUUID(), agent, kind: "paste-code", startedAt };
const child = this.spawnImpl(binaryPath, [...cap.loginCommand], {
env: extendedPathEnv$1(),
stdio: ["pipe", "pipe", "pipe"]
});
const record = {
handle,
child,
buffer: "",
settled: false,
expiresAtMs: startedAt + this.ttlMs,
gcTimer: null,
status: { status: "pending", handle },
ready: Promise.resolve()
};
child.stdout?.on("data", (c) => {
record.buffer += c.toString();
});
child.stderr?.on("data", (c) => {
record.buffer += c.toString();
});
child.on("exit", (code) => this.onExit(record, code));
child.on("error", (err) => this.fail(record, err.message));
this.records.set(handle.id, record);
record.ready = this.waitForPrompt(record);
await record.ready;
logger.debug(`[auth-flow ${agent}] CLI login started id=${handle.id}`);
return record.status;
}
poll(id) {
const record = this.records.get(id);
if (!record) return void 0;
if (!record.settled && this.now() >= record.expiresAtMs) {
this.kill(record);
record.status = { status: "expired", handle: record.handle, finishedAt: this.now() };
record.settled = true;
this.scheduleGc(record);
}
return record.status;
}
/**
* Relay the code the user copied from the vendor's page into the CLI's
* stdin. The CLI performs the exchange, so a wrong code fails inside the
* vendor's own client with the vendor's own error, not ours.
*/
async submit(id, code) {
const record = this.records.get(id);
if (!record) throw new Error(`cli login flow not found: ${id}`);
if (record.settled) return record.status;
const trimmed = code.trim();
if (!trimmed) return this.fail(record, "no authorization code provided");
const stdin = record.child.stdin;
if (!stdin || stdin.destroyed) {
return this.fail(record, "the login process is no longer accepting input");
}
stdin.write(`${trimmed}
`);
record.status = { status: "pending", handle: record.handle };
logger.debug(`[auth-flow ${record.handle.agent}] relayed authorization code to the CLI`);
return record.status;
}
async cancel(id) {
const record = this.records.get(id);
if (!record || record.settled) return;
this.kill(record);
record.status = { status: "cancelled", handle: record.handle, finishedAt: this.now() };
record.settled = true;
this.scheduleGc(record);
}
dispose() {
for (const record of this.records.values()) {
if (record.gcTimer) clearTimeout(record.gcTimer);
if (!record.settled) this.kill(record);
}
this.records.clear();
}
// ── internals ────────────────────────────────────────────────────────
/**
* Wait for the CLI to print its authorize URL, then publish
* `awaiting-user`. If the CLI finishes or dies first, the exit handler has
* already settled the record and we leave it alone.
*/
async waitForPrompt(record) {
const deadline = this.now() + this.promptTimeoutMs;
for (; ; ) {
if (record.settled) return;
const url = record.buffer.match(URL_RE)?.[1]?.replace(/[.,)\]]+$/, "");
if (url) {
const wantsCode = CODE_PROMPT_RE.test(record.buffer);
record.status = {
status: "awaiting-user",
handle: record.handle,
payload: {
kind: "paste-code",
authorizationUrl: url,
instructions: wantsCode ? "Open the link, sign in, then paste the code shown in your browser." : "Open the link and approve the sign-in. This finishes on its own.",
expiresAt: record.expiresAtMs
}
};
return;
}
if (this.now() > deadline) {
this.fail(record, `${record.handle.agent} did not print a sign-in link in time`);
return;
}
await new Promise((r) => setTimeout(r, 200));
}
}
onExit(record, code) {
if (record.settled) return;
if (code === 0) {
record.status = { status: "succeeded", handle: record.handle, finishedAt: this.now() };
record.settled = true;
invalidateProbeCache();
this.scheduleGc(record);
logger.debug(`[auth-flow ${record.handle.agent}] CLI login succeeded`);
return;
}
const tail = record.buffer.split("\n").map((l) => l.trim()).filter(Boolean).slice(-3).join(" | ");
this.fail(record, `sign-in exited ${code}${tail ? `: ${tail}` : ""}`);
}
fail(record, error) {
if (record.settled) return record.status;
record.status = { status: "failed", handle: record.handle, finishedAt: this.now(), error };
record.settled = true;
this.kill(record);
this.scheduleGc(record);
logger.debug(`[auth-flow ${record.handle.agent}] CLI login failed: ${error}`);
return record.status;
}
kill(record) {
try {
record.child.kill("SIGTERM");
} catch {
}
}
scheduleGc(record) {
if (record.gcTimer) clearTimeout(record.gcTimer);
record.gcTimer = setTimeout(() => this.records.delete(record.handle.id), TERMINAL_RETENTION_MS$2);
}
}
async function startAuthFlow(agent, mode, runners) {
const log = runners.log ?? (() => {
});
if (mode === "device-code") {
const adapter = runners.pickDeviceCodeAdapter(agent);
const handle = await runners.deviceCode.start(adapter);
return await runners.deviceCode.getStatus(handle.id);
}
if (mode === "paste") {
const handle = runners.paste.start(agent);
const status = runners.paste.poll(handle.id);
if (!status) {
throw new Error(`auth-flow-start: paste flow ${handle.id} vanished immediately after start`);
}
return status;
}
if (mode === "paste-code" || mode === "cli-spawn") {
if (runners.cliLogin.supports(agent)) {
try {
return await runners.cliLogin.start(agent);
} catch (err) {
log(`cli login unavailable for ${agent}: ${err instanceof Error ? err.message : String(err)}`);
}
}
if (agent !== "claude") {
throw new Error(
`auth-flow-start: ${agent} has no paste-code login without its CLI installed`
);
}
return runners.claudePasteCode.start();
}
throw new Error(`auth-flow-start: unsupported mode '${mode}'`);
}
const ENDPOINT$1 = "https://api.anthropic.com/v1/messages";
const TIMEOUT_MS$3 = 5e3;
function describeKey$3(key) {
const trimmed = key.trim();
const tail = trimmed.length >= 4 ? trimmed.slice(-4) : trimmed;
return `<${trimmed.length} chars, \u2026${tail}>`;
}
const validateClaudeKey = async (key) => {
const trimmed = key.trim();
if (!trimmed) {
return { valid: false, error: "empty key" };
}
logger.debug(`[auth/paste/claude] validating key ${describeKey$3(trimmed)}`);
try {
const res = await axios.post(
ENDPOINT$1,
{
model: "claude-3-5-haiku-20241022",
max_tokens: 1,
messages: [{ role: "user", content: "hi" }]
},
{
timeout: TIMEOUT_MS$3,
headers: {
"x-api-key": trimmed,
"anthropic-version": "2023-06-01",
"content-type": "application/json"
},
validateStatus: () => true
}
);
if (res.status === 200) {
return { valid: true };
}
if (res.status === 401 || res.status === 403) {
return { valid: false, error: `unauthorized (status ${res.status})` };
}
if (res.status === 400) {
const body = res.data;
const t = body?.error?.type;
if (t === "authentication_error" || t === "permission_error") {
return { valid: false, error: t };
}
return { valid: true };
}
return { valid: false, error: `unexpected status ${res.status}` };
} catch (err) {
const ax = err;
const msg = ax.code ?? ax.message ?? "network error";
logger.debug(`[auth/paste/claude] validator threw: ${msg}`);
return { valid: false, error: msg };
}
};
const ENDPOINT = "https://api.openai.com/v1/models";
const TIMEOUT_MS$2 = 5e3;
function describeKey$2(key) {
const trimmed = key.trim();
const tail = trimmed.length >= 4 ? trimmed.slice(-4) : trimmed;
return `<${trimmed.length} chars, \u2026${tail}>`;
}
const validateCodexKey = async (key) => {
const trimmed = key.trim();
if (!trimmed) {
return { valid: false, error: "empty key" };
}
logger.debug(`[auth/paste/codex] validating key ${describeKey$2(trimmed)}`);
try {
const res = await axios.get(ENDPOINT, {
timeout: TIMEOUT_MS$2,
headers: {
Authorization: `Bearer ${trimmed}`
},
validateStatus: () => true
});
if (res.status === 200) {
const orgHeader = res.headers["openai-organization"] ?? res.headers["x-organization"];
const result = { valid: true };
if (orgHeader && typeof orgHeader === "string") {
result.identity = { account: orgHeader };
}
return result;
}
if (res.status === 401 || res.status === 403) {
return { valid: false, error: `unauthorized (status ${res.status})` };
}
return { valid: false, error: `unexpected status ${res.status}` };
} catch (err) {
const ax = err;
const msg = ax.code ?? ax.message ?? "network error";
logger.debug(`[auth/paste/codex] validator threw: ${msg}`);
return { valid: false, error: msg };
}
};
const BASE$1 = "https://generativelanguage.googleapis.com/v1/models";
const TIMEOUT_MS$1 = 5e3;
function describeKey$1(key) {
const trimmed = key.trim();
const tail = trimmed.length >= 4 ? trimmed.slice(-4) : trimmed;
return `<${trimmed.length} chars, \u2026${tail}>`;
}
const validateGeminiKey = async (key) => {
const trimmed = key.trim();
if (!trimmed) {
return { valid: false, error: "empty key" };
}
logger.debug(`[auth/paste/gemini] validating key ${describeKey$1(trimmed)}`);
try {
const res = await axios.get(BASE$1, {
timeout: TIMEOUT_MS$1,
params: { key: trimmed },
validateStatus: () => true
});
if (res.status === 200) {
return { valid: true };
}
if (res.status === 400 || res.status === 401 || res.status === 403) {
return { valid: false, error: `unauthorized (status ${res.status})` };
}
return { valid: false, error: `unexpected status ${res.status}` };
} catch (err) {
const ax = err;
const msg = ax.code ?? ax.message ?? "network error";
logger.debug(`[auth/paste/gemini] validator threw: ${msg}`);
return { valid: false, error: msg };
}
};
const BASE = "https://api.x.ai/v1/models";
const TIMEOUT_MS = 5e3;
function describeKey(key) {
const trimmed = key.trim();
const tail = trimmed.length >= 4 ? trimmed.slice(-4) : trimmed;
return `<${trimmed.length} chars, \u2026${tail}>`;
}
const validateGrokKey = async (key) => {
const trimmed = key.trim();
if (!trimmed) {
return { valid: false, error: "empty key" };
}
logger.debug(`[auth/paste/grok] validating key ${describeKey(trimmed)}`);
try {
const res = await axios.get(BASE, {
timeout: TIMEOUT_MS,
headers: { Authorization: `Bearer ${trimmed}` },
validateStatus: () => true
});
if (res.status === 200) {
return { valid: true };
}
if (res.status === 401 || res.status === 403) {
return { valid: false, error: `unauthorized (status ${res.status})` };
}
return { valid: false, error: `unexpected status ${res.status}` };
} catch (err) {
const ax = err;
const msg = ax.code ?? ax.message ?? "network error";
logger.debug(`[auth/paste/grok] validator threw: ${msg}`);
return { valid: false, error: msg };
}
};
const validateConsortiumKey = async () => {
throw new Error("not-applicable: consortium auth does not use BYOK paste");
};
function atomicWriteSecret(path$1, contents) {
const parent = path.dirname(path$1);
fs.mkdirSync(parent, { recursive: true });
const tmp = `${path$1}.tmp`;
const fd = fs.openSync(tmp, "w", 384);
try {
fs.writeSync(fd, contents);
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
fs.chmodSync(tmp, 384);
fs.renameSync(tmp, path$1);
fs.chmodSync(path$1, 384);
}
const persistClaudeKey = async (key, options) => {
const path$1 = path.join(options.homeDir, ".claude", ".credentials.json");
const payload = JSON.stringify({
access_token: key,
source: "consortium-byok"
});
atomicWriteSecret(path$1, payload);
return { path: path$1 };
};
const persistCodexKey = async (key, options) => {
const env = options.env ?? process.env;
const codexHome = env.CODEX_HOME && env.CODEX_HOME.length > 0 ? env.CODEX_HOME : path.join(options.homeDir, ".codex");
const path$1 = path.join(codexHome, "auth.json");
const payload = JSON.stringify({ OPENAI_API_KEY: key });
atomicWriteSecret(path$1, payload);
return { path: path$1 };
};
const persistGeminiKey = async (key, options) => {
const path$1 = path.join(options.homeDir, ".config", "gemini", "credentials.json");
const payload = JSON.stringify({ api_key: key });
atomicWriteSecret(path$1, payload);
return { path: path$1 };
};
const persistGrokKey = async (key, options) => {
const path$1 = path.join(options.homeDir, ".config", "grok", "credentials.json");
const payload = JSON.stringify({ api_key: key });
atomicWriteSecret(path$1, payload);
return { path: path$1 };
};
const persistConsortiumKey = async () => {
throw new Error("not-applicable: consortium auth does not use BYOK paste");
};
const DEFAULT_VALIDATORS = {
claude: validateClaudeKey,
codex: validateCodexKey,
gemini: validateGeminiKey,
grok: validateGrokKey,
consortium: validateConsortiumKey
};
const DEFAULT_PERSISTERS = {
claude: persistClaudeKey,
codex: persistCodexKey,
gemini: persistGeminiKey,
grok: persistGrokKey,
consortium: persistConsortiumKey
};
const INSTRUCTIONS = {
claude: "Paste your Anthropic API key (starts with `sk-ant-`). Get one at https://console.anthropic.com/settings/keys.",
codex: "Paste your OpenAI API key (starts with `sk-`). Get one at https://platform.openai.com/api-keys.",
gemini: "Paste your Google Gemini API key. Get one at https://aistudio.google.com/app/apikey.",
grok: "Paste your xAI API key (starts with `xai-`). Get one at https://console.x.ai/.",
consortium: "Consortium uses a QR / email / browser flow \u2014 paste is not applicable for this agent."
};
class PasteFlow {
entries = /* @__PURE__ */ new Map();
validators;
persisters;
homeDir;
env;
nowFn;
invalidate;
constructor(options = {}) {
this.validators = { ...DEFAULT_VALIDATORS, ...options.validators };
this.persisters = { ...DEFAULT_PERSISTERS, ...options.persisters };
this.homeDir = options.homeDir ?? os.homedir();
this.env = options.env ?? process.env;
this.nowFn = options.now ?? Date.now;
this.invalidate = options.invalidateProbeCache ?? (() => {
});
}
start(agent) {
if (!this.persisters[agent]) {
throw new Error(
`auth-flow: ${agent} has no BYOK paste path \u2014 it reads its key from an environment variable or its own config, not from a Consortium-written file`
);
}
const handle = {
id: node_crypto.randomUUID(),
agent,
kind: "paste",
startedAt: this.nowFn()
};
const status = {
status: "awaiting-user",
handle,
payload: {
kind: "paste",
instructions: INSTRUCTIONS[agent] ?? "Paste your API key for this provider."
}
};
this.entries.set(handle.id, { handle, status, settled: false });
logger.debug(`[auth/paste] started flow agent=${agent} id=${handle.id}`);
return handle;
}
/**
* Look up the current status for a handle. Mirrors the
* DeviceCodeFlow contract so `auth-flow-poll` can fan out.
*/
poll(handleId) {
return this.entries.get(handleId)?.status;
}
async submit(handleId, key) {
const entry = this.entries.get(handleId);
if (!entry) {
throw new Error(`unknown paste flow handle: ${handleId}`);
}
if (entry.settled) {
return entry.status;
}
const { handle } = entry;
const agent = handle.agent;
const validator = this.validators[agent];
let result;
try {
result = await validator(key);
} catch (err) {
const msg = err.message ?? "validator threw";
const failed = {
status: "failed",
handle,
finishedAt: this.nowFn(),
error: msg
};
entry.status = failed;
entry.settled = true;
logger.debug(`[auth/paste] validator threw agent=${agent} id=${handleId}: ${msg}`);
return failed;
}
if (!result.valid) {
const failed = {
status: "failed",
handle,
finishedAt: this.nowFn(),
error: result.error ?? "invalid key"
};
entry.status = failed;
entry.settled = true;
logger.debug(
`[auth/paste] invalid key agent=${agent} id=${handleId} error=${result.error ?? "unknown"}`
);
return failed;
}
const persister = this.persisters[agent];
try {
const persisted = await persister(key, { homeDir: this.homeDir, env: this.env });
logger.debug(
`[auth/paste] persisted agent=${agent} id=${handleId} path=${persisted.path}`
);
} catch (err) {
const msg = err.message ?? "persist failed";
const failed = {
status: "failed",
handle,
finishedAt: this.nowFn(),
error: `persist: ${msg}`
};
entry.status = failed;
entry.settled = true;
return failed;
}
try {
this.invalidate(agent);
} catch (err) {
logger.debug(
`[auth/paste] invalidate hook threw (ignored): ${err.message}`
);
}
const succeeded = {
status: "succeeded",
handle,
finishedAt: this.nowFn(),
identity: result.identity
};
entry.status = succeeded;
entry.settled = true;
return succeeded;
}
async cancel(handleId) {
const entry = this.entries.get(handleId);
if (!entry) return;
if (entry.settled) return;
const cancelled = {
status: "cancelled",
handle: entry.handle,
finishedAt: this.nowFn()
};
entry.status = cancelled;
entry.settled = true;
logger.debug(`[auth/paste] cancelled id=${handleId}`);
}
}
const CLAUDE_LOOPBACK_PORT = 54545;
const CLAUDE_OAUTH = {
CLIENT_ID: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
AUTHORIZE_URL: "https://claude.ai/oauth/authorize",
TOKEN_URL: "https://console.anthropic.com/v1/oauth/token",
REDIRECT_URI: `http://localhost:${CLAUDE_LOOPBACK_PORT}/callback`,
SCOPE: "user:inference"
};
const CLAUDE_OOB_REDIRECT_URI = CLAUDE_OAUTH.REDIRECT_URI;
function base64url(buf) {
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function generateClaudePkce() {
const verifier = base64url(node_crypto.randomBytes(32));
const challenge = base64url(node_crypto.createHash("sha256").update(verifier).digest());
return { verifier, challenge };
}
function generateClaudeState() {
return base64url(node_crypto.randomBytes(32));
}
function buildClaudeAuthorizeUrl(opts) {
const params = new URLSearchParams({
client_id: CLAUDE_OAUTH.CLIENT_ID,
response_type: "code",
redirect_uri: opts.redirectUri ?? CLAUDE_OAUTH.REDIRECT_URI,
scope: CLAUDE_OAUTH.SCOPE,
code_challenge: opts.challenge,
code_challenge_method: "S256",
state: opts.state
});
if (opts.displayCode !== false) params.set("code", "true");
return `${CLAUDE_OAUTH.AUTHORIZE_URL}?${params}`;
}
async function exchangeClaudeCode(opts) {
const doFetch = opts.fetchImpl ?? globalThis.fetch;
const response = await doFetch(CLAUDE_OAUTH.TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code: opts.code,
redirect_uri: opts.redirectUri,
client_id: CLAUDE_OAUTH.CLIENT_ID,
code_verifier: opts.verifier,
state: opts.state
})
});
if (!response.ok) {
throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return {
raw: data,
token: data.access_token,
expires: Date.now() + data.expires_in * 1e3
};
}
function parsePastedClaudeCode(pasted) {
const trimmed = pasted.trim();
if (/^https?:\/\//i.test(trimmed)) {
try {
const url = new URL(trimmed);
const code2 = url.searchParams.get("code") ?? "";
const state2 = url.searchParams.get("state") ?? void 0;
if (code2) return { code: code2, state: state2 };
} catch {
}
}
const [code, state] = trimmed.split("#", 2);
return { code: code.trim(), state: state?.trim() || void 0 };
}
const SCOPE = CLAUDE_OAUTH.SCOPE;
const REDIRECT_URI = CLAUDE_OOB_REDIRECT_URI;
const EXPIRES_IN_MS = 10 * 60 * 1e3;
const TERMINAL_RETENTION_MS$1 = 5 * 60 * 1e3;
class ClaudePasteCodeFlow {
records = /* @__PURE__ */ new Map();
homeDir;
fetchImpl;
now;
constructor(opts = {}) {
this.homeDir = opts.homeDir ?? os.homedir();
this.fetchImpl = opts.fetch ?? globalThis.fetch;
this.now = opts.now ?? (() => Date.now());
}
start() {
const { verifier, challenge } = generateClaudePkce();
const state = node_crypto.randomUUID();
const id = node_crypto.randomUUID();
const startedAt = this.now();
const expiresAtMs = startedAt + EXPIRES_IN_MS;
const authorizationUrl = buildClaudeAuthorizeUrl({
challenge,
state,
redirectUri: REDIRECT_URI,
displayCode: true
});
const handle = { id, agent: "claude", kind: "paste-code", startedAt };
const status = {
status: "awaiting-user",
handle,
payload: {
kind: "paste-code",
authorizationUrl,
instructions: "Open the link, sign in to your Claude account, then copy the code shown in the browser and paste it here.",
expiresAt: expiresAtMs
}
};
this.records.set(id, { handle, status, verifier, state, expiresAtMs, settled: false, gcTimer: null });
logger.debug(`[auth-flow claude] paste-code started id=${id}`);
return status;
}
poll(id) {
const record = this.records.get(id);
if (!record) return void 0;
if (!record.settled && this.now() >= record.expiresAtMs) {
record.status = { status: "expired", handle: record.handle, finishedAt: this.now() };
record.settled = true;
this.scheduleGc(record);
}
return record.status;
}
async submit(id, codeAndState) {
const record = this.records.get(id);
if (!record) throw new Error(`claude paste-code flow not found: ${id}`);
if (record.settled) return record.status;
const { code, state: returnedState } = parsePastedClaudeCode(codeAndState);
if (!code) return this.fail(record, "no authorization code provided");
if (returnedState && returnedState !== record.state) {
return this.fail(record, "state mismatch \u2014 restart the login");
}
let json;
try {
const res = await this.fetchImpl(CLAUDE_OAUTH.TOKEN_URL, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code,
state: record.state,
client_id: CLAUDE_OAUTH.CLIENT_ID,
redirect_uri: REDIRECT_URI,
code_verifier: record.verifier
})
});
json = await res.json().catch(() => ({}));
if (!res.ok || !json.access_token) {
return this.fail(record, json.error_description ?? json.error ?? `token exchange failed (HTTP ${res.status})`);
}
} catch (err) {
return this.fail(record, err instanceof Error ? err.message : "token exchange failed");
}
const expiresAt = json.expires_in ? this.now() + json.expires_in * 1e3 : void 0;
try {
await this.persist(json, expiresAt);
} catch (err) {
return this.fail(record, `persist failed: ${err instanceof Error ? err.message : String(err)}`);
}
const identity = {
email: json.account?.email_address,
account: json.organization?.name,
expiresAt
};
record.status = { status: "succeeded", handle: record.handle, finishedAt: this.now(), identity };
record.settled = true;
invalidateProbeCache();
this.scheduleGc(record);
logger.debug(`[auth-flow claude] paste-code succeeded id=${id}`);
return record.status;
}
async cancel(id) {
const record = this.records.get(id);
if (!record || record.settled) return;
record.status = { status: "cancelled", handle: record.handle, finishedAt: this.now() };
record.settled = true;
this.scheduleGc(record);
}
dispose() {
for (const r of this.records.values()) if (r.gcTimer) clearTimeout(r.gcTimer);
this.records.clear();
}
fail(record, error) {
record.status = { status: "failed", handle: record.handle, finishedAt: this.now(), error };
record.settled = true;
this.scheduleGc(record);
logger.debug(`[auth-flow claude] paste-code failed id=${record.handle.id}: ${error}`);
return record.status;
}
scheduleGc(record) {
if (record.gcTimer) clearTimeout(record.gcTimer);
record.gcTimer = setTimeout(() => this.records.delete(record.handle.id), TERMINAL_RETENTION_MS$1);
}
/** Write ~/.claude/.credentials.json in the nested shape the CLI reads. */
async persist(token, expiresAt) {
const creds = {
claudeAiOauth: {
accessToken: token.access_token,
refreshToken: token.refresh_token,
expiresAt,
scopes: token.scope ? token.scope.split(" ") : SCOPE.split(" ")
}
};
const dir = path.join(this.homeDir, ".claude");
const file = path.join(dir, ".credentials.json");
await fs$3.mkdir(dir, { recursive: true, mode: 448 });
const tmp = `${file}.tmp-${process.pid}-${this.now()}`;
await fs$3.writeFile(tmp, JSON.stringify(creds, null, 2), { mode: 384 });
try {
await fs$3.chmod(tmp, 384);
await fs$3.rename(tmp, file);
await fs$3.chmod(file, 384);
} catch (err) {
await fs$3.rm(tmp, { force: true }).catch(() => {
});
throw err;
}
await fs$3.chmod(dir, 448).catch(() => {
});
}
}
const INSTALL_AGENT_KINDS = [
"claude",
"codex",
"gemini",
"grok",
"pi"
];
const INSTALL_RECIPES = {
claude: {
agent: "claude",
binary: "claude",
install: { method: "npm", npmPackage: "@anthropic-ai/claude-code" }
},
codex: {
agent: "codex",
binary: "codex",
install: { method: "npm", npmPackage: "@openai/codex" }
},
gemini: {
agent: "gemini",
binary: "gemini",
install: { method: "npm", npmPackage: "@google/gemini-cli" }
},
grok: {
agent: "grok",
binary: "grok",
// Official xAI installer. Note: the npm name `grok-build-cli` seen in
// older app code is third-party — do NOT use it. The curl installer
// is the vetted source for the `grok` binary (`grok agent stdio`,
// `~/.grok/auth.json`).
install: { method: "curl", scriptUrl: "https://x.ai/cli/install.sh" }
},
pi: {
agent: "pi",
binary: "pi",
// `--ignore-scripts`: pi's postinstall is not needed for the binary
// and avoids running arbitrary lifecycle scripts during a remote
// install.
install: {
method: "npm",
npmPackage: "@earendil-works/pi-coding-agent",
npmExtraArgs: ["--ignore-scripts"]
}
}
};
function isInstallAgentKind(value) {
return typeof value === "string" && INSTALL_AGENT_KINDS.includes(value);
}
const TERMINAL_RETENTION_MS = 5 * 60 * 1e3;
const MAX_LOG_LINES = 800;
const MAX_LINE_LEN = 2e3;
const VERSION_PROBE_TIMEOUT_MS = 1e4;
const TERMINAL_PHASES = /* @__PURE__ */ new Set([
"succeeded",
"failed",
"unsupported",
"cancelled"
]);
function extendedPathEnv() {
const extra = [
"/usr/local/bin",
"/opt/homebrew/bin",
path.join(os.homedir(), ".local/bin"),
path.join(os.homedir(), ".npm-global/bin")
];
const PATH = [process.env.PATH, ...extra].filter(Boolean).join(":");
return { ...process.env, PATH };
}
class InstallFlow {
jobs = /* @__PURE__ */ new Map();
now;
constructor(opts = {}) {
this.now = opts.now ?? (() => Date.now());
}
/**
* Begin (or rejoin) an install for `agent`. Returns the initial status
* immediately; the child runs in the background. A second call while a
* job is in flight returns that job's handle (de-dupe / rejoin).
*/
start(agent, _action = "install") {
if (!isInstallAgentKind(agent)) {
throw new Error(`agent-install-start: unsupported agent '${agent}'`);
}
const existing = this.findActiveByAgent(agent);
if (existing) {
return this.snapshot(existing, 0);
}
const recipe = INSTALL_RECIPES[agent];
const job = {
handleId: node_crypto.randomUUID(),
agent,
recipe,
phase: "preflight",
startedAt: this.now(),
logs: [],
nextSeq: 0,
child: null,
cancelled: false,
gcTimer: null
};
this.jobs.set(job.handleId, job);
this.appendLog(job, "info", `Installing ${agent}\u2026`);
void this.run(job);
return this.snapshot(job, 0);
}
poll(handleId, sinceCursor = 0) {
const job = this.jobs.get(handleId);
if (!job) throw new Error(`agent-install-poll: unknown handleId ${handleId}`);
return this.snapshot(job, sinceCursor);
}
cancel(handleId) {
const job = this.jobs.get(handleId);
if (!job) return { ok: false };
if (TERMINAL_PHASES.has(job.phase)) return { ok: true };
job.cancelled = true;
if (job.child && !job.child.killed) {
try {
job.child.kill("SIGTERM");
} catch {
}
}
this.toTerminal(job, "cancelled");
return { ok: true };
}
dispose() {
for (const job of this.jobs.values()) {
if (job.gcTimer) clearTimeout(job.gcTimer);
if (job.child && !job.child.killed) {
try {
job.child.kill("SIGKILL");
} catch {
}
}
}
this.jobs.clear();
}
// ── internals ────────────────────────────────────────────────────────
async run(job) {
try {
if (job.recipe.install.method === "npm") {
await this.runNpm(job);
} else {
await this.runCurl(job);
}
} catch (err) {
if (job.cancelled) return;
this.fail(job, "install-failed", err instanceof Error ? err.message : String(err));
}
}
async runNpm(job) {
const recipe = job.recipe;
if (recipe.install.method !== "npm") return;
const npmPath = await whichBinary("npm");
if (job.cancelled) return;
if (!npmPath) {
this.toUnsupported(
job,
"no-npm",
"npm was not found on this machine. Install Node.js (which bundles npm) and try again."
);
return;
}
const versioned = recipe.pinVersion ? `${recipe.install.npmPackage}@${recipe.pinVersion}` : recipe.install.npmPackage;
job.packageSpec = versioned;
const args = ["install", "-g", versioned, ...recipe.install.npmExtraArgs ?? []];
this.appendLog(job, "info", `npm ${args.join(" ")}`);
await this.spawnAndStream(job, npmPath, args);
}
async runCurl(job) {
const recipe = job.recipe;
if (recipe.install.method !== "curl") return;
const curlPath = await whichBinary("curl");
if (job.cancelled) return;
if (!curlPath) {
this.toUnsupported(
job,
"no-curl",
"curl was not found on this machine. Install curl and try again."
);
return;
}
const url = recipe.install.scriptUrl;
job.packageSpec = url;
this.appendLog(job, "info", `curl -fsSL ${url} | sh`);
await this.spawnAndStream(job, "/bin/sh", ["-c", 'curl -fsSL "$0" | sh', url]);
}
/** Spawn, stream stdout/stderr line-by-line, then verify on exit. */
spawnAndStream(job, command, args) {
return new Promise((resolve) => {
job.phase = "installing";
const child = node_child_process.spawn(command, args, { env: extendedPathEnv() });
job.child = child;
let outBuf = "";
let errBuf = "";
const pump = (chunk, stream) => {
const acc = (stream === "stdout" ? outBuf : errBuf) + chunk.toString("utf8");
const lines = acc.split("\n");
const tail = lines.pop() ?? "";
if (stream === "stdout") outBuf = tail;
else errBuf = tail;
for (const line of lines) {
if (line.length > 0) this.appendLog(job, stream, line);
}
};
child.stdout?.on("data", (c) => pump(c, "stdout"));
child.stderr?.on("data", (c) => pump(c, "stderr"));
child.on("error", (err) => {
if (job.cancelled) {
resolve();
return;
}
this.fail(job, "spawn-failed", err.message);
resolve();
});
child.on("close", (code) => {
if (outBuf.length > 0) this.appendLog(job, "stdout", outBuf);
if (errBuf.length > 0) this.appendLog(job, "stderr", errBuf);
job.child = null;
if (job.cancelled) {
resolve();
return;
}
if (code !== 0) {
this.classifyAndFail(job, errBuf, code);
resolve();
return;
}
void this.verify(job).then(resolve);
});
});
}
async verify(job) {
job.phase = "verifying";
this.appendLog(job, "info", `Verifying ${job.recipe.binary} on PATH\u2026`);
const binPath = await whichBinary(job.recipe.binary);
if (job.cancelled) return;
if (!binPath) {
this.fail(
job,
"not-on-path",
`Installed, but '${job.recipe.binary}' is not on the daemon's PATH. Check the npm global bin location.`
);
return;
}
const version = await this.probeVersion(binPath);
if (job.cancelled) return;
job.version = version ?? void 0;
this.appendLog(job, "info", version ? `Installed ${job.recipe.binary} ${version}` : `Installed ${job.recipe.binary}`);
this.toTerminal(job, "succeeded");
}
probeVersion(binPath) {
return new Promise((resolve) => {
node_child_process.execFile(binPath, ["--version"], { timeout: VERSION_PROBE_TIMEOUT_MS }, (error, stdout) => {
if (error) {
resolve(null);
return;
}
resolve(stdout.split("\n")[0]?.trim() || null);
});
});
}
classifyAndFail(job, stderrTail, code) {
const hay = (stderrTail + job.logs.slice(-20).map((l) => l.text).join("\n")).toLowerCase();
if (hay.includes("eacces") || hay.includes("permission denied")) {
this.fail(
job,
"eacces",
"Permission denied writing the npm global directory. Use a Node version manager or fix npm global permissions, then retry."
);
return;
}
if (hay.includes("eai_again") || hay.includes("network") || hay.includes("etimedout") || hay.includes("enotfound")) {
this.fail(job, "network", "Network error reaching the package registry. Check connectivity and retry.");
return;
}
this.fail(job, "install-failed", `Install exited with code ${code ?? "null"}.`);
}
fail(job, code, message) {
job.error = { code, message };
this.appendLog(job, "info", `Failed: ${message}`);
this.toTerminal(job, "failed");
}
toUnsupported(job, code, message) {
job.error = { code, message };
this.appendLog(job, "info", message);
this.toTerminal(job, "unsupported");
}
toTerminal(job, phase) {
if (TERMINAL_PHASES.has(job.phase)) return;
job.phase = phase;
job.finishedAt = this.now();
logger.debug(`[cli-install] ${job.agent} ${phase}${job.error ? ` code=${job.error.code}` : ""}`);
if (job.gcTimer) clearTimeout(job.gcTimer);
job.gcTimer = setTimeout(() => this.jobs.delete(job.handleId), TERMINAL_RETENTION_MS);
}
appendLog(job, stream, text) {
const clipped = text.length > MAX_LINE_LEN ? `${text.slice(0, MAX_LINE_LEN)}\u2026` : text;
job.logs.push({ seq: job.nextSeq++, stream, text: clipped });
if (job.logs.length > MAX_LOG_LINES) job.logs.splice(0, job.logs.length - MAX_LOG_LINES);
}
findActiveByAgent(agent) {
for (const job of this.jobs.values()) {
if (job.agent === agent && !TERMINAL_PHASES.has(job.phase)) return job;
}
return void 0;
}
snapshot(job, sinceCursor) {
const logs = sinceCursor > 0 ? job.logs.filter((l) => l.seq >= sinceCursor) : job.logs.slice();
return {
handleId: job.handleId,
agent: job.agent,
phase: job.phase,
package: job.packageSpec,
version: job.version,
error: job.error,
startedAt: job.startedAt,
finishedAt: job.finishedAt,
logCursor: job.nextSeq,
logs
};
}
}
function installsDisabled() {
return process.env.CONSORTIUM_AGENT_INSTALL === "0";
}
function registerCliInstallHandlers(rpcHandlerManager, installFlow) {
rpcHandlerManager.registerHandler("agent-install-start", async (params) => {
if (installsDisabled()) {
throw new Error("Remote installs are disabled on this machine (CONSORTIUM_AGENT_INSTALL=0).");
}
const agent = String(params?.agent ?? "");
return installFlow.start(agent, "install");
});
rpcHandlerManager.registerHandler("agent-install-poll", async (params) => {
const handleId = String(params?.handleId ?? "");
if (!handleId) throw new Error("agent-install-poll: handleId required");
const sinceCursor = Number(params?.sinceCursor ?? 0);
return installFlow.poll(handleId, Number.isFinite(sinceCursor) ? sinceCursor : 0);
});
rpcHandlerManager.registerHandler("agent-install-cancel", async (params) => {
const handleId = String(params?.handleId ?? "");
if (!handleId) throw new Error("agent-install-cancel: handleId required");
return installFlow.cancel(handleId);
});
}
const AgentKindSchema = z.z.enum(["claude", "codex", "gemini", "grok", "pi", "opencode", "consortium"]);
z.z.enum(["machine", "managed", "byok", "auto"]);
const AuthCredentialOriginSchema = z.z.enum(["env", "keychain", "file", "cli"]);
const AuthIdentitySchema = z.z.object({
email: z.z.string().optional(),
account: z.z.string().optional(),
expiresAt: z.z.number().optional()
});
const AuthWarningSchema = z.z.enum(["binary-missing", "keychain-locked"]);
const AuthStateSchema = z.z.object({
agent: AgentKindSchema,
status: z.z.enum(["present", "missing", "invalid", "unknown"]),
source: AuthCredentialOriginSchema.optional(),
identity: AuthIdentitySchema.optional(),
scopes: z.z.array(z.z.string()).optional(),
lastCheckedAt: z.z.number(),
error: z.z.string().optional(),
/**
* Soft warning, never set on existing returns from older probes.
* Backward compatible: optional, ignored by old clients.
*/
warning: AuthWarningSchema.optional(),
/**
* Whether the agent's CLI binary is installed and runnable on this
* machine (`<binary> --version` exits 0). Orthogonal to `status`,
* which describes credentials only — together they give the four
* distinguishable states of defect A-9:
*
* status=present + binaryPresent=true → ready
* status=present + binaryPresent=false → logged in, CLI missing
* (also sets warning
* `binary-missing`)
* status=missing + binaryPresent=true → installed but logged out
* status=missing + binaryPresent=false → not installed
*
* `undefined` means "not applicable / not checked" — e.g. the
* `consortium` probe (its binary is this very process) or an older
* daemon that predates this field. Consumers MUST treat `undefined`
* as unknown, never as `false`.
*/
binaryPresent: z.z.boolean().optional()
});
z.z.object({
states: z.z.array(AuthStateSchema),
probedAt: z.z.number()
});
const AuthFlowKindSchema = z.z.enum(["device-code", "browser", "paste", "paste-code", "cli-spawn"]);
const AuthFlowHandleSchema = z.z.object({
id: z.z.string(),
agent: AgentKindSchema,
kind: AuthFlowKindSchema,
startedAt: z.z.number()
});
const AuthFlowDeviceCodePayloadSchema = z.z.object({
kind: z.z.literal("device-code"),
verificationUrl: z.z.string(),
userCode: z.z.string(),
expiresAt: z.z.number()
});
const AuthFlowBrowserPayloadSchema = z.z.object({
kind: z.z.literal("browser"),
authorizationUrl: z.z.string(),
expiresAt: z.z.number()
});
const AuthFlowPastePayloadSchema = z.z.object({
kind: z.z.literal("paste"),
instructions: z.z.string()
});
const AuthFlowPasteCodePayloadSchema = z.z.object({
kind: z.z.literal("paste-code"),
authorizationUrl: z.z.string(),
instructions: z.z.string(),
expiresAt: z.z.number()
});
const AuthFlowCliSpawnPayloadSchema = z.z.object({
kind: z.z.literal("cli-spawn"),
command: z.z.string(),
args: z.z.array(z.z.string())
});
const AuthFlowAwaitingUserPayloadSchema = z.z.discriminatedUnion("kind", [
AuthFlowDeviceCodePayloadSchema,
AuthFlowBrowserPayloadSchema,
AuthFlowPastePayloadSchema,
AuthFlowPasteCodePayloadSchema,
AuthFlowCliSpawnPayloadSchema
]);
z.z.discriminatedUnion("status", [
z.z.object({
status: z.z.literal("pending"),
handle: AuthFlowHandleSchema
}),
z.z.object({
status: z.z.literal("awaiting-user"),
handle: AuthFlowHandleSchema,
payload: AuthFlowAwaitingUserPayloadSchema
}),
z.z.object({
status: z.z.literal("succeeded"),
handle: AuthFlowHandleSchema,
finishedAt: z.z.number(),
identity: AuthIdentitySchema.optional()
}),
z.z.object({
status: z.z.literal("failed"),
handle: AuthFlowHandleSchema,
finishedAt: z.z.number(),
error: z.z.string()
}),
z.z.object({
status: z.z.literal("cancelled"),
handle: AuthFlowHandleSchema,
finishedAt: z.z.number()
}),
z.z.object({
status: z.z.literal("expired"),
handle: AuthFlowHandleSchema,
finishedAt: z.z.number()
})
]);
class ProvisioningEventEmitter {
inner = new node_events.EventEmitter();
on(event, listener) {
this.inner.on(event, listener);
return this;
}
once(event, listener) {
this.inner.once(event, listener);
return this;
}
off(event, listener) {
this.inner.off(event, listener);
return this;
}
emit(event, payload) {
return this.inner.emit(event, payload);
}
/**
* Remove all listeners for a given event, or all listeners across
* all events if no event is specified. Used by tests.
*/
removeAllListeners(event) {
if (event !== void 0) {
this.inner.removeAllListeners(event);
} else {
this.inner.removeAllListeners();
}
return this;
}
}
const provisioningEvents = new ProvisioningEventEmitter();
let sentry = null;
let initialized = false;
function loadSentry() {
try {
const mod = require("@sentry/node");
return mod;
} catch (err) {
logger.debug("[observability] @sentry/node not installed; observability disabled", err);
return null;
}
}
const PII_KEY_PATTERN = /^(authorization|cookie|token|secret|access[_-]?key|refresh[_-]?token|password|email|host|hostname|machineid)$/i;
function scrub(data) {
if (!data) return void 0;
const out = {};
for (const [k, v] of Object.entries(data)) {
if (PII_KEY_PATTERN.test(k)) {
out[k] = "<redacted>";
continue;
}
if (typeof v === "string" && (v.includes("Bearer ") || /eyJ[A-Za-z0-9_-]{10,}/.test(v))) {
out[k] = "<redacted>";
continue;
}
out[k] = v;
}
return out;
}
function initObservability() {
if (initialized) return;
initialized = true;
const dsn = process.env.CONSORTIUM_SENTRY_DSN;
if (!dsn) {
logger.debug("[observability] CONSORTIUM_SENTRY_DSN not set; observability disabled");
return;
}
const candidate = loadSentry();
if (!candidate) return;
sentry = candidate;
try {
sentry.init({
dsn,
release: `consortium-cli@${configuration.currentCliVersion}`,
environment: process.env.CONSORTIUM_ENV ?? "production",
// We want to capture unhandled errors but NOT auto-attach
// request bodies (which may contain bearer tokens).
sendDefaultPii: false,
// Use a low traces sample rate; this is for errors, not perf.
tracesSampleRate: 0,
beforeSend(event) {
try {
const req = event.request;
if (req?.headers) {
for (const k of Object.keys(req.headers)) {
if (PII_KEY_PATTERN.test(k)) req.headers[k] = "<redacted>";
}
}
if (req && "cookies" in req) req.cookies = void 0;
} catch {
}
return event;
}
});
sentry.setTag("component", "daemon");
sentry.setTag("cli_version", configuration.currentCliVersion);
} catch (err) {
logger.debug("[observability] Sentry.init threw; disabling", err);
sentry = null;
return;
}
installGlobalHandlers();
installProvisioningBreadcrumbs();
}
function captureError(err, context) {
if (!sentry) return;
try {
sentry.captureException(err, { extra: scrub(context) });
} catch (innerErr) {
logger.debug("[observability] captureError failed", innerErr);
}
}
function captureBreadcrumb(message, data) {
if (!sentry) return;
try {
sentry.addBreadcrumb({
category: "daemon",
message,
data: scrub(data),
level: "info"
});
} catch {
}
}
function incrementCounter(name, tags) {
if (!sentry) return;
try {
sentry.captureMessage(`counter.${name}`, {
level: "info",
tags: { counter: name, ...tags ?? {} }
});
} catch {
}
}
function installGlobalHandlers() {
process.on("unhandledRejection", (reason) => {
captureError(reason, { source: "unhandledRejection" });
});
process.on("uncaughtException", (err) => {
captureError(err, { source: "uncaughtException" });
});
}
function installProvisioningBreadcrumbs() {
const safeOn = (name, fn) => {
try {
provisioningEvents.on(name, (e) => {
try {
fn(e);
} catch {
}
});
} catch {
}
};
safeOn("account-switching", (e) => {
captureBreadcrumb("provisioning.account-switching", {
sessionCount: Array.isArray(e?.sessions) ? e.sessions.length : 0
});
});
safeOn("account-switched", () => {
captureBreadcrumb("provisioning.account-switched");
});
safeOn("restart-requested", (e) => {
captureBreadcrumb("provisioning.restart-requested", { reason: e?.reason });
incrementCounter("daemon-restart-reason", { reason: String(e?.reason ?? "unknown") });
});
safeOn("deprovisioned", (e) => {
captureBreadcrumb("provisioning.deprovisioned", {
terminatedSessions: Array.isArray(e?.terminatedSessions) ? e.terminatedSessions.length : 0
});
});
safeOn("provisioned", (e) => {
captureBreadcrumb("provisioning.provisioned", { nextState: e?.nextState });
incrementCounter("provision-success", { nextState: String(e?.nextState ?? "unknown") });
});
}
const ACCOUNT_USAGE_MAX_AGE_MS = 30 * 60 * 1e3;
const MAX_ENTRIES = 512;
const registry = /* @__PURE__ */ new Map();
function keyOf(snapshot) {
return `${snapshot.accountKeyId ?? ""}|${snapshot.providerId}|${snapshot.limitKind}`;
}
function isExpired(snapshot, now) {
if (snapshot.resetsAt !== void 0 && snapshot.resetsAt <= now) return true;
return now - snapshot.fetchedAt > ACCOUNT_USAGE_MAX_AGE_MS;
}
function recordAccountUsage(snapshot) {
if (!snapshot || !snapshot.accountKeyId || !snapshot.providerId) return;
if (!Number.isFinite(snapshot.fetchedAt)) return;
const normalized = { ...snapshot, source: "daemon-headers" };
const key = keyOf(normalized);
const existing = registry.get(key);
if (existing && existing.fetchedAt > normalized.fetchedAt) return;
if (!existing && registry.size >= MAX_ENTRIES) {
pruneExpired(Date.now());
if (registry.size >= MAX_ENTRIES) return;
}
registry.set(key, normalized);
}
function recordAccountUsageBatch(snapshots) {
for (const s of snapshots) recordAccountUsage(s);
}
function pruneExpired(now) {
for (const [key, snapshot] of registry) {
if (isExpired(snapshot, now)) registry.delete(key);
}
}
function listAccountUsage(now = Date.now()) {
pruneExpired(now);
return [...registry.values()].sort((a, b) => b.fetchedAt - a.fetchedAt);
}
function snapshotsFromCodexRateLimits(raw, ctx) {
if (!raw || typeof raw !== "object") return [];
const now = ctx.now ?? Date.now();
const providerId = ctx.providerId ?? "openai";
const obj = raw;
const windows = [];
for (const label of ["primary", "secondary"]) {
const w = obj[label];
if (w && typeof w === "object") windows.push({ window: w, label });
}
if (windows.length === 0 && typeof obj.used_percent === "number") {
windows.push({ window: obj, label: "primary" });
}
const out = [];
for (const { window, label } of windows) {
const pct = typeof window.used_percent === "number" && Number.isFinite(window.used_percent) ? window.used_percent : void 0;
if (pct === void 0) continue;
let resetsAt;
if (typeof window.resets_in_seconds === "number" && Number.isFinite(window.resets_in_seconds)) {
resetsAt = now + window.resets_in_seconds * 1e3;
} else if (typeof window.resets_at === "number" && Number.isFinite(window.resets_at)) {
resetsAt = window.resets_at < 1e12 ? window.resets_at * 1e3 : window.resets_at;
} else if (typeof window.resets_at === "string") {
const parsed = Date.parse(window.resets_at);
if (Number.isFinite(parsed)) resetsAt = parsed;
}
const minutes = typeof window.window_minutes === "number" ? window.window_minutes : void 0;
out.push({
accountKeyId: ctx.accountKeyId,
providerId,
source: "daemon-headers",
limitKind: "plan-window",
status: pct >= 100 ? "limited" : pct >= 80 ? "warning" : "ok",
pct,
unit: "percent",
resetsAt,
note: minutes !== void 0 ? `${label} window: ${minutes}m` : label,
fetchedAt: now
});
}
return out;
}
function buildMachineIdempotencyKey(machineId, callId, ordinal) {
const base = `ue1:mch:${machineId}:${callId}`;
const n = ordinal ?? 0;
return n === 0 ? base : `${base}:${n}`;
}
const DEFAULTS = {
maxBytes: 50 * 1024 * 1024,
segmentMaxBytes: 4 * 1024 * 1024,
flushIntervalMs: 250,
flushMaxEntries: 64,
maxEntryBytes: 32 * 1024
};
const SEGMENT_PREFIX = "seg-";
const SEGMENT_SUFFIX = ".jsonl";
const STATS_FILE = "stats.json";
function emptyStats() {
return {
droppedEntries: 0,
rejectedEntries: 0,
corruptLines: 0,
uploadedEntries: 0,
discardedEntries: 0,
lastDropAt: null
};
}
function defaultUsageQueueDir() {
return path.join(configuration.consortiumHomeDir, "usage-queue");
}
class UsageQueue {
dir;
maxBytes;
segmentMaxBytes;
flushIntervalMs;
flushMaxEntries;
maxEntryBytes;
activeName = null;
activeFd = null;
activeBytes = 0;
pending = [];
pendingBytes = 0;
flushTimer = null;
seq = 0;
closed = false;
counters;
constructor(options = {}) {
this.dir = options.dir ?? defaultUsageQueueDir();
this.maxBytes = options.maxBytes ?? DEFAULTS.maxBytes;
this.segmentMaxBytes = options.segmentMaxBytes ?? DEFAULTS.segmentMaxBytes;
this.flushIntervalMs = options.flushIntervalMs ?? DEFAULTS.flushIntervalMs;
this.flushMaxEntries = options.flushMaxEntries ?? DEFAULTS.flushMaxEntries;
this.maxEntryBytes = options.maxEntryBytes ?? DEFAULTS.maxEntryBytes;
fs.mkdirSync(this.dir, { recursive: true, mode: 448 });
this.counters = this.loadStats();
for (const name of this.allSegments()) {
const n = Number(name.slice(SEGMENT_PREFIX.length).split("-")[1]);
if (Number.isFinite(n) && n >= this.seq) this.seq = n + 1;
}
}
// ── Append (hot path) ────────────────────────────────────────────────────
/**
* Buffer one entry for durable append. Synchronous, allocation-light,
* never throws. Returns `'rejected'` only for an entry that can never be
* written (unserialisable or absurdly large) — the caller should not retry.
*/
append(entry) {
if (this.closed) return "rejected";
let line;
try {
line = JSON.stringify(entry);
} catch {
this.bumpStat("rejectedEntries");
return "rejected";
}
if (line.includes("\n") || Buffer.byteLength(line) > this.maxEntryBytes) {
this.bumpStat("rejectedEntries");
return "rejected";
}
this.pending.push(line);
this.pendingBytes += Buffer.byteLength(line) + 1;
if (this.pending.length >= this.flushMaxEntries) {
this.flushSync();
} else if (!this.flushTimer) {
this.flushTimer = setTimeout(() => {
this.flushSync();
}, this.flushIntervalMs);
this.flushTimer.unref?.();
}
return "queued";
}
/** Buffered-but-not-yet-fsync'd entry count. Test/observability seam. */
get pendingCount() {
return this.pending.length;
}
// ── Durability ───────────────────────────────────────────────────────────
/**
* Write every buffered line to the active segment and fsync it. Safe to
* call at any time (shutdown hook, before a drain, from the timer). Never
* throws: a full or read-only disk must not take the daemon down.
*/
flushSync() {
if (this.flushTimer) {
clearTimeout(this.flushTimer);
this.flushTimer = null;
}
if (this.pending.length === 0) return;
const payload = this.pending.join("\n") + "\n";
const bytes = Buffer.byteLength(payload);
try {
const fd = this.openActive();
fs.writeSync(fd, payload);
fs.fsyncSync(fd);
this.activeBytes += bytes;
this.pending = [];
this.pendingBytes = 0;
} catch (err) {
const lost = this.pending.length;
this.pending = [];
this.pendingBytes = 0;
this.bumpStat("rejectedEntries", lost);
logger.debug(`[USAGE QUEUE] flush failed, dropped ${lost} entries: ${err}`);
return;
}
if (this.activeBytes >= this.segmentMaxBytes) this.sealActive();
this.enforceCap();
}
/**
* Close the active segment so a drain may read it. The next append starts
* a fresh segment. Idempotent.
*/
sealActive() {
if (this.activeFd !== null) {
try {
fs.closeSync(this.activeFd);
} catch {
}
}
this.activeFd = null;
this.activeName = null;
this.activeBytes = 0;
}
/** flushSync + sealActive + close. Call from the daemon shutdown path. */
close() {
this.flushSync();
this.sealActive();
this.closed = true;
}
openActive() {
if (this.activeFd !== null) return this.activeFd;
const name = `${SEGMENT_PREFIX}${String(Date.now()).padStart(15, "0")}-${String(this.seq++).padStart(6, "0")}${SEGMENT_SUFFIX}`;
const full = path.join(this.dir, name);
const fd = fs.openSync(full, "a", 384);
this.activeFd = fd;
this.activeName = name;
this.activeBytes = (() => {
try {
return fs.statSync(full).size;
} catch {
return 0;
}
})();
return fd;
}
// ── Segment inventory ────────────────────────────────────────────────────
allSegments() {
let names;
try {
names = fs.readdirSync(this.dir);
} catch {
return [];
}
return names.filter((n) => n.startsWith(SEGMENT_PREFIX) && n.endsWith(SEGMENT_SUFFIX)).sort();
}
/** Sealed (drainable) segments, oldest first. Excludes the open segment. */
sealedSegments() {
return this.allSegments().filter((n) => n !== this.activeName);
}
/** Total on-disk bytes across all segments. */
totalBytes() {
let total = 0;
for (const name of this.allSegments()) {
try {
total += fs.statSync(path.join(this.dir, name)).size;
} catch {
}
}
return total;
}
/**
* Parse one sealed segment. Tolerant by construction: a truncated trailing
* line (the crash signature) or any unparsable line is skipped and counted
* as `corruptLines` rather than poisoning the whole segment.
*/
readSegment(name) {
let raw;
try {
raw = fs.readFileSync(path.join(this.dir, name), "utf8");
} catch {
return [];
}
const out = [];
let corrupt = 0;
for (const line of raw.split("\n")) {
if (line.length === 0) continue;
try {
const parsed = JSON.parse(line);
if (parsed && parsed.v === 1 && typeof parsed.idempotencyKey === "string" && parsed.event) {
out.push(parsed);
} else {
corrupt++;
}
} catch {
corrupt++;
}
}
if (corrupt > 0) this.bumpStat("corruptLines", corrupt);
return out;
}
/** Every queued entry across every SEALED segment, oldest first. */
readAll() {
const out = [];
for (const name of this.sealedSegments()) out.push(...this.readSegment(name));
return out;
}
/**
* Replace a segment with the subset that has NOT settled yet, atomically
* (write tmp → fsync → rename). A crash mid-rewrite therefore either keeps
* the old segment (re-upload, deduped server-side) or lands the new one —
* never a half file.
*/
rewriteSegment(name, entries) {
const full = path.join(this.dir, name);
if (entries.length === 0) {
this.removeSegment(name);
return;
}
const tmp = `${full}.tmp`;
try {
const payload = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
const fd = fs.openSync(tmp, "w", 384);
try {
fs.writeSync(fd, payload);
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
fs.renameSync(tmp, full);
} catch (err) {
try {
fs.unlinkSync(tmp);
} catch {
}
logger.debug(`[USAGE QUEUE] segment rewrite failed for ${name}: ${err}`);
}
}
removeSegment(name) {
try {
fs.unlinkSync(path.join(this.dir, name));
} catch {
}
}
// ── Backpressure ─────────────────────────────────────────────────────────
/**
* Enforce the byte ceiling by deleting the OLDEST sealed segments first.
* Oldest-dropped (not newest) because a stale month-old event is worth
* strictly less than the call that just happened, and because dropping the
* head keeps the queue draining in order.
*/
enforceCap() {
let total = this.totalBytes();
if (total <= this.maxBytes) return;
for (const name of this.sealedSegments()) {
if (total <= this.maxBytes) break;
const full = path.join(this.dir, name);
let size = 0;
let lines = 0;
try {
size = fs.statSync(full).size;
lines = this.readSegment(name).length;
} catch {
}
this.removeSegment(name);
total -= size;
if (lines > 0) {
this.counters.droppedEntries += lines;
this.counters.lastDropAt = Date.now();
}
}
if (total > this.maxBytes && this.activeName) {
this.sealActive();
}
this.persistStats();
logger.debug(`[USAGE QUEUE] backpressure: dropped to ${total} bytes (total dropped entries ${this.counters.droppedEntries})`);
}
// ── Stats ────────────────────────────────────────────────────────────────
stats() {
return { ...this.counters };
}
/** Uploader bookkeeping: entries the server accepted / permanently refused. */
recordUploaded(count) {
this.bumpStat("uploadedEntries", count);
}
recordDiscarded(count) {
this.bumpStat("discardedEntries", count);
}
bumpStat(key, by = 1) {
if (key === "lastDropAt") return;
this.counters[key] += by;
if (key === "droppedEntries" || key === "discardedEntries") this.counters.lastDropAt = Date.now();
this.persistStats();
}
loadStats() {
try {
const raw = JSON.parse(fs.readFileSync(path.join(this.dir, STATS_FILE), "utf8"));
const base = emptyStats();
for (const k of Object.keys(base)) {
const v = raw[k];
if (typeof v === "number") base[k] = v;
}
return base;
} catch {
return emptyStats();
}
}
persistStats() {
const full = path.join(this.dir, STATS_FILE);
const tmp = `${full}.tmp`;
try {
fs.writeFileSync(tmp, JSON.stringify(this.counters), { mode: 384 });
fs.renameSync(tmp, full);
} catch {
}
}
}
function createUsageQueueSink(queue, opts) {
return {
emit(event) {
try {
if (!event || typeof event.callId !== "string" || event.callId.length === 0) return;
queue.append({
v: 1,
idempotencyKey: buildMachineIdempotencyKey(opts.machineId, event.callId, event.ordinal),
machineId: opts.machineId,
queuedAt: Date.now(),
event
});
} catch (err) {
logger.debug(`[USAGE QUEUE] sink emit failed: ${err}`);
}
}
};
}
const DEFAULT_BATCH = 500;
const DEFAULT_INTERVAL_MS = 6e4;
const DEFAULT_MIN_BACKOFF_MS = 5e3;
const DEFAULT_MAX_BACKOFF_MS = 15 * 6e4;
function toWire(entry) {
return { idempotencyKey: entry.idempotencyKey, ...entry.event };
}
async function defaultPost(url, body, headers) {
const res = await axios.post(url, body, {
headers,
timeout: 3e4,
// We branch on the status ourselves — a 4xx must not become a throw,
// because "permanently rejected" and "retry later" need different
// queue handling.
validateStatus: () => true
});
return { status: res.status, data: res.data };
}
class UsageQueueUploader {
queue;
machineId;
url;
getToken;
batchSize;
intervalMs;
minBackoffMs;
maxBackoffMs;
post;
timer = null;
running = false;
stopped = true;
backoffMs;
constructor(options) {
this.queue = options.queue;
this.machineId = options.machineId;
this.url = `${options.serverUrl.replace(/\/+$/, "")}/v1/usage-events`;
this.getToken = options.getToken;
this.batchSize = Math.max(1, Math.min(options.batchSize ?? DEFAULT_BATCH, DEFAULT_BATCH));
this.intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
this.minBackoffMs = options.minBackoffMs ?? DEFAULT_MIN_BACKOFF_MS;
this.maxBackoffMs = options.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
this.post = options.post ?? defaultPost;
this.backoffMs = this.minBackoffMs;
}
/** Begin the periodic drain. Idempotent. */
start() {
if (!this.stopped) return;
this.stopped = false;
this.schedule(0);
}
stop() {
this.stopped = true;
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
}
schedule(delayMs) {
if (this.stopped) return;
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.timer = null;
void this.tick();
}, delayMs);
this.timer.unref?.();
}
async tick() {
if (this.stopped) return;
let next = this.intervalMs;
try {
const result = await this.drainOnce();
if (result.backoff || result.ledgerDisabled) {
next = this.backoffMs;
this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
} else {
this.backoffMs = this.minBackoffMs;
next = this.queue.sealedSegments().length > 0 ? 0 : this.intervalMs;
}
} catch (err) {
logger.debug(`[USAGE QUEUE] drain threw: ${err}`);
next = this.backoffMs;
this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
}
this.schedule(next);
}
/** One full pass over the sealed segments. Never throws. */
async drainOnce() {
const out = {
batches: 0,
written: 0,
duplicate: 0,
retained: 0,
discarded: 0,
ledgerDisabled: false,
backoff: false
};
if (this.running) return out;
this.running = true;
try {
this.queue.flushSync();
this.queue.sealActive();
for (const segment of this.queue.sealedSegments()) {
const entries = this.queue.readSegment(segment);
if (entries.length === 0) {
this.queue.removeSegment(segment);
continue;
}
const retained = [];
let aborted = false;
for (let i = 0; i < entries.length; i += this.batchSize) {
const chunk = entries.slice(i, i + this.batchSize);
const verdict = await this.postBatch(chunk);
out.batches++;
if (verdict.kind === "retry" || verdict.kind === "disabled") {
if (verdict.kind === "disabled") out.ledgerDisabled = true;
else out.backoff = true;
retained.push(...entries.slice(i));
aborted = true;
break;
}
for (const entry of chunk) {
const r = verdict.results.get(entry.idempotencyKey);
if (r === "written") out.written++;
else if (r === "duplicate") out.duplicate++;
else if (r === "invalid") out.discarded++;
else retained.push(entry);
}
}
this.queue.rewriteSegment(segment, retained);
out.retained += retained.length;
if (aborted) break;
}
if (out.written + out.duplicate > 0) this.queue.recordUploaded(out.written + out.duplicate);
if (out.discarded > 0) this.queue.recordDiscarded(out.discarded);
return out;
} finally {
this.running = false;
}
}
async postBatch(chunk) {
let status;
let data;
try {
const res = await this.post(
this.url,
{ machineId: this.machineId, events: chunk.map(toWire) },
{
"Content-Type": "application/json",
Authorization: `Bearer ${this.getToken()}`
}
);
status = res.status;
data = res.data;
} catch (err) {
logger.debug(`[USAGE QUEUE] upload transport error: ${err}`);
return { kind: "retry" };
}
if (status === 200 || status === 202) {
const body = data ?? {};
if (body.ledgerDisabled) return { kind: "disabled" };
const results2 = /* @__PURE__ */ new Map();
for (const r of body.results ?? []) {
if (r && typeof r.key === "string") results2.set(r.key, r.result);
}
return { kind: "settled", results: results2 };
}
if (status === 401 || status === 403 || status === 408 || status === 429 || status >= 500) {
logger.debug(`[USAGE QUEUE] upload retryable status ${status}`);
return { kind: "retry" };
}
logger.debug(`[USAGE QUEUE] upload permanently rejected with status ${status}; dropping ${chunk.length} entries`);
const results = /* @__PURE__ */ new Map();
for (const entry of chunk) results.set(entry.idempotencyKey, "invalid");
return { kind: "settled", results };
}
}
let sharedQueue = null;
function getUsageQueue(options) {
if (!sharedQueue) sharedQueue = new UsageQueue(options);
return sharedQueue;
}
const AuthProbeRequestSchema = z.z.object({
agents: z.z.array(AgentKindSchema).optional()
});
const execFileAsync = util.promisify(child_process.execFile);
const HEARTBEAT_INTERVAL_MS = 2e4;
const RECONNECT_WATCHDOG_MS = 3e4;
async function getAvailableMemoryBytes() {
if (process.platform === "darwin") {
try {
const { stdout } = await execFileAsync("vm_stat", [], { timeout: 1e3 });
const pageSizeMatch = stdout.match(/page size of (\d+) bytes/);
if (!pageSizeMatch) return null;
const pageSize = parseInt(pageSizeMatch[1], 10);
const num = (label) => {
const m = stdout.match(new RegExp(`Pages ${label}:\\s+(\\d+)\\.`));
return m ? parseInt(m[1], 10) : 0;
};
const reclaimablePages = num("free") + num("inactive") + num("speculative") + num("purgeable");
return reclaimablePages * pageSize;
} catch {
return null;
}
}
if (process.platform === "linux") {
try {
const data = await fs$1.promises.readFile("/proc/meminfo", "utf8");
const availMatch = data.match(/^MemAvailable:\s+(\d+)\s+kB/m);
if (availMatch) return parseInt(availMatch[1], 10) * 1024;
const free = data.match(/^MemFree:\s+(\d+)\s+kB/m);
const buffers = data.match(/^Buffers:\s+(\d+)\s+kB/m);
const cached = data.match(/^Cached:\s+(\d+)\s+kB/m);
if (free && buffers && cached) {
return (parseInt(free[1], 10) + parseInt(buffers[1], 10) + parseInt(cached[1], 10)) * 1024;
}
return null;
} catch {
return null;
}
}
return null;
}
async function sampleCpuPercent() {
const sample = () => {
let idle = 0;
let total = 0;
for (const c of os$1.cpus()) {
for (const t of Object.values(c.times)) total += t;
idle += c.times.idle;
}
return { idle, total };
};
const a = sample();
await new Promise((r) => setTimeout(r, 200));
const b = sample();
const idleDelta = b.idle - a.idle;
const totalDelta = b.total - a.total;
if (totalDelta <= 0) return 0;
return Math.max(0, Math.min(100, Math.round((1 - idleDelta / totalDelta) * 100)));
}
async function handleFsReaddir(requestedPath) {
const realPath = path$1.resolve(requestedPath);
try {
const entries = await fs$1.promises.readdir(realPath, { withFileTypes: true });
const result = await Promise.all(entries.map(async (entry) => {
const fullPath = path$1.join(realPath, entry.name);
try {
const stat = await fs$1.promises.stat(fullPath);
return {
name: entry.name,
type: entry.isDirectory() ? "directory" : entry.isSymbolicLink() ? "symlink" : "file",
size: stat.size,
modified: stat.mtimeMs
};
} catch {
return {
name: entry.name,
type: entry.isDirectory() ? "directory" : "file",
size: 0,
modified: 0
};
}
}));
return { entries: result };
} catch (e) {
logger.debug(`[API MACHINE] fs-readdir error for "${realPath}": ${e.message}`);
return { entries: [], error: e.message };
}
}
async function handleFsStat(requestedPath) {
const realPath = path$1.resolve(requestedPath);
try {
const stat = await fs$1.promises.stat(realPath);
return {
stat: {
size: stat.size,
modified: stat.mtimeMs,
isDirectory: stat.isDirectory(),
isFile: stat.isFile(),
permissions: (stat.mode & 511).toString(8)
}
};
} catch (e) {
logger.debug(`[API MACHINE] fs-stat error for "${realPath}": ${e.message}`);
return { stat: null, error: e.message };
}
}
class ApiMachineClient {
constructor(token, machine) {
this.token = token;
this.machine = machine;
this.rpcHandlerManager = new RpcHandlerManager({
scopePrefix: this.machine.id,
encryptionKey: this.machine.encryptionKey,
encryptionVariant: this.machine.encryptionVariant,
logger: (msg, data) => logger.debug(msg, data),
// Every failed machine RPC (decryption failure or handler throw)
// lands in crash reporting; no-op when Sentry DSN is unset.
onError: (error, context) => captureError(error, context)
});
registerCommonHandlers(this.rpcHandlerManager, process.cwd());
}
socket;
/**
* Escape hatch for subsystems (e.g. harness profile-sync) that need to
* register custom events on the machine-scoped socket. Returns the live
* Socket.IO client; may be undefined before connect() is called.
*/
getRawSocket() {
return this.socket;
}
/** Initialised async in connect(); guarded by the null check in handlers */
terminalManager = null;
keepAliveInterval = null;
rpcHandlerManager;
/**
* Single shared device-code flow runner per daemon process. Wave 2
* of agent-auth-foundation. Handles `claude login` / `gcloud auth
* login --no-browser` and any future RFC 8628 device-code flow.
*/
deviceCodeFlow = new DeviceCodeFlow();
pasteFlow = new PasteFlow({ invalidateProbeCache: () => invalidateProbeCache() });
/** Claude subscription paste-code (OOB OAuth) flow. */
claudePasteCodeFlow = new ClaudePasteCodeFlow();
/**
* Drives an agent's OWN `login` command (see auth/flows/cliLogin.ts). This
* is preferred over every daemon-implemented OAuth path when the binary is
* installed, because the CLI holds its own PKCE verifier, talks to its own
* (current) endpoints, and writes its own credential file.
*/
cliLoginFlow = new CliLoginFlow();
/**
* Single shared agent-CLI install runner (start → poll → cancel). Gated
* daemon-side by CONSORTIUM_AGENT_INSTALL=0. See cli-install/installFlow.ts.
*/
installFlow = new InstallFlow();
hasConnectedOnce = false;
consecutiveAuthErrors = 0;
disconnectedAt = null;
reconnectTimer = null;
manualReconnectAttempts = 0;
destroyed = false;
/**
* L3.3 — drains the durable machine-origin UsageEvent queue to
* `POST /v1/usage-events`. The daemon owns it because the daemon owns the
* server credential; the per-session LMP only ever writes to the queue.
* Created lazily on first successful connect (see `startUsageUploader`).
*/
usageUploader = null;
setRPCHandlers({
spawnSession,
stopSession,
reprovisionSession,
requestShutdown
}) {
this.rpcHandlerManager.registerHandler("spawn-consortium-session", async (params) => {
const { directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token, environmentVariables, resume, resumeSessionId, profileKeyMode, pmMode, worktreeMode, cardId, cardSlug, projectId, repoBaseBranch, authSource, authIdentity, providerId, modelId, byokEnvVar, byokBaseURL, accountKeyId, _reqId } = params || {};
logger.debug(`[API MACHINE] Spawning session with params: ${JSON.stringify(params)}`);
if (!directory) {
throw new Error("Directory is required");
}
const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation, agent, token, environmentVariables, resume, resumeSessionId, profileKeyMode, pmMode, worktreeMode, cardId, cardSlug, projectId, repoBaseBranch, authSource, authIdentity, providerId, modelId, byokEnvVar, byokBaseURL, accountKeyId, requestId: typeof _reqId === "string" ? _reqId : void 0 });
switch (result.type) {
case "success":
logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
return { type: "success", sessionId: result.sessionId };
case "requestToApproveDirectoryCreation":
logger.debug(`[API MACHINE] Requesting directory creation approval for: ${result.directory}`);
return { type: "requestToApproveDirectoryCreation", directory: result.directory };
case "error":
throw new Error(result.errorMessage);
case "mint_failed":
logger.debug(`[API MACHINE] Spawn refused: mint_failed scope=${result.scope} code=${result.errorCode}`);
return {
type: "mint_failed",
errorCode: result.errorCode,
message: result.message,
scope: result.scope
};
}
});
this.rpcHandlerManager.registerHandler("stop-session", (params) => {
const { sessionId } = params || {};
if (!sessionId) {
throw new Error("Session ID is required");
}
const success = stopSession(sessionId);
if (!success) {
throw new Error("Session not found or failed to stop");
}
logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
return { message: "Session stopped" };
});
this.rpcHandlerManager.registerHandler("reprovision-session", async (params) => {
const sessionId = typeof params?.sessionId === "string" ? params.sessionId : void 0;
if (!sessionId) {
throw new Error("Session ID is required");
}
const resume = params?.resume && typeof params.resume === "object" ? {
sessionId: typeof params.resume.sessionId === "string" ? params.resume.sessionId : void 0,
rolloutPath: typeof params.resume.rolloutPath === "string" ? params.resume.rolloutPath : void 0
} : void 0;
const result = await reprovisionSession(sessionId, resume);
switch (result.type) {
case "success":
logger.debug(`[API MACHINE] Reprovisioned session ${sessionId} -> ${result.sessionId}`);
return { type: "success", sessionId: result.sessionId };
case "error":
throw new Error(result.errorMessage);
case "requestToApproveDirectoryCreation":
throw new Error(`Reprovision failed: working directory '${result.directory}' is unexpectedly missing`);
case "mint_failed":
logger.debug(`[API MACHINE] Reprovision refused: mint_failed scope=${result.scope} code=${result.errorCode}`);
return {
type: "mint_failed",
errorCode: result.errorCode,
message: result.message,
scope: result.scope
};
}
});
this.rpcHandlerManager.registerHandler("stop-daemon", () => {
logger.debug("[API MACHINE] Received stop-daemon RPC request");
setTimeout(() => {
logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
requestShutdown();
}, 100);
return { message: "Daemon stop request acknowledged, starting shutdown sequence..." };
});
this.rpcHandlerManager.registerHandler("auth-probe", async (params) => {
const parsed = AuthProbeRequestSchema.safeParse(params ?? {});
if (!parsed.success) {
throw new Error(`Invalid auth-probe request: ${parsed.error.message}`);
}
const requested = parsed.data.agents;
const result = await runAllProbes({ agents: requested });
const states = requested ? result.states.filter((s) => requested.includes(s.agent)) : result.states;
logger.debug(`[API MACHINE] auth-probe completed: ${states.map((s) => `${s.agent}=${s.status}`).join(", ")}`);
return { states, probedAt: result.probedAt };
});
this.rpcHandlerManager.registerHandler("auth-capabilities", async () => {
return {
capabilities: connectableAgents(),
// Included so the app can show "verified against codex 0.144.4"
// and so a stale daemon is visible rather than silent.
all: AGENT_AUTH_CAPABILITIES,
cliVersion: configuration.currentCliVersion
};
});
this.rpcHandlerManager.registerHandler("account-usage", async (params) => {
const requested = typeof params?.accountKeyId === "string" ? params.accountKeyId : void 0;
const providerId = typeof params?.providerId === "string" ? params.providerId : void 0;
let snapshots = listAccountUsage();
if (requested) snapshots = snapshots.filter((s) => s.accountKeyId === requested);
if (providerId) snapshots = snapshots.filter((s) => s.providerId === providerId);
return { ok: true, snapshots, fetchedAt: Date.now() };
});
this.rpcHandlerManager.registerHandler("get-util", async () => {
const cpuCount = os$1.cpus().length;
const loadOneMin = os$1.loadavg()[0];
const memTotalBytes = os$1.totalmem();
const cpuPercent = process.platform === "win32" ? await sampleCpuPercent() : loadOneMin > 0 ? Math.min(100, Math.round(loadOneMin / cpuCount * 100)) : null;
const availableBytes = await getAvailableMemoryBytes() ?? os$1.freemem();
return {
cpuPercent,
memoryUsedMb: Math.round((memTotalBytes - availableBytes) / 1024 / 1024),
memoryTotalMb: Math.round(memTotalBytes / 1024 / 1024)
};
});
this.rpcHandlerManager.registerHandler("get-hardware", async (params) => {
return await detectHardware({ force: !!params?.force });
});
this.rpcHandlerManager.registerHandler("auth-flow-start", async (params) => {
const agent = AgentKindSchema.parse(params?.agent);
const mode = AuthFlowKindSchema.parse(params?.mode);
return await startAuthFlow(agent, mode, {
deviceCode: this.deviceCodeFlow,
paste: this.pasteFlow,
claudePasteCode: this.claudePasteCodeFlow,
cliLogin: {
supports: (a) => CliLoginFlow.supports(a),
start: (a) => this.cliLoginFlow.start(a)
},
pickDeviceCodeAdapter,
log: (m) => logger.debug(`[API MACHINE] ${m}`)
});
});
this.rpcHandlerManager.registerHandler("auth-flow-poll", async (params) => {
const handleId = String(params?.handleId ?? "");
if (!handleId) throw new Error("auth-flow-poll: handleId required");
const cliLoginStatus = this.cliLoginFlow.poll(handleId);
if (cliLoginStatus) return cliLoginStatus;
const pasteCodeStatus = this.claudePasteCodeFlow.poll(handleId);
if (pasteCodeStatus) return pasteCodeStatus;
const dcStatus = await this.deviceCodeFlow.getStatus(handleId);
if (dcStatus) return dcStatus;
const pasteStatus = this.pasteFlow.poll(handleId);
if (pasteStatus) return pasteStatus;
throw new Error(`auth-flow-poll: unknown handleId ${handleId}`);
});
this.rpcHandlerManager.registerHandler("auth-flow-submit-paste", async (params) => {
const handleId = String(params?.handleId ?? "");
const key = String(params?.key ?? "");
if (!handleId) throw new Error("auth-flow-submit-paste: handleId required");
if (!key) throw new Error("auth-flow-submit-paste: key required");
if (this.cliLoginFlow.poll(handleId)) {
return await this.cliLoginFlow.submit(handleId, key);
}
if (this.claudePasteCodeFlow.poll(handleId)) {
return await this.claudePasteCodeFlow.submit(handleId, key);
}
return await this.pasteFlow.submit(handleId, key);
});
this.rpcHandlerManager.registerHandler("auth-flow-cancel", async (params) => {
const handleId = String(params?.handleId ?? "");
if (!handleId) throw new Error("auth-flow-cancel: handleId required");
if (this.cliLoginFlow.poll(handleId)) {
await this.cliLoginFlow.cancel(handleId);
return { ok: true };
}
if (this.claudePasteCodeFlow.poll(handleId)) {
await this.claudePasteCodeFlow.cancel(handleId);
return { ok: true };
}
const dcStatus = await this.deviceCodeFlow.getStatus(handleId);
if (dcStatus) {
await this.deviceCodeFlow.cancel(handleId);
return { ok: true };
}
const pasteStatus = this.pasteFlow.poll(handleId);
if (pasteStatus) {
await this.pasteFlow.cancel(handleId);
return { ok: true };
}
throw new Error(`auth-flow-cancel: unknown handleId ${handleId}`);
});
this.rpcHandlerManager.registerHandler("auth-clear-creds", async (params) => {
const agent = AgentKindSchema.parse(params?.agent);
logger.debug(`[API MACHINE] auth-clear-creds for agent=${agent}`);
const result = clearAgentCredentials(agent);
invalidateProbeCache();
return { ok: result.ok, cleared: result.cleared, skipped: result.skipped };
});
registerCliInstallHandlers(this.rpcHandlerManager, this.installFlow);
}
/**
* Update machine metadata
* Currently unused, changes from the mobile client are more likely
* for example to set a custom name.
*/
async updateMachineMetadata(handler) {
await backoff(async () => {
const updated = handler(this.machine.metadata);
const answer = await this.socket.emitWithAck("machine-update-metadata", {
machineId: this.machine.id,
metadata: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
expectedVersion: this.machine.metadataVersion
});
if (answer.result === "success") {
this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
this.machine.metadataVersion = answer.version;
logger.debug("[API MACHINE] Metadata updated successfully");
} else if (answer.result === "version-mismatch") {
if (answer.version > this.machine.metadataVersion) {
this.machine.metadataVersion = answer.version;
this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.metadata));
}
throw new Error("Metadata version mismatch");
}
});
}
/**
* Update daemon state (runtime info) - similar to session updateAgentState
* Simplified without lock - relies on backoff for retry
*/
async updateDaemonState(handler) {
await backoff(async () => {
const updated = handler(this.machine.daemonState);
const answer = await this.socket.emitWithAck("machine-update-state", {
machineId: this.machine.id,
daemonState: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, updated)),
expectedVersion: this.machine.daemonStateVersion
});
if (answer.result === "success") {
this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
this.machine.daemonStateVersion = answer.version;
logger.debug("[API MACHINE] Daemon state updated successfully");
} else if (answer.result === "version-mismatch") {
if (answer.version > this.machine.daemonStateVersion) {
this.machine.daemonStateVersion = answer.version;
this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(answer.daemonState));
}
throw new Error("Daemon state version mismatch");
}
});
}
connect(options) {
if (this.socket) {
logger.debug("[API MACHINE] connect() called with a socket already present \u2014 detaching the previous one");
this.socket.removeAllListeners();
this.socket.io.removeAllListeners();
this.socket.close();
}
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
this.socket = socket_ioClient.io(serverUrl, {
transports: ["websocket"],
auth: {
token: this.token,
clientType: "machine-scoped",
machineId: this.machine.id
},
path: "/v1/updates",
reconnection: true,
reconnectionDelay: 1e3,
reconnectionDelayMax: 1e4,
reconnectionAttempts: Infinity,
ackTimeout: 3e4
});
if (!this.terminalManager) {
getTerminalManager().then((mgr) => {
this.terminalManager = mgr;
}).catch((err) => {
logger.debug(`[API MACHINE] TerminalManager init failed: ${err}`);
});
}
this.socket.on("connect", async () => {
this.stopKeepAlive();
const isReconnect = this.hasConnectedOnce;
this.hasConnectedOnce = true;
this.consecutiveAuthErrors = 0;
this.manualReconnectAttempts = 0;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (isReconnect) {
const disconnectDuration = this.disconnectedAt ? Date.now() - this.disconnectedAt : Infinity;
this.disconnectedAt = null;
logger.debug(`[API MACHINE] Reconnected to server (was disconnected for ${Math.round(disconnectDuration / 1e3)}s)`);
if (disconnectDuration > 18e4) {
this.terminalManager?.closeAllPty();
logger.debug("[API MACHINE] Closed orphan PTY sessions after extended disconnect");
}
if (options?.onReconnect) {
try {
const freshMachine = await options.onReconnect();
this.machine.metadata = freshMachine.metadata;
this.machine.metadataVersion = freshMachine.metadataVersion;
this.machine.daemonState = freshMachine.daemonState;
this.machine.daemonStateVersion = freshMachine.daemonStateVersion;
logger.debug("[API MACHINE] Re-registered machine after reconnect");
} catch (err) {
logger.debug(`[API MACHINE] Re-registration failed, resetting version numbers to avoid permanent mismatch loop: ${err}`);
this.machine.metadataVersion = 0;
this.machine.daemonStateVersion = 0;
}
}
} else {
logger.debug("[API MACHINE] Connected to server");
}
if (options?.organizationId) {
this.socket.emit("org:join", { organizationId: options.organizationId });
logger.debug(`[API MACHINE] Joined org room: ${options.organizationId}`);
}
this.updateDaemonState((state) => ({
...state,
status: "running",
pid: process.pid,
httpPort: this.machine.daemonState?.httpPort,
startedAt: Date.now()
})).catch((err) => {
logger.debug(`[API MACHINE] Failed to update daemon state to running: ${err}`);
});
await this.rpcHandlerManager.onSocketConnect(this.socket);
if (!this.socket.connected) return;
this.socket.emit("rpc-ready");
if (this.socket.connected) {
this.startKeepAlive();
}
this.startUsageUploader();
if (this.terminalManager && this.socket.connected) {
this.terminalManager.listTerminals().then((terminals) => {
if (this.socket.connected) {
this.socket.emit("terminal:list", {
machineId: this.machine.id,
terminals
});
}
}).catch(() => {
});
}
});
this.socket.on("disconnect", (reason) => {
logger.debug(`[API MACHINE] Disconnected from server (reason: ${reason})`);
this.disconnectedAt = Date.now();
this.rpcHandlerManager.onSocketDisconnect();
this.stopKeepAlive();
this.scheduleRecoveryFromDisconnect(options, reason);
});
this.socket.on("server-shutting-down", (data) => {
logger.debug(`[API MACHINE] Server shutting down (reason: ${data.reason}), will reconnect automatically`);
this.socket.io.engine?.close();
});
this.socket.on("rpc-request", async (data, callback) => {
logger.debugLargeJson(`[API MACHINE] Received RPC request:`, data);
callback(await this.rpcHandlerManager.handleRequest(data));
});
this.socket.on("update", (data) => {
if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
const update = data.body;
if (update.metadata) {
logger.debug("[API MACHINE] Received external metadata update");
this.machine.metadata = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.metadata.value));
this.machine.metadataVersion = update.metadata.version;
}
if (update.daemonState) {
logger.debug("[API MACHINE] Received external daemon state update");
this.machine.daemonState = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(update.daemonState.value));
this.machine.daemonStateVersion = update.daemonState.version;
}
} else {
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
}
});
this.socket.on("ephemeral", async (data) => {
try {
if (data && data.type === "automation-run") {
if (this.rpcHandlerManager.hasHandler("automation-run")) {
logger.debug(`[API MACHINE] automation-run ephemeral \u2192 local handler (run ${data.runId ?? "?"})`);
await this.rpcHandlerManager.callLocal("automation-run", data);
} else {
logger.debug("[API MACHINE] automation-run ephemeral: no local handler registered");
}
return;
}
if (!data || data.type !== "harness-rpc") return;
const command = String(data.command || "");
const harnessType = String(data.harnessType || "");
if (!command || !harnessType) return;
const method = `harness-${command}`;
if (!this.rpcHandlerManager.hasHandler(method)) {
logger.debug(`[API MACHINE] harness-rpc: no local handler for ${method}`);
return;
}
const { type: _type, command: _command, ...forward } = data;
logger.debug(`[API MACHINE] harness-rpc \u2192 ${method} (${harnessType}${data.vpsInstanceId ? `, vps=${data.vpsInstanceId}` : ""})`);
await this.rpcHandlerManager.callLocal(method, forward);
} catch (e) {
logger.debug(`[API MACHINE] harness-rpc dispatch failed: ${e.message}`);
}
});
const viewerToPersistent = /* @__PURE__ */ new Map();
const resolveTerminal = (terminalId) => viewerToPersistent.get(terminalId) ?? terminalId;
this.socket.on("pty:open", async (data, callback) => {
const { terminalId, cols, rows, persistentTerminalId } = data;
if (!terminalId || !cols || !rows) {
callback({ ok: false, error: "Missing required parameters" });
return;
}
const mgr = this.terminalManager;
const emitData = (outputData) => {
const encrypted = encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, outputData.toString("base64")));
this.socket.emit("pty:data", {
machineId: this.machine.id,
terminalId,
data: encrypted
});
};
const emitExit = () => {
this.socket.emit("pty:exit", {
machineId: this.machine.id,
terminalId
});
};
if (persistentTerminalId && mgr) {
const attachResult = await mgr.attachTerminal(
persistentTerminalId,
terminalId,
// use the pty-scoped terminalId as the viewerId
Number(cols),
Number(rows),
emitData,
() => emitExit()
);
if (attachResult.ok) {
viewerToPersistent.set(terminalId, persistentTerminalId);
if (attachResult.replay && attachResult.replay.length > 0) {
emitData(attachResult.replay);
}
callback({ ok: true });
return;
}
logger.debug(`[API MACHINE] Persistent attach failed for ${persistentTerminalId}: ${attachResult.error}`);
callback({ ok: false, error: attachResult.error ?? "Persistent terminal no longer exists" });
return;
}
const result = mgr ? mgr.openPty(terminalId, Number(cols), Number(rows), emitData, emitExit) : { ok: false, error: "TerminalManager not ready" };
callback(result);
});
this.socket.on("pty:data", (data) => {
const { terminalId, data: encryptedData } = data;
if (!terminalId || !encryptedData) return;
const decrypted = decrypt(this.machine.encryptionKey, this.machine.encryptionVariant, decodeBase64(encryptedData));
if (!decrypted || !this.terminalManager) return;
const buf = Buffer.from(decrypted, "base64");
if (!this.terminalManager.writeTerminal(resolveTerminal(terminalId), buf)) {
this.terminalManager.writePty(terminalId, buf);
}
});
this.socket.on("pty:resize", (data) => {
const { terminalId, cols, rows } = data;
if (!terminalId || !cols || !rows || !this.terminalManager) return;
const c = Number(cols);
const r = Number(rows);
if (!this.terminalManager.resizeTerminal(resolveTerminal(terminalId), terminalId, c, r)) {
this.terminalManager.resizePty(terminalId, c, r);
}
});
this.socket.on("pty:close", (data) => {
const { terminalId } = data;
if (!terminalId || !this.terminalManager) return;
this.terminalManager.detachTerminal(resolveTerminal(terminalId), terminalId);
viewerToPersistent.delete(terminalId);
this.terminalManager.closePty(terminalId);
});
this.socket.on("terminal:create", async (data, callback) => {
const mgr = this.terminalManager;
const requestedMode = data.persistenceMode ?? "tmux";
if (!mgr) {
callback({ ok: false, mode: requestedMode, error: "TerminalManager not ready" });
return;
}
const caps = mgr.getCapabilities();
let mode = requestedMode;
if (mode === "ephemeral" || mode === "tmux" && !caps.tmux || mode === "zellij" && !caps.zellij) {
mode = mgr.defaultMode();
}
if (mode === "ephemeral") {
callback({ ok: false, mode, error: "No persistent terminal backend available on this machine" });
return;
}
try {
const result = await mgr.createTerminal(mode, {
id: data.terminalId,
cols: Number(data.cols ?? 80),
rows: Number(data.rows ?? 24),
cwd: data.cwd,
shell: data.shell
});
callback(result);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
callback({ ok: false, mode, error: message });
}
});
this.socket.on("terminal:list", async (_data, callback) => {
const mgr = this.terminalManager;
if (!mgr) {
callback({ ok: true, terminals: [] });
return;
}
const terminals = await mgr.listTerminals();
callback({ ok: true, terminals });
});
this.socket.on("terminal:destroy", async (data, callback) => {
const mgr = this.terminalManager;
if (!mgr) {
callback({ ok: false, error: "TerminalManager not ready" });
return;
}
await mgr.destroyTerminal(data.terminalId);
callback({ ok: true });
});
this.socket.on("machine:fs-readdir", async (data) => {
const result = await handleFsReaddir(data.path);
this.socket.emit("machine:fs-readdir-result", {
requestId: data.requestId,
payload: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, result))
});
});
this.socket.on("machine:fs-stat", async (data) => {
const result = await handleFsStat(data.path);
this.socket.emit("machine:fs-stat-result", {
requestId: data.requestId,
payload: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, result))
});
});
this.socket.on("connect_error", (error) => {
const isAuthError = error.message.includes("authentication") || error.message.includes("Invalid") || error.message.includes("suspended");
if (isAuthError) {
this.consecutiveAuthErrors++;
logger.debug(`[API MACHINE] Auth error #${this.consecutiveAuthErrors}: ${error.message}`);
if (this.consecutiveAuthErrors >= 5) {
logger.debug(`[API MACHINE] Authentication permanently failed after ${this.consecutiveAuthErrors} attempts. Shutting down.`);
this.socket.disconnect();
if (options?.onAuthFailure) {
options.onAuthFailure();
}
return;
}
}
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
this.scheduleManualReconnect(options, isAuthError);
});
this.socket.io.on("error", (error) => {
logger.debug("[API MACHINE] Socket error:", error);
});
}
/**
* Recovery for a socket that was ESTABLISHED and then dropped.
*
* `scheduleManualReconnect` covers handshake failures (`connect_error`).
* Nothing covered a *post-connect* drop: the disconnect handler only
* stopped the keep-alive and trusted `reconnection: true` to bring the
* socket back. That trust is misplaced for two of Socket.IO v4's
* disconnect reasons, and the gap is a real outage — on 2026-08-16
* Station 1 logged `Disconnected from server` and then went silent for
* hours: process alive, log still writing, zero sockets open, machine
* reading offline. `Restart=always` cannot help because nothing exits.
*
* Reasons and who owns recovery:
* `io client disconnect` — we called disconnect(). Intentional; do
* nothing, or shutdown() would reopen itself.
* `io server disconnect` — the server called socket.disconnect(). The
* Manager treats this as terminal and will
* NOT retry. We must reconnect ourselves.
* everything else — transport close/error, ping timeout, parse
* error. The Manager does retry these, so
* arm a watchdog instead of racing it, and
* take over only if it has not recovered.
*
* The watchdog shares `reconnectTimer` with the manual scheduler on
* purpose: a successful `connect` clears that timer, so recovering by
* either route disarms the other automatically.
*/
scheduleRecoveryFromDisconnect(options, reason) {
if (this.destroyed) return;
if (reason === "io client disconnect") return;
if (reason === "io server disconnect") {
logger.debug("[API MACHINE] Server closed the socket; Socket.IO will not retry it \u2014 reconnecting manually");
this.scheduleManualReconnect(options, false);
return;
}
if (this.reconnectTimer) return;
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
if (this.destroyed || this.socket.connected) return;
logger.debug(`[API MACHINE] Still disconnected ${RECONNECT_WATCHDOG_MS / 1e3}s after "${reason}" \u2014 taking over from Socket.IO's reconnection`);
this.scheduleManualReconnect(options, false);
}, RECONNECT_WATCHDOG_MS);
}
/**
* Schedule a manual reconnect with exponential backoff + jitter. Called
* from the connect_error handler because Socket.IO v4 does not retry
* middleware-rejected handshakes on its own (see comment in the handler).
*
* If a refreshToken callback is provided and the rejection looked like
* an auth error, the new token is swapped into socket.auth before the
* next attempt. Backoff: 1s, 2s, 4s, 8s, 10s (cap), with up to 1s of
* random jitter on top. The 10s cap matches socket.io's
* reconnectionDelayMax and keeps the worst-case heartbeat gap under the
* app's 60s offline threshold so a reconnecting daemon never reads as
* offline. Reset to 0 on successful connect.
*/
scheduleManualReconnect(options, refreshIfPossible) {
if (this.destroyed) return;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.manualReconnectAttempts++;
const delay = Math.min(1e3 * Math.pow(2, this.manualReconnectAttempts - 1), 1e4) + Math.floor(Math.random() * 1e3);
logger.debug(`[API MACHINE] Scheduling manual reconnect attempt #${this.manualReconnectAttempts} in ${delay}ms (refresh=${refreshIfPossible && !!options?.refreshToken})`);
this.reconnectTimer = setTimeout(async () => {
this.reconnectTimer = null;
if (this.destroyed) return;
if (this.socket.connected) return;
if (refreshIfPossible && options?.refreshToken) {
try {
const fresh = await options.refreshToken();
if (fresh && fresh !== this.token) {
this.token = fresh;
this.socket.auth.token = fresh;
logger.debug("[API MACHINE] Refreshed auth token before manual reconnect");
} else {
logger.debug("[API MACHINE] Token refresh returned no new token; will retry with existing token");
}
} catch (err) {
logger.debug(`[API MACHINE] Token refresh threw: ${err}`);
}
}
logger.debug(`[API MACHINE] Manual reconnect attempt #${this.manualReconnectAttempts} firing`);
this.socket.connect();
}, delay);
}
/**
* L3.3 — start the durable usage-queue drain.
*
* Idempotent: the uploader is created once per client and `start()` is a
* no-op while it is already running, so reconnect storms cannot stack
* timers. `getToken` is read lazily on every batch so a token refreshed by
* `scheduleManualReconnect` is picked up without recreating anything.
*
* Never fatal: a queue that cannot be opened (read-only home dir) must not
* stop the daemon from serving sessions — metering is telemetry, not the
* product.
*/
startUsageUploader() {
if (this.destroyed) return;
try {
if (!this.usageUploader) {
this.usageUploader = new UsageQueueUploader({
queue: getUsageQueue(),
machineId: this.machine.id,
serverUrl: configuration.serverUrl,
getToken: () => this.token
});
}
this.usageUploader.start();
} catch (err) {
logger.debug(`[API MACHINE] usage-event uploader unavailable: ${err}`);
}
}
startKeepAlive() {
this.stopKeepAlive();
const sendHeartbeat = () => {
const payload = {
machineId: this.machine.id,
time: Date.now()
};
if (process.env.DEBUG) {
logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
}
this.socket.emit("machine-alive", payload);
};
sendHeartbeat();
this.keepAliveInterval = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
logger.debug(`[API MACHINE] Keep-alive started (immediate + ${HEARTBEAT_INTERVAL_MS / 1e3}s interval)`);
}
stopKeepAlive() {
if (this.keepAliveInterval) {
clearInterval(this.keepAliveInterval);
this.keepAliveInterval = null;
logger.debug("[API MACHINE] Keep-alive stopped");
}
}
/**
* Emit an encrypted OpenClaw activity event to be relayed to the mobile app.
* The encryptedPayload should already be base64(encrypted(event data)).
*/
emitOpenClawActivity(eventType, agentId, encryptedPayload) {
if (!this.socket?.connected) {
logger.debug("[API MACHINE] Cannot emit openclaw:activity - socket not connected");
return;
}
this.socket.emit("openclaw:activity", {
machineId: this.machine.id,
eventType,
agentId,
encryptedPayload,
timestamp: Date.now()
});
}
/**
* Emit one streaming agent-chat frame. The payload is encrypted here
* with the machine key so the server relays ciphertext only; the app
* decrypts with the same machine key it already holds.
*/
emitAgentChatFrame(frame) {
if (!this.socket?.connected) {
logger.debug("[API MACHINE] Cannot emit harness:agent-chat-frame - socket not connected");
return;
}
this.socket.emit("harness:agent-chat-frame", {
machineId: this.machine.id,
requestId: frame.requestId,
agentId: frame.agentId,
seq: frame.seq,
encryptedPayload: encodeBase64(encrypt(this.machine.encryptionKey, this.machine.encryptionVariant, frame.payload)),
done: frame.done,
timestamp: Date.now()
});
}
/**
* Emit an OpenClaw status update (unencrypted, for quick display).
*/
emitOpenClawStatus(status, version, gatewayPort, channels, agentCount) {
if (!this.socket?.connected) {
logger.debug("[API MACHINE] Cannot emit openclaw:status - socket not connected");
return;
}
this.socket.emit("openclaw:status", {
machineId: this.machine.id,
status,
version,
gatewayPort,
channels,
agentCount,
timestamp: Date.now()
});
}
/**
* Emit a deployment health status event to the server for mobile app display.
*
* Accepts the full set of deployment lifecycle statuses (health, container,
* deploy/stop/remove) plus optional metadata fields used by the deployment
* RPC handlers in daemon/run.ts.
*/
emitDeploymentStatus(data) {
if (!this.socket?.connected) {
logger.debug("[API MACHINE] Cannot emit deployment-status - socket not connected");
return;
}
this.socket.emit("deployment-status", { timestamp: Date.now(), ...data });
}
/** Exposes the RPC handler manager for registering additional handlers (e.g. OpenClaw) */
get rpcHandlers() {
return this.rpcHandlerManager;
}
/** Exposes the machine's encryption key for the bridge to encrypt activity payloads */
get encryptionKey() {
return this.machine.encryptionKey;
}
/** Exposes the encryption variant */
get encryptionVariant() {
return this.machine.encryptionVariant;
}
shutdown() {
logger.debug("[API MACHINE] Shutting down");
this.destroyed = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.terminalManager?.closeAllPty();
this.stopKeepAlive();
this.usageUploader?.stop();
try {
getUsageQueue().close();
} catch {
}
if (this.socket) {
this.socket.close();
logger.debug("[API MACHINE] Socket closed");
}
this.deviceCodeFlow.dispose();
this.claudePasteCodeFlow.dispose();
this.cliLoginFlow.dispose();
this.installFlow.dispose();
}
}
function pickDeviceCodeAdapter(agent) {
const cap = getAgentAuthCapability(agent);
if (cap?.remoteMode === "device-code") {
return new NativeDeviceAuthAdapter(agent);
}
const alternative = cap?.remoteMode ?? cap?.localMode;
throw new Error(
`auth-flow: ${agent} does not support device-code` + (alternative ? ` \u2014 use mode '${alternative}'` : " \u2014 no subscription login exists for this agent; use an API key")
);
}
const SCHEMA_VERSION = 1;
function storePath() {
return `${configuration.consortiumHomeDir}/session-keys.json`;
}
function lockPath() {
return `${storePath()}.lock`;
}
async function readStoreRaw() {
const path = storePath();
if (!fs.existsSync(path)) {
return { schemaVersion: SCHEMA_VERSION, keys: {} };
}
try {
const content = await fs$3.readFile(path, "utf8");
const parsed = JSON.parse(content);
const keys = parsed.keys && typeof parsed.keys === "object" ? parsed.keys : {};
return {
...parsed,
schemaVersion: parsed.schemaVersion ?? SCHEMA_VERSION,
keys
};
} catch (err) {
logger.warn("[sessionKeyStore] failed to read store, treating as empty", err);
return { schemaVersion: SCHEMA_VERSION, keys: {} };
}
}
async function withLock(fn) {
const LOCK_RETRY_INTERVAL_MS = 100;
const MAX_LOCK_ATTEMPTS = 50;
const STALE_LOCK_TIMEOUT_MS = 1e4;
await fs$3.mkdir(configuration.consortiumHomeDir, { recursive: true, mode: 448 });
let handle;
for (let attempts = 0; attempts < MAX_LOCK_ATTEMPTS; attempts++) {
try {
handle = await fs$3.open(lockPath(), fs$3.constants.O_CREAT | fs$3.constants.O_EXCL | fs$3.constants.O_WRONLY);
break;
} catch (err) {
const code = err?.code;
if (code !== "EEXIST") throw err;
try {
const stat = await (await fs$3.open(lockPath(), fs$3.constants.O_RDONLY)).stat();
if (Date.now() - stat.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
await fs$3.unlink(lockPath()).catch(() => {
});
continue;
}
} catch {
}
await new Promise((r) => setTimeout(r, LOCK_RETRY_INTERVAL_MS));
}
}
if (!handle) {
throw new Error("[sessionKeyStore] could not acquire lock after 5s");
}
try {
return await fn();
} finally {
try {
await handle.close();
} catch {
}
try {
await fs$3.unlink(lockPath());
} catch {
}
}
}
async function saveSessionKey(sessionId, encryptionKey, encryptionVariant) {
await withLock(async () => {
const store = await readStoreRaw();
store.keys[sessionId] = {
encryptionKey: encodeBase64(encryptionKey),
encryptionVariant,
createdAt: Date.now()
};
store.schemaVersion = SCHEMA_VERSION;
const tmp = `${storePath()}.tmp`;
await fs$3.writeFile(tmp, JSON.stringify(store, null, 2), { mode: 384 });
await fs$3.rename(tmp, storePath());
});
}
class PushNotificationClient {
token;
baseUrl;
expo;
constructor(token, baseUrl = "https://api.consortium.dev") {
this.token = token;
this.baseUrl = baseUrl;
this.expo = new expoServerSdk.Expo();
}
/**
* Fetch all push tokens for the authenticated user
*/
async fetchPushTokens() {
try {
const response = await axios.get(
`${this.baseUrl}/v1/push-tokens`,
{
headers: {
"Authorization": `Bearer ${this.token}`,
"Content-Type": "application/json"
}
}
);
logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
response.data.tokens.forEach((token, index) => {
logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
});
return response.data.tokens;
} catch (error) {
logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Send push notification via Expo Push API with retry
* @param messages - Array of push messages to send
*/
async sendPushNotifications(messages) {
logger.debug(`Sending ${messages.length} push notifications`);
const validMessages = messages.filter((message) => {
if (Array.isArray(message.to)) {
return message.to.every((token) => expoServerSdk.Expo.isExpoPushToken(token));
}
return expoServerSdk.Expo.isExpoPushToken(message.to);
});
if (validMessages.length === 0) {
logger.debug("No valid Expo push tokens found");
return;
}
const chunks = this.expo.chunkPushNotifications(validMessages);
for (const chunk of chunks) {
const startTime = Date.now();
const timeout = 3e5;
let attempt = 0;
while (true) {
try {
const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
const errors = ticketChunk.filter((ticket) => ticket.status === "error");
if (errors.length > 0) {
const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
logger.debug("[PUSH] Some notifications failed:", errorDetails);
}
if (errors.length === ticketChunk.length) {
throw new Error("All push notifications in chunk failed");
}
break;
} catch (error) {
const elapsed = Date.now() - startTime;
if (elapsed >= timeout) {
logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
break;
}
attempt++;
const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
const remainingTime = timeout - elapsed;
const waitTime = Math.min(delay, remainingTime);
if (waitTime > 0) {
logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
}
}
}
logger.debug(`Push notifications sent successfully`);
}
/**
* Send a push notification to all registered devices for the user
* @param title - Notification title
* @param body - Notification body
* @param data - Additional data to send with the notification
*/
sendToAllDevices(title, body, data) {
logger.debug(`[PUSH] sendToAllDevices called with title: "${title}", body: "${body}"`);
(async () => {
try {
logger.debug("[PUSH] Fetching push tokens...");
const tokens = await this.fetchPushTokens();
logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
tokens.forEach((token, index) => {
logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}`);
});
if (tokens.length === 0) {
logger.debug("No push tokens found for user");
return;
}
const messages = tokens.map((token, index) => {
logger.debug(`[PUSH] Creating message ${index + 1} for token`);
return {
to: token.token,
title,
body,
data,
sound: "default",
priority: "high"
};
});
logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
await this.sendPushNotifications(messages);
logger.debug("[PUSH] Push notifications sent successfully");
} catch (error) {
logger.debug("[PUSH] Error sending to all devices:", error);
}
})();
}
}
function startOfflineReconnection(config) {
let reconnected = false;
let session = null;
let timeoutId = null;
let failureCount = 0;
let cancelled = false;
const defaultHealthCheck = async () => {
await axios.get(`${config.serverUrl}/v1/sessions`, {
timeout: 5e3,
validateStatus: (status) => status < 500
// 4xx = server is up, 5xx = server error
});
};
const healthCheck = config.healthCheck ?? defaultHealthCheck;
const initialDelayMs = config.initialDelayMs ?? 5e3;
const attemptReconnect = async () => {
if (reconnected || cancelled) return;
try {
await healthCheck();
if (cancelled) return;
session = await config.onReconnected();
if (cancelled) return;
reconnected = true;
config.onNotify("\u2705 Reconnected! Session syncing in background.");
logger.debug("[OfflineReconnection] Successfully reconnected");
} catch (e) {
if (axios.isAxiosError(e) && e.response?.status === 401) {
logger.debug("[OfflineReconnection] Authentication error, stopping retries");
cancelled = true;
config.onNotify("\u274C Authentication failed. Please re-authenticate with `consortium auth`.");
config.onCleanup?.();
return;
}
failureCount++;
const delay = exponentialBackoffDelay(failureCount, 5e3, 6e4, 10);
logger.debug(`[OfflineReconnection] Attempt ${failureCount} failed, retrying in ${delay}ms`);
if (!cancelled) {
timeoutId = setTimeout(attemptReconnect, delay);
}
}
};
timeoutId = setTimeout(attemptReconnect, initialDelayMs);
return {
cancel: () => {
cancelled = true;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
config.onCleanup?.();
},
getSession: () => session,
isReconnected: () => reconnected
};
}
const NETWORK_ERROR_CODES = [
"ECONNREFUSED",
"ENOTFOUND",
"ETIMEDOUT",
"ECONNRESET",
"EHOSTUNREACH",
"ENETUNREACH"
];
function isNetworkError(code) {
return code !== void 0 && NETWORK_ERROR_CODES.includes(code);
}
const ERROR_DESCRIPTIONS = {
// Network errors (Node.js)
ECONNREFUSED: "server not accepting connections",
ENOTFOUND: "server hostname not found",
ETIMEDOUT: "connection timed out",
ECONNRESET: "connection reset by server",
EHOSTUNREACH: "server host unreachable",
ENETUNREACH: "network unreachable",
// HTTP errors
"401": "authentication failed - run `consortium auth`",
"403": "access forbidden",
"404": "endpoint not found, check server deployment",
"429": "rate limit exceeded",
"500": "server internal error",
"502": "bad gateway",
"503": "service unavailable"
};
class OfflineState {
state = "online";
failures = /* @__PURE__ */ new Map();
// Dedupe by operation
backend = "Claude";
/** Report failure - accumulates context, prints once on first offline transition */
fail(failure) {
this.failures.set(failure.operation, failure);
if (this.state === "online") {
this.state = "offline";
this.print();
}
}
/** Reset on reconnection */
recover() {
this.state = "online";
this.failures.clear();
}
/** Set backend name before API calls */
setBackend(name) {
this.backend = name;
}
/** Check current state */
isOffline() {
return this.state === "offline";
}
/** Reset for testing - clears all state */
reset() {
this.state = "online";
this.failures.clear();
this.backend = "Claude";
}
print() {
const summary = [...this.failures.values()].map((f) => {
const desc = f.errorCode ? `${f.errorCode} - ${ERROR_DESCRIPTIONS[f.errorCode] || "unknown error"}` : "unknown error";
const url = f.url ? ` at ${f.url}` : "";
return `${f.operation} failed: ${desc}${url}`;
}).join("; ");
console.log(`\u26A0\uFE0F Consortium server unreachable, offline mode with auto-reconnect enabled - error details: ${summary}`);
const allDetails = [...this.failures.values()].flatMap((f) => f.details || []);
allDetails.forEach((line) => console.log(chalk.yellow(` \u2192 ${line}`)));
}
}
const connectionState = new OfflineState();
function isAuthResetSignal(err) {
if (!err || typeof err !== "object") return false;
const resp = err.response;
if (resp && resp.status === 401) {
const headers = resp.headers ?? {};
const headerVal = headers["x-auth-reset"] ?? headers["X-Auth-Reset"];
if (headerVal === "1" || headerVal === 1) return true;
const body = resp.data;
if (body && body.code === "AUTH_RESET") return true;
return false;
}
const data = err.data;
return !!data && data.code === "AUTH_RESET";
}
const RATE_LIMIT_MAX_RETRIES = 3;
const RATE_LIMIT_DEFAULT_DELAY_MS = 5e3;
class AuthExpiredError extends Error {
constructor() {
super("Authentication token is invalid or expired");
this.name = "AuthExpiredError";
}
}
function parseRetryAfterMs(error) {
const retryAfter = error.response?.headers?.["retry-after"];
if (retryAfter) {
const seconds = Number(retryAfter);
if (!isNaN(seconds) && seconds > 0) return seconds * 1e3;
}
const body = error.response?.data;
if (body?.message && typeof body.message === "string") {
const match = body.message.match(/retry in (\d+) second/);
if (match) return Number(match[1]) * 1e3;
}
return RATE_LIMIT_DEFAULT_DELAY_MS;
}
class ApiClient {
static async create(credential) {
return new ApiClient(credential);
}
credential;
pushClient;
constructor(credential) {
this.credential = credential;
this.pushClient = new PushNotificationClient(credential.token, configuration.serverUrl);
}
/**
* Create a new session or load existing one with the given tag
*/
async getOrCreateSession(opts) {
if (!this.credential.encryption) {
throw new Error("Cannot create session without encryption credentials. Please re-authenticate with mobile or web.");
}
let dataEncryptionKey = null;
let encryptionKey;
let encryptionVariant;
if (this.credential.encryption.type === "dataKey") {
encryptionKey = getRandomBytes(32);
encryptionVariant = "dataKey";
let encryptedDataKey = libsodiumEncryptForPublicKey(encryptionKey, this.credential.encryption.publicKey);
dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
dataEncryptionKey.set([0], 0);
dataEncryptionKey.set(encryptedDataKey, 1);
} else {
encryptionKey = this.credential.encryption.secret;
encryptionVariant = "legacy";
}
for (let attempt = 0; attempt <= RATE_LIMIT_MAX_RETRIES; attempt++) {
try {
const response = await axios.post(
`${configuration.serverUrl}/v1/sessions`,
{
tag: opts.tag,
metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
agentState: opts.state ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.state)) : null,
dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : null,
organizationId: opts.organizationId || void 0,
...opts.projectId ? { projectId: opts.projectId } : {}
},
{
headers: {
"Authorization": `Bearer ${this.credential.token}`,
"Content-Type": "application/json"
},
timeout: 6e4
// 1 minute timeout for very bad network connections
}
);
logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
let raw = response.data.session;
let session = {
id: raw.id,
tag: raw.tag,
seq: raw.seq,
metadata: decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)),
metadataVersion: raw.metadataVersion,
agentState: raw.agentState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.agentState)) : null,
agentStateVersion: raw.agentStateVersion,
encryptionKey,
encryptionVariant,
userContentPublicKey: this.credential.encryption?.type === "dataKey" ? this.credential.encryption.publicKey : void 0
};
try {
await saveSessionKey(session.id, session.encryptionKey, session.encryptionVariant);
} catch (err) {
logger.warn("[api] failed to persist session key (non-fatal)", err);
}
return session;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 429) {
const retryMs = parseRetryAfterMs(error);
if (attempt < RATE_LIMIT_MAX_RETRIES) {
logger.debug(`[API] Rate limited on session creation, retrying in ${retryMs}ms (attempt ${attempt + 1}/${RATE_LIMIT_MAX_RETRIES})`);
console.log(chalk.yellow(`\u26A0\uFE0F Server rate limit hit, retrying in ${Math.ceil(retryMs / 1e3)}s...`));
await delay(retryMs);
continue;
}
logger.debug("[API] Rate limit retries exhausted for session creation, falling back to offline mode");
connectionState.fail({
operation: "Session creation",
errorCode: "429",
url: `${configuration.serverUrl}/v1/sessions`,
details: ["Rate limit exceeded after retries, will retry automatically"]
});
return null;
}
logger.debug("[API] [ERROR] Failed to get or create session:", error);
if (error && typeof error === "object" && "code" in error) {
const errorCode = error.code;
if (isNetworkError(errorCode)) {
connectionState.fail({
operation: "Session creation",
caller: "api.getOrCreateSession",
errorCode,
url: `${configuration.serverUrl}/v1/sessions`
});
return null;
}
}
if (axios.isAxiosError(error) && error.response?.status === 401) {
if (isAuthResetSignal(error)) {
try {
const { clearCredentials } = await Promise.resolve().then(function () { return persistence; });
await clearCredentials();
} catch {
}
throw new AuthExpiredError();
}
logger.debug("[API] Transient 401 (no AUTH_RESET) on session creation; keeping creds, will retry");
connectionState.fail({
operation: "Session creation",
caller: "api.getOrCreateSession",
errorCode: "401",
url: `${configuration.serverUrl}/v1/sessions`
});
return null;
}
const is404Error = axios.isAxiosError(error) && error.response?.status === 404 || error && typeof error === "object" && "response" in error && error.response?.status === 404;
if (is404Error) {
connectionState.fail({
operation: "Session creation",
errorCode: "404",
url: `${configuration.serverUrl}/v1/sessions`
});
return null;
}
if (axios.isAxiosError(error) && error.response?.status) {
const status = error.response.status;
if (status >= 500) {
connectionState.fail({
operation: "Session creation",
errorCode: String(status),
url: `${configuration.serverUrl}/v1/sessions`,
details: ["Server encountered an error, will retry automatically"]
});
return null;
}
}
throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
return null;
}
/**
* Register or update machine with the server
* Returns the current machine state from the server with decrypted metadata and daemonState
*/
async getOrCreateMachine(opts) {
if (!this.credential.encryption) {
throw new Error("Cannot register machine without encryption credentials. Please re-authenticate with mobile or web.");
}
let dataEncryptionKey = null;
let encryptionKey;
let encryptionVariant;
if (this.credential.encryption.type === "dataKey") {
encryptionVariant = "dataKey";
encryptionKey = this.credential.encryption.machineKey;
let encryptedDataKey = libsodiumEncryptForPublicKey(this.credential.encryption.machineKey, this.credential.encryption.publicKey);
dataEncryptionKey = new Uint8Array(encryptedDataKey.length + 1);
dataEncryptionKey.set([0], 0);
dataEncryptionKey.set(encryptedDataKey, 1);
} else {
encryptionKey = this.credential.encryption.secret;
encryptionVariant = "legacy";
}
const createMinimalMachine = () => ({
id: opts.machineId,
encryptionKey,
encryptionVariant,
metadata: opts.metadata,
metadataVersion: 0,
daemonState: opts.daemonState || null,
daemonStateVersion: 0,
registered: false
});
for (let attempt = 0; attempt <= RATE_LIMIT_MAX_RETRIES; attempt++) {
try {
const response = await axios.post(
`${configuration.serverUrl}/v1/machines`,
{
id: opts.machineId,
metadata: encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.metadata)),
daemonState: opts.daemonState ? encodeBase64(encrypt(encryptionKey, encryptionVariant, opts.daemonState)) : void 0,
dataEncryptionKey: dataEncryptionKey ? encodeBase64(dataEncryptionKey) : void 0,
vpsInstanceId: opts.vpsInstanceId,
organizationId: opts.organizationId || void 0
},
{
headers: {
"Authorization": `Bearer ${this.credential.token}`,
"Content-Type": "application/json"
},
timeout: 6e4
// 1 minute timeout for very bad network connections
}
);
const raw = response.data.machine;
logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
const machine = {
id: raw.id,
encryptionKey,
encryptionVariant,
metadata: raw.metadata ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.metadata)) : null,
metadataVersion: raw.metadataVersion || 0,
daemonState: raw.daemonState ? decrypt(encryptionKey, encryptionVariant, decodeBase64(raw.daemonState)) : null,
daemonStateVersion: raw.daemonStateVersion || 0,
registered: true
};
return machine;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 429) {
const retryMs = parseRetryAfterMs(error);
if (attempt < RATE_LIMIT_MAX_RETRIES) {
logger.debug(`[API] Rate limited on machine registration, retrying in ${retryMs}ms (attempt ${attempt + 1}/${RATE_LIMIT_MAX_RETRIES})`);
console.log(chalk.yellow(`\u26A0\uFE0F Server rate limit hit, retrying in ${Math.ceil(retryMs / 1e3)}s...`));
await delay(retryMs);
continue;
}
logger.debug("[API] Rate limit retries exhausted for machine registration, falling back to offline mode");
connectionState.fail({
operation: "Machine registration",
errorCode: "429",
url: `${configuration.serverUrl}/v1/machines`,
details: ["Rate limit exceeded after retries, will retry automatically"]
});
return createMinimalMachine();
}
if (axios.isAxiosError(error) && error.code && isNetworkError(error.code)) {
connectionState.fail({
operation: "Machine registration",
caller: "api.getOrCreateMachine",
errorCode: error.code,
url: `${configuration.serverUrl}/v1/machines`
});
return createMinimalMachine();
}
if (axios.isAxiosError(error) && error.response?.status) {
const status = error.response.status;
if (status === 401) {
if (isAuthResetSignal(error)) {
try {
const { clearCredentials } = await Promise.resolve().then(function () { return persistence; });
await clearCredentials();
logger.debug("[API] Cleared stale credentials after AUTH_RESET 401");
} catch {
}
throw new AuthExpiredError();
}
logger.debug("[API] Transient 401 (no AUTH_RESET) on machine registration; keeping creds, using minimal machine");
return createMinimalMachine();
}
if (status === 409) {
const errorBody = error.response?.data;
if (errorBody?.error === "machine_id_conflict" && attempt === 0) {
const { randomUUID } = await import('node:crypto');
const { updateSettings } = await Promise.resolve().then(function () { return persistence; });
const newMachineId = randomUUID();
try {
await updateSettings(async (s) => ({ ...s, machineId: newMachineId }));
logger.debug(`[API] machine_id_conflict on ${opts.machineId}; rotated to ${newMachineId} and retrying`);
console.log(chalk.yellow(
`\u26A0\uFE0F Machine ID was registered to a different account; rotating to a fresh ID for this account.`
));
opts.machineId = newMachineId;
continue;
} catch (rotErr) {
logger.debug("[API] failed to rotate machineId after 409:", rotErr);
}
}
console.log(chalk.yellow(
`\u26A0\uFE0F Machine registration rejected by the server with status ${status}`
));
console.log(chalk.yellow(
` \u2192 This machine ID is already registered to another account on the server`
));
console.log(chalk.yellow(
` \u2192 Run 'consortium doctor clean' to reset local state and generate a new machine ID`
));
return createMinimalMachine();
}
if (status === 403) {
console.log(chalk.yellow(
`\u26A0\uFE0F Machine registration rejected by the server with status 403`
));
return createMinimalMachine();
}
if (status >= 500) {
connectionState.fail({
operation: "Machine registration",
errorCode: String(status),
url: `${configuration.serverUrl}/v1/machines`,
details: ["Server encountered an error, will retry automatically"]
});
return createMinimalMachine();
}
if (status === 404) {
connectionState.fail({
operation: "Machine registration",
errorCode: "404",
url: `${configuration.serverUrl}/v1/machines`
});
return createMinimalMachine();
}
}
throw error;
}
}
return createMinimalMachine();
}
sessionSyncClient(session) {
return new ApiSessionClient(this.credential.token, session);
}
machineSyncClient(machine) {
return new ApiMachineClient(this.credential.token, machine);
}
push() {
return this.pushClient;
}
/**
* Register a vendor API token with the server
* The token is sent as a JSON string - server handles encryption
*/
async registerVendorToken(vendor, apiKey) {
try {
const response = await axios.post(
`${configuration.serverUrl}/v1/connect/${vendor}/register`,
{
token: JSON.stringify(apiKey)
},
{
headers: {
"Authorization": `Bearer ${this.credential.token}`,
"Content-Type": "application/json"
},
timeout: 5e3
}
);
if (response.status !== 200 && response.status !== 201) {
throw new Error(`Server returned status ${response.status}`);
}
logger.debug(`[API] Vendor token for ${vendor} registered successfully`);
} catch (error) {
logger.debug(`[API] [ERROR] Failed to register vendor token:`, error);
throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
/**
* Get vendor API token from the server
* Returns the token if it exists, null otherwise
*/
async getVendorToken(vendor) {
try {
const response = await axios.get(
`${configuration.serverUrl}/v1/connect/${vendor}/token`,
{
headers: {
"Authorization": `Bearer ${this.credential.token}`,
"Content-Type": "application/json"
},
timeout: 5e3
}
);
if (response.status === 404) {
logger.debug(`[API] No vendor token found for ${vendor}`);
return null;
}
if (response.status !== 200) {
throw new Error(`Server returned status ${response.status}`);
}
logger.debug(`[API] Raw vendor token response:`, {
status: response.status,
dataKeys: Object.keys(response.data || {}),
hasToken: "token" in (response.data || {}),
tokenType: typeof response.data?.token
});
let tokenData = null;
if (response.data?.token) {
if (typeof response.data.token === "string") {
try {
tokenData = JSON.parse(response.data.token);
} catch (parseError) {
logger.debug(`[API] Failed to parse token as JSON, using as string:`, parseError);
tokenData = response.data.token;
}
} else if (response.data.token !== null) {
tokenData = response.data.token;
} else {
logger.debug(`[API] Token is null for ${vendor}, treating as not found`);
return null;
}
} else if (response.data && typeof response.data === "object") {
if (response.data.token === null && Object.keys(response.data).length === 1) {
logger.debug(`[API] Response contains only null token for ${vendor}, treating as not found`);
return null;
}
tokenData = response.data;
}
if (tokenData === null || tokenData && typeof tokenData === "object" && tokenData.token === null && Object.keys(tokenData).length === 1) {
logger.debug(`[API] Token data is null for ${vendor}`);
return null;
}
logger.debug(`[API] Vendor token for ${vendor} retrieved successfully`, {
tokenDataType: typeof tokenData,
tokenDataKeys: tokenData && typeof tokenData === "object" ? Object.keys(tokenData) : "not an object"
});
return tokenData;
} catch (error) {
if (error.response?.status === 404) {
logger.debug(`[API] No vendor token found for ${vendor}`);
return null;
}
logger.debug(`[API] [ERROR] Failed to get vendor token:`, error);
return null;
}
}
/**
* Fetches the list of organizations the user belongs to.
* Used for the interactive org picker on CLI startup.
*
* THROWS on failure (network / 5xx / auth). Previously this swallowed
* errors and returned `[]`, which conflated "the user has no orgs" with
* "we couldn't reach the server". The daemon path used that empty array to
* register the machine UNSCOPED, so a transient 5xx during startup
* orphaned the machine outside any org. Callers must now distinguish the
* two: a real empty list returns `[]`, an error rejects. Interactive
* callers (`auth`) already wrap this in try/catch and degrade gracefully;
* the daemon falls back to the persisted org or refuses to register
* unscoped on a fetch failure.
*/
async fetchOrgs() {
try {
const response = await axios.get(
`${configuration.serverUrl}/v1/orgs`,
{
headers: {
"Authorization": `Bearer ${this.credential.token}`,
"Content-Type": "application/json"
},
timeout: 1e4
}
);
return response.data ?? [];
} catch (error) {
const status = axios.isAxiosError(error) ? error.response?.status : void 0;
logger.debug(`[API] Failed to fetch orgs (status=${status ?? "n/a"}):`, error);
throw error;
}
}
}
var api = /*#__PURE__*/Object.freeze({
__proto__: null,
ApiClient: ApiClient,
AuthExpiredError: AuthExpiredError
});
const UsageSchema = z.z.object({
input_tokens: z.z.number().int().nonnegative(),
cache_creation_input_tokens: z.z.number().int().nonnegative().optional(),
cache_read_input_tokens: z.z.number().int().nonnegative().optional(),
output_tokens: z.z.number().int().nonnegative(),
service_tier: z.z.string().optional()
}).passthrough();
const RawJSONLinesSchema = z.z.discriminatedUnion("type", [
// User message - validates uuid and message.content
z.z.object({
type: z.z.literal("user"),
isSidechain: z.z.boolean().optional(),
isMeta: z.z.boolean().optional(),
uuid: z.z.string(),
// Used in getMessageKey()
message: z.z.object({
content: z.z.union([z.z.string(), z.z.any()])
// Used in sessionScanner.ts
}).passthrough()
}).passthrough(),
// Assistant message - only validates uuid and type
// message object is optional to handle synthetic error messages (isApiErrorMessage: true)
// which may have different structure than normal assistant messages
z.z.object({
uuid: z.z.string(),
type: z.z.literal("assistant"),
message: z.z.object({
usage: UsageSchema.optional(),
// Used in apiSession.ts
model: z.z.string().optional()
// Used for cost calculation
}).passthrough().optional()
}).passthrough(),
// Summary message - validates summary and leafUuid
z.z.object({
type: z.z.literal("summary"),
summary: z.z.string(),
// Used in apiSession.ts
leafUuid: z.z.string()
// Used in getMessageKey()
}).passthrough(),
// System message - validates uuid
z.z.object({
type: z.z.literal("system"),
uuid: z.z.string()
// Used in getMessageKey()
}).passthrough()
]);
exports.ApiClient = ApiClient;
exports.ApiSessionClient = ApiSessionClient;
exports.AsyncLock = AsyncLock;
exports.AuthExpiredError = AuthExpiredError;
exports.CLAUDE_LOOPBACK_PORT = CLAUDE_LOOPBACK_PORT;
exports.CLAUDE_OOB_REDIRECT_URI = CLAUDE_OOB_REDIRECT_URI;
exports.DEFAULT_GATEWAY_PORT = DEFAULT_GATEWAY_PORT;
exports.HOSTED_SESSION_TERMINAL_PREFIX = HOSTED_SESSION_TERMINAL_PREFIX;
exports.RawJSONLinesSchema = RawJSONLinesSchema;
exports.acquireDaemonLock = acquireDaemonLock;
exports.api = api;
exports.authChallenge = authChallenge;
exports.backoff = backoff;
exports.buildClaudeAuthorizeUrl = buildClaudeAuthorizeUrl;
exports.captureError = captureError;
exports.checkGatewayHealth = checkGatewayHealth;
exports.clearActiveOrgId = clearActiveOrgId;
exports.clearCredentials = clearCredentials;
exports.clearDaemonState = clearDaemonState;
exports.clearMachineId = clearMachineId;
exports.configuration = configuration;
exports.connectionState = connectionState;
exports.createUsageQueueSink = createUsageQueueSink;
exports.decodeBase64 = decodeBase64;
exports.decodeBase64$1 = decodeBase64$1;
exports.decrypt = decrypt;
exports.decryptBox = decryptBox;
exports.decryptDriveContent = decryptDriveContent;
exports.decryptDriveDek = decryptDriveDek;
exports.decryptDriveMetadata = decryptDriveMetadata;
exports.decryptWithDataKey = decryptWithDataKey;
exports.delay = delay;
exports.detectHardware = detectHardware;
exports.detectOpenClaw = detectOpenClaw;
exports.encodeBase64 = encodeBase64;
exports.encodeBase64$1 = encodeBase64$1;
exports.encodeBase64Url = encodeBase64Url;
exports.encrypt = encrypt;
exports.encryptBox = encryptBox;
exports.encryptDriveContent = encryptDriveContent;
exports.encryptDriveDek = encryptDriveDek;
exports.encryptDriveMetadata = encryptDriveMetadata;
exports.encryptWithDataKey = encryptWithDataKey;
exports.exchangeClaudeCode = exchangeClaudeCode;
exports.forceCleanupStaleLock = forceCleanupStaleLock;
exports.formatPosixSpawnpGuidance = formatPosixSpawnpGuidance;
exports.generateClaudePkce = generateClaudePkce;
exports.generateClaudeState = generateClaudeState;
exports.generateDriveDek = generateDriveDek;
exports.getAccountBundle = getAccountBundle;
exports.getActiveBrain = getActiveBrain;
exports.getActiveOrgId = getActiveOrgId;
exports.getAgentAuthCapability = getAgentAuthCapability;
exports.getBrain = getBrain;
exports.getCryptoEnv = getCryptoEnv;
exports.getLatestDaemonLog = getLatestDaemonLog;
exports.getProfileEnvironmentVariables = getProfileEnvironmentVariables;
exports.getProviderKey = getProviderKey;
exports.getTerminalManager = getTerminalManager;
exports.getUsageQueue = getUsageQueue;
exports.incrementCounter = incrementCounter;
exports.initObservability = initObservability;
exports.isAuthResetSignal = isAuthResetSignal;
exports.isPosixSpawnpError = isPosixSpawnpError;
exports.isSealedDekContent = isSealedDekContent;
exports.isSealedDekReadable = isSealedDekReadable;
exports.listAccounts = listAccounts;
exports.listBrains = listBrains;
exports.logger = logger;
exports.markAccountStatus = markAccountStatus;
exports.maskedTailFromBundle = maskedTailFromBundle;
exports.matchErrorSignature = matchErrorSignature;
exports.openSealedDekBytesSync = openSealedDekBytesSync;
exports.openSealedDekBytesWithDek = openSealedDekBytesWithDek;
exports.openSealedDekContentSync = openSealedDekContentSync;
exports.packageJson = packageJson;
exports.parsePastedClaudeCode = parsePastedClaudeCode;
exports.persistence = persistence;
exports.projectPath = projectPath;
exports.provisioningEvents = provisioningEvents;
exports.readCredentials = readCredentials;
exports.readDaemonState = readDaemonState;
exports.readDaemonStateSync = readDaemonStateSync;
exports.readSettings = readSettings;
exports.recordAccountUsageBatch = recordAccountUsageBatch;
exports.registerAccount = registerAccount;
exports.registerProviderKey = registerProviderKey;
exports.releaseDaemonLock = releaseDaemonLock;
exports.removeAccount = removeAccount;
exports.removeProviderKey = removeProviderKey;
exports.resolveAccountForProvider = resolveAccountForProvider;
exports.resolveClaudeConfigDir = resolveClaudeConfigDir;
exports.resolveTmuxBinary = resolveTmuxBinary;
exports.resolveZellijBinary = resolveZellijBinary;
exports.runStartupMigration = runStartupMigration;
exports.setActiveBrainKey = setActiveBrainKey;
exports.setActiveOrgId = setActiveOrgId;
exports.setBrainSeq = setBrainSeq;
exports.setProviderDefault = setProviderDefault;
exports.setProviderFailover = setProviderFailover;
exports.signChallenge = signChallenge;
exports.snapshotsFromCodexRateLimits = snapshotsFromCodexRateLimits;
exports.sodium = sodium;
exports.spawnDiagnostics = spawnDiagnostics;
exports.spawnPtyWithDiagnostics = spawnPtyWithDiagnostics;
exports.startOfflineReconnection = startOfflineReconnection;
exports.toMetadataFields = toMetadataFields;
exports.touchDaemonLock = touchDaemonLock;
exports.updateAccountBundle = updateAccountBundle;
exports.updateSettings = updateSettings;
exports.upsertBrain = upsertBrain;
exports.usageFromAgentTokenCount = usageFromAgentTokenCount;
exports.usageFromCodexLastTokenUsage = usageFromCodexLastTokenUsage;
exports.validateProfileForAgent = validateProfileForAgent;
exports.whichBinary = whichBinary;
exports.writeCredentialsDataKey = writeCredentialsDataKey;
exports.writeCredentialsLegacy = writeCredentialsLegacy;
exports.writeCredentialsTokenOnly = writeCredentialsTokenOnly;
exports.writeDaemonState = writeDaemonState;