From 2c29ab242ba5953c227c4da43ca781bc1e1a09f1 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sun, 24 May 2026 11:12:59 +0000 Subject: [PATCH] feat: route chinese m3 io through controlled api --- internal/cloud/code-agent-chat.mjs | 259 +++++++++++++++++- .../code-agent-session-registry.test.mjs | 234 ++++++++++++++++ 2 files changed, 488 insertions(+), 5 deletions(-) diff --git a/internal/cloud/code-agent-chat.mjs b/internal/cloud/code-agent-chat.mjs index 7af5ecc4..fc8e23ef 100644 --- a/internal/cloud/code-agent-chat.mjs +++ b/internal/cloud/code-agent-chat.mjs @@ -1118,6 +1118,46 @@ async function callM3IoSkillRunner({ intent, conversationId, sessionId, traceId, } async function executeM3IoSkillIntent({ intent, traceId, requestId, actorId, env, now, requestJson }) { + if (intent?.blocker) { + const blocker = m3IoIntentBlocker(intent.blocker, { traceId }); + return { + commandArgs: [ + "m3", + "io", + "--trace-id", + traceId, + "--request-id", + requestId, + "--actor-id", + actorId + ], + skillResult: m3IoSkillBlockedBeforeRequestResult({ + traceId, + requestId, + actorId, + blocker, + route: HWLAB_M3_IO_API_ROUTE, + method: "POST", + action: "m3.io.blocked", + hwlabApi: { + route: HWLAB_M3_IO_API_ROUTE, + redactedUrl: null, + source: "intent-validation", + baseUrlConfigured: Boolean(configuredCloudApiBaseUrl(env)), + requiredEnv: [...HWLAB_M3_IO_API_BASE_URL_ENVS], + recommendedEnv: HWLAB_M3_IO_API_BASE_URL_ENV, + missingConfig: [], + cloudApiOnly: true, + directGatewayCalls: false, + directBoxCalls: false, + directPatchPanelCalls: false + }, + startedAt: nowIso(now), + finishedAt: nowIso(now) + }) + }; + } + if (!m3IoIntentHasSupportedAction(intent)) { const blocker = m3IoIntentActionMissingBlocker({ traceId }); return { @@ -1213,6 +1253,31 @@ function m3IoIntentActionMissingBlocker({ traceId }) { }; } +function m3IoIntentBlocker(blocker, { traceId } = {}) { + const code = blocker?.code ?? "m3_io_scope_blocked"; + const layer = blocker?.layer ?? "intent"; + return { + code, + layer, + category: blocker?.category ?? "scope_blocked", + retryable: false, + source: "code-agent-m3-skill-cli", + summary: blocker?.summary ?? "M3 IO request is outside the allowed controlled virtual IO surface.", + message: blocker?.message ?? "Code Agent only supports the M3 virtual IO controlled cloud-api path.", + zh: blocker?.zh ?? "当前只支持 M3 虚拟 IO 受控链路。", + userMessage: blocker?.userMessage ?? "当前只支持 M3 虚拟 IO 受控链路:box-simu-1 DO1=true/false 写入,或 box-simu-2 DI1 读取。", + traceId, + route: HWLAB_M3_IO_API_ROUTE, + toolName: HWLAB_M3_IO_SKILL_NAME, + allowed: { + read: `${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}`, + write: `${M3_IO_TOPOLOGY.sourceResourceId}:${M3_IO_TOPOLOGY.sourcePort}=true|false`, + route: HWLAB_M3_IO_API_ROUTE + }, + requested: blocker?.requested ?? null + }; +} + function m3IoSkillBlockedBeforeRequestResult({ traceId, requestId, @@ -1745,6 +1810,18 @@ function m3IoStructuredResult({ skillResult = {}, stdioResult = {}, commandArgs auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null, evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null }, + zh: m3IoChineseResult({ + action, + isBlocker, + blocker, + route, + method, + do1Value, + di1Value, + trusted, + durable, + skillResult + }), session: { sessionId: stdioResult.session?.sessionId ?? null, sessionMode: stdioResult.sessionMode ?? stdioResult.runner?.sessionMode ?? null, @@ -1772,7 +1849,9 @@ function m3IoStructuredResult({ skillResult = {}, stdioResult = {}, commandArgs trustBlocker: skillResult.trustBlocker ?? null, note: trusted ? "operation/audit/evidence 已有可信持久化记录;仍不是 M3 DEV-LIVE 验收结论。" - : "本次不伪造 DEV-LIVE:控制链路结果与可信持久化状态分开展示。" + : skillResult.controlReady === true + ? "控制链路可达,但可信记录未 green,不能作为 DEV-LIVE 可信闭环通过。" + : "本次不伪造 DEV-LIVE:控制链路结果与可信持久化状态分开展示。" }, blocker, blockers: skillResult.blockers ?? (blocker ? [blocker] : []), @@ -1832,6 +1911,62 @@ function m3IoStructuredSummary({ action, isBlocker, blocker, route, do1Value, di return `M3 IO 结果:${actionText};trusted=${trusted ? "true" : "false"};durable=${durable ? "true" : "false"}。`; } +function m3IoChineseResult({ action, isBlocker, blocker, route, method, do1Value, di1Value, trusted, durable, skillResult = {} }) { + const expectedValue = action === "do.write" ? do1Value : null; + const targetResource = action === "do.write" + ? `${M3_IO_TOPOLOGY.sourceResourceId}:${M3_IO_TOPOLOGY.sourcePort}` + : `${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}`; + const actualResult = action === "do.write" + ? `${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}=${formatM3Value(di1Value)}` + : action === "di.read" + ? `${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}=${formatM3Value(di1Value)}` + : "未执行"; + const controlReachable = skillResult.controlReady === true; + const trustedGreen = trusted === true; + const durableGreen = durable?.durable === true && !durable?.blocker; + return { + status: isBlocker ? "blocked" : trustedGreen && durableGreen ? "succeeded" : "degraded", + targetResource, + action: action === "do.write" + ? "写入 DO1 并回读 DI1" + : action === "di.read" + ? "读取 DI1" + : "M3 IO 请求", + expectedValue, + readValue: action === "di.read" ? di1Value : null, + actualResult, + source: `HWLAB cloud-api 受控链路 ${method} ${route}`, + traceId: skillResult.traceId ?? null, + operationId: skillResult.operationId ?? null, + auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null, + evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null, + controlPath: { + reachable: controlReachable, + cloudApiRouteOnly: true, + route, + directGatewayCalls: false, + directBoxCalls: false, + directPatchPanelCalls: false + }, + runtime: { + durableGreen, + trustedGreen, + durableStatus: durable?.status ?? null, + durableBlocker: durable?.blocker ?? null, + note: controlReachable && !trustedGreen + ? "控制链路可达,但可信记录未 green,不能作为 DEV-LIVE 可信闭环通过。" + : trustedGreen + ? "控制链路和可信记录均为 green;仍不在本接口内声明 live 已更新。" + : "控制链路或可信记录未 green。" + }, + blocker: blocker?.code ? { + code: blocker.code, + layer: blocker.layer ?? null, + message: blocker.userMessage ?? blocker.zh ?? blocker.message ?? blocker.code + } : null + }; +} + function redactM3IoCommandArgs(commandArgs = []) { const redacted = []; for (let index = 0; index < commandArgs.length; index += 1) { @@ -2040,12 +2175,30 @@ function isSessionContextRequest(text) { function detectM3IoIntent(text) { const normalized = String(text ?? ""); - if (!/\bM3\b|DO1|DI1|数字输出|数字输入|回读|读回|状态|status/u.test(normalized)) { + if (!/\bM\d+\b|\bM3\b|DO\s*\d+|DI\s*\d+|数字输出|数字输入|box[-_ ]?simu|res_boxsimu|回读|读回|状态|status|串口|serial|PWM|DAP/u.test(normalized)) { return null; } if (isDirectM3HardwareAccessRequest(normalized)) { return null; } + const scopeBlocker = detectM3IoScopeBlocker(normalized); + if (scopeBlocker) { + return { + kind: "m3_io", + toolName: HWLAB_M3_IO_SKILL_NAME, + action: null, + blocker: scopeBlocker + }; + } + const explicitTargetBlocker = detectM3IoTargetBlocker(normalized, "any"); + if (explicitTargetBlocker) { + return { + kind: "m3_io", + toolName: HWLAB_M3_IO_SKILL_NAME, + action: null, + blocker: explicitTargetBlocker + }; + } if (/DEV-LIVE|验收|acceptance|(?:M3|M4|M5).{0,20}(?:通过|pass|green|accept)/iu.test(normalized)) { return null; } @@ -2075,6 +2228,15 @@ function detectM3IoIntent(text) { } if (wantsRead && !wantsWrite) { + const targetBlocker = detectM3IoTargetBlocker(normalized, "di.read"); + if (targetBlocker) { + return { + kind: "m3_io", + toolName: HWLAB_M3_IO_SKILL_NAME, + action: null, + blocker: targetBlocker + }; + } return { kind: "m3_io", toolName: HWLAB_M3_IO_SKILL_NAME, @@ -2082,6 +2244,15 @@ function detectM3IoIntent(text) { }; } + const targetBlocker = detectM3IoTargetBlocker(normalized, "do.write"); + if (targetBlocker) { + return { + kind: "m3_io", + toolName: HWLAB_M3_IO_SKILL_NAME, + action: null, + blocker: targetBlocker + }; + } const value = parseM3WriteValue(normalized); if (typeof value !== "boolean") { return { @@ -2098,6 +2269,79 @@ function detectM3IoIntent(text) { }; } +function detectM3IoScopeBlocker(text) { + const value = String(text ?? ""); + const unsupportedStage = value.match(/\bM(?!3\b)\d+\b/iu)?.[0] ?? null; + if (unsupportedStage && /(?:IO|DI|DO|控制|读取|读|写|状态|硬件|hardware)/iu.test(value)) { + return m3ScopeBlocker({ + code: "m3_io_scope_blocked", + summary: `Unsupported hardware stage ${unsupportedStage} requested.`, + zh: `当前 Code Agent 只支持 M3 虚拟 IO,暂不支持 ${unsupportedStage} IO。`, + requested: unsupportedStage + }); + } + const dangerous = value.match(/真实硬件|物理硬件|real\s+hardware|production\s+hardware|PROD|任意\s*shell|shell|bash|sh\s+-c|kubectl|数据库写入|写入数据库|database\s+write|SQL\s+write|绕过\s*(?:patch-panel|cloud-api|HWLAB API)|bypass\s+(?:patch-panel|cloud-api|HWLAB API)|批量脚本|脚本批量|batch\s+script|DAP|串口|serial|PWM/iu)?.[0] ?? null; + if (dangerous && /(?:M3|IO|DI|DO|box[-_ ]?simu|res_boxsimu|硬件|hardware|patch-panel|cloud-api|HWLAB API)/iu.test(value)) { + return m3ScopeBlocker({ + code: "m3_io_scope_blocked", + summary: `Dangerous or out-of-scope M3 IO request was blocked: ${dangerous}.`, + zh: "当前只支持 M3 虚拟 IO 受控链路;不能操作真实硬件、shell、数据库、串口/DAP/PWM、批量脚本,或绕过 cloud-api/patch-panel。", + requested: dangerous + }); + } + return null; +} + +function detectM3IoTargetBlocker(text, action) { + const value = String(text ?? ""); + const unsupportedPort = value.match(/\b(?:DO|DI)\s*(?!1\b)\d+\b|数字(?:输出|输入)\s*(?!1\b)\d+|PWM|DAP|串口|serial/iu)?.[0] ?? null; + if (unsupportedPort) { + return m3ScopeBlocker({ + code: "m3_io_target_unsupported", + category: "target_unsupported", + summary: `Unsupported M3 IO target ${unsupportedPort} requested.`, + zh: "当前只支持 M3 阶段允许的 DO1/DI1,不能执行 DO2/DI2、DAP、串口或 PWM 等请求。", + requested: unsupportedPort + }); + } + + const mentionsBox1 = /(?:box[-_ ]?simu[-_ ]?1|boxsimu[_-]?1|res_boxsimu_1)/iu.test(value); + const mentionsBox2 = /(?:box[-_ ]?simu[-_ ]?2|boxsimu[_-]?2|res_boxsimu_2)/iu.test(value); + if (action === "do.write" && mentionsBox2 && !mentionsBox1) { + return m3ScopeBlocker({ + code: "m3_io_target_unsupported", + category: "target_unsupported", + summary: "DO write target is not the allowed res_boxsimu_1:DO1 endpoint.", + zh: "DO 写入仅允许目标 box-simu-1 DO1;不能把 box-simu-2 当作 DO 写入目标。", + requested: "box-simu-2 DO1" + }); + } + if (action === "di.read" && mentionsBox1 && !mentionsBox2) { + return m3ScopeBlocker({ + code: "m3_io_target_unsupported", + category: "target_unsupported", + summary: "DI read target is not the allowed res_boxsimu_2:DI1 endpoint.", + zh: "DI 读取仅允许目标 box-simu-2 DI1;不能把 box-simu-1 当作 DI 读取目标。", + requested: "box-simu-1 DI1" + }); + } + return null; +} + +function m3ScopeBlocker({ code, category = "scope_blocked", summary, zh, requested }) { + return { + code, + layer: "intent", + category, + retryable: false, + summary, + message: "M3 IO request is outside the allowed controlled virtual IO surface.", + zh, + userMessage: `${zh} 当前只支持 box-simu-1 DO1=true/false 写入和 box-simu-2 DI1 读取,且必须走 cloud-api ${HWLAB_M3_IO_API_ROUTE}。`, + requested + }; +} + 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)) { @@ -2116,8 +2360,10 @@ function isM3TopologyRouteMention(text) { } 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; + if (/\bfalse\b|\boff\b|关闭|置低|低电平|拉低|断开/iu.test(text)) return false; + if (/\btrue\b|\bon\b|打开|置高|高电平|拉高|闭合/iu.test(text)) return true; + if (/(?:设置为|设为|写成|写入|置为|set\s+to|=|:)\s*0\b/iu.test(text)) return false; + if (/(?:设置为|设为|写成|写入|置为|set\s+to|=|:)\s*1\b/iu.test(text)) return true; return null; } @@ -2267,6 +2513,9 @@ function m3IoSkillReply(skillResult, m3Io = null) { if (m3Io?.blocker?.code) { lines.push(`Blocker: ${m3Io.blocker.code};blocker=${m3Io.blocker.code};layer=${m3Io.blocker.layer ?? "unknown"}。`); } + if (m3Io?.trust?.note) { + lines.push(`可信记录:${m3Io.trust.note}`); + } lines.push(`可信状态:trusted=${m3Io?.trust?.trusted === true ? "true" : "false"};durable=${m3Io?.trust?.durable === true ? "true" : "false"};这不是 M3 DEV-LIVE 验收结论。`); return boundToolOutput(lines.join("\n"), READONLY_TOOL_OUTPUT_LIMIT).text; } @@ -2411,7 +2660,7 @@ function structuredCompletionBlocker(result, context = {}) { } function m3CompletionBlockerCode(primary = {}) { - if (["m3_gateway_session_unavailable", "runtime_durable_not_green", "skill_cli_api_base_missing", "m3_io_intent_action_missing"].includes(primary?.code)) { + if (["m3_gateway_session_unavailable", "runtime_durable_not_green", "skill_cli_api_base_missing", "m3_io_intent_action_missing", "m3_io_scope_blocked", "m3_io_target_unsupported"].includes(primary?.code)) { return "m3_readiness_blocked"; } return primary?.code ?? "m3_io_blocker"; diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index a5c18428..926ba5fc 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -1319,6 +1319,162 @@ test("Code Agent M3 true/false/read requests bypass Codex stdio and use Skill CL assert.equal(calls.length, cases.length); }); +test("Code Agent parses exact Chinese M3 IO requests and returns structured Chinese result", async () => { + const calls = []; + const cases = [ + { + suffix: "read_di1", + message: "读取 box-simu-2 DI1 状态", + action: "di.read", + value: true, + zhAction: "读取 DI1", + targetResource: "res_boxsimu_2:DI1" + }, + { + suffix: "write_do1_true", + message: "把 box-simu-1 DO1 设置为 true", + action: "do.write", + value: true, + zhAction: "写入 DO1 并回读 DI1", + targetResource: "res_boxsimu_1:DO1" + }, + { + suffix: "write_do1_false", + message: "把 box-simu-1 DO1 设置为 false", + action: "do.write", + value: false, + zhAction: "写入 DO1 并回读 DI1", + targetResource: "res_boxsimu_1:DO1" + } + ]; + + for (const item of cases) { + const payload = await handleCodeAgentChat( + { + conversationId: `cnv_m3_exact_zh_${item.suffix}`, + traceId: `trc_m3_exact_zh_${item.suffix}`, + message: item.message + }, + { + now: () => "2026-05-24T10:00:00.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 exact Chinese M3 IO"); + }, + m3IoSkillRequestJson: async (url, request) => { + calls.push({ suffix: item.suffix, url, request }); + assert.equal(new URL(url).pathname, HWLAB_M3_IO_API_ROUTE); + assert.equal(new URL(url).hostname, "hwlab-cloud-api.hwlab-dev.svc.cluster.local"); + assert.equal(request.method, "POST"); + assert.equal(request.body.action, item.action); + assert.equal(request.body.source, "hwlab-agent-runtime.m3-io"); + if (item.action === "do.write") { + assert.equal(request.body.resourceId, "res_boxsimu_1"); + assert.equal(request.body.port, "DO1"); + assert.equal(request.body.value, item.value); + } else { + assert.equal(request.body.resourceId, "res_boxsimu_2"); + assert.equal(request.body.port, "DI1"); + assert.equal(Object.hasOwn(request.body, "value"), false); + } + return { + ok: true, + status: 200, + body: { + serviceId: "hwlab-cloud-api", + contractVersion: "m3-io-control-v1", + status: "succeeded", + accepted: true, + action: item.action, + traceId: `trc_m3_exact_zh_${item.suffix}`, + operationId: `op_m3_exact_zh_${item.suffix}`, + auditId: `aud_m3_exact_zh_${item.suffix}`, + evidenceId: `evd_m3_exact_zh_${item.suffix}`, + auditState: { + status: item.action === "do.write" ? "written_non_durable" : "read" + }, + evidenceState: { + status: "blocked", + sourceKind: "BLOCKED", + blocker: "runtime_durable_not_green", + writeStatus: item.action === "do.write" ? "written_non_durable" : "not_written" + }, + durableStatus: { + status: "degraded", + durable: false, + blocker: "runtime_durable_not_green" + }, + result: item.action === "do.write" + ? { + value: item.value, + targetReadback: { + status: "succeeded", + value: item.value, + resourceId: "res_boxsimu_2", + port: "DI1" + } + } + : { + status: "succeeded", + value: item.value, + 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, "hwlab-skill-cli"); + assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready); + assert.equal(payload.responseType, "m3_io_result"); + assert.equal(payload.m3Io.zh.status, "degraded"); + assert.equal(payload.m3Io.zh.targetResource, item.targetResource); + assert.equal(payload.m3Io.zh.action, item.zhAction); + assert.equal(payload.m3Io.zh.traceId, `trc_m3_exact_zh_${item.suffix}`); + assert.equal(payload.m3Io.zh.operationId, `op_m3_exact_zh_${item.suffix}`); + assert.equal(payload.m3Io.zh.auditId, `aud_m3_exact_zh_${item.suffix}`); + assert.equal(payload.m3Io.zh.evidenceId, `evd_m3_exact_zh_${item.suffix}`); + assert.equal(payload.m3Io.zh.controlPath.reachable, true); + assert.equal(payload.m3Io.zh.controlPath.cloudApiRouteOnly, true); + assert.equal(payload.m3Io.zh.runtime.durableGreen, false); + assert.equal(payload.m3Io.zh.runtime.trustedGreen, false); + assert.equal(payload.m3Io.zh.runtime.durableBlocker, "runtime_durable_not_green"); + assert.match(payload.m3Io.zh.runtime.note, /控制链路可达,但可信记录未 green/u); + assert.match(payload.reply.content, /控制链路可达,但可信记录未 green/u); + assert.equal(payload.toolCalls[0].route, HWLAB_M3_IO_API_ROUTE); + assert.equal(payload.toolCalls[0].method, "POST"); + assert.equal(payload.toolCalls[0].hwlabApi.cloudApiOnly, true); + assert.equal(payload.toolCalls[0].directGatewayCalls, false); + assert.equal(payload.toolCalls[0].directBoxCalls, false); + assert.equal(payload.toolCalls[0].directPatchPanelCalls, false); + assert.equal(payload.providerTrace.fallbackUsed, false); + } + + assert.equal(calls.length, cases.length); + for (const call of calls) { + assert.equal(call.url.includes("gateway"), false); + assert.equal(call.url.includes("box-simu"), false); + assert.equal(call.url.includes("patch-panel"), false); + } +}); + test("Code Agent ambiguous M3 IO control request returns structured blocker without Codex stdio", async () => { const codexStdioCalls = []; const payload = await handleCodeAgentChat( @@ -1385,6 +1541,84 @@ test("Code Agent ambiguous M3 IO control request returns structured blocker with assert.equal(codexStdioCalls.length, 0); }); +test("Code Agent blocks out-of-scope or dangerous Chinese M3 IO requests before HWLAB API call", async () => { + const messages = [ + { + suffix: "real_hardware", + message: "把真实硬件 M3 DO1 设置为 true", + code: "m3_io_scope_blocked" + }, + { + suffix: "shell", + message: "用任意 shell 批量脚本绕过 patch-panel 写入 M3 DO1 true", + code: "m3_io_scope_blocked" + }, + { + suffix: "di2", + message: "读取 box-simu-2 DI2 状态", + code: "m3_io_target_unsupported" + }, + { + suffix: "wrong_box", + message: "把 box-simu-2 DO1 设置为 true", + code: "m3_io_target_unsupported" + }, + { + suffix: "serial_pwm", + message: "通过串口读取 M3 PWM 状态", + code: "m3_io_scope_blocked" + } + ]; + + for (const item of messages) { + const payload = await handleCodeAgentChat( + { + conversationId: `cnv_m3_dangerous_zh_${item.suffix}`, + traceId: `trc_m3_dangerous_zh_${item.suffix}`, + message: item.message + }, + { + now: () => "2026-05-24T10:01:00.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 dangerous M3 IO"); + }, + m3IoSkillRequestJson: async () => { + throw new Error("dangerous or out-of-scope M3 IO must block before HWLAB API request"); + } + } + ); + + validateCodeAgentChatSchema(payload); + assert.equal(payload.status, "completed"); + assert.equal(payload.provider, "hwlab-skill-cli"); + assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked); + assert.equal(payload.responseType, "m3_io_blocker"); + assert.equal(payload.blocker.code, "m3_readiness_blocked"); + assert.equal(payload.toolCalls.length, 1); + assert.equal(payload.toolCalls[0].status, "blocked"); + assert.equal(payload.toolCalls[0].route, HWLAB_M3_IO_API_ROUTE); + assert.equal(payload.toolCalls[0].blocker.code, item.code); + assert.equal(payload.toolCalls[0].hwlabApi.source, "intent-validation"); + assert.equal(payload.toolCalls[0].accepted, false); + assert.equal(payload.toolCalls[0].operationId, null); + assert.equal(payload.toolCalls[0].directGatewayCalls, false); + assert.equal(payload.toolCalls[0].directBoxCalls, false); + assert.equal(payload.toolCalls[0].directPatchPanelCalls, false); + assert.equal(payload.providerTrace.fallbackUsed, false); + assert.equal(payload.m3Io.zh.status, "blocked"); + assert.equal(payload.m3Io.zh.controlPath.cloudApiRouteOnly, true); + assert.match(payload.m3Io.zh.blocker.message, /当前只支持/u); + assert.match(payload.reply.content, /M3 IO 阻塞/u); + assert.match(payload.reply.content, /Blocker:/u); + } +}); + test("Code Agent M3 status uses Skill CLI GET /v1/m3/status and keeps route evidence", async () => { const calls = []; const payload = await handleCodeAgentChat(