UNPKG

@sellable/install

Version:

One-command installer for Sellable MCP in Claude Code, Codex, and Hermes

1,030 lines (1,012 loc) โ€ข 33.2 kB
import { execFile, spawn } from "node:child_process"; import { createHash, randomBytes, timingSafeEqual } from "node:crypto"; import { appendFileSync, chmodSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { createServer } from "node:http"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { endpointHermesSessionId, endpointMcpDescriptorPath, } from "../mcp-context-proxy.mjs"; import { normalizeRunsApiBaseUrl } from "./endpoint-model-selection.mjs"; import { validateOpenerOutputContract } from "./hermes-runs-client.mjs"; import { parseRunsApiConnections, RUNS_API_CONNECTIONS_ENV, } from "./runs-api-connections.mjs"; // Each pool runs as many generations at once as one Hermes endpoint may // (ENDPOINT_USER_MAX_CONCURRENCY); the runtime still caps the machine at 10. export const CLAUDE_RUNS_MAX_CONCURRENCY = 8; const MAX_BODY = 320 * 1024; const MAX_OUTPUT = 48 * 1024; const MAX_CLI_OUTPUT = 512 * 1024; const MCP_SESSION = fileURLToPath( new URL("./claude-code-mcp-session.mjs", import.meta.url) ); const hash = (s) => createHash("sha256").update(s).digest("hex"); const record = (v) => v && typeof v === "object" && !Array.isArray(v); // The public service (models null) has no model list: any Claude model, at // the efforts the CLI takes. const publicModel = (body) => typeof body.model === "string" && /^claude-[a-z0-9-]{1,100}$/.test(body.model) && ["low", "medium", "high"].includes(body.model_options.reasoning_effort); // The run's one remote MCP server, in the shape the Runs client sends it. function remoteTools(tools) { const entry = Array.isArray(tools) && tools.length === 1 ? tools[0] : null; const url = normalizeRunsApiBaseUrl(entry?.server_url); const names = entry?.allowed_tools; if ( !record(entry) || Object.keys(entry).sort().join(",") !== "allowed_tools,authorization,require_approval,server_label,server_url,type" || entry.type !== "mcp" || entry.server_label !== "sellable" || entry.require_approval !== "never" || !url?.startsWith("https://") || typeof entry.authorization !== "string" || !/^srt_[A-Za-z0-9-]{1,128}_[A-Za-z0-9_-]{43}$/.test(entry.authorization) || !Array.isArray(names) || names.length < 1 || names.length > 32 || new Set(names).size !== names.length || names.some( (name) => typeof name !== "string" || !/^[a-z][a-z0-9_]{0,63}$/.test(name) ) ) throw new Error("claude_runs_tools_rejected"); return { names: [...names], remote: { url, authorization: entry.authorization }, }; } export function validateClaudeRunsRequest(body, models) { const allowed = [ "input", "instructions", "conversation_history", "session_id", "provider", "model", "model_options", ...(models === null ? ["tools"] : []), ]; if ( !record(body) || Object.keys(body).some((k) => !allowed.includes(k)) || typeof body.input !== "string" || !body.input.length || Buffer.byteLength(body.input) > 128 * 1024 || typeof body.instructions !== "string" || !body.instructions.length || Buffer.byteLength(body.instructions) > 128 * 1024 || !Array.isArray(body.conversation_history) || body.conversation_history.length > 64 || Buffer.byteLength(JSON.stringify(body.conversation_history)) > 128 * 1024 || body.conversation_history.some( (item) => !record(item) || Object.keys(item).sort().join(",") !== "content,role" || !["user", "assistant"].includes(item.role) || typeof item.content !== "string" ) || !/^sellable_endpoint_[a-f0-9]{32}$/.test(body.session_id ?? "") || body.provider !== "claude-code" || !record(body.model_options) || Object.keys(body.model_options).some( (key) => !["reasoning_effort", "sellable_opener_output_contract"].includes(key) ) || !(models === null ? publicModel(body) : models.some( (model) => model.provider === body.provider && model.model === body.model && model.reasoningEfforts.includes(body.model_options.reasoning_effort) )) ) throw new Error("claude_runs_request_rejected"); const contract = body.model_options.sellable_opener_output_contract; const schema = contract === undefined ? null : validateOpenerOutputContract(contract).schema; return { input: body.conversation_history.length ? JSON.stringify({ conversation_history: body.conversation_history, input: body.input, }) : body.input, instructions: body.instructions, schema, model: body.model, effort: body.model_options.reasoning_effort, sessionId: body.session_id, tools: body.tools === undefined ? null : remoteTools(body.tools), }; } /** * A run's own allowed tools, read from the signed turn descriptor the runtime * installed for its session before submitting. With a tool list, only runs on * those endpoints get tools (and must have some); without one, every run gets * exactly the tools its descriptor allows, and a run allowed none is tool-free * (null). This only narrows: the context proxy verifies the descriptor itself * before it lists or calls any tool. */ export function endpointToolNames({ turnDescriptorPath, sessionId, toolEndpointIds = undefined, }) { const path = endpointMcpDescriptorPath(turnDescriptorPath, sessionId); if (!path) return null; try { if (!lstatSync(path).isFile()) throw new Error("claude_runs_tool_descriptor_rejected"); } catch (error) { if (error.code === "ENOENT") return null; throw error; } const body = JSON.parse(readFileSync(path, "utf8"))?.body; if (toolEndpointIds && !toolEndpointIds.includes(body?.endpointId)) return null; const names = body?.allowedToolNames; if ( endpointHermesSessionId(body) !== sessionId || !Array.isArray(names) || (toolEndpointIds && !names.length) || names.some( (name) => typeof name !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(name) ) ) throw new Error("claude_runs_tool_descriptor_rejected"); return names.length ? [...names] : null; } export function claudeInvocation( request, root, mcpSpecPath = null, configDir = `${root}/claude` ) { return { command: `${root}/cli/node_modules/.bin/claude`, args: [ "-p", "--model", request.model, "--effort", request.effort, "--output-format", "json", ...(request.schema ? ["--json-schema", JSON.stringify(request.schema)] : []), "--tools", "", "--permission-mode", "dontAsk", "--strict-mcp-config", "--mcp-config", // A tool lane's only MCP server is the run's own session wrapper, and a // public run's is its own remote server, read from its private file. // Either way its allowed tools are exactly the run's. Every other run // has none. ...(request.tools ? [ request.tools.remote ? request.mcpConfigPath : JSON.stringify({ mcpServers: { sellable: { command: process.execPath, args: [MCP_SESSION, mcpSpecPath], }, }, }), "--allowedTools", request.tools.names .map((name) => `mcp__sellable__${name}`) .join(","), ] : ['{"mcpServers":{}}']), "--setting-sources", "", "--settings", '{"disableAllHooks":true,"autoMemoryEnabled":false}', "--disable-slash-commands", "--no-session-persistence", "--max-turns", request.tools ? "16" : "3", "--system-prompt", request.instructions, ], // Whitelist instead of inheriting the runtime's provider keys, URLs, hooks, // plugin paths, NODE_OPTIONS, or workspace access credentials. options: { cwd: `${root}/work`, detached: true, stdio: ["pipe", "pipe", "pipe"], env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: configDir, CLAUDE_CONFIG_DIR: configDir, LANG: "C.UTF-8", CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1", }, }, }; } // Uses the same isolated environment as generation. Only a typed status leaves // this boundary; CLI stdout can contain account identity and is never forwarded. export function readClaudeAuthentication({ root, configDir = `${root}/claude`, execFileImpl = execFile, timeoutMs = 8_000, } = {}) { const invocation = claudeInvocation( { instructions: "", input: "", model: "claude-opus", effort: "high" }, root, null, configDir ); return new Promise((resolve) => { execFileImpl( invocation.command, ["auth", "status"], { cwd: invocation.options.cwd, env: invocation.options.env, timeout: timeoutMs, maxBuffer: 16 * 1024, encoding: "utf8", }, (error, stdout) => { try { const value = JSON.parse(stdout); if (value.loggedIn === false) return resolve("SIGN_IN_REQUIRED"); if (!error && value.loggedIn === true) return resolve("READY"); } catch { /* A missing or incompatible CLI is unavailable, never ready. */ } resolve("UNAVAILABLE"); } ); }); } // Each sign-in is its own Claude config folder: the primary `claude`, then // `accounts/<name>` in name order. Refresh tokens rotate inside a folder, so a // login is never copied between folders. export function claudeAccountDirs(root) { const dirs = [`${root}/claude`]; let names = []; try { names = readdirSync(`${root}/accounts`).sort(); } catch { /* No extra accounts. */ } for (const name of names) if ( /^[a-z0-9][a-z0-9-]{0,62}$/.test(name) && existsSync(`${root}/accounts/${name}/.credentials.json`) ) dirs.push(`${root}/accounts/${name}`); return dirs; } const MONTHS = "janfebmaraprmayjunjulaugsepoctnovdec"; const USAGE_LIMIT_RETRY_MS = 15 * 60_000; const SIGN_IN_TRUST_MS = 5 * 60_000; // Claude Code reports a used-up plan as an API 429 result, for example // "You've hit your weekly limit ยท resets Oct 7, 10am (UTC)". Returns when that // account can be tried again, or null for any other result. export function claudeUsageLimit(stdout, now = Date.now()) { let value; try { value = JSON.parse(stdout); } catch { return null; } if ( !record(value) || value.is_error !== true || value.api_error_status !== 429 ) return null; const reset = /resets\s+(?:([a-z]{3})[a-z]*\s+(\d{1,2}),?\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(UTC\)/i.exec( String(value.result ?? "") ); let retryAt = null; if (reset) { const hour = (Number(reset[3]) % 12) + (reset[5].toLowerCase() === "pm" ? 12 : 0); const minute = Number(reset[4] ?? 0); const today = new Date(now); if (reset[1]) { const month = MONTHS.indexOf(reset[1].toLowerCase()) / 3; if (Number.isInteger(month)) { retryAt = Date.UTC( today.getUTCFullYear(), month, Number(reset[2]), hour, minute ); if (retryAt < now - 86_400_000) retryAt = Date.UTC( today.getUTCFullYear() + 1, month, Number(reset[2]), hour, minute ); } } else { retryAt = Date.UTC( today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate(), hour, minute ); if (retryAt <= now) retryAt += 86_400_000; } } if (retryAt === null || retryAt <= now || retryAt > now + 8 * 86_400_000) retryAt = now + USAGE_LIMIT_RETRY_MS; return { retryAt }; } // A failure reason the run may report: a fixed code, never raw CLI text. const RUN_FAILURE_CODE = /^claude_runs_[a-z_]{1,48}$/; export function parseClaudeResult(stdout, request) { const result = JSON.parse(stdout); const models = Object.keys(result.modelUsage ?? {}); if ( result.type !== "result" || result.is_error !== false || result.subtype !== "success" || (request.schema ? !record(result.structured_output) : typeof result.result !== "string") ) throw new Error("claude_runs_output_rejected"); if ((result.permission_denials ?? []).length) throw new Error("claude_runs_permission_denied"); if ( !models.length || models.some( (model) => model !== request.model && !model.startsWith(`${request.model}-`) ) ) throw new Error("claude_runs_model_mismatch"); const output = request.schema ? JSON.stringify(result.structured_output) : result.result; if (!output.trim()) throw new Error("claude_runs_output_empty"); if (Buffer.byteLength(output) > MAX_OUTPUT) throw new Error("claude_runs_output_too_large"); return { output, runtime: { provider: "claude-code", model: models[0] }, usage: { input_tokens: result.usage?.input_tokens ?? 0, output_tokens: result.usage?.output_tokens ?? 0, cache_read_tokens: result.usage?.cache_read_input_tokens ?? 0, cache_write_tokens: result.usage?.cache_creation_input_tokens ?? 0, }, }; } export function startClaudeRun( request, { root, configDir = `${root}/claude`, spawnImpl = spawn, timeoutMs = 750_000, } = {} ) { // The tool server's environment travels in a private per-run file, never // in argv, and is removed when the run ends. const mcpSpecPath = request.tools?.server ? join(root, "work", `mcp-session-${randomBytes(12).toString("hex")}.json`) : null; if (mcpSpecPath) writeFileSync( mcpSpecPath, JSON.stringify({ sessionId: request.sessionId, ...request.tools.server }), { mode: 0o600, flag: "wx" } ); const removeSpec = () => { if (mcpSpecPath) rmSync(mcpSpecPath, { force: true }); }; const invocation = claudeInvocation(request, root, mcpSpecPath, configDir); let child; try { child = spawnImpl(invocation.command, invocation.args, invocation.options); } catch (error) { removeSpec(); throw error; } let reason = null; let killTimer; const stop = (code = "claude_runs_cancelled") => { reason ??= code; try { process.kill(-child.pid, "SIGTERM"); } catch { child.kill("SIGTERM"); } killTimer ??= setTimeout(() => { try { process.kill(-child.pid, "SIGKILL"); } catch { child.kill("SIGKILL"); } }, 5_000); killTimer.unref(); }; const promise = new Promise((resolve, reject) => { const chunks = []; let bytes = 0; const timer = setTimeout(() => stop("claude_runs_timeout"), timeoutMs); child.stdout.on("data", (chunk) => { bytes += chunk.length; if (bytes > MAX_CLI_OUTPUT) stop("claude_runs_output_too_large"); else chunks.push(chunk); }); // Drain diagnostics without logging prompts, credentials, or message bodies. child.stderr.on("data", () => {}); child.once("error", () => { clearTimeout(timer); removeSpec(); reject(new Error("claude_runs_cli_unavailable")); }); child.once("close", (code) => { clearTimeout(timer); clearTimeout(killTimer); removeSpec(); const stdout = Buffer.concat(chunks).toString("utf8"); const limit = reason ? null : claudeUsageLimit(stdout); if (limit) return reject( Object.assign(new Error("claude_runs_usage_limited"), limit) ); if (reason || code !== 0) return reject(new Error(reason ?? "claude_runs_cli_failed")); try { resolve(parseClaudeResult(stdout, request)); } catch (error) { reject( error instanceof Error && RUN_FAILURE_CODE.test(error.message) ? error : new Error("claude_runs_output_rejected") ); } }); child.stdin.on("error", () => {}); child.stdin.end(request.input); }); return { promise, stop }; } // A public run's remote server and grant, in a 0600 file under a 0700 // directory of its own: never in argv or the CLI's environment. function writeRemoteMcpConfig(dir, remote) { mkdirSync(dir, { recursive: true, mode: 0o700 }); chmodSync(dir, 0o700); const stat = lstatSync(dir); if (stat.isSymbolicLink() || !stat.isDirectory() || (stat.mode & 0o077) !== 0) throw new Error("claude_runs_run_directory_rejected"); const path = join(dir, "mcp.json"); writeFileSync( path, JSON.stringify({ mcpServers: { sellable: { type: "http", url: remote.url, headers: { Authorization: `Bearer ${remote.authorization}` }, }, }, }), { mode: 0o600, flag: "wx" } ); return path; } export function createClaudeRunsApi({ token, connection, // "public": the Admin's internet-facing listener. The bearer is its only // scope, and a run's tools come from its own request, never a local lane. mode = "loopback", root = connection?.localRoot, execute = null, audit = () => {}, maxConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY, // Tool lanes (replies, follow-ups) have their own slots so a batch of // openers can never crowd them out. maxToolConcurrency = CLAUDE_RUNS_MAX_CONCURRENCY, toolLanes = null, checkAuthentication = (configDir) => readClaudeAuthentication({ root, configDir }), accounts = () => claudeAccountDirs(root), startRun = startClaudeRun, now = Date.now, } = {}) { if (!/^[a-f0-9]{64}$/.test(token ?? "")) throw new Error("claude_runs_key_rejected"); // An account whose plan is used up rests until its reset; runs go to the // next signed-in account meanwhile, and back to the first once it resets. const usageLimitedUntil = new Map(); const resting = (configDir) => (usageLimitedUntil.get(configDir) ?? 0) > now(); // `claude auth status` starts a whole CLI: on the 4-CPU Admin machine seven // at once take longer than its 8s timeout. Runs that arrive together share // one check, a good answer is trusted for five minutes, and only a definite // SIGN_IN_REQUIRED keeps a run off an account; an unanswered check runs and // lets the CLI itself fail the run if the sign-in is really gone. const signedInAt = new Map(); const signInChecks = new Map(); const signInState = (configDir) => { if (now() - (signedInAt.get(configDir) ?? -Infinity) < SIGN_IN_TRUST_MS) return Promise.resolve("READY"); let check = signInChecks.get(configDir); if (!check) { check = Promise.resolve() .then(() => checkAuthentication(configDir)) .catch(() => "UNAVAILABLE") .then((state) => { signInChecks.delete(configDir); if (state === "READY") signedInAt.set(configDir, now()); else signedInAt.delete(configDir); return state; }); signInChecks.set(configDir, check); } return check; }; const runOnAccounts = (request) => { let activeRun; let stopped = false; return { promise: (async () => { let failure = null; for (const configDir of accounts()) { if (resting(configDir)) { failure = "claude_runs_usage_limited"; continue; } const state = await signInState(configDir); if (stopped) throw new Error("claude_runs_cancelled"); if (state === "SIGN_IN_REQUIRED") { failure ??= "claude_runs_sign_in_required"; continue; } activeRun = startRun(request, { root, configDir }); try { return await activeRun.promise; } catch (error) { if (stopped || error.message !== "claude_runs_usage_limited") { signedInAt.delete(configDir); throw error; } usageLimitedUntil.set(configDir, error.retryAt); failure = "claude_runs_usage_limited"; } } throw new Error(failure ?? "claude_runs_sign_in_required"); })(), stop() { stopped = true; activeRun?.stop(); }, }; }; const executeRun = execute ?? runOnAccounts; const readHealth = async () => { const dirs = accounts(); let ready = 0; let signedOut = 0; const retryAts = []; for (const configDir of dirs) { if (resting(configDir)) { retryAts.push(usageLimitedUntil.get(configDir)); continue; } const state = await signInState(configDir); if (state === "READY") ready++; if (state === "SIGN_IN_REQUIRED") signedOut++; } return { authentication: ready ? "READY" : signedOut === dirs.length ? "SIGN_IN_REQUIRED" : "UNAVAILABLE", accounts: { total: dirs.length, ready, usageLimited: retryAts.length, retryAt: retryAts.length ? new Date(Math.min(...retryAts)).toISOString() : null, }, }; }; // The run's own tools and the profile's MCP server, or null for a tool-free run. const resolveTools = (request) => { if (!toolLanes) return null; const names = endpointToolNames({ turnDescriptorPath: toolLanes.turnDescriptorPath, sessionId: request.sessionId, toolEndpointIds: connection.toolEndpointIds, }); if (!names) return null; const server = toolLanes.server(); if (!server) throw new Error("claude_runs_tool_server_unavailable"); return { names, server: { command: server.command, args: [...server.args], env: { PATH: "/usr/local/bin:/usr/bin:/bin", ...server.env }, }, }; }; const recordAudit = (entry) => { try { audit(entry); } catch { /* Logging cannot crash the Admin runtime. */ } }; const runs = new Map(); const inFlight = { plain: 0, tools: 0 }; const limits = { plain: maxConcurrency, tools: maxToolConcurrency }; const full = (pool) => inFlight[pool] >= limits[pool] || runs.size >= 200; const json = (res, status, body) => { res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store", "retry-after": "2", }); res.end(JSON.stringify(body)); }; const terminal = (run, event, fields) => { Object.assign(run, fields); run.event = { event, run_id: run.run_id, timestamp: new Date().toISOString(), ...(run.status === "completed" ? { output: run.output, runtime: run.runtime, usage: run.usage } : run.status === "failed" ? { error: run.error } : {}), }; for (const res of run.listeners) res.end(`data: ${JSON.stringify(run.event)}\n\n`); run.listeners.clear(); recordAudit({ runId: run.run_id, status: run.status, toolLane: run.pool === "tools", servedRuntime: run.runtime ?? null, usage: run.usage ?? null, durationMs: Date.now() - run.startedAt, outputDigest: run.output ? hash(run.output) : null, }); setTimeout(() => runs.delete(run.run_id), 3_600_000).unref(); }; const server = createServer(async (req, res) => { const provided = Buffer.from(req.headers.authorization ?? ""); const expected = Buffer.from(`Bearer ${token}`); if ( provided.length !== expected.length || !timingSafeEqual(provided, expected) ) return json(res, 401, { error: "claude_runs_auth_required" }); if ( mode !== "public" && (req.headers["x-sellable-workspace-id"] !== connection.workspaceId || req.headers["x-sellable-agent-id"] !== connection.agentId) ) return json(res, 403, { error: "claude_runs_scope_rejected" }); if (req.method === "GET" && req.url === "/health") { const { authentication, accounts: accountHealth } = await readHealth(); // Public health names no accounts, load, or scope. if (mode === "public") return json(res, 200, { ok: authentication === "READY", authentication, provider: "claude-code", tools: ["mcp"], }); return json(res, 200, { ok: authentication === "READY", authentication, accounts: accountHealth, provider: "claude-code", active: inFlight.plain + inFlight.tools, toolActive: inFlight.tools, scope: { workspaceId: connection.workspaceId, agentId: connection.agentId, }, }); } if (req.method === "POST" && req.url === "/v1/runs") { if (full("plain") && ((mode !== "public" && !toolLanes) || full("tools"))) return json(res, 429, { error: "claude_runs_busy" }); let request; try { let size = 0; const chunks = []; for await (const chunk of req) { size += chunk.length; if (size > MAX_BODY) { json(res, 413, { error: "claude_runs_input_too_large" }); return; } chunks.push(chunk); } const body = JSON.parse(Buffer.concat(chunks).toString("utf8")); request = validateClaudeRunsRequest( body, mode === "public" ? null : connection.models ); // A public run's tools are its own entry's, never a local lane's. if (mode !== "public") request.tools = resolveTools(request); } catch (error) { recordAudit({ status: "request-rejected", reason: /^claude_runs_[a-z_]+$/.test(error.message ?? "") ? error.message : "claude_runs_invalid_request", }); return json(res, 400, { error: "claude_runs_request_rejected" }); } // Check again after reading: concurrent uploads cannot over-admit. const pool = request.tools ? "tools" : "plain"; if (full(pool)) return json(res, 429, { error: "claude_runs_busy" }); const run = { run_id: `run_${randomBytes(16).toString("hex")}`, status: "running", startedAt: Date.now(), listeners: new Set(), consumed: false, pool, }; inFlight[pool]++; runs.set(run.run_id, run); const runDir = request.tools?.remote ? join(root, "work", "runs", run.run_id) : null; const removeRunDir = () => { if (runDir) rmSync(runDir, { recursive: true, force: true }); }; try { if (runDir) request.mcpConfigPath = writeRemoteMcpConfig( runDir, request.tools.remote ); run.execution = executeRun(request); run.execution.promise .then( (result) => terminal(run, "run.completed", { status: "completed", ...result, }), (error) => terminal( run, run.stopping && error.message === "claude_runs_cancelled" ? "run.cancelled" : "run.failed", run.stopping && error.message === "claude_runs_cancelled" ? { status: "cancelled" } : { status: "failed", // The reason, so a failure can be told apart later; // anything else the CLI threw stays generic. error: RUN_FAILURE_CODE.test(error?.message ?? "") ? error.message : "claude_runs_generation_failed", } ) ) .finally(() => { inFlight[pool]--; removeRunDir(); }); } catch { inFlight[pool]--; removeRunDir(); terminal(run, "run.failed", { status: "failed", error: "claude_runs_cli_unavailable", }); } return json(res, 202, { run_id: run.run_id, status: "started" }); } const match = /^\/v1\/runs\/(run_[a-f0-9]{32})(?:\/(events|stop|approval))?$/.exec( req.url ?? "" ); const run = match && runs.get(match[1]); if (!run) return json(res, 404, { error: "claude_runs_run_missing" }); if (req.method === "GET" && !match[2]) return json(res, 200, { run_id: run.run_id, status: run.status, ...(run.output ? { output: run.output, runtime: run.runtime, usage: run.usage } : {}), ...(run.error ? { error: run.error } : {}), }); if (req.method === "GET" && match[2] === "events") { if (run.consumed) return json(res, 404, { error: "claude_runs_events_consumed" }); run.consumed = true; res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", }); res.flushHeaders(); if (run.event) return res.end(`data: ${JSON.stringify(run.event)}\n\n`); run.listeners.add(res); const heartbeat = setInterval( () => res.write(": keep-alive\n\n"), 15_000 ); res.once("close", () => { clearInterval(heartbeat); run.listeners.delete(res); }); return; } if (req.method === "POST" && match[2] === "stop") { if (run.event) return json(res, 200, { run_id: run.run_id, status: run.status, ...(run.output ? { output: run.output, runtime: run.runtime, usage: run.usage } : {}), }); run.stopping = true; run.execution.stop(); return json(res, 200, { run_id: run.run_id, status: "stopping" }); } return json(res, 405, { error: "claude_runs_method_rejected" }); }); server.stopActiveRuns = async () => { const activeRuns = [...runs.values()].filter( (run) => !run.event && run.execution ); for (const run of activeRuns) { run.stopping = true; run.execution.stop(); } await Promise.allSettled(activeRuns.map((run) => run.execution.promise)); }; return server; } const readKey = (path) => readFileSync(path, "utf8").trim(); // Every listener audits to its own root and stops its runs before closing. const auditTo = (root) => (entry) => appendFileSync( `${root}/audit.jsonl`, `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`, { mode: 0o600 } ); const listen = (server, port, host) => new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, host, resolve); }); async function stopServer(server) { await server.stopActiveRuns(); await new Promise((resolve) => { server.close(resolve); server.closeAllConnections(); }); } // The Admin runtime owns these listeners for its lifetime. Image-owned startup // plus the persistent CLI/auth directory makes normal restarts repeatable. export async function startConfiguredClaudeRunsApis({ env = process.env, diagnostic = () => {}, createApi = createClaudeRunsApi, readToken = readKey, toolLanes = null, } = {}) { const services = []; for (const connection of parseRunsApiConnections( env[RUNS_API_CONNECTIONS_ENV] )) { if ( !connection.localRoot || connection.runtimeId !== env.SELLABLE_RUNTIME_ID ) continue; try { const token = readToken(connection.apiKeyFile); const server = createApi({ token, connection, toolLanes, audit: auditTo(connection.localRoot), }); await listen( server, Number(new URL(connection.baseUrl).port), "127.0.0.1" ); services.push(server); diagnostic({ ok: true, code: "claude_runs_api_ready", baseUrl: connection.baseUrl, }); } catch { diagnostic({ ok: false, code: "claude_runs_api_unavailable" }); } } return { async stop() { for (const server of services) await stopServer(server); }, }; } // The Admin's internet-facing listener; Fly terminates TLS at the edge. It // starts only on a declared port with the machine's private connection key. export async function startPublicClaudeRunsApi({ env = process.env, diagnostic = () => {}, root = "/opt/data/private-opener", createApi = createClaudeRunsApi, readToken = readKey, } = {}) { const value = env.SELLABLE_AGENT_PUBLIC_RUNS_SERVICE_PORT ?? ""; const port = Number(value); const keyFile = `${root}/api-key`; let key = null; try { key = lstatSync(keyFile); } catch { /* No key, no public listener. */ } if ( !/^[0-9]{4,5}$/.test(value) || port < 1024 || port > 65535 || // lstat: a symlink is not a file. !key?.isFile() || (key.mode & 0o077) !== 0 ) return null; const server = createApi({ mode: "public", token: readToken(keyFile), root, audit: auditTo(root), }); await listen(server, port, "0.0.0.0"); diagnostic({ ok: true, code: "claude_runs_public_api_ready", port }); return { stop: () => stopServer(server) }; }