feat: route code agent m3 io through skill cli

This commit is contained in:
Code Queue Review
2026-05-23 21:58:17 +00:00
parent 88f4144cab
commit ee30594523
5 changed files with 833 additions and 138 deletions
+342 -125
View File
@@ -30,6 +30,7 @@ import {
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,
@@ -59,7 +60,6 @@ const M3_IO_SKILL_LIMITATION_FLAGS = Object.freeze([
"m3-io-only",
"hwlab-api-route-only",
"not-generic-hardware-control",
"not-codex-stdio",
"not-durable-session"
]);
const OPENAI_FALLBACK_RUNNER_KIND = "openai-responses-fallback";
@@ -128,21 +128,6 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
try {
const message = normalizeUserMessage(params.message);
const runnerIntent = detectReadOnlyRunnerIntent(message);
if (runnerIntent.kind === "m3_io") {
const runnerResult = await callM3IoSkillRunner({
intent: runnerIntent,
conversationId,
traceId,
sessionId: requestedSessionId,
env: options.env ?? process.env,
now: options.now,
workspace: options.workspace,
sessionRegistry: options.sessionRegistry,
requestJson: options.m3IoSkillRequestJson,
codexStdioManager: options.codexStdioManager
});
return completedRunnerPayload({ base, runnerResult, messageId, now: options.now });
}
const securityIntent = runnerIntent.kind === "security" ? runnerIntent : null;
if (securityIntent) {
@@ -168,19 +153,50 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
providerPlan.mode === "codex-stdio" ||
(providerPlan.mode !== "openai" && codexStdioAvailability.canStartLongLivedCodexStdio === true);
if (shouldUseCodexStdio) {
const stdioResult = await callCodexStdioRunner({
message,
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,
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
});
return completedRunnerPayload({ base, runnerResult: stdioResult, messageId, now: options.now });
}
if (runnerIntent.kind === "m3_io") {
const runnerResult = await callM3IoSkillRunner({
intent: runnerIntent,
conversationId,
sessionId: requestedSessionId,
traceId,
sessionId: requestedSessionId,
env: options.env ?? process.env,
now: options.now,
workspace: options.workspace,
timeoutMs: options.timeoutMs,
model: providerPlan.model,
sessionRegistry: options.sessionRegistry,
requestJson: options.m3IoSkillRequestJson,
codexStdioManager: options.codexStdioManager
});
return completedRunnerPayload({ base, runnerResult: stdioResult, messageId, now: options.now });
return completedRunnerPayload({ base, runnerResult, messageId, now: options.now });
}
if (runnerIntent.kind !== "none") {
@@ -315,14 +331,14 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
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 (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.runnerTrace !== undefined) payload.runnerTrace = error.runnerTrace;
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.availability !== undefined) {
payload.availability = error.availability;
} else if (["provider_unavailable", "provider_timeout", "codex_cli_binary_missing"].includes(error.code)) {
@@ -422,23 +438,31 @@ function validateCodeAgentToolCallContract(payload) {
}
if (!isM3IoToolCall(toolCall, payload)) continue;
const structuredBlocked = toolCall.status === "blocked" && Boolean(toolCall.blocker?.code || toolCall.capabilityBlocker?.code || toolCall.error?.code);
if (toolCall.route !== HWLAB_M3_IO_API_ROUTE && !structuredBlocked) {
throw new Error(`code agent M3 toolCalls[${index}] must show route ${HWLAB_M3_IO_API_ROUTE} or structured blocked`);
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);
@@ -571,6 +595,7 @@ export async function describeCodeAgentAvailability(env = process.env, options =
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,
@@ -739,6 +764,7 @@ async function inspectReadOnlyRunnerAvailability(env, options = {}) {
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,
@@ -751,6 +777,7 @@ async function inspectReadOnlyRunnerAvailability(env, options = {}) {
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,
@@ -1180,7 +1207,7 @@ async function callM3IoSkillRunner({ intent, conversationId, sessionId, traceId,
const registry = resolveCodeAgentSessionRegistry({ sessionRegistry });
const m3ApiBaseUrl = configuredCloudApiBaseUrl(env);
const sessionCapabilityLevel = m3ApiBaseUrl
? HWLAB_M3_IO_CAPABILITY_LEVELS.ready
? m3SessionCapabilityLevelForIntent(intent)
: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked;
const sessionAcquire = registry.acquire({
conversationId,
@@ -1238,12 +1265,14 @@ async function callM3IoSkillRunner({ intent, conversationId, sessionId, traceId,
let session = sessionAcquire.session;
if (!m3ApiBaseUrl) {
const blocker = m3IoApiBaseUrlMissingBlocker();
const route = intent.action === "status" ? HWLAB_M3_STATUS_API_ROUTE : HWLAB_M3_IO_API_ROUTE;
const blocker = m3IoApiBaseUrlMissingBlocker({ route });
const skillResult = m3IoSkillMissingApiBaseUrlResult({
traceId,
requestId: `req_${randomUUID()}`,
actorId: "usr_code_agent",
blocker,
route,
startedAt,
finishedAt: nowIso(now)
});
@@ -1281,6 +1310,47 @@ async function callM3IoSkillRunner({ intent, conversationId, sessionId, traceId,
});
}
async function callCodexStdioWithM3IoSkillRunner({
message,
intent,
conversationId,
sessionId,
traceId,
env,
now,
workspace,
timeoutMs,
model,
codexStdioManager,
requestJson
}) {
const stdioResult = await callCodexStdioRunner({
message,
conversationId,
sessionId,
traceId,
env,
now,
workspace,
timeoutMs,
model,
codexStdioManager
});
const commandArgs = m3IoSkillArgsForIntent(intent, { env, traceId });
const skillResult = await runM3IoSkillCommand(commandArgs, {
env,
now,
requestJson
});
return codexStdioM3IoSkillRunnerResult({
stdioResult,
skillResult,
commandArgs,
traceId,
now
});
}
function m3IoSkillRunnerResult({
skillResult,
commandArgs,
@@ -1297,61 +1367,19 @@ function m3IoSkillRunnerResult({
const capabilityLevel = skillResult.capabilityLevel ?? (
skillResult.ok ? HWLAB_M3_IO_CAPABILITY_LEVELS.ready : HWLAB_M3_IO_CAPABILITY_LEVELS.blocked
);
const toolCall = {
id: `tool_${randomUUID()}`,
type: "skill-cli",
name: HWLAB_M3_IO_SKILL_NAME,
status: skillResult.status === "succeeded" || skillResult.accepted === true ? "completed" : "blocked",
const route = m3SkillRoute(skillResult);
const method = m3SkillMethod(skillResult);
const toolCall = m3IoSkillToolCall({
skillResult,
commandArgs,
cwd: resolvedWorkspace,
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: skillResult.method ?? "POST",
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: HWLAB_M3_IO_API_ROUTE,
method: skillResult.method ?? "POST",
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
};
route,
method
});
session = releaseReadOnlySession(registry, session, { now, traceId, conversationId });
const runner = m3IoSkillRunnerDescriptor({ workspace: resolvedWorkspace, session });
const runner = m3IoSkillRunnerDescriptor({ workspace: resolvedWorkspace, session, skillResult });
const runnerTracePayload = m3IoSkillRunnerTrace({
traceId,
workspace: resolvedWorkspace,
@@ -1359,7 +1387,7 @@ function m3IoSkillRunnerResult({
events: [
"intent:m3_io",
"tool:skill-cli:started",
`route:${HWLAB_M3_IO_API_ROUTE}`,
`route:${route}`,
`tool:${toolCall.status}`
],
startedAt,
@@ -1392,13 +1420,7 @@ function m3IoSkillRunnerResult({
toolCalls: [toolCall],
skills: {
status: "used",
items: [{
name: HWLAB_M3_IO_SKILL_NAME,
summary: `Controlled M3 DO1/DI1 adapter for HWLAB API ${HWLAB_M3_IO_API_ROUTE}.`,
route: HWLAB_M3_IO_API_ROUTE,
capabilityLevel,
version: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION
}],
items: [m3IoSkillItem({ capabilityLevel, route })],
count: 1,
blockers: skillResult.blockers ?? (skillResult.blocker ? [skillResult.blocker] : [])
},
@@ -1406,24 +1428,186 @@ function m3IoSkillRunnerResult({
runnerTrace: runnerTracePayload,
capabilityLevel,
blockers: skillResult.blockers ?? (skillResult.blocker ? [skillResult.blocker] : []),
route: HWLAB_M3_IO_API_ROUTE,
route,
toolName: HWLAB_M3_IO_SKILL_NAME,
providerTrace: m3IoSkillProviderTrace({ skillResult, route, method, capabilityLevel })
};
}
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: {
runnerKind: M3_IO_SKILL_RUNNER_KIND,
skill: HWLAB_M3_IO_SKILL_NAME,
route: HWLAB_M3_IO_API_ROUTE,
method: skillResult.method ?? "POST",
...(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,
capabilityLevel,
controlReady: skillResult.controlReady === true,
accepted: skillResult.accepted,
status: skillResult.status,
fallbackUsed: false
}
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
};
}
@@ -1451,15 +1635,29 @@ function m3IoToolCallApiTarget(hwlabApi = {}) {
return target;
}
function m3IoSkillMissingApiBaseUrlResult({ traceId, requestId, actorId, blocker, startedAt, finishedAt }) {
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: HWLAB_M3_IO_API_ROUTE,
method: "POST",
route,
method,
hwlabApi: {
route: HWLAB_M3_IO_API_ROUTE,
route,
redactedUrl: null,
source: "missing-config",
baseUrlConfigured: false,
@@ -1550,7 +1748,7 @@ function m3IoSkillMissingApiBaseUrlResult({ traceId, requestId, actorId, blocker
userMessage: blocker.userMessage,
message: blocker.message,
traceId,
route: HWLAB_M3_IO_API_ROUTE,
route,
toolName: HWLAB_M3_IO_SKILL_NAME,
missingConfig: blocker.missingConfig
},
@@ -1611,7 +1809,7 @@ function detectReadOnlyRunnerIntent(message) {
function detectM3IoIntent(text) {
const normalized = String(text ?? "");
if (!/\bM3\b|DO1|DI1|数字输出|数字输入|回读|读回/u.test(normalized)) {
if (!/\bM3\b|DO1|DI1|数字输出|数字输入|回读|读回|状态|status/u.test(normalized)) {
return null;
}
if (/(?:直接|direct|gateway-simu|box-simu|patch-panel|hwlab-patch-panel|\/invoke|\/sync\/tick)/iu.test(normalized)) {
@@ -1623,6 +1821,14 @@ function detectM3IoIntent(text) {
const wantsRead = /(?:DI1|数字输入).{0,16}(?:read|readback|读取|回读|读回|查询)|(?:read|readback|读取|回读|读回|查询).{0,16}(?:DI1|数字输入)/iu.test(normalized);
const wantsWrite = /(?:DO1|数字输出).{0,20}(?:write|set|置|写|打开|关闭|true|false|高|低)|(?:write|set|置|写|打开|关闭).{0,20}(?:DO1|数字输出)/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;
}
@@ -2225,14 +2431,17 @@ function readOnlyRunnerResult({ content, workspace, toolCalls, skills, runner, r
function m3IoSkillArgsForIntent(intent, { env, traceId }) {
const args = [
"m3",
"io",
"--action",
intent.action,
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);
@@ -2260,7 +2469,7 @@ function m3IoSkillApiBaseUrlSource(env = process.env) {
return "missing-config";
}
function m3IoApiBaseUrlMissingBlocker() {
function m3IoApiBaseUrlMissingBlocker({ route = HWLAB_M3_IO_API_ROUTE } = {}) {
return {
code: "skill_cli_api_base_missing",
layer: "skill-cli-config",
@@ -2272,7 +2481,7 @@ function m3IoApiBaseUrlMissingBlocker() {
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: HWLAB_M3_IO_API_ROUTE,
route,
toolName: HWLAB_M3_IO_SKILL_NAME,
missingConfig: [
...HWLAB_M3_IO_API_BASE_URL_ENVS,
@@ -2298,12 +2507,17 @@ function m3IoSkillReply(skillResult) {
lines.push(`DO1 requested value=${String(skillResult.command?.value)}; DI1 readback=${String(skillResult.result?.targetReadback?.value ?? "unknown")}.`);
} else if (skillResult.action === "di.read") {
lines.push(`DI1 value=${String(skillResult.result?.value ?? "unknown")}.`);
} else if (skillResult.readonly === true || skillResult.route === HWLAB_M3_STATUS_API_ROUTE) {
lines.push(`M3 status readback DI1=${String(skillResult.readback?.value ?? "unknown")}; readiness=${skillResult.readiness?.status ?? "unknown"}.`);
}
lines.push("Boundary: Code Agent -> Skill CLI -> HWLAB API /v1/m3/io; no direct gateway/box/patch-panel call; no OpenAI fallback used. This is source/artifact control capability, not DEV-LIVE verification.");
lines.push(`Boundary: Code Agent -> Skill CLI -> HWLAB API ${skillResult.route}; no direct gateway/box/patch-panel call; no OpenAI fallback used. This is source/artifact control capability, not DEV-LIVE verification.`);
return boundToolOutput(lines.join("\n"), READONLY_TOOL_OUTPUT_LIMIT).text;
}
function m3IoSkillRunnerDescriptor({ workspace, session = null } = {}) {
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,
@@ -2321,14 +2535,14 @@ function m3IoSkillRunnerDescriptor({ workspace, session = null } = {}) {
durableSession: false,
writeCapable: true,
readOnly: false,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.ready,
capabilityLevel,
skill: {
name: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route: HWLAB_M3_IO_API_ROUTE
route
},
toolPolicy: {
allowed: [`POST ${HWLAB_M3_IO_API_ROUTE}`],
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],
@@ -2337,7 +2551,7 @@ function m3IoSkillRunnerDescriptor({ workspace, session = null } = {}) {
secretValuesPrinted: false,
kubeconfigRead: false,
cloudApiRouteOnly: true,
allowedRoute: HWLAB_M3_IO_API_ROUTE,
allowedRoute: route,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
@@ -2352,6 +2566,8 @@ function m3IoSkillRunnerTrace({ traceId, events, startedAt, finishedAt = started
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,
@@ -2369,8 +2585,8 @@ function m3IoSkillRunnerTrace({ traceId, events, startedAt, finishedAt = started
startedAt,
finishedAt,
events,
route: HWLAB_M3_IO_API_ROUTE,
method: skillResult?.method ?? "POST",
route,
method,
skill: HWLAB_M3_IO_SKILL_NAME,
capabilityLevel,
controlReady: skillResult?.controlReady === true,
@@ -2389,7 +2605,7 @@ function m3IoSkillRunnerTrace({ traceId, events, startedAt, finishedAt = started
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false,
note: `Controlled M3 IO uses Skill CLI -> HWLAB API ${HWLAB_M3_IO_API_ROUTE}; it is not OpenAI fallback and does not directly call gateway, box, or patch-panel.`
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.`
};
}
@@ -2553,6 +2769,7 @@ function runnerSafetyContract() {
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
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { existsSync } from "node:fs";
import { mkdtemp, rm } from "node:fs/promises";
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
@@ -15,7 +15,8 @@ import {
import {
HWLAB_M3_IO_API_BASE_URL_ENV,
HWLAB_M3_IO_CAPABILITY_LEVELS,
HWLAB_M3_IO_API_ROUTE
HWLAB_M3_IO_API_ROUTE,
HWLAB_M3_STATUS_API_ROUTE
} from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
test("code agent session registry creates, reuses, and expires sessions without storing secrets", () => {
@@ -911,6 +912,232 @@ test("Code Agent M3 DO false write exposes readback and identifier contract", as
assert.equal(calls[0].url.includes("patch-panel"), false);
});
test("Code Agent M3 DO write uses Codex stdio session and appends Skill CLI toolCall", async () => {
const calls = [];
const fakeCodex = await createFakeCodexCommand();
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_stdio_m3_skill",
createRpcClient: async () => ({
async initialize() {
return { tools: ["codex", "codex-reply"] };
},
async listTools() {
return ["codex", "codex-reply"];
},
async callTool(name, args) {
calls.push({ kind: "codex", name, args });
return {
structuredContent: {
threadId: "thread_stdio_m3_skill",
content: "准备通过受控 Skill CLI 执行 M3 IO。"
}
};
},
close() {}
})
});
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_stdio_m3_skill",
traceId: "trc_stdio_m3_skill",
message: "通过 HWLAB API 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1。"
},
{
now: () => "2026-05-23T00:08:40.000Z",
codexStdioManager: manager,
env: {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
CODEX_HOME: process.cwd(),
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667"
},
callProvider: async () => {
throw new Error("OpenAI fallback must not be used for M3 IO control");
},
m3IoSkillRequestJson: async (url, request) => {
calls.push({ kind: "skill", url, request });
assert.equal(new URL(url).pathname, HWLAB_M3_IO_API_ROUTE);
assert.equal(request.method, "POST");
assert.equal(request.body.action, "do.write");
assert.equal(request.body.resourceId, "res_boxsimu_1");
assert.equal(request.body.port, "DO1");
assert.equal(request.body.value, true);
assert.equal(request.body.source, "hwlab-agent-runtime.m3-io");
return {
ok: true,
status: 200,
body: {
serviceId: "hwlab-cloud-api",
contractVersion: "m3-io-control-v1",
status: "succeeded",
accepted: true,
action: "do.write",
traceId: "trc_stdio_m3_skill",
operationId: "op_m3_do_write_stdio_skill",
auditId: "aud_m3_do_write_stdio_skill_succeeded",
evidenceId: "evd_m3_do_write_stdio_skill_succeeded",
auditState: {
status: "written_non_durable"
},
evidenceState: {
status: "blocked",
sourceKind: "BLOCKED",
blocker: "runtime_durable_not_green",
writeStatus: "written_non_durable"
},
durableStatus: {
status: "degraded",
durable: false,
blocker: "runtime_durable_not_green"
},
result: {
value: true,
targetReadback: {
status: "succeeded",
value: true,
resourceId: "res_boxsimu_2",
port: "DI1"
}
},
controlPath: {
status: "succeeded",
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: true,
frontendBypass: false
}
}
};
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.runner.kind, "codex-mcp-stdio-runner");
assert.equal(payload.sessionMode, "codex-mcp-stdio-long-lived");
assert.equal(payload.implementationType, "repo-owned-codex-mcp-stdio-session");
assert.equal(payload.session.longLivedSession, true);
assert.equal(payload.session.codexStdio, true);
assert.equal(payload.longLivedSessionGate.status, "pass");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
assert.equal(payload.toolCalls.length, 2);
assert.equal(payload.toolCalls[0].type, "codex-stdio");
const skillTool = payload.toolCalls[1];
assert.equal(skillTool.type, "skill-cli");
assert.equal(skillTool.name, "hwlab-agent-runtime.m3-io");
assert.equal(skillTool.status, "completed");
assert.equal(skillTool.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(skillTool.method, "POST");
assert.equal(skillTool.accepted, true);
assert.equal(skillTool.operationId, "op_m3_do_write_stdio_skill");
assert.equal(skillTool.auditId, "aud_m3_do_write_stdio_skill_succeeded");
assert.equal(skillTool.evidenceId, "evd_m3_do_write_stdio_skill_succeeded");
assert.equal(skillTool.readback.value, true);
assert.equal(payload.runnerTrace.runnerKind, "codex-mcp-stdio-runner");
assert.equal(payload.runnerTrace.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(payload.runnerTrace.method, "POST");
assert.equal(payload.providerTrace.codexStdio, true);
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(payload.providerTrace.readback.value, true);
assert.ok(payload.runner.toolPolicy.allowed.includes(`POST ${HWLAB_M3_IO_API_ROUTE}`));
assert.equal(JSON.stringify(payload.toolCalls).includes("gateway-simu"), false);
assert.equal(JSON.stringify(payload.toolCalls).includes("box-simu"), false);
assert.equal(JSON.stringify(payload.toolCalls).includes("patch-panel"), false);
assert.equal(calls.filter((call) => call.kind === "codex").length, 1);
assert.equal(calls.filter((call) => call.kind === "skill").length, 1);
await rm(fakeCodex.root, { recursive: true, force: true });
});
test("Code Agent M3 status uses Skill CLI GET /v1/m3/status and keeps route evidence", async () => {
const calls = [];
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_m3_status_skill",
traceId: "trc_m3_status_skill",
message: "读取 M3 status 聚合状态"
},
{
now: () => "2026-05-23T00:09:10.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
OPENAI_API_KEY: "must-not-be-used"
},
callProvider: async () => {
throw new Error("OpenAI fallback must not be used for M3 status");
},
m3IoSkillRequestJson: async (url, request) => {
calls.push({ url, request });
assert.equal(new URL(url).pathname, HWLAB_M3_STATUS_API_ROUTE);
assert.equal(request.method, "GET");
return {
ok: true,
status: 200,
body: {
status: "live",
sourceKind: "DEV-LIVE",
traceId: "trc_m3_status_skill",
boxes: [{
id: "boxsimu_2",
resourceId: "res_boxsimu_2",
online: true,
ports: {
DI1: {
value: false
}
}
}],
patchPanel: {
serviceId: "hwlab-patch-panel",
observable: true,
connectionActive: true
},
trust: {
operationId: "op_m3_status_skill",
traceId: "trc_m3_status_skill",
auditId: "aud_m3_status_skill",
evidenceId: "evd_m3_status_skill",
durableStatus: "green",
blocker: null,
readStatus: {
audit: "read",
evidence: "read"
},
runtime: {
durable: true
}
}
}
};
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-skill-cli");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.readonly);
assert.equal(payload.toolCalls[0].route, HWLAB_M3_STATUS_API_ROUTE);
assert.equal(payload.toolCalls[0].method, "GET");
assert.equal(payload.toolCalls[0].accepted, true);
assert.equal(payload.toolCalls[0].operationId, "op_m3_status_skill");
assert.equal(payload.toolCalls[0].readback.value, false);
assert.equal(payload.runnerTrace.route, HWLAB_M3_STATUS_API_ROUTE);
assert.equal(payload.providerTrace.method, "GET");
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(calls.length, 1);
});
test("Code Agent blocks direct gateway or patch-panel requests instead of using M3 Skill CLI", async () => {
const direct = await handleCodeAgentChat(
{
@@ -946,6 +1173,21 @@ function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function createFakeCodexCommand() {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-codex-"));
const packageRoot = path.join(root, "node_modules", "@openai", "codex");
const command = path.join(packageRoot, "bin", "codex.js");
const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex");
await mkdir(path.dirname(command), { recursive: true });
await mkdir(path.dirname(native), { recursive: true });
await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8");
await writeFile(command, "#!/usr/bin/env node\nconsole.log('codex 0.128.0');\n", "utf8");
await writeFile(native, "#!/bin/sh\nexit 0\n", "utf8");
await chmod(command, 0o755);
await chmod(native, 0o755);
return { root, command };
}
test("Code Agent M3 Skill CLI blocks missing service-local HWLAB API base URL before loopback fallback", async () => {
const payload = await handleCodeAgentChat(
{
@@ -1040,16 +1282,15 @@ test("Codex stdio manager reports concrete blockers without falling back to read
test("Codex stdio feasibility verifies writable workspace, CODEX_HOME, and version-probed command", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-stdio-workspace-"));
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-codex-home-"));
const fakeCodex = await createFakeCodexCommand();
try {
const manager = createCodexStdioSessionManager();
const localCodexCommand = path.join(process.cwd(), "node_modules", ".bin", "codex");
const codexCommand = existsSync(localCodexCommand) ? localCodexCommand : "codex";
const availability = manager.describe({
env: {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_CODEX_COMMAND: codexCommand,
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
@@ -1058,7 +1299,7 @@ test("Codex stdio feasibility verifies writable workspace, CODEX_HOME, and versi
}
});
assert.equal(availability.command, codexCommand);
assert.equal(availability.command, fakeCodex.command);
assert.equal(availability.binary.present, true);
assert.equal(availability.binary.versionDetected, true);
assert.equal(availability.workspace, workspace);
@@ -1082,11 +1323,13 @@ test("Codex stdio feasibility verifies writable workspace, CODEX_HOME, and versi
} finally {
await rm(workspace, { recursive: true, force: true });
await rm(codexHome, { recursive: true, force: true });
await rm(fakeCodex.root, { recursive: true, force: true });
}
});
test("repo-owned Codex stdio manager creates and reuses long-lived sessions with trace evidence", async () => {
const calls = [];
const fakeCodex = await createFakeCodexCommand();
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_stdio_ready",
createRpcClient: async () => ({
@@ -1114,6 +1357,7 @@ test("repo-owned Codex stdio manager creates and reuses long-lived sessions with
CODEX_HOME: process.cwd(),
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
@@ -1167,9 +1411,11 @@ test("repo-owned Codex stdio manager creates and reuses long-lived sessions with
assert.equal(calls[0].name, "codex");
assert.equal(calls[1].name, "codex-reply");
assert.equal(calls[1].args.threadId, "thread_stdio_ready");
await rm(fakeCodex.root, { recursive: true, force: true });
});
test("Codex stdio runner startup failure marks session failed and blocks long-lived gate", async () => {
const fakeCodex = await createFakeCodexCommand();
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_stdio_start_failed",
createRpcClient: async () => ({
@@ -1185,6 +1431,7 @@ test("Codex stdio runner startup failure marks session failed and blocks long-li
CODEX_HOME: process.cwd(),
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
@@ -1221,9 +1468,11 @@ test("Codex stdio runner startup failure marks session failed and blocks long-li
assert.equal(serialized.includes("sk-test-secret"), false);
assert.equal(serialized.includes("TOKEN=abc123"), false);
assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false);
await rm(fakeCodex.root, { recursive: true, force: true });
});
test("Codex stdio protocol missing tools is a structured blocker, not a passed session", async () => {
const fakeCodex = await createFakeCodexCommand();
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_stdio_missing_tool",
createRpcClient: async () => ({
@@ -1242,6 +1491,7 @@ test("Codex stdio protocol missing tools is a structured blocker, not a passed s
CODEX_HOME: process.cwd(),
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
@@ -1271,4 +1521,5 @@ test("Codex stdio protocol missing tools is a structured blocker, not a passed s
assert.ok(payload.availability.codexStdio.blockerCodes.includes("stdio_protocol_not_wired"));
assert.deepEqual(payload.codexStdioFeasibility.protocol.toolsObserved, ["codex"]);
assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false);
await rm(fakeCodex.root, { recursive: true, force: true });
});
+38 -1
View File
@@ -1067,7 +1067,7 @@ async function handleCodeAgentChatHttp(request, response, options) {
skillsDirsExact: options.skillsDirsExact,
skillsDiscoveryDelayMs: options.skillsDiscoveryDelayMs,
sessionRegistry: options.sessionRegistry,
m3IoSkillRequestJson: options.m3IoSkillRequestJson,
m3IoSkillRequestJson: options.m3IoSkillRequestJson ?? createCodeAgentM3HwlabApiRequestJson(options),
codexStdioManager: options.codexStdioManager
}
);
@@ -1075,6 +1075,43 @@ async function handleCodeAgentChatHttp(request, response, options) {
sendJson(response, payload.status === "failed" && payload.error?.code === "invalid_params" ? 400 : 200, payload);
}
function createCodeAgentM3HwlabApiRequestJson(options = {}) {
return async (targetUrl, request = {}) => {
const url = new URL(targetUrl);
const route = url.pathname;
if (route === M3_IO_CONTROL_ROUTE && request.method === "POST") {
return {
ok: true,
status: 200,
body: await handleM3IoControl(request.body ?? {}, {
runtimeStore: options.runtimeStore,
now: options.now,
env: options.env,
requestJson: options.m3IoRequestJson
})
};
}
if (route === M3_STATUS_ROUTE && request.method === "GET") {
return {
ok: true,
status: 200,
body: await describeM3StatusLive(options)
};
}
return {
ok: false,
status: 404,
body: {
error: {
code: "not_found",
message: `Unsupported Code Agent M3 HWLAB API route ${route}`
}
},
error: `Unsupported Code Agent M3 HWLAB API route ${route}`
};
};
}
async function handleM3IoControlHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
+174 -5
View File
@@ -1,13 +1,14 @@
import assert from "node:assert/strict";
import { createServer as createHttpServer } from "node:http";
import { createServer as createTcpServer } from "node:net";
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
import { chmod, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { createCloudApiServer } from "./server.mjs";
import { validateCodeAgentChatSchema } from "./code-agent-chat.mjs";
import { createCodexStdioSessionManager } from "./codex-stdio-session.mjs";
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
import {
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
@@ -990,8 +991,9 @@ function layerForBlockedCase(blockedLayer) {
return blockedLayer;
}
async function m3ReadinessRequestJson(url) {
async function m3ReadinessRequestJson(url, request = {}) {
const parsed = new URL(url);
const body = request.body ?? {};
if (url.startsWith("http://gateway-1") && parsed.pathname === "/status") {
return {
ok: true,
@@ -1041,6 +1043,9 @@ async function m3ReadinessRequestJson(url) {
};
}
if (url.startsWith("http://gateway-2") && parsed.pathname === "/invoke") {
const value = request.body?.operationId?.includes("do_write") || request.body?.operationId?.includes("do-write")
? true
: false;
return {
ok: true,
status: 200,
@@ -1051,11 +1056,11 @@ async function m3ReadinessRequestJson(url) {
boxId: "boxsimu_2",
resourceId: "res_boxsimu_2",
port: "DI1",
value: false,
value,
state: {
port: "DI1",
direction: "input",
value: false,
value,
source: "patch-panel",
sourceResourceId: "res_boxsimu_1",
sourcePort: "DO1",
@@ -1108,6 +1113,34 @@ async function m3ReadinessRequestJson(url) {
}
};
}
if (url.startsWith("http://patch-panel") && parsed.pathname === "/sync/tick") {
return {
ok: true,
status: 200,
body: {
accepted: true,
propagatedBy: "hwlab-patch-panel",
routeCount: 1,
deliveryCount: 1,
routes: [
{
accepted: true,
deliveryCount: 1,
deliveries: [
{
resourceId: "res_boxsimu_2",
port: "DI1",
value: body.signals?.[0]?.value ?? false,
sourceResourceId: "res_boxsimu_1",
sourcePort: "DO1",
deliveryStatus: "applied"
}
]
}
]
}
};
}
throw new Error(`unexpected M3 readiness request ${url}`);
}
@@ -1454,12 +1487,33 @@ test("cloud api /v1 describes Code Agent egress blocker without leaking API key"
});
test("cloud api health reports provider, durable DB, and codex stdio ready when runtime contract passes", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-health-codex-stdio-"));
const codexHome = path.join(workspace, "codex-home");
const fakeCodex = await createFakeCodexCommand();
await mkdir(codexHome, { recursive: true });
const manager = createCodexStdioSessionManager({
createRpcClient: async () => ({
async initialize() {
return { tools: ["codex", "codex-reply"] };
},
async listTools() {
return ["codex", "codex-reply"];
},
close() {}
})
});
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
HWLAB_CODE_AGENT_WORKSPACE: workspace,
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:17680/v1/responses",
HWLAB_CLOUD_DB_URL: "postgres://hwlab_test:password@db.internal.local:5432/hwlab",
HWLAB_CLOUD_DB_SSL_MODE: "disable"
@@ -1484,7 +1538,8 @@ test("cloud api health reports provider, durable DB, and codex stdio ready when
async readiness() {
return durableReadyRuntime();
}
}
},
codexStdioManager: manager
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
@@ -1528,6 +1583,8 @@ test("cloud api health reports provider, durable DB, and codex stdio ready when
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(fakeCodex.root, { recursive: true, force: true });
}
});
@@ -1815,6 +1872,103 @@ test("cloud api /v1/agent/chat routes M3 IO through Skill CLI to /v1/m3/io only"
}
});
test("cloud api /v1/agent/chat routes Codex stdio M3 request through in-process HWLAB API handler", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-m3-skill-"));
const fakeCodex = await createFakeCodexCommand();
const gatewayCalls = [];
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_server_stdio_m3_skill",
createRpcClient: async () => ({
async initialize() {
return { tools: ["codex", "codex-reply"] };
},
async listTools() {
return ["codex", "codex-reply"];
},
async callTool() {
return {
structuredContent: {
threadId: "thread_server_stdio_m3_skill",
content: "stdio session ready for M3 skill."
}
};
},
close() {}
})
});
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
CODEX_HOME: workspace,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_WORKSPACE: workspace,
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
codexStdioManager: manager,
m3IoRequestJson: async (url, request) => {
gatewayCalls.push({ url, request });
return m3ReadinessRequestJson(url, request);
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const payload = await postAgent(port, {
conversationId: "cnv_server_stdio_m3_skill",
traceId: "trc_server_stdio_m3_skill",
message: "通过 HWLAB API 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1。"
});
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.runner.kind, "codex-mcp-stdio-runner");
assert.equal(payload.sessionMode, "codex-mcp-stdio-long-lived");
assert.equal(payload.longLivedSessionGate.status, "pass");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
assert.equal(payload.toolCalls.length, 2);
assert.equal(payload.toolCalls[0].type, "codex-stdio");
const skillTool = payload.toolCalls[1];
assert.equal(skillTool.type, "skill-cli");
assert.equal(skillTool.name, "hwlab-agent-runtime.m3-io");
assert.equal(skillTool.route, "/v1/m3/io");
assert.equal(skillTool.method, "POST");
assert.equal(skillTool.accepted, true);
assert.equal(skillTool.readback.value, true);
assert.equal(skillTool.controlReady, true);
assert.equal(skillTool.operationId.startsWith("op_m3_do_write_"), true);
assert.match(skillTool.auditId, /^aud_m3_do_write_/u);
assert.match(skillTool.evidenceId, /^evd_m3_do_write_/u);
assert.equal(payload.runnerTrace.route, "/v1/m3/io");
assert.equal(payload.providerTrace.codexStdio, true);
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.ok(payload.runner.toolPolicy.allowed.includes("POST /v1/m3/io"));
assert.deepEqual(gatewayCalls.map((call) => new URL(call.url).pathname), [
"/status",
"/invoke",
"/sync/tick",
"/status",
"/invoke"
]);
assert.equal(/https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel)|:7101\b|:7201\b|:7301\b|\/invoke\b|\/sync\/tick\b/iu.test(JSON.stringify(payload.toolCalls)), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(fakeCodex.root, { recursive: true, force: true });
}
});
test("cloud api /v1/agent/chat blocks direct M3 API base targets before request and without toolCall URL leakage", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-direct-target-"));
const calls = [];
@@ -2857,6 +3011,21 @@ async function postAgent(port, body) {
return response.json();
}
async function createFakeCodexCommand() {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-codex-"));
const packageRoot = path.join(root, "node_modules", "@openai", "codex");
const command = path.join(packageRoot, "bin", "codex.js");
const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex");
await mkdir(path.dirname(command), { recursive: true });
await mkdir(path.dirname(native), { recursive: true });
await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8");
await writeFile(command, "#!/usr/bin/env node\nconsole.log('codex 0.128.0');\n", "utf8");
await writeFile(native, "#!/bin/sh\nexit 0\n", "utf8");
await chmod(command, 0o755);
await chmod(native, 0o755);
return { root, command };
}
async function postAgentRaw(port, body, headers = {}) {
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
+22 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import http from "node:http";
import https from "node:https";
import os from "node:os";
@@ -240,7 +240,9 @@ async function runLocalContractSmoke() {
"",
"# Stdio Smoke"
].join("\n"));
let fakeCodex = null;
try {
fakeCodex = await createFakeCodexCommand();
const stdioManager = createCodexStdioSessionManager({
idFactory: () => "ses_code_agent_chat_smoke_stdio",
createRpcClient: async () => ({
@@ -267,6 +269,7 @@ async function runLocalContractSmoke() {
CODEX_HOME: stdioCodexHome,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_CODEX_WORKSPACE: stdioWorkspace,
@@ -352,6 +355,9 @@ async function runLocalContractSmoke() {
logOk("codex stdio runner passes long-lived workspace-write session capability");
} finally {
await rm(stdioWorkspaceRoot, { recursive: true, force: true });
if (fakeCodex) {
await rm(fakeCodex.root, { recursive: true, force: true });
}
}
const runnerSecondTurn = await handleCodeAgentChat(
@@ -752,6 +758,21 @@ function usage() {
};
}
async function createFakeCodexCommand() {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-codex-"));
const packageRoot = path.join(root, "node_modules", "@openai", "codex");
const command = path.join(packageRoot, "bin", "codex.js");
const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex");
await mkdir(path.dirname(command), { recursive: true });
await mkdir(path.dirname(native), { recursive: true });
await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8");
await writeFile(command, "#!/usr/bin/env node\nconsole.log('codex 0.128.0');\n", "utf8");
await writeFile(native, "#!/bin/sh\nexit 0\n", "utf8");
await chmod(command, 0o755);
await chmod(native, 0o755);
return { root, command };
}
function agentChatEndpoint(value) {
const url = new URL(value);
if (url.pathname.endsWith("/v1/agent/chat")) return url;