diff --git a/internal/cloud/code-agent-chat.mjs b/internal/cloud/code-agent-chat.mjs index 2133cd7b..131e6f65 100644 --- a/internal/cloud/code-agent-chat.mjs +++ b/internal/cloud/code-agent-chat.mjs @@ -2202,6 +2202,12 @@ function detectReadOnlyRunnerIntent(message, options = {}) { reason: "Code Agent 安全边界不读取或输出 secret、token、kubeconfig、密码、私钥或环境变量原文。" }; } + if (isPcGatewayCodexToolRequest(text)) { + return { + kind: "none", + toolName: "codex-stdio.pc-gateway-wrapper" + }; + } const m3IoIntent = detectM3IoIntent(text); if (m3IoIntent) { return m3IoIntent; @@ -2221,6 +2227,14 @@ function detectReadOnlyRunnerIntent(message, options = {}) { return { kind: "none" }; } +function isPcGatewayCodexToolRequest(text) { + const value = String(text ?? ""); + const mentionsGatewayTool = /(?:\/app\/tools\/hwlab-gateway-shell\.mjs|hwlab-gateway-shell\.mjs|hardware\.invoke\.shell|PC\s*gateway|gws_DESKTOP-[A-Z0-9-]+|cap_windows_cmd_exec|res_windows_host)/iu.test(value); + const mentionsWindowsCommand = /(?:Windows\s+cmd|cmd\s*\/c|shell\.exec|hostname|PowerShell|powershell)/iu.test(value); + const mentionsWindowsSkill = /(?:C:\\Users\\liang\\\.agents\\skills|\.agents[\\/]+skills|keil-cli\.py|\bKeil\b|MDK-ARM|\.uvprojx|F:\\Work\\constart|job-status)/iu.test(value); + return mentionsGatewayTool && (mentionsWindowsCommand || mentionsWindowsSkill); +} + function detectExternalNetworkIntent(text) { const value = String(text ?? "").trim(); if (!value) return null; diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index 5a41ea9e..e3b492fb 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -1889,6 +1889,73 @@ test("Code Agent PC gateway prompt reaches Codex stdio instead of internal hardw } }); +test("Code Agent Keil gateway prompt is not blocked by M3 IO intent guard", async () => { + const calls = []; + let providerCalled = false; + const fakeCodex = await createFakeCodexCommand(); + const codexHome = await prepareFakeCodexHome(); + const registry = createCodeAgentSessionRegistry(); + const manager = createCodexStdioSessionManager({ + idFactory: () => "ses_keil_gateway_stdio", + createRpcClient: async () => createFakeAppServerClient({ + calls, + responses: ["已通过 PC gateway 调用 Windows Keil skill,success=true,return_code=0,生成 FREQ_Controller_FW.hex。"] + }) + }); + + try { + const payload = await handleCodeAgentChat( + { + conversationId: "cnv_keil_gateway_stdio", + traceId: "trc_keil_gateway_stdio", + projectId: "prj_mvp_topology", + message: String.raw`请用 /app/tools/hwlab-gateway-shell.mjs 通过 PC gateway 调用 Windows cmd,进入 C:\Users\liang\.agents\skills\keil,然后运行 py -3 keil-cli.py build -p "F:\Work\constart\projects\71-00075-11\FirmWare\MDK-ARM\FREQ_Controller_FW.uvprojx" -t FREQ_Controller_FW;如果返回 job id,请用 py -3 keil-cli.py job-status 轮询到终态。` + }, + { + now: () => "2026-05-24T14:35:00.000Z", + 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_SANDBOX: "danger-full-access", + HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), + HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses" + }, + sessionRegistry: registry, + codexStdioManager: manager, + callProvider: async () => { + providerCalled = true; + throw new Error("text fallback must not be used"); + }, + m3IoSkillRequestJson: async () => { + throw new Error("Keil gateway prompt must not be routed to M3 IO"); + } + } + ); + + validateCodeAgentChatSchema(payload); + assert.equal(payload.status, "completed"); + assert.equal(payload.provider, "codex-stdio"); + assert.equal(payload.responseType, undefined); + assert.notEqual(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked); + assert.equal(providerCalled, false); + const turn = calls.find((call) => call.method === "turn/start"); + assert.ok(turn, "Codex stdio turn/start should be called"); + assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-shell\.mjs/u); + assert.match(turn.args.prompt, /keil-cli\.py build/u); + assert.match(turn.args.prompt, /job-status/u); + assert.match(turn.args.prompt, /FREQ_Controller_FW\.uvprojx/u); + } finally { + await rm(fakeCodex.root, { recursive: true, force: true }); + await rm(codexHome, { recursive: true, force: true }); + } +}); + function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }