Files
pikasTech-HWLAB/internal/cloud/code-agent-chat.mjs
T
2026-05-24 04:07:32 +00:00

2387 lines
88 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
codeAgentSecretRefPlaceholder,
inspectCodeAgentProviderEnv
} from "./code-agent-contract.mjs";
import {
CODEX_STDIO_BACKEND,
CODEX_STDIO_CAPABILITY_LEVEL,
CODEX_STDIO_IMPLEMENTATION_TYPE,
CODEX_STDIO_PROVIDER,
CODEX_STDIO_RUNNER_KIND,
CODEX_STDIO_SESSION_MODE,
createCodexStdioSessionManager,
longLivedSessionGate as codexLongLivedSessionGate
} from "./codex-stdio-session.mjs";
import {
createCodeAgentTraceRecorder,
defaultCodeAgentTraceStore
} from "./code-agent-trace-store.mjs";
import {
DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS,
createCodeAgentSessionRegistry
} from "./code-agent-session-registry.mjs";
import {
HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
HWLAB_M3_IO_CAPABILITY_LEVELS,
HWLAB_M3_IO_API_BASE_URL_ENV,
HWLAB_M3_IO_API_BASE_URL_ENVS,
HWLAB_M3_IO_API_ROUTE,
HWLAB_M3_STATUS_API_ROUTE,
HWLAB_M3_IO_DEV_SERVICE_BASE_URL,
HWLAB_M3_IO_SKILL_NAME,
configuredCloudApiBaseUrl,
runM3IoSkillCommand
} from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
const DEFAULT_MODEL = DEV_CODE_AGENT_PROVIDER_CONTRACT.model;
const DEFAULT_PROJECT_ID = "prj_hwlab-cloud-workbench";
const READONLY_RUNNER_PROVIDER = "codex-readonly-runner";
const READONLY_RUNNER_BACKEND = "hwlab-cloud-api/codex-readonly-runner";
const READONLY_RUNNER_MODEL = "read-only-tools";
const READONLY_RUNNER_KIND = "hwlab-readonly-runner";
const READONLY_RUNNER_SANDBOX = "read-only";
const READONLY_SESSION_MODE = "controlled-readonly-session-registry";
const READONLY_IMPLEMENTATION_TYPE = "controlled-readonly-session-registry";
const READONLY_SESSION_CAPABILITY_LEVEL = "read-only-session-tools";
const M3_IO_SKILL_PROVIDER = "hwlab-skill-cli";
const M3_IO_SKILL_BACKEND = "hwlab-cloud-api/hwlab-agent-runtime-skill-cli";
const M3_IO_SKILL_MODEL = "controlled-m3-io";
const M3_IO_SKILL_RUNNER_KIND = "hwlab-m3-io-skill-cli";
const M3_IO_SKILL_SANDBOX = "hwlab-api-route-only";
const M3_IO_SKILL_SESSION_MODE = "controlled-m3-io-skill-cli";
const M3_IO_SKILL_IMPLEMENTATION_TYPE = "skill-cli-hwlab-api-adapter";
const M3_IO_SKILL_LIMITATION_FLAGS = Object.freeze([
"m3-io-only",
"hwlab-api-route-only",
"not-generic-hardware-control",
"not-durable-session"
]);
const OPENAI_FALLBACK_RUNNER_KIND = "openai-responses-fallback";
const DIRECT_HARDWARE_TARGET_PATTERN = /https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel|hwlab-patch-panel)[^\s"']*|\/invoke\b|\/sync\/tick\b|:7101\b|:7201\b|:7301\b/iu;
const READONLY_TOOL_OUTPUT_LIMIT = 4000;
const MAX_READONLY_SESSIONS = 200;
const CODE_AGENT_PROVIDER_SECRET_REF = codeAgentSecretRefPlaceholder().replace("secretRef:", "");
const CODEX_STDIO_SOURCE_ISSUE = "pikasTech/HWLAB#275";
const READONLY_LIMITATION_FLAGS = Object.freeze([
"not-codex-stdio",
"not-write-capable",
"process-local-session-registry"
]);
const CODEX_STDIO_LIMITATION_FLAGS = Object.freeze([
"hardware-control-via-cloud-api-only",
"no-direct-gateway-link",
"no-direct-box-simu-link",
"no-direct-patch-panel-link",
"secret-values-redacted"
]);
const CODE_AGENT_SYSTEM_PROMPT = [
"你是 HWLAB 云工作台的 Code Agent。",
"请用中文直接回答用户的工作台问题。",
"当前最小版本可以帮助用户整理任务、查看云工作台资源和说明下一步;不要声称已经执行硬件变更。",
"不要声称已通过 M3、M4 或 M5 验收。",
"不要声称 OpenAI Responses 文本 fallback 具备 Codex runner、workspace、tools 或 skills 能力。",
"不要输出 secret、token、kubeconfig 或环境变量原文。"
].join("\n");
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const defaultCodeAgentSessionRegistry = createCodeAgentSessionRegistry({
idleTimeoutMs: DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS,
maxSessions: MAX_READONLY_SESSIONS
});
const defaultCodexStdioSessionManager = createCodexStdioSessionManager();
export async function handleCodeAgentChat(params = {}, options = {}) {
const timestamp = nowIso(options.now);
const { conversationId, sessionId, requestedSessionId } = resolveConversationSessionIds(params);
const messageId = `msg_${randomUUID()}`;
const traceId = cleanProtocolId(params.traceId, "trc") || `trc_${randomUUID()}`;
const providerPlan = resolveProviderPlan(options.env ?? process.env, options);
const sessionRegistry = resolveCodeAgentSessionRegistry(options);
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const traceRecorder = createCodeAgentTraceRecorder({
traceStore,
traceId,
now: options.now,
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace: options.workspace,
sandbox: "workspace-write",
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE
});
const base = {
conversationId,
sessionId,
messageId,
status: "running",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
projectId: cleanProjectId(params.projectId) || DEFAULT_PROJECT_ID
};
try {
const message = normalizeUserMessage(params.message);
const runnerIntent = detectReadOnlyRunnerIntent(message);
traceRecorder.append({
type: "request",
status: "accepted",
label: "request:accepted",
promptSummary: message.length > 160 ? `${message.slice(0, 157)}...` : message,
waitingFor: "codex-stdio-readiness"
});
const securityIntent = runnerIntent.kind === "security" ? runnerIntent : null;
if (securityIntent) {
traceRecorder.append({
type: "error",
status: "blocked",
label: "security:blocked",
errorCode: "security_blocked",
message: securityIntent.reason,
terminal: true
});
throw runnerError("security_blocked", securityIntent.reason, {
provider: CODEX_STDIO_PROVIDER,
model: providerPlan.model,
backend: CODEX_STDIO_BACKEND,
workspace: options.workspace ?? repoRoot,
sandbox: "workspace-write",
toolCalls: [],
skills: notRequestedSkills(),
runner: codexStdioBlockedRunnerDescriptor({ workspace: options.workspace, session: null }),
runnerTrace: traceRecorder.runnerTrace({
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace: options.workspace ?? repoRoot,
sandbox: "workspace-write",
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
limitations: ["security-blocked-before-stdio"]
}),
capabilityLevel: "blocked",
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
runnerLimitations: ["security-blocked-before-stdio"],
blockers: [{
code: "security_blocked",
sourceIssue: "pikasTech/HWLAB#275",
summary: securityIntent.reason
}],
route: null,
toolName: securityIntent.toolName ?? null
});
}
if (runnerIntent.kind === "session_context") {
traceRecorder.append({
type: "session",
status: "context_requested",
label: "session:context_requested",
waitingFor: "codex-stdio"
});
}
const codexStdioAvailability = await inspectCodexStdioFeasibility(options.env ?? process.env, options);
if (codexStdioLongLivedReady(codexStdioAvailability)) {
const stdioResult = runnerIntent.kind === "m3_io"
? await callCodexStdioWithM3IoSkillRunner({
message,
intent: runnerIntent,
conversationId,
sessionId: requestedSessionId,
traceId,
env: options.env ?? process.env,
now: options.now,
workspace: options.workspace,
timeoutMs: options.timeoutMs,
model: providerPlan.model,
codexStdioManager: options.codexStdioManager,
traceRecorder,
conversationFacts: conversationFactsForPrompt(sessionRegistry, conversationId),
requestJson: options.m3IoSkillRequestJson
})
: await callCodexStdioRunner({
message,
conversationId,
sessionId: requestedSessionId,
traceId,
env: options.env ?? process.env,
now: options.now,
workspace: options.workspace,
timeoutMs: options.timeoutMs,
model: providerPlan.model,
codexStdioManager: options.codexStdioManager,
traceRecorder,
conversationFacts: conversationFactsForPrompt(sessionRegistry, conversationId)
});
return completedRunnerPayload({ base, runnerResult: stdioResult, messageId, now: options.now, sessionRegistry });
}
const primaryBlocker = codexStdioAvailability.blockers?.[0] ?? null;
traceRecorder.append({
type: "error",
status: "blocked",
label: "codex-stdio:blocked",
errorCode: primaryBlocker?.code ?? "codex_stdio_blocked",
message: primaryBlocker?.summary ?? "Codex stdio is not available; no local fallback will be used.",
waitingFor: "codex-stdio-readiness",
terminal: true
});
throw runnerError(primaryBlocker?.code ?? "codex_stdio_blocked", "Codex stdio long-lived session is unavailable; no controlled-readonly, skills, shell, or text fallback will answer this request.", {
provider: CODEX_STDIO_PROVIDER,
model: providerPlan.model,
backend: CODEX_STDIO_BACKEND,
workspace: codexStdioAvailability.workspace ?? options.workspace ?? repoRoot,
sandbox: codexStdioAvailability.sandbox ?? "workspace-write",
session: null,
toolCalls: [],
skills: notRequestedSkills(),
runner: codexStdioBlockedRunnerDescriptor({ workspace: codexStdioAvailability.workspace ?? options.workspace, session: null }),
runnerTrace: traceRecorder.runnerTrace({
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace: codexStdioAvailability.workspace ?? options.workspace ?? repoRoot,
sandbox: codexStdioAvailability.sandbox ?? "workspace-write",
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
limitations: ["no-fallback", "codex-stdio-required"],
outputTruncated: false
}),
capabilityLevel: "blocked",
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionReuse: null,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
runnerLimitations: ["codex-stdio-required", "no-controlled-readonly-fallback", "no-text-fallback"],
codexStdioFeasibility: codexStdioAvailability,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
runnerKind: CODEX_STDIO_RUNNER_KIND,
session: null,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdioFeasibility: codexStdioAvailability
}),
blockers: codexStdioAvailability.blockers?.length
? codexStdioAvailability.blockers
: [{
code: "codex_stdio_blocked",
sourceIssue: "pikasTech/HWLAB#275",
summary: "Codex stdio readiness did not pass."
}],
route: "/v1/agent/chat",
toolName: runnerIntent.toolName ?? "codex-stdio.session",
availability: await describeCodeAgentAvailability(options.env ?? process.env, options)
});
} catch (error) {
const failedAt = nowIso(options.now);
const payload = {
...base,
status: chatStatusForError(error),
updatedAt: failedAt,
error: normalizeChatError(error, {
traceId,
provider: error.provider ?? base.provider,
model: error.model ?? base.model,
backend: error.backend ?? base.backend,
runner: error.runner,
capabilityLevel: error.capabilityLevel ?? "blocked",
route: error.route,
toolName: error.toolName
})
};
if (payload.error?.blocker) payload.blocker = payload.error.blocker;
if (payload.error?.blockers) payload.blockers = payload.error.blockers;
if (error.provider) payload.provider = error.provider;
if (error.model) payload.model = error.model;
if (error.backend) payload.backend = error.backend;
if (error.workspace !== undefined) payload.workspace = error.workspace;
if (error.sandbox !== undefined) payload.sandbox = error.sandbox;
if (error.session !== undefined) payload.session = error.session;
if (error.toolCalls !== undefined) payload.toolCalls = error.toolCalls;
if (error.skills !== undefined) payload.skills = error.skills;
if (error.runner !== undefined) payload.runner = error.runner;
if (error.runnerTrace !== undefined) payload.runnerTrace = error.runnerTrace;
if (payload.runnerTrace === undefined) {
payload.runnerTrace = traceRecorder.runnerTrace({
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace: error.workspace ?? options.workspace ?? repoRoot,
sandbox: error.sandbox ?? "workspace-write",
sessionMode: error.sessionMode ?? CODEX_STDIO_SESSION_MODE,
implementationType: error.implementationType ?? CODEX_STDIO_IMPLEMENTATION_TYPE,
limitations: error.runnerLimitations ?? ["codex-stdio-required"]
});
}
if (error.capabilityLevel !== undefined) payload.capabilityLevel = error.capabilityLevel;
if (error.sessionMode !== undefined) payload.sessionMode = error.sessionMode;
if (error.sessionReuse !== undefined) payload.sessionReuse = error.sessionReuse;
if (error.implementationType !== undefined) payload.implementationType = error.implementationType;
if (error.runnerLimitations !== undefined) payload.runnerLimitations = error.runnerLimitations;
if (error.codexStdioFeasibility !== undefined) payload.codexStdioFeasibility = error.codexStdioFeasibility;
if (error.longLivedSessionGate !== undefined) payload.longLivedSessionGate = error.longLivedSessionGate;
if (error.conversationFacts !== undefined) {
payload.conversationFacts = error.conversationFacts;
} else if (typeof sessionRegistry.getConversationFacts === "function") {
payload.conversationFacts = sessionRegistry.getConversationFacts(conversationId);
}
if (payload.conversationFacts && payload.error?.blocker) payload.error.blocker.conversationFacts = payload.conversationFacts;
if (payload.conversationFacts && payload.blocker) payload.blocker.conversationFacts = payload.conversationFacts;
if (error.availability !== undefined) {
payload.availability = error.availability;
} else if (["provider_unavailable", "provider_timeout", "codex_cli_binary_missing"].includes(error.code)) {
payload.availability = await describeCodeAgentAvailability(options.env ?? process.env, options);
} else if (["runner_unavailable", "tool_unavailable", "skills_unavailable", "security_blocked", "codex_stdio_blocked", "codex_stdio_failed", "codex_stdio_protocol_blocked", "codex_stdio_empty_response", "codex_stdio_command_probe_failed"].includes(error.code)) {
payload.availability = await describeCodeAgentAvailability(options.env ?? process.env, options);
}
return payload;
}
}
function completedRunnerPayload({ base, runnerResult, messageId, now, sessionRegistry }) {
const completedAt = nowIso(now);
const blocker = structuredCompletionBlocker(runnerResult, {
traceId: base.traceId,
provider: runnerResult.provider,
backend: runnerResult.backend,
runner: runnerResult.runner,
capabilityLevel: runnerResult.capabilityLevel
});
const payload = finalizeCodeAgentChatPayload({
...base,
sessionId: runnerResult.session?.sessionId ?? base.sessionId,
status: "completed",
updatedAt: completedAt,
provider: runnerResult.provider,
model: runnerResult.model,
backend: runnerResult.backend,
workspace: runnerResult.workspace,
sandbox: runnerResult.sandbox,
session: runnerResult.session,
sessionMode: runnerResult.sessionMode,
sessionReuse: runnerResult.sessionReuse,
implementationType: runnerResult.implementationType,
runnerLimitations: runnerResult.runnerLimitations,
codexStdioFeasibility: runnerResult.codexStdioFeasibility,
longLivedSessionGate: runnerResult.longLivedSessionGate,
toolCalls: runnerResult.toolCalls,
skills: runnerResult.skills,
runner: runnerResult.runner,
runnerTrace: runnerResult.runnerTrace,
capabilityLevel: runnerResult.capabilityLevel,
reply: {
messageId,
role: "assistant",
content: runnerResult.content,
createdAt: completedAt
},
...(blocker ? { blocker, blockers: [blocker, ...(runnerResult.blockers ?? [])].filter(Boolean) } : {}),
usage: null,
providerTrace: runnerResult.providerTrace
});
return attachConversationFacts(payload, sessionRegistry, { now });
}
export function validateCodeAgentChatSchema(payload) {
const required = [
"conversationId",
"sessionId",
"messageId",
"status",
"createdAt",
"updatedAt",
"traceId",
"provider",
"model",
"backend"
];
for (const field of required) {
if (!payload || typeof payload !== "object" || !Object.hasOwn(payload, field)) {
throw new Error(`code agent chat response missing ${field}`);
}
}
if (!["running", "completed", "failed", "timeout", "error", "canceled"].includes(payload.status)) {
throw new Error(`code agent chat response has invalid status ${JSON.stringify(payload.status)}`);
}
if (Number.isNaN(Date.parse(payload.createdAt)) || Number.isNaN(Date.parse(payload.updatedAt))) {
throw new Error("code agent chat response timestamps must be RFC 3339 strings");
}
if (["failed", "timeout", "error", "canceled"].includes(payload.status) && typeof payload.error?.message !== "string") {
throw new Error("non-completed code agent chat response must include error.message");
}
if (payload.status === "completed" && (typeof payload.reply?.content !== "string" || !payload.reply.content.trim())) {
throw new Error("completed code agent chat response must include non-empty reply.content");
}
validateCodeAgentToolCallContract(payload);
}
function validateCodeAgentToolCallContract(payload) {
if (!Array.isArray(payload.toolCalls)) return;
for (const [index, toolCall] of payload.toolCalls.entries()) {
if (!toolCall || typeof toolCall !== "object") {
throw new Error(`code agent toolCalls[${index}] must be an object`);
}
const directTarget = findDirectHardwareTarget(toolCall);
if (directTarget) {
throw new Error(`code agent toolCalls[${index}] must not include direct gateway/box/patch-panel target ${directTarget}`);
}
if (!isM3IoToolCall(toolCall, payload)) continue;
const structuredBlocked = toolCall.status === "blocked" && Boolean(toolCall.blocker?.code || toolCall.capabilityBlocker?.code || toolCall.error?.code);
if (!m3ToolCallRouteAllowed(toolCall.route) && !structuredBlocked) {
throw new Error(`code agent M3 toolCalls[${index}] must show route ${HWLAB_M3_IO_API_ROUTE}/${HWLAB_M3_STATUS_API_ROUTE} or structured blocked`);
}
if (toolCall.route === HWLAB_M3_IO_API_ROUTE && (toolCall.method ?? "POST") !== "POST") {
throw new Error(`code agent M3 toolCalls[${index}] must use POST ${HWLAB_M3_IO_API_ROUTE}`);
}
if (toolCall.route === HWLAB_M3_STATUS_API_ROUTE && (toolCall.method ?? "GET") !== "GET") {
throw new Error(`code agent M3 toolCalls[${index}] must use GET ${HWLAB_M3_STATUS_API_ROUTE}`);
}
}
}
function isM3IoToolCall(toolCall, payload) {
return toolCall.name === HWLAB_M3_IO_SKILL_NAME ||
toolCall.route === HWLAB_M3_IO_API_ROUTE ||
toolCall.route === HWLAB_M3_STATUS_API_ROUTE ||
payload.provider === M3_IO_SKILL_PROVIDER ||
payload.runner?.kind === M3_IO_SKILL_RUNNER_KIND ||
payload.runnerTrace?.runnerKind === M3_IO_SKILL_RUNNER_KIND;
}
function m3ToolCallRouteAllowed(route) {
return route === HWLAB_M3_IO_API_ROUTE || route === HWLAB_M3_STATUS_API_ROUTE;
}
function findDirectHardwareTarget(value, seen = new Set()) {
if (typeof value === "string") {
const match = value.match(DIRECT_HARDWARE_TARGET_PATTERN);
return match?.[0] ?? null;
}
if (!value || typeof value !== "object") return null;
if (seen.has(value)) return null;
seen.add(value);
for (const child of Object.values(value)) {
const match = findDirectHardwareTarget(child, seen);
if (match) return match;
}
return null;
}
function finalizeCodeAgentChatPayload(payload) {
validateCodeAgentChatSchema(payload);
return payload;
}
function attachConversationFacts(payload, sessionRegistry, { now } = {}) {
if (!sessionRegistry || typeof sessionRegistry.recordFact !== "function") return payload;
const conversationFacts = sessionRegistry.recordFact(payload.conversationId, {
kind: payload.toolCalls?.some((toolCall) => toolCall?.name === "session.context") ? "session_context" : "runner_turn",
conversationId: payload.conversationId,
sessionId: payload.sessionId,
traceId: payload.traceId,
provider: payload.provider,
backend: payload.backend,
workspace: payload.workspace,
sandbox: payload.sandbox,
session: payload.session,
sessionMode: payload.sessionMode,
sessionReuse: payload.sessionReuse,
implementationType: payload.implementationType,
runner: payload.runner,
runnerTrace: payload.runnerTrace,
capabilityLevel: payload.capabilityLevel,
toolCalls: payload.toolCalls,
skills: payload.skills
}, { now });
return {
...payload,
conversationFacts
};
}
export async function describeCodeAgentAvailability(env = process.env, options = {}) {
const providerPlan = resolveProviderPlan(env, options);
const providerContract = inspectCodeAgentProviderEnv(env);
const missingEnv = providerPlan.mode === "openai"
? providerContract.missingEnv
: [];
if (providerPlan.mode === "codex-cli" && !env.OPENAI_API_KEY) {
missingEnv.push("OPENAI_API_KEY");
}
const sessionRegistry = resolveCodeAgentSessionRegistry(options);
const runnerAvailability = await inspectReadOnlyRunnerAvailability(env, {
...options,
sessionRegistry
});
const codexStdio = await inspectCodexStdioFeasibility(env, options);
const codexStdioReady = codexStdioLongLivedReady(codexStdio);
const codexStdioActive = providerPlan.mode !== "openai" && codexStdioReady;
const activeRunnerAvailability = codexStdioActive
? codexStdioRunnerAvailability(codexStdio)
: codexStdioBlockedRunnerAvailability(codexStdio);
const blocked = providerPlan.mode === "openai"
? !providerContract.ready
: providerPlan.mode === "codex-stdio"
? !codexStdioReady
: missingEnv.length > 0;
const fallbackAvailability = {
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
mode: providerPlan.mode,
status: blocked ? "blocked" : "available",
ready: !blocked,
capabilityLevel: blocked ? "blocked" : "text-chat-only",
blocker: blocked ? fallbackBlockerForPlan({ providerPlan, providerContract, codexStdio }) : null,
reason: blocked ? "provider_unavailable" : null,
missingEnv,
secretRefs: blocked ? providerContract.secretRefs : [],
egress: providerContract.egress,
safety: providerContract.safety
};
const longLivedGate = longLivedSessionGate({
provider: codexStdioActive ? CODEX_STDIO_PROVIDER : runnerAvailability.provider,
runnerKind: codexStdioActive ? CODEX_STDIO_RUNNER_KIND : runnerAvailability.kind,
sessionMode: codexStdioActive ? CODEX_STDIO_SESSION_MODE : READONLY_SESSION_MODE,
implementationType: codexStdioActive ? CODEX_STDIO_IMPLEMENTATION_TYPE : READONLY_IMPLEMENTATION_TYPE,
codexStdioFeasibility: codexStdio
});
const agentKind = codexStdioActive ? "codex-stdio-feasible" : "codex-stdio-blocked";
const capabilityStatus = codexStdioActive ? "codex-stdio-feasible" : "blocked";
const codeAgentReady = codexStdioActive;
const status = codexStdioActive ? "codex-stdio-feasible" : "blocked";
const capabilityLevel = codexStdioActive ? "long-lived-codex-stdio-session" : "blocked";
return {
endpoint: "POST /v1/agent/chat",
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
mode: providerPlan.mode,
schema: [
"conversationId",
"sessionId",
"messageId",
"status",
"createdAt",
"updatedAt",
"traceId",
"provider",
"model",
"backend",
"runner",
"workspace",
"sandbox",
"toolCalls",
"skills",
"runnerTrace",
"capabilityLevel",
"session",
"sessionMode",
"sessionReuse",
"implementationType",
"runnerLimitations",
"codexStdioFeasibility",
"longLivedSessionGate",
"error.message",
"error.code",
"error.missingEnv",
"availability.status"
],
runner: activeRunnerAvailability,
fallback: fallbackAvailability,
codexStdio,
status,
readinessStatus: status,
agentKind,
capabilityStatus,
blocker: codeAgentReady ? null : primaryCodeAgentBlocker({ blocked, providerContract, providerPlan, codexStdio, runnerAvailability }),
reason: codeAgentReady ? null : primaryCodeAgentReason({ blocked, codexStdio, runnerAvailability }),
summary: codeAgentAvailabilitySummary({ blocked, codexStdio, runnerAvailability }),
m3IoSkill: runnerAvailability.m3IoSkill,
missingEnv,
secretRefs: blocked ? providerContract.secretRefs : [],
egress: providerContract.egress,
safety: providerContract.safety,
capabilityLevel,
workspace: activeRunnerAvailability.workspace,
sandbox: activeRunnerAvailability.sandbox,
session: activeRunnerAvailability.sessionRegistry?.recentSessions?.[0] ?? null,
sessionStatus: activeRunnerAvailability.sessionRegistry?.recentSessions?.[0]?.status ?? null,
sessionRegistry: activeRunnerAvailability.sessionRegistry ?? runnerAvailability.sessionRegistry,
sessionMode: activeRunnerAvailability.sessionMode,
implementationType: activeRunnerAvailability.implementationType,
runnerLimitations: activeRunnerAvailability.runnerLimitations,
codexStdioFeasibility: codexStdio,
longLivedSessionGate: longLivedGate,
blockers: [
...codexStdio.blockers,
...(blocked && providerPlan.mode !== "codex-stdio" ? [{
code: "openai_fallback_blocked",
sourceIssue: "pikasTech/HWLAB#143",
summary: providerPlan.mode === "openai" ? providerContract.blocker : "Provider fallback credential/config is incomplete."
}] : [])
],
blockerCodes: [
...codexStdio.blockerCodes,
...(blocked && providerPlan.mode !== "codex-stdio" ? ["openai_fallback_blocked"] : [])
],
ready: codeAgentReady,
partialReady: false
};
}
function fallbackBlockerForPlan({ providerPlan, providerContract, codexStdio }) {
if (providerPlan.mode === "openai") return providerContract.blocker;
if (providerPlan.mode === "codex-stdio") {
return codexStdio.blockers?.[0]?.summary ?? "Codex stdio long-lived session is blocked.";
}
return "凭证缺口";
}
function codexStdioLongLivedReady(codexStdio = {}) {
return Boolean(
(codexStdio.ready === true || codexStdio.canStartLongLivedCodexStdio === true) &&
codexStdio.commandProbe?.ready === true
);
}
function codexStdioRunnerAvailability(codexStdio = {}) {
const workspace = codexStdio.workspace ?? repoRoot;
const sandbox = codexStdio.sandbox ?? "workspace-write";
return {
kind: CODEX_STDIO_RUNNER_KIND,
backend: CODEX_STDIO_BACKEND,
provider: CODEX_STDIO_PROVIDER,
workspace,
sandbox,
mode: CODEX_STDIO_SESSION_MODE,
sessionMode: CODEX_STDIO_SESSION_MODE,
session: CODEX_STDIO_SESSION_MODE,
status: "available",
ready: true,
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
longLivedSession: true,
durableSession: true,
durable: true,
codexStdio: true,
writeCapable: true,
readOnly: false,
runnerLimitations: [...CODEX_STDIO_LIMITATION_FLAGS],
codexStdioFeasibility: codexStdio,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
runnerKind: CODEX_STDIO_RUNNER_KIND,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdioFeasibility: codexStdio
}),
sessionRegistry: codexStdioSessionRegistrySummary(codexStdio),
safety: {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
hardwareControlViaCloudApiOnly: true,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
m3m4m5AcceptanceClaimsAllowed: false
}
};
}
function codexStdioBlockedRunnerAvailability(codexStdio = {}) {
const workspace = codexStdio.workspace ?? repoRoot;
const sandbox = codexStdio.sandbox ?? "workspace-write";
return {
kind: CODEX_STDIO_RUNNER_KIND,
backend: CODEX_STDIO_BACKEND,
provider: CODEX_STDIO_PROVIDER,
workspace,
sandbox,
mode: CODEX_STDIO_SESSION_MODE,
sessionMode: CODEX_STDIO_SESSION_MODE,
session: CODEX_STDIO_SESSION_MODE,
status: "blocked",
ready: false,
capabilityLevel: "blocked",
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
longLivedSession: false,
durableSession: false,
durable: false,
codexStdio: true,
writeCapable: false,
readOnly: false,
runnerLimitations: ["codex-stdio-required", "no-controlled-readonly-fallback", "no-text-fallback"],
codexStdioFeasibility: codexStdio,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
runnerKind: CODEX_STDIO_RUNNER_KIND,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdioFeasibility: codexStdio
}),
sessionRegistry: codexStdioSessionRegistrySummary(codexStdio),
safety: {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
hardwareControlViaCloudApiOnly: true,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
m3m4m5AcceptanceClaimsAllowed: false
}
};
}
function codexStdioSessionRegistrySummary(codexStdio = {}) {
return {
kind: "codex-stdio-session-registry",
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
status: "available",
sessionCount: codexStdio.sessionLifecycle?.activeSessions ?? codexStdio.lifecycleSupervisor?.activeSessions ?? null,
maxSessions: codexStdio.sessionLifecycle?.maxSessions ?? codexStdio.lifecycleSupervisor?.maxSessions ?? null,
idleTimeoutMs: codexStdio.sessionLifecycle?.idleTimeoutMs ?? codexStdio.lifecycleSupervisor?.idleTimeoutMs ?? null,
statuses: ["creating", "ready", "busy", "idle", "timeout", "error", "canceled", "interrupted", "expired", "failed"],
statusCounts: codexStdio.statusCounts ?? null,
sessionMode: CODEX_STDIO_SESSION_MODE,
recentSessions: Array.isArray(codexStdio.recentSessions) ? codexStdio.recentSessions : [],
longLivedCodexStdio: true,
longLivedSession: true,
durable: true,
codexStdio: true,
secretMaterialStored: false,
valuesRedacted: true
};
}
function primaryCodeAgentBlocker({ blocked, providerContract, providerPlan, codexStdio, runnerAvailability }) {
if (codexStdio.blockers?.length > 0) return codexStdio.blockers[0].summary;
if (blocked && !runnerAvailability.ready) return providerPlan.mode === "openai" ? providerContract.blocker : "凭证缺口";
if (runnerAvailability.ready) return "Codex stdio is blocked; controlled read-only runner fallback is disabled for /v1/agent/chat.";
return null;
}
function primaryCodeAgentReason({ blocked, codexStdio, runnerAvailability }) {
if (codexStdio.blockers?.length > 0) return codexStdio.blockers[0].code;
if (blocked && !runnerAvailability.ready) return "provider_unavailable";
if (runnerAvailability.ready) return "codex_stdio_blocked_fallback_disabled";
return null;
}
function codeAgentAvailabilitySummary({ blocked, codexStdio, runnerAvailability }) {
if (codexStdio.ready) {
return "Codex stdio long-lived session adapter is feasible; /v1/agent/chat can create/reuse/cancel/reap sessions with trace capture. Hardware control still must use cloud-api/HWLAB API/skill CLI.";
}
if (runnerAvailability.ready) return "Codex stdio long-lived session is blocked; /v1/agent/chat will not use controlled-readonly-session-registry, local skills discovery, shell/file shortcuts, or text fallback.";
return blocked
? "OpenAI fallback 和 Codex stdio long-lived session 都 blocked;不返回伪完成。"
: "Only text-chat fallback is available; it does not satisfy the Codex stdio session gate.";
}
function resolveProviderPlan(env, options = {}) {
const provider = String(env.HWLAB_CODE_AGENT_PROVIDER || "auto").trim().toLowerCase();
const model = firstNonEmpty(
options.model,
env.HWLAB_CODE_AGENT_MODEL,
env.OPENAI_MODEL,
env.CODE_QUEUE_DEFAULT_MODEL,
DEFAULT_MODEL
);
if (provider === "openai") {
return {
provider: "openai-responses",
model,
backend: "hwlab-cloud-api/openai-responses",
mode: "openai"
};
}
if (provider === "codex-cli" || provider === "codex") {
return {
provider: "codex-cli",
model,
backend: "hwlab-cloud-api/codex-cli",
mode: "codex-cli"
};
}
if (provider === "codex-stdio" || provider === "codex-mcp-stdio") {
return {
provider: CODEX_STDIO_PROVIDER,
model,
backend: CODEX_STDIO_BACKEND,
mode: "codex-stdio"
};
}
return {
provider: env.OPENAI_API_KEY ? "openai-responses" : "codex-cli",
model,
backend: env.OPENAI_API_KEY ? "hwlab-cloud-api/openai-responses" : "hwlab-cloud-api/codex-cli",
mode: env.OPENAI_API_KEY ? "openai" : "codex-cli",
auto: true
};
}
async function inspectReadOnlyRunnerAvailability(env, options = {}) {
const workspace = resolveRunnerWorkspace(env, options);
const workspaceReady = Boolean(workspace && existsSync(workspace));
const skillsDirs = resolveSkillDirs(env, options);
const skillsDirsPresent = skillsDirs.filter((dir) => existsSync(dir));
const codexStdioFeasibility = await inspectCodexStdioFeasibility(env, options);
const sessionRegistry = resolveCodeAgentSessionRegistry(options).describe();
const m3IoApiBaseUrl = configuredCloudApiBaseUrl(env);
return {
kind: READONLY_RUNNER_KIND,
backend: READONLY_RUNNER_BACKEND,
provider: READONLY_RUNNER_PROVIDER,
workspace,
sandbox: READONLY_RUNNER_SANDBOX,
mode: READONLY_SESSION_MODE,
sessionMode: READONLY_SESSION_MODE,
session: READONLY_SESSION_MODE,
status: workspaceReady ? "available" : "blocked",
ready: workspaceReady,
capabilityLevel: workspaceReady ? READONLY_SESSION_CAPABILITY_LEVEL : "blocked",
implementationType: READONLY_IMPLEMENTATION_TYPE,
longLivedSession: true,
durableSession: false,
codexStdio: false,
writeCapable: false,
runnerLimitations: [...READONLY_LIMITATION_FLAGS],
codexStdioFeasibility,
longLivedSessionGate: longLivedSessionGate({
provider: READONLY_RUNNER_PROVIDER,
runnerKind: READONLY_RUNNER_KIND,
sessionMode: READONLY_SESSION_MODE,
implementationType: READONLY_IMPLEMENTATION_TYPE,
codexStdioFeasibility
}),
sessionRegistry,
skillsDirs,
skillsDirsPresent,
m3IoSkill: {
status: m3IoApiBaseUrl ? "available" : "blocked",
service: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route: HWLAB_M3_IO_API_ROUTE,
statusRoute: HWLAB_M3_STATUS_API_ROUTE,
capabilityLevel: m3IoApiBaseUrl
? HWLAB_M3_IO_CAPABILITY_LEVELS.ready
: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
blockedCapabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
hwlabApi: {
source: m3IoApiBaseUrl ? m3IoSkillApiBaseUrlSource(env) : "missing-config",
baseUrlConfigured: Boolean(m3IoApiBaseUrl),
redactedBaseUrl: m3IoApiBaseUrl ? redactUrl(m3IoApiBaseUrl) : null,
recommendedEnv: HWLAB_M3_IO_API_BASE_URL_ENV,
requiredEnv: [...HWLAB_M3_IO_API_BASE_URL_ENVS]
},
blocker: m3IoApiBaseUrl ? null : m3IoApiBaseUrlMissingBlocker(),
allowedRoutes: [`POST ${HWLAB_M3_IO_API_ROUTE}`, `GET ${HWLAB_M3_STATUS_API_ROUTE}`],
directGatewayCallsAllowed: false,
directBoxCallsAllowed: false,
directPatchPanelCallsAllowed: false,
fallbackAllowed: false
},
safety: runnerSafetyContract()
};
}
async function callCodexStdioRunner({ message, conversationId, sessionId, traceId, env, now, workspace, timeoutMs, model, codexStdioManager, traceRecorder, conversationFacts }) {
const manager = resolveCodexStdioSessionManager({ codexStdioManager });
try {
return await manager.chat({
message,
conversationId,
sessionId,
traceId,
env,
now,
workspace,
timeoutMs,
model,
traceRecorder,
conversationFacts
});
} catch (error) {
const availability = error.availability ?? manager.describe({ env, workspace });
const session = error.session ?? null;
const trace = error.runnerTrace ?? {
traceId,
runnerKind: CODEX_STDIO_RUNNER_KIND,
workspace: workspace ?? availability.workspace ?? repoRoot,
sandbox: availability.sandbox ?? "workspace-write",
sessionMode: CODEX_STDIO_SESSION_MODE,
events: [`blocked:${error.code ?? "codex_stdio_failed"}`],
valuesPrinted: false
};
throw runnerError(error.code ?? "codex_stdio_failed", error.message, {
provider: CODEX_STDIO_PROVIDER,
model,
backend: CODEX_STDIO_BACKEND,
workspace: workspace ?? availability.workspace ?? null,
sandbox: availability.sandbox ?? "workspace-write",
session,
toolCalls: Array.isArray(error.toolCalls) ? error.toolCalls : [],
skills: error.skills ?? notRequestedSkills(),
runner: {
kind: CODEX_STDIO_RUNNER_KIND,
provider: CODEX_STDIO_PROVIDER,
backend: CODEX_STDIO_BACKEND,
workspace: workspace ?? availability.workspace ?? repoRoot,
sandbox: availability.sandbox ?? "workspace-write",
session: CODEX_STDIO_SESSION_MODE,
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionId: session?.sessionId ?? null,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdio: true,
longLivedSession: true,
durableSession: true,
writeCapable: true,
readOnly: false,
capabilityLevel: "blocked"
},
runnerTrace: trace,
capabilityLevel: "blocked",
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionReuse: session ? sessionReuseEvidence(session) : null,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
runnerLimitations: ["codex-stdio-blocked"],
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
}),
blockers: error.blockers ?? availability.blockers,
route: error.route ?? null,
toolName: error.toolName ?? (error.missingTools?.length ? "codex-stdio.required-tools" : "codex-stdio.session"),
missingTools: error.missingTools,
availability: await describeCodeAgentAvailability(env, { codexStdioManager: manager, workspace })
});
}
}
async function callCodexStdioWithM3IoSkillRunner({
message,
intent,
conversationId,
sessionId,
traceId,
env,
now,
workspace,
timeoutMs,
model,
codexStdioManager,
traceRecorder,
conversationFacts,
requestJson
}) {
const stdioResult = await callCodexStdioRunner({
message,
conversationId,
sessionId,
traceId,
env,
now,
workspace,
timeoutMs,
model,
codexStdioManager,
traceRecorder,
conversationFacts
});
traceRecorder?.append({
type: "tool_call",
status: "started",
label: "tool:hwlab-m3-io:started",
toolName: HWLAB_M3_IO_SKILL_NAME,
waitingFor: HWLAB_M3_IO_API_ROUTE
});
const commandArgs = m3IoSkillArgsForIntent(intent, { env, traceId });
const skillResult = await runM3IoSkillCommand(commandArgs, {
env,
now,
requestJson
});
traceRecorder?.append({
type: "tool_call",
status: skillResult.ok ? "completed" : "blocked",
label: `tool:hwlab-m3-io:${skillResult.ok ? "completed" : "blocked"}`,
toolName: HWLAB_M3_IO_SKILL_NAME,
outputSummary: `route=${skillResult.route}; status=${skillResult.status}; operationId=${skillResult.operationId ?? "null"}`,
terminal: false
});
return codexStdioM3IoSkillRunnerResult({
stdioResult,
skillResult,
commandArgs,
traceId,
now
});
}
function codexStdioM3IoSkillRunnerResult({ stdioResult, skillResult, commandArgs, traceId, now }) {
const capabilityLevel = skillResult.capabilityLevel ?? (
skillResult.ok ? HWLAB_M3_IO_CAPABILITY_LEVELS.ready : HWLAB_M3_IO_CAPABILITY_LEVELS.blocked
);
const route = m3SkillRoute(skillResult);
const method = m3SkillMethod(skillResult);
const toolCall = m3IoSkillToolCall({
skillResult,
commandArgs,
cwd: stdioResult.workspace,
capabilityLevel,
route,
method
});
const blockers = skillResult.blockers ?? (skillResult.blocker ? [skillResult.blocker] : []);
const runnerTrace = {
...stdioResult.runnerTrace,
events: [
...(stdioResult.runnerTrace?.events ?? []),
"tool:skill-cli:started",
`route:${route}`,
`tool:${toolCall.status}`
],
route,
method,
skill: HWLAB_M3_IO_SKILL_NAME,
capabilityLevel,
controlReady: skillResult.controlReady === true,
operationId: skillResult.operationId,
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null,
readback: skillResult.readback ?? skillResult.result?.targetReadback ?? null,
accepted: skillResult.accepted,
status: skillResult.status,
blocker: skillResult.blocker ?? null,
trustBlocker: skillResult.trustBlocker ?? null,
blockers,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false,
finishedAt: nowIso(now)
};
const runner = {
...stdioResult.runner,
skill: {
name: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route
},
toolPolicy: {
...(stdioResult.runner?.toolPolicy ?? {}),
allowed: [
...new Set([
...(stdioResult.runner?.toolPolicy?.allowed ?? []),
`${method} ${route}`
])
]
}
};
return {
...stdioResult,
content: `${stdioResult.content}\n\n${m3IoSkillReply(skillResult)}`,
toolCalls: [
...(stdioResult.toolCalls ?? []),
toolCall
],
skills: {
status: "used",
items: [
...(stdioResult.skills?.items ?? []),
m3IoSkillItem({ capabilityLevel, route })
],
count: (stdioResult.skills?.count ?? 0) + 1,
blockers
},
runner,
runnerTrace,
capabilityLevel,
blockers,
route,
toolName: HWLAB_M3_IO_SKILL_NAME,
providerTrace: {
...(stdioResult.providerTrace ?? {}),
...m3IoSkillProviderTrace({ skillResult, route, method, capabilityLevel }),
runnerKind: stdioResult.runner?.kind ?? CODEX_STDIO_RUNNER_KIND,
codexStdio: true,
transport: "stdio+skill-cli"
}
};
}
function m3IoSkillToolCall({ skillResult, commandArgs, cwd, capabilityLevel, route, method }) {
return {
id: `tool_${randomUUID()}`,
type: "skill-cli",
name: HWLAB_M3_IO_SKILL_NAME,
status: skillResult.status === "succeeded" || skillResult.accepted === true ? "completed" : "blocked",
cwd,
command: redactText(`node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs ${redactM3IoCommandArgs(commandArgs).join(" ")}`),
exitCode: skillResult.ok ? 0 : 2,
stdout: boundToolOutput(JSON.stringify({
route: skillResult.route,
method,
status: skillResult.status,
accepted: skillResult.accepted,
traceId: skillResult.traceId,
operationId: skillResult.operationId,
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null,
audit: skillResult.audit,
evidence: skillResult.evidence,
durable: skillResult.durable,
blocker: skillResult.blocker,
result: skillResult.result,
readback: skillResult.readback ?? skillResult.result?.targetReadback ?? null,
safety: skillResult.safety
})).text,
stderrSummary: skillResult.blocker?.code ?? "",
outputTruncated: false,
route,
method,
hwlabApi: m3IoToolCallApiTarget(skillResult.hwlabApi),
capabilityLevel,
controlReady: skillResult.controlReady === true,
accepted: skillResult.accepted,
operationStatus: skillResult.status,
apiStatus: skillResult.status,
operationId: skillResult.operationId,
traceId: skillResult.traceId,
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null,
audit: skillResult.audit,
evidence: skillResult.evidence,
durable: skillResult.durable,
readback: skillResult.readback ?? skillResult.result?.targetReadback ?? null,
blocker: skillResult.blocker,
capabilityBlocker: skillResult.capabilityBlocker ?? null,
trustBlocker: skillResult.trustBlocker ?? null,
blockers: skillResult.blockers ?? (skillResult.blocker ? [skillResult.blocker] : []),
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false
};
}
function m3IoSkillItem({ capabilityLevel, route }) {
return {
name: HWLAB_M3_IO_SKILL_NAME,
summary: `Controlled M3 DO1/DI1 adapter for HWLAB API ${route}.`,
route,
capabilityLevel,
version: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION
};
}
function m3IoSkillProviderTrace({ skillResult, route, method, capabilityLevel }) {
return {
runnerKind: M3_IO_SKILL_RUNNER_KIND,
skill: HWLAB_M3_IO_SKILL_NAME,
route,
method,
traceId: skillResult.traceId,
operationId: skillResult.operationId,
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null,
readback: skillResult.readback ?? skillResult.result?.targetReadback ?? null,
capabilityLevel,
controlReady: skillResult.controlReady === true,
accepted: skillResult.accepted,
status: skillResult.status,
fallbackUsed: false
};
}
function redactM3IoCommandArgs(commandArgs = []) {
const redacted = [];
for (let index = 0; index < commandArgs.length; index += 1) {
redacted.push(commandArgs[index]);
if (commandArgs[index] === "--api-base-url" && index + 1 < commandArgs.length) {
redacted.push("<hwlab-api-base-url>");
index += 1;
}
}
return redacted;
}
function m3IoToolCallApiTarget(hwlabApi = {}) {
const target = {
...hwlabApi,
route: hwlabApi?.route ?? HWLAB_M3_IO_API_ROUTE
};
delete target.url;
if (findDirectHardwareTarget(target.redactedUrl)) {
target.redactedUrl = null;
}
return target;
}
function m3SkillRoute(skillResult = {}) {
return skillResult.route === HWLAB_M3_STATUS_API_ROUTE ? HWLAB_M3_STATUS_API_ROUTE : HWLAB_M3_IO_API_ROUTE;
}
function m3SkillMethod(skillResult = {}) {
return m3SkillRoute(skillResult) === HWLAB_M3_STATUS_API_ROUTE ? "GET" : skillResult.method ?? "POST";
}
function m3SessionCapabilityLevelForIntent(intent = {}) {
if (intent.action === "status") return HWLAB_M3_IO_CAPABILITY_LEVELS.readonly;
return HWLAB_M3_IO_CAPABILITY_LEVELS.ready;
}
function m3IoSkillMissingApiBaseUrlResult({ traceId, requestId, actorId, blocker, route = HWLAB_M3_IO_API_ROUTE, startedAt, finishedAt }) {
const method = route === HWLAB_M3_STATUS_API_ROUTE ? "GET" : "POST";
return {
ok: false,
service: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route,
method,
hwlabApi: {
route,
redactedUrl: null,
source: "missing-config",
baseUrlConfigured: false,
requiredEnv: [...HWLAB_M3_IO_API_BASE_URL_ENVS],
recommendedEnv: HWLAB_M3_IO_API_BASE_URL_ENV,
missingConfig: [
...HWLAB_M3_IO_API_BASE_URL_ENVS,
"contract:hwlab-agent-runtime.m3-io.apiBaseUrl"
],
cloudApiOnly: true,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false
},
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
controlReady: false,
action: "m3.io",
accepted: false,
status: "blocked",
traceId,
requestId,
actorId,
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written",
durableStatus: null,
summary: "blocked before HWLAB API request"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED",
blocker: blocker.code,
writeStatus: "not_written",
summary: "blocked before HWLAB API request"
},
durable: {
status: "blocked",
durable: false,
blocker: blocker.code,
category: blocker.category,
summary: blocker.message
},
blocker,
capabilityBlocker: blocker,
trustBlocker: null,
blockers: [blocker],
readiness: {
status: "blocked",
controlReady: false,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
route: HWLAB_M3_IO_API_ROUTE,
blocker,
trustBlocker: null
},
command: null,
result: {
value: null,
targetReadback: null
},
readback: null,
controlPath: {
cloudApi: false,
gatewaySimu: false,
boxSimu: false,
patchPanel: false,
frontendBypass: false
},
safety: {
cloudApiRouteOnly: true,
allowedRoute: HWLAB_M3_IO_API_ROUTE,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false,
openAiFallbackUsed: false
},
httpStatus: 0,
error: {
code: blocker.code,
layer: blocker.layer,
category: blocker.category,
blocker,
retryable: blocker.retryable,
userMessage: blocker.userMessage,
message: blocker.message,
traceId,
route,
toolName: HWLAB_M3_IO_SKILL_NAME,
missingConfig: blocker.missingConfig
},
rawStatus: null,
startedAt,
finishedAt,
response: null
};
}
function detectReadOnlyRunnerIntent(message) {
const text = String(message ?? "").trim();
if (isSecretReadRequest(text)) {
return {
kind: "security",
toolName: "security.secret-redaction",
reason: "Code Agent 安全边界不读取或输出 secret、token、kubeconfig、密码、私钥或环境变量原文。"
};
}
const m3IoIntent = detectM3IoIntent(text);
if (m3IoIntent) {
return m3IoIntent;
}
if (isHardwareWriteOrAcceptanceRequest(text)) {
return {
kind: "security",
toolName: "security.hardware-boundary",
reason: `Code Agent 安全边界不直接调用 gateway/box-simu/patch-panel、泛化硬件写接口或宣称 M3/M4/M5 验收通过;M3 DO1/DI1 只能通过 Codex stdio -> Skill CLI -> HWLAB API ${HWLAB_M3_IO_API_ROUTE}。`
};
}
if (isSessionContextRequest(text)) {
return { kind: "session_context", toolName: "session.context" };
}
return { kind: "none" };
}
function isSessionContextRequest(text) {
const value = String(text ?? "");
const referencesEarlierTurns = /(?:前两轮|前2轮|上一轮|上两轮|刚才|前面|之前|previous|earlier|context|上下文|根据.*(?:结果|内容|事实)|基于.*(?:结果|内容|事实))/iu.test(value);
const asksForFacts = /(?:工作目录|当前目录|workspace|路径|skills?|skill|技能|能使用|可用|session|conversation|会话|trace|runner)/iu.test(value);
return referencesEarlierTurns && asksForFacts;
}
function detectM3IoIntent(text) {
const normalized = String(text ?? "");
if (!/\bM3\b|DO1|DI1|数字输出|数字输入|回读|读回|状态|status/u.test(normalized)) {
return null;
}
if (isDirectM3HardwareAccessRequest(normalized)) {
return null;
}
if (/DEV-LIVE|验收|acceptance|(?:M3|M4|M5).{0,20}(?:通过|pass|green|accept)/iu.test(normalized)) {
return null;
}
const topologyMentioned = isM3TopologyRouteMention(normalized);
const wantsRead = /(?:DI1|数字输入).{0,24}(?:read|readback|读取|回读|读回|查询|状态|当前)|(?:read|readback|读取|回读|读回|查询|状态|当前).{0,24}(?:DI1|数字输入)/iu.test(normalized);
const wantsWrite = /(?:DO1|数字输出).{0,30}(?:write|set|置|写|打开|关闭|true|false|高|低)|(?:write|set|置|写|打开|关闭).{0,30}(?:DO1|数字输出)/iu.test(normalized) ||
(topologyMentioned && /(?:执行|run|闭环|loop|控制|触发)/iu.test(normalized));
const wantsStatus = /(?:M3).{0,16}(?:status|状态|聚合|health|readiness)|(?:status|状态|聚合|health|readiness).{0,16}(?:M3)/iu.test(normalized);
if (wantsStatus && !wantsRead && !wantsWrite) {
return {
kind: "m3_io",
toolName: HWLAB_M3_IO_SKILL_NAME,
action: "status"
};
}
if (!wantsRead && !wantsWrite) {
return null;
}
if (wantsRead && !wantsWrite) {
return {
kind: "m3_io",
toolName: HWLAB_M3_IO_SKILL_NAME,
action: "di.read"
};
}
const value = parseM3WriteValue(normalized);
if (typeof value !== "boolean") {
return {
kind: "m3_io",
toolName: HWLAB_M3_IO_SKILL_NAME,
action: "do.write"
};
}
return {
kind: "m3_io",
toolName: HWLAB_M3_IO_SKILL_NAME,
action: "do.write",
value
};
}
function isDirectM3HardwareAccessRequest(text) {
const value = String(text ?? "");
if (/(?:https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel|hwlab-patch-panel)|\/invoke\b|\/sync\/tick\b|:7101\b|:7201\b|:7301\b)/iu.test(value)) {
return true;
}
if (/(?:直接|direct).{0,24}(?:gateway-simu|box-simu|patch-panel|hwlab-patch-panel|gateway|box|\/invoke|\/sync\/tick)|(?:gateway-simu|box-simu|patch-panel|hwlab-patch-panel).{0,24}(?:直接|direct|\/invoke|\/sync\/tick)/iu.test(value)) {
return true;
}
return false;
}
function isM3TopologyRouteMention(text) {
const value = String(text ?? "");
return /res_boxsimu_1\s*:?\s*DO1[\s\S]{0,80}hwlab-patch-panel[\s\S]{0,80}res_boxsimu_2\s*:?\s*DI1/iu.test(value) ||
/DO1\s*[-=]*>\s*(?:hwlab-)?patch-panel\s*[-=]*>\s*DI1/iu.test(value);
}
function parseM3WriteValue(text) {
if (/\btrue\b|\b1\b|\bon\b|打开|置高|高电平|拉高|闭合/iu.test(text)) return true;
if (/\bfalse\b|\b0\b|\boff\b|关闭|置低|低电平|拉低|断开/iu.test(text)) return false;
return null;
}
function isSecretReadRequest(text) {
const asksToRead = /(?:print|show|cat|read|list|dump|echo|输出|显示|读取|列出|查看|打印)/iu.test(text);
const secretTerm = /(?:secret|token|kubeconfig|OPENAI_API_KEY|DATABASE_URL|password|passwd|credential|private key|私钥|密码|凭证|环境变量|密钥)/iu.test(text);
return asksToRead && secretTerm;
}
function isHardwareWriteOrAcceptanceRequest(text) {
const hardwareTarget = /(?:hardware\.operation\.request|hardware\.invoke\.shell|audit\.event\.write|evidence\.record\.write|gateway-simu|box-simu|patch-panel|hwlab-patch-panel|硬件写|直接调用|\/invoke|\/sync\/tick)/iu.test(text);
const mutationVerb = /(?:call|invoke|write|mutate|apply|rollout|accept|pass|验收|通过|写入|调用|变更|操作)/iu.test(text);
const acceptanceClaim = /(?:M3|M4|M5).{0,20}(?:pass|accept|green|通过|验收|完成)/iu.test(text);
return (hardwareTarget && mutationVerb) || acceptanceClaim;
}
function resolveRunnerWorkspace(env = process.env, options = {}) {
const configured = firstNonEmpty(
options.workspace,
env.HWLAB_CODE_AGENT_WORKSPACE,
env.HWLAB_RUNNER_WORKSPACE,
env.WORKSPACE
);
return path.resolve(configured || repoRoot);
}
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 strict = options.skillsDirsExact === true || env.HWLAB_CODE_AGENT_SKILLS_STRICT === "1";
if (strict) {
return [...new Set(configured
.filter((dir) => typeof dir === "string" && dir.trim())
.map((dir) => path.resolve(dir.trim())))];
}
return [...new Set([
...configured,
path.join(os.homedir(), ".agents", "skills"),
"/root/.agents/skills",
"/home/ubuntu/.agents/skills",
path.join(repoRoot, "skills")
]
.filter((dir) => typeof dir === "string" && dir.trim())
.map((dir) => path.resolve(dir.trim())))];
}
function isForbiddenPath(value) {
const normalized = String(value ?? "").replaceAll("\\", "/").toLowerCase();
return /(^|\/)(?:\.env(?:\.|$)|\.npmrc$|\.pypirc$|id_rsa$|id_ed25519$|kubeconfig$|k3s\.yaml$|credentials?$|secrets?$|token(?:s)?$|database-url$)/u.test(normalized) ||
/(?:secret|token|password|passwd|private[_-]?key|openai_api_key|database_url|kubeconfig)/u.test(normalized);
}
function notRequestedSkills() {
return {
status: "not_requested",
items: [],
count: 0,
blockers: []
};
}
function m3IoSkillArgsForIntent(intent, { env, traceId }) {
const args = [
"m3",
intent.action === "status" ? "status" : "io"
];
if (intent.action !== "status") {
args.push("--action", intent.action);
}
args.push(
"--trace-id",
traceId,
"--actor-id",
"usr_code_agent"
);
const apiBaseUrl = configuredCloudApiBaseUrl(env);
if (apiBaseUrl) {
args.push("--api-base-url", apiBaseUrl);
}
if (intent.action === "do.write") {
if (typeof intent.value === "boolean") {
args.push("--value", String(intent.value));
}
args.push(
"--approved",
"--policy",
"hwlab-api-control-with-approval",
"--approval-reason",
"user explicitly requested res_boxsimu_1 DO1 write through HWLAB API"
);
} else {
args.push("--policy", "hwlab-api-readonly");
}
return args;
}
function m3IoSkillApiBaseUrlSource(env = process.env) {
for (const name of HWLAB_M3_IO_API_BASE_URL_ENVS) {
if (firstNonEmpty(env[name])) return `env:${name}`;
}
return "missing-config";
}
function m3IoApiBaseUrlMissingBlocker({ route = HWLAB_M3_IO_API_ROUTE } = {}) {
return {
code: "skill_cli_api_base_missing",
layer: "skill-cli-config",
category: "needs_config",
retryable: false,
source: "code-agent-m3-skill-cli",
summary: `${HWLAB_M3_IO_API_BASE_URL_ENV} is required so the Skill CLI can reach cloud-api from inside the cloud-api runtime container.`,
message: `Set ${HWLAB_M3_IO_API_BASE_URL_ENV} or another supported HWLAB API base URL contract in the cloud-api runtime; the runner will not fall back to a loopback URL or direct hardware services.`,
zh: `cloud-api 运行时缺少 ${HWLAB_M3_IO_API_BASE_URL_ENV},Skill CLI 无法从容器内访问 HWLAB API;不会回退到 loopback URL 或直连硬件服务。`,
userMessage: "M3 Skill CLI 缺少 HWLAB API base URL 配置,需要补齐安全 env 或 contract。",
traceId: null,
route,
toolName: HWLAB_M3_IO_SKILL_NAME,
missingConfig: [
...HWLAB_M3_IO_API_BASE_URL_ENVS,
"contract:hwlab-agent-runtime.m3-io.apiBaseUrl"
]
};
}
function m3IoSkillReply(skillResult) {
const lines = [
`M3 IO Skill CLI result: 已通过 Code Agent -> Skill CLI -> HWLAB API ${skillResult.route} 处理请求;status=${skillResult.status}; accepted=${skillResult.accepted}; traceId=${skillResult.traceId}; operationId=${skillResult.operationId ?? "null"}。`
];
if (skillResult.audit?.summary) {
lines.push(`Audit: ${skillResult.audit.summary}audit 摘要)。`);
}
if (skillResult.evidence?.summary) {
lines.push(`Evidence: ${skillResult.evidence.summary}evidence 摘要)。`);
}
if (skillResult.blocker) {
lines.push(`Blocker: ${skillResult.blocker.code}; ${skillResult.blocker.zh ?? skillResult.blocker.message}blocker)。`);
}
if (skillResult.action === "do.write") {
lines.push(`DO1 写入请求:res_boxsimu_1:DO1=${String(skillResult.command?.value)}DI1 readbackres_boxsimu_2:DI1=${String(skillResult.result?.targetReadback?.value ?? "unknown")}。`);
} else if (skillResult.action === "di.read") {
lines.push(`DI1 读取结果:res_boxsimu_2:DI1=${String(skillResult.result?.value ?? "unknown")}。`);
} else if (skillResult.readonly === true || skillResult.route === HWLAB_M3_STATUS_API_ROUTE) {
lines.push(`M3 status readbackres_boxsimu_2:DI1=${String(skillResult.readback?.value ?? "unknown")}readiness=${skillResult.readiness?.status ?? "unknown"}。`);
}
lines.push(`Boundary/边界:本次只走 Code Agent -> Skill CLI -> HWLAB API ${skillResult.route};没有直连 gateway/box/patch-panel,也没有使用 OpenAI fallback 冒充 toolCall。这是受控操作结果,不是 M3 DEV-LIVE 验收结论。`);
return boundToolOutput(lines.join("\n"), READONLY_TOOL_OUTPUT_LIMIT).text;
}
function m3IoSkillRunnerDescriptor({ workspace, session = null, skillResult = null } = {}) {
const route = m3SkillRoute(skillResult);
const method = m3SkillMethod(skillResult);
const capabilityLevel = skillResult?.capabilityLevel ?? HWLAB_M3_IO_CAPABILITY_LEVELS.ready;
return {
kind: M3_IO_SKILL_RUNNER_KIND,
provider: M3_IO_SKILL_PROVIDER,
backend: M3_IO_SKILL_BACKEND,
workspace: workspace ?? repoRoot,
sandbox: M3_IO_SKILL_SANDBOX,
session: M3_IO_SKILL_SESSION_MODE,
sessionMode: M3_IO_SKILL_SESSION_MODE,
sessionId: session?.sessionId ?? null,
turn: session?.turn ?? null,
sessionReused: session?.reused ?? false,
implementationType: M3_IO_SKILL_IMPLEMENTATION_TYPE,
codexStdio: false,
longLivedSession: false,
durableSession: false,
writeCapable: true,
readOnly: false,
capabilityLevel,
skill: {
name: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route
},
toolPolicy: {
allowed: [`${method} ${route}`],
blocked: ["gateway-direct-call", "box-simu-direct-call", "patch-panel-direct-call", "generic-hardware-rpc", "OpenAI-hardware-fallback", "M3/M4/M5-acceptance-claim"]
},
runnerLimitations: [...M3_IO_SKILL_LIMITATION_FLAGS],
safety: {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
cloudApiRouteOnly: true,
allowedRoute: route,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
openAiFallbackAllowed: false,
m3m4m5AcceptanceClaimsAllowed: false,
outputLimitBytes: READONLY_TOOL_OUTPUT_LIMIT
}
};
}
function m3IoSkillRunnerTrace({ traceId, events, startedAt, finishedAt = startedAt, outputTruncated, workspace = repoRoot, session = null, skillResult = null }) {
const capabilityLevel = skillResult?.capabilityLevel ?? (
skillResult?.ok ? HWLAB_M3_IO_CAPABILITY_LEVELS.ready : HWLAB_M3_IO_CAPABILITY_LEVELS.blocked
);
const route = m3SkillRoute(skillResult);
const method = m3SkillMethod(skillResult);
return {
traceId,
runnerKind: M3_IO_SKILL_RUNNER_KIND,
workspace,
sandbox: M3_IO_SKILL_SANDBOX,
sessionMode: M3_IO_SKILL_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: M3_IO_SKILL_IMPLEMENTATION_TYPE,
limitations: [...M3_IO_SKILL_LIMITATION_FLAGS],
startedAt,
finishedAt,
events,
route,
method,
skill: HWLAB_M3_IO_SKILL_NAME,
capabilityLevel,
controlReady: skillResult?.controlReady === true,
operationId: skillResult?.operationId ?? null,
auditId: skillResult?.auditId ?? skillResult?.audit?.auditId ?? null,
evidenceId: skillResult?.evidenceId ?? skillResult?.evidence?.evidenceId ?? null,
readback: skillResult?.readback ?? skillResult?.result?.targetReadback ?? null,
accepted: skillResult?.accepted ?? false,
status: skillResult?.status ?? "blocked",
blocker: skillResult?.blocker ?? null,
trustBlocker: skillResult?.trustBlocker ?? null,
blockers: skillResult?.blockers ?? [],
outputTruncated: Boolean(outputTruncated),
valuesPrinted: false,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false,
note: `Controlled M3 IO uses Skill CLI -> HWLAB API ${route}; it is not OpenAI fallback and does not directly call gateway, box, or patch-panel.`
};
}
function structuredCompletionBlocker(result, context = {}) {
if (!result || typeof result !== "object") return null;
if (result.provider === OPENAI_FALLBACK_RUNNER_KIND || result.runner?.kind === OPENAI_FALLBACK_RUNNER_KIND || result.capabilityLevel === "text-chat-only") {
return structuredBlocker({
code: "text_chat_only_fallback",
layer: "provider",
message: "OpenAI Responses fallback is text chat only and cannot satisfy Code Agent runner/session/tool capability.",
userMessage: "当前仍是文本 fallback,只能回答普通问题,不能当作真实 Code Agent runner/session/tool 能力。",
retryable: false,
traceId: context.traceId,
provider: result.provider ?? context.provider,
backend: result.backend ?? context.backend,
runner: result.runner ?? context.runner,
capabilityLevel: result.capabilityLevel ?? context.capabilityLevel,
blockers: result.longLivedSessionGate?.blockers
});
}
const m3SkillTool = Array.isArray(result.toolCalls)
? result.toolCalls.find((toolCall) => toolCall?.name === HWLAB_M3_IO_SKILL_NAME)
: null;
if ((result.provider === M3_IO_SKILL_PROVIDER || m3SkillTool) && result.capabilityLevel === HWLAB_M3_IO_CAPABILITY_LEVELS.blocked) {
const primary = m3SkillTool?.blocker ?? result.skills?.blockers?.[0] ?? result.runnerTrace?.blocker ?? null;
return structuredBlocker({
code: primary?.code === "hwlab_api_unavailable" ? "hwlab_api_unavailable" : "m3_readiness_blocked",
layer: primary?.code === "hwlab_api_unavailable" ? "hwlab-api" : "m3-readiness",
message: primary?.message ?? "M3 IO Skill CLI did not reach a ready HWLAB API control path.",
userMessage: primary?.zh ?? "M3 控制链路仍受阻,前端应显示为能力未就绪,而不是发送失败。",
retryable: primary?.code === "hwlab_api_unavailable",
traceId: context.traceId,
provider: result.provider,
backend: result.backend,
runner: result.runner,
capabilityLevel: result.capabilityLevel,
route: HWLAB_M3_IO_API_ROUTE,
toolName: HWLAB_M3_IO_SKILL_NAME,
blockers: result.skills?.blockers
});
}
return null;
}
function sessionReuseEvidence(session) {
return {
conversationId: session.conversationId,
sessionId: session.sessionId,
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 releaseReadOnlySession(registry, session, { now, traceId, conversationId } = {}) {
return registry.release(session.sessionId, {
now,
traceId,
conversationId,
reused: session.reused,
status: "idle"
}) ?? session;
}
function resolveCodeAgentSessionRegistry(options = {}) {
return options.sessionRegistry &&
typeof options.sessionRegistry.acquire === "function" &&
typeof options.sessionRegistry.release === "function" &&
typeof options.sessionRegistry.describe === "function"
? options.sessionRegistry
: defaultCodeAgentSessionRegistry;
}
function resolveCodexStdioSessionManager(options = {}) {
return options.codexStdioManager &&
typeof options.codexStdioManager.describe === "function" &&
typeof options.codexStdioManager.chat === "function" &&
typeof options.codexStdioManager.cancel === "function" &&
typeof options.codexStdioManager.reapIdle === "function"
? options.codexStdioManager
: defaultCodexStdioSessionManager;
}
function runnerSafetyContract() {
return {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
hardwareWritesAllowed: false,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
m3IoSkillCliAllowedRoute: HWLAB_M3_IO_API_ROUTE,
m3StatusSkillCliAllowedRoute: HWLAB_M3_STATUS_API_ROUTE,
openAiHardwareFallbackAllowed: false,
m3m4m5AcceptanceClaimsAllowed: false,
outputLimitBytes: READONLY_TOOL_OUTPUT_LIMIT
};
}
async function inspectCodexStdioFeasibility(env = process.env, options = {}) {
const manager = resolveCodexStdioSessionManager(options);
const descriptor = {
env,
workspace: options.workspace,
command: options.command,
sandbox: options.sandbox
};
const feasibility = typeof manager.probe === "function"
? await manager.probe(descriptor)
: manager.describe(descriptor);
return {
...feasibility,
checked: true,
implementationRequired: "codex-stdio-or-equivalent-long-lived-runner",
currentImplementation: feasibility.ready ? CODEX_STDIO_IMPLEMENTATION_TYPE : READONLY_IMPLEMENTATION_TYPE,
sourceIssue: CODEX_STDIO_SOURCE_ISSUE
};
}
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();
return codexLongLivedSessionGate({
provider: normalizedProvider,
runnerKind: normalizedRunnerKind,
session,
sessionMode: normalizedSessionMode,
implementationType: normalizedImplementation,
codexStdioFeasibility
});
}
function codexStdioBlockedRunnerDescriptor({ workspace, session = null } = {}) {
return {
kind: CODEX_STDIO_RUNNER_KIND,
provider: CODEX_STDIO_PROVIDER,
backend: CODEX_STDIO_BACKEND,
workspace: workspace ?? repoRoot,
sandbox: "workspace-write",
session: CODEX_STDIO_SESSION_MODE,
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionId: session?.sessionId ?? null,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdio: true,
longLivedSession: false,
durableSession: false,
writeCapable: false,
readOnly: false,
capabilityLevel: "blocked",
toolPolicy: {
allowed: ["codex", "codex-reply"],
blocked: ["controlled-readonly-session-registry", "skills.discover-local-fallback", "pwd-local-fallback", "ls-local-fallback", "rg-local-fallback", "cat-local-fallback", "openai-text-fallback"]
},
runnerLimitations: ["codex-stdio-required", "no-controlled-readonly-fallback", "no-text-fallback"],
safety: runnerSafetyContract()
};
}
function runnerError(code, message, details = {}) {
const error = new Error(message);
error.code = code;
Object.assign(error, {
provider: READONLY_RUNNER_PROVIDER,
model: READONLY_RUNNER_MODEL,
backend: READONLY_RUNNER_BACKEND,
sandbox: READONLY_RUNNER_SANDBOX,
...details
});
return error;
}
function boundToolOutput(value, maxLength = READONLY_TOOL_OUTPUT_LIMIT) {
const text = String(value ?? "");
if (text.length <= maxLength) return { text, truncated: false };
return {
text: `${text.slice(0, maxLength)}\n[output truncated at ${maxLength} bytes]`,
truncated: true
};
}
function normalizeUserMessage(value) {
if (typeof value !== "string") {
throw badRequest("message must be a string");
}
const message = value.trim();
if (!message) {
throw badRequest("message is required");
}
if (message.length > 4000) {
throw badRequest("message exceeds 4000 characters");
}
return message;
}
function chatStatusForError(error) {
const code = String(error?.code ?? "");
if (code === "codex_stdio_canceled" || code === "session_canceled") return "canceled";
if (code === "codex_stdio_timeout" || code === "provider_timeout" || code === "session_timeout") return "timeout";
if (code === "session_error") return "error";
if (/timeout|timed out|超时/iu.test(String(error?.message ?? ""))) return "timeout";
return "failed";
}
function conversationFactsForPrompt(sessionRegistry, conversationId) {
if (!sessionRegistry || typeof sessionRegistry.getConversationFacts !== "function") return null;
try {
return sessionRegistry.getConversationFacts(conversationId);
} catch {
return null;
}
}
export function createCodeAgentErrorPayload({
code = "code_agent_failed",
message = "Code Agent request failed",
reason,
traceId = "trc_unassigned",
conversationId = "cnv_unassigned",
sessionId = "cnv_unassigned",
messageId = "msg_unassigned",
provider = "unassigned",
model = "unknown",
backend = "hwlab-cloud-api/code-agent-chat",
layer,
retryable,
capabilityLevel = "blocked",
route = "/v1/agent/chat",
toolName = null,
now
} = {}) {
const timestamp = nowIso(now);
const error = new Error(message);
Object.assign(error, { code, reason, layer, retryable, provider, model, backend, capabilityLevel, route, toolName });
const normalized = normalizeChatError(error, { traceId, provider, model, backend, capabilityLevel, route, toolName });
return {
conversationId,
sessionId,
messageId,
status: "failed",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider,
model,
backend,
capabilityLevel,
error: normalized,
blocker: normalized.blocker,
blockers: normalized.blockers
};
}
function normalizeChatError(error, context = {}) {
const code = String(error.code || "code_agent_failed");
const taxonomy = errorTaxonomy(code, error);
const provider = error.provider ?? context.provider ?? READONLY_RUNNER_PROVIDER;
const model = error.model ?? context.model ?? READONLY_RUNNER_MODEL;
const backend = error.backend ?? context.backend ?? READONLY_RUNNER_BACKEND;
const runnerKind = error.runner?.kind ?? error.runnerKind ?? context.runner?.kind ?? null;
const traceId = error.traceId ?? context.traceId ?? null;
const capabilityLevel = error.capabilityLevel ?? context.capabilityLevel ?? "blocked";
const route = error.route ?? context.route ?? routeForError(code, error);
const toolName = error.toolName ?? context.toolName ?? toolNameForError(code, error);
const message = redactText(error.message || taxonomy.message || "Code Agent request failed");
const blockers = normalizeBlockers(error.blockers, {
fallbackCode: code,
layer: taxonomy.layer,
message,
userMessage: taxonomy.userMessage,
retryable: taxonomy.retryable
});
const blocker = structuredBlocker({
code,
layer: taxonomy.layer,
message,
userMessage: error.userMessage ?? taxonomy.userMessage,
retryable: error.retryable ?? taxonomy.retryable,
traceId,
provider,
backend,
runner: error.runner ?? context.runner,
capabilityLevel,
route,
toolName,
category: taxonomy.category,
blockers
});
const normalized = {
code,
layer: taxonomy.layer,
category: taxonomy.category,
blocker,
retryable: blocker.retryable,
userMessage: blocker.userMessage,
message,
traceId,
provider,
model,
backend,
runner: runnerKind,
runnerKind,
capabilityLevel,
route,
toolName,
missingConfig: safeMissingConfig(error),
blockers
};
for (const key of [
"missingEnv",
"missingCommands",
"command",
"exitCode",
"providerStatus",
"stderrSummary",
"missingTools",
"nextEvidence",
"reason"
]) {
if (error[key] !== undefined) {
normalized[key] = sanitizeErrorField(key, error[key]);
}
}
return normalized;
}
function errorTaxonomy(code, error = {}) {
const catalog = {
invalid_params: {
layer: "api",
category: "api_error",
retryable: true,
userMessage: "请求参数不完整或格式不正确,请修正后重试。"
},
parse_error: {
layer: "api",
category: "api_error",
retryable: true,
userMessage: "请求 JSON 无法解析,请修正后重试。"
},
provider_unavailable: {
layer: error.missingEnv?.length || error.missingCommands?.length ? "provider-config" : "provider",
category: error.missingEnv?.length || error.missingCommands?.length ? "needs_config" : "provider",
retryable: !(error.missingEnv?.length || error.missingCommands?.length),
userMessage: error.missingEnv?.length || error.missingCommands?.length
? "Code Agent provider 配置缺失,需要维护者补齐后才能使用。"
: "Code Agent provider 暂不可用,可稍后重试。"
},
provider_timeout: {
layer: "provider",
category: "timeout",
retryable: true,
userMessage: "Code Agent provider 响应超时,输入已保留,可稍后重试。"
},
codex_cli_binary_missing: {
layer: "runner",
category: "needs_config",
retryable: false,
userMessage: "Codex CLI binary 未在运行时 PATH 中找到;当前不会安装临时二进制,也不会冒充真实 Codex。"
},
session_busy: {
layer: "session",
category: "runner_busy",
retryable: true,
userMessage: "Code Agent session 正在处理上一轮请求,请稍后重试。"
},
session_expired: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 已过期,可重新发送建立新的 session。"
},
session_reuse_conflict: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "当前 conversation 与 session 绑定不一致,可新建会话后重试。"
},
session_failed: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 已失败,可重新发送建立新的 session。"
},
session_interrupted: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 已中断,可重新发送建立新的 session。"
},
session_canceled: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 已取消,输入和 trace 已保留,可重试上一条消息。"
},
session_timeout: {
layer: "session",
category: "timeout",
retryable: true,
userMessage: "Code Agent session 已超时,输入和 trace 已保留,可重试上一条消息。"
},
session_error: {
layer: "session",
category: "session_blocked",
retryable: true,
userMessage: "Code Agent session 返回错误,输入和 trace 已保留,可重试上一条消息。"
},
runner_unavailable: {
layer: "runner",
category: "runner_blocked",
retryable: true,
userMessage: "Code Agent runner 暂不可用,可稍后重试或等待工作区挂载恢复。"
},
tool_unavailable: {
layer: "tool",
category: "capability_unavailable",
retryable: false,
userMessage: "当前 Code Agent 未开放该工具能力。"
},
skills_unavailable: {
layer: "skill-cli",
category: "needs_config",
retryable: false,
userMessage: "Code Agent Skill 清单未挂载或不可读,需要补齐运行时配置。"
},
security_blocked: {
layer: "security",
category: "security_blocked",
retryable: false,
userMessage: "该请求被安全边界阻断,不能读取敏感信息或绕过 HWLAB API。"
},
skill_cli_api_base_missing: {
layer: "skill-cli-config",
category: "needs_config",
retryable: false,
userMessage: "M3 Skill CLI 缺少 HWLAB API base URL 配置,需要补齐安全 env 或 contract。"
},
hwlab_api_unavailable: {
layer: "hwlab-api",
category: "retryable",
retryable: true,
userMessage: "HWLAB API 当前不可达,M3 控制未执行,可稍后重试。"
},
m3_readiness_blocked: {
layer: "m3-readiness",
category: "capability_unavailable",
retryable: false,
userMessage: "M3 控制链路尚未就绪,不能把本次结果标记为真实可控。"
},
text_chat_only_fallback: {
layer: "provider",
category: "fallback",
retryable: false,
userMessage: "当前仍是文本 fallback,不具备 runner/session/tool/HWLAB API 控制能力。"
},
codex_stdio_blocked: {
layer: "runner",
category: "capability_unavailable",
retryable: false,
userMessage: "Codex stdio 长会话能力仍受阻,不能作为完整 Code Agent。"
},
codex_stdio_protocol_blocked: {
layer: "runner",
category: "capability_unavailable",
retryable: false,
userMessage: "Codex stdio 协议工具不完整,不能作为完整 Code Agent。"
},
codex_stdio_empty_response: {
layer: "runner",
category: "runner_blocked",
retryable: true,
userMessage: "Codex stdio 未返回有效回复,可稍后重试。"
},
codex_stdio_timeout: {
layer: "runner",
category: "timeout",
retryable: true,
userMessage: "Codex stdio runner 响应超时,输入、sessionId 和 traceId 已保留,可重试。"
},
codex_stdio_canceled: {
layer: "runner",
category: "canceled",
retryable: true,
userMessage: "本次 Codex stdio 请求已取消;输入、sessionId 和 traceId 已保留,可重试上一条消息。"
},
codex_stdio_command_probe_failed: {
layer: "runner",
category: "runner_blocked",
retryable: true,
userMessage: "Codex stdio 基础命令探针失败,readiness 不会标为完整 ready;输入已保留,可稍后重试。"
},
codex_stdio_failed: {
layer: "runner",
category: "runner_blocked",
retryable: true,
userMessage: "Codex stdio runner 执行失败,可稍后重试。"
}
};
return catalog[code] ?? {
layer: "api",
category: "api_error",
retryable: true,
userMessage: "Code Agent 请求失败,输入已保留,可稍后重试。"
};
}
function structuredBlocker({ code, layer, message, userMessage, retryable, traceId, provider, backend, runner, capabilityLevel, route, toolName, category, blockers }) {
return {
code,
layer,
category: category ?? layer,
retryable: Boolean(retryable),
summary: redactText(message),
userMessage,
traceId: traceId ?? null,
provider: provider ?? null,
backend: backend ?? null,
runner: runner?.kind ?? runner ?? null,
capabilityLevel: capabilityLevel ?? null,
route: route ?? null,
toolName: toolName ?? null,
blockerCodes: Array.isArray(blockers) ? blockers.map((blocker) => blocker.code).filter(Boolean) : []
};
}
function normalizeBlockers(blockers, fallback) {
const items = Array.isArray(blockers) ? blockers : [];
const normalized = items
.filter((blocker) => blocker && typeof blocker === "object")
.map((blocker) => ({
code: String(blocker.code ?? fallback.fallbackCode),
layer: blocker.layer ?? fallback.layer,
category: blocker.category ?? fallback.layer,
retryable: Boolean(blocker.retryable ?? fallback.retryable),
summary: redactText(blocker.summary ?? blocker.message ?? fallback.message),
userMessage: blocker.userMessage ?? blocker.zh ?? fallback.userMessage,
sourceIssue: blocker.sourceIssue,
route: blocker.route,
toolName: blocker.toolName
}));
if (normalized.length > 0) return normalized;
return [{
code: fallback.fallbackCode,
layer: fallback.layer,
category: fallback.layer,
retryable: Boolean(fallback.retryable),
summary: redactText(fallback.message),
userMessage: fallback.userMessage
}];
}
function safeMissingConfig(error) {
return [
...(Array.isArray(error.missingEnv) ? error.missingEnv : []),
...(Array.isArray(error.missingCommands) ? error.missingCommands.map((command) => `command:${command}`) : []),
...(Array.isArray(error.missingTools) ? error.missingTools.map((tool) => `tool:${tool}`) : [])
].filter((item) => typeof item === "string" && /^[A-Za-z0-9_./:-]+$/u.test(item));
}
function sanitizeErrorField(key, value) {
if (key === "missingEnv" || key === "missingCommands" || key === "missingTools") {
return Array.isArray(value) ? value.filter((item) => typeof item === "string").map((item) => redactText(item)) : [];
}
if (key === "providerStatus" || key === "exitCode") return value;
return redactText(value);
}
function routeForError(code, error = {}) {
if (error.route !== undefined) return error.route;
if (["skill_cli_api_base_missing", "hwlab_api_unavailable", "m3_readiness_blocked"].includes(code)) return HWLAB_M3_IO_API_ROUTE;
return null;
}
function toolNameForError(code, error = {}) {
if (error.toolName !== undefined) return error.toolName;
if (["skill_cli_api_base_missing", "hwlab_api_unavailable", "m3_readiness_blocked"].includes(code)) return HWLAB_M3_IO_SKILL_NAME;
return null;
}
function badRequest(message) {
const error = new Error(message);
error.code = "invalid_params";
return error;
}
function cleanProtocolId(value, prefix) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (!trimmed) return null;
if (/^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$/u.test(trimmed)) return trimmed;
return `${prefix}_${trimmed.replace(/[^A-Za-z0-9._:-]/gu, "-").slice(0, 80)}`;
}
function cleanProjectId(value) {
return cleanProtocolId(value, "prj");
}
function resolveConversationSessionIds(params = {}) {
const requestedConversationId = cleanProtocolId(params.conversationId, "cnv");
const requestedSessionId = cleanProtocolId(params.sessionId, "ses");
if (requestedConversationId && requestedSessionId) {
return {
conversationId: requestedConversationId,
sessionId: requestedSessionId,
requestedSessionId
};
}
if (requestedConversationId) {
return {
conversationId: requestedConversationId,
sessionId: requestedConversationId,
requestedSessionId: null
};
}
if (requestedSessionId) {
const conversationId = `cnv_${requestedSessionId.replace(/^[a-z][a-z0-9]*_/u, "")}`;
return {
conversationId,
sessionId: requestedSessionId,
requestedSessionId
};
}
const conversationId = `cnv_${randomUUID()}`;
return {
conversationId,
sessionId: conversationId,
requestedSessionId: null
};
}
function nowIso(now) {
return typeof now === "function" ? now() : new Date().toISOString();
}
function firstNonEmpty(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value.trim();
}
return "";
}
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***");
}
function redactUrl(value) {
try {
const url = new URL(value);
url.username = "";
url.password = "";
return url.toString();
} catch {
return redactText(String(value ?? "")).replace(/\/\/[^/@]+@/u, "//***@");
}
}