import { spawn } from "node:child_process"; import { randomUUID } from "node:crypto"; import { accessSync, constants as fsConstants, existsSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; export const CODEX_STDIO_PROVIDER = "codex-stdio"; export const CODEX_STDIO_BACKEND = "hwlab-cloud-api/codex-mcp-stdio"; export const CODEX_STDIO_RUNNER_KIND = "codex-mcp-stdio-runner"; export const CODEX_STDIO_SESSION_MODE = "codex-mcp-stdio-long-lived"; export const CODEX_STDIO_IMPLEMENTATION_TYPE = "repo-owned-codex-mcp-stdio-session"; export const CODEX_STDIO_CAPABILITY_LEVEL = "long-lived-codex-stdio-session"; export const CODEX_STDIO_SANDBOX = "workspace-write"; export const DEFAULT_CODEX_STDIO_IDLE_TIMEOUT_MS = 30 * 60 * 1000; export const DEFAULT_CODEX_STDIO_MAX_SESSIONS = 64; export const DEFAULT_CODEX_STDIO_COMMAND = "codex"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const MCP_PROTOCOL_VERSION = "2024-11-05"; const CODEX_STDIO_REQUIRED_TOOLS = Object.freeze(["codex", "codex-reply"]); const CODEX_STDIO_BOUNDARY_INSTRUCTIONS = [ "You are the HWLAB Cloud Workbench Code Agent.", "Use the provided workspace and repo-owned Codex stdio session only.", "Do not read or print secrets, tokens, kubeconfig files, DB URLs, private keys, or raw environment values.", "Do not call gateway, box-simu, or patch-panel directly.", "Hardware control requests must go through cloud-api/HWLAB API/skill CLI controlled paths.", "Do not claim M3, M4, or M5 acceptance unless the response includes live HWLAB evidence." ].join("\n"); export function createCodexStdioSessionManager(options = {}) { const idleTimeoutMs = positiveInteger(options.idleTimeoutMs, DEFAULT_CODEX_STDIO_IDLE_TIMEOUT_MS); const maxSessions = positiveInteger(options.maxSessions, DEFAULT_CODEX_STDIO_MAX_SESSIONS); const idFactory = typeof options.idFactory === "function" ? options.idFactory : () => `ses_${randomUUID()}`; const nowDefault = options.now; const sessions = new Map(); const conversations = new Map(); let rpcClient = options.rpcClient ?? null; let rpcStartedAt = null; let rpcToolNames = null; function describe(params = {}) { const env = params.env ?? options.env ?? process.env; const workspace = resolveCodexWorkspace(env, params); const command = resolveCodexCommand(env, params, options); const sandbox = resolveCodexSandbox(env, params); const enabled = codexStdioEnabled(env, params, options); const supervisor = supervisorState(env, params, options, enabled); const tokenBoundary = tokenBoundaryState(env); const binaryOnPath = commandOnPathSync(command, env); const workspaceInfo = workspaceStateSync(workspace, sandbox); const egress = egressState(env); const blockers = []; if (!enabled || supervisor.configured !== true) { blockers.push({ code: "codex_stdio_supervisor_disabled", sourceIssue: "pikasTech/HWLAB#275", summary: "Repo-owned Codex stdio supervisor is not enabled for this runtime." }); } if (!binaryOnPath) { blockers.push({ code: "codex_cli_binary_missing", sourceIssue: "pikasTech/HWLAB#275", summary: `Codex CLI command ${command} is not present on the runtime PATH.` }); } if (!workspaceInfo.exists || !workspaceInfo.readable) { blockers.push({ code: "workspace_mount_missing", sourceIssue: "pikasTech/HWLAB#275", summary: `Configured Codex workspace is not mounted/readable: ${workspace}.` }); } if (sandbox === "workspace-write" && workspaceInfo.writable !== true) { blockers.push({ code: "workspace_write_boundary_blocked", sourceIssue: "pikasTech/HWLAB#275", summary: `Configured Codex workspace is not writable for workspace-write sandbox: ${workspace}.` }); } if (!tokenBoundary.present) { blockers.push({ code: "provider_token_boundary", sourceIssue: "pikasTech/HWLAB#275", summary: "A long-lived Codex stdio runner needs a repo-owned provider token boundary; this endpoint only reports token presence and never reads or prints token material." }); } if (egress.directPublicOpenAi) { blockers.push({ code: "codex_stdio_egress_boundary", sourceIssue: "pikasTech/HWLAB#275", summary: "Codex stdio provider egress must use the DEV egress/proxy boundary, not direct public api.openai.com." }); } const ready = blockers.length === 0; return { kind: CODEX_STDIO_RUNNER_KIND, provider: CODEX_STDIO_PROVIDER, backend: CODEX_STDIO_BACKEND, status: ready ? "feasible" : "blocked", ready, canStartLongLivedCodexStdio: ready, command, binaryOnPath, workspace, workspaceState: workspaceInfo, sandbox, enabled, supervisor, tokenBoundary, egress, protocol: { transport: "stdio", command: `${command} mcp-server`, protocolVersion: MCP_PROTOCOL_VERSION, adapter: "Model Context Protocol tools/call", requiredTools: [...CODEX_STDIO_REQUIRED_TOOLS], toolsObserved: rpcToolNames ? [...rpcToolNames] : [] }, sessionLifecycle: { implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, create: true, reuse: true, cancel: true, reap: true, idleTimeout: true, idleTimeoutMs, traceCapture: true, maxSessions, activeSessions: sessions.size, inMemoryIndex: true, codexThreadIdCaptured: true, valuesRedacted: true }, blockers, blockerCodes: blockers.map((blocker) => blocker.code), safety: codexStdioSafety() }; } async function chat(params = {}) { const env = params.env ?? options.env ?? process.env; const now = params.now ?? nowDefault; const timestamp = timestampFor(now); const traceId = optionalId(params.traceId) ?? `trc_${randomUUID()}`; const conversationId = requiredId(params.conversationId, "cnv"); const workspace = resolveCodexWorkspace(env, params); const sandbox = resolveCodexSandbox(env, params); const availability = describe({ ...params, env, workspace, sandbox }); if (!availability.ready) { throw codexStdioError("codex_stdio_blocked", "Codex stdio session is blocked by runtime readiness gates.", { availability, blockers: availability.blockers }); } const acquire = await acquireSession({ conversationId, requestedSessionId: params.sessionId, traceId, workspace, sandbox, now }); if (!acquire.ok) { throw codexStdioError(acquire.code, acquire.message, { availability, session: acquire.session, blockers: [acquire.blocker] }); } let session = acquire.session; const events = [ "stdio:acquire", session.reused ? "session:reused" : "session:created", session.threadId ? "codex-thread:reused" : "codex-thread:create", `turn:${session.turn}` ]; const startedAt = timestamp; try { const client = await ensureRpcClient({ env, availability, timeoutMs: params.timeoutMs }); events.push("stdio:ready"); const toolName = session.threadId ? "codex-reply" : "codex"; const toolArguments = session.threadId ? { threadId: session.threadId, prompt: buildCodexUserPrompt(params.message, { conversationId, traceId }) } : { prompt: buildCodexUserPrompt(params.message, { conversationId, traceId }), cwd: workspace, sandbox, model: params.model, "approval-policy": "never", "developer-instructions": CODEX_STDIO_BOUNDARY_INSTRUCTIONS }; const toolResult = await client.callTool(toolName, dropEmpty(toolArguments), effectiveTimeout(params.timeoutMs)); const codexOutput = extractCodexToolOutput(toolResult); if (!codexOutput.content) { throw codexStdioError("codex_stdio_empty_response", "Codex stdio tool call completed without assistant content.", { availability, session }); } session = releaseSession(session.sessionId, { now, traceId, conversationId, reused: session.reused, threadId: codexOutput.threadId ?? session.threadId, status: "idle" }) ?? session; events.push(`tool:${toolName}:completed`); return { provider: CODEX_STDIO_PROVIDER, model: firstNonEmpty(params.model, env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL, "codex-default"), backend: CODEX_STDIO_BACKEND, content: codexOutput.content, workspace, sandbox, session, sessionMode: CODEX_STDIO_SESSION_MODE, sessionReuse: sessionReuseEvidence(session), implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"], codexStdioFeasibility: availability, longLivedSessionGate: longLivedSessionGate({ provider: CODEX_STDIO_PROVIDER, runnerKind: CODEX_STDIO_RUNNER_KIND, session, sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, codexStdioFeasibility: availability }), toolCalls: [{ id: `tool_${randomUUID()}`, type: "codex-stdio", name: toolName, status: "completed", cwd: workspace, command: "codex mcp-server tools/call", exitCode: 0, stdout: codexOutput.threadId ? `threadId=${codexOutput.threadId}` : "threadId=captured", stderrSummary: "", outputTruncated: false, traceId }], skills: { status: "not_requested", items: [], count: 0, blockers: [] }, runner: runnerDescriptor({ workspace, sandbox, session }), runnerTrace: runnerTrace({ traceId, workspace, sandbox, session, events, startedAt, outputTruncated: false }), capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL, providerTrace: { transport: "stdio", protocol: "mcp", command: `${availability.command} mcp-server`, toolName, threadId: codexOutput.threadId ?? session.threadId ?? null, valuesPrinted: false } }; } catch (error) { session = failSession(session.sessionId, { now, traceId, conversationId, reused: session.reused, statusReason: error.code ?? "codex_stdio_failed" }) ?? session; if (error.code && error.code.startsWith("codex_stdio")) { error.session = session; error.availability = error.availability ?? describe({ ...params, env, workspace, sandbox }); error.runnerTrace = runnerTrace({ traceId, workspace, sandbox, session, events: [...events, `blocked:${error.code}`], startedAt, outputTruncated: false }); throw error; } throw codexStdioError("codex_stdio_failed", redactText(error.message || "Codex stdio session failed."), { availability, session, runnerTrace: runnerTrace({ traceId, workspace, sandbox, session, events: [...events, "blocked:codex_stdio_failed"], startedAt, outputTruncated: false }) }); } } function cancel(sessionId, params = {}) { const session = sessions.get(requiredId(sessionId, "ses")) ?? null; if (!session) return null; const timestamp = timestampFor(params.now ?? nowDefault); session.status = "interrupted"; session.updatedAt = timestamp; session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId; session.currentTraceId = null; session.statusReason = params.reason ?? "cancelled"; if (rpcClient && typeof rpcClient.close === "function") { rpcClient.close(); rpcClient = null; rpcStartedAt = null; rpcToolNames = null; } return publicSession(session, { conversationId: params.conversationId, reused: true }); } function reapIdle(params = {}) { const timestamp = timestampFor(params.now ?? nowDefault); const timestampMs = Date.parse(timestamp); const reaped = []; for (const session of sessions.values()) { if (!sessionExpired(session, timestampMs)) continue; session.status = "expired"; session.updatedAt = timestamp; session.currentTraceId = null; reaped.push(session.sessionId); } if (sessionsBusyCount() === 0 && reaped.length > 0 && rpcClient && typeof rpcClient.close === "function") { rpcClient.close(); rpcClient = null; rpcStartedAt = null; rpcToolNames = null; } return { reapedCount: reaped.length, sessionIds: reaped }; } function get(sessionId, params = {}) { const session = sessions.get(requiredId(sessionId, "ses")) ?? null; return session ? publicSession(session, params) : null; } function clear() { sessions.clear(); conversations.clear(); if (rpcClient && typeof rpcClient.close === "function") rpcClient.close(); rpcClient = null; rpcStartedAt = null; rpcToolNames = null; } async function ensureRpcClient({ env, availability, timeoutMs } = {}) { if (rpcClient && typeof rpcClient.callTool === "function") return rpcClient; rpcClient = options.createRpcClient ? await options.createRpcClient({ env, availability }) : createCodexMcpJsonLineClient({ command: availability.command, env: childProcessEnv(env), cwd: availability.workspace }); if (typeof rpcClient.initialize === "function") { const init = await rpcClient.initialize(effectiveTimeout(timeoutMs)); rpcToolNames = Array.isArray(init?.tools) ? init.tools : rpcToolNames; } if (!rpcToolNames && typeof rpcClient.listTools === "function") { rpcToolNames = await rpcClient.listTools(effectiveTimeout(timeoutMs)); } const missingTools = CODEX_STDIO_REQUIRED_TOOLS.filter((name) => !rpcToolNames?.includes(name)); if (missingTools.length > 0) { throw codexStdioError("codex_stdio_protocol_blocked", `Codex stdio server did not expose required tools: ${missingTools.join(", ")}.`, { availability, missingTools }); } rpcStartedAt = timestampFor(nowDefault); return rpcClient; } async function acquireSession(params = {}) { const timestamp = timestampFor(params.now ?? nowDefault); const timestampMs = Date.parse(timestamp); const conversationId = requiredId(params.conversationId, "cnv"); const requestedSessionId = optionalId(params.requestedSessionId); const mappedSessionId = conversations.get(conversationId) ?? null; if (mappedSessionId && requestedSessionId && mappedSessionId !== requestedSessionId) { const mappedSession = sessions.get(mappedSessionId) ?? null; return blockedAcquire({ code: "session_reuse_conflict", summary: `conversationId ${conversationId} is already bound to sessionId ${mappedSessionId}; requested ${requestedSessionId}.`, session: mappedSession, timestamp, traceId: params.traceId }); } const effectiveSessionId = mappedSessionId || requestedSessionId || requiredId(idFactory(), "ses"); let session = sessions.get(effectiveSessionId) ?? null; const reused = Boolean(session); if (session && sessionExpired(session, timestampMs)) { session.status = "expired"; session.updatedAt = timestamp; session.currentTraceId = null; session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId; return blockedAcquire({ code: "session_expired", summary: `Codex stdio session ${effectiveSessionId} expired after ${session.idleTimeoutMs}ms idle timeout.`, session, timestamp, traceId: params.traceId }); } if (session?.status === "busy") { return blockedAcquire({ code: "session_busy", summary: `Codex stdio session ${effectiveSessionId} is already busy with traceId ${session.currentTraceId ?? "unknown"}.`, session, timestamp, traceId: params.traceId }); } if (!session) { session = { sessionId: effectiveSessionId, conversationIds: new Set(), status: "creating", workspace: params.workspace, sandbox: params.sandbox, runnerKind: CODEX_STDIO_RUNNER_KIND, capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, createdAt: timestamp, updatedAt: timestamp, idleTimeoutMs, expiresAt: plusMs(timestampMs, idleTimeoutMs), lastTraceId: optionalId(params.traceId), currentTraceId: null, turn: 0, threadId: null, durable: true, longLivedSession: true, codexStdio: true, writeCapable: true, secretMaterialStored: false }; sessions.set(effectiveSessionId, session); } session.conversationIds.add(conversationId); session.workspace = params.workspace ?? session.workspace; session.sandbox = params.sandbox ?? session.sandbox; session.status = "busy"; session.updatedAt = timestamp; session.expiresAt = plusMs(timestampMs, idleTimeoutMs); session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId; session.currentTraceId = optionalId(params.traceId); session.turn += 1; conversations.set(conversationId, effectiveSessionId); pruneSessions(); return { ok: true, reused, session: publicSession(session, { conversationId, reused }) }; } function releaseSession(sessionId, params = {}) { const session = sessions.get(requiredId(sessionId, "ses")) ?? null; if (!session) return null; const timestamp = timestampFor(params.now ?? nowDefault); const timestampMs = Date.parse(timestamp); session.status = params.status ?? "idle"; session.updatedAt = timestamp; session.expiresAt = plusMs(timestampMs, idleTimeoutMs); session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId; session.currentTraceId = null; session.threadId = optionalId(params.threadId) ?? session.threadId; session.statusReason = params.statusReason ?? null; return publicSession(session, { conversationId: params.conversationId, reused: params.reused }); } function failSession(sessionId, params = {}) { return releaseSession(sessionId, { ...params, status: "failed" }); } function pruneSessions() { if (sessions.size <= maxSessions) return; const sorted = [...sessions.entries()] .sort((a, b) => String(a[1].updatedAt).localeCompare(String(b[1].updatedAt))); for (const [sessionId, session] of sorted.slice(0, sessions.size - maxSessions)) { sessions.delete(sessionId); for (const conversationId of session.conversationIds) { if (conversations.get(conversationId) === sessionId) conversations.delete(conversationId); } } } function sessionsBusyCount() { return [...sessions.values()].filter((session) => session.status === "busy").length; } return { describe, chat, cancel, reapIdle, get, clear, longLivedSessionGate, get rpcStartedAt() { return rpcStartedAt; } }; } export function createCodexMcpJsonLineClient({ command = DEFAULT_CODEX_STDIO_COMMAND, env = process.env, cwd = repoRoot } = {}) { const child = spawn(command, ["mcp-server"], { cwd, env, stdio: ["pipe", "pipe", "pipe"] }); let nextId = 1; let stdoutBuffer = ""; let stderr = ""; const pending = new Map(); let initialized = false; child.stdout.on("data", (chunk) => { stdoutBuffer += chunk.toString("utf8"); let newlineIndex = stdoutBuffer.indexOf("\n"); while (newlineIndex >= 0) { const line = stdoutBuffer.slice(0, newlineIndex).trim(); stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1); if (line) handleLine(line); newlineIndex = stdoutBuffer.indexOf("\n"); } }); child.stderr.on("data", (chunk) => { stderr = tailText(`${stderr}${chunk.toString("utf8")}`, 4000); }); child.on("error", (error) => rejectAll(`Codex stdio process error: ${error.message}`)); child.on("close", (code, signal) => rejectAll(`Codex stdio process closed code=${code ?? "null"} signal=${signal ?? "null"}`)); function handleLine(line) { let payload = null; try { payload = JSON.parse(line); } catch { return; } const request = pending.get(payload.id); if (!request) return; pending.delete(payload.id); clearTimeout(request.timer); if (payload.error) { request.reject(new Error(redactText(payload.error.message ?? JSON.stringify(payload.error)))); } else { request.resolve(payload.result); } } function rejectAll(message) { for (const [id, request] of pending.entries()) { pending.delete(id); clearTimeout(request.timer); request.reject(new Error(redactText(`${message}; stderr=${tailText(stderr, 800)}`))); } } function request(method, params = {}, timeoutMs = 30000) { const id = nextId; nextId += 1; const message = JSON.stringify({ jsonrpc: "2.0", id, method, params }); return new Promise((resolve, reject) => { const timer = setTimeout(() => { pending.delete(id); reject(new Error(`Codex stdio request ${method} timed out after ${timeoutMs}ms`)); }, timeoutMs); pending.set(id, { resolve, reject, timer }); child.stdin.write(`${message}\n`, "utf8", (error) => { if (!error) return; pending.delete(id); clearTimeout(timer); reject(error); }); }); } async function initialize(timeoutMs = 30000) { if (initialized) return { tools: await listTools(timeoutMs) }; await request("initialize", { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "hwlab-cloud-api", version: "0" } }, timeoutMs); child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} })}\n`); initialized = true; return { tools: await listTools(timeoutMs) }; } async function listTools(timeoutMs = 30000) { const result = await request("tools/list", {}, timeoutMs); return Array.isArray(result?.tools) ? result.tools.map((tool) => tool?.name).filter(Boolean) : []; } async function callTool(name, args = {}, timeoutMs = 120000) { return request("tools/call", { name, arguments: args }, timeoutMs); } function close() { child.kill("SIGTERM"); } return { initialize, listTools, callTool, close }; } export function longLivedSessionGate({ provider, runnerKind, session, sessionMode, implementationType, codexStdioFeasibility } = {}) { const normalizedProvider = String(provider ?? "").trim(); const normalizedRunnerKind = String(runnerKind ?? "").trim(); const normalizedSessionMode = String(sessionMode ?? "").trim(); const normalizedImplementation = String(implementationType ?? "").trim(); const blockers = []; if (normalizedProvider === "openai-responses" || normalizedRunnerKind === "openai-responses-fallback") { blockers.push({ code: "openai_responses_fallback_not_session", sourceIssue: "pikasTech/HWLAB#317", summary: "OpenAI Responses fallback is text-chat-only and cannot pass the long-lived Codex session gate." }); } if (normalizedRunnerKind === "codex-cli-one-shot-ephemeral" || normalizedSessionMode === "ephemeral-one-shot") { blockers.push({ code: "one_shot_runner_not_long_lived", sourceIssue: "pikasTech/HWLAB#317", summary: "codex exec --ephemeral / one-shot runner output is not a reusable long-lived runner session." }); } if (normalizedImplementation === "controlled-readonly-session-registry") { blockers.push({ code: "controlled_readonly_not_long_lived_stdio", sourceIssue: "pikasTech/HWLAB#317", summary: "This response is backed by the controlled read-only session registry, not Codex stdio or an equivalent long-lived protocol adapter." }); } if (session && !["idle", "ready", "busy"].includes(session.status)) { blockers.push({ code: `session_${session.status || "inactive"}`, sourceIssue: "pikasTech/HWLAB#317", summary: `Long-lived Codex stdio session is not active: status=${session.status || "unknown"}.` }); } for (const blocker of codexStdioFeasibility?.blockers ?? []) { if (!blocker?.code || blockers.some((item) => item.code === blocker.code)) continue; blockers.push({ code: blocker.code, sourceIssue: blocker.sourceIssue ?? "pikasTech/HWLAB#317", summary: blocker.summary ?? `Codex stdio feasibility blocker: ${blocker.code}.` }); } const feasible = codexStdioFeasibility?.canStartLongLivedCodexStdio === true || codexStdioFeasibility?.ready === true; const sessionPass = normalizedProvider === CODEX_STDIO_PROVIDER && normalizedRunnerKind === CODEX_STDIO_RUNNER_KIND && normalizedSessionMode === CODEX_STDIO_SESSION_MODE && normalizedImplementation === CODEX_STDIO_IMPLEMENTATION_TYPE && session?.longLivedSession === true && session?.codexStdio === true && session?.writeCapable === true && session?.durable === true && ["idle", "ready", "busy"].includes(session?.status) && feasible && blockers.length === 0; const feasiblePass = normalizedProvider === CODEX_STDIO_PROVIDER && normalizedRunnerKind === CODEX_STDIO_RUNNER_KIND && normalizedSessionMode === CODEX_STDIO_SESSION_MODE && normalizedImplementation === CODEX_STDIO_IMPLEMENTATION_TYPE && !session && feasible && blockers.length === 0; const pass = sessionPass || feasiblePass; return { status: pass ? "pass" : "blocked", pass, requiredCapability: "long-lived-codex-stdio-session", currentCapability: normalizedImplementation || normalizedSessionMode || normalizedRunnerKind || "unknown", sessionId: session?.sessionId ?? null, sessionStatus: session?.status ?? null, runnerKind: normalizedRunnerKind || null, provider: normalizedProvider || null, sessionMode: normalizedSessionMode || null, implementationType: normalizedImplementation || null, blockers, sourceIssue: "pikasTech/HWLAB#317", summary: pass ? "Long-lived Codex stdio/session gate passed." : "Long-lived Codex stdio/session gate remains blocked; current response must not be reported as full Code Agent completion." }; } function publicSession(session, { conversationId = null, reused = null } = {}) { const conversationIds = [...session.conversationIds]; return { sessionId: session.sessionId, conversationId: conversationId ?? conversationIds[0] ?? null, conversationIds, status: session.status, workspace: session.workspace, sandbox: session.sandbox, runnerKind: session.runnerKind, capabilityLevel: session.capabilityLevel, implementationType: session.implementationType, createdAt: session.createdAt, updatedAt: session.updatedAt, idleTimeoutMs: session.idleTimeoutMs, expiresAt: session.expiresAt, lastTraceId: session.lastTraceId, currentTraceId: session.currentTraceId, turn: session.turn, threadId: session.threadId ?? null, reused: reused === null ? undefined : Boolean(reused), durable: session.durable === true, longLivedSession: session.longLivedSession === true, codexStdio: session.codexStdio === true, writeCapable: session.writeCapable === true, secretMaterialStored: false, valuesRedacted: true, ...(session.statusReason ? { statusReason: session.statusReason } : {}) }; } function blockedAcquire({ code, summary, session, timestamp, traceId }) { const publicEvidence = session ? publicSession(session, { timestamp, reused: true }) : { status: "failed", updatedAt: timestamp, lastTraceId: optionalId(traceId), secretMaterialStored: false, valuesRedacted: true }; return { ok: false, code, message: summary, session: publicEvidence, blocker: { code, sourceIssue: "pikasTech/HWLAB#317", summary } }; } function runnerDescriptor({ workspace, sandbox, session }) { return { kind: CODEX_STDIO_RUNNER_KIND, provider: CODEX_STDIO_PROVIDER, backend: CODEX_STDIO_BACKEND, workspace, sandbox, session: CODEX_STDIO_SESSION_MODE, sessionMode: CODEX_STDIO_SESSION_MODE, sessionId: session?.sessionId ?? null, turn: session?.turn ?? null, sessionReused: session?.reused ?? false, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, codexStdio: true, longLivedSession: true, durableSession: true, writeCapable: true, readOnly: false, capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL, toolPolicy: { allowed: ["codex", "codex-reply", "workspace-read", "workspace-write"], blocked: ["secret-read", "kubeconfig-read", "gateway-direct-call", "box-simu-direct-call", "patch-panel-direct-call", "M3/M4/M5-acceptance-claim-without-evidence"] }, runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"], safety: codexStdioSafety() }; } function runnerTrace({ traceId, workspace, sandbox, session, events, startedAt, outputTruncated }) { return { traceId, runnerKind: CODEX_STDIO_RUNNER_KIND, workspace, sandbox, sessionMode: CODEX_STDIO_SESSION_MODE, sessionId: session?.sessionId ?? null, sessionStatus: session?.status ?? null, idleTimeoutMs: session?.idleTimeoutMs ?? null, lastTraceId: session?.lastTraceId ?? null, turn: session?.turn ?? null, sessionReused: session?.reused ?? false, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, limitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"], startedAt, finishedAt: timestampFor(), events, outputTruncated: Boolean(outputTruncated), valuesPrinted: false, note: "Repo-owned Codex MCP stdio supervisor manages create/reuse/cancel/reap/idle timeout and trace capture; hardware control remains routed through cloud-api/HWLAB API/skill CLI boundaries." }; } function sessionReuseEvidence(session) { return { conversationId: session.conversationId, sessionId: session.sessionId, threadId: session.threadId, mapped: true, reused: session.reused, turn: session.turn, previousTurns: Math.max(0, session.turn - 1), workspace: session.workspace, createdAt: session.createdAt, updatedAt: session.updatedAt, idleTimeoutMs: session.idleTimeoutMs, expiresAt: session.expiresAt, lastTraceId: session.lastTraceId, status: session.status }; } function extractCodexToolOutput(toolResult) { const direct = toolResult?.structuredContent ?? toolResult?.output ?? toolResult; const directThreadId = optionalId(direct?.threadId ?? direct?.conversationId); const directContent = firstNonEmpty(direct?.content, direct?.message, direct?.text); if (directThreadId || directContent) { return { threadId: directThreadId, content: redactText(directContent) }; } for (const item of toolResult?.content ?? []) { if (typeof item?.text !== "string") continue; const parsed = parseJsonOrNull(item.text); if (parsed) { const threadId = optionalId(parsed.threadId ?? parsed.conversationId); const content = firstNonEmpty(parsed.content, parsed.message, parsed.text); if (threadId || content) { return { threadId, content: redactText(content) }; } } if (item.text.trim()) { return { threadId: null, content: redactText(item.text.trim()) }; } } return { threadId: null, content: "" }; } function buildCodexUserPrompt(message, { conversationId, traceId }) { return [ `conversationId: ${conversationId}`, `traceId: ${traceId}`, "", "User message:", String(message ?? "").trim() ].join("\n"); } function resolveCodexWorkspace(env = process.env, options = {}) { return path.resolve(firstNonEmpty( options.workspace, env.HWLAB_CODE_AGENT_CODEX_WORKSPACE, env.HWLAB_CODE_AGENT_WORKSPACE, env.HWLAB_RUNNER_WORKSPACE, env.WORKSPACE, repoRoot )); } function resolveCodexCommand(env = process.env, params = {}, options = {}) { const configured = firstNonEmpty(params.command, options.command, env.HWLAB_CODE_AGENT_CODEX_COMMAND); if (configured) return configured; const nodeModulesCodex = path.join(repoRoot, "node_modules", ".bin", "codex"); if (existsSync(nodeModulesCodex)) return nodeModulesCodex; return DEFAULT_CODEX_STDIO_COMMAND; } function resolveCodexSandbox(env = process.env, options = {}) { const value = firstNonEmpty(options.sandbox, env.HWLAB_CODE_AGENT_CODEX_SANDBOX, CODEX_STDIO_SANDBOX); return ["read-only", "workspace-write"].includes(value) ? value : CODEX_STDIO_SANDBOX; } function codexStdioEnabled(env, params, options) { if (params.enabled === true || options.enabled === true) return true; return env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED === "1" || env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED === "true"; } function supervisorState(env, params, options, enabled) { const configured = params.supervisorEnabled === true || options.supervisorEnabled === true || env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR === "repo-owned" || env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR === "enabled" || (enabled && env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR !== "disabled"); return { configured, mode: configured ? "repo-owned-node-supervisor" : "disabled", processModel: "codex mcp-server over stdio", cancelSupported: true, reapSupported: true, secretMaterialStored: false, valuesRedacted: true }; } function tokenBoundaryState(env = process.env) { const present = hasEnvValue(env, "OPENAI_API_KEY") || hasEnvValue(env, "CODEX_API_KEY") || hasEnvValue(env, "CODEX_HOME") || env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY === "configured" || env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY === "present"; return { present, sources: [ hasEnvValue(env, "OPENAI_API_KEY") ? "OPENAI_API_KEY" : null, hasEnvValue(env, "CODEX_API_KEY") ? "CODEX_API_KEY" : null, hasEnvValue(env, "CODEX_HOME") ? "CODEX_HOME" : null, env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY ? "HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY" : null ].filter(Boolean), secretMaterialRead: false, secretValuesPrinted: false, valuesRedacted: true }; } function egressState(env = process.env) { const baseUrl = firstNonEmpty(env.HWLAB_CODE_AGENT_OPENAI_BASE_URL, env.OPENAI_BASE_URL); const directPublicOpenAi = /^https:\/\/api\.openai\.com\/v1\/?/u.test(baseUrl); return { configured: Boolean(baseUrl), directPublicOpenAi, valueRedacted: true }; } function workspaceStateSync(workspace, sandbox) { const state = { exists: false, readable: false, writable: false, writeRequired: sandbox === "workspace-write" }; try { state.exists = existsSync(workspace); if (!state.exists) return state; state.readable = accessSyncBoolean(workspace, fsConstants.R_OK); state.writable = accessSyncBoolean(workspace, fsConstants.W_OK); return state; } catch { return state; } } function accessSyncBoolean(target, mode) { try { accessSync(target, mode); return true; } catch { return false; } } function commandOnPathSync(command, env = process.env) { if (!command) return false; if (command.includes("/") || command.includes("\\")) { return existsSync(command); } const paths = String(Object.hasOwn(env, "PATH") ? env.PATH : process.env.PATH || "").split(path.delimiter).filter(Boolean); return paths.some((dir) => existsSync(path.join(dir, command))); } function childProcessEnv(env = process.env) { return { PATH: Object.hasOwn(env, "PATH") ? env.PATH : process.env.PATH || "/usr/local/bin:/usr/bin:/bin", HOME: env.HOME || process.env.HOME || os.homedir(), ...(env.CODEX_HOME ? { CODEX_HOME: env.CODEX_HOME } : {}), ...(env.OPENAI_API_KEY ? { OPENAI_API_KEY: env.OPENAI_API_KEY } : {}), ...(env.CODEX_API_KEY ? { CODEX_API_KEY: env.CODEX_API_KEY } : {}), ...(env.HWLAB_CODE_AGENT_OPENAI_BASE_URL ? { OPENAI_BASE_URL: env.HWLAB_CODE_AGENT_OPENAI_BASE_URL } : {}), ...(env.OPENAI_BASE_URL ? { OPENAI_BASE_URL: env.OPENAI_BASE_URL } : {}) }; } function codexStdioSafety() { return { secretsRead: false, secretValuesPrinted: false, kubeconfigRead: false, prodTouched: false, hardwareWritesAllowed: false, directGatewayCallsAllowed: false, directBoxSimuCallsAllowed: false, directPatchPanelCallsAllowed: false, hardwareControlPath: "cloud-api/HWLAB API/skill CLI only", valuesRedacted: true }; } function codexStdioError(code, message, details = {}) { const error = new Error(message); error.code = code; Object.assign(error, { provider: CODEX_STDIO_PROVIDER, backend: CODEX_STDIO_BACKEND, capabilityLevel: "blocked", ...details }); return error; } function sessionExpired(session, timestampMs) { if (!Number.isFinite(timestampMs) || session.status === "expired") return session.status === "expired"; return Date.parse(session.expiresAt) <= timestampMs; } function timestampFor(now) { const value = typeof now === "function" ? now() : now; if (typeof value === "string" && !Number.isNaN(Date.parse(value))) return value; if (value instanceof Date && !Number.isNaN(value.valueOf())) return value.toISOString(); return new Date().toISOString(); } function plusMs(timestampMs, offsetMs) { return new Date(timestampMs + offsetMs).toISOString(); } function requiredId(value, fallbackPrefix) { const id = optionalId(value); if (id) return id; return `${fallbackPrefix}_${randomUUID()}`; } function optionalId(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function positiveInteger(value, fallback) { return Number.isInteger(value) && value > 0 ? value : fallback; } function firstNonEmpty(...values) { for (const value of values) { if (typeof value === "string" && value.trim()) return value.trim(); } return ""; } function hasEnvValue(env, name) { return typeof env?.[name] === "string" && env[name].trim().length > 0; } function effectiveTimeout(timeoutMs) { return Number.isInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs : 120000; } function dropEmpty(value) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== "")); } function parseJsonOrNull(value) { try { return JSON.parse(value); } catch { return null; } } function tailText(value, maxLength = 1200) { const text = String(value ?? "").trim(); if (text.length <= maxLength) return text; return text.slice(text.length - maxLength); } function redactText(value) { return String(value ?? "") .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gu, "Bearer ***") .replace(/sk-[A-Za-z0-9._-]+/gu, "sk-***") .replace(/([A-Za-z0-9_]*TOKEN[A-Za-z0-9_]*=)[^\s]+/giu, "$1***") .replace(/([A-Za-z0-9_]*KEY[A-Za-z0-9_]*=)[^\s]+/giu, "$1***"); }