import { randomUUID, X509Certificate } from "node:crypto"; import { spawnSync } from "node:child_process"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises"; import net from "node:net"; import os from "node:os"; import path from "node:path"; import tls from "node:tls"; import { fileURLToPath } from "node:url"; import { pathToFileURL } from "node:url"; import { desktopBootstrapPath, normalizeWorkspaceRootPath, openworkEnvStorePath, openworkServerConfigPath, resolveWorkspaceOpencodeConfigPath, } from "@openwork/paths"; import { dedupeCertificates, resolveSystemCaBundle, summarizeSystemCaSources, systemPlatformCertificateLoader, } from "./system-ca.mjs"; const __runtimeDir = path.dirname(fileURLToPath(import.meta.url)); const DIRECT_RUNTIME = "direct"; const OPENWORK_SERVER_PORT_RANGE_START = 48_000; const OPENWORK_SERVER_PORT_RANGE_END = 51_000; const MAX_BOOTSTRAP_BYTES = 256 * 1024; const MAX_CHAIN_REPAIR_BODY_BYTES = 64 * 1024; const MAX_CHAIN_REPAIR_ORIGINS = 3; const CHAIN_REPAIR_TOTAL_TIMEOUT_MS = 20000; const CHAIN_REPAIR_SOCKET_TIMEOUT_MS = 8000; const CHAIN_REPAIR_FETCH_TIMEOUT_MS = 8000; const CERTIFICATE_VERIFY_USE_CHROMIUM = -3; const CERTIFICATE_VERIFY_OK = 0; const ERR_CERT_AUTHORITY_INVALID = -202; const MAX_CERTIFICATE_CHAIN_LENGTH = 10; const PEM_CERTIFICATE_PATTERN = /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g; /** @type {Map} */ const chainRepairRootCache = new Map(); /** @param {unknown} value */ function parsePemCertificates(value) { return String(value ?? "").match(PEM_CERTIFICATE_PATTERN) ?? []; } /** @param {string} value */ function parseCertificate(value) { try { return new X509Certificate(value); } catch { return null; } } /** @param {X509Certificate} certificate */ function certificateIsCurrent(certificate, now = Date.now()) { const validFrom = Date.parse(certificate.validFrom); const validTo = Date.parse(certificate.validTo); return Number.isFinite(validFrom) && Number.isFinite(validTo) && validFrom <= now && now <= validTo; } /** @param {X509Certificate} certificate @param {string | undefined} hostname */ function certificateMatchesHostname(certificate, hostname) { const value = String(hostname ?? "").trim(); if (!value) return false; try { return net.isIP(value) ? certificate.checkIP(value) !== undefined : certificate.checkHost(value) !== undefined; } catch { return false; } } /** @param {X509Certificate} certificate */ function certificateAllowsTlsServerAuthentication(certificate) { return certificate.keyUsage?.some((usage) => usage === "1.3.6.1.5.5.7.3.1" || /server ?auth/i.test(usage) ) === true; } /** * @typedef {Object} ElectronCertificate * @property {string} data * @property {ElectronCertificate} [issuerCert] */ /** @param {ElectronCertificate | undefined} certificate */ function presentedCertificateChain(certificate) { const chain = []; const seen = new Set(); let current = certificate; while (current) { if (chain.length >= MAX_CERTIFICATE_CHAIN_LENGTH || typeof current.data !== "string") return null; const parsed = parseCertificate(current.data); if (!parsed) return null; const key = parsed.raw.toString("base64"); if (seen.has(key)) return null; seen.add(key); chain.push(parsed); current = current.issuerCert; } return chain; } /** @param {ElectronCertificate | undefined} certificate @param {string | undefined} hostname @param {X509Certificate[]} trustedAnchors */ function chainEndsAtTrustedAnchor(certificate, hostname, trustedAnchors) { const chain = presentedCertificateChain(certificate); if (!chain || chain.length === 0 || chain.some((entry) => !certificateIsCurrent(entry))) return false; if (!certificateAllowsTlsServerAuthentication(chain[0])) return false; if (!certificateMatchesHostname(chain[0], hostname)) return false; for (let index = 0; index < chain.length - 1; index += 1) { if (chain[index + 1].ca !== true) return false; if (!chain[index].checkIssued(chain[index + 1]) || !chain[index].verify(chain[index + 1].publicKey)) return false; } const terminal = chain.at(-1); if (!terminal) return false; return trustedAnchors.some((anchor) => anchor.ca === true && terminal.checkIssued(anchor) && terminal.verify(anchor.publicKey) ); } /** * @param {string[]} trustedCertificates * @returns {(request: { verificationResult?: string, errorCode?: number, hostname?: string, certificate?: ElectronCertificate }, callback: (result: number) => void) => void} */ export function createSystemCaCertificateVerifyProc(trustedCertificates) { const trustedAnchors = trustedCertificates.map(parseCertificate).filter((certificate) => certificate !== null); return (request, callback) => { const authorityInvalid = request.verificationResult ? request.verificationResult === "net::ERR_CERT_AUTHORITY_INVALID" && (request.errorCode === undefined || request.errorCode === ERR_CERT_AUTHORITY_INVALID) : request.errorCode === ERR_CERT_AUTHORITY_INVALID; if (authorityInvalid && chainEndsAtTrustedAnchor(request.certificate, request.hostname, trustedAnchors)) { callback(CERTIFICATE_VERIFY_OK); return; } callback(CERTIFICATE_VERIFY_USE_CHROMIUM); }; } function truncateOutput(value, limit = 8000) { const text = String(value ?? ""); return text.length <= limit ? text : text.slice(text.length - limit); } function appendOutput(state, key, chunk) { const next = `${state[key] ?? ""}${String(chunk ?? "")}`; state[key] = truncateOutput(next); } function normalizeWorkspaceKey(value, platform = process.platform) { try { const normalized = normalizeWorkspaceRootPath(value, { platform }); if (!normalized) return ""; const paths = platform === "win32" ? path.win32 : path; return paths.resolve(normalized).replace(/\\/g, "/").toLowerCase(); } catch { return ""; } } function normalizeServerCredentials(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; const clientToken = typeof value.clientToken === "string" && value.clientToken.trim() ? value.clientToken : null; const hostToken = typeof value.hostToken === "string" && value.hostToken.trim() ? value.hostToken : null; if (!clientToken || !hostToken) return null; return { clientToken, hostToken, ownerToken: typeof value.ownerToken === "string" && value.ownerToken.trim() ? value.ownerToken : null, updatedAt: typeof value.updatedAt === "number" && Number.isFinite(value.updatedAt) ? value.updatedAt : 0, }; } export function migrateOpenworkServerTokenStore(value) { const source = value && typeof value === "object" && !Array.isArray(value) ? value : {}; const sourceWorkspaces = source.workspaces && typeof source.workspaces === "object" && !Array.isArray(source.workspaces) ? source.workspaces : {}; const workspaceEntries = Object.entries(sourceWorkspaces); const legacyCredentials = workspaceEntries .flatMap(([workspaceKey, entry]) => { const credentials = normalizeServerCredentials(entry); return credentials ? [{ workspaceKey, credentials }] : []; }) .sort((left, right) => { const updatedAtDifference = right.credentials.updatedAt - left.credentials.updatedAt; if (updatedAtDifference !== 0) return updatedAtDifference; return left.workspaceKey < right.workspaceKey ? -1 : left.workspaceKey > right.workspaceKey ? 1 : 0; })[0]?.credentials; const credentials = normalizeServerCredentials(source.credentials) ?? legacyCredentials ?? { clientToken: randomUUID(), hostToken: randomUUID(), ownerToken: null, updatedAt: nowMs(), }; return { version: 2, credentials }; } export function prioritizeWorkspacePaths(preferredPath, workspacePaths = [], options = {}) { const platform = options.platform ?? process.platform; const paths = []; const seen = new Set(); const add = (value) => { let workspacePath; try { workspacePath = normalizeWorkspaceRootPath(value, { platform }); } catch { return; } const key = normalizeWorkspaceKey(workspacePath, platform); if (!workspacePath || !key || seen.has(key)) return; paths.push(workspacePath); seen.add(key); }; add(preferredPath); for (const workspacePath of workspacePaths) add(workspacePath); return paths; } function workspaceInaccessibleError(workspacePath, cause) { if (cause && typeof cause === "object" && cause.code === "workspace_inaccessible") return cause; const error = new Error(`Workspace path is not accessible: ${workspacePath}`, { cause }); Object.defineProperties(error, { code: { value: "workspace_inaccessible", enumerable: true }, workspacePath: { value: workspacePath, enumerable: true }, }); return error; } export async function prepareRuntimeWorkspaceRoot(projectDir, options = {}) { const rawProjectDir = String(projectDir ?? "").trim(); try { const workspaceRoot = normalizeWorkspaceRootPath(rawProjectDir, { platform: options.platform ?? process.platform, }); if (!workspaceRoot) throw new Error("projectDir is required"); await (options.mkdirImpl ?? mkdir)(workspaceRoot, { recursive: true }); if (typeof options.ensureConfig === "function") { await options.ensureConfig(workspaceRoot); } return workspaceRoot; } catch (error) { throw workspaceInaccessibleError(rawProjectDir, error); } } export function resolveOpenworkServerConfigPath(env = process.env) { return openworkServerConfigPath({ env }); } export function seedWorkspacePathsForEmbeddedServer(workspacePaths, serverConfigExists) { return serverConfigExists ? [] : workspacePaths; } export function selectStickyOpenworkPortWorkspace(requestedWorkspacePaths = [], serverWorkspacePaths = []) { for (const value of [...requestedWorkspacePaths, ...serverWorkspacePaths]) { const workspacePath = String(value ?? "").trim(); if (workspacePath) return workspacePath; } return ""; } export function resolveEvalLocalServerDelayMs(env = process.env) { const delayMs = Number(env.OPENWORK_EVAL_LOCAL_SERVER_DELAY_MS); return Number.isFinite(delayMs) && delayMs > 0 ? delayMs : 0; } export function reconcileInjectedUserEnv({ processEnv, inheritedEnv, userEnv, previouslyInjectedKeys = new Set(), }) { for (const key of previouslyInjectedKeys) { if (Object.prototype.hasOwnProperty.call(userEnv, key)) continue; if (Object.prototype.hasOwnProperty.call(inheritedEnv, key)) { processEnv[key] = inheritedEnv[key]; } else { delete processEnv[key]; } } for (const [key, value] of Object.entries(userEnv)) { if (Object.prototype.hasOwnProperty.call(inheritedEnv, key)) continue; processEnv[key] = value; } return new Set( Object.keys(userEnv).filter( (key) => !Object.prototype.hasOwnProperty.call(inheritedEnv, key), ), ); } export function commandMatchesPackagedSidecar(command, sidecarDirs = []) { const value = String(command ?? ""); if (!sidecarDirs.some((dir) => String(dir ?? "").trim() && value.includes(dir))) { return false; } return /(?:^|[/\\])opencode[^/\\\s]*\s+serve\b/.test(value); } export function embeddedServerImportUrl(embeddedPath) { const url = pathToFileURL(embeddedPath); try { const stats = statSync(embeddedPath); url.searchParams.set("mtimeMs", String(stats.mtimeMs)); url.searchParams.set("size", String(stats.size)); } catch { // Fall back to the deterministic file URL if stat fails; startup can continue. } return url.href; } function nowMs() { return Date.now(); } function createEngineState() { return { child: null, childExited: true, runtime: DIRECT_RUNTIME, projectDir: null, hostname: null, port: null, baseUrl: null, opencodeUsername: null, opencodePassword: null, opencodeBinPath: null, opencodeBinSource: null, managedByServer: false, managedPid: null, managedIsAlive: null, lastStdout: null, lastStderr: null, execution: null, }; } export function snapshotEngineState(state) { const child = state.childExited ? null : state.child; let managedRunning = false; if (state.managedByServer && typeof state.managedIsAlive === "function") { try { managedRunning = state.managedIsAlive() === true; } catch { managedRunning = false; } } const childRunning = Boolean(child && child.exitCode === null && !child.killed); return { running: managedRunning || childRunning, runtime: state.runtime, managedByServer: state.managedByServer === true, baseUrl: state.baseUrl, projectDir: state.projectDir, hostname: state.hostname, port: state.port, opencodeUsername: state.opencodeUsername, opencodePassword: state.opencodePassword, opencodeBinPath: state.opencodeBinPath, opencodeBinSource: state.opencodeBinSource, pid: state.managedByServer ? state.managedPid ?? null : child?.pid ?? null, lastStdout: state.lastStdout, lastStderr: state.lastStderr, execution: state.execution, }; } function createOpenworkServerState() { return { child: null, childExited: true, inProcess: false, // Monotonic per-start identity assigned by startOpenworkServerInner. // Sticky ports and persisted tokens make the connection details identical // across restarts, so clients need this to observe a new server lifetime. generation: null, remoteAccessEnabled: false, host: null, port: null, baseUrl: null, connectUrl: null, mdnsUrl: null, lanUrl: null, clientToken: null, ownerToken: null, hostToken: null, managedOpencodeBinPath: null, managedOpencodeBinSource: null, lastStdout: null, lastStderr: null, managedOpencodeExecution: null, }; } export function snapshotOpenworkServerState(state) { const child = state.childExited ? null : state.child; const running = state.inProcess || Boolean(child && child.exitCode === null && !child.killed); return { running, generation: typeof state.generation === "number" ? state.generation : null, remoteAccessEnabled: state.remoteAccessEnabled, host: state.host, port: state.port, baseUrl: state.baseUrl, connectUrl: state.connectUrl, mdnsUrl: state.mdnsUrl, lanUrl: state.lanUrl, clientToken: state.clientToken, ownerToken: state.ownerToken, hostToken: state.hostToken, managedOpencodeBinPath: state.managedOpencodeBinPath, managedOpencodeBinSource: state.managedOpencodeBinSource, pid: child?.pid ?? null, lastStdout: state.lastStdout, lastStderr: state.lastStderr, managedOpencodeExecution: state.managedOpencodeExecution, }; } /** * Decide whether an engineStart request keeps the running embedded server. * * The engine is multi-instance: it boots a per-directory instance on demand, * so a request for a different workspace retargets the running runtime * instead of restarting it. A restart here would abort every in-flight run, * including sessions still working in the workspace being left. Only an * explicit forceRestart or a host rebind (remote access change) gives up the * running server. */ export function resolveOpenworkServerReuse({ forceRestart, inProcess, lifecycleState, remoteAccessEnabled, requestedRemoteAccess, currentProjectDir, requestedProjectDir, platform, }) { if (forceRestart === true) return { reuse: false, retarget: false }; if (inProcess !== true || lifecycleState !== "healthy") return { reuse: false, retarget: false }; if (remoteAccessEnabled !== (requestedRemoteAccess === true)) return { reuse: false, retarget: false }; const retarget = normalizeWorkspaceKey(currentProjectDir, platform) !== normalizeWorkspaceKey(requestedProjectDir, platform); return { reuse: true, retarget }; } /** * A failed server start must not leave the state objects describing the * runtime it already stopped: snapshotOpenworkServerState would report * running:true with a dead baseUrl and assertOpenworkServerReady would pass * against it. Keeps accumulated output for diagnostics and the project dir so * a retry via engineRestart still knows its workspace. The engine state only * resets when this start owned the engine (manageOpencode) — an external * engine keeps running regardless of the server's fate. */ export function resetRuntimeStatesAfterFailedServerStart(openworkServerStateRef, engineStateRef, options = {}) { const serverStdout = openworkServerStateRef.lastStdout; const serverStderr = openworkServerStateRef.lastStderr; Object.assign(openworkServerStateRef, createOpenworkServerState()); openworkServerStateRef.lastStdout = serverStdout; openworkServerStateRef.lastStderr = serverStderr; if (options.manageOpencode === true) { const engineStdout = engineStateRef.lastStdout; const engineStderr = engineStateRef.lastStderr; const projectDir = engineStateRef.projectDir; Object.assign(engineStateRef, createEngineState()); engineStateRef.lastStdout = engineStdout; engineStateRef.lastStderr = engineStderr; engineStateRef.projectDir = projectDir; } } function assertOpenworkServerReady(snapshot) { if (!snapshot?.running) { throw new Error("OpenWork server did not stay running after startup."); } if (!snapshot.baseUrl) { throw new Error("OpenWork server did not report a base URL after startup."); } if (!snapshot.ownerToken && !snapshot.clientToken) { throw new Error("OpenWork server did not report an access token after startup."); } return snapshot; } async function fileExists(targetPath) { try { await readFile(targetPath); return true; } catch { return false; } } async function readJsonFile(targetPath, fallback) { try { const raw = await readFile(targetPath, "utf8"); return JSON.parse(raw); } catch { return fallback; } } function selectLanAddress() { const interfaces = os.networkInterfaces(); for (const entries of Object.values(interfaces)) { for (const entry of entries ?? []) { if (entry && entry.family === "IPv4" && entry.internal === false) { return entry.address; } } } return null; } function buildConnectUrls(port) { const hostname = os.hostname().trim(); const mdnsUrl = hostname ? `http://${hostname.replace(/\.local$/i, "")}.local:${port}` : null; const lan = selectLanAddress(); const lanUrl = lan ? `http://${lan}:${port}` : null; return { connectUrl: lanUrl ?? mdnsUrl, mdnsUrl, lanUrl, }; } function targetTriple() { if (process.platform === "darwin") { return process.arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin"; } if (process.platform === "linux") { return process.arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu"; } if (process.platform === "win32") { return process.arch === "arm64" ? "aarch64-pc-windows-msvc" : "x86_64-pc-windows-msvc"; } return null; } function binaryFileNames(baseName) { const ext = process.platform === "win32" ? ".exe" : ""; const triple = targetTriple(); return [ triple ? `${baseName}-${triple}${ext}` : null, `${baseName}${ext}`, ].filter(Boolean); } function isDirectory(targetPath) { try { return statSync(targetPath).isDirectory(); } catch { return false; } } function nvmVersionBinPaths(home) { const base = path.join(home, ".nvm", "versions", "node"); try { return readdirSync(base, { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => path.join(base, entry.name, "bin")) .filter(isDirectory) .sort() .reverse(); } catch { return []; } } function pathHelperEntries() { if (process.platform !== "darwin") return []; const result = spawnSync("/usr/libexec/path_helper", ["-s"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }); if (result.status !== 0) return []; const stdout = String(result.stdout ?? ""); const match = stdout.match(/PATH="([^"]+)"/) ?? stdout.match(/PATH=([^;\n]+)/); return match?.[1]?.split(path.delimiter).filter(Boolean) ?? []; } function extraPathEntries() { const home = os.homedir(); const candidates = []; if (process.platform === "darwin") { candidates.push( ...pathHelperEntries(), "/opt/homebrew/bin", "/opt/homebrew/sbin", "/usr/local/bin", "/usr/local/sbin", path.join(home, ".nvm", "current", "bin"), ...nvmVersionBinPaths(home), path.join(home, ".fnm", "current", "bin"), path.join(home, ".volta", "bin"), path.join(home, "Library", "pnpm"), path.join(home, ".bun", "bin"), path.join(home, ".cargo", "bin"), path.join(home, ".pyenv", "shims"), path.join(home, ".local", "bin"), ); } if (process.platform === "linux") { candidates.push( "/usr/local/bin", "/usr/local/sbin", path.join(home, ".nvm", "current", "bin"), ...nvmVersionBinPaths(home), path.join(home, ".fnm", "current", "bin"), path.join(home, ".volta", "bin"), path.join(home, ".local", "share", "pnpm"), path.join(home, ".bun", "bin"), path.join(home, ".cargo", "bin"), path.join(home, ".pyenv", "shims"), path.join(home, ".local", "bin"), ); } if (process.platform === "win32") { candidates.push( path.join(home, ".volta", "bin"), path.join(home, ".bun", "bin"), path.join(home, ".cargo", "bin"), process.env.APPDATA ? path.join(process.env.APPDATA, "npm") : null, process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, "pnpm") : null, ); } return candidates.filter((entry) => entry && isDirectory(entry)); } function enrichedPath(sidecarDirs, currentPath) { const entries = [ ...sidecarDirs.filter(isDirectory), ...extraPathEntries(), ...String(currentPath ?? "").split(path.delimiter).filter(Boolean), ]; const deduped = entries.filter((entry, index) => entries.indexOf(entry) === index); return deduped.length > 0 ? deduped.join(path.delimiter) : null; } async function portAvailable(host, port) { return new Promise((resolve) => { const server = net.createServer(); server.unref(); server.once("error", () => resolve(false)); server.listen({ host, port }, () => { server.close(() => resolve(true)); }); }); } async function findFreePort(host = "127.0.0.1") { return new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); server.once("error", reject); server.listen({ host, port: 0 }, () => { const address = server.address(); if (!address || typeof address === "string") { server.close(() => reject(new Error("Failed to allocate a free port."))); return; } const { port } = address; server.close(() => resolve(port)); }); }); } async function fetchJson(url, options = {}, timeoutMs = 3000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { // loopback-fetch: fetchJson callers pass runtime-managed 127.0.0.1 server URLs. const response = await fetch(url, { ...options, signal: controller.signal, headers: { Accept: "application/json", ...(options.headers ?? {}), }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return await response.json(); } finally { clearTimeout(timer); } } export function resolveUserEnvFilePath(env = process.env) { return openworkEnvStorePath({ env }); } const USER_ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; const USER_ENV_RESERVED_PREFIXES = ["OPENWORK_", "OPENCODE_"]; // Synchronous, best-effort; absent or malformed returns {}. Reserved prefixes // are stripped so a tampered file can never shadow OPENWORK_* / OPENCODE_*. function loadUserEnvFile(env = process.env) { try { const raw = readFileSync(resolveUserEnvFilePath(env), "utf8"); const parsed = JSON.parse(raw); if (!parsed || !Array.isArray(parsed.variables)) return {}; const out = {}; for (const entry of parsed.variables) { if (!entry || typeof entry !== "object") continue; const { key, value } = entry; if (typeof key !== "string" || typeof value !== "string") continue; if (!USER_ENV_KEY_PATTERN.test(key)) continue; if (USER_ENV_RESERVED_PREFIXES.some((p) => key.startsWith(p))) continue; out[key] = value; } return out; } catch { return {}; } } /** * @param {unknown} value * @returns {value is Record} */ function isPlainObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } /** * Reduces an administrator-provisioned Den control-plane URL to its exact * origin. Keep this in sync with apps/server/src/enterprise-den-origin.ts. * * @param {unknown} rawValue * @returns {string | null} */ function exactEnterpriseOrigin(rawValue) { if (typeof rawValue !== "string") return null; const raw = rawValue.trim(); if (!raw || raw.length > 2 * 1024) return null; try { const url = new URL(raw); if (url.protocol !== "https:") return null; if (url.username || url.password || url.search || url.hash) return null; return url.origin; } catch { return null; } } /** * @param {string} filePath * @returns {Promise} */ async function readActivatedEnterpriseOrigin(filePath) { if (typeof filePath !== "string" || !filePath.trim()) return null; try { const fileStat = await stat(filePath); if (!fileStat.isFile() || fileStat.size > MAX_BOOTSTRAP_BYTES) return null; const raw = await readFile(filePath, "utf8"); if (Buffer.byteLength(raw, "utf8") > MAX_BOOTSTRAP_BYTES) return null; const data = JSON.parse(raw); if (!isPlainObject(data)) return null; const activation = isPlainObject(data.enterpriseActivation) ? data.enterpriseActivation : null; if (!activation) return null; if (typeof activation.activatedAt !== "string" || !activation.activatedAt.trim()) return null; return exactEnterpriseOrigin(activation.denBaseUrl); } catch { return null; } } /** * @param {Iterable} values * @returns {string[]} */ function normalizeRepairOrigins(values) { const seen = new Set(); const origins = []; for (const value of values) { const origin = exactEnterpriseOrigin(value); if (!origin || seen.has(origin)) continue; origins.push(origin); seen.add(origin); if (origins.length >= MAX_CHAIN_REPAIR_ORIGINS) break; } return origins; } /** * @param {string} origin * @returns {{ host: string, port: number }} */ function tlsTargetFromOrigin(origin) { const url = new URL(origin); return { host: url.hostname.replace(/^\[(.*)\]$/, "$1"), port: url.port ? Number(url.port) : 443, }; } /** * @param {unknown} error * @returns {string} */ function errorCode(error) { return isPlainObject(error) && typeof error.code === "string" ? error.code : "unknown"; } /** * @param {RuntimeChainRepairSocket} socket * @returns {void} */ function destroySocket(socket) { try { socket.destroy(); } catch { // Best effort. } } /** * @typedef {Object} RuntimeSystemCaTlsModule * @property {(type?: string) => string[]} [getCACertificates] * @property {(certificates: string[]) => void} [setDefaultCACertificates] */ /** * @typedef {Object} RuntimeChainRepairFetchResponse * @property {boolean} [ok] * @property {number} [status] * @property {() => Promise} arrayBuffer */ /** * @typedef {Object} RuntimeChainRepairFetchOptions * @property {AbortSignal} [signal] */ /** * @typedef {(input: string, init?: RuntimeChainRepairFetchOptions) => Promise} RuntimeChainRepairFetch */ /** * @typedef {Object} RuntimeChainRepairTlsConnectOptions * @property {string} host * @property {number} port * @property {string} servername * @property {boolean} rejectUnauthorized * @property {number} timeout */ /** * @typedef {Object} RuntimeChainRepairPeerCertificate * @property {Buffer} [raw] * @property {RuntimeChainRepairPeerCertificate} [issuerCertificate] */ /** * @typedef {Object} RuntimeChainRepairSocket * @property {boolean} [authorized] * @property {unknown} [authorizationError] * @property {(eventName: string, listener: (...args: unknown[]) => void) => RuntimeChainRepairSocket} once * @property {(eventName: string, listener: (...args: unknown[]) => void) => RuntimeChainRepairSocket} off * @property {(timeout: number) => RuntimeChainRepairSocket} setTimeout * @property {() => void} destroy * @property {() => X509Certificate | undefined} [getPeerX509Certificate] * @property {(detailed?: boolean) => RuntimeChainRepairPeerCertificate | null} [getPeerCertificate] */ /** * @typedef {(options: RuntimeChainRepairTlsConnectOptions) => RuntimeChainRepairSocket} RuntimeChainRepairTlsConnect */ /** * @typedef {Object} RuntimeChainRepairOptions * @property {string[]} [origins] * @property {RuntimeChainRepairFetch} [fetchImpl] * @property {() => string[]} [rootsProvider] * @property {RuntimeChainRepairTlsConnect} [tlsConnectImpl] * @property {string} [bootstrapPath] * @property {boolean} [disabled] */ /** * @typedef {Object} ResolveSystemCaEnvOptions * @property {RuntimeSystemCaTlsModule} [tlsModule] * @property {string} userDataDir * @property {NodeJS.ProcessEnv} [parentEnv] * @property {(...args: unknown[]) => void} [logInfo] * @property {() => Promise} [loadPlatformCertificates] * @property {string} [platformSourceName] * @property {NodeJS.Platform} [platform] * @property {RuntimeChainRepairOptions} [chainRepair] */ /** * @param {string} origin * @param {RuntimeChainRepairTlsConnect} tlsConnectImpl * @param {boolean} rejectUnauthorized * @returns {Promise} */ function connectForChainRepair(origin, tlsConnectImpl, rejectUnauthorized) { const target = tlsTargetFromOrigin(origin); return new Promise((resolve, reject) => { let settled = false; let socket; const finish = (value, isError) => { if (settled) return; settled = true; if (socket) { socket.off("secureConnect", onSecureConnect); socket.off("error", onError); socket.off("timeout", onTimeout); } if (isError) reject(value); else resolve(value); }; const onSecureConnect = () => finish(socket, false); const onError = (error) => { if (socket) destroySocket(socket); finish(error, true); }; const onTimeout = () => { const error = new Error("TLS connection timed out"); Object.defineProperty(error, "code", { value: "ETIMEDOUT" }); if (socket) destroySocket(socket); finish(error, true); }; try { socket = tlsConnectImpl({ host: target.host, port: target.port, servername: target.host, rejectUnauthorized, timeout: CHAIN_REPAIR_SOCKET_TIMEOUT_MS, }); socket.once("secureConnect", onSecureConnect); socket.once("error", onError); socket.once("timeout", onTimeout); socket.setTimeout(CHAIN_REPAIR_SOCKET_TIMEOUT_MS); } catch (error) { finish(error, true); } }); } /** * @param {string} origin * @param {RuntimeChainRepairTlsConnect} tlsConnectImpl * @returns {Promise} */ async function strictProbeChainRepair(origin, tlsConnectImpl) { let socket; try { socket = await connectForChainRepair(origin, tlsConnectImpl, true); return socket.authorized === true ? null : String(socket.authorizationError || "UNAUTHORIZED"); } catch (error) { return errorCode(error); } finally { if (socket) destroySocket(socket); } } /** * @param {RuntimeChainRepairPeerCertificate | null} peer * @returns {boolean} */ function peerChainIsLeafOnly(peer) { if (!peer || typeof peer !== "object") return true; const issuer = peer.issuerCertificate; if (!issuer || issuer === peer) return true; if (peer.raw && issuer.raw && Buffer.compare(Buffer.from(peer.raw), Buffer.from(issuer.raw)) === 0) return true; return false; } /** * @param {string} origin * @param {RuntimeChainRepairTlsConnect} tlsConnectImpl * @returns {Promise<{ leaf: X509Certificate, leafOnly: boolean } | null>} */ async function introspectLeafCertificate(origin, tlsConnectImpl) { let socket; try { socket = await connectForChainRepair(origin, tlsConnectImpl, false); if (typeof socket.getPeerX509Certificate !== "function") return null; const leaf = socket.getPeerX509Certificate(); if (!leaf) return null; const peer = typeof socket.getPeerCertificate === "function" ? socket.getPeerCertificate(true) : null; return { leaf, leafOnly: peerChainIsLeafOnly(peer) }; } catch { return null; } finally { if (socket) destroySocket(socket); } } /** * @param {X509Certificate} certificate * @returns {string[]} */ function caIssuerUrls(certificate) { const urls = []; const infoAccess = typeof certificate.infoAccess === "string" ? certificate.infoAccess : ""; for (const line of infoAccess.split(/\r?\n/)) { const match = /^\s*CA Issuers\s*-\s*URI:(.+?)\s*$/i.exec(line); if (!match) continue; try { const url = new URL(match[1].trim()); if (url.protocol !== "http:" && url.protocol !== "https:") continue; urls.push(url.href); if (urls.length >= 2) break; } catch { // Ignore malformed AIA entries. } } return urls; } /** * @param {ArrayBuffer} bytes * @returns {X509Certificate} */ function certificateFromBody(bytes) { const buffer = Buffer.from(bytes); try { return new X509Certificate(buffer); } catch (bufferError) { const text = buffer.toString("utf8"); if (/-----BEGIN CERTIFICATE-----/.test(text)) return new X509Certificate(text); throw bufferError; } } /** * @param {X509Certificate} certificate * @returns {string} */ function certificateCommonName(certificate) { for (const line of certificate.subject.split(/\r?\n/)) { const match = /^CN\s*=\s*(.+)$/.exec(line.trim()); if (match) return match[1].trim(); } return certificate.subject || "unknown subject"; } /** * @param {string} pem * @returns {X509Certificate | null} */ function cachedRootCertificate(pem) { const key = String(pem ?? "").trim(); if (!key) return null; if (chainRepairRootCache.has(key)) return chainRepairRootCache.get(key) ?? null; try { const certificate = new X509Certificate(key); chainRepairRootCache.set(key, certificate); return certificate; } catch { chainRepairRootCache.set(key, null); return null; } } /** * @param {X509Certificate} intermediate * @param {() => string[]} rootsProvider * @returns {boolean} */ function intermediateChainsToTrustedRoot(intermediate, rootsProvider) { let roots = []; try { roots = rootsProvider(); } catch { roots = []; } for (const pem of roots) { const root = cachedRootCertificate(pem); if (root && intermediate.checkIssued(root) && intermediate.verify(root.publicKey)) return true; } return false; } /** * @param {X509Certificate} leaf * @param {X509Certificate} intermediate * @param {() => string[]} rootsProvider * @returns {string | null} */ function refusalReason(leaf, intermediate, rootsProvider) { if (intermediate.ca !== true) return "fetched certificate is not a CA"; if (leaf.checkIssued(intermediate) !== true) return "fetched certificate did not issue leaf"; if (leaf.verify(intermediate.publicKey) !== true) return "leaf signature verification failed"; if (!intermediateChainsToTrustedRoot(intermediate, rootsProvider)) return "fetched certificate does not chain to a trusted public root"; return null; } /** * @param {string} url * @param {RuntimeChainRepairFetch} fetchImpl * @returns {Promise} */ async function fetchIntermediateCertificate(url, fetchImpl) { const response = await fetchImpl(url, { signal: AbortSignal.timeout(CHAIN_REPAIR_FETCH_TIMEOUT_MS) }); if (!response || typeof response.arrayBuffer !== "function") return null; if (response.ok === false) return null; const bytes = await response.arrayBuffer(); if (bytes.byteLength > MAX_CHAIN_REPAIR_BODY_BYTES) return null; return certificateFromBody(bytes); } /** * @param {ResolveSystemCaEnvOptions} options * @returns {Promise} */ async function resolveChainRepairOrigins(options) { const env = options.parentEnv ?? {}; const chainRepair = options.chainRepair ?? {}; if (chainRepair.origins) return normalizeRepairOrigins(chainRepair.origins); const envOrigins = typeof env.OPENWORK_CHAIN_REPAIR_ORIGINS === "string" ? env.OPENWORK_CHAIN_REPAIR_ORIGINS : ""; if (envOrigins.trim()) return normalizeRepairOrigins(envOrigins.split(",")); const bootstrapPath = chainRepair.bootstrapPath ?? desktopBootstrapPath({ env }); const origin = await readActivatedEnterpriseOrigin(bootstrapPath); return origin ? [origin] : []; } /** * @param {ResolveSystemCaEnvOptions} options * @returns {Promise<{ pems: string[], timedOut: boolean }>} */ async function repairIncompleteChains(options) { const env = options.parentEnv ?? {}; const chainRepair = options.chainRepair ?? {}; const logInfo = options.logInfo; if (chainRepair.disabled === true || String(env.OPENWORK_DISABLE_CHAIN_REPAIR ?? "").trim() === "1") { if (typeof logInfo === "function") logInfo("OpenWork runtime: chain repair disabled by OPENWORK_DISABLE_CHAIN_REPAIR."); return { pems: [], timedOut: false }; } const origins = await resolveChainRepairOrigins(options); if (origins.length === 0) { if (!chainRepair.origins && !String(env.OPENWORK_CHAIN_REPAIR_ORIGINS ?? "").trim() && typeof logInfo === "function") { logInfo("OpenWork runtime: chain repair skipped: no activation record."); } return { pems: [], timedOut: false }; } const fetchImpl = chainRepair.fetchImpl ?? globalThis.fetch; const tlsModule = options.tlsModule ?? tls; const tlsConnectImpl = chainRepair.tlsConnectImpl ?? tls.connect; const totalTimeoutValue = Number(env.OPENWORK_CHAIN_REPAIR_TIMEOUT_MS); const totalTimeoutMs = Number.isFinite(totalTimeoutValue) && totalTimeoutValue >= 1000 && totalTimeoutValue <= 120000 ? totalTimeoutValue : CHAIN_REPAIR_TOTAL_TIMEOUT_MS; const rootsProvider = chainRepair.rootsProvider ?? (() => { if (typeof tlsModule?.getCACertificates !== "function") return []; const roots = tlsModule.getCACertificates("default"); return Array.isArray(roots) ? roots : []; }); if (typeof fetchImpl !== "function") { if (typeof logInfo === "function") { for (const origin of origins) logInfo(`OpenWork runtime: chain repair skipped for ${origin}: fetch unavailable`); } return { pems: [], timedOut: false }; } const run = async () => { const pems = []; for (const origin of origins) { const strictError = await strictProbeChainRepair(origin, tlsConnectImpl); if (strictError === null) { if (typeof logInfo === "function") logInfo(`OpenWork runtime: chain ok for ${origin}`); continue; } if (strictError !== "UNABLE_TO_VERIFY_LEAF_SIGNATURE") { if (typeof logInfo === "function") logInfo(`OpenWork runtime: chain repair skipped for ${origin}: ${strictError}`); continue; } const leafState = await introspectLeafCertificate(origin, tlsConnectImpl); if (!leafState) { if (typeof logInfo === "function") logInfo(`OpenWork runtime: chain repair skipped for ${origin}: certificate introspection failed`); continue; } if (!leafState.leafOnly) { if (typeof logInfo === "function") logInfo(`OpenWork runtime: chain repair skipped for ${origin}: served chain includes an intermediate`); continue; } const issuerUrls = caIssuerUrls(leafState.leaf); if (issuerUrls.length === 0) { if (typeof logInfo === "function") logInfo(`OpenWork runtime: chain repair skipped for ${origin}: no CA Issuers AIA URL`); continue; } let repaired = false; for (const url of issuerUrls) { let intermediate; try { intermediate = await fetchIntermediateCertificate(url, fetchImpl); } catch { intermediate = null; } if (!intermediate) continue; const reason = refusalReason(leafState.leaf, intermediate, rootsProvider); if (reason) { if (typeof logInfo === "function") logInfo(`OpenWork runtime: chain repair refused for ${origin}: ${reason}`); continue; } pems.push(intermediate.toString()); repaired = true; if (typeof logInfo === "function") { logInfo(`OpenWork runtime: chain repaired for ${origin}: added "${certificateCommonName(intermediate)}"`); } break; } if (!repaired && typeof logInfo === "function") { logInfo(`OpenWork runtime: chain repair skipped for ${origin}: no usable AIA issuer certificate`); } } return { pems, timedOut: false }; }; let timeoutId; const timeout = new Promise((resolve) => { timeoutId = setTimeout(() => resolve({ pems: [], timedOut: true }), totalTimeoutMs); timeoutId.unref?.(); }); try { return await Promise.race([run(), timeout]); } finally { clearTimeout(timeoutId); } } /** * @param {ResolveSystemCaEnvOptions} options * @returns {Promise<{ childEnv: NodeJS.ProcessEnv, trustedCertificates: string[] }>} */ async function resolveSystemCa({ tlsModule = tls, userDataDir, parentEnv = process.env, logInfo = console.info, loadPlatformCertificates, platformSourceName, platform = process.platform, chainRepair, }) { const env = parentEnv ?? {}; if (Object.prototype.hasOwnProperty.call(env, "NODE_EXTRA_CA_CERTS")) { if (typeof logInfo === "function") { logInfo("OpenWork runtime: NODE_EXTRA_CA_CERTS is already set; skipping system CA bundle export."); } try { const configuredPem = await readFile(String(env.NODE_EXTRA_CA_CERTS), "utf8"); return { childEnv: {}, trustedCertificates: parsePemCertificates(configuredPem) }; } catch { return { childEnv: {}, trustedCertificates: [] }; } } try { const platformLoader = loadPlatformCertificates ? { name: platformSourceName || "platform-stores", load: loadPlatformCertificates } : systemPlatformCertificateLoader(platform); const bundle = await resolveSystemCaBundle({ runtime: () => { if (typeof tlsModule?.getCACertificates !== "function") return []; const certs = tlsModule.getCACertificates("system"); return Array.isArray(certs) ? certs : []; }, platform: platformLoader, }); if (typeof logInfo === "function") { logInfo(`OpenWork runtime: system CA bundle sources ${summarizeSystemCaSources(bundle.sources)}`); } let repairedPems = []; try { const repaired = await repairIncompleteChains({ tlsModule, userDataDir, parentEnv: env, logInfo, loadPlatformCertificates, platformSourceName, platform, chainRepair, }); repairedPems = repaired.pems; if (repaired.timedOut && typeof logInfo === "function") { logInfo("OpenWork runtime: chain repair skipped: timed out"); } } catch { repairedPems = []; } const certificates = dedupeCertificates([...bundle.certificates, ...repairedPems]); if (certificates.length === 0) return { childEnv: {}, trustedCertificates: bundle.certificates }; if (typeof tlsModule?.getCACertificates === "function" && typeof tlsModule?.setDefaultCACertificates === "function") { try { const defaultCerts = tlsModule.getCACertificates("default"); tlsModule.setDefaultCACertificates(dedupeCertificates([...(Array.isArray(defaultCerts) ? defaultCerts : []), ...certificates])); } catch { // Best-effort only; child processes still receive NODE_EXTRA_CA_CERTS. } } const pem = certificates.join("\n"); if (!pem) return { childEnv: {}, trustedCertificates: bundle.certificates }; const bundlePath = path.join(userDataDir, "system-ca-bundle.pem"); await mkdir(path.dirname(bundlePath), { recursive: true }); await writeFile(bundlePath, `${pem}\n`, "utf8"); return { childEnv: { NODE_EXTRA_CA_CERTS: bundlePath }, trustedCertificates: bundle.certificates, }; } catch { return { childEnv: {}, trustedCertificates: [] }; } } export async function resolveSystemCaEnv(options) { return (await resolveSystemCa(options)).childEnv; } /** * @param {NodeJS.ProcessEnv} [baseEnv] * @param {NodeJS.ProcessEnv} [caEnv] * @param {NodeJS.ProcessEnv} [extra] * @returns {NodeJS.ProcessEnv} */ export function mergeSystemCaChildEnv(baseEnv = {}, caEnv = {}, extra = {}) { return { ...baseEnv, ...(Object.prototype.hasOwnProperty.call(baseEnv, "NODE_EXTRA_CA_CERTS") ? {} : caEnv), ...extra, }; } export function createRuntimeManager({ app, desktopRoot, listLocalWorkspacePaths, localManagedMcpVaultKey, workspaceMkdir = mkdir, workspacePlatform = process.platform, }) { const inheritedProcessEnv = { ...process.env }; let injectedUserEnvKeys = new Set(); const engineState = createEngineState(); const openworkServerState = createOpenworkServerState(); // Monotonic across this Electron process. Never reset with the server // state: each successful server start must be observable as a new // generation even when ports and tokens are reused. let openworkServerGenerationCounter = 0; // Serialize engine lifecycle operations. Without this, concurrent renderer // invocations of engineStart/engineStop/engineRestart race: each call's // stopAllRuntimeChildren kills the previous call's freshly-started server, // and the prior call then times out its /health probe. /** @type {Promise} */ let runtimeLifecycleQueue = Promise.resolve(); let lifecycleState = "idle"; /** * Serialize engine lifecycle operations; preserves the wrapped function's * return type (untyped, this collapsed runtime-manager inference to * Promise and blocked tightening the DesktopCommandMap results). * @template T * @param {() => Promise} fn * @returns {Promise} */ function withRuntimeLifecycle(fn) { const next = runtimeLifecycleQueue.then(fn, fn); runtimeLifecycleQueue = next.catch(() => {}); return next; } const userDataDir = app.getPath("userData"); const sidecarDirs = [ path.join(desktopRoot, "resources", "sidecars"), process.resourcesPath ? path.join(process.resourcesPath, "sidecars") : null, path.join(path.dirname(app.getPath("exe")), "sidecars"), ].filter(Boolean); let systemCaPromise = null; function systemCa() { systemCaPromise ??= resolveSystemCa({ tlsModule: tls, userDataDir, parentEnv: { ...loadUserEnvFile(process.env), ...process.env }, }); return systemCaPromise; } function openworkServerTokenStorePath() { const override = process.env.OPENWORK_SERVER_TOKEN_STORE_PATH?.trim(); if (override) return path.resolve(override); return path.join(userDataDir, "openwork-server-tokens.json"); } function openworkServerStatePath() { const override = process.env.OPENWORK_SERVER_STATE_PATH?.trim(); if (override) return path.resolve(override); return path.join(userDataDir, "openwork-server-state.json"); } function managedOpencodeWorkdir() { return path.join(userDataDir, "managed-opencode-workdir"); } async function loadTokenStore() { const stored = await readJsonFile(openworkServerTokenStorePath(), { version: 1, workspaces: {} }); const migrated = migrateOpenworkServerTokenStore(stored); if (JSON.stringify(stored) !== JSON.stringify(migrated)) { await saveTokenStore(migrated); } return migrated; } async function saveTokenStore(store) { const filePath = openworkServerTokenStorePath(); await mkdir(path.dirname(filePath), { recursive: true }); await writeFile(filePath, `${JSON.stringify(store, null, 2)}\n`, "utf8"); } async function loadPortState() { return readJsonFile(openworkServerStatePath(), { version: 4, workspacePorts: {}, preferredPort: null, }); } async function savePortState(state) { const filePath = openworkServerStatePath(); await mkdir(path.dirname(filePath), { recursive: true }); await writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, "utf8"); } async function loadServerCredentials() { const store = await loadTokenStore(); return store.credentials; } async function persistServerOwnerToken(ownerToken) { const store = await loadTokenStore(); store.credentials.ownerToken = ownerToken; store.credentials.updatedAt = nowMs(); await saveTokenStore(store); } async function readPreferredOpenworkPort(workspaceKey) { const state = await loadPortState(); const normalized = normalizeWorkspaceKey(workspaceKey, workspacePlatform); if (normalized && state.workspacePorts?.[normalized]) { return state.workspacePorts[normalized]; } return state.preferredPort ?? null; } async function persistPreferredOpenworkPort(workspaceKey, port) { const state = await loadPortState(); const normalized = normalizeWorkspaceKey(workspaceKey, workspacePlatform); state.version = 4; state.workspacePorts ??= {}; if (normalized) { state.workspacePorts[normalized] = port; state.preferredPort = null; } else { state.preferredPort = port; } await savePortState(state); } async function waitForPortAvailable(host, port, timeoutMs = 2000) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { if (await portAvailable(host, port)) return true; await new Promise((resolve) => setTimeout(resolve, 100)); } return portAvailable(host, port); } async function resolveOpenworkPort(host, workspaceKey, currentPort = null) { const preferredPort = await readPreferredOpenworkPort(workspaceKey); if (currentPort && (await waitForPortAvailable(host, currentPort))) { return { port: currentPort, preferredPort }; } if (preferredPort && (await waitForPortAvailable(host, preferredPort))) { return { port: preferredPort, preferredPort }; } return { port: await findFreePort(host), preferredPort }; } async function ensureDevModePaths() { const root = path.join(userDataDir, "openwork-dev-data"); const paths = { homeDir: path.join(root, "home"), xdgConfigHome: path.join(root, "xdg", "config"), xdgDataHome: path.join(root, "xdg", "data"), xdgCacheHome: path.join(root, "xdg", "cache"), xdgStateHome: path.join(root, "xdg", "state"), opencodeConfigDir: path.join(root, "config", "opencode"), }; for (const dir of Object.values(paths)) { await mkdir(dir, { recursive: true }); } await mkdir(path.join(paths.xdgDataHome, "opencode"), { recursive: true }); return paths; } async function buildChildEnv(extra = {}) { /** @type {NodeJS.ProcessEnv} */ // User env is layered first so process.env + any caller overrides always // win. See apps/server/src/env-file.ts — all loaders must agree on path + // reserved-keys policy. const devPaths = process.env.OPENWORK_DEV_MODE === "1" && process.env.OPENWORK_DEV_SHARED_STATE !== "1" ? await ensureDevModePaths() : null; const userEnvPathEnv = devPaths ? { ...process.env, HOME: devPaths.homeDir, USERPROFILE: devPaths.homeDir, XDG_CONFIG_HOME: devPaths.xdgConfigHome, } : process.env; const userEnv = loadUserEnvFile(userEnvPathEnv); injectedUserEnvKeys = reconcileInjectedUserEnv({ processEnv: process.env, inheritedEnv: inheritedProcessEnv, userEnv, previouslyInjectedKeys: injectedUserEnvKeys, }); const baseEnv = { ...userEnv, ...process.env, BUN_CONFIG_DNS_RESULT_ORDER: "verbatim", }; const caEnv = Object.prototype.hasOwnProperty.call(baseEnv, "NODE_EXTRA_CA_CERTS") ? {} : (await systemCa()).childEnv; // Bun honors Node's NODE_EXTRA_CA_CERTS, so bundled Bun sidecars inherit // the exported OS trust store through the same child env variable. const env = mergeSystemCaChildEnv(baseEnv, caEnv, extra); const pathKey = Object.prototype.hasOwnProperty.call(env, "PATH") || !Object.prototype.hasOwnProperty.call(env, "Path") ? "PATH" : "Path"; const pathEnv = enrichedPath(sidecarDirs, env[pathKey]); if (pathEnv) { env[pathKey] = pathEnv; } if (devPaths) { env.OPENWORK_DEV_MODE = "1"; env.HOME = devPaths.homeDir; env.USERPROFILE = devPaths.homeDir; env.XDG_CONFIG_HOME = devPaths.xdgConfigHome; env.XDG_DATA_HOME = devPaths.xdgDataHome; env.XDG_CACHE_HOME = devPaths.xdgCacheHome; env.XDG_STATE_HOME = devPaths.xdgStateHome; env.OPENCODE_CONFIG_DIR = devPaths.opencodeConfigDir; env.OPENCODE_TEST_HOME = devPaths.homeDir; } return env; } function resolveBinaryInfo(baseName, extraPaths = []) { for (const directory of [...sidecarDirs, ...extraPaths]) { for (const fileName of binaryFileNames(baseName)) { const candidate = path.join(directory, fileName); if (existsSync(candidate)) { return { path: candidate, source: "bundled" }; } } } const pathEntries = (enrichedPath([], process.env.PATH) ?? "") .split(path.delimiter) .filter(Boolean); for (const entry of pathEntries) { for (const fileName of binaryFileNames(baseName)) { const candidate = path.join(entry, fileName); if (existsSync(candidate)) { return { path: candidate, source: "path" }; } } } if (baseName === "opencode") { for (const candidate of [ path.join(app.getPath("home"), ".opencode", "bin", process.platform === "win32" ? "opencode.exe" : "opencode"), path.join("/opt/homebrew/bin", process.platform === "win32" ? "opencode.exe" : "opencode"), path.join("/usr/local/bin", process.platform === "win32" ? "opencode.exe" : "opencode"), path.join("/usr/bin", process.platform === "win32" ? "opencode.exe" : "opencode"), ]) { if (existsSync(candidate)) { return { path: candidate, source: "known-location" }; } } } return null; } function resolveBinary(baseName, extraPaths = []) { return resolveBinaryInfo(baseName, extraPaths)?.path ?? null; } function resolveOpencodeBinary(opencodeBinPath) { const explicitPath = typeof opencodeBinPath === "string" ? opencodeBinPath.trim() : ""; return explicitPath ? { path: explicitPath, source: "custom" } : resolveBinaryInfo("opencode"); } function resolveDockerCandidates() { const candidates = []; const seen = new Set(); for (const key of ["OPENWORK_DOCKER_BIN", "OPENWRK_DOCKER_BIN", "DOCKER_BIN"]) { const value = process.env[key]?.trim(); if (value && !seen.has(value)) { seen.add(value); candidates.push(value); } } for (const entry of (process.env.PATH ?? "").split(path.delimiter).filter(Boolean)) { const candidate = path.join(entry, process.platform === "win32" ? "docker.exe" : "docker"); if (!seen.has(candidate)) { seen.add(candidate); candidates.push(candidate); } } for (const candidate of [ "/opt/homebrew/bin/docker", "/usr/local/bin/docker", "/Applications/Docker.app/Contents/Resources/bin/docker", ]) { if (!seen.has(candidate)) { seen.add(candidate); candidates.push(candidate); } } return candidates.filter((candidate) => existsSync(candidate)); } function runDockerCommandDetailed(args, timeoutMs = 8000) { const tried = [...resolveDockerCandidates(), process.platform === "win32" ? "docker.exe" : "docker"]; const errors = []; for (const program of tried) { try { const result = spawnSync(program, args, { encoding: "utf8", timeout: timeoutMs, windowsHide: true, }); return { program, status: typeof result.status === "number" ? result.status : -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "", }; } catch (error) { errors.push(error instanceof Error ? error.message : String(error)); } } throw new Error( `Failed to run docker: ${errors.join("; ")} (Set OPENWORK_DOCKER_BIN to your docker binary if needed)`, ); } const legacyOpenworkContainerPrefix = `${["openwork", "orchestrator"].join("-")}-`; async function listOpenworkManagedContainers() { const result = runDockerCommandDetailed(["ps", "-a", "--format", "{{.Names}}"], 8000); if (result.status !== 0) { const combined = `${result.stdout.trim()}\n${result.stderr.trim()}`.trim(); throw new Error(combined || `docker ps -a failed (status ${result.status})`); } return result.stdout .split(/\r?\n/) .map((line) => line.trim()) .filter((name) => name && (name.startsWith(legacyOpenworkContainerPrefix) || name.startsWith("openwork-dev-") || name.startsWith("openwrk-"))) .sort(); } async function runShellCommand(program, args, options = {}) { const result = spawnSync(program, args, { encoding: "utf8", cwd: options.cwd, env: options.env, shell: false, windowsHide: true, timeout: options.timeoutMs, }); return { status: typeof result.status === "number" ? result.status : -1, stdout: result.stdout ?? "", stderr: result.stderr ?? "", }; } function engineDoctor(options = {}) { const resolved = resolveOpencodeBinary(options?.opencodeBinPath); if (!resolved?.path) { return { found: false, inPath: false, resolvedPath: null, resolvedSource: null, version: null, supportsServe: false, notes: ["OpenCode binary not found in bundled sidecars or PATH."], serveHelpStatus: null, serveHelpStdout: null, serveHelpStderr: null, }; } const versionResult = spawnSync(resolved.path, ["--version"], { encoding: "utf8" }); const helpResult = spawnSync(resolved.path, ["serve", "--help"], { encoding: "utf8" }); const notes = [`Using ${resolved.source}: ${resolved.path}`]; if (versionResult.status !== 0) { notes.push("OpenCode version probe failed."); } if (helpResult.status !== 0) { notes.push("OpenCode serve --help probe failed."); } return { found: true, inPath: resolved.source === "path", resolvedPath: resolved.path, resolvedSource: resolved.source, version: versionResult.stdout?.trim() || versionResult.stderr?.trim() || null, supportsServe: helpResult.status === 0, notes, serveHelpStatus: typeof helpResult.status === "number" ? helpResult.status : null, serveHelpStdout: helpResult.stdout?.trim() || null, serveHelpStderr: helpResult.stderr?.trim() || null, }; } async function pinnedOpencodeInstallCommand() { const constantsPath = path.resolve(desktopRoot, "../../constants.json"); const payload = JSON.parse(await readFile(constantsPath, "utf8")); const version = String(payload?.opencodeVersion ?? "").trim().replace(/^v/, ""); if (!version) { throw new Error("constants.json is missing opencodeVersion"); } return `curl -fsSL https://opencode.ai/install | bash -s -- --version ${version} --no-modify-path`; } function processMatchesSidecar(command) { return commandMatchesPackagedSidecar(command, sidecarDirs); } function killProcessId(pid, signal = "SIGTERM") { if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return; try { process.kill(pid, signal); } catch { // Process already exited or is not ours. } } async function cleanupPackagedSidecars() { if (!app.isPackaged) return; // Safety net: an unclean Electron quit can orphan sidecars. Packaged builds // should always own a fresh runtime per app launch, so remove any leftover // sidecars from this app bundle before choosing ports for the new runtime. const result = spawnSync("ps", ["-Ao", "pid=,command="], { encoding: "utf8" }); const rows = String(result.stdout ?? "").split(/\r?\n/); const pids = []; for (const row of rows) { const match = row.match(/^\s*(\d+)\s+(.+)$/); if (!match) continue; const pid = Number(match[1]); const command = match[2] ?? ""; if (processMatchesSidecar(command)) pids.push(pid); } for (const pid of pids) killProcessId(pid, "SIGTERM"); if (pids.length > 0) { await new Promise((resolve) => setTimeout(resolve, 500)); for (const pid of pids) killProcessId(pid, "SIGKILL"); } } async function stopChild(state, options = {}) { const child = state.child; state.child = null; state.childExited = true; if (!child || child.exitCode != null || child.killed) return; if (options.requestShutdown) { try { const shutdownRequested = await options.requestShutdown(); if (shutdownRequested) { await new Promise((resolve) => setTimeout(resolve, 750)); } } catch { // ignore } } if (child.exitCode == null && !child.killed) { child.kill("SIGTERM"); await new Promise((resolve) => setTimeout(resolve, 500)); if (child.exitCode == null && !child.killed) { child.kill("SIGKILL"); } } } async function ensureOpencodeConfig(projectDir) { const configPath = resolveWorkspaceOpencodeConfigPath(projectDir); if (await fileExists(configPath)) return; await mkdir(path.dirname(configPath), { recursive: true }); await writeFile( configPath, `${JSON.stringify({ $schema: "https://opencode.ai/config.json" }, null, 2)}\n`, "utf8", ); } async function issueOwnerToken(baseUrl, hostToken) { const payload = await fetchJson( `${baseUrl.replace(/\/+$/, "")}/tokens`, { method: "POST", headers: { "Content-Type": "application/json", "X-OpenWork-Host-Token": hostToken, }, body: JSON.stringify({ scope: "owner", label: "OpenWork desktop owner token" }), }, 5000, ); const token = typeof payload?.token === "string" ? payload.token.trim() : ""; return token || null; } // In-process server handle. Kept alive across restarts so we can stop it. let inProcessServer = null; async function startOpenworkServer(options) { // The inner start stops any previous runtime before mutating state, so a // throw below always happens with nothing left running. try { return await startOpenworkServerInner(options); } catch (error) { resetRuntimeStatesAfterFailedServerStart(openworkServerState, engineState, options); throw error; } } async function startOpenworkServerInner(options) { const evalDelayMs = resolveEvalLocalServerDelayMs(); if (evalDelayMs > 0) { await new Promise((resolve) => setTimeout(resolve, evalDelayMs)); } const currentPort = openworkServerState.port; // Stop any previously running in-process server if (inProcessServer) { try { await inProcessServer.stop(); } catch { /* ignore */ } inProcessServer = null; } await stopChild(openworkServerState); const host = options.remoteAccessEnabled ? "0.0.0.0" : "127.0.0.1"; const managedOpencode = options.manageOpencode ? resolveOpencodeBinary(options.opencodeBinPath) : null; openworkServerState.managedOpencodeBinPath = managedOpencode?.path ?? null; openworkServerState.managedOpencodeBinSource = managedOpencode?.source ?? null; if (options.manageOpencode) { engineState.opencodeBinPath = managedOpencode?.path ?? null; engineState.opencodeBinSource = managedOpencode?.source ?? null; } // Inject user env vars so the server and managed OpenCode inherit them. const serverEnv = await buildChildEnv({}); Object.assign(process.env, serverEnv); // Once the embedded server has a persisted registry, it is the source of // truth. Do not pass Electron's legacy workspace list as CLI workspaces or // the server config loader will ignore server.json and lose server-created // workspaces after restart. const serverConfigPath = resolveOpenworkServerConfigPath(process.env); const requestedWorkspacePaths = prioritizeWorkspacePaths("", options.workspacePaths, { platform: workspacePlatform, }); const workspacePaths = seedWorkspacePathsForEmbeddedServer( requestedWorkspacePaths, existsSync(serverConfigPath), ); const activeWorkspace = selectStickyOpenworkPortWorkspace(requestedWorkspacePaths, workspacePaths); const portSelection = await resolveOpenworkPort(host, activeWorkspace, currentPort); const tokens = await loadServerCredentials(); // One call: resolve config, spawn managed OpenCode, start HTTP server. // Dev must prefer apps/server/dist; build output also stages a packaged // copy under apps/desktop/server for electron-builder. const devPath = path.resolve(__runtimeDir, "..", "..", "server", "dist", "embedded.js"); const packagedPaths = [ path.resolve(__runtimeDir, "..", "server", "dist", "embedded.js"), ...(process.resourcesPath ? [path.resolve(process.resourcesPath, "server", "dist", "embedded.js")] : []), ]; const candidates = process.env.OPENWORK_DEV_MODE === "1" ? [devPath, ...packagedPaths] : [...packagedPaths, devPath]; const embeddedPath = candidates.find((candidate) => existsSync(candidate)); if (!embeddedPath) { throw new Error(`Cannot find OpenWork embedded server bundle. Checked: ${candidates.join(", ")}`); } const { startEmbeddedServer } = await import(embeddedServerImportUrl(embeddedPath)); // startEmbeddedServer falls back to an OS-assigned port if `port` races // into EADDRINUSE (see apps/server/src/serve-node.ts), so the bound port // below is authoritative. const handle = await startEmbeddedServer({ host, port: portSelection.port, corsOrigins: ["*"], approvalMode: "auto", configPath: serverConfigPath, workspaces: workspacePaths, token: tokens.clientToken, hostToken: tokens.hostToken, opencodeBaseUrl: options.opencodeBaseUrl ?? undefined, opencodeDirectory: activeWorkspace || undefined, manageOpencode: options.manageOpencode === true, opencodeBin: managedOpencode?.path ?? undefined, opencodeCwd: managedOpencodeWorkdir(), localManagedMcpVaultKey, }); inProcessServer = handle; openworkServerState.managedOpencodeExecution = handle.managedOpencodeExecution ?? null; engineState.managedByServer = Boolean(handle.managedOpencode); engineState.managedPid = handle.managedOpencode?.pid ?? null; engineState.managedIsAlive = handle.managedOpencode?.isAlive ?? null; const boundPort = handle.port; const baseUrl = handle.url; openworkServerState.inProcess = true; openworkServerGenerationCounter += 1; openworkServerState.generation = openworkServerGenerationCounter; openworkServerState.remoteAccessEnabled = options.remoteAccessEnabled; openworkServerState.host = host; openworkServerState.port = boundPort; openworkServerState.baseUrl = baseUrl; openworkServerState.clientToken = tokens.clientToken; openworkServerState.hostToken = tokens.hostToken; const connectUrls = options.remoteAccessEnabled ? buildConnectUrls(boundPort) : { connectUrl: null, mdnsUrl: null, lanUrl: null }; openworkServerState.connectUrl = connectUrls.connectUrl; openworkServerState.mdnsUrl = connectUrls.mdnsUrl; openworkServerState.lanUrl = connectUrls.lanUrl; // No health check needed -- startServer() resolves only after the listener is bound. let workspaceList = null; let ownerToken = tokens.ownerToken?.trim() || null; if (ownerToken) { try { workspaceList = await fetchJson(`${baseUrl}/workspaces`, { headers: { Authorization: `Bearer ${ownerToken}` }, }, 5000); } catch { ownerToken = null; } } ownerToken ||= await issueOwnerToken(baseUrl, tokens.hostToken); openworkServerState.ownerToken = ownerToken; if (ownerToken) { await persistServerOwnerToken(ownerToken); } if (ownerToken) { try { const list = workspaceList ?? await fetchJson(`${baseUrl}/workspaces`, { headers: { Authorization: `Bearer ${ownerToken}` }, }, 5000); const first = Array.isArray(list?.items) ? list.items[0] : undefined; const opencode = first?.opencode; if (opencode?.baseUrl) { engineState.runtime = DIRECT_RUNTIME; engineState.projectDir = opencode.directory ?? activeWorkspace ?? null; engineState.hostname = new URL(opencode.baseUrl).hostname; engineState.port = Number(new URL(opencode.baseUrl).port) || null; engineState.baseUrl = opencode.baseUrl; engineState.opencodeUsername = opencode.username ?? null; engineState.opencodePassword = opencode.password ?? null; engineState.execution = handle.managedOpencodeExecution ?? null; engineState.child = null; engineState.childExited = false; } } catch (error) { appendOutput(openworkServerState, "lastStderr", `OpenWork server workspace probe: ${error instanceof Error ? error.message : String(error)}\n`); } } if (!portSelection.preferredPort || boundPort === portSelection.preferredPort) { await persistPreferredOpenworkPort(activeWorkspace, boundPort); } return snapshotOpenworkServerState(openworkServerState); } async function stopAllRuntimeChildren() { // Stop the in-process server (and its managed OpenCode child) if running. if (inProcessServer) { try { await inProcessServer.stop(); } catch { /* ignore */ } inProcessServer = null; } await stopChild(openworkServerState); await stopChild(engineState); Object.assign(engineState, createEngineState()); Object.assign(openworkServerState, createOpenworkServerState()); } async function prepareFreshRuntime() { lifecycleState = "cleaning"; await stopAllRuntimeChildren(); await cleanupPackagedSidecars(); lifecycleState = "idle"; } function settleAfterWorkspacePreparationFailure() { if (snapshotOpenworkServerState(openworkServerState).running) { lifecycleState = "healthy"; return; } Object.assign(engineState, createEngineState()); Object.assign(openworkServerState, createOpenworkServerState()); lifecycleState = "idle"; } async function ensureOpenwork(options) { let openworkServer; try { openworkServer = await startOpenworkServer({ workspacePaths: options.workspacePaths, opencodeBaseUrl: engineState.baseUrl, opencodeUsername: engineState.opencodeUsername, opencodePassword: engineState.opencodePassword, remoteAccessEnabled: options.remoteAccessEnabled, manageOpencode: options.manageOpencode === true, opencodeBinPath: options.opencodeBinPath, }); } catch (error) { appendOutput(engineState, "lastStderr", `OpenWork server: ${error instanceof Error ? error.message : String(error)}\n`); throw error; } assertOpenworkServerReady(openworkServer); } async function engineStart(projectDir, options = {}) { const rawProjectDir = String(projectDir ?? "").trim(); if (!rawProjectDir) { throw new Error("projectDir is required"); } let safeProjectDir; try { safeProjectDir = normalizeWorkspaceRootPath(rawProjectDir, { platform: workspacePlatform }); } catch (error) { settleAfterWorkspacePreparationFailure(); throw workspaceInaccessibleError(rawProjectDir, error); } // Reuse a healthy server instead of tearing it down. During boot the // main process kicks off bootRuntimeForSelectedWorkspace while renderer // routes independently call ensureDesktopLocalOpenworkConnection. Both go // through this serialized path; without this guard the second call runs // prepareFreshRuntime (killing the freshly bound server) and then rebinds // the sticky preferred port, racing the not-yet-released socket into // EADDRINUSE and leaving the runtime in error -> boot screen. // resolveOpenworkServerReuse also spans workspace switches: requesting a // different projectDir retargets the running runtime instead of killing // the process and every in-flight run with it. const reuseDecision = resolveOpenworkServerReuse({ forceRestart: options.forceRestart, inProcess: openworkServerState.inProcess, lifecycleState, remoteAccessEnabled: openworkServerState.remoteAccessEnabled, requestedRemoteAccess: options.openworkRemoteAccess, currentProjectDir: engineState.projectDir, requestedProjectDir: safeProjectDir, platform: workspacePlatform, }); if (reuseDecision.reuse) { const existing = snapshotOpenworkServerState(openworkServerState); if (existing.running && existing.baseUrl && (existing.ownerToken || existing.clientToken)) { if (reuseDecision.retarget) { try { safeProjectDir = await prepareRuntimeWorkspaceRoot(safeProjectDir, { platform: workspacePlatform, mkdirImpl: workspaceMkdir, ensureConfig: ensureOpencodeConfig, }); } catch (error) { settleAfterWorkspacePreparationFailure(); throw error; } engineState.projectDir = safeProjectDir; await persistPreferredOpenworkPort(safeProjectDir, openworkServerState.port); } return snapshotEngineState(engineState); } } lifecycleState = "starting"; try { safeProjectDir = await prepareRuntimeWorkspaceRoot(safeProjectDir, { platform: workspacePlatform, mkdirImpl: workspaceMkdir, ensureConfig: ensureOpencodeConfig, }); } catch (error) { settleAfterWorkspacePreparationFailure(); throw error; } await prepareFreshRuntime(); const workspacePaths = prioritizeWorkspacePaths(safeProjectDir, options.workspacePaths, { platform: workspacePlatform, }); const runtime = DIRECT_RUNTIME; try { lifecycleState = "starting"; engineState.runtime = runtime; engineState.projectDir = safeProjectDir; engineState.child = null; engineState.childExited = true; await ensureOpenwork({ projectDir: safeProjectDir, workspacePaths, remoteAccessEnabled: options.openworkRemoteAccess === true, manageOpencode: true, opencodeBinPath: options.opencodeBinPath, }); lifecycleState = "healthy"; return snapshotEngineState(engineState); } catch (error) { lifecycleState = "error"; throw error; } } async function engineStop() { lifecycleState = "stopping"; await stopAllRuntimeChildren(); lifecycleState = "idle"; return snapshotEngineState(engineState); } async function engineRestart(options = {}) { const projectDir = engineState.projectDir; if (!projectDir) { throw new Error("OpenCode is not configured for a local workspace"); } const openworkRemoteAccess = typeof options.openworkRemoteAccess === "boolean" ? options.openworkRemoteAccess : openworkServerState.remoteAccessEnabled; return engineStart(projectDir, { runtime: engineState.runtime, workspacePaths: [projectDir], opencodeEnableExa: options.opencodeEnableExa, openworkRemoteAccess, forceRestart: true, }); } async function engineInfo() { if (inProcessServer?.managedOpencode) { engineState.managedPid = inProcessServer.managedOpencode.pid ?? null; } return { ...snapshotEngineState(engineState), lifecycleState }; } async function runtimeStatus() { return { lifecycleState, engine: await engineInfo(), enginePool: inProcessServer?.managedOpencodePool?.() ?? null, openworkServer: snapshotOpenworkServerState(openworkServerState), }; } async function openworkServerInfo() { return snapshotOpenworkServerState(openworkServerState); } async function openworkServerRestart(options = {}) { const workspacePaths = prioritizeWorkspacePaths(engineState.projectDir, await listLocalWorkspacePaths(), { platform: workspacePlatform, }); const shouldManageOpencode = Boolean( openworkServerState.managedOpencodeBinPath || engineState.opencodeBinPath || !engineState.baseUrl, ); return startOpenworkServer({ workspacePaths, opencodeBaseUrl: shouldManageOpencode ? null : engineState.baseUrl, opencodeUsername: shouldManageOpencode ? null : engineState.opencodeUsername, opencodePassword: shouldManageOpencode ? null : engineState.opencodePassword, remoteAccessEnabled: options.remoteAccessEnabled === true, manageOpencode: shouldManageOpencode, opencodeBinPath: engineState.opencodeBinPath ?? openworkServerState.managedOpencodeBinPath, }); } async function engineInstall() { if (process.platform === "win32") { return { ok: false, status: -1, stdout: "", stderr: "Guided install is not supported on Windows yet. Install the OpenWork-pinned OpenCode version manually, then restart OpenWork.", }; } const installDir = path.join(app.getPath("home"), ".opencode", "bin"); const command = await pinnedOpencodeInstallCommand(); const result = await runShellCommand("bash", ["-lc", command], { env: { ...(await buildChildEnv()), OPENCODE_INSTALL_DIR: installDir }, timeoutMs: 180_000, }); return { ok: result.status === 0, status: result.status, stdout: result.stdout, stderr: result.stderr, }; } async function opencodeMcpAuth(projectDir, serverName) { const safeProjectDir = String(projectDir ?? "").trim(); const safeServerName = String(serverName ?? "").trim(); if (!safeProjectDir) { throw new Error("project_dir is required"); } if (!safeServerName) { throw new Error("server_name is required"); } const program = resolveBinary("opencode"); if (!program) { throw new Error("Failed to locate opencode."); } const result = await runShellCommand(program, ["mcp", "auth", safeServerName], { cwd: safeProjectDir, env: await buildChildEnv(), timeoutMs: 120_000, }); return { ok: result.status === 0, status: result.status, stdout: result.stdout, stderr: result.stderr, }; } async function sandboxCleanupOpenworkContainers() { const candidates = await listOpenworkManagedContainers().catch((error) => { throw error; }); const removed = []; const errors = []; for (const name of candidates) { try { const result = runDockerCommandDetailed(["rm", "-f", name], 20_000); if (result.status === 0) { removed.push(name); } else { errors.push(`${name}: exit ${result.status}: ${(result.stdout + "\n" + result.stderr).trim()}`); } } catch (error) { errors.push(`${name}: ${error instanceof Error ? error.message : String(error)}`); } } return { candidates, removed, errors }; } return { systemCaCertificates: async () => (await systemCa()).trustedCertificates, engineStart: (projectDir, options) => withRuntimeLifecycle(() => engineStart(projectDir, options)), engineStop: () => withRuntimeLifecycle(() => engineStop()), engineRestart: (options) => withRuntimeLifecycle(() => engineRestart(options)), prepareFreshRuntime: () => withRuntimeLifecycle(() => prepareFreshRuntime()), dispose: () => withRuntimeLifecycle(() => stopAllRuntimeChildren()), runtimeStatus, engineInfo, engineDoctor, engineInstall, openworkServerInfo, openworkServerRestart: (options) => withRuntimeLifecycle(() => openworkServerRestart(options)), opencodeMcpAuth, sandboxCleanupOpenworkContainers, }; }