Files
pikasTech-HWLAB/internal/cloud/codex-stdio-session.mjs
T
2026-05-23 23:45:09 +00:00

2405 lines
82 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { spawn, spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { accessSync, constants as fsConstants, existsSync, realpathSync } from "node:fs";
import { mkdir, readdir, readFile, rm, rmdir, stat, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const CODEX_STDIO_PROVIDER = "codex-stdio";
export const CODEX_STDIO_BACKEND = "hwlab-cloud-api/codex-mcp-stdio";
export const CODEX_STDIO_RUNNER_KIND = "codex-mcp-stdio-runner";
export const CODEX_STDIO_SESSION_MODE = "codex-mcp-stdio-long-lived";
export const CODEX_STDIO_IMPLEMENTATION_TYPE = "repo-owned-codex-mcp-stdio-session";
export const CODEX_STDIO_CAPABILITY_LEVEL = "long-lived-codex-stdio-session";
export const CODEX_STDIO_SANDBOX = "workspace-write";
export const DEFAULT_CODEX_STDIO_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
export const DEFAULT_CODEX_STDIO_MAX_SESSIONS = 64;
export const DEFAULT_CODEX_STDIO_COMMAND = "codex";
export const DEFAULT_CODEX_STDIO_PROBE_TTL_MS = 30 * 1000;
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const MCP_PROTOCOL_VERSION = "2024-11-05";
const CODEX_STDIO_REQUIRED_TOOLS = Object.freeze(["codex", "codex-reply"]);
const CODEX_STDIO_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 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 sessions = new Map();
const conversations = new Map();
let rpcClient = options.rpcClient ?? null;
let rpcStartedAt = null;
let rpcToolNames = null;
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 tokenBoundary = tokenBoundaryState(env);
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 egress = egressState(env);
const protocol = protocolState({
command,
binary,
supervisor,
toolsObserved: params.toolsObserved ?? rpcToolNames ?? cachedProbeTools({ 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 (!tokenBoundary.present) {
blockers.push({
code: "provider_token_boundary",
sourceIssue: "pikasTech/HWLAB#275",
summary: "A long-lived Codex stdio runner needs a repo-owned provider token boundary; this endpoint only reports token presence and never reads or prints token material."
});
}
if (egress.directPublicOpenAi) {
blockers.push({
code: "codex_stdio_egress_boundary",
sourceIssue: "pikasTech/HWLAB#275",
summary: "Codex stdio provider egress must use the DEV egress/proxy boundary, not direct public api.openai.com."
});
}
const ready = blockers.length === 0;
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,
sandbox,
enabled,
supervisor,
tokenBoundary,
egress,
protocol,
stdioProtocol: protocol,
protocolProbe: protocolProbeSummary({ command, workspace, codexHome, probe: protocolProbe }),
commandProbe: commandProbeState,
sessionLifecycle: lifecycle,
lifecycleSupervisor: lifecycle,
workspaceMount: workspaceContractState(workspaceInfo, sandbox, workspace),
cancelReapTraceReadiness: cancelReapTraceState(lifecycle),
runtimeContract: runtimeContract({
ready,
binary,
protocol,
lifecycle,
workspaceInfo,
workspace,
sandbox,
codexHome,
codexHomeInfo,
tokenBoundary,
egress,
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);
let availability = describe({ ...params, env, workspace, sandbox });
const startupBlockers = availability.blockers.filter((blocker) => blocker.code !== "stdio_protocol_not_wired");
if (startupBlockers.length > 0) {
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) {
throw codexStdioError(acquire.code, acquire.message, {
availability,
session: acquire.session,
blockers: [acquire.blocker]
});
}
let session = acquire.session;
const events = [
"stdio:acquire",
session.reused ? "session:reused" : "session:created",
session.threadId ? "codex-thread:reused" : "codex-thread:create",
`turn:${session.turn}`
];
const startedAt = timestamp;
try {
const client = await ensureRpcClient({ env, availability, timeoutMs: params.timeoutMs });
availability = describe({ ...params, env, workspace, sandbox });
events.push("stdio:ready");
const sidecar = await collectWorkspaceSidecarEvidence({
message: params.message,
workspace,
traceId,
env,
now
});
if (shouldReturnSidecarOnly(sidecar)) {
if (sidecar.skills.status === "blocked") {
throw codexStdioError("skills_unavailable", "No usable SKILL.md manifest was found for the Codex stdio runner.", {
availability,
session,
toolCalls: sidecar.toolCalls,
skills: sidecar.skills,
blockers: sidecar.skills.blockers,
route: null,
toolName: "skills.discover"
});
}
session = releaseSession(session.sessionId, {
now,
traceId,
conversationId,
reused: session.reused,
status: "idle"
}) ?? session;
events.push(...sidecar.events);
return codexStdioSidecarResult({
message: params.message,
workspace,
sandbox,
session,
sidecar,
availability,
traceId,
startedAt,
events,
model: params.model,
env,
providerTraceToolName: "workspace-sidecar"
});
}
const toolName = session.threadId ? "codex-reply" : "codex";
const toolArguments = session.threadId
? {
threadId: session.threadId,
prompt: buildCodexUserPrompt(params.message, { conversationId, traceId })
}
: {
prompt: buildCodexUserPrompt(params.message, { conversationId, traceId }),
cwd: workspace,
sandbox,
model: params.model,
"approval-policy": "never",
"developer-instructions": CODEX_STDIO_BOUNDARY_INSTRUCTIONS
};
const toolResult = await client.callTool(toolName, dropEmpty(toolArguments), effectiveTimeout(params.timeoutMs));
const codexOutput = extractCodexToolOutput(toolResult);
if (!codexOutput.content) {
throw codexStdioError("codex_stdio_empty_response", "Codex stdio tool call completed without assistant content.", {
availability,
session
});
}
session = releaseSession(session.sessionId, {
now,
traceId,
conversationId,
reused: session.reused,
threadId: codexOutput.threadId ?? session.threadId,
status: "idle"
}) ?? session;
events.push(`tool:${toolName}:completed`);
return {
provider: CODEX_STDIO_PROVIDER,
model: firstNonEmpty(params.model, env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL, "codex-default"),
backend: CODEX_STDIO_BACKEND,
content: codexReplyWithSidecar(codexOutput.content, 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-stdio",
name: toolName,
status: "completed",
cwd: workspace,
command: "codex mcp-server tools/call",
exitCode: 0,
stdout: codexOutput.threadId ? `threadId=${codexOutput.threadId}` : "threadId=captured",
stderrSummary: "",
outputTruncated: false,
traceId
}, ...sidecar.toolCalls],
skills: sidecar.skills,
runner: runnerDescriptor({ workspace, sandbox, session }),
runnerTrace: runnerTrace({
traceId,
workspace,
sandbox,
session,
events: [...events, ...sidecar.events],
startedAt,
outputTruncated: sidecar.outputTruncated
}),
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
providerTrace: {
transport: "stdio",
protocol: "mcp",
command: `${availability.command} mcp-server`,
toolName,
threadId: codexOutput.threadId ?? session.threadId ?? null,
valuesPrinted: false
}
};
} catch (error) {
closeRpcClient();
session = failSession(session.sessionId, {
now,
traceId,
conversationId,
reused: session.reused,
statusReason: error.code ?? "codex_stdio_failed"
}) ?? session;
if (error.code && (error.code.startsWith("codex_stdio") || ["skills_unavailable"].includes(error.code))) {
error.session = session;
error.availability = error.availability ?? describe({ ...params, env, workspace, sandbox });
error.runnerTrace = runnerTrace({
traceId,
workspace,
sandbox,
session,
events: [...events, `blocked:${error.code}`],
startedAt,
outputTruncated: false
});
throw error;
}
throw codexStdioError("codex_stdio_failed", redactText(error.message || "Codex stdio session failed."), {
availability,
session,
runnerTrace: runnerTrace({
traceId,
workspace,
sandbox,
session,
events: [...events, "blocked:codex_stdio_failed"],
startedAt,
outputTruncated: false
})
});
}
}
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 && Array.isArray(rpcToolNames) && rpcToolNames.length > 0) {
availability = describe({ ...params, env, workspace, command, sandbox, toolsObserved: rpcToolNames });
return ensureCommandProbeReady({
env,
workspace,
command,
sandbox,
codexHome,
availability,
now: params.now ?? nowDefault,
probeTimeoutMs: params.commandProbeTimeoutMs
});
}
if (!params.forceProbe && cachedProbeTools({ command, workspace, codexHome, probe: protocolProbe })) {
availability = describe({ ...params, env, workspace, command, sandbox, toolsObserved: protocolProbe.toolsObserved });
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 })
: createCodexMcpJsonLineClient({
command: availability.command,
env: childProcessEnv(env),
cwd: availability.workspace
});
const timeoutMs = positiveInteger(params.probeTimeoutMs, 10000);
const init = typeof probeClient.initialize === "function" ? await probeClient.initialize(timeoutMs) : null;
const toolsObserved = Array.isArray(init?.tools)
? init.tools
: typeof probeClient.listTools === "function"
? await probeClient.listTools(timeoutMs)
: [];
rpcToolNames = toolsObserved;
protocolProbe = {
status: "ready",
ready: true,
command,
workspace,
codexHome,
toolsObserved,
startedAt,
finishedAt: timestampFor(params.now ?? nowDefault),
expiresAtMs: Date.now() + probeCacheTtlMs,
secretMaterialRead: false,
valuesRedacted: true
};
availability = describe({ ...params, env, workspace, command, sandbox, toolsObserved });
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,
toolsObserved: [],
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, toolsObserved: [] });
} 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 = "interrupted";
session.updatedAt = timestamp;
session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId;
session.currentTraceId = null;
session.statusReason = params.reason ?? "cancelled";
if (rpcClient && typeof rpcClient.close === "function") {
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;
rpcToolNames = null;
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, toolsObserved: availability.protocol?.toolsObserved ?? rpcToolNames });
}
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, toolsObserved: availability.protocol?.toolsObserved ?? rpcToolNames });
}
async function ensureRpcClient({ env, availability, timeoutMs } = {}) {
if (rpcClient && typeof rpcClient.callTool === "function") return rpcClient;
rpcClient = options.createRpcClient
? await options.createRpcClient({ env, availability })
: createCodexMcpJsonLineClient({
command: availability.command,
env: childProcessEnv(env),
cwd: availability.workspace
});
if (typeof rpcClient.initialize === "function") {
const init = await rpcClient.initialize(effectiveTimeout(timeoutMs));
rpcToolNames = Array.isArray(init?.tools) ? init.tools : rpcToolNames;
}
if (!rpcToolNames && typeof rpcClient.listTools === "function") {
rpcToolNames = await rpcClient.listTools(effectiveTimeout(timeoutMs));
}
const missingTools = CODEX_STDIO_REQUIRED_TOOLS.filter((name) => !rpcToolNames?.includes(name));
if (missingTools.length > 0) {
const observedTools = Array.isArray(rpcToolNames) ? [...rpcToolNames] : [];
closeRpcClient();
throw codexStdioError("codex_stdio_protocol_blocked", `Codex stdio server did not expose required tools: ${missingTools.join(", ")}.`, {
availability: describe({
env,
workspace: availability.workspace,
sandbox: availability.sandbox,
command: availability.command,
toolsObserved: observedTools
}),
missingTools
});
}
rpcStartedAt = timestampFor(nowDefault);
return rpcClient;
}
async function acquireSession(params = {}) {
const timestamp = timestampFor(params.now ?? nowDefault);
const timestampMs = Date.parse(timestamp);
const conversationId = requiredId(params.conversationId, "cnv");
const requestedSessionId = optionalId(params.requestedSessionId);
const mappedSessionId = conversations.get(conversationId) ?? null;
if (mappedSessionId && requestedSessionId && mappedSessionId !== requestedSessionId) {
const mappedSession = sessions.get(mappedSessionId) ?? null;
return blockedAcquire({
code: "session_reuse_conflict",
summary: `conversationId ${conversationId} is already bound to sessionId ${mappedSessionId}; requested ${requestedSessionId}.`,
session: mappedSession,
timestamp,
traceId: params.traceId
});
}
const effectiveSessionId = mappedSessionId || requestedSessionId || requiredId(idFactory(), "ses");
let session = sessions.get(effectiveSessionId) ?? null;
const reused = Boolean(session);
if (session && sessionExpired(session, timestampMs)) {
session.status = "expired";
session.updatedAt = timestamp;
session.currentTraceId = null;
session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId;
return blockedAcquire({
code: "session_expired",
summary: `Codex stdio session ${effectiveSessionId} expired after ${session.idleTimeoutMs}ms idle timeout.`,
session,
timestamp,
traceId: params.traceId
});
}
if (session?.status === "busy") {
return blockedAcquire({
code: "session_busy",
summary: `Codex stdio session ${effectiveSessionId} is already busy with traceId ${session.currentTraceId ?? "unknown"}.`,
session,
timestamp,
traceId: params.traceId
});
}
if (!session) {
session = {
sessionId: effectiveSessionId,
conversationIds: new Set(),
status: "creating",
workspace: params.workspace,
sandbox: params.sandbox,
runnerKind: CODEX_STDIO_RUNNER_KIND,
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 = params.status ?? "idle";
session.updatedAt = timestamp;
session.expiresAt = plusMs(timestampMs, idleTimeoutMs);
session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId;
session.currentTraceId = null;
session.threadId = optionalId(params.threadId) ?? session.threadId;
session.statusReason = params.statusReason ?? null;
return publicSession(session, {
conversationId: params.conversationId,
reused: params.reused
});
}
function failSession(sessionId, params = {}) {
return releaseSession(sessionId, { ...params, status: "failed" });
}
function pruneSessions() {
if (sessions.size <= maxSessions) return;
const sorted = [...sessions.entries()]
.sort((a, b) => String(a[1].updatedAt).localeCompare(String(b[1].updatedAt)));
for (const [sessionId, session] of sorted.slice(0, sessions.size - maxSessions)) {
sessions.delete(sessionId);
for (const conversationId of session.conversationIds) {
if (conversations.get(conversationId) === sessionId) conversations.delete(conversationId);
}
}
}
function sessionsBusyCount() {
return [...sessions.values()].filter((session) => session.status === "busy").length;
}
return {
describe,
probe,
chat,
cancel,
reapIdle,
get,
clear,
longLivedSessionGate,
get rpcStartedAt() {
return rpcStartedAt;
}
};
}
export function createCodexMcpJsonLineClient({ command = DEFAULT_CODEX_STDIO_COMMAND, env = process.env, cwd = repoRoot } = {}) {
const child = spawn(command, ["mcp-server"], {
cwd,
env,
stdio: ["pipe", "pipe", "pipe"]
});
let nextId = 1;
let stdoutBuffer = "";
let stderr = "";
const pending = new Map();
let initialized = false;
child.stdout.on("data", (chunk) => {
stdoutBuffer += chunk.toString("utf8");
let newlineIndex = stdoutBuffer.indexOf("\n");
while (newlineIndex >= 0) {
const line = stdoutBuffer.slice(0, newlineIndex).trim();
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
if (line) handleLine(line);
newlineIndex = stdoutBuffer.indexOf("\n");
}
});
child.stderr.on("data", (chunk) => {
stderr = tailText(`${stderr}${chunk.toString("utf8")}`, 4000);
});
child.on("error", (error) => rejectAll(`Codex stdio process error: ${error.message}`));
child.on("close", (code, signal) => rejectAll(`Codex stdio process closed code=${code ?? "null"} signal=${signal ?? "null"}`));
function handleLine(line) {
let payload = null;
try {
payload = JSON.parse(line);
} catch {
return;
}
const request = pending.get(payload.id);
if (!request) return;
pending.delete(payload.id);
clearTimeout(request.timer);
if (payload.error) {
request.reject(new Error(redactText(payload.error.message ?? JSON.stringify(payload.error))));
} else {
request.resolve(payload.result);
}
}
function rejectAll(message) {
for (const [id, request] of pending.entries()) {
pending.delete(id);
clearTimeout(request.timer);
request.reject(new Error(redactText(`${message}; stderr=${tailText(stderr, 800)}`)));
}
}
function request(method, params = {}, timeoutMs = 30000) {
const id = nextId;
nextId += 1;
const message = JSON.stringify({
jsonrpc: "2.0",
id,
method,
params
});
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error(`Codex stdio request ${method} timed out after ${timeoutMs}ms`));
}, timeoutMs);
pending.set(id, { resolve, reject, timer });
child.stdin.write(`${message}\n`, "utf8", (error) => {
if (!error) return;
pending.delete(id);
clearTimeout(timer);
reject(error);
});
});
}
async function initialize(timeoutMs = 30000) {
if (initialized) return { tools: await listTools(timeoutMs) };
await request("initialize", {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: {},
clientInfo: {
name: "hwlab-cloud-api",
version: "0"
}
}, timeoutMs);
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} })}\n`);
initialized = true;
return { tools: await listTools(timeoutMs) };
}
async function listTools(timeoutMs = 30000) {
const result = await request("tools/list", {}, timeoutMs);
return Array.isArray(result?.tools) ? result.tools.map((tool) => tool?.name).filter(Boolean) : [];
}
async function callTool(name, args = {}, timeoutMs = 150000) {
return request("tools/call", {
name,
arguments: args
}, timeoutMs);
}
function close() {
child.kill("SIGTERM");
}
return {
initialize,
listTools,
callTool,
close
};
}
export function longLivedSessionGate({
provider,
runnerKind,
session,
sessionMode,
implementationType,
codexStdioFeasibility
} = {}) {
const normalizedProvider = String(provider ?? "").trim();
const normalizedRunnerKind = String(runnerKind ?? "").trim();
const normalizedSessionMode = String(sessionMode ?? "").trim();
const normalizedImplementation = String(implementationType ?? "").trim();
const blockers = [];
if (normalizedProvider === "openai-responses" || normalizedRunnerKind === "openai-responses-fallback") {
blockers.push({
code: "openai_responses_fallback_not_session",
sourceIssue: "pikasTech/HWLAB#317",
summary: "OpenAI Responses fallback is text-chat-only and cannot pass the long-lived Codex session gate."
});
}
if (normalizedRunnerKind === "codex-cli-one-shot-ephemeral" || normalizedSessionMode === "ephemeral-one-shot") {
blockers.push({
code: "one_shot_runner_not_long_lived",
sourceIssue: "pikasTech/HWLAB#317",
summary: "codex exec --ephemeral / one-shot runner output is not a reusable long-lived runner session."
});
}
if (normalizedImplementation === "controlled-readonly-session-registry") {
blockers.push({
code: "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", "codex-reply", "workspace-read", "workspace-write"],
blocked: ["secret-read", "kubeconfig-read", "gateway-direct-call", "box-simu-direct-call", "patch-panel-direct-call", "M3/M4/M5-acceptance-claim-without-evidence"]
},
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
safety: codexStdioSafety()
};
}
function runnerTrace({ traceId, workspace, sandbox, session, events, startedAt, outputTruncated }) {
return {
traceId,
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace,
sandbox,
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionId: session?.sessionId ?? null,
sessionStatus: session?.status ?? null,
idleTimeoutMs: session?.idleTimeoutMs ?? null,
lastTraceId: session?.lastTraceId ?? null,
turn: session?.turn ?? null,
sessionReused: session?.reused ?? false,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
limitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
startedAt,
finishedAt: timestampFor(),
events,
outputTruncated: Boolean(outputTruncated),
valuesPrinted: false,
note: "Repo-owned Codex MCP stdio supervisor manages create/reuse/cancel/reap/idle timeout and trace capture; hardware control remains routed through cloud-api/HWLAB API/skill CLI boundaries."
};
}
function codexStdioSidecarResult({
message,
workspace,
sandbox,
session,
sidecar,
availability,
traceId,
startedAt,
events,
model,
env,
providerTraceToolName
}) {
return {
provider: CODEX_STDIO_PROVIDER,
model: firstNonEmpty(model, env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL, "codex-default"),
backend: CODEX_STDIO_BACKEND,
content: sidecarOnlyReply({ message, sidecar, session }),
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: sidecar.toolCalls,
skills: sidecar.skills,
runner: runnerDescriptor({ workspace, sandbox, session }),
runnerTrace: runnerTrace({
traceId,
workspace,
sandbox,
session,
events,
startedAt,
outputTruncated: sidecar.outputTruncated
}),
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
providerTrace: {
transport: "stdio",
protocol: "mcp",
command: `${availability.command} mcp-server`,
toolName: providerTraceToolName,
threadId: session.threadId ?? null,
sidecarOnly: true,
valuesPrinted: false
}
};
}
function shouldReturnSidecarOnly(sidecar = {}) {
const intent = sidecar.intent ?? {};
const requested = ["pwd", "ls", "skills", "smoke"].filter((name) => intent[name] === true);
if (requested.length === 0) return false;
return sidecar.toolCalls.length > 0;
}
function sidecarOnlyReply({ message, sidecar, session }) {
const intent = sidecar.intent ?? {};
const lines = [];
if (intent.pwd) {
const pwd = sidecar.toolCalls.find((toolCall) => toolCall.name === "pwd");
lines.push("当前 Codex stdio 受控 workspace 目录:", pwd?.stdout ?? "");
}
if (intent.ls) {
const ls = sidecar.toolCalls.find((toolCall) => toolCall.name === "ls");
lines.push(lines.length ? "" : "", "目录列表:", ls?.stdout || "(empty)");
}
if (sidecar.skills.status === "ready") {
lines.push(lines.length ? "" : "", stdioSkillsReply(sidecar.skills));
}
if (intent.smoke) {
const smokeCalls = sidecar.toolCalls
.filter((toolCall) => toolCall.name.startsWith("workspace.smoke."))
.map((toolCall) => `${toolCall.name}:${toolCall.status}`);
if (smokeCalls.length > 0) lines.push(lines.length ? "" : "", `workspace smoke: ${smokeCalls.join(", ")}`);
}
lines.push(
lines.length ? "" : "",
`Session: mode=${CODEX_STDIO_SESSION_MODE}; sessionId=${session.sessionId}; status=${session.status}; reused=${session.reused}; turn=${session.turn}.`,
"边界:provider=codex-stdiobackend=hwlab-cloud-api/codex-mcp-stdio;硬件控制仅允许 cloud-api/HWLAB API/skill CLI;未读取 secret/token/kubeconfig。"
);
return boundToolOutput(lines.filter((line, index) => line !== "" || index > 0).join("\n")).text || String(message ?? "").trim();
}
function stdioSkillsReply(skills) {
const lines = [
`已发现 ${skills.totalCount} 个可用 skill${skills.truncated ? `,本次显示前 ${skills.count} 个` : ""}`
];
for (const skill of skills.items) {
const meta = [
`source=${skill.source}`,
skill.version ? `version=${skill.version}` : null,
skill.commit ? `commit=${skill.commit}` : null
].filter(Boolean).join(" / ");
lines.push(`- ${skill.name}: ${skill.summary}${meta ? ` (${meta})` : ""}`);
}
lines.push("说明:这是 Codex stdio runner 的受控 skills discovery,未读取 secret/token/kubeconfig。");
return lines.join("\n");
}
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: ""
};
}
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(`tool:workspace.pwd:${toolCall.status}`);
}
if (intent.ls) {
const toolCall = await lsToolCall({ workspace, target: intent.target, traceId });
toolCalls.push(toolCall);
events.push(`tool:workspace.ls:${toolCall.status}`);
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(`tool:skills.discover:${skills.status === "ready" ? "completed" : "blocked"}`);
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(`tool:${call.name}:${call.status}`);
outputTruncated = outputTruncated || smokeCalls.some((call) => call.outputTruncated);
}
return {
intent,
toolCalls,
skills,
events,
outputTruncated
};
}
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, sidecar) {
const lines = [String(content ?? "").trim()].filter(Boolean);
const callNames = sidecar.toolCalls.map((call) => `${call.name}:${call.status}`);
if (callNames.length > 0) {
lines.push("", `runner tool evidence: ${callNames.join(", ")}`);
}
if (sidecar.skills.status === "ready") {
lines.push(`skills discovered: ${sidecar.skills.count}/${sidecar.skills.totalCount}`);
}
return lines.join("\n");
}
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 }) {
return [
`conversationId: ${conversationId}`,
`traceId: ${traceId}`,
"",
"User message:",
String(message ?? "").trim()
].join("\n");
}
function resolveCodexWorkspace(env = process.env, options = {}) {
return path.resolve(firstNonEmpty(
options.workspace,
env.HWLAB_CODE_AGENT_CODEX_WORKSPACE,
env.HWLAB_CODE_AGENT_WORKSPACE,
env.HWLAB_RUNNER_WORKSPACE,
env.WORKSPACE,
repoRoot
));
}
function resolveCodexCommand(env = process.env, params = {}, options = {}) {
const configured = firstNonEmpty(params.command, options.command, env.HWLAB_CODE_AGENT_CODEX_COMMAND);
if (configured) return configured;
const nodeModulesCodex = path.join(repoRoot, "node_modules", ".bin", "codex");
if (existsSync(nodeModulesCodex)) return nodeModulesCodex;
return DEFAULT_CODEX_STDIO_COMMAND;
}
function resolveCodexSandbox(env = process.env, options = {}) {
const value = firstNonEmpty(options.sandbox, env.HWLAB_CODE_AGENT_CODEX_SANDBOX, CODEX_STDIO_SANDBOX);
return ["read-only", "workspace-write"].includes(value) ? value : CODEX_STDIO_SANDBOX;
}
function 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 mcp-server over stdio",
cancelSupported: true,
reapSupported: true,
secretMaterialStored: false,
valuesRedacted: true
};
}
function tokenBoundaryState(env = process.env) {
const present = hasEnvValue(env, "OPENAI_API_KEY") ||
hasEnvValue(env, "CODEX_API_KEY") ||
env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY === "configured" ||
env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY === "present";
return {
present,
sources: [
hasEnvValue(env, "OPENAI_API_KEY") ? "OPENAI_API_KEY" : null,
hasEnvValue(env, "CODEX_API_KEY") ? "CODEX_API_KEY" : null,
env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY ? "HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY" : null
].filter(Boolean),
secretMaterialRead: false,
secretValuesPrinted: false,
valuesRedacted: true
};
}
function egressState(env = process.env) {
const baseUrl = firstNonEmpty(env.HWLAB_CODE_AGENT_OPENAI_BASE_URL, env.OPENAI_BASE_URL);
const directPublicOpenAi = /^https:\/\/api\.openai\.com\/v1\/?/u.test(baseUrl);
return {
configured: Boolean(baseUrl),
directPublicOpenAi,
valueRedacted: true
};
}
function workspaceStateSync(workspace, sandbox) {
const state = {
exists: false,
readable: false,
writable: false,
writeRequired: sandbox === "workspace-write"
};
try {
state.exists = existsSync(workspace);
if (!state.exists) return state;
state.readable = accessSyncBoolean(workspace, fsConstants.R_OK);
state.writable = accessSyncBoolean(workspace, fsConstants.W_OK);
return state;
} catch {
return state;
}
}
function 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 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, toolsObserved }) {
const observedTools = Array.isArray(toolsObserved) ? toolsObserved.filter(Boolean) : [];
const missingTools = CODEX_STDIO_REQUIRED_TOOLS.filter((tool) => !observedTools.includes(tool));
const wired = binary.present === true && supervisor.configured === true && observedTools.length > 0 && missingTools.length === 0;
const probeReady = binary.present === true && supervisor.configured === true;
return {
transport: "stdio",
command: `${command} mcp-server`,
protocolVersion: MCP_PROTOCOL_VERSION,
adapter: "Model Context Protocol tools/call",
requiredTools: [...CODEX_STDIO_REQUIRED_TOOLS],
toolsObserved: observedTools,
missingTools,
wired,
probeReady,
status: wired ? "wired" : "blocked",
blocker: wired ? null : "stdio_protocol_not_wired",
summary: wired
? "Codex stdio MCP protocol is wired and required tools were observed."
: "Codex stdio MCP protocol has not been proven: required tools codex/codex-reply were not observed through repo-owned stdio.",
nextEvidence: "Start `codex mcp-server`, run MCP initialize plus tools/list, and observe codex and codex-reply without printing token material.",
secretMaterialRead: false,
valuesRedacted: true
};
}
function cachedProbeTools({ 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 Array.isArray(probe.toolsObserved) ? probe.toolsObserved : null;
}
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,
toolsObserved: [],
secretMaterialRead: false,
valuesRedacted: true
};
}
return {
status: probe.status,
ready: probe.ready === true,
toolsObserved: Array.isArray(probe.toolsObserved) ? [...probe.toolsObserved] : [],
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,
tokenBoundary,
egress,
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,
requiredTools: [...protocol.requiredTools],
toolsObserved: [...protocol.toolsObserved],
missingTools: [...protocol.missingTools],
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),
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
},
cancelReapTraceReadiness: cancelReapTraceState(lifecycle),
secretBoundary: {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
valuesRedacted: true
}
};
}
function codexHomeContractState(codexHomeInfo, codexHome) {
const ready = codexHomeInfo.exists && codexHomeInfo.readable && codexHomeInfo.writable;
return {
path: codexHome,
status: ready ? "ready" : "blocked",
exists: codexHomeInfo.exists,
readable: codexHomeInfo.readable,
writable: codexHomeInfo.writable,
writeRequired: true,
blocker: ready ? null : !codexHomeInfo.exists || !codexHomeInfo.readable ? "codex_home_missing" : "codex_home_write_blocked",
secretMaterialRead: false,
valuesRedacted: true
};
}
function childProcessEnv(env = process.env) {
return {
PATH: Object.hasOwn(env, "PATH") ? env.PATH : process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
HOME: env.HOME || process.env.HOME || os.homedir(),
CODEX_HOME: resolveCodexHome(env),
...(env.OPENAI_API_KEY ? { OPENAI_API_KEY: env.OPENAI_API_KEY } : {}),
...(env.CODEX_API_KEY ? { CODEX_API_KEY: env.CODEX_API_KEY } : {}),
...(env.HWLAB_CODE_AGENT_OPENAI_BASE_URL ? { OPENAI_BASE_URL: env.HWLAB_CODE_AGENT_OPENAI_BASE_URL } : {}),
...(env.OPENAI_BASE_URL ? { OPENAI_BASE_URL: env.OPENAI_BASE_URL } : {})
};
}
function codexStdioSafety() {
return {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
prodTouched: false,
hardwareWritesAllowed: false,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
hardwareControlPath: "cloud-api/HWLAB API/skill CLI only",
valuesRedacted: true
};
}
function codexStdioError(code, message, details = {}) {
const error = new Error(message);
error.code = code;
Object.assign(error, {
provider: CODEX_STDIO_PROVIDER,
backend: CODEX_STDIO_BACKEND,
capabilityLevel: "blocked",
...details
});
return error;
}
function sessionExpired(session, timestampMs) {
if (!Number.isFinite(timestampMs) || session.status === "expired") return session.status === "expired";
return Date.parse(session.expiresAt) <= timestampMs;
}
function timestampFor(now) {
const value = typeof now === "function" ? now() : now;
if (typeof value === "string" && !Number.isNaN(Date.parse(value))) return value;
if (value instanceof Date && !Number.isNaN(value.valueOf())) return value.toISOString();
return new Date().toISOString();
}
function plusMs(timestampMs, offsetMs) {
return new Date(timestampMs + offsetMs).toISOString();
}
function requiredId(value, fallbackPrefix) {
const id = optionalId(value);
if (id) return id;
return `${fallbackPrefix}_${randomUUID()}`;
}
function optionalId(value) {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
function positiveInteger(value, fallback) {
return Number.isInteger(value) && value > 0 ? value : fallback;
}
function firstNonEmpty(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value.trim();
}
return "";
}
function hasEnvValue(env, name) {
return typeof env?.[name] === "string" && env[name].trim().length > 0;
}
function effectiveTimeout(timeoutMs) {
return Number.isInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs : 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***");
}