Files
pikasTech-HWLAB/internal/cloud/codex-stdio-session-helpers.ts
T

1877 lines
68 KiB
TypeScript
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 { spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { accessSync, constants as fsConstants, existsSync, realpathSync, statSync } 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";
import {
codeAgentSessionLifecycleSummary,
decorateCodeAgentSession
} from "./code-agent-session-lifecycle.ts";
import { runnerTraceFromSnapshot } from "./code-agent-trace-store.ts";
import {
CODEX_APP_SERVER_PROTOCOL,
CODEX_APP_SERVER_REQUIRED_METHODS,
CODEX_APP_SERVER_WIRE_API,
CODEX_CHILD_NO_PROXY_REQUIRED,
CODEX_CHILD_STRIPPED_ENV_KEYS,
CODEX_STDIO_BACKEND,
CODEX_STDIO_BOUNDARY_INSTRUCTIONS,
CODEX_STDIO_CAPABILITY_LEVEL,
CODEX_STDIO_COMMAND_PROBE_TIMEOUT_MS,
CODEX_STDIO_IMPLEMENTATION_TYPE,
CODEX_STDIO_PROVIDER,
CODEX_STDIO_RUNNER_KIND,
CODEX_STDIO_SANDBOX,
CODEX_STDIO_SESSION_MODE,
CODEX_STDIO_SKILL_LIMIT,
CODEX_STDIO_SKILL_REPLY_LIMIT,
CODEX_STDIO_SKILL_SUMMARY_LIMIT,
CODEX_STDIO_TOOL_OUTPUT_LIMIT,
codexAppServerProviderBaseUrl,
DEFAULT_CODEX_STDIO_COMMAND,
DEFAULT_CODEX_STDIO_IDLE_TIMEOUT_MS,
DEFAULT_CODEX_STDIO_MAX_SESSIONS,
DEFAULT_CODEX_STDIO_PROBE_TTL_MS,
DEFAULT_CODEX_STDIO_TURN_ACTIVITY_TIMEOUT_MS,
DEFAULT_CODEX_STDIO_TURN_HARD_TIMEOUT_MS
} from "./codex-stdio-session.ts";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
export function appServerActivityTimeoutError({ timeoutMs, idleMs, lastActivityAt, waitingFor, threadId, turnId, partialAssistant = false, diagnosis = null }) {
const diagnosisSuffix = diagnosis?.code ? ` diagnosis=${diagnosis.code}.` : "";
const error = new Error(partialAssistant
? `Codex app-server idle timeout after partial assistant output: no turn/completed for ${idleMs}ms, threshold ${timeoutMs}ms; waitingFor=${waitingFor ?? "unknown"}.${diagnosisSuffix}`
: `Codex app-server idle timeout: no new events for ${idleMs}ms, threshold ${timeoutMs}ms; waitingFor=${waitingFor ?? "unknown"}.${diagnosisSuffix}`);
Object.assign(error, {
code: "codex_stdio_idle_timeout",
timeoutMs,
idleMs,
lastActivityAt,
waitingFor,
threadId,
turnId,
partialAssistant,
diagnosis,
diagnosisCode: diagnosis?.code ?? null,
stage: "app-server-turn"
});
return error;
}
export function appServerHardTimeoutError({ hardTimeoutMs, timeoutMs, idleMs, lastActivityAt, waitingFor, threadId, turnId, diagnosis = null }) {
const diagnosisSuffix = diagnosis?.code ? ` diagnosis=${diagnosis.code}.` : "";
const error = new Error(`Codex app-server hard timeout reached after ${hardTimeoutMs}ms; last activity was ${idleMs}ms ago; waitingFor=${waitingFor ?? "unknown"}.${diagnosisSuffix}`);
Object.assign(error, {
code: "codex_stdio_hard_timeout",
timeoutMs,
hardTimeoutMs,
idleMs,
lastActivityAt,
waitingFor,
threadId,
turnId,
diagnosis,
diagnosisCode: diagnosis?.code ?? null,
stage: "app-server-turn"
});
return error;
}
export function normalizeAppServerTurnResult(rawResult, state, fallback) {
const raw = extractAppServerRecord(rawResult) ?? {};
return {
threadId: optionalId(raw.threadId) ?? state.threadId ?? fallback.threadId ?? null,
turnId: optionalId(raw.turnId) ?? state.turnId ?? fallback.turnId ?? null,
assistantText: redactText(firstNonEmpty(raw.assistantText, state.assistantText)),
finalResponse: redactText(firstNonEmpty(raw.finalResponse, raw.content, raw.text, state.finalResponse, state.assistantText)),
terminalStatus: appServerTerminalStatus(raw.terminalStatus ?? raw.status ?? state.terminalStatus),
terminalError: raw.terminalError ? redactText(raw.terminalError) : state.terminalError ?? null,
lastActivityAt: raw.lastActivityAt ?? state.lastActivityAt ?? null,
idleMs: raw.idleMs ?? state.idleMs ?? null,
waitingFor: raw.waitingFor ?? state.waitingFor ?? null,
appServerExit: raw.appServerExit ?? null,
diagnosis: raw.diagnosis ?? state.diagnosis ?? null
};
}
export function summarizeAppServerTurn(turnResult) {
return [
turnResult?.threadId ? `threadId=${turnResult.threadId}` : "threadId=not_observed",
turnResult?.turnId ? `turnId=${turnResult.turnId}` : null,
`terminalStatus=${turnResult?.terminalStatus ?? "unknown"}`,
firstNonEmpty(turnResult?.finalResponse, turnResult?.assistantText) ? `assistantChars=${String(firstNonEmpty(turnResult.finalResponse, turnResult.assistantText)).length}` : null
].filter(Boolean).join(" ");
}
export function codexStdioProviderTrace({ availability, toolName, turnResult, session } = {}) {
return {
transport: "stdio",
protocol: CODEX_APP_SERVER_PROTOCOL,
wireApi: CODEX_APP_SERVER_WIRE_API,
runnerKind: CODEX_STDIO_RUNNER_KIND,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
command: `${availability?.command || "/app/node_modules/.bin/codex"} app-server --listen stdio://`,
toolName,
threadId: turnResult?.threadId ?? session?.threadId ?? null,
turnId: turnResult?.turnId ?? null,
terminalStatus: turnResult?.terminalStatus ?? null,
appServerExit: turnResult?.appServerExit ?? null,
diagnosis: turnResult?.diagnosis ?? null,
valuesPrinted: false
};
}
export function codexAppServerTextInput(text) {
return [{ type: "text", text: String(text ?? ""), text_elements: [] }];
}
export function extractAppServerRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
}
export function extractAppServerString(record, key) {
return typeof record?.[key] === "string" && record[key].trim() ? record[key].trim() : null;
}
export function appServerTerminalStatus(value) {
const raw = String(value ?? "").trim().toLowerCase();
if (raw === "completed" || raw === "complete" || raw === "success" || raw === "succeeded") return "completed";
if (raw === "canceled" || raw === "cancelled") return "canceled";
if (raw === "interrupted") return "interrupted";
if (raw === "timeout" || raw === "timed_out") return "timeout";
if (raw === "failed" || raw === "error") return "failed";
return raw ? "failed" : null;
}
export function appServerCommandStatus(item) {
const raw = String(item?.status ?? "").trim().toLowerCase();
if (raw === "completed" || raw === "complete" || raw === "success" || raw === "succeeded") return "completed";
if (raw === "failed" || raw === "error") return "failed";
if (Number.isInteger(item?.exitCode)) return item.exitCode === 0 ? "completed" : "failed";
return raw || "completed";
}
export function commandExecutionCommandText(item) {
return firstCommandExecutionText(
item?.command,
item?.cmd,
item?.argv,
item?.input,
item?.metadata?.command
);
}
export function commandExecutionOutputText(item) {
return firstCommandExecutionText(
item?.aggregatedOutput,
item?.output,
item?.stdout,
item?.result,
item?.metadata?.output
);
}
export function commandExecutionErrorText(item) {
const error = item?.error;
const message = typeof error === "string" ? error : typeof error?.message === "string" ? error.message : "";
const text = firstCommandExecutionText(item?.stderr, message, item?.metadata?.stderr);
return text ? tailText(redactText(text), 600) : undefined;
}
export function firstCommandExecutionText(...values) {
for (const value of values) {
const text = commandExecutionTextValue(value);
if (text) return text;
}
return "";
}
export function commandExecutionTextValue(value) {
if (typeof value === "string") return value.trim();
if (Array.isArray(value)) return value.map((item) => commandExecutionTextValue(item)).filter(Boolean).join(" ").trim();
if (value && typeof value === "object") {
return firstCommandExecutionText(value.text, value.content, value.message, value.stdout, value.stderr, value.command, value.cmd);
}
return "";
}
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 resumableSessionStatuses = ["idle", "ready", "busy", "failed", "interrupted", "timeout", "error", "canceled", "cancelled"];
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 && !resumableSessionStatuses.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 &&
resumableSessionStatuses.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."
};
}
export function publicSession(session, { conversationId = null, reused = null } = {}) {
const conversationIds = [...session.conversationIds];
return decorateCodeAgentSession({
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 } : {})
}, { reused });
}
export function blockedAcquire({ code, summary, session, timestamp, traceId, currentTrace = null, diagnosis = null }) {
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,
currentTraceId: currentTrace?.traceId ?? session?.currentTraceId ?? null,
currentTraceStatus: currentTrace?.status ?? null,
currentTraceLastEvent: currentTrace?.lastEvent ?? null,
currentTraceUpdatedAt: currentTrace?.updatedAt ?? null,
diagnosis
}
};
}
export function runnerDescriptor({ workspace, sandbox, session }) {
return {
kind: CODEX_STDIO_RUNNER_KIND,
provider: CODEX_STDIO_PROVIDER,
backend: CODEX_STDIO_BACKEND,
workspace,
sandbox,
session: CODEX_STDIO_SESSION_MODE,
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionId: session?.sessionId ?? null,
turn: session?.turn ?? null,
sessionReused: session?.reused ?? false,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdio: true,
longLivedSession: true,
durableSession: true,
writeCapable: true,
readOnly: false,
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
toolPolicy: {
allowed: ["codex-app-server.thread/start", "codex-app-server.thread/resume", "codex-app-server.turn/start", "workspace-read", "workspace-write"],
blocked: ["secret-read", "kubeconfig-read", "gateway-direct-call", "box-simu-direct-call", "patch-panel-direct-call", "M3/M4/M5-acceptance-claim-without-evidence"]
},
runnerLimitations: ["secret-values-redacted"],
safety: codexStdioSafety()
};
}
export function runnerTrace({ traceRecorder, traceId, workspace, sandbox, session, startedAt, outputTruncated }) {
const sessionLifecycleStatus = codeAgentSessionLifecycleSummary({ session }).status;
const snapshot = traceRecorder?.snapshot({
sessionId: session?.sessionId,
sessionStatus: session?.status,
sessionLifecycleStatus,
turn: session?.turn,
workspace,
sandbox,
runnerKind: CODEX_STDIO_RUNNER_KIND,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE
});
return runnerTraceFromSnapshot(snapshot ?? {
traceId,
events: [],
eventLabels: [],
updatedAt: timestampFor(),
startedAt
}, {
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace,
sandbox,
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionId: session?.sessionId ?? null,
sessionStatus: session?.status ?? null,
sessionLifecycleStatus,
idleTimeoutMs: session?.idleTimeoutMs ?? null,
lastTraceId: session?.lastTraceId ?? traceId,
turn: session?.turn ?? null,
sessionReused: session?.reused ?? false,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
limitations: ["secret-values-redacted"],
startedAt,
outputTruncated: Boolean(outputTruncated),
note: "Repo-owned Codex app-server stdio supervisor manages create/reuse/cancel/reap/idle timeout and real-time trace capture; hardware control remains routed through cloud-api/HWLAB API/skill CLI boundaries."
});
}
export 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,
lifecycleStatus: session.lifecycleStatus ?? codeAgentSessionLifecycleSummary({ session }).status
};
}
export 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: ""
};
}
export function summarizePrompt(message) {
const value = String(message ?? "").replace(/\s+/gu, " ").trim();
if (!value) return "empty prompt";
return value.length > 160 ? `${value.slice(0, 157)}...` : value;
}
export function summarizeToolResult(toolResult) {
const content = extractCodexToolOutput(toolResult);
if (content.threadId || content.content) {
return [
content.threadId ? `threadId=${content.threadId}` : "threadId=not_observed",
content.content ? `assistantChars=${content.content.length}` : null
].filter(Boolean).join(" ");
}
if (Array.isArray(toolResult?.content)) return `contentItems=${toolResult.content.length}`;
return "tool result captured";
}
export async function collectWorkspaceSidecarEvidence({ message, workspace, traceId, env, now, skillsDirs, skillsDirsExact } = {}) {
const intent = detectWorkspaceSidecarIntent(message);
const toolCalls = [];
const events = [];
let skills = notRequestedSkills();
let outputTruncated = false;
if (intent.pwd) {
const toolCall = await pwdToolCall({ workspace, traceId });
toolCalls.push(toolCall);
events.push(toolTraceEvent(toolCall, { labelPrefix: "sidecar" }));
}
if (intent.ls) {
const toolCall = await lsToolCall({ workspace, target: intent.target, traceId });
toolCalls.push(toolCall);
events.push(toolTraceEvent(toolCall, { labelPrefix: "sidecar" }));
outputTruncated = outputTruncated || toolCall.outputTruncated;
}
if (intent.skills) {
skills = await discoverSkillsForStdio({ env, traceId, skillsDirs, skillsDirsExact });
toolCalls.push({
id: `tool_${randomUUID()}`,
type: "file-read",
name: "skills.discover",
status: skills.status === "ready" ? "completed" : "blocked",
cwd: workspace,
exitCode: skills.status === "ready" ? 0 : 1,
stdout: skills.status === "ready" ? `skills=${skills.count}` : "",
stderrSummary: skills.status === "ready" ? "" : "skills_unavailable",
outputTruncated: Boolean(skills.truncated),
traceId
});
events.push({
type: "tool_call",
status: skills.status === "ready" ? "completed" : "blocked",
label: `sidecar:skills.discover:${skills.status === "ready" ? "completed" : "blocked"}`,
toolName: "skills.discover",
outputSummary: skills.status === "ready" ? `skills=${skills.count}` : "skills_unavailable",
outputTruncated: Boolean(skills.truncated)
});
outputTruncated = outputTruncated || Boolean(skills.truncated);
}
if (intent.smoke) {
const smokeCalls = await workspaceSmokeToolCalls({ workspace, traceId, now });
toolCalls.push(...smokeCalls);
for (const call of smokeCalls) events.push(toolTraceEvent(call, { labelPrefix: "sidecar" }));
outputTruncated = outputTruncated || smokeCalls.some((call) => call.outputTruncated);
}
return {
intent,
toolCalls,
skills,
events,
outputTruncated
};
}
export function toolTraceEvent(toolCall, { labelPrefix = "tool" } = {}) {
return {
type: "tool_call",
status: toolCall.status,
label: `${labelPrefix}:${toolCall.name}:${toolCall.status}`,
toolName: toolCall.name,
outputSummary: firstNonEmpty(toolCall.stderrSummary, toolCall.stdout ? `${toolCall.name} output captured` : null),
outputTruncated: toolCall.outputTruncated === true
};
}
export 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)
};
}
export 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 || ".";
}
export 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
};
}
export 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 });
}
}
export 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;
}
export async function discoverSkillsForStdio({ env = process.env, traceId, skillsDirs, skillsDirsExact } = {}) {
const checkedDirs = resolveSkillDirs(env, { skillsDirs, skillsDirsExact });
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: boundSkillText(manifest.description ?? firstMarkdownSummary(manifest.body) ?? "No description provided."),
source: manifestPath,
manifestPath,
manifest: {
type: "SKILL.md",
path: manifestPath,
root: dir,
relativePath: path.relative(dir, manifestPath) || "SKILL.md"
},
sourceRoot: dir,
version: manifest.version ?? null,
commitId: manifest.commitId ?? null,
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,
sourceIssues: ["pikasTech/HWLAB#136", "pikasTech/HWLAB#237"],
blockers: [
{
code: "skills_unavailable",
sourceIssue: "pikasTech/HWLAB#136",
summary: "No readable SKILL.md files were found in the configured Codex stdio skills directories."
},
{
code: "skills_manifest_missing",
sourceIssue: "pikasTech/HWLAB#237",
summary: "Codex stdio skills discovery requires mounted skill manifests; generic text fallback must not invent skills."
}
],
valuesPrinted: false
};
}
return {
status: "ready",
code: "skills_ready",
items: returned,
count: returned.length,
totalCount: unique.length,
truncated: unique.length > returned.length,
checkedDirs,
sources,
sourceIssues: [],
blockers: [],
valuesPrinted: false
};
}
export function resolveSkillDirs(env = process.env, options = {}) {
const configured = Array.isArray(options.skillsDirs)
? options.skillsDirs
: String(firstNonEmpty(env.HWLAB_CODE_AGENT_SKILLS_DIRS, env.UNIDESK_SKILLS_PATH, ""))
.split(/[,;]/u)
.flatMap((part) => part.split(path.delimiter));
const normalized = configured
.filter((dir) => typeof dir === "string" && dir.trim())
.map((dir) => path.resolve(dir.trim()));
if (options.skillsDirsExact === true || env.HWLAB_CODE_AGENT_SKILLS_STRICT === "1") {
return [...new Set(normalized)];
}
const defaults = [env.HWLAB_PREINSTALLED_SKILLS_DIR || "/app/skills", env.HWLAB_USER_SKILLS_DIR]
.filter((dir) => typeof dir === "string" && dir.trim())
.map((dir) => path.resolve(dir.trim()));
return [...new Set(normalized.length > 0 ? normalized : defaults)];
}
export 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;
}
export 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,
version: firstManifestField(frontmatter, ["version", "skillVersion"]),
commitId: firstManifestField(frontmatter, ["commitId", "commit", "gitCommit", "revision"]),
body
} : null;
} catch {
return null;
}
}
export 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;
}
export function firstMarkdownSummary(body) {
return String(body ?? "")
.split(/\r?\n/u)
.map((line) => line.trim())
.find((line) => line && !line.startsWith("#") && !line.startsWith("-")) ?? null;
}
export function firstManifestField(frontmatter, keys) {
for (const key of keys) {
const value = String(frontmatter?.[key] ?? "").trim();
if (value) return boundSkillText(value, 120);
}
return null;
}
export function boundSkillText(value, limit = CODEX_STDIO_SKILL_SUMMARY_LIMIT) {
const text = redactText(String(value ?? "").replace(/\s+/gu, " ").trim());
if (text.length <= limit) return text;
return `${text.slice(0, Math.max(0, limit - 3))}...`;
}
export function dedupeSkills(items) {
const seen = new Set();
const deduped = [];
for (const item of items) {
const key = [
String(item.name ?? "").toLowerCase(),
item.sourceRoot ? path.resolve(item.sourceRoot) : "",
item.manifest?.relativePath ?? (item.manifestPath ? path.basename(item.manifestPath) : "")
].join("\0");
if (seen.has(key)) continue;
seen.add(key);
deduped.push(item);
}
return deduped;
}
export function notRequestedSkills() {
return {
status: "not_requested",
items: [],
count: 0,
blockers: []
};
}
export function codexReplyWithSidecar(content, sidecar = {}) {
if (sidecar.intent?.skills && sidecar.skills?.status === "ready") {
return skillsDiscoveryReply(sidecar.skills);
}
return String(content ?? "").trim();
}
export function skillsDiscoveryReply(skills) {
const items = Array.isArray(skills?.items) ? skills.items : [];
const listed = items.slice(0, CODEX_STDIO_SKILL_REPLY_LIMIT);
const sourcePaths = Array.isArray(skills?.sources)
? skills.sources.map((source) => `${source.status}:${source.path}`).slice(0, 8)
: [];
const lines = [
"这是当前 Codex stdio runner 读取到的真实 skill manifest 清单;没有使用文本 fallback。",
`数量:本次返回 ${skills.count ?? listed.length} 个;已发现总数 ${skills.totalCount ?? items.length}${skills.truncated ? `;列表已截断到 ${skills.count ?? listed.length} 个` : ""}。`,
sourcePaths.length ? `来源目录:${sourcePaths.join("")}` : null,
""
].filter((line) => line !== null);
for (const [index, item] of listed.entries()) {
const metadata = [
item.version ? `version=${item.version}` : null,
item.commitId ? `commitId=${item.commitId}` : null,
item.manifestPath ? `manifest=${item.manifestPath}` : item.source ? `source=${item.source}` : null
].filter(Boolean).join("");
lines.push(`${index + 1}. ${item.name}${item.summary}${metadata ? `${metadata}` : ""}`);
}
if (items.length > listed.length) {
lines.push(`其余 ${items.length - listed.length} 个 skill 未在正文展开,可在 payload.skills.items 查看。`);
}
return lines.join("\n").trim();
}
export 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";
}
}
};
}
export function isPathInside(child, parent) {
const relative = path.relative(parent, child);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
export 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);
}
export 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
};
}
export function safeDisplayPath(value) {
return redactText(String(value ?? ".")).replace(/\s+/gu, " ");
}
export function buildCodexUserPrompt(message, { conversationId, traceId, conversationFacts, sidecar, gatewayShellTimeoutMs }) {
return [
`conversationId: ${conversationId}`,
`traceId: ${traceId}`,
`gatewayShellTimeoutMs: ${gatewayShellTimeoutMs}`,
"",
CODEX_STDIO_BOUNDARY_INSTRUCTIONS.replace(/<gatewayShellTimeoutMs>/gu, String(gatewayShellTimeoutMs)),
codexConversationFactsPrompt(conversationFacts),
codexSidecarSkillsPrompt(sidecar),
"",
"User message:",
String(message ?? "").trim()
].filter((line) => line !== null && line !== undefined).join("\n");
}
export function normalizeGatewayShellTimeoutMs(value, env = process.env) {
const parsed = Number.parseInt(value ?? env.HWLAB_GATEWAY_SHELL_TIMEOUT_MS ?? "", 10);
if (!Number.isInteger(parsed) || parsed <= 0) return 120000;
return Math.min(Math.max(parsed, 30000), 600000);
}
export function codexSidecarSkillsPrompt(sidecar) {
if (!sidecar?.intent?.skills) return null;
const skills = sidecar.skills;
if (!skills || typeof skills !== "object") return null;
if (skills.status !== "ready") {
const blockers = Array.isArray(skills.blockers)
? skills.blockers.map((blocker) => safePromptFact(`${blocker?.code ?? "blocked"}:${blocker?.sourceIssue ?? ""}`)).slice(0, 4).join(",")
: "skills_unavailable";
return [
"Current skills discovery facts (authoritative, bounded, redacted):",
`- status=${safePromptFact(skills.status)}; count=0; blockers=${blockers}.`,
"- Do not invent skill names. The API will return a structured skills_unavailable blocker."
].join("\n");
}
const items = Array.isArray(skills.items) ? skills.items.slice(0, 12) : [];
return [
"Current skills discovery facts (authoritative, bounded, redacted):",
`- status=ready; returned=${safePromptFact(skills.count)}; total=${safePromptFact(skills.totalCount)}; truncated=${skills.truncated === true ? "true" : "false"}.`,
...items.map((item) => `- ${safePromptFact(item.name)} | ${safePromptFact(item.summary)} | version=${safePromptFact(item.version)} | commitId=${safePromptFact(item.commitId)} | manifest=${safePromptFact(item.manifestPath ?? item.source)}`)
].join("\n");
}
export function codexConversationFactsPrompt(conversationFacts) {
if (!conversationFacts || typeof conversationFacts !== "object" || Number(conversationFacts.turnCount ?? 0) <= 0) {
return null;
}
return [
"Prior session facts (bounded, redacted; use for Chinese answers about previous turns):",
`- sessionId=${safePromptFact(conversationFacts.sessionId)}; sessionStatus=${safePromptFact(conversationFacts.sessionStatus)}; sessionMode=${safePromptFact(conversationFacts.sessionMode)}; turnCount=${safePromptFact(conversationFacts.turnCount)}.`,
`- provider/backend/runner=${safePromptFact(conversationFacts.latestProvider)}/${safePromptFact(conversationFacts.latestBackend)}/${safePromptFact(conversationFacts.runnerKind)}; capabilityLevel=${safePromptFact(conversationFacts.capabilityLevel)}.`,
`- workspace=${safePromptFact(conversationFacts.workspace)}; sandbox=${safePromptFact(conversationFacts.sandbox)}.`,
`- traceIds=${promptList(conversationFacts.traceIds, 6)}; latestTraceId=${safePromptFact(conversationFacts.latestTraceId)}.`,
`- skills=${promptSkills(conversationFacts.latestSkills)}.`,
`- toolCalls=${promptToolCalls(conversationFacts.recentToolCalls)}.`
].join("\n");
}
export function promptSkills(skills) {
if (!skills || typeof skills !== "object") return "not_requested";
const names = Array.isArray(skills.names) ? skills.names.filter(Boolean).slice(0, 8) : [];
const count = skills.totalCount ?? skills.count ?? names.length;
return `${safePromptFact(skills.status)} count=${safePromptFact(count)}${names.length ? ` names=${names.map(safePromptFact).join(",")}` : ""}`;
}
export function promptToolCalls(toolCalls) {
if (!Array.isArray(toolCalls) || toolCalls.length === 0) return "none";
return toolCalls
.slice(-6)
.map((toolCall) => [toolCall?.name, toolCall?.status, toolCall?.route].filter(Boolean).map(safePromptFact).join(":"))
.filter(Boolean)
.join(",") || "none";
}
export function promptList(values, limit) {
return Array.isArray(values) && values.length > 0
? values.filter(Boolean).slice(-limit).map(safePromptFact).join(",")
: "none";
}
export function safePromptFact(value) {
if (value === undefined || value === null || value === "") return "none";
return redactText(String(value).replace(/\s+/gu, " ").trim()).slice(0, 180);
}
export 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
));
}
export 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;
}
export 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", "danger-full-access"].includes(value) ? value : CODEX_STDIO_SANDBOX;
}
export 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")
));
}
export 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";
}
export function supervisorState(env, params, options, enabled) {
const configured = params.supervisorEnabled === true ||
options.supervisorEnabled === true ||
env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR === "repo-owned" ||
env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR === "enabled" ||
(enabled && env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR !== "disabled");
return {
configured,
mode: configured ? "repo-owned-node-supervisor" : "disabled",
processModel: "codex app-server --listen stdio://",
cancelSupported: true,
reapSupported: true,
secretMaterialStored: false,
valuesRedacted: true
};
}
export function tokenBoundaryState(env = process.env, codexHomeFiles = null) {
const authPresent = codexHomeFiles?.authPresent === true && codexHomeFiles?.authReadable === true;
const present = authPresent ||
env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY === "configured" ||
env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY === "present";
return {
present,
sources: [
authPresent ? "CODEX_HOME/auth.json" : null,
env.HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY ? "HWLAB_CODE_AGENT_CODEX_TOKEN_BOUNDARY" : null
].filter(Boolean),
childEnvStripsProviderSecrets: true,
secretMaterialRead: false,
secretValuesPrinted: false,
valuesRedacted: true
};
}
export 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
};
}
export 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;
}
}
export 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;
}
}
export function codexHomeFilesStateSync(codexHome) {
const configPath = path.join(codexHome, "config.toml");
const authPath = path.join(codexHome, "auth.json");
const configPresent = filePresentSync(configPath);
const authPresent = filePresentSync(authPath);
return {
configPath,
authPath,
configPresent,
configReadable: configPresent && accessSyncBoolean(configPath, fsConstants.R_OK),
authPresent,
authReadable: authPresent && accessSyncBoolean(authPath, fsConstants.R_OK),
mountContract: "writable CODEX_HOME emptyDir with read-only config.toml/auth.json file mounts",
secretMaterialRead: false,
valuesRedacted: true
};
}
export function filePresentSync(targetPath) {
try {
return existsSync(targetPath) && statSync(targetPath).isFile();
} catch {
return false;
}
}
export function accessSyncBoolean(target, mode) {
try {
accessSync(target, mode);
return true;
} catch {
return false;
}
}
export function pathReadableSync(targetPath) {
return accessSyncBoolean(targetPath, fsConstants.R_OK);
}
export 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
};
}
export 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."
};
}
export 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;
}
export 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;
}
export function commandOnPathSync(command, env = process.env) {
return Boolean(commandPathSync(command, env));
}
export 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)
};
}
}
export function protocolState({ command, binary, supervisor, initialized }) {
const protocolInitialized = initialized === true;
const wired = binary.present === true && supervisor.configured === true && protocolInitialized;
const probeReady = binary.present === true && supervisor.configured === true;
return {
transport: "stdio",
command: `${command} app-server --listen stdio://`,
protocolVersion: CODEX_APP_SERVER_PROTOCOL,
adapter: "Codex app-server JSON-RPC line protocol",
requiredMethods: [...CODEX_APP_SERVER_REQUIRED_METHODS],
initialized: protocolInitialized,
toolsObserved: [],
missingTools: [],
wired,
probeReady,
status: wired ? "wired" : "blocked",
blocker: wired ? null : "stdio_protocol_not_wired",
summary: wired
? "Codex app-server stdio protocol is initialized through the repo-owned JSON-RPC client."
: "Codex app-server stdio protocol has not been proven through initialize/thread/start/turn/start.",
nextEvidence: "Start `codex app-server --listen stdio://`, run initialize, then thread/start or thread/resume plus turn/start without printing token material.",
secretMaterialRead: false,
valuesRedacted: true
};
}
export function cachedProtocolReady({ command, workspace, codexHome, probe }) {
if (!probe || probe.ready !== true) return null;
if (probe.command !== command || probe.workspace !== workspace || probe.codexHome !== codexHome) return null;
if (Number.isFinite(probe.expiresAtMs) && probe.expiresAtMs <= Date.now()) return null;
return probe.initialized === true;
}
export 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;
}
export function protocolProbeSummary({ command, workspace, codexHome, probe }) {
if (!probe || probe.command !== command || probe.workspace !== workspace || probe.codexHome !== codexHome) {
return {
status: "not_run",
ready: false,
initialized: false,
requiredMethods: [...CODEX_APP_SERVER_REQUIRED_METHODS],
secretMaterialRead: false,
valuesRedacted: true
};
}
return {
status: probe.status,
ready: probe.ready === true,
initialized: probe.initialized === true,
requiredMethods: Array.isArray(probe.requiredMethods) ? [...probe.requiredMethods] : [...CODEX_APP_SERVER_REQUIRED_METHODS],
startedAt: probe.startedAt,
finishedAt: probe.finishedAt,
error: probe.error ?? null,
secretMaterialRead: false,
valuesRedacted: true
};
}
export 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
};
}
export 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
};
}
export 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
};
}
export 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"
};
}
export function runtimeContract({
ready,
binary,
protocol,
lifecycle,
workspaceInfo,
workspace,
sandbox,
codexHome,
codexHomeInfo,
codexHomeFiles,
tokenBoundary,
egress,
childEnvBoundary,
commandProbe
}) {
return {
contractVersion: "codex-runtime-feasibility-v1",
status: ready ? "ready" : "blocked",
ready,
binary: {
status: binary.present && binary.executable && binary.nativeDependencyPresent ? "present" : binary.present ? "blocked" : "missing",
command: binary.command,
present: binary.present,
executable: binary.executable === true,
nativeDependencyPresent: binary.nativeDependencyPresent === true,
version: binary.version,
versionDetected: binary.versionDetected,
nextEvidence: binary.nextEvidence
},
stdioProtocol: {
status: protocol.status,
wired: protocol.wired,
command: protocol.command,
protocolVersion: protocol.protocolVersion,
requiredMethods: [...protocol.requiredMethods],
initialized: protocol.initialized === true,
nextEvidence: protocol.nextEvidence
},
commandProbe: commandProbe ?? {
status: "not_run",
ready: false,
probe: "workspace.pwd",
toolCalls: [],
secretMaterialRead: false,
valuesRedacted: true
},
lifecycleSupervisor: {
status: lifecycle.status,
present: lifecycle.present,
create: lifecycle.create,
reuse: lifecycle.reuse,
cancel: lifecycle.cancel,
reap: lifecycle.reap,
traceCapture: lifecycle.traceCapture,
idleTimeoutMs: lifecycle.idleTimeoutMs
},
workspaceMount: workspaceContractState(workspaceInfo, sandbox, workspace),
codexHome: codexHomeContractState(codexHomeInfo, codexHome, codexHomeFiles),
sandbox,
tokenBoundary: {
status: tokenBoundary.present ? "present" : "blocked",
present: tokenBoundary.present,
sources: tokenBoundary.sources,
secretMaterialRead: false,
valuesRedacted: true
},
egress: {
status: egress.directPublicOpenAi ? "blocked" : egress.configured ? "configured" : "not_configured",
configured: egress.configured,
directPublicOpenAi: egress.directPublicOpenAi,
valueRedacted: true
},
childEnvBoundary,
cancelReapTraceReadiness: cancelReapTraceState(lifecycle),
secretBoundary: {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
valuesRedacted: true
}
};
}
export function codexHomeContractState(codexHomeInfo, codexHome, codexHomeFiles = null) {
const filesReady = codexHomeFiles?.configPresent === true &&
codexHomeFiles?.configReadable === true &&
codexHomeFiles?.authPresent === true &&
codexHomeFiles?.authReadable === true;
const ready = codexHomeInfo.exists && codexHomeInfo.readable && codexHomeInfo.writable && filesReady;
return {
path: codexHome,
status: ready ? "ready" : "blocked",
exists: codexHomeInfo.exists,
readable: codexHomeInfo.readable,
writable: codexHomeInfo.writable,
configToml: {
path: codexHomeFiles?.configPath ?? path.join(codexHome, "config.toml"),
present: codexHomeFiles?.configPresent === true,
readable: codexHomeFiles?.configReadable === true
},
authJson: {
path: codexHomeFiles?.authPath ?? path.join(codexHome, "auth.json"),
present: codexHomeFiles?.authPresent === true,
readable: codexHomeFiles?.authReadable === true,
valuesRedacted: true
},
writeRequired: true,
blocker: ready
? null
: !codexHomeInfo.exists || !codexHomeInfo.readable
? "codex_home_missing"
: codexHomeInfo.writable !== true
? "codex_home_write_blocked"
: codexHomeFiles?.configPresent !== true || codexHomeFiles?.configReadable !== true
? "codex_home_config_missing"
: "codex_home_auth_missing",
secretMaterialRead: false,
valuesRedacted: true
};
}
export function childProcessEnv(env = process.env) {
const noProxy = mergeNoProxy(env.NO_PROXY, env.no_proxy, CODEX_CHILD_NO_PROXY_REQUIRED, codexProviderNoProxyEntries(env));
return {
PATH: Object.hasOwn(env, "PATH") ? env.PATH : process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
HOME: env.HOME || process.env.HOME || os.homedir(),
CODEX_HOME: resolveCodexHome(env),
CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "hwlab_code_agent",
UNIDESK_SKILLS_PATH: "/app/skills",
...(env.HWLAB_CLOUD_API_URL ? { HWLAB_CLOUD_API_URL: env.HWLAB_CLOUD_API_URL } : {}),
...(env.HWLAB_RUNTIME_API_URL ? { HWLAB_RUNTIME_API_URL: env.HWLAB_RUNTIME_API_URL } : {}),
...(env.HWLAB_RUNTIME_WEB_URL ? { HWLAB_RUNTIME_WEB_URL: env.HWLAB_RUNTIME_WEB_URL } : {}),
...(env.HWLAB_RUNTIME_NAMESPACE ? { HWLAB_RUNTIME_NAMESPACE: env.HWLAB_RUNTIME_NAMESPACE } : {}),
...(env.HWLAB_RUNTIME_LANE ? { HWLAB_RUNTIME_LANE: env.HWLAB_RUNTIME_LANE } : {}),
...(env.HWLAB_RUNTIME_ENDPOINT_SOURCE ? { HWLAB_RUNTIME_ENDPOINT_SOURCE: env.HWLAB_RUNTIME_ENDPOINT_SOURCE } : {}),
...(env.HWLAB_RUNTIME_ENDPOINT_LOCKED ? { HWLAB_RUNTIME_ENDPOINT_LOCKED: env.HWLAB_RUNTIME_ENDPOINT_LOCKED } : {}),
...(env.HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME ? { HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: env.HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME } : {}),
...(env.HWLAB_API_KEY ? { HWLAB_API_KEY: env.HWLAB_API_KEY } : {}),
NO_PROXY: noProxy,
no_proxy: noProxy,
...(env.LANG ? { LANG: env.LANG } : {}),
...(env.LC_ALL ? { LC_ALL: env.LC_ALL } : {}),
...(env.SSL_CERT_FILE ? { SSL_CERT_FILE: env.SSL_CERT_FILE } : {}),
...(env.NODE_EXTRA_CA_CERTS ? { NODE_EXTRA_CA_CERTS: env.NODE_EXTRA_CA_CERTS } : {}),
...(env.GIT_CONFIG_COUNT ? gitConfigEnv(env) : {})
};
}
export function gitConfigEnv(env = process.env) {
const count = Number.parseInt(env.GIT_CONFIG_COUNT ?? "", 10);
if (!Number.isInteger(count) || count < 0 || count > 64) return {};
const result = { GIT_CONFIG_COUNT: String(count) };
for (let index = 0; index < count; index += 1) {
const keyName = `GIT_CONFIG_KEY_${index}`;
const valueName = `GIT_CONFIG_VALUE_${index}`;
if (typeof env[keyName] === "string") result[keyName] = env[keyName];
if (typeof env[valueName] === "string") result[valueName] = env[valueName];
}
return result;
}
export function childProcessEnvState(env = process.env) {
const child = childProcessEnv(env);
const inheritedForbidden = CODEX_CHILD_STRIPPED_ENV_KEYS.filter((key) => hasEnvValue(child, key));
const sourceForbiddenPresent = CODEX_CHILD_STRIPPED_ENV_KEYS.filter((key) => hasEnvValue(env, key));
const noProxyEntries = splitNoProxy(child.NO_PROXY);
const requiredMissing = CODEX_CHILD_NO_PROXY_REQUIRED.filter((entry) => !noProxyEntries.includes(entry.toLowerCase()));
const providerNoProxy = codexProviderNoProxyEntries(env);
return {
status: inheritedForbidden.length === 0 && requiredMissing.length === 0 ? "ready" : "blocked",
forbiddenSourceEnvKeysPresent: sourceForbiddenPresent,
forbiddenEnvPresent: inheritedForbidden.length > 0,
strippedEnvKeys: sourceForbiddenPresent.filter((key) => !inheritedForbidden.includes(key)),
inheritedForbiddenEnvKeys: inheritedForbidden,
noProxy: {
present: Boolean(child.NO_PROXY),
required: [...CODEX_CHILD_NO_PROXY_REQUIRED],
provider: providerNoProxy,
missing: requiredMissing,
merged: noProxyEntries
},
codeXHome: child.CODEX_HOME,
secretMaterialRead: false,
valuesRedacted: true
};
}
export function codexProviderNoProxyEntries(env = process.env) {
const baseUrl = codexAppServerProviderBaseUrl(env);
if (!baseUrl) return [];
try {
const url = new URL(baseUrl);
const hostname = url.hostname.replace(/^\[|\]$/gu, "");
return [hostname, url.host].filter((entry, index, items) => entry && items.indexOf(entry) === index);
} catch {
return [];
}
}
export function rpcClientConfigSignature({ availability, args = [], childEnv = {} } = {}) {
return JSON.stringify({
command: availability?.command ?? null,
workspace: availability?.workspace ?? null,
codexHome: childEnv.CODEX_HOME ?? null,
args
});
}
export function mergeNoProxy(...parts) {
const entries = [];
const seen = new Set();
for (const entry of parts.flatMap((part) => Array.isArray(part) ? part : splitNoProxy(part))) {
const normalized = String(entry ?? "").trim();
if (!normalized) continue;
const key = normalized.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
entries.push(normalized);
}
return entries.join(",");
}
export function splitNoProxy(value) {
return String(value ?? "")
.split(/[,;\s]+/u)
.map((item) => item.trim())
.filter(Boolean);
}
export function codexStdioSafety() {
return {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
prodTouched: false,
hardwareWritesAllowed: true,
directGatewayCallsAllowed: true,
directBoxSimuCallsAllowed: true,
directPatchPanelCallsAllowed: true,
hardwareControlPath: "codex-stdio-full-access",
valuesRedacted: true
};
}
export 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;
}
export function sessionExpired(session, timestampMs) {
if (!Number.isFinite(timestampMs) || session.status === "expired") return session.status === "expired";
return Date.parse(session.expiresAt) <= timestampMs;
}
export 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();
}
export function plusMs(timestampMs, offsetMs) {
return new Date(timestampMs + offsetMs).toISOString();
}
export function requiredId(value, fallbackPrefix) {
const id = optionalId(value);
if (id) return id;
return `${fallbackPrefix}_${randomUUID()}`;
}
export function optionalId(value) {
return typeof value === "string" && value.trim() ? value.trim() : null;
}
export function positiveInteger(value, fallback) {
return Number.isInteger(value) && value > 0 ? value : fallback;
}
export function firstNonEmpty(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value.trim();
}
return "";
}
export function hasEnvValue(env, name) {
return typeof env?.[name] === "string" && env[name].trim().length > 0;
}
export function effectiveTimeout(timeoutMs) {
return effectiveActivityTimeout(timeoutMs);
}
export function effectiveActivityTimeout(timeoutMs) {
return Number.isInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_CODEX_STDIO_TURN_ACTIVITY_TIMEOUT_MS;
}
export function effectiveHardTimeout(hardTimeoutMs, activityTimeoutMs = null) {
const activityMs = effectiveActivityTimeout(activityTimeoutMs);
const configured = Number.isInteger(hardTimeoutMs) && hardTimeoutMs > 0
? hardTimeoutMs
: DEFAULT_CODEX_STDIO_TURN_HARD_TIMEOUT_MS;
return Math.max(configured, activityMs);
}
export function dropEmpty(value) {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== ""));
}
export function parseJsonOrNull(value) {
try {
return JSON.parse(value);
} catch {
return null;
}
}
export function tailText(value, maxLength = 1200) {
const text = String(value ?? "").trim();
if (text.length <= maxLength) return text;
return text.slice(text.length - maxLength);
}
export 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
};
}
export 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***");
}