import { spawn, spawnSync } from "node:child_process"; import { randomUUID } from "node:crypto"; import { lookup as dnsLookup } from "node:dns/promises"; import { accessSync, constants as fsConstants, existsSync, realpathSync, statSync } from "node:fs"; import { mkdir, readdir, readFile, rm, rmdir, stat, writeFile } from "node:fs/promises"; import { isIP } from "node:net"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { createCodeAgentTraceRecorder, defaultCodeAgentTraceStore, runnerTraceFromSnapshot } from "./code-agent-trace-store.mjs"; export const CODEX_STDIO_PROVIDER = "codex-stdio"; export const CODEX_STDIO_BACKEND = "hwlab-cloud-api/codex-app-server-stdio"; export const CODEX_STDIO_RUNNER_KIND = "codex-app-server-stdio-runner"; export const CODEX_STDIO_SESSION_MODE = "codex-app-server-stdio-long-lived"; export const CODEX_STDIO_IMPLEMENTATION_TYPE = "repo-owned-codex-app-server-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"; export const DEFAULT_CODEX_STDIO_PROBE_TTL_MS = 30 * 1000; const DEFAULT_CODEX_STDIO_TURN_ACTIVITY_TIMEOUT_MS = 150000; const DEFAULT_CODEX_STDIO_TURN_HARD_TIMEOUT_MS = 10 * 60 * 1000; export const CODEX_STDIO_SESSION_STATUSES = Object.freeze([ "creating", "ready", "busy", "idle", "timeout", "error", "canceled", "interrupted", "expired", "failed" ]); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const CODEX_APP_SERVER_PROTOCOL = "codex-app-server-jsonrpc-stdio"; const CODEX_APP_SERVER_WIRE_API = "responses"; const CODEX_APP_SERVER_REQUIRED_METHODS = Object.freeze(["initialize", "thread/start", "thread/resume", "turn/start"]); const CODEX_STDIO_TOOL_OUTPUT_LIMIT = 4000; const CODEX_STDIO_SKILL_LIMIT = 40; const CODEX_STDIO_COMMAND_PROBE_TIMEOUT_MS = 3000; const CODEX_STDIO_COMMAND_PROBE_TRACE_ID = "trc_codex_stdio_command_probe"; const CODEX_STDIO_COMMAND_PROBE_CONVERSATION_ID = "cnv_codex_stdio_command_probe"; const EXTERNAL_NETWORK_TOOL_NAME = "external.network.check"; const EXTERNAL_NETWORK_DEFAULT_TIMEOUT_MS = 8000; const EXTERNAL_NETWORK_DNS_TIMEOUT_MS = 1500; const EXTERNAL_NETWORK_HTTP_METHOD = "HEAD"; const CODEX_CHILD_STRIPPED_ENV_KEYS = Object.freeze([ "OPENAI_API_KEY", "CODEX_API_KEY", "HWLAB_CODE_AGENT_OPENAI_BASE_URL", "OPENAI_BASE_URL", "OPENAI_API_BASE", "OPENAI_RESPONSES_URL", "CODEX_OPENAI_BASE_URL", "CODEX_BASE_URL", "HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy" ]); const CODEX_CHILD_NO_PROXY_REQUIRED = Object.freeze([ "hyueapi.com", ".hyueapi.com", "hyui.com", ".hyui.com", "127.0.0.1", "localhost", "::1", "api.minimaxi.com", ".minimaxi.com" ]); 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 probeCacheTtlMs = positiveInteger(options.probeCacheTtlMs, DEFAULT_CODEX_STDIO_PROBE_TTL_MS); const idFactory = typeof options.idFactory === "function" ? options.idFactory : () => `ses_${randomUUID()}`; const nowDefault = options.now; const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const sessions = new Map(); const conversations = new Map(); let rpcClient = options.rpcClient ?? null; let rpcStartedAt = null; let rpcInitialized = false; let protocolProbe = null; let commandProbe = 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 binary = codexBinaryState(command, env); const binaryOnPath = binary.present; const workspaceInfo = workspaceStateSync(workspace, sandbox); const codexHome = resolveCodexHome(env); const codexHomeInfo = directoryStateSync(codexHome, { writableRequired: true }); const codexHomeFiles = codexHomeFilesStateSync(codexHome); const tokenBoundary = tokenBoundaryState(env, codexHomeFiles); const egress = egressState(env); const childEnvBoundary = childProcessEnvState(env); const protocol = protocolState({ command, binary, supervisor, initialized: params.protocolInitialized ?? (rpcInitialized || cachedProtocolReady({ command, workspace, codexHome, probe: protocolProbe })) }); const lifecycle = lifecycleState({ supervisor, idleTimeoutMs, maxSessions, activeSessions: sessions.size }); const blockers = []; 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.`, evidence: binary.nextEvidence }); } else if (binary.executable !== true) { blockers.push({ code: "codex_cli_not_executable", sourceIssue: "pikasTech/HWLAB#377", summary: `Codex CLI command ${command} is present but not executable or --version failed.`, evidence: binary.nextEvidence }); } else if (binary.nativeDependencyPresent !== true) { blockers.push({ code: "codex_cli_native_dependency_missing", sourceIssue: "pikasTech/HWLAB#377", summary: `Codex CLI command ${command} is present, but the native @openai/codex executable dependency was not found.`, evidence: binary.nativeDependencyEvidence }); } 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." }); blockers.push({ code: "runner_lifecycle_missing", sourceIssue: "pikasTech/HWLAB#275", summary: "Repo-owned lifecycle supervisor is not configured, so create/reuse/cancel/reap/trace readiness cannot pass." }); } if (!protocol.wired) { blockers.push({ code: "stdio_protocol_not_wired", sourceIssue: "pikasTech/HWLAB#275", summary: protocol.summary, evidence: protocol.nextEvidence }); } const commandProbeState = commandProbeSummary({ command, workspace, codexHome, probe: commandProbe }); if (commandProbeState.status === "blocked") { blockers.push({ code: "codex_stdio_command_probe_failed", sourceIssue: "pikasTech/HWLAB#275", summary: "Codex stdio command/session probe failed on the same controlled workspace tool path used by /v1/agent/chat.", evidence: commandProbeState.error }); } 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 (!codexHomeInfo.exists || !codexHomeInfo.readable) { blockers.push({ code: "codex_home_missing", sourceIssue: "pikasTech/HWLAB#377", summary: `Configured CODEX_HOME is not present/readable: ${codexHome}.` }); } if (codexHomeInfo.writable !== true) { blockers.push({ code: "codex_home_write_blocked", sourceIssue: "pikasTech/HWLAB#377", summary: `Configured CODEX_HOME is not writable for Codex session state: ${codexHome}.` }); } if (codexHomeFiles.configPresent !== true || codexHomeFiles.configReadable !== true) { blockers.push({ code: "codex_home_config_missing", sourceIssue: "pikasTech/HWLAB#426", summary: `Configured CODEX_HOME does not expose readable Codex config.toml: ${codexHomeFiles.configPath}.` }); } if (codexHomeFiles.authPresent !== true || codexHomeFiles.authReadable !== true) { blockers.push({ code: "codex_home_auth_missing", sourceIssue: "pikasTech/HWLAB#426", summary: `Configured CODEX_HOME does not expose readable Codex auth.json: ${codexHomeFiles.authPath}.` }); } if (!tokenBoundary.present) { blockers.push({ code: "provider_token_boundary", sourceIssue: "pikasTech/HWLAB#426", summary: "A long-lived Codex app-server runner needs a repo-owned /codex-home auth boundary; this endpoint only reports file presence and never reads or prints token material." }); } if (childEnvBoundary.forbiddenEnvPresent) { blockers.push({ code: "codex_child_env_polluted", sourceIssue: "pikasTech/HWLAB#426", summary: "Codex child process environment still contains provider/proxy env keys that must be stripped before app-server startup." }); } if (egress.directPublicOpenAi) { blockers.push({ code: "codex_stdio_egress_boundary", sourceIssue: "pikasTech/HWLAB#426", summary: "Codex app-server provider egress must use /codex-home config/auth and DEV egress boundary, not direct public api.openai.com env fallback." }); } const ready = blockers.length === 0; const startupBlockers = blockers.filter((blocker) => blocker.code !== "stdio_protocol_not_wired"); const startupReady = startupBlockers.length === 0 && protocol.probeReady === true; return { kind: CODEX_STDIO_RUNNER_KIND, provider: CODEX_STDIO_PROVIDER, backend: CODEX_STDIO_BACKEND, status: ready ? "feasible" : "blocked", ready, startupReady, canStartLongLivedCodexStdio: startupReady, command, binary, binaryOnPath, workspace, workspaceState: workspaceInfo, codexHome, codexHomeState: codexHomeInfo, codexHomeFiles, sandbox, enabled, supervisor, tokenBoundary, egress, childEnvBoundary, protocol, stdioProtocol: protocol, protocolProbe: protocolProbeSummary({ command, workspace, codexHome, probe: protocolProbe }), commandProbe: commandProbeState, sessionLifecycle: lifecycle, lifecycleSupervisor: lifecycle, recentSessions: recentSessions(), statusCounts: sessionStatusCounts(), workspaceMount: workspaceContractState(workspaceInfo, sandbox, workspace), cancelReapTraceReadiness: cancelReapTraceState(lifecycle), runtimeContract: runtimeContract({ ready, binary, protocol, lifecycle, workspaceInfo, workspace, sandbox, codexHome, codexHomeInfo, codexHomeFiles, tokenBoundary, egress, childEnvBoundary, commandProbe: commandProbeState }), 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 traceRecorder = params.traceRecorder ?? createCodeAgentTraceRecorder({ traceStore, traceId, now, runnerKind: CODEX_STDIO_RUNNER_KIND, workspace, sandbox, sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE }); traceRecorder.append({ type: "request", status: "received", label: "request:received", promptSummary: summarizePrompt(params.message), waitingFor: "runtime-readiness" }); let availability = describe({ ...params, env, workspace, sandbox }); const startupBlockers = availability.blockers.filter((blocker) => blocker.code !== "stdio_protocol_not_wired"); if (startupBlockers.length > 0) { traceRecorder.append({ type: "error", status: "blocked", label: "runtime-readiness:blocked", errorCode: startupBlockers[0]?.code ?? "codex_stdio_blocked", message: startupBlockers[0]?.summary ?? "Codex stdio session is blocked by runtime readiness gates.", terminal: true }); throw codexStdioError("codex_stdio_blocked", "Codex stdio session is blocked by runtime readiness gates.", { availability: { ...availability, blockers: startupBlockers, blockerCodes: startupBlockers.map((blocker) => blocker.code) }, blockers: startupBlockers }); } const acquire = await acquireSession({ conversationId, requestedSessionId: params.sessionId, traceId, workspace, sandbox, now }); if (!acquire.ok) { traceRecorder.append({ type: "session", status: "blocked", label: `session:${acquire.code}`, errorCode: acquire.code, message: acquire.message, sessionId: acquire.session?.sessionId, sessionStatus: acquire.session?.status, terminal: true }); throw codexStdioError(acquire.code, acquire.message, { availability, session: acquire.session, blockers: [acquire.blocker] }); } let session = acquire.session; const startedAt = timestamp; traceRecorder.append({ type: "session", status: session.reused ? "reused" : "created", label: session.reused ? "session:reused" : "session:created", sessionId: session.sessionId, sessionStatus: session.status, sessionReused: session.reused, turn: session.turn, waitingFor: "stdio-client" }); try { const client = await ensureRpcClient({ env, availability, timeoutMs: params.timeoutMs }); availability = describe({ ...params, env, workspace, sandbox }); traceRecorder.append({ type: "stdio", status: "ready", label: "stdio:ready", sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: "prompt-send" }); const externalNetworkIntent = normalizeExternalNetworkIntent(params.externalNetworkIntent, params.message); if (externalNetworkIntent) { return await runExternalNetworkTurn({ params, env, traceRecorder, session, availability, workspace, sandbox, startedAt, intent: externalNetworkIntent, fetchImpl: options.fetchImpl, lookupImpl: options.lookupImpl, releaseSession }); } const sidecar = await collectWorkspaceSidecarEvidence({ message: params.message, workspace, traceId, env, now }); for (const event of sidecar.events) traceRecorder.append(event); const prompt = buildCodexUserPrompt(params.message, { conversationId, traceId, conversationFacts: params.conversationFacts }); const toolName = session.threadId ? "codex-app-server.thread/resume+turn/start" : "codex-app-server.thread/start+turn/start"; traceRecorder.append({ type: "prompt", status: "sent", label: "prompt:sent", toolName, promptSummary: summarizePrompt(params.message), sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: "app-server-thread" }); traceRecorder.append({ type: "tool_call", status: "started", label: `tool:${toolName}:started`, toolName, sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: "app-server-turn" }); const turnResult = await runAppServerTurn({ client, session, prompt, workspace, sandbox, model: firstNonEmpty(params.model, env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL, "codex-default"), traceRecorder, timeoutMs: effectiveActivityTimeout(params.timeoutMs), hardTimeoutMs: effectiveHardTimeout(params.hardTimeoutMs, params.timeoutMs) }); traceRecorder.append({ type: "tool_call", status: "output_chunk", label: `tool:${toolName}:output_chunk`, toolName, outputSummary: summarizeAppServerTurn(turnResult), sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: "assistant-message" }); if (turnResult.terminalStatus !== "completed") { throw codexStdioError("codex_stdio_failed", redactText(turnResult.terminalError || `Codex app-server turn finished with status=${turnResult.terminalStatus || "unknown"}.`), { availability, session, terminalStatus: turnResult.terminalStatus, threadId: turnResult.threadId, turnId: turnResult.turnId, appServerExit: turnResult.appServerExit }); } const assistantContent = redactText(firstNonEmpty(turnResult.finalResponse, turnResult.assistantText)); if (!assistantContent) { throw codexStdioError("codex_stdio_empty_response", "Codex app-server turn completed without assistant content.", { availability, session, terminalStatus: turnResult.terminalStatus, threadId: turnResult.threadId, turnId: turnResult.turnId }); } session = releaseSession(session.sessionId, { now, traceId, conversationId, reused: session.reused, threadId: turnResult.threadId ?? session.threadId, status: "idle" }) ?? session; traceRecorder.append({ type: "tool_call", status: "completed", label: `tool:${toolName}:completed`, toolName, outputSummary: turnResult.threadId ? `threadId=${turnResult.threadId} turnId=${turnResult.turnId ?? "unknown"}` : "threadId=captured", sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn }); traceRecorder.append({ type: "assistant_message", status: "chunk", label: "assistant:chunk", chunk: assistantContent.slice(0, 400), sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: "assistant-message-complete" }); traceRecorder.append({ type: "assistant_message", status: "completed", label: "assistant:completed", sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, terminal: true }); return { provider: CODEX_STDIO_PROVIDER, model: firstNonEmpty(params.model, env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL, "codex-default"), backend: CODEX_STDIO_BACKEND, content: codexReplyWithSidecar(assistantContent, sidecar), 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-app-server-stdio", name: toolName, status: "completed", cwd: workspace, command: "codex app-server --listen stdio://", exitCode: 0, stdout: [ turnResult.threadId ? `threadId=${turnResult.threadId}` : "threadId=captured", turnResult.turnId ? `turnId=${turnResult.turnId}` : null, `terminalStatus=${turnResult.terminalStatus}` ].filter(Boolean).join(" "), stderrSummary: "", outputTruncated: false, traceId }, ...sidecar.toolCalls], skills: sidecar.skills, runner: runnerDescriptor({ workspace, sandbox, session }), runnerTrace: runnerTrace({ traceRecorder, traceId, workspace, sandbox, session, startedAt, outputTruncated: sidecar.outputTruncated }), capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL, providerTrace: { transport: "stdio", protocol: CODEX_APP_SERVER_PROTOCOL, wireApi: CODEX_APP_SERVER_WIRE_API, runnerKind: CODEX_STDIO_RUNNER_KIND, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, command: `${availability.command} app-server --listen stdio://`, toolName, threadId: turnResult.threadId ?? session.threadId ?? null, turnId: turnResult.turnId ?? null, terminalStatus: turnResult.terminalStatus, valuesPrinted: false } }; } catch (error) { closeRpcClient(); const timeout = /timed out|timeout|超时/iu.test(String(error.message ?? "")); const currentSession = sessions.get(session.sessionId) ?? null; const canceled = currentSession?.status === "canceled"; session = canceled ? publicSession(currentSession, { conversationId, reused: session.reused }) : failSession(session.sessionId, { now, traceId, conversationId, reused: session.reused, status: timeout ? "timeout" : "failed", statusReason: timeout ? "codex_stdio_timeout" : error.code ?? "codex_stdio_failed" }) ?? session; traceRecorder.append({ type: canceled ? "cancel" : timeout ? "timeout" : "error", status: canceled ? "canceled" : "failed", label: canceled ? "cancel:canceled" : timeout ? "timeout" : `error:${error.code ?? "codex_stdio_failed"}`, errorCode: canceled ? "codex_stdio_canceled" : error.code ?? (timeout ? "codex_stdio_timeout" : "codex_stdio_failed"), message: canceled ? "Codex stdio request was canceled by the user." : error.message || "Codex stdio session failed.", timeoutMs: timeout ? error.timeoutMs ?? effectiveActivityTimeout(params.timeoutMs) : undefined, idleMs: timeout ? error.idleMs : undefined, lastActivityAt: timeout ? error.lastActivityAt : undefined, hardTimeoutMs: timeout ? error.hardTimeoutMs : undefined, sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: canceled ? "user-retry" : timeout ? error.waitingFor ?? "codex-stdio-tool-response" : null, terminal: true }); if (canceled) { throw codexStdioError("codex_stdio_canceled", "Codex stdio request was canceled by the user.", { availability, session, runnerTrace: runnerTrace({ traceRecorder, traceId, workspace, sandbox, session, startedAt, outputTruncated: false }) }); } if (error.code && (error.code.startsWith("codex_stdio") || [ "skills_unavailable", "external_network_blocked", "network_tool_unavailable", "network_timeout" ].includes(error.code))) { error.session = session; error.availability = error.availability ?? describe({ ...params, env, workspace, sandbox }); error.runnerTrace = runnerTrace({ traceRecorder, traceId, workspace, sandbox, session, startedAt, outputTruncated: false }); throw error; } throw codexStdioError("codex_stdio_failed", redactText(error.message || "Codex stdio session failed."), { availability, session, runnerTrace: runnerTrace({ traceRecorder, traceId, workspace, sandbox, session, startedAt, outputTruncated: false }) }); } } async function probe(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 codexHome = resolveCodexHome(env); let availability = describe({ ...params, env, workspace, command, sandbox }); const startupBlockers = availability.blockers.filter((blocker) => blocker.code !== "stdio_protocol_not_wired"); if (startupBlockers.length > 0) return availability; if (rpcClient && rpcInitialized === true) { availability = describe({ ...params, env, workspace, command, sandbox, protocolInitialized: true }); return ensureCommandProbeReady({ env, workspace, command, sandbox, codexHome, availability, now: params.now ?? nowDefault, probeTimeoutMs: params.commandProbeTimeoutMs }); } if (!params.forceProbe && cachedProtocolReady({ command, workspace, codexHome, probe: protocolProbe })) { availability = describe({ ...params, env, workspace, command, sandbox, protocolInitialized: true }); return ensureCommandProbeReady({ env, workspace, command, sandbox, codexHome, availability, now: params.now ?? nowDefault, probeTimeoutMs: params.commandProbeTimeoutMs }); } let probeClient = null; const startedAt = timestampFor(params.now ?? nowDefault); try { if (options.createProbeRpcClient) { probeClient = await options.createProbeRpcClient({ env, availability }); } else if (options.createRpcClient) { probeClient = await options.createRpcClient({ env, availability, probe: true }); } else { probeClient = createCodexAppServerJsonLineClient({ command: availability.command, args: codexAppServerArgs(env), env: childProcessEnv(env), cwd: availability.workspace }); } const timeoutMs = positiveInteger(params.probeTimeoutMs, 10000); if (typeof probeClient.initialize === "function") await probeClient.initialize(timeoutMs); protocolProbe = { status: "ready", ready: true, command, workspace, codexHome, initialized: true, requiredMethods: [...CODEX_APP_SERVER_REQUIRED_METHODS], startedAt, finishedAt: timestampFor(params.now ?? nowDefault), expiresAtMs: Date.now() + probeCacheTtlMs, secretMaterialRead: false, valuesRedacted: true }; availability = describe({ ...params, env, workspace, command, sandbox, protocolInitialized: true }); return ensureCommandProbeReady({ env, workspace, command, sandbox, codexHome, availability, now: params.now ?? nowDefault, probeTimeoutMs: params.commandProbeTimeoutMs }); } catch (error) { protocolProbe = { status: "blocked", ready: false, command, workspace, codexHome, initialized: false, requiredMethods: [...CODEX_APP_SERVER_REQUIRED_METHODS], startedAt, finishedAt: timestampFor(params.now ?? nowDefault), expiresAtMs: Date.now() + Math.min(probeCacheTtlMs, 5000), error: redactText(error.message), secretMaterialRead: false, valuesRedacted: true }; return describe({ ...params, env, workspace, command, sandbox, protocolInitialized: false }); } finally { if (probeClient && typeof probeClient.close === "function") { probeClient.close(); } } } function cancel(sessionId, params = {}) { const session = sessions.get(requiredId(sessionId, "ses")) ?? null; if (!session) return null; const timestamp = timestampFor(params.now ?? nowDefault); session.status = "canceled"; session.updatedAt = timestamp; session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId; session.currentTraceId = null; session.statusReason = params.reason ?? "user_canceled"; if (rpcClient && typeof rpcClient.close === "function") { closeRpcClient(); } 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) { closeRpcClient(); } 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(); closeRpcClient(); } function closeRpcClient() { if (rpcClient && typeof rpcClient.close === "function") { rpcClient.close(); } rpcClient = null; rpcStartedAt = null; rpcInitialized = false; protocolProbe = null; commandProbe = null; } async function ensureCommandProbeReady({ env, workspace, command, sandbox, codexHome, availability, now, probeTimeoutMs } = {}) { if (cachedCommandProbe({ command, workspace, codexHome, probe: commandProbe })) { return describe({ env, workspace, command, sandbox, protocolInitialized: availability.protocol?.initialized ?? rpcInitialized }); } const startedAt = timestampFor(now); try { const sidecar = await collectWorkspaceSidecarEvidence({ message: "用pwd列出你当前的工作目录", workspace, traceId: CODEX_STDIO_COMMAND_PROBE_TRACE_ID, env, now }); const pwdCall = sidecar.toolCalls.find((toolCall) => toolCall.name === "pwd"); if (pwdCall?.status === "blocked") { throw new Error(pwdCall.stderrSummary || "controlled pwd probe was blocked"); } if (!pwdCall || pwdCall.status !== "completed" || String(pwdCall.stdout ?? "").trim() !== workspace) { throw new Error("controlled pwd probe did not return the configured workspace"); } commandProbe = { status: "ready", ready: true, command, workspace, codexHome, probe: "workspace.pwd", conversationId: CODEX_STDIO_COMMAND_PROBE_CONVERSATION_ID, traceId: CODEX_STDIO_COMMAND_PROBE_TRACE_ID, toolCalls: [{ name: pwdCall.name, status: pwdCall.status, cwd: pwdCall.cwd, command: pwdCall.command, exitCode: pwdCall.exitCode, stdoutSummary: "workspace path matched", outputTruncated: pwdCall.outputTruncated === true }], startedAt, finishedAt: timestampFor(now), expiresAtMs: Date.now() + probeCacheTtlMs, timeoutMs: positiveInteger(probeTimeoutMs, CODEX_STDIO_COMMAND_PROBE_TIMEOUT_MS), secretMaterialRead: false, valuesRedacted: true }; } catch (error) { commandProbe = { status: "blocked", ready: false, command, workspace, codexHome, probe: "workspace.pwd", conversationId: CODEX_STDIO_COMMAND_PROBE_CONVERSATION_ID, traceId: CODEX_STDIO_COMMAND_PROBE_TRACE_ID, toolCalls: [], startedAt, finishedAt: timestampFor(now), expiresAtMs: Date.now() + Math.min(probeCacheTtlMs, 5000), timeoutMs: positiveInteger(probeTimeoutMs, CODEX_STDIO_COMMAND_PROBE_TIMEOUT_MS), error: redactText(error.message), secretMaterialRead: false, valuesRedacted: true }; } return describe({ env, workspace, command, sandbox, protocolInitialized: availability.protocol?.initialized ?? rpcInitialized }); } async function ensureRpcClient({ env, availability, timeoutMs } = {}) { if (rpcClient && typeof rpcClient.startTurn === "function") return rpcClient; rpcClient = options.createRpcClient ? await options.createRpcClient({ env, availability }) : createCodexAppServerJsonLineClient({ command: availability.command, args: codexAppServerArgs(env), env: childProcessEnv(env), cwd: availability.workspace }); if (typeof rpcClient.initialize === "function") { await rpcClient.initialize(effectiveTimeout(timeoutMs)); } rpcInitialized = true; 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 }); } let effectiveSessionId = mappedSessionId || requestedSessionId || requiredId(idFactory(), "ses"); let session = sessions.get(effectiveSessionId) ?? null; let 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 && ["failed", "interrupted", "timeout", "error", "canceled"].includes(session.status) && !requestedSessionId && mappedSessionId) { conversations.delete(conversationId); effectiveSessionId = requiredId(idFactory(), "ses"); session = null; reused = false; } if (session && ["failed", "interrupted", "timeout", "error", "canceled"].includes(session.status)) { return blockedAcquire({ code: `session_${session.status}`, summary: `Codex stdio session ${effectiveSessionId} is ${session.status}; create a new session before retrying.`, 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, sessionMode: CODEX_STDIO_SESSION_MODE, 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 = CODEX_STDIO_SESSION_STATUSES.includes(params.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: 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; } function recentSessions() { return [...sessions.values()] .sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt))) .slice(0, 10) .map((session) => publicSession(session)); } function sessionStatusCounts() { const counts = {}; for (const status of CODEX_STDIO_SESSION_STATUSES) counts[status] = 0; for (const session of sessions.values()) { counts[session.status] = (counts[session.status] ?? 0) + 1; } return counts; } return { describe, probe, chat, cancel, reapIdle, get, clear, longLivedSessionGate, get rpcStartedAt() { return rpcStartedAt; } }; } export function codexAppServerArgs(env = process.env) { return [ "app-server", ...codexAppServerProviderConfigArgs(env), "--listen", "stdio://" ]; } export function codexAppServerProviderBaseUrl(env = process.env) { const configured = firstNonEmpty(env.HWLAB_CODE_AGENT_OPENAI_BASE_URL, env.OPENAI_BASE_URL); if (!configured) return null; try { const url = new URL(configured); url.hash = ""; url.search = ""; const normalizedPath = url.pathname.replace(/\/+$/u, ""); if (normalizedPath.endsWith("/responses")) { url.pathname = normalizedPath.slice(0, -"/responses".length) || "/"; } const serialized = url.toString(); return serialized.endsWith("/") && url.pathname === "/" ? serialized.slice(0, -1) : serialized.replace(/\/$/u, ""); } catch { return null; } } function codexAppServerProviderConfigArgs(env = process.env) { const baseUrl = codexAppServerProviderBaseUrl(env); if (!baseUrl) return []; return [ "-c", "model_provider=\"OpenAI\"", "-c", `model_providers.OpenAI.base_url=${tomlString(baseUrl)}`, "-c", "model_providers.OpenAI.wire_api=\"responses\"", "-c", "model_providers.OpenAI.requires_openai_auth=true" ]; } function tomlString(value) { return JSON.stringify(String(value ?? "")); } export function createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STDIO_COMMAND, args = null, env = process.env, cwd = repoRoot, onNotification = null } = {}) { const spawnArgs = Array.isArray(args) ? args : codexAppServerArgs(env); const child = spawn(command, spawnArgs, { cwd, env, stdio: ["pipe", "pipe", "pipe"] }); let nextId = 1; let stdoutBuffer = ""; let stderr = ""; const pending = new Map(); let initialized = false; let notificationHandler = typeof onNotification === "function" ? onNotification : null; let closed = false; let closeResolve; const closedPromise = new Promise((resolve) => { closeResolve = resolve; }); 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) => closeWith(`Codex app-server process error: ${error.message}`, 127, "error")); child.on("close", (code, signal) => closeWith(`Codex app-server process closed code=${code ?? "null"} signal=${signal ?? "null"}`, code, signal)); function handleLine(line) { let payload = null; try { payload = JSON.parse(line); } catch { return; } const id = typeof payload.id === "number" ? payload.id : null; const method = typeof payload.method === "string" ? payload.method : null; if (id !== null && method === null) { const request = pending.get(id); if (!request) return; pending.delete(id); clearTimeout(request.timer); if (payload.error) { request.reject(new Error(redactText(payload.error.message ?? JSON.stringify(payload.error)))); } else { request.resolve(payload.result); } return; } if (id !== null && method !== null) { handleServerRequest(id, method); return; } if (method !== null && notificationHandler) { notificationHandler(payload); } } function closeWith(message, code, signal) { if (closed) return; closed = true; for (const [id, request] of pending.entries()) { pending.delete(id); clearTimeout(request.timer); request.reject(new Error(redactText(`${message}; stderr=${tailText(stderr, 800)}`))); } closeResolve({ code: Number.isInteger(code) ? code : null, signal: typeof signal === "string" ? signal : null, stderrTail: redactText(tailText(stderr, 8000)) }); } function request(method, params = {}, timeoutMs = 30000) { if (closed) { return Promise.reject(new Error("Codex app-server process is already closed.")); } const id = nextId; nextId += 1; const message = JSON.stringify({ 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 { initialized: true, requiredMethods: [...CODEX_APP_SERVER_REQUIRED_METHODS] }; await request("initialize", { clientInfo: { name: "hwlab-cloud-api", title: "HWLAB Cloud API Code Agent", version: "0" }, capabilities: { experimentalApi: true } }, timeoutMs); notify("initialized", {}); initialized = true; return { initialized: true, requiredMethods: [...CODEX_APP_SERVER_REQUIRED_METHODS] }; } function notify(method, params = {}) { child.stdin.write(`${JSON.stringify({ method, params })}\n`); } function handleServerRequest(id, method) { if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") { child.stdin.write(`${JSON.stringify({ id, result: { decision: "decline" } })}\n`); return; } child.stdin.write(`${JSON.stringify({ id, error: { code: -32601, message: `Unsupported client-side request: ${method}` } })}\n`); } async function startThread({ model, cwd: threadCwd, sandbox, approvalPolicy = "never", serviceName = "hwlab-cloud-api" } = {}, timeoutMs = 30000) { const result = await request("thread/start", dropEmpty({ model, cwd: threadCwd, sandbox, approvalPolicy, serviceName }), timeoutMs); const threadId = extractAppServerString(extractAppServerRecord(result)?.thread, "id"); if (!threadId) throw new Error("thread/start response did not include thread.id"); return { threadId, result }; } async function resumeThread({ threadId, model, cwd: threadCwd, sandbox, approvalPolicy = "never" } = {}, timeoutMs = 30000) { const result = await request("thread/resume", dropEmpty({ threadId, model, cwd: threadCwd, sandbox, approvalPolicy }), timeoutMs); const resumedThreadId = extractAppServerString(extractAppServerRecord(result)?.thread, "id") ?? optionalId(threadId); if (!resumedThreadId) throw new Error("thread/resume response did not include thread.id"); return { threadId: resumedThreadId, result }; } async function startTurn({ threadId, prompt, model, cwd: turnCwd, approvalPolicy = "never", effort = null } = {}, timeoutMs = 30000) { const result = await request("turn/start", dropEmpty({ threadId, input: codexAppServerTextInput(prompt), cwd: turnCwd, approvalPolicy, model, effort }), timeoutMs); const turnId = extractAppServerString(extractAppServerRecord(result)?.turn, "id"); if (!turnId) throw new Error("turn/start response did not include turn.id"); return { turnId, result }; } function setNotificationHandler(handler) { notificationHandler = typeof handler === "function" ? handler : null; } function close() { if (closed) return; child.kill("SIGTERM"); } return { closedPromise, initialize, startThread, resumeThread, startTurn, setNotificationHandler, close }; } async function runAppServerTurn({ client, session, prompt, workspace, sandbox, model, traceRecorder, timeoutMs, hardTimeoutMs } = {}) { const state = createAppServerTurnState({ traceRecorder, session }); const effectiveMs = effectiveActivityTimeout(timeoutMs); const effectiveHardMs = effectiveHardTimeout(hardTimeoutMs, effectiveMs); if (typeof client.setNotificationHandler === "function") { client.setNotificationHandler((message) => state.handle(message)); } try { let threadId = optionalId(session?.threadId); if (threadId) { state.appendTrace({ type: "thread", status: "started", label: "thread:resume:started", sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, threadId, waitingFor: "thread/resume" }); const resumed = await client.resumeThread({ threadId, model, cwd: workspace, sandbox, approvalPolicy: "never" }, effectiveMs); threadId = optionalId(resumed?.threadId) ?? threadId; state.setThreadId(threadId); state.appendTrace({ type: "thread", status: "completed", label: "thread:resume:completed", sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, threadId, waitingFor: "turn/start" }); } else { state.appendTrace({ type: "thread", status: "started", label: "thread:start:started", sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: "thread/start" }); const started = await client.startThread({ model, cwd: workspace, sandbox, approvalPolicy: "never", serviceName: "hwlab-cloud-api" }, effectiveMs); threadId = optionalId(started?.threadId); if (!threadId) throw new Error("thread/start response did not include thread.id"); state.setThreadId(threadId); state.appendTrace({ type: "thread", status: "completed", label: "thread:start:completed", sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, threadId, waitingFor: "turn/start" }); } state.appendTrace({ type: "turn", status: "started", label: "turn:start:started", sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, threadId, waitingFor: "turn/start" }); const turnStart = await client.startTurn({ threadId, prompt, model, cwd: workspace, sandbox, approvalPolicy: "never" }, effectiveMs); const turnId = optionalId(turnStart?.turnId); if (!turnId) throw new Error("turn/start response did not include turn.id"); state.setTurnId(turnId); state.appendTrace({ type: "turn", status: "started", label: "turn:start:completed", sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, threadId, turnId, waitingFor: "turn/completed" }); const rawResult = await state.wait(effectiveMs, client.closedPromise, { hardTimeoutMs: effectiveHardMs }); return normalizeAppServerTurnResult(rawResult, state.snapshot(), { threadId, turnId }); } finally { if (typeof client.setNotificationHandler === "function") { client.setNotificationHandler(null); } } } function createAppServerTurnState({ traceRecorder, session } = {}) { let threadId = optionalId(session?.threadId); let turnId = null; let assistantText = ""; let finalResponse = ""; let terminal = null; let terminalResolve; const terminalPromise = new Promise((resolve) => { terminalResolve = resolve; }); let lastActivityAt = Date.now(); let lastActivityLabel = "turn-state:created"; let lastWaitingFor = "app-server-turn"; function setThreadId(value) { threadId = optionalId(value) ?? threadId; } function setTurnId(value) { turnId = optionalId(value) ?? turnId; } function activitySnapshot(referenceNow = Date.now()) { return { lastActivityAt: new Date(lastActivityAt).toISOString(), lastActivityLabel, idleMs: Math.max(0, referenceNow - lastActivityAt), waitingFor: lastWaitingFor }; } function observeActivity({ label = "app-server:activity", waitingFor = null } = {}) { lastActivityAt = Date.now(); lastActivityLabel = label; if (waitingFor) lastWaitingFor = waitingFor; return activitySnapshot(lastActivityAt); } function appendTrace(event = {}) { const appended = traceRecorder?.append(event); observeActivity({ label: appended?.label ?? event.label ?? event.type ?? "trace:event", waitingFor: appended?.waitingFor ?? event.waitingFor ?? null }); return appended; } function finish(status, error = null, extra = {}) { if (terminal) return; terminal = { terminalStatus: appServerTerminalStatus(status), terminalError: error ? redactText(error) : null, threadId, turnId, ...activitySnapshot(), ...extra }; terminalResolve(terminal); } function handle(message) { const method = typeof message?.method === "string" ? message.method : "unknown"; observeActivity({ label: `app-server:${method}`, waitingFor: appServerWaitingForMethod(method) }); const params = extractAppServerRecord(message?.params); const item = extractAppServerRecord(params?.item); const turn = extractAppServerRecord(params?.turn); if (method === "thread/started") { setThreadId(extractAppServerString(extractAppServerRecord(params?.thread), "id")); appendTrace({ type: "thread", status: "completed", label: "thread:started", sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, threadId, waitingFor: "turn/start" }); return; } if (method === "turn/started") { setTurnId(extractAppServerString(turn, "id")); appendTrace({ type: "turn", status: "started", label: "turn:started", sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, threadId, turnId, waitingFor: "assistant-message" }); return; } if (method === "item/agentMessage/delta") { const delta = String(params?.delta ?? ""); assistantText += delta; if (delta) { appendTrace({ type: "assistant_message", status: "chunk", label: "assistant:chunk", chunk: delta.slice(0, 400), sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, threadId, turnId, waitingFor: "turn/completed" }); } return; } if (method === "item/completed" && item?.type === "agentMessage") { if (typeof item.text === "string") finalResponse = item.text; appendTrace({ type: "assistant_message", status: "completed", label: "assistant:item_completed", sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, threadId, turnId, waitingFor: "turn/completed" }); return; } if (method === "item/commandExecution/outputDelta" || method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") { appendTrace({ type: method.startsWith("item/reasoning/") ? "reasoning" : "tool_call", status: "output_chunk", label: method, outputSummary: String(params?.delta ?? "").slice(0, 400), sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, threadId, turnId, waitingFor: "turn/completed" }); return; } if (method === "error") { const error = extractAppServerRecord(params?.error); const message = typeof error?.message === "string" ? error.message : "Codex app-server error"; const willRetry = params?.willRetry === true; setThreadId(extractAppServerString(params, "threadId")); setTurnId(extractAppServerString(params, "turnId")); appendTrace({ type: willRetry ? "provider_retry" : "error", status: willRetry ? "retrying" : "failed", label: willRetry ? "app-server:retrying" : "app-server:error", errorCode: willRetry ? "codex_stdio_provider_retry" : "codex_stdio_failed", message: redactText(message), sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, threadId, turnId, waitingFor: willRetry ? "turn/completed" : "user-retry", terminal: !willRetry }); if (!willRetry) finish("failed", message); return; } if (method === "turn/completed") { const status = appServerTerminalStatus(turn?.status); const error = extractAppServerRecord(turn?.error); const message = typeof error?.message === "string" ? error.message : null; appendTrace({ type: "turn", status, label: `turn:completed:${status ?? "unknown"}`, sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, threadId, turnId, errorCode: status === "completed" ? null : "codex_stdio_failed", message: message ? redactText(message) : undefined, terminal: status !== "completed" }); finish(status, message); } } async function wait(timeoutMs, closedPromise, { hardTimeoutMs = null } = {}) { let activityTimer; let hardTimer; const timeoutPromise = new Promise((_, reject) => { const checkActivity = () => { const now = Date.now(); const activity = activitySnapshot(now); if (activity.idleMs >= timeoutMs) { const error = appServerActivityTimeoutError({ timeoutMs, idleMs: activity.idleMs, lastActivityAt: activity.lastActivityAt, waitingFor: activity.waitingFor, threadId, turnId }); appendTrace({ type: "timeout", status: "failed", label: "timeout:no_activity", errorCode: error.code, message: error.message, timeoutMs, idleMs: activity.idleMs, lastActivityAt: activity.lastActivityAt, sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, threadId, turnId, waitingFor: activity.waitingFor, terminal: true }); reject(error); return; } activityTimer = setTimeout(checkActivity, Math.max(1, timeoutMs - activity.idleMs)); }; activityTimer = setTimeout(checkActivity, timeoutMs); }); const hardTimeoutPromise = hardTimeoutMs ? new Promise((_, reject) => { hardTimer = setTimeout(() => { const activity = activitySnapshot(); const error = appServerHardTimeoutError({ hardTimeoutMs, timeoutMs, idleMs: activity.idleMs, lastActivityAt: activity.lastActivityAt, waitingFor: activity.waitingFor, threadId, turnId }); appendTrace({ type: "timeout", status: "failed", label: "timeout:hard_cap", errorCode: error.code, message: error.message, timeoutMs, hardTimeoutMs, idleMs: activity.idleMs, lastActivityAt: activity.lastActivityAt, sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, threadId, turnId, waitingFor: activity.waitingFor, terminal: true }); reject(error); }, hardTimeoutMs); }) : null; const candidates = [terminalPromise]; if (closedPromise && typeof closedPromise.then === "function") { candidates.push(closedPromise.then((exit) => { if (terminal) return terminal; return { terminalStatus: "failed", terminalError: "Codex app-server transport closed before turn/completed.", threadId, turnId, ...activitySnapshot(), appServerExit: exit }; })); } try { return await Promise.race([...candidates, timeoutPromise, hardTimeoutPromise].filter(Boolean)); } finally { clearTimeout(activityTimer); clearTimeout(hardTimer); } } function snapshot() { const activity = activitySnapshot(); return { threadId, turnId, assistantText: redactText(assistantText), finalResponse: redactText(firstNonEmpty(finalResponse, assistantText)), terminalStatus: terminal?.terminalStatus ?? null, terminalError: terminal?.terminalError ?? null, lastActivityAt: terminal?.lastActivityAt ?? activity.lastActivityAt, idleMs: terminal?.idleMs ?? activity.idleMs, waitingFor: terminal?.waitingFor ?? activity.waitingFor }; } return { appendTrace, handle, setThreadId, setTurnId, wait, snapshot }; } function appServerWaitingForMethod(method) { if (method === "thread/started") return "turn/start"; if (method === "turn/started") return "assistant-message"; if (method === "item/agentMessage/delta") return "turn/completed"; if (method === "item/completed") return "turn/completed"; if (method === "item/commandExecution/outputDelta") return "turn/completed"; if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") return "turn/completed"; if (method === "error") return "turn/completed"; if (method === "turn/completed") return "turn/completed"; return "app-server-notification"; } function appServerActivityTimeoutError({ timeoutMs, idleMs, lastActivityAt, waitingFor, threadId, turnId }) { const error = new Error(`Codex app-server idle timeout: no new events for ${idleMs}ms, threshold ${timeoutMs}ms; waitingFor=${waitingFor ?? "unknown"}.`); Object.assign(error, { code: "codex_stdio_idle_timeout", timeoutMs, idleMs, lastActivityAt, waitingFor, threadId, turnId, stage: "app-server-turn" }); return error; } function appServerHardTimeoutError({ hardTimeoutMs, timeoutMs, idleMs, lastActivityAt, waitingFor, threadId, turnId }) { const error = new Error(`Codex app-server hard timeout reached after ${hardTimeoutMs}ms; last activity was ${idleMs}ms ago; waitingFor=${waitingFor ?? "unknown"}.`); Object.assign(error, { code: "codex_stdio_hard_timeout", timeoutMs, hardTimeoutMs, idleMs, lastActivityAt, waitingFor, threadId, turnId, stage: "app-server-turn" }); return error; } function normalizeAppServerTurnResult(rawResult, state, fallback) { const raw = extractAppServerRecord(rawResult) ?? {}; return { threadId: optionalId(raw.threadId) ?? state.threadId ?? fallback.threadId ?? null, turnId: optionalId(raw.turnId) ?? state.turnId ?? fallback.turnId ?? null, assistantText: redactText(firstNonEmpty(raw.assistantText, state.assistantText)), finalResponse: redactText(firstNonEmpty(raw.finalResponse, raw.content, raw.text, state.finalResponse, state.assistantText)), terminalStatus: appServerTerminalStatus(raw.terminalStatus ?? raw.status ?? state.terminalStatus), terminalError: raw.terminalError ? redactText(raw.terminalError) : state.terminalError ?? null, lastActivityAt: raw.lastActivityAt ?? state.lastActivityAt ?? null, idleMs: raw.idleMs ?? state.idleMs ?? null, waitingFor: raw.waitingFor ?? state.waitingFor ?? null, appServerExit: raw.appServerExit ?? null }; } function summarizeAppServerTurn(turnResult) { return [ turnResult?.threadId ? `threadId=${turnResult.threadId}` : "threadId=not_observed", turnResult?.turnId ? `turnId=${turnResult.turnId}` : null, `terminalStatus=${turnResult?.terminalStatus ?? "unknown"}`, firstNonEmpty(turnResult?.finalResponse, turnResult?.assistantText) ? `assistantChars=${String(firstNonEmpty(turnResult.finalResponse, turnResult.assistantText)).length}` : null ].filter(Boolean).join(" "); } function codexAppServerTextInput(text) { return [{ type: "text", text: String(text ?? ""), text_elements: [] }]; } function extractAppServerRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null; } function extractAppServerString(record, key) { return typeof record?.[key] === "string" && record[key].trim() ? record[key].trim() : null; } function appServerTerminalStatus(value) { const raw = String(value ?? "").trim().toLowerCase(); if (raw === "completed" || raw === "complete" || raw === "success" || raw === "succeeded") return "completed"; if (raw === "canceled" || raw === "cancelled") return "canceled"; if (raw === "interrupted") return "interrupted"; if (raw === "timeout" || raw === "timed_out") return "timeout"; if (raw === "failed" || raw === "error") return "failed"; return raw ? "failed" : null; } 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: "codex_stdio_blocked_readonly_session_available", sourceIssue: "pikasTech/HWLAB#317", summary: "This response is backed by a reusable controlled read-only session registry, not Codex stdio or an equivalent full Code Agent 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, sessionMode: session.sessionMode, 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-app-server.thread/start", "codex-app-server.thread/resume", "codex-app-server.turn/start", "workspace-read", "workspace-write", EXTERNAL_NETWORK_TOOL_NAME], 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({ traceRecorder, traceId, workspace, sandbox, session, startedAt, outputTruncated }) { const snapshot = traceRecorder?.snapshot({ sessionId: session?.sessionId, sessionStatus: session?.status, turn: session?.turn, workspace, sandbox, runnerKind: CODEX_STDIO_RUNNER_KIND, sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE }); return runnerTraceFromSnapshot(snapshot ?? { traceId, events: [], eventLabels: [], updatedAt: timestampFor(), startedAt }, { 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 ?? traceId, turn: session?.turn ?? null, sessionReused: session?.reused ?? false, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, limitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"], startedAt, outputTruncated: Boolean(outputTruncated), note: "Repo-owned Codex app-server stdio supervisor manages create/reuse/cancel/reap/idle timeout and real-time 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 summarizePrompt(message) { const value = String(message ?? "").replace(/\s+/gu, " ").trim(); if (!value) return "empty prompt"; return value.length > 160 ? `${value.slice(0, 157)}...` : value; } function summarizeToolResult(toolResult) { const content = extractCodexToolOutput(toolResult); if (content.threadId || content.content) { return [ content.threadId ? `threadId=${content.threadId}` : "threadId=not_observed", content.content ? `assistantChars=${content.content.length}` : null ].filter(Boolean).join(" "); } if (Array.isArray(toolResult?.content)) return `contentItems=${toolResult.content.length}`; return "tool result captured"; } async function runExternalNetworkTurn({ params, env, traceRecorder, session, availability, workspace, sandbox, startedAt, intent, fetchImpl, lookupImpl, releaseSession: releaseSessionFn } = {}) { const traceId = optionalId(params?.traceId) ?? traceRecorder?.traceId; const now = params?.now; const toolName = EXTERNAL_NETWORK_TOOL_NAME; traceRecorder.append({ type: "prompt", status: "sent", label: "prompt:sent", toolName, promptSummary: summarizePrompt(params?.message), sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: "network-policy" }); traceRecorder.append({ type: "tool_call", status: "started", label: `tool:${toolName}:started`, toolName, sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: "network:policy" }); traceRecorder.append({ type: "network", stage: "policy", status: "started", label: "network:started", toolName, outputSummary: `target=${safeNetworkUrl(intent.url)}`, sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: "network:http" }); const check = await controlledExternalNetworkCheck({ intent, env, timeoutMs: params?.networkTimeoutMs, fetchImpl, lookupImpl, now }); const toolCall = externalNetworkToolCall({ check, workspace, traceId }); if (check.ok !== true) { traceRecorder.append({ type: "network", stage: check.stage ?? "blocked", status: check.code === "network_timeout" ? "timeout" : "blocked", label: check.code === "network_timeout" ? "network:timeout" : "network:blocked", toolName, errorCode: check.code, message: check.userMessage, outputSummary: check.summary, timeoutMs: check.timeoutMs, sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn, waitingFor: check.code === "network_timeout" ? "external-network-response" : "network-policy" }); traceRecorder.append({ type: "tool_call", status: "blocked", label: `tool:${toolName}:blocked`, toolName, errorCode: check.code, outputSummary: check.summary, sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn }); const traceSnapshot = traceRecorder.snapshot({ sessionId: session.sessionId, sessionStatus: session.status, turn: session.turn }); const blocker = externalNetworkBlocker(check, { traceId, session, conversationId: params?.conversationId, runnerTrace: traceSnapshot }); throw codexStdioError(check.code, check.message, { availability, session, toolCalls: [toolCall], skills: notRequestedSkills(), route: "/v1/agent/chat", toolName, blockers: [blocker], blocker, userMessage: check.userMessage, retryable: check.retryable, stage: check.stage, targetUrl: check.url, timeoutMs: check.timeoutMs }); } const released = releaseSessionFn(session.sessionId, { now, traceId, conversationId: params.conversationId, reused: session.reused, threadId: session.threadId, status: "idle" }) ?? session; traceRecorder.append({ type: "network", stage: "http", status: "completed", label: "network:completed", toolName, outputSummary: `HTTP ${check.httpStatus} ${check.statusText || ""}; elapsedMs=${check.elapsedMs}`, sessionId: released.sessionId, sessionStatus: released.status, turn: released.turn }); traceRecorder.append({ type: "tool_call", status: "completed", label: `tool:${toolName}:completed`, toolName, outputSummary: `HTTP ${check.httpStatus}; target=${safeNetworkUrl(check.url)}`, sessionId: released.sessionId, sessionStatus: released.status, turn: released.turn }); const content = externalNetworkReply(check); traceRecorder.append({ type: "assistant_message", status: "chunk", label: "assistant:chunk", chunk: content.slice(0, 400), sessionId: released.sessionId, sessionStatus: released.status, turn: released.turn, waitingFor: "assistant-message-complete" }); traceRecorder.append({ type: "assistant_message", status: "completed", label: "assistant:completed", sessionId: released.sessionId, sessionStatus: released.status, turn: released.turn, terminal: true }); return { provider: CODEX_STDIO_PROVIDER, model: firstNonEmpty(params.model, env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL, "codex-default"), backend: CODEX_STDIO_BACKEND, content, workspace, sandbox, session: released, sessionMode: CODEX_STDIO_SESSION_MODE, sessionReuse: sessionReuseEvidence(released), implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted", "external-network-http-check-only"], codexStdioFeasibility: availability, longLivedSessionGate: longLivedSessionGate({ provider: CODEX_STDIO_PROVIDER, runnerKind: CODEX_STDIO_RUNNER_KIND, session: released, sessionMode: CODEX_STDIO_SESSION_MODE, implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, codexStdioFeasibility: availability }), toolCalls: [toolCall], skills: notRequestedSkills(), runner: runnerDescriptor({ workspace, sandbox, session: released }), runnerTrace: runnerTrace({ traceRecorder, traceId, workspace, sandbox, session: released, startedAt, outputTruncated: false }), capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL, providerTrace: { transport: "stdio+controlled-network", protocol: `${CODEX_APP_SERVER_PROTOCOL}+http-head`, command: `${availability.command} app-server --listen stdio:// + ${toolName}`, toolName, targetUrl: safeNetworkUrl(check.url), method: check.method, httpStatus: check.httpStatus, elapsedMs: check.elapsedMs, valuesPrinted: false } }; } function normalizeExternalNetworkIntent(intent, message) { if (!intent || intent.kind !== "external_network") return null; const url = normalizeNetworkUrl(intent.targetUrl ?? intent.url ?? extractNetworkTargetFromText(message)); if (!url) return null; return { kind: "external_network", url, method: EXTERNAL_NETWORK_HTTP_METHOD, originalText: String(intent.originalText ?? message ?? "").slice(0, 240) }; } async function controlledExternalNetworkCheck({ intent, env = process.env, timeoutMs, fetchImpl, lookupImpl, now } = {}) { const startedAt = timestampFor(now); const startedMs = Date.now(); const url = normalizeNetworkUrl(intent?.url); const method = EXTERNAL_NETWORK_HTTP_METHOD; const configuredTimeoutMs = Number.parseInt(env.HWLAB_CODE_AGENT_EXTERNAL_NETWORK_TIMEOUT_MS ?? "", 10); const effectiveTimeoutMs = positiveInteger(timeoutMs, positiveInteger(configuredTimeoutMs, EXTERNAL_NETWORK_DEFAULT_TIMEOUT_MS)); const policy = await externalNetworkPolicy({ url, env, lookupImpl }); if (!policy.ok) { return { ...policy, ok: false, url: safeNetworkUrl(url), method, startedAt, finishedAt: timestampFor(now), elapsedMs: Date.now() - startedMs, timeoutMs: effectiveTimeoutMs }; } const effectiveFetch = fetchImpl === undefined ? globalThis.fetch : fetchImpl; if (typeof effectiveFetch !== "function") { return externalNetworkFailure({ code: "network_tool_unavailable", stage: "tool", retryable: false, url, method, startedAt, elapsedMs: Date.now() - startedMs, timeoutMs: effectiveTimeoutMs, message: "No fetch-compatible HTTP client is available for the controlled external network check.", userMessage: "当前运行环境没有可用的受控 HTTP 网络检查工具;未访问外网,也不会伪造成功。" }); } const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), effectiveTimeoutMs); try { const response = await effectiveFetch(url, { method, redirect: "manual", cache: "no-store", headers: { "user-agent": "HWLAB-Code-Agent-Network-Check/1.0", accept: "text/html,application/json;q=0.5,*/*;q=0.1" }, signal: controller.signal }); const elapsedMs = Date.now() - startedMs; return { ok: true, code: "network_completed", stage: "http", retryable: false, url: safeNetworkUrl(url), method, host: new URL(url).hostname, httpStatus: Number(response?.status ?? 0), statusText: safeNetworkStatusText(response?.statusText), reachable: Number(response?.status ?? 0) > 0, redirected: response?.url ? safeNetworkUrl(response.url) !== safeNetworkUrl(url) : false, finalUrl: safeNetworkUrl(response?.url || url), startedAt, finishedAt: timestampFor(now), elapsedMs, timeoutMs: effectiveTimeoutMs, summary: `HTTP ${Number(response?.status ?? 0)} ${safeNetworkStatusText(response?.statusText)}`.trim(), userMessage: "外部网络检查已完成。" }; } catch (error) { const elapsedMs = Date.now() - startedMs; const aborted = error?.name === "AbortError" || /abort|timeout|timed out/iu.test(String(error?.message ?? "")); return externalNetworkFailure({ code: aborted ? "network_timeout" : "external_network_blocked", stage: aborted ? "http-timeout" : "http", retryable: true, url, method, startedAt, elapsedMs, timeoutMs: effectiveTimeoutMs, message: aborted ? `Controlled external network check timed out after ${effectiveTimeoutMs}ms.` : `Controlled external network check failed: ${redactText(error?.message ?? "network error")}`, userMessage: aborted ? `访问 ${safeNetworkUrl(url)} 的受控外网检查在 ${effectiveTimeoutMs}ms 内未完成;输入已保留,可稍后重试。` : `访问 ${safeNetworkUrl(url)} 的受控外网检查失败;当前不会回退成文本成功。` }); } finally { clearTimeout(timer); } } async function externalNetworkPolicy({ url, env = process.env, lookupImpl } = {}) { if (!url) { return externalNetworkFailure({ code: "external_network_blocked", stage: "policy", retryable: false, url, message: "No external HTTP(S) target was detected.", userMessage: "没有识别到可访问的公网 HTTP(S) 目标;请提供明确 URL,例如 https://github.com。" }); } if (/^(?:0|false|deny|denied|disabled|off)$/iu.test(String(env.HWLAB_CODE_AGENT_EXTERNAL_NETWORK ?? env.HWLAB_CODE_AGENT_NETWORK ?? "").trim())) { return externalNetworkFailure({ code: "external_network_blocked", stage: "policy", retryable: false, url, message: "External network checks are disabled by runtime policy.", userMessage: "当前 Code Agent 运行策略禁止外部网络访问;未访问外网,也不会伪造成功。" }); } let parsed; try { parsed = new URL(url); } catch { return externalNetworkFailure({ code: "external_network_blocked", stage: "policy", retryable: false, url, message: "External network target URL is invalid.", userMessage: "外部网络目标 URL 格式不正确;请改用明确的 http(s) URL。" }); } if (!["http:", "https:"].includes(parsed.protocol)) { return externalNetworkFailure({ code: "external_network_blocked", stage: "policy", retryable: false, url, message: "External network checks only allow HTTP(S).", userMessage: "当前只允许受控 HTTP(S) 外网检查;未访问该目标。" }); } const host = parsed.hostname.toLowerCase(); if (isForbiddenNetworkHost(host)) { return externalNetworkFailure({ code: "external_network_blocked", stage: "policy", retryable: false, url, message: "External network target is local, private, or a forbidden HWLAB runtime host.", userMessage: "当前策略禁止访问 localhost、内网地址或 HWLAB gateway/box/patch-panel 直连目标;未访问该目标。" }); } const allowlist = parseNetworkAllowlist(env.HWLAB_CODE_AGENT_EXTERNAL_NETWORK_ALLOWLIST); if (allowlist.length > 0 && !allowlist.some((pattern) => networkHostMatches(host, pattern))) { return externalNetworkFailure({ code: "external_network_blocked", stage: "policy", retryable: false, url, message: `External network target ${host} is not in the configured allowlist.`, userMessage: `当前外网策略不允许访问 ${host};请让维护者确认 allowlist 或换用允许的公网目标。` }); } const lookup = lookupImpl === undefined ? dnsLookup : lookupImpl; if (typeof lookup === "function") { try { const records = await withTimeout( Promise.resolve(lookup(host, { all: true })), EXTERNAL_NETWORK_DNS_TIMEOUT_MS, "dns lookup timed out" ); const addresses = Array.isArray(records) ? records.map((record) => typeof record === "string" ? record : record?.address).filter(Boolean) : [typeof records === "string" ? records : records?.address].filter(Boolean); if (addresses.some((address) => isPrivateNetworkAddress(address))) { return externalNetworkFailure({ code: "external_network_blocked", stage: "policy", retryable: false, url, message: "External network target resolved to a private/local address.", userMessage: "目标解析到内网或本地地址;当前策略已阻断,未访问该目标。" }); } } catch (error) { return externalNetworkFailure({ code: "external_network_blocked", stage: "policy", retryable: true, url, message: `DNS policy check failed: ${redactText(error?.message ?? "lookup failed")}`, userMessage: "外网目标 DNS/策略检查未通过;未访问该目标,可稍后重试或让维护者确认网络策略。" }); } } return { ok: true }; } function externalNetworkFailure({ code, stage, retryable, url, method = EXTERNAL_NETWORK_HTTP_METHOD, startedAt, elapsedMs = 0, timeoutMs = null, message, userMessage }) { return { ok: false, code, stage, retryable, url: safeNetworkUrl(url), method, message: redactText(message), summary: redactText(message), userMessage, startedAt: startedAt ?? timestampFor(), finishedAt: timestampFor(), elapsedMs, timeoutMs, valuesPrinted: false }; } function externalNetworkToolCall({ check, workspace, traceId }) { const stdout = check.ok === true ? { targetUrl: safeNetworkUrl(check.url), method: check.method, httpStatus: check.httpStatus, statusText: check.statusText, reachable: check.reachable, elapsedMs: check.elapsedMs, finalUrl: safeNetworkUrl(check.finalUrl) } : { targetUrl: safeNetworkUrl(check.url), method: check.method, blocker: check.code, stage: check.stage, elapsedMs: check.elapsedMs, timeoutMs: check.timeoutMs }; const bounded = boundToolOutput(JSON.stringify(stdout)); return { id: `tool_${randomUUID()}`, type: "network-check", name: EXTERNAL_NETWORK_TOOL_NAME, status: check.ok === true ? "completed" : "blocked", cwd: workspace, command: `${check.method ?? EXTERNAL_NETWORK_HTTP_METHOD} ${safeNetworkUrl(check.url)}`, exitCode: check.ok === true ? 0 : 2, stdout: bounded.text, stderrSummary: check.ok === true ? "" : check.code, outputTruncated: bounded.truncated, traceId, route: "/v1/agent/chat", method: check.method ?? EXTERNAL_NETWORK_HTTP_METHOD, targetUrl: safeNetworkUrl(check.url), httpStatus: check.httpStatus ?? null, elapsedMs: check.elapsedMs ?? null, blocker: check.ok === true ? null : { code: check.code, stage: check.stage, retryable: check.retryable, userMessage: check.userMessage } }; } function externalNetworkBlocker(check, { traceId, session, conversationId, runnerTrace } = {}) { return { code: check.code, layer: check.code === "network_tool_unavailable" ? "network-tool" : "network", category: check.code === "network_timeout" ? "timeout" : check.code === "network_tool_unavailable" ? "needs_config" : "capability_unavailable", retryable: Boolean(check.retryable), summary: check.summary, userMessage: check.userMessage, traceId, sessionId: session?.sessionId ?? null, conversationId: conversationId ?? session?.conversationId ?? null, stage: check.stage, lastEvent: runnerTrace?.lastEvent ?? null, elapsedMs: runnerTrace?.elapsedMs ?? check.elapsedMs ?? null, targetUrl: safeNetworkUrl(check.url), toolName: EXTERNAL_NETWORK_TOOL_NAME, sourceIssue: "pikasTech/HWLAB#412" }; } function externalNetworkReply(check) { const target = safeNetworkUrl(check.url); const status = `HTTP ${check.httpStatus}${check.statusText ? ` ${check.statusText}` : ""}`; const reachability = check.httpStatus >= 500 ? "网络已到达目标,但目标返回服务端错误。" : "网络已到达目标。"; return [ `${target.includes("github.com") ? "GitHub" : "目标站点"}可以访问:受控检查 ${check.method} ${target} 返回 ${status},用时 ${check.elapsedMs}ms。`, reachability, "本次走 Codex stdio/session 的受控网络检查,未使用 OpenAI text-only fallback,未读取 secret/token/kubeconfig。" ].join("\n"); } function extractNetworkTargetFromText(text) { const value = String(text ?? ""); const url = value.match(/\bhttps?:\/\/[^\s"'<>,。!?))]+/iu)?.[0]; if (url) return url; if (/\bgithub(?:\.com)?\b|github\.com|GitHub/u.test(value)) return "https://github.com/"; const host = value.match(/\b([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)(?:\/[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]*)?/u)?.[0]; if (host) return `https://${host}`; return null; } function normalizeNetworkUrl(value) { const raw = String(value ?? "").trim().replace(/[,。!?))]+$/u, ""); if (!raw) return null; const withScheme = /^[a-z][a-z0-9+.-]*:\/\//iu.test(raw) ? raw : raw.toLowerCase() === "github" ? "https://github.com/" : `https://${raw}`; try { const url = new URL(withScheme); url.username = ""; url.password = ""; url.search = ""; url.hash = ""; if (!url.pathname) url.pathname = "/"; return url.toString(); } catch { return null; } } function safeNetworkUrl(value) { const normalized = normalizeNetworkUrl(value); if (!normalized) return "unknown"; try { const url = new URL(normalized); url.username = ""; url.password = ""; url.search = ""; url.hash = ""; return redactText(url.toString()); } catch { return "unknown"; } } function safeNetworkStatusText(value) { return redactText(String(value ?? "").replace(/\s+/gu, " ").trim()).slice(0, 80); } function parseNetworkAllowlist(value) { return String(value ?? "") .split(/[,;\s]+/u) .map((item) => item.trim().toLowerCase()) .filter(Boolean); } function networkHostMatches(host, pattern) { if (!pattern) return false; if (pattern.startsWith("*.")) return host === pattern.slice(2) || host.endsWith(pattern.slice(1)); return host === pattern; } function isForbiddenNetworkHost(host) { if (!host) return true; if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true; if (/^(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel|hwlab-patch-panel)(?:\.|$)/iu.test(host)) return true; if (!host.includes(".") && host !== "github.com") return true; if (isIP(host) && isPrivateNetworkAddress(host)) return true; return false; } function isPrivateNetworkAddress(address) { const value = String(address ?? "").trim().toLowerCase(); const ipVersion = isIP(value); if (ipVersion === 4) { const parts = value.split(".").map((part) => Number.parseInt(part, 10)); return parts[0] === 10 || parts[0] === 127 || (parts[0] === 169 && parts[1] === 254) || (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) || (parts[0] === 192 && parts[1] === 168) || parts[0] === 0; } if (ipVersion === 6) { return value === "::1" || value.startsWith("fc") || value.startsWith("fd") || value.startsWith("fe80:") || value === "::"; } return false; } function withTimeout(promise, timeoutMs, message) { let timer; return Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs); }) ]).finally(() => clearTimeout(timer)); } async function collectWorkspaceSidecarEvidence({ message, workspace, traceId, env, now } = {}) { const intent = detectWorkspaceSidecarIntent(message); const toolCalls = []; const events = []; let skills = notRequestedSkills(); let outputTruncated = false; if (intent.pwd) { const toolCall = await pwdToolCall({ workspace, traceId }); toolCalls.push(toolCall); events.push(toolTraceEvent(toolCall, { labelPrefix: "sidecar" })); } if (intent.ls) { const toolCall = await lsToolCall({ workspace, target: intent.target, traceId }); toolCalls.push(toolCall); events.push(toolTraceEvent(toolCall, { labelPrefix: "sidecar" })); outputTruncated = outputTruncated || toolCall.outputTruncated; } if (intent.skills) { skills = await discoverSkillsForStdio({ env, traceId }); toolCalls.push({ id: `tool_${randomUUID()}`, type: "file-read", name: "skills.discover", status: skills.status === "ready" ? "completed" : "blocked", cwd: workspace, exitCode: skills.status === "ready" ? 0 : 1, stdout: skills.status === "ready" ? `skills=${skills.count}` : "", stderrSummary: skills.status === "ready" ? "" : "skills_unavailable", outputTruncated: Boolean(skills.truncated), traceId }); events.push({ type: "tool_call", status: skills.status === "ready" ? "completed" : "blocked", label: `sidecar:skills.discover:${skills.status === "ready" ? "completed" : "blocked"}`, toolName: "skills.discover", outputSummary: skills.status === "ready" ? `skills=${skills.count}` : "skills_unavailable", outputTruncated: Boolean(skills.truncated) }); outputTruncated = outputTruncated || Boolean(skills.truncated); } if (intent.smoke) { const smokeCalls = await workspaceSmokeToolCalls({ workspace, traceId, now }); toolCalls.push(...smokeCalls); for (const call of smokeCalls) events.push(toolTraceEvent(call, { labelPrefix: "sidecar" })); outputTruncated = outputTruncated || smokeCalls.some((call) => call.outputTruncated); } return { intent, toolCalls, skills, events, outputTruncated }; } function toolTraceEvent(toolCall, { labelPrefix = "tool" } = {}) { return { type: "tool_call", status: toolCall.status, label: `${labelPrefix}:${toolCall.name}:${toolCall.status}`, toolName: toolCall.name, outputSummary: firstNonEmpty(toolCall.stderrSummary, toolCall.stdout ? `${toolCall.name} output captured` : null), outputTruncated: toolCall.outputTruncated === true }; } function detectWorkspaceSidecarIntent(message) { const text = String(message ?? ""); const lower = text.toLowerCase(); const wantsPwd = /\bpwd\b/u.test(lower) || /(?:当前|打印|显示|查看).{0,12}(?:工作目录|目录|路径|workspace)/iu.test(text); const explicitLs = /\bls\b/u.test(lower) || /(?:列出|查看).{0,12}(?:目录|文件)/u.test(text); return { pwd: wantsPwd, ls: explicitLs && (!wantsPwd || /\bls\b/u.test(lower) || /(?:文件|目录列表|所有文件)/u.test(text)), skills: /(?:可用|能使用|加载|列出|所有).{0,16}(?:skills?|skill|技能)|(?:skills?|skill|技能).{0,16}(?:可用|能使用|加载|列出|所有)/iu.test(text), smoke: /(?:write|写入|读取|清理|cleanup|smoke|冒烟).{0,24}(?:workspace|工作区|临时|tmp)|(?:workspace|工作区).{0,24}(?:write|写入|读取|清理|smoke|冒烟)/iu.test(text), target: extractWorkspaceTarget(text) }; } function extractWorkspaceTarget(text) { const value = String(text ?? ""); const quoted = value.match(/[`"']([^`"']{1,240})[`"']/u)?.[1]; if (quoted) return quoted.trim(); const inlinePath = value.match(/((?:\.{1,2}\/|\/)[A-Za-z0-9._@:/+=-]+|[A-Za-z0-9._@+=-]+\.[A-Za-z0-9._-]+)/u)?.[1]; return inlinePath || "."; } async function pwdToolCall({ workspace, traceId }) { try { const workspaceStat = await stat(workspace); if (!workspaceStat.isDirectory()) { return blockedToolCall({ name: "pwd", type: "workspace-read", workspace, traceId, reason: "configured workspace is not a directory" }); } } catch (error) { return blockedToolCall({ name: "pwd", type: "workspace-read", workspace, traceId, reason: error.message || "configured workspace is not readable" }); } return { id: `tool_${randomUUID()}`, type: "workspace-read", name: "pwd", status: "completed", cwd: workspace, command: "pwd", exitCode: 0, stdout: redactText(workspace), stderrSummary: "", outputTruncated: false, traceId }; } async function lsToolCall({ workspace, target = ".", traceId }) { const targetInfo = resolveWorkspaceTarget(workspace, target, { mustExist: true }); const blockedReason = targetInfo.blocked ? targetInfo.reason : await targetInfo.check(); if (blockedReason) { return blockedToolCall({ name: "ls", type: "workspace-read", workspace, traceId, reason: blockedReason }); } try { const info = await stat(targetInfo.path); const entries = info.isDirectory() ? await readdir(targetInfo.path, { withFileTypes: true }) : [{ name: path.basename(targetInfo.path), isDirectory: () => false, isSymbolicLink: () => false }]; const lines = entries .sort((a, b) => a.name.localeCompare(b.name, "en")) .slice(0, 200) .map((entry) => `${entry.isDirectory() ? "dir " : entry.isSymbolicLink() ? "link" : "file"} ${entry.name}`); const bounded = boundToolOutput(redactText(lines.join("\n"))); return { id: `tool_${randomUUID()}`, type: "workspace-read", name: "ls", status: "completed", cwd: workspace, command: `ls ${safeDisplayPath(targetInfo.relative || ".")}`, exitCode: 0, stdout: bounded.text, stderrSummary: entries.length > 200 ? "entry limit 200 reached" : "", outputTruncated: bounded.truncated || entries.length > 200, traceId }; } catch (error) { return blockedToolCall({ name: "ls", type: "workspace-read", workspace, traceId, reason: error.message }); } } async function workspaceSmokeToolCalls({ workspace, traceId, now }) { const parentDir = path.join(workspace, ".hwlab-code-agent-smoke"); const dir = path.join(parentDir, `run-${randomUUID()}`); const filename = "stdio-smoke.txt"; const filePath = path.join(dir, filename); const content = `traceId=${traceId}\ncreatedAt=${timestampFor(now)}\n`; const calls = []; try { await mkdir(dir, { recursive: true }); await writeFile(filePath, content, { encoding: "utf8", flag: "wx" }); calls.push({ id: `tool_${randomUUID()}`, type: "workspace-write", name: "workspace.smoke.write", status: "completed", cwd: workspace, command: `write ${safeDisplayPath(path.relative(workspace, filePath))}`, exitCode: 0, stdout: "written", stderrSummary: "", outputTruncated: false, traceId }); const readBack = await readFile(filePath, "utf8"); const bounded = boundToolOutput(redactText(readBack), 300); calls.push({ id: `tool_${randomUUID()}`, type: "workspace-read", name: "workspace.smoke.read", status: "completed", cwd: workspace, command: `read ${safeDisplayPath(path.relative(workspace, filePath))}`, exitCode: 0, stdout: bounded.text, stderrSummary: "", outputTruncated: bounded.truncated, traceId }); await rm(filePath, { force: true }); await rmdir(dir); await rmdir(parentDir).catch(() => {}); calls.push({ id: `tool_${randomUUID()}`, type: "workspace-write", name: "workspace.smoke.cleanup", status: "completed", cwd: workspace, command: `rm ${safeDisplayPath(path.relative(workspace, filePath))}`, exitCode: 0, stdout: "removed", stderrSummary: "", outputTruncated: false, traceId }); } catch (error) { calls.push(blockedToolCall({ name: "workspace.smoke", type: "workspace-write", workspace, traceId, reason: error.message })); try { await rm(dir, { recursive: true, force: true }); await rmdir(parentDir).catch(() => {}); } catch { // Best-effort cleanup; blocked tool evidence keeps readiness precise. } } return calls; } async function discoverSkillsForStdio({ env = process.env, traceId } = {}) { const checkedDirs = resolveSkillDirs(env); const sources = []; const items = []; for (const dir of checkedDirs) { if (!pathReadableSync(dir)) { sources.push({ path: dir, status: "missing_or_unreadable" }); continue; } sources.push({ path: dir, status: "readable" }); const manifests = await skillManifestPaths(dir); for (const manifestPath of manifests) { const manifest = await readSkillManifest(manifestPath); if (!manifest) continue; items.push({ name: manifest.name, summary: manifest.description ?? firstMarkdownSummary(manifest.body) ?? "No description provided.", source: manifestPath, sourceRoot: dir, traceId }); } } const unique = dedupeSkills(items).sort((a, b) => a.name.localeCompare(b.name, "en")); const returned = unique.slice(0, CODEX_STDIO_SKILL_LIMIT); if (returned.length === 0) { return { status: "blocked", code: "skills_unavailable", items: [], count: 0, totalCount: 0, checkedDirs, sources, blockers: [{ code: "skills_unavailable", sourceIssue: "pikasTech/HWLAB#136", summary: "No readable SKILL.md files were found in the configured Codex stdio skills directories." }], valuesPrinted: false }; } return { status: "ready", code: "skills_ready", items: returned, count: returned.length, totalCount: unique.length, truncated: unique.length > returned.length, checkedDirs, sources, blockers: [], valuesPrinted: false }; } function resolveSkillDirs(env = process.env) { const configured = String(firstNonEmpty(env.HWLAB_CODE_AGENT_SKILLS_DIRS, env.UNIDESK_SKILLS_PATH, "")) .split(/[,;]/u) .flatMap((part) => part.split(path.delimiter)) .map((dir) => dir.trim()) .filter(Boolean); if (env.HWLAB_CODE_AGENT_SKILLS_STRICT === "1") { return [...new Set(configured.map((dir) => path.resolve(dir)))]; } return [...new Set([ ...configured, "/app/skills", path.join(repoRoot, "skills"), path.join(os.homedir(), ".agents", "skills"), "/root/.agents/skills", "/home/ubuntu/.agents/skills" ].map((dir) => path.resolve(dir)))]; } async function skillManifestPaths(skillsDir) { const direct = path.join(skillsDir, "SKILL.md"); const manifests = []; if (pathReadableSync(direct)) manifests.push(direct); let entries = []; try { entries = await readdir(skillsDir, { withFileTypes: true }); } catch { return manifests; } for (const entry of entries) { if (!entry.isDirectory()) continue; const manifestPath = path.join(skillsDir, entry.name, "SKILL.md"); if (pathReadableSync(manifestPath)) manifests.push(manifestPath); } return manifests; } async function readSkillManifest(manifestPath) { try { const text = await readFile(manifestPath, "utf8"); const frontmatter = parseFrontmatter(text); const body = text.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/u, ""); const name = frontmatter.name ?? path.basename(path.dirname(manifestPath)); return name ? { name, description: frontmatter.description, body } : null; } catch { return null; } } function parseFrontmatter(text) { const match = String(text ?? "").match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/u); if (!match) return {}; const data = {}; for (const line of match[1].split(/\r?\n/u)) { const field = line.match(/^([A-Za-z0-9_-]+):\s*(.*)\s*$/u); if (!field) continue; data[field[1]] = field[2].replace(/^["']|["']$/gu, "").trim(); } return data; } function firstMarkdownSummary(body) { return String(body ?? "") .split(/\r?\n/u) .map((line) => line.trim()) .find((line) => line && !line.startsWith("#") && !line.startsWith("-")) ?? null; } function dedupeSkills(items) { const seen = new Set(); const deduped = []; for (const item of items) { const key = item.name.toLowerCase(); if (seen.has(key)) continue; seen.add(key); deduped.push(item); } return deduped; } function notRequestedSkills() { return { status: "not_requested", items: [], count: 0, blockers: [] }; } function codexReplyWithSidecar(content) { return String(content ?? "").trim(); } function resolveWorkspaceTarget(workspace, target, { mustExist = false } = {}) { const rawTarget = String(target || ".").trim(); if (!rawTarget) return { blocked: true, reason: "missing target path" }; if (isForbiddenPath(rawTarget)) { return { blocked: true, reason: "security_blocked: target path may expose secrets or forbidden runtime material" }; } const resolved = path.resolve(workspace, rawTarget); let realWorkspace = workspace; let realResolved = resolved; try { realWorkspace = realpathSync(workspace); realResolved = existsSync(resolved) ? realpathSync(resolved) : path.resolve(realWorkspace, path.relative(workspace, resolved)); } catch { realWorkspace = path.resolve(workspace); realResolved = path.resolve(resolved); } if (!isPathInside(realResolved, realWorkspace)) { return { blocked: true, reason: "security_blocked: target path is outside the runner workspace" }; } const relative = path.relative(realWorkspace, realResolved) || "."; if (isForbiddenPath(relative)) { return { blocked: true, reason: "security_blocked: target path is not allowed" }; } return { path: realResolved, relative, async check() { if (!mustExist) return null; try { await stat(realResolved); return null; } catch { return "target path is not readable"; } } }; } function isPathInside(child, parent) { const relative = path.relative(parent, child); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); } function isForbiddenPath(value) { const normalized = String(value ?? "").replaceAll("\\", "/").toLowerCase(); return /(^|\/)(?:\.env(?:\.|$)|\.npmrc$|\.pypirc$|id_rsa$|id_ed25519$|kubeconfig$|k3s\.yaml$|credentials?$|secrets?$|token(?:s)?$|database-url$)/u.test(normalized) || /(?:secret|token|password|passwd|private[_-]?key|openai_api_key|database_url|kubeconfig)/u.test(normalized); } function blockedToolCall({ name, type, workspace, traceId, reason }) { return { id: `tool_${randomUUID()}`, type, name, status: "blocked", cwd: workspace, exitCode: 1, stdout: "", stderrSummary: `security_blocked: ${redactText(reason)}`, outputTruncated: false, traceId }; } function safeDisplayPath(value) { return redactText(String(value ?? ".")).replace(/\s+/gu, " "); } function buildCodexUserPrompt(message, { conversationId, traceId, conversationFacts }) { return [ `conversationId: ${conversationId}`, `traceId: ${traceId}`, codexConversationFactsPrompt(conversationFacts), "", "User message:", String(message ?? "").trim() ].filter((line) => line !== null && line !== undefined).join("\n"); } function codexConversationFactsPrompt(conversationFacts) { if (!conversationFacts || typeof conversationFacts !== "object" || Number(conversationFacts.turnCount ?? 0) <= 0) { return null; } return [ "Prior session facts (bounded, redacted; use for Chinese answers about previous turns):", `- sessionId=${safePromptFact(conversationFacts.sessionId)}; sessionStatus=${safePromptFact(conversationFacts.sessionStatus)}; sessionMode=${safePromptFact(conversationFacts.sessionMode)}; turnCount=${safePromptFact(conversationFacts.turnCount)}.`, `- provider/backend/runner=${safePromptFact(conversationFacts.latestProvider)}/${safePromptFact(conversationFacts.latestBackend)}/${safePromptFact(conversationFacts.runnerKind)}; capabilityLevel=${safePromptFact(conversationFacts.capabilityLevel)}.`, `- workspace=${safePromptFact(conversationFacts.workspace)}; sandbox=${safePromptFact(conversationFacts.sandbox)}.`, `- traceIds=${promptList(conversationFacts.traceIds, 6)}; latestTraceId=${safePromptFact(conversationFacts.latestTraceId)}.`, `- skills=${promptSkills(conversationFacts.latestSkills)}.`, `- toolCalls=${promptToolCalls(conversationFacts.recentToolCalls)}.` ].join("\n"); } function promptSkills(skills) { if (!skills || typeof skills !== "object") return "not_requested"; const names = Array.isArray(skills.names) ? skills.names.filter(Boolean).slice(0, 8) : []; const count = skills.totalCount ?? skills.count ?? names.length; return `${safePromptFact(skills.status)} count=${safePromptFact(count)}${names.length ? ` names=${names.map(safePromptFact).join(",")}` : ""}`; } function promptToolCalls(toolCalls) { if (!Array.isArray(toolCalls) || toolCalls.length === 0) return "none"; return toolCalls .slice(-6) .map((toolCall) => [toolCall?.name, toolCall?.status, toolCall?.route].filter(Boolean).map(safePromptFact).join(":")) .filter(Boolean) .join(",") || "none"; } function promptList(values, limit) { return Array.isArray(values) && values.length > 0 ? values.filter(Boolean).slice(-limit).map(safePromptFact).join(",") : "none"; } function safePromptFact(value) { if (value === undefined || value === null || value === "") return "none"; return redactText(String(value).replace(/\s+/gu, " ").trim()).slice(0, 180); } 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 resolveCodexHome(env = process.env) { return path.resolve(firstNonEmpty( env.CODEX_HOME, env.HWLAB_CODE_AGENT_CODEX_HOME, path.join(env.HOME || process.env.HOME || os.homedir(), ".codex") )); } 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 app-server --listen stdio://", cancelSupported: true, reapSupported: true, secretMaterialStored: false, valuesRedacted: true }; } function tokenBoundaryState(env = process.env, codexHomeFiles = null) { const authPresent = codexHomeFiles?.authPresent === true && codexHomeFiles?.authReadable === true; const present = authPresent || env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY === "configured" || env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY === "present"; return { present, sources: [ authPresent ? "CODEX_HOME/auth.json" : null, env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY ? "HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY" : null ].filter(Boolean), childEnvStripsProviderSecrets: true, 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 directoryStateSync(targetPath, { writableRequired = false } = {}) { const state = { path: targetPath, exists: false, readable: false, writable: false, writeRequired: writableRequired }; try { state.exists = existsSync(targetPath); if (!state.exists) return state; state.readable = accessSyncBoolean(targetPath, fsConstants.R_OK); state.writable = accessSyncBoolean(targetPath, fsConstants.W_OK); return state; } catch { return state; } } function codexHomeFilesStateSync(codexHome) { const configPath = path.join(codexHome, "config.toml"); const authPath = path.join(codexHome, "auth.json"); const configPresent = filePresentSync(configPath); const authPresent = filePresentSync(authPath); return { configPath, authPath, configPresent, configReadable: configPresent && accessSyncBoolean(configPath, fsConstants.R_OK), authPresent, authReadable: authPresent && accessSyncBoolean(authPath, fsConstants.R_OK), mountContract: "writable CODEX_HOME emptyDir with read-only config.toml/auth.json file mounts", secretMaterialRead: false, valuesRedacted: true }; } function filePresentSync(targetPath) { try { return existsSync(targetPath) && statSync(targetPath).isFile(); } catch { return false; } } function accessSyncBoolean(target, mode) { try { accessSync(target, mode); return true; } catch { return false; } } function pathReadableSync(targetPath) { return accessSyncBoolean(targetPath, fsConstants.R_OK); } function codexBinaryState(command, env = process.env) { const resolvedPath = commandPathSync(command, env); const present = Boolean(resolvedPath); const versionProbe = present ? codexVersionProbe(command, env) : { attempted: false, ok: false, version: null, error: null, exitCode: null }; const nativeDependency = present ? codexNativeDependencyState(resolvedPath) : { present: false, path: null, evidence: "Codex CLI command was not present, so native dependency was not checked." }; return { command, present, binaryPresent: present, path: resolvedPath, executable: versionProbe.ok, version: versionProbe.version, versionDetected: versionProbe.ok, versionProbe, nativeDependencyPresent: nativeDependency.present, nativeDependencyPath: nativeDependency.path, nativeDependencyEvidence: nativeDependency.evidence, nextEvidence: present ? versionProbe.ok ? "codex --version was probed without printing secrets; stdio protocol readiness is checked separately." : `Codex CLI command ${command} exists but --version did not complete successfully.` : `Install/provide a repo-controlled Codex CLI binary on PATH or set HWLAB_CODE_AGENT_CODEX_COMMAND to an approved binary path; checked command=${command}.`, secretMaterialRead: false, valuesRedacted: true }; } function codexNativeDependencyState(resolvedCommandPath) { const candidates = []; let resolved = path.resolve(resolvedCommandPath); try { resolved = realpathSync(resolved); } catch { // Fall back to the resolved command path; the binary blocker will report executability. } const packageRoot = findPackageRoot(resolved); if (packageRoot) { candidates.push( path.join(packageRoot, "..", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex"), path.join(packageRoot, "..", "codex-linux-arm64", "vendor", "aarch64-unknown-linux-musl", "codex", "codex"), path.join(packageRoot, "vendor", "x86_64-unknown-linux-musl", "codex", "codex"), path.join(packageRoot, "vendor", "aarch64-unknown-linux-musl", "codex", "codex") ); } const existing = candidates.find((candidate) => existsSync(candidate) && accessSyncBoolean(candidate, fsConstants.X_OK)) ?? null; return { present: Boolean(existing), path: existing, evidence: existing ? "Native @openai/codex executable dependency exists and is executable." : "Expected native @openai/codex executable dependency was not found next to the CLI package." }; } function findPackageRoot(resolvedCommandPath) { let current = path.dirname(resolvedCommandPath); for (let depth = 0; depth < 8; depth += 1) { if (existsSync(path.join(current, "package.json"))) return current; const next = path.dirname(current); if (next === current) return null; current = next; } return null; } function commandPathSync(command, env = process.env) { if (!command) return null; if (command.includes("/") || command.includes("\\")) { return existsSync(command) ? command : null; } const paths = String(Object.hasOwn(env, "PATH") ? env.PATH : process.env.PATH || "").split(path.delimiter).filter(Boolean); const resolved = paths.map((dir) => path.join(dir, command)).find((candidate) => existsSync(candidate)); return resolved ?? null; } function commandOnPathSync(command, env = process.env) { return Boolean(commandPathSync(command, env)); } function codexVersionProbe(command, env = process.env) { try { const result = spawnSync(command, ["--version"], { env: childProcessEnv(env), encoding: "utf8", timeout: 3000, windowsHide: true }); const output = redactText(`${result.stdout ?? ""}\n${result.stderr ?? ""}`).trim(); const version = output.split(/\s+/u).find((part) => /\d+\.\d+(?:\.\d+)?/u.test(part)) ?? (output ? tailText(output, 160) : null); return { attempted: true, ok: result.status === 0 && Boolean(version), version, exitCode: result.status, error: result.error ? redactText(result.error.message) : null }; } catch (error) { return { attempted: true, ok: false, version: null, exitCode: null, error: redactText(error.message) }; } } function protocolState({ command, binary, supervisor, initialized }) { const protocolInitialized = initialized === true; const wired = binary.present === true && supervisor.configured === true && protocolInitialized; const probeReady = binary.present === true && supervisor.configured === true; return { transport: "stdio", command: `${command} app-server --listen stdio://`, protocolVersion: CODEX_APP_SERVER_PROTOCOL, adapter: "Codex app-server JSON-RPC line protocol", requiredMethods: [...CODEX_APP_SERVER_REQUIRED_METHODS], initialized: protocolInitialized, toolsObserved: [], missingTools: [], wired, probeReady, status: wired ? "wired" : "blocked", blocker: wired ? null : "stdio_protocol_not_wired", summary: wired ? "Codex app-server stdio protocol is initialized through the repo-owned JSON-RPC client." : "Codex app-server stdio protocol has not been proven through initialize/thread/start/turn/start.", nextEvidence: "Start `codex app-server --listen stdio://`, run initialize, then thread/start or thread/resume plus turn/start without printing token material.", secretMaterialRead: false, valuesRedacted: true }; } function cachedProtocolReady({ command, workspace, codexHome, probe }) { if (!probe || probe.ready !== true) return null; if (probe.command !== command || probe.workspace !== workspace || probe.codexHome !== codexHome) return null; if (Number.isFinite(probe.expiresAtMs) && probe.expiresAtMs <= Date.now()) return null; return probe.initialized === true; } function cachedCommandProbe({ command, workspace, codexHome, probe }) { if (!probe || probe.ready !== true) return null; if (probe.command !== command || probe.workspace !== workspace || probe.codexHome !== codexHome) return null; if (Number.isFinite(probe.expiresAtMs) && probe.expiresAtMs <= Date.now()) return null; return probe; } function protocolProbeSummary({ command, workspace, codexHome, probe }) { if (!probe || probe.command !== command || probe.workspace !== workspace || probe.codexHome !== codexHome) { return { status: "not_run", ready: false, initialized: false, requiredMethods: [...CODEX_APP_SERVER_REQUIRED_METHODS], secretMaterialRead: false, valuesRedacted: true }; } return { status: probe.status, ready: probe.ready === true, initialized: probe.initialized === true, requiredMethods: Array.isArray(probe.requiredMethods) ? [...probe.requiredMethods] : [...CODEX_APP_SERVER_REQUIRED_METHODS], startedAt: probe.startedAt, finishedAt: probe.finishedAt, error: probe.error ?? null, secretMaterialRead: false, valuesRedacted: true }; } function commandProbeSummary({ command, workspace, codexHome, probe }) { if (!probe || probe.command !== command || probe.workspace !== workspace || probe.codexHome !== codexHome) { return { status: "not_run", ready: false, probe: "workspace.pwd", toolCalls: [], secretMaterialRead: false, valuesRedacted: true }; } return { status: probe.status, ready: probe.ready === true, probe: probe.probe ?? "workspace.pwd", conversationId: probe.conversationId ?? null, traceId: probe.traceId ?? null, toolCalls: Array.isArray(probe.toolCalls) ? [...probe.toolCalls] : [], startedAt: probe.startedAt, finishedAt: probe.finishedAt, error: probe.error ?? null, timeoutMs: probe.timeoutMs ?? CODEX_STDIO_COMMAND_PROBE_TIMEOUT_MS, secretMaterialRead: false, valuesRedacted: true }; } function lifecycleState({ supervisor, idleTimeoutMs, maxSessions, activeSessions }) { const ready = supervisor.configured === true; return { implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE, status: ready ? "present" : "blocked", present: ready, create: ready, reuse: ready, cancel: ready, reap: ready, idleTimeout: ready, idleTimeoutMs, traceCapture: ready, maxSessions, activeSessions, inMemoryIndex: ready, codexThreadIdCaptured: ready, blocker: ready ? null : "runner_lifecycle_missing", summary: ready ? "Repo-owned supervisor contract covers create/reuse/cancel/reap/idle timeout/trace capture." : "Repo-owned lifecycle supervisor is missing; long-lived Code Agent session gate must remain blocked.", secretMaterialStored: false, valuesRedacted: true }; } function workspaceContractState(workspaceInfo, sandbox, workspace) { const ready = workspaceInfo.exists && workspaceInfo.readable && (sandbox !== "workspace-write" || workspaceInfo.writable === true); return { path: workspace, status: ready ? "ready" : "blocked", mounted: workspaceInfo.exists, readable: workspaceInfo.readable, writable: workspaceInfo.writable, sandbox, writeRequired: sandbox === "workspace-write", blocker: ready ? null : "workspace_mount_missing", secretMaterialRead: false, valuesRedacted: true }; } function cancelReapTraceState(lifecycle) { const ready = lifecycle.cancel === true && lifecycle.reap === true && lifecycle.traceCapture === true; return { status: ready ? "ready" : "blocked", cancel: lifecycle.cancel === true, reap: lifecycle.reap === true, traceCapture: lifecycle.traceCapture === true, idleTimeout: lifecycle.idleTimeout === true, blocker: ready ? null : "runner_lifecycle_missing" }; } function runtimeContract({ ready, binary, protocol, lifecycle, workspaceInfo, workspace, sandbox, codexHome, codexHomeInfo, codexHomeFiles, tokenBoundary, egress, childEnvBoundary, commandProbe }) { return { contractVersion: "codex-runtime-feasibility-v1", status: ready ? "ready" : "blocked", ready, binary: { status: binary.present && binary.executable && binary.nativeDependencyPresent ? "present" : binary.present ? "blocked" : "missing", command: binary.command, present: binary.present, executable: binary.executable === true, nativeDependencyPresent: binary.nativeDependencyPresent === true, version: binary.version, versionDetected: binary.versionDetected, nextEvidence: binary.nextEvidence }, stdioProtocol: { status: protocol.status, wired: protocol.wired, command: protocol.command, protocolVersion: protocol.protocolVersion, requiredMethods: [...protocol.requiredMethods], initialized: protocol.initialized === true, nextEvidence: protocol.nextEvidence }, commandProbe: commandProbe ?? { status: "not_run", ready: false, probe: "workspace.pwd", toolCalls: [], secretMaterialRead: false, valuesRedacted: true }, lifecycleSupervisor: { status: lifecycle.status, present: lifecycle.present, create: lifecycle.create, reuse: lifecycle.reuse, cancel: lifecycle.cancel, reap: lifecycle.reap, traceCapture: lifecycle.traceCapture, idleTimeoutMs: lifecycle.idleTimeoutMs }, workspaceMount: workspaceContractState(workspaceInfo, sandbox, workspace), codexHome: codexHomeContractState(codexHomeInfo, codexHome, codexHomeFiles), sandbox, tokenBoundary: { status: tokenBoundary.present ? "present" : "blocked", present: tokenBoundary.present, sources: tokenBoundary.sources, secretMaterialRead: false, valuesRedacted: true }, egress: { status: egress.directPublicOpenAi ? "blocked" : egress.configured ? "configured" : "not_configured", configured: egress.configured, directPublicOpenAi: egress.directPublicOpenAi, valueRedacted: true }, childEnvBoundary, cancelReapTraceReadiness: cancelReapTraceState(lifecycle), secretBoundary: { secretsRead: false, secretValuesPrinted: false, kubeconfigRead: false, valuesRedacted: true } }; } function codexHomeContractState(codexHomeInfo, codexHome, codexHomeFiles = null) { const filesReady = codexHomeFiles?.configPresent === true && codexHomeFiles?.configReadable === true && codexHomeFiles?.authPresent === true && codexHomeFiles?.authReadable === true; const ready = codexHomeInfo.exists && codexHomeInfo.readable && codexHomeInfo.writable && filesReady; return { path: codexHome, status: ready ? "ready" : "blocked", exists: codexHomeInfo.exists, readable: codexHomeInfo.readable, writable: codexHomeInfo.writable, configToml: { path: codexHomeFiles?.configPath ?? path.join(codexHome, "config.toml"), present: codexHomeFiles?.configPresent === true, readable: codexHomeFiles?.configReadable === true }, authJson: { path: codexHomeFiles?.authPath ?? path.join(codexHome, "auth.json"), present: codexHomeFiles?.authPresent === true, readable: codexHomeFiles?.authReadable === true, valuesRedacted: true }, writeRequired: true, blocker: ready ? null : !codexHomeInfo.exists || !codexHomeInfo.readable ? "codex_home_missing" : codexHomeInfo.writable !== true ? "codex_home_write_blocked" : codexHomeFiles?.configPresent !== true || codexHomeFiles?.configReadable !== true ? "codex_home_config_missing" : "codex_home_auth_missing", secretMaterialRead: false, valuesRedacted: true }; } function childProcessEnv(env = process.env) { const noProxy = mergeNoProxy(env.NO_PROXY, env.no_proxy, CODEX_CHILD_NO_PROXY_REQUIRED, codexProviderNoProxyEntries(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(), CODEX_HOME: resolveCodexHome(env), CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "hwlab_code_agent", UNIDESK_SKILLS_PATH: firstNonEmpty(env.HWLAB_CODE_AGENT_SKILLS_DIRS, env.UNIDESK_SKILLS_PATH, "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills"), NO_PROXY: noProxy, no_proxy: noProxy, ...(env.LANG ? { LANG: env.LANG } : {}), ...(env.LC_ALL ? { LC_ALL: env.LC_ALL } : {}), ...(env.SSL_CERT_FILE ? { SSL_CERT_FILE: env.SSL_CERT_FILE } : {}), ...(env.NODE_EXTRA_CA_CERTS ? { NODE_EXTRA_CA_CERTS: env.NODE_EXTRA_CA_CERTS } : {}), ...(env.GIT_CONFIG_COUNT ? gitConfigEnv(env) : {}) }; } function gitConfigEnv(env = process.env) { const count = Number.parseInt(env.GIT_CONFIG_COUNT ?? "", 10); if (!Number.isInteger(count) || count < 0 || count > 64) return {}; const result = { GIT_CONFIG_COUNT: String(count) }; for (let index = 0; index < count; index += 1) { const keyName = `GIT_CONFIG_KEY_${index}`; const valueName = `GIT_CONFIG_VALUE_${index}`; if (typeof env[keyName] === "string") result[keyName] = env[keyName]; if (typeof env[valueName] === "string") result[valueName] = env[valueName]; } return result; } function childProcessEnvState(env = process.env) { const child = childProcessEnv(env); const inheritedForbidden = CODEX_CHILD_STRIPPED_ENV_KEYS.filter((key) => hasEnvValue(child, key)); const sourceForbiddenPresent = CODEX_CHILD_STRIPPED_ENV_KEYS.filter((key) => hasEnvValue(env, key)); const noProxyEntries = splitNoProxy(child.NO_PROXY); const requiredMissing = CODEX_CHILD_NO_PROXY_REQUIRED.filter((entry) => !noProxyEntries.includes(entry.toLowerCase())); const providerNoProxy = codexProviderNoProxyEntries(env); return { status: inheritedForbidden.length === 0 && requiredMissing.length === 0 ? "ready" : "blocked", forbiddenSourceEnvKeysPresent: sourceForbiddenPresent, forbiddenEnvPresent: inheritedForbidden.length > 0, strippedEnvKeys: sourceForbiddenPresent.filter((key) => !inheritedForbidden.includes(key)), inheritedForbiddenEnvKeys: inheritedForbidden, noProxy: { present: Boolean(child.NO_PROXY), required: [...CODEX_CHILD_NO_PROXY_REQUIRED], provider: providerNoProxy, missing: requiredMissing, merged: noProxyEntries }, codeXHome: child.CODEX_HOME, secretMaterialRead: false, valuesRedacted: true }; } function codexProviderNoProxyEntries(env = process.env) { const baseUrl = codexAppServerProviderBaseUrl(env); if (!baseUrl) return []; try { const url = new URL(baseUrl); const hostname = url.hostname.replace(/^\[|\]$/gu, ""); return [hostname, url.host].filter((entry, index, items) => entry && items.indexOf(entry) === index); } catch { return []; } } function mergeNoProxy(...parts) { const entries = []; const seen = new Set(); for (const entry of parts.flatMap((part) => Array.isArray(part) ? part : splitNoProxy(part))) { const normalized = String(entry ?? "").trim(); if (!normalized) continue; const key = normalized.toLowerCase(); if (seen.has(key)) continue; seen.add(key); entries.push(normalized); } return entries.join(","); } function splitNoProxy(value) { return String(value ?? "") .split(/[,;\s]+/u) .map((item) => item.trim()) .filter(Boolean); } 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 effectiveActivityTimeout(timeoutMs); } function effectiveActivityTimeout(timeoutMs) { return Number.isInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_CODEX_STDIO_TURN_ACTIVITY_TIMEOUT_MS; } function effectiveHardTimeout(hardTimeoutMs, activityTimeoutMs = null) { const activityMs = effectiveActivityTimeout(activityTimeoutMs); const configured = Number.isInteger(hardTimeoutMs) && hardTimeoutMs > 0 ? hardTimeoutMs : DEFAULT_CODEX_STDIO_TURN_HARD_TIMEOUT_MS; return Math.max(configured, activityMs); } 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 boundToolOutput(value, maxLength = CODEX_STDIO_TOOL_OUTPUT_LIMIT) { const text = String(value ?? ""); if (text.length <= maxLength) return { text, truncated: false }; return { text: `${text.slice(0, maxLength)}\n[output truncated at ${maxLength} bytes]`, truncated: true }; } 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***"); }