Files
pikasTech-HWLAB/internal/cloud/codex-stdio-session.mjs
T
2026-05-23 17:43:41 +00:00

1459 lines
49 KiB
JavaScript

import { spawn, spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { accessSync, constants as fsConstants, existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const CODEX_STDIO_PROVIDER = "codex-stdio";
export const CODEX_STDIO_BACKEND = "hwlab-cloud-api/codex-mcp-stdio";
export const CODEX_STDIO_RUNNER_KIND = "codex-mcp-stdio-runner";
export const CODEX_STDIO_SESSION_MODE = "codex-mcp-stdio-long-lived";
export const CODEX_STDIO_IMPLEMENTATION_TYPE = "repo-owned-codex-mcp-stdio-session";
export const CODEX_STDIO_CAPABILITY_LEVEL = "long-lived-codex-stdio-session";
export const CODEX_STDIO_SANDBOX = "workspace-write";
export const DEFAULT_CODEX_STDIO_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
export const DEFAULT_CODEX_STDIO_MAX_SESSIONS = 64;
export const DEFAULT_CODEX_STDIO_COMMAND = "codex";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const MCP_PROTOCOL_VERSION = "2024-11-05";
const CODEX_STDIO_REQUIRED_TOOLS = Object.freeze(["codex", "codex-reply"]);
const CODEX_STDIO_BOUNDARY_INSTRUCTIONS = [
"You are the HWLAB Cloud Workbench Code Agent.",
"Use the provided workspace and repo-owned Codex stdio session only.",
"Do not read or print secrets, tokens, kubeconfig files, DB URLs, private keys, or raw environment values.",
"Do not call gateway, box-simu, or patch-panel directly.",
"Hardware control requests must go through cloud-api/HWLAB API/skill CLI controlled paths.",
"Do not claim M3, M4, or M5 acceptance unless the response includes live HWLAB evidence."
].join("\n");
export function createCodexStdioSessionManager(options = {}) {
const idleTimeoutMs = positiveInteger(options.idleTimeoutMs, DEFAULT_CODEX_STDIO_IDLE_TIMEOUT_MS);
const maxSessions = positiveInteger(options.maxSessions, DEFAULT_CODEX_STDIO_MAX_SESSIONS);
const idFactory = typeof options.idFactory === "function" ? options.idFactory : () => `ses_${randomUUID()}`;
const nowDefault = options.now;
const sessions = new Map();
const conversations = new Map();
let rpcClient = options.rpcClient ?? null;
let rpcStartedAt = null;
let rpcToolNames = null;
function describe(params = {}) {
const env = params.env ?? options.env ?? process.env;
const workspace = resolveCodexWorkspace(env, params);
const command = resolveCodexCommand(env, params, options);
const sandbox = resolveCodexSandbox(env, params);
const enabled = codexStdioEnabled(env, params, options);
const supervisor = supervisorState(env, params, options, enabled);
const tokenBoundary = tokenBoundaryState(env);
const 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
});
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
});
}
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
});
}
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,
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
}),
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 toolName = session.threadId ? "codex-reply" : "codex";
const toolArguments = session.threadId
? {
threadId: session.threadId,
prompt: buildCodexUserPrompt(params.message, { conversationId, traceId })
}
: {
prompt: buildCodexUserPrompt(params.message, { conversationId, traceId }),
cwd: workspace,
sandbox,
model: params.model,
"approval-policy": "never",
"developer-instructions": CODEX_STDIO_BOUNDARY_INSTRUCTIONS
};
const toolResult = await client.callTool(toolName, dropEmpty(toolArguments), effectiveTimeout(params.timeoutMs));
const codexOutput = extractCodexToolOutput(toolResult);
if (!codexOutput.content) {
throw codexStdioError("codex_stdio_empty_response", "Codex stdio tool call completed without assistant content.", {
availability,
session
});
}
session = releaseSession(session.sessionId, {
now,
traceId,
conversationId,
reused: session.reused,
threadId: codexOutput.threadId ?? session.threadId,
status: "idle"
}) ?? session;
events.push(`tool:${toolName}:completed`);
return {
provider: CODEX_STDIO_PROVIDER,
model: firstNonEmpty(params.model, env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL, "codex-default"),
backend: CODEX_STDIO_BACKEND,
content: codexOutput.content,
workspace,
sandbox,
session,
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionReuse: sessionReuseEvidence(session),
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
codexStdioFeasibility: availability,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
runnerKind: CODEX_STDIO_RUNNER_KIND,
session,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdioFeasibility: availability
}),
toolCalls: [{
id: `tool_${randomUUID()}`,
type: "codex-stdio",
name: toolName,
status: "completed",
cwd: workspace,
command: "codex mcp-server tools/call",
exitCode: 0,
stdout: codexOutput.threadId ? `threadId=${codexOutput.threadId}` : "threadId=captured",
stderrSummary: "",
outputTruncated: false,
traceId
}],
skills: {
status: "not_requested",
items: [],
count: 0,
blockers: []
},
runner: runnerDescriptor({ workspace, sandbox, session }),
runnerTrace: runnerTrace({
traceId,
workspace,
sandbox,
session,
events,
startedAt,
outputTruncated: false
}),
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
providerTrace: {
transport: "stdio",
protocol: "mcp",
command: `${availability.command} mcp-server`,
toolName,
threadId: codexOutput.threadId ?? session.threadId ?? null,
valuesPrinted: false
}
};
} catch (error) {
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")) {
error.session = session;
error.availability = error.availability ?? describe({ ...params, env, workspace, sandbox });
error.runnerTrace = runnerTrace({
traceId,
workspace,
sandbox,
session,
events: [...events, `blocked:${error.code}`],
startedAt,
outputTruncated: false
});
throw error;
}
throw codexStdioError("codex_stdio_failed", redactText(error.message || "Codex stdio session failed."), {
availability,
session,
runnerTrace: runnerTrace({
traceId,
workspace,
sandbox,
session,
events: [...events, "blocked:codex_stdio_failed"],
startedAt,
outputTruncated: false
})
});
}
}
function cancel(sessionId, params = {}) {
const session = sessions.get(requiredId(sessionId, "ses")) ?? null;
if (!session) return null;
const timestamp = timestampFor(params.now ?? nowDefault);
session.status = "interrupted";
session.updatedAt = timestamp;
session.lastTraceId = optionalId(params.traceId) ?? session.lastTraceId;
session.currentTraceId = null;
session.statusReason = params.reason ?? "cancelled";
if (rpcClient && typeof rpcClient.close === "function") {
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;
}
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,
chat,
cancel,
reapIdle,
get,
clear,
longLivedSessionGate,
get rpcStartedAt() {
return rpcStartedAt;
}
};
}
export function createCodexMcpJsonLineClient({ command = DEFAULT_CODEX_STDIO_COMMAND, env = process.env, cwd = repoRoot } = {}) {
const child = spawn(command, ["mcp-server"], {
cwd,
env,
stdio: ["pipe", "pipe", "pipe"]
});
let nextId = 1;
let stdoutBuffer = "";
let stderr = "";
const pending = new Map();
let initialized = false;
child.stdout.on("data", (chunk) => {
stdoutBuffer += chunk.toString("utf8");
let newlineIndex = stdoutBuffer.indexOf("\n");
while (newlineIndex >= 0) {
const line = stdoutBuffer.slice(0, newlineIndex).trim();
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
if (line) handleLine(line);
newlineIndex = stdoutBuffer.indexOf("\n");
}
});
child.stderr.on("data", (chunk) => {
stderr = tailText(`${stderr}${chunk.toString("utf8")}`, 4000);
});
child.on("error", (error) => rejectAll(`Codex stdio process error: ${error.message}`));
child.on("close", (code, signal) => rejectAll(`Codex stdio process closed code=${code ?? "null"} signal=${signal ?? "null"}`));
function handleLine(line) {
let payload = null;
try {
payload = JSON.parse(line);
} catch {
return;
}
const request = pending.get(payload.id);
if (!request) return;
pending.delete(payload.id);
clearTimeout(request.timer);
if (payload.error) {
request.reject(new Error(redactText(payload.error.message ?? JSON.stringify(payload.error))));
} else {
request.resolve(payload.result);
}
}
function rejectAll(message) {
for (const [id, request] of pending.entries()) {
pending.delete(id);
clearTimeout(request.timer);
request.reject(new Error(redactText(`${message}; stderr=${tailText(stderr, 800)}`)));
}
}
function request(method, params = {}, timeoutMs = 30000) {
const id = nextId;
nextId += 1;
const message = JSON.stringify({
jsonrpc: "2.0",
id,
method,
params
});
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pending.delete(id);
reject(new Error(`Codex stdio request ${method} timed out after ${timeoutMs}ms`));
}, timeoutMs);
pending.set(id, { resolve, reject, timer });
child.stdin.write(`${message}\n`, "utf8", (error) => {
if (!error) return;
pending.delete(id);
clearTimeout(timer);
reject(error);
});
});
}
async function initialize(timeoutMs = 30000) {
if (initialized) return { tools: await listTools(timeoutMs) };
await request("initialize", {
protocolVersion: MCP_PROTOCOL_VERSION,
capabilities: {},
clientInfo: {
name: "hwlab-cloud-api",
version: "0"
}
}, timeoutMs);
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized", params: {} })}\n`);
initialized = true;
return { tools: await listTools(timeoutMs) };
}
async function listTools(timeoutMs = 30000) {
const result = await request("tools/list", {}, timeoutMs);
return Array.isArray(result?.tools) ? result.tools.map((tool) => tool?.name).filter(Boolean) : [];
}
async function callTool(name, args = {}, timeoutMs = 120000) {
return request("tools/call", {
name,
arguments: args
}, timeoutMs);
}
function close() {
child.kill("SIGTERM");
}
return {
initialize,
listTools,
callTool,
close
};
}
export function longLivedSessionGate({
provider,
runnerKind,
session,
sessionMode,
implementationType,
codexStdioFeasibility
} = {}) {
const normalizedProvider = String(provider ?? "").trim();
const normalizedRunnerKind = String(runnerKind ?? "").trim();
const normalizedSessionMode = String(sessionMode ?? "").trim();
const normalizedImplementation = String(implementationType ?? "").trim();
const blockers = [];
if (normalizedProvider === "openai-responses" || normalizedRunnerKind === "openai-responses-fallback") {
blockers.push({
code: "openai_responses_fallback_not_session",
sourceIssue: "pikasTech/HWLAB#317",
summary: "OpenAI Responses fallback is text-chat-only and cannot pass the long-lived Codex session gate."
});
}
if (normalizedRunnerKind === "codex-cli-one-shot-ephemeral" || normalizedSessionMode === "ephemeral-one-shot") {
blockers.push({
code: "one_shot_runner_not_long_lived",
sourceIssue: "pikasTech/HWLAB#317",
summary: "codex exec --ephemeral / one-shot runner output is not a reusable long-lived runner session."
});
}
if (normalizedImplementation === "controlled-readonly-session-registry") {
blockers.push({
code: "controlled_readonly_not_long_lived_stdio",
sourceIssue: "pikasTech/HWLAB#317",
summary: "This response is backed by the controlled read-only session registry, not Codex stdio or an equivalent long-lived protocol adapter."
});
}
if (session && !["idle", "ready", "busy"].includes(session.status)) {
blockers.push({
code: `session_${session.status || "inactive"}`,
sourceIssue: "pikasTech/HWLAB#317",
summary: `Long-lived Codex stdio session is not active: status=${session.status || "unknown"}.`
});
}
for (const blocker of codexStdioFeasibility?.blockers ?? []) {
if (!blocker?.code || blockers.some((item) => item.code === blocker.code)) continue;
blockers.push({
code: blocker.code,
sourceIssue: blocker.sourceIssue ?? "pikasTech/HWLAB#317",
summary: blocker.summary ?? `Codex stdio feasibility blocker: ${blocker.code}.`
});
}
const feasible = codexStdioFeasibility?.canStartLongLivedCodexStdio === true || codexStdioFeasibility?.ready === true;
const sessionPass =
normalizedProvider === CODEX_STDIO_PROVIDER &&
normalizedRunnerKind === CODEX_STDIO_RUNNER_KIND &&
normalizedSessionMode === CODEX_STDIO_SESSION_MODE &&
normalizedImplementation === CODEX_STDIO_IMPLEMENTATION_TYPE &&
session?.longLivedSession === true &&
session?.codexStdio === true &&
session?.writeCapable === true &&
session?.durable === true &&
["idle", "ready", "busy"].includes(session?.status) &&
feasible &&
blockers.length === 0;
const feasiblePass =
normalizedProvider === CODEX_STDIO_PROVIDER &&
normalizedRunnerKind === CODEX_STDIO_RUNNER_KIND &&
normalizedSessionMode === CODEX_STDIO_SESSION_MODE &&
normalizedImplementation === CODEX_STDIO_IMPLEMENTATION_TYPE &&
!session &&
feasible &&
blockers.length === 0;
const pass = sessionPass || feasiblePass;
return {
status: pass ? "pass" : "blocked",
pass,
requiredCapability: "long-lived-codex-stdio-session",
currentCapability: normalizedImplementation || normalizedSessionMode || normalizedRunnerKind || "unknown",
sessionId: session?.sessionId ?? null,
sessionStatus: session?.status ?? null,
runnerKind: normalizedRunnerKind || null,
provider: normalizedProvider || null,
sessionMode: normalizedSessionMode || null,
implementationType: normalizedImplementation || null,
blockers,
sourceIssue: "pikasTech/HWLAB#317",
summary: pass
? "Long-lived Codex stdio/session gate passed."
: "Long-lived Codex stdio/session gate remains blocked; current response must not be reported as full Code Agent completion."
};
}
function publicSession(session, { conversationId = null, reused = null } = {}) {
const conversationIds = [...session.conversationIds];
return {
sessionId: session.sessionId,
conversationId: conversationId ?? conversationIds[0] ?? null,
conversationIds,
status: session.status,
workspace: session.workspace,
sandbox: session.sandbox,
runnerKind: session.runnerKind,
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 sessionReuseEvidence(session) {
return {
conversationId: session.conversationId,
sessionId: session.sessionId,
threadId: session.threadId,
mapped: true,
reused: session.reused,
turn: session.turn,
previousTurns: Math.max(0, session.turn - 1),
workspace: session.workspace,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
idleTimeoutMs: session.idleTimeoutMs,
expiresAt: session.expiresAt,
lastTraceId: session.lastTraceId,
status: session.status
};
}
function extractCodexToolOutput(toolResult) {
const direct = toolResult?.structuredContent ?? toolResult?.output ?? toolResult;
const directThreadId = optionalId(direct?.threadId ?? direct?.conversationId);
const directContent = firstNonEmpty(direct?.content, direct?.message, direct?.text);
if (directThreadId || directContent) {
return {
threadId: directThreadId,
content: redactText(directContent)
};
}
for (const item of toolResult?.content ?? []) {
if (typeof item?.text !== "string") continue;
const parsed = parseJsonOrNull(item.text);
if (parsed) {
const threadId = optionalId(parsed.threadId ?? parsed.conversationId);
const content = firstNonEmpty(parsed.content, parsed.message, parsed.text);
if (threadId || content) {
return {
threadId,
content: redactText(content)
};
}
}
if (item.text.trim()) {
return {
threadId: null,
content: redactText(item.text.trim())
};
}
}
return {
threadId: null,
content: ""
};
}
function buildCodexUserPrompt(message, { conversationId, traceId }) {
return [
`conversationId: ${conversationId}`,
`traceId: ${traceId}`,
"",
"User message:",
String(message ?? "").trim()
].join("\n");
}
function resolveCodexWorkspace(env = process.env, options = {}) {
return path.resolve(firstNonEmpty(
options.workspace,
env.HWLAB_CODE_AGENT_CODEX_WORKSPACE,
env.HWLAB_CODE_AGENT_WORKSPACE,
env.HWLAB_RUNNER_WORKSPACE,
env.WORKSPACE,
repoRoot
));
}
function resolveCodexCommand(env = process.env, params = {}, options = {}) {
const configured = firstNonEmpty(params.command, options.command, env.HWLAB_CODE_AGENT_CODEX_COMMAND);
if (configured) return configured;
const nodeModulesCodex = path.join(repoRoot, "node_modules", ".bin", "codex");
if (existsSync(nodeModulesCodex)) return nodeModulesCodex;
return DEFAULT_CODEX_STDIO_COMMAND;
}
function resolveCodexSandbox(env = process.env, options = {}) {
const value = firstNonEmpty(options.sandbox, env.HWLAB_CODE_AGENT_CODEX_SANDBOX, CODEX_STDIO_SANDBOX);
return ["read-only", "workspace-write"].includes(value) ? value : CODEX_STDIO_SANDBOX;
}
function 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 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
};
return {
command,
present,
binaryPresent: present,
path: resolvedPath,
version: versionProbe.version,
versionDetected: versionProbe.ok,
versionProbe,
nextEvidence: present
? "codex --version was probed without printing secrets; stdio protocol readiness is checked separately."
: `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 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 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
}) {
return {
contractVersion: "codex-runtime-feasibility-v1",
status: ready ? "ready" : "blocked",
ready,
binary: {
status: binary.present ? "present" : "missing",
command: binary.command,
present: binary.present,
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
},
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 : 120000;
}
function dropEmpty(value) {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ""));
}
function parseJsonOrNull(value) {
try {
return JSON.parse(value);
} catch {
return null;
}
}
function tailText(value, maxLength = 1200) {
const text = String(value ?? "").trim();
if (text.length <= maxLength) return text;
return text.slice(text.length - maxLength);
}
function redactText(value) {
return String(value ?? "")
.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gu, "Bearer ***")
.replace(/sk-[A-Za-z0-9._-]+/gu, "sk-***")
.replace(/([A-Za-z0-9_]*TOKEN[A-Za-z0-9_]*=)[^\s]+/giu, "$1***")
.replace(/([A-Za-z0-9_]*KEY[A-Za-z0-9_]*=)[^\s]+/giu, "$1***");
}