Files
pikasTech-HWLAB/internal/cloud/codex-stdio-session.mjs
T
2026-05-24 14:53:14 +08:00

3799 lines
130 KiB
JavaScript

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;
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_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: effectiveTimeout(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,
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 ? effectiveTimeout(params.timeoutMs) : undefined,
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn,
waitingFor: canceled ? "user-retry" : timeout ? "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 {
probeClient = options.createProbeRpcClient
? await options.createProbeRpcClient({ env, availability })
: options.createRpcClient
? await options.createRpcClient({ env, availability, probe: true })
: createCodexAppServerJsonLineClient({
command: availability.command,
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,
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 createCodexAppServerJsonLineClient({ command = DEFAULT_CODEX_STDIO_COMMAND, env = process.env, cwd = repoRoot, onNotification = null } = {}) {
const child = spawn(command, ["app-server", "--listen", "stdio://"], {
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
} = {}) {
const state = createAppServerTurnState({ traceRecorder, session });
const effectiveMs = effectiveTimeout(timeoutMs);
if (typeof client.setNotificationHandler === "function") {
client.setNotificationHandler((message) => state.handle(message));
}
try {
let threadId = optionalId(session?.threadId);
if (threadId) {
traceRecorder.append({
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);
traceRecorder.append({
type: "thread",
status: "completed",
label: "thread:resume:completed",
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn,
threadId,
waitingFor: "turn/start"
});
} else {
traceRecorder.append({
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);
traceRecorder.append({
type: "thread",
status: "completed",
label: "thread:start:completed",
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn,
threadId,
waitingFor: "turn/start"
});
}
traceRecorder.append({
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);
traceRecorder.append({
type: "turn",
status: "started",
label: "turn:start:completed",
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn,
threadId,
turnId,
waitingFor: "turn/completed"
});
const rawResult = typeof client.waitForTurn === "function"
? await client.waitForTurn(turnId, effectiveMs)
: await state.wait(effectiveMs, client.closedPromise);
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;
});
function setThreadId(value) {
threadId = optionalId(value) ?? threadId;
}
function setTurnId(value) {
turnId = optionalId(value) ?? turnId;
}
function finish(status, error = null, extra = {}) {
if (terminal) return;
terminal = {
terminalStatus: appServerTerminalStatus(status),
terminalError: error ? redactText(error) : null,
threadId,
turnId,
...extra
};
terminalResolve(terminal);
}
function handle(message) {
const method = typeof message?.method === "string" ? message.method : "unknown";
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"));
traceRecorder?.append({
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"));
traceRecorder?.append({
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) {
traceRecorder?.append({
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;
traceRecorder?.append({
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") {
traceRecorder?.append({
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";
traceRecorder?.append({
type: "error",
status: "failed",
label: "app-server:error",
errorCode: "codex_stdio_failed",
message: redactText(message),
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: "user-retry"
});
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;
traceRecorder?.append({
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) {
let timer;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`Codex app-server turn timed out after ${timeoutMs}ms`)), timeoutMs);
});
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,
appServerExit: exit
};
}));
}
try {
return await Promise.race([...candidates, timeoutPromise]);
} finally {
clearTimeout(timer);
}
}
function snapshot() {
return {
threadId,
turnId,
assistantText: redactText(assistantText),
finalResponse: redactText(firstNonEmpty(finalResponse, assistantText)),
terminalStatus: terminal?.terminalStatus ?? null,
terminalError: terminal?.terminalError ?? null
};
}
return {
handle,
setThreadId,
setTurnId,
wait,
snapshot
};
}
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,
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);
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()));
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],
missing: requiredMissing,
merged: noProxyEntries
},
codeXHome: child.CODEX_HOME,
secretMaterialRead: false,
valuesRedacted: true
};
}
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 Number.isInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs : 150000;
}
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***");
}