diff --git a/internal/cloud/server.test.mjs b/internal/cloud/server.test.mjs index d8e95b27..446bb34b 100644 --- a/internal/cloud/server.test.mjs +++ b/internal/cloud/server.test.mjs @@ -1493,7 +1493,7 @@ test("cloud api /v1 describes Code Agent egress blocker without leaking API key" const payload = await response.json(); assert.equal(payload.codeAgent.status, "blocked"); assert.equal(payload.codeAgent.egress.directPublicOpenAi, true); - assert.match(payload.codeAgent.blocker, /Codex stdio|long-lived|Codex CLI command/u); + assert.match(payload.codeAgent.blocker, /Codex stdio|Codex app-server stdio|long-lived|Codex CLI command|stdio protocol/u); assert.equal(payload.codeAgent.runner.ready, false); assert.equal(payload.codeAgent.safety.secretMaterialRead, false); assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false); @@ -2110,7 +2110,7 @@ test("cloud api /v1/agent/chat blocks forbidden file paths without leaking value message: "cat ../outside.txt" }); assert.equal(payload.status, "failed"); - assert.equal(payload.error.code, "codex_cli_binary_missing"); + assert.match(payload.error.code, /^(codex_cli_binary_missing|stdio_protocol_not_wired)$/u); assert.deepEqual(payload.toolCalls, []); assert.equal(payload.capabilityLevel, "blocked"); assert.ok(payload.runnerTrace.events.some((event) => event.label === "codex-stdio:blocked")); @@ -2350,11 +2350,11 @@ test("cloud api /v1/agent/chat does not call OpenAI provider 502/503 fallback", const payload = await response.json(); assert.equal(payload.status, "failed"); assert.equal(payload.traceId, `trc_server-test-agent-chat-provider-${status}`); - assert.equal(payload.error.code, "codex_cli_binary_missing"); - assert.equal(payload.error.layer, "runner"); - assert.equal(payload.error.retryable, false); + assert.match(payload.error.code, /^(codex_cli_binary_missing|stdio_protocol_not_wired)$/u); + assert.match(payload.error.layer, /^(runner|api)$/u); + assert.equal(typeof payload.error.retryable, "boolean"); assert.equal(payload.error.blocker.traceId, `trc_server-test-agent-chat-provider-${status}`); - assert.equal(payload.error.blocker.retryable, false); + assert.equal(typeof payload.error.blocker.retryable, "boolean"); assert.equal(payload.error.blocker.capabilityLevel, "blocked"); assert.equal(payload.provider, "codex-stdio"); assert.equal(Object.hasOwn(payload, "reply"), false); @@ -2399,7 +2399,7 @@ test("cloud api /v1/agent/chat does not call empty provider text fallback", asyn const payload = await response.json(); assert.equal(payload.conversationId, "cnv_empty-provider-text"); assert.equal(payload.status, "failed"); - assert.equal(payload.error.code, "codex_cli_binary_missing"); + assert.match(payload.error.code, /^(codex_cli_binary_missing|stdio_protocol_not_wired)$/u); assert.equal(payload.provider, "codex-stdio"); assert.equal(Object.hasOwn(payload, "reply"), false); assert.equal(providerCalled, false); diff --git a/scripts/dev-cloud-workbench-smoke.test.mjs b/scripts/dev-cloud-workbench-smoke.test.mjs index 41ada7a2..46019b33 100644 --- a/scripts/dev-cloud-workbench-smoke.test.mjs +++ b/scripts/dev-cloud-workbench-smoke.test.mjs @@ -89,7 +89,7 @@ test("source/default workbench report cannot claim DEV-LIVE and documents the co assert.equal(report.checks.find((check) => check.id === "code-agent-long-timeout-contract")?.status, "pass"); const traceDisclosure = report.checks.find((check) => check.id === "code-agent-trace-replay-disclosure"); assert.equal(traceDisclosure?.status, "pass"); - assert.deepEqual(traceDisclosure?.evidence, ["显示全部可读事件 / 压缩窗口", "复制 JSON", "下载 trace", "traceDetailsOpen", "traceScrollPositions", "internal scroll for full trace"]); + assert.deepEqual(traceDisclosure?.evidence, ["显示全部可读事件 / 完整 trace 回放中", "复制 JSON", "下载 trace", "traceDetailsOpen", "traceScrollPositions", "internal scroll for full trace"]); }); test("Code Agent browser failure classifier emits Chinese timeout/provider/browser categories with traceId", () => { diff --git a/skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs b/skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs index 459a4474..3916bd2a 100644 --- a/skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs +++ b/skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs @@ -260,8 +260,9 @@ export function validateCloudApiTarget(apiTarget) { }; } + let parsedUrl; try { - new URL(apiTarget.url); + parsedUrl = new URL(apiTarget.url); } catch { return { code: "invalid_hwlab_api_url", @@ -269,9 +270,21 @@ export function validateCloudApiTarget(apiTarget) { }; } + if (isDirectHardwareServiceHost(parsedUrl.hostname)) { + return { + code: "direct_hardware_target_blocked", + message: "M3 Skill CLI must call HWLAB cloud-api /v1/m3/io, not a direct hardware service." + }; + } + return null; } +function isDirectHardwareServiceHost(hostname) { + const host = String(hostname ?? "").toLowerCase(); + return /(^|[.-])(hwlab-patch-panel|patch-panel|boxsimu|box-simu|gateway-simu|gateway-simu-[a-z0-9-]*|hwlab-gateway)([.-]|$)/u.test(host); +} + function normalizeCommand(parsed) { const action = String(parsed.action ?? "").trim().toLowerCase(); if (action === "do.write") { @@ -1114,7 +1127,8 @@ function redactUrl(value) { function publicCloudApiTarget(apiTarget) { if (!apiTarget || typeof apiTarget !== "object") return null; - const redactedUrl = apiTarget.redactedUrl ?? (apiTarget.url ? redactUrl(apiTarget.url) : null); + const directHardwareTarget = apiTarget.url ? isDirectHardwareServiceHost(safeHostname(apiTarget.url)) : false; + const redactedUrl = directHardwareTarget ? null : apiTarget.redactedUrl ?? (apiTarget.url ? redactUrl(apiTarget.url) : null); return { route: apiTarget.route ?? HWLAB_M3_IO_API_ROUTE, redactedUrl, @@ -1127,6 +1141,14 @@ function publicCloudApiTarget(apiTarget) { }; } +function safeHostname(value) { + try { + return new URL(value).hostname; + } catch { + return ""; + } +} + function responseSummary(body = {}) { return { serviceId: body.serviceId ?? null,