Merge pull request #468 from pikasTech/fix/code-agent-pc-gateway-capability

源码化 Code Agent PC gateway 能力路由
This commit is contained in:
Lyon
2026-05-24 20:34:27 +08:00
committed by GitHub
8 changed files with 1643 additions and 5 deletions
@@ -66,6 +66,20 @@ report、issue、PR 或截图。
- OpenAI Responses fallback 只能标记为 `openai-responses-fallback` /
`text-chat-only`,不得满足 Codex runner capability gate。默认路径不得用 fallback
冒充完整 Code Agent;仅明确允许 fallback 时才可作为普通文本备用通道返回。
- 已登记 PC gateway `shell.exec` capability 是受控硬件能力例外:当请求能从显式
`projectId/gatewaySessionId/resourceId/capabilityId` 或唯一 DEV MVP topology 解析时,
`/v1/agent/chat` 可以选择 `hwlab-hardware-capability` runner,经 cloud-api 进程内
JSON-RPC helper 调用 `hardware.invoke.shell`。该 helper 必须使用冻结的
`hwlab-cloud-api` service id 常量,返回 payload 需包含 `toolCalls[]``operationId`
`dispatchStatus``exitCode`、stdout/stderr 摘要、`runnerTrace.eventLabels`
`capabilityLevel`。拓扑缺失、不唯一、gateway offline、session/capability missing、
dispatch failed 或 JSON-RPC meta/serviceId 无效时必须返回结构化 blocker,不能转入
Codex stdio 或 OpenAI 文本 fallback 冒充成功。
- `security.hardware-boundary` 仍然阻断直接 gateway/box-simu/patch-panel URL、`/invoke`
`/sync/tick` 和泛化硬件写绕过;受控 `hardware.invoke.shell` capability route 只允许
已登记、可解析的 cloud-api 调用,不开放任意 shell。JSON-RPC/REST bridge 拒绝未知
`serviceId` 时,错误原因应可见为 `unknown serviceId ...` 或等价结构化 reason,且不得泄露
Secret/token。
当前 DEV/runtime 若要升级为完整 #275 runner,至少需要 repo-owned 的 Codex CLI/stdio 或等价
runner 二进制/协议适配、session supervisor 生命周期、workspace mount 与 sandbox 合同、token/Secret
+72 -3
View File
@@ -31,6 +31,13 @@ import {
codeAgentSessionLifecycleSummary,
decorateCodeAgentSession
} from "./code-agent-session-lifecycle.mjs";
import {
CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED,
CODE_AGENT_HARDWARE_PROVIDER,
HARDWARE_INVOKE_SHELL_METHOD,
callCodeAgentHardwareRunner,
detectHardwareCapabilityIntent
} from "./code-agent-hardware-runner.mjs";
import {
HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
HWLAB_M3_IO_CAPABILITY_LEVELS,
@@ -142,13 +149,21 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
try {
const message = normalizeUserMessage(params.message);
const runnerIntent = detectReadOnlyRunnerIntent(message);
const runnerIntent = detectReadOnlyRunnerIntent(message, {
params,
env: options.env ?? process.env,
hardwareTopology: options.hardwareCapabilityTopology
});
traceRecorder.append({
type: "request",
status: "accepted",
label: "request:accepted",
promptSummary: message.length > 160 ? `${message.slice(0, 157)}...` : message,
waitingFor: runnerIntent.kind === "m3_io" ? HWLAB_M3_IO_API_ROUTE : "codex-stdio-readiness"
waitingFor: runnerIntent.kind === "m3_io"
? HWLAB_M3_IO_API_ROUTE
: runnerIntent.kind === "hardware_shell"
? HARDWARE_INVOKE_SHELL_METHOD
: "codex-stdio-readiness"
});
const securityIntent = runnerIntent.kind === "security" ? runnerIntent : null;
@@ -213,6 +228,22 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
return completedRunnerPayload({ base, runnerResult, messageId, now: options.now, sessionRegistry });
}
if (runnerIntent.kind === "hardware_shell") {
const runnerResult = await callCodeAgentHardwareRunner({
intent: runnerIntent,
conversationId,
sessionId: requestedSessionId,
traceId,
now: options.now,
workspace: options.workspace,
sessionRegistry,
gatewayRegistry: options.gatewayRegistry,
invokeHardwareShellRpc: options.invokeHardwareShellRpc,
traceRecorder
});
return completedRunnerPayload({ base, runnerResult, messageId, now: options.now, sessionRegistry });
}
if (runnerIntent.kind === "session_context") {
traceRecorder.append({
type: "session",
@@ -412,6 +443,14 @@ function completedRunnerPayload({ base, runnerResult, messageId, now, sessionReg
capabilityLevel: runnerResult.capabilityLevel,
...(runnerResult.responseType ? { responseType: runnerResult.responseType } : {}),
...(runnerResult.m3Io ? { m3Io: runnerResult.m3Io } : {}),
...(runnerResult.hardware ? { hardware: runnerResult.hardware } : {}),
...(runnerResult.operationId !== undefined ? { operationId: runnerResult.operationId } : {}),
...(runnerResult.dispatchStatus !== undefined ? { dispatchStatus: runnerResult.dispatchStatus } : {}),
...(runnerResult.exitCode !== undefined ? { exitCode: runnerResult.exitCode } : {}),
...(runnerResult.stdoutSummary !== undefined ? { stdoutSummary: runnerResult.stdoutSummary } : {}),
...(runnerResult.stderrSummary !== undefined ? { stderrSummary: runnerResult.stderrSummary } : {}),
...(runnerResult.route !== undefined ? { route: runnerResult.route } : {}),
...(runnerResult.toolName !== undefined ? { toolName: runnerResult.toolName } : {}),
reply: {
messageId,
role: "assistant",
@@ -2176,7 +2215,7 @@ function m3IoSkillMissingApiBaseUrlResult({ traceId, requestId, actorId, blocker
};
}
function detectReadOnlyRunnerIntent(message) {
function detectReadOnlyRunnerIntent(message, options = {}) {
const text = String(message ?? "").trim();
if (isSecretReadRequest(text)) {
@@ -2186,6 +2225,12 @@ function detectReadOnlyRunnerIntent(message) {
reason: "Code Agent 安全边界不读取或输出 secret、token、kubeconfig、密码、私钥或环境变量原文。"
};
}
const hardwareIntent = detectHardwareCapabilityIntent(text, {
params: options.params,
env: options.env,
topology: options.hardwareTopology
});
if (hardwareIntent) return hardwareIntent;
const m3IoIntent = detectM3IoIntent(text);
if (m3IoIntent) {
return m3IoIntent;
@@ -2716,6 +2761,28 @@ function structuredCompletionBlocker(result, context = {}) {
blockers: result.m3Io?.blockers ?? result.skills?.blockers
});
}
const hardwareTool = Array.isArray(result.toolCalls)
? result.toolCalls.find((toolCall) => toolCall?.name === HARDWARE_INVOKE_SHELL_METHOD)
: null;
if ((result.provider === CODE_AGENT_HARDWARE_PROVIDER || hardwareTool || result.hardware?.type === "hardware_capability_blocker") && result.capabilityLevel === CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED) {
const primary = result.hardware?.blocker ?? hardwareTool?.blocker ?? result.blocker ?? result.blockers?.[0] ?? result.runnerTrace?.blocker ?? null;
return structuredBlocker({
code: primary?.code ?? "hardware_capability_blocked",
layer: primary?.layer ?? "hardware-capability",
message: primary?.message ?? primary?.summary ?? "Registered hardware capability route is blocked.",
userMessage: primary?.userMessage ?? "已登记硬件 capability 路由当前受阻,本次未进入 Codex stdio 或文本 fallback。",
retryable: primary?.retryable ?? false,
traceId: context.traceId,
provider: result.provider,
backend: result.backend,
runner: result.runner,
capabilityLevel: result.capabilityLevel,
route: HARDWARE_INVOKE_SHELL_METHOD,
toolName: HARDWARE_INVOKE_SHELL_METHOD,
category: primary?.category ?? "hardware_capability_blocked",
blockers: result.blockers ?? result.hardware?.blockers ?? result.skills?.blockers
});
}
return null;
}
@@ -3380,12 +3447,14 @@ function sanitizeErrorField(key, 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;
if (String(code).startsWith("hardware_")) return HARDWARE_INVOKE_SHELL_METHOD;
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;
if (String(code).startsWith("hardware_")) return HARDWARE_INVOKE_SHELL_METHOD;
return null;
}
File diff suppressed because it is too large Load Diff
@@ -16,6 +16,11 @@ import {
classifyCodeAgentChatReadiness,
classifyCodexRunnerCapability
} from "../../scripts/src/code-agent-response-contract.mjs";
import {
createCodeAgentHardwareInvokeShellRpc
} from "./code-agent-hardware-runner.mjs";
import { createGatewayDemoRegistry } from "./gateway-demo-registry.mjs";
import { handleJsonRpcRequest } from "./json-rpc.mjs";
import {
HWLAB_M3_IO_API_BASE_URL_ENV,
HWLAB_M3_IO_CAPABILITY_LEVELS,
@@ -1820,10 +1825,296 @@ test("Code Agent blocks direct gateway or patch-panel requests instead of using
assert.match(direct.error.message, /Skill CLI -> HWLAB API \/v1\/m3\/io/u);
});
test("Code Agent routes explicit registered PC gateway shell.exec through controlled hardware runner", async () => {
let providerCalled = false;
let dispatchCall = null;
const registry = createCodeAgentSessionRegistry({
idFactory: () => "ses_pc_gateway_shell"
});
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_pc_gateway_shell",
traceId: "trc_pc_gateway_shell",
projectId: "prj_mvp_topology",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
message: "请通过已登记 PC gateway shell.exec 执行 Windows cmdcmd /c echo HWLAB-PC-GW && hostname"
},
{
now: () => "2026-05-23T00:08:00.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
},
sessionRegistry: registry,
callProvider: async () => {
providerCalled = true;
throw new Error("hardware route must not use text fallback");
},
codexStdioManager: {
describe() {
throw new Error("hardware route must not inspect Codex stdio fallback");
},
async probe() {
throw new Error("hardware route must not probe Codex stdio fallback");
},
async chat() {
throw new Error("hardware route must not call Codex stdio fallback");
},
cancel() {},
reapIdle() {}
},
invokeHardwareShellRpc: async (call) => {
dispatchCall = call;
assert.equal(call.serviceId, "hwlab-cloud-api");
assert.equal(call.method, "hardware.invoke.shell");
assert.equal(call.params.projectId, "prj_mvp_topology");
assert.equal(call.params.gatewaySessionId, "gws_DESKTOP-1MHOD9I");
assert.equal(call.params.resourceId, "res_windows_host");
assert.equal(call.params.capabilityId, "cap_windows_cmd_exec");
assert.equal(call.params.input.command, "cmd /c echo HWLAB-PC-GW && hostname");
return {
jsonrpc: "2.0",
id: call.requestId,
result: {
accepted: true,
status: "succeeded",
operationId: "op_pc_gateway_shell",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
dispatch: {
shellExecuted: true,
dispatchStatus: "succeeded",
exitCode: 0,
stdout: "HWLAB-PC-GW\r\nDESKTOP-1MHOD9I\r\n",
stderr: ""
}
}
};
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-hardware-capability");
assert.equal(payload.backend, "hwlab-cloud-api/hardware.invoke.shell");
assert.equal(payload.capabilityLevel, "pc-gateway-shell-executed");
assert.equal(payload.toolCalls[0].name, "hardware.invoke.shell");
assert.equal(payload.toolCalls[0].status, "completed");
assert.equal(payload.toolCalls[0].dispatchStatus, "succeeded");
assert.equal(payload.toolCalls[0].exitCode, 0);
assert.equal(payload.toolCalls[0].operationId, "op_pc_gateway_shell");
assert.match(payload.toolCalls[0].stdoutSummary, /HWLAB-PC-GW/u);
assert.match(payload.toolCalls[0].stdoutSummary, /DESKTOP-1MHOD9I/u);
assert.equal(payload.operationId, "op_pc_gateway_shell");
assert.equal(payload.dispatchStatus, "succeeded");
assert.equal(payload.exitCode, 0);
assert.match(payload.stdoutSummary, /DESKTOP-1MHOD9I/u);
assert.equal(payload.runnerTrace.eventLabels.includes("tool:hardware.invoke.shell:completed"), true);
assert.equal(payload.runnerTrace.directGatewayCalls, false);
assert.equal(payload.providerTrace.serviceId, "hwlab-cloud-api");
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(providerCalled, false);
assert.equal(dispatchCall !== null, true);
});
test("Code Agent returns structured hardware blocker for ambiguous PC gateway topology without fallback", async () => {
let dispatchCalled = false;
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_pc_gateway_ambiguous",
traceId: "trc_pc_gateway_ambiguous",
message: "请通过 PC gateway shell.exec 执行 Windows cmdcmd /c echo SHOULD_NOT_RUN"
},
{
now: () => "2026-05-23T00:08:30.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
},
hardwareCapabilityTopology: [
{
projectId: "prj_mvp_topology",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
capabilityName: "shell.exec"
},
{
projectId: "prj_mvp_topology",
gatewaySessionId: "gws_DESKTOP-SECOND",
resourceId: "res_windows_host_2",
capabilityId: "cap_windows_cmd_exec",
capabilityName: "shell.exec"
}
],
callProvider: async () => {
throw new Error("ambiguous hardware route must not use text fallback");
},
codexStdioManager: {
describe() {
throw new Error("ambiguous hardware route must not inspect Codex stdio fallback");
},
async probe() {
throw new Error("ambiguous hardware route must not probe Codex stdio fallback");
},
async chat() {
throw new Error("ambiguous hardware route must not call Codex stdio fallback");
},
cancel() {},
reapIdle() {}
},
invokeHardwareShellRpc: async () => {
dispatchCalled = true;
throw new Error("ambiguous hardware route must not dispatch");
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-hardware-capability");
assert.equal(payload.capabilityLevel, "blocked");
assert.equal(payload.blocker.code, "hardware_topology_ambiguous");
assert.equal(payload.blocker.route, "hardware.invoke.shell");
assert.equal(payload.blocker.toolName, "hardware.invoke.shell");
assert.equal(payload.toolCalls[0].name, "hardware.invoke.shell");
assert.equal(payload.toolCalls[0].status, "blocked");
assert.equal(payload.toolCalls[0].blocker.code, "hardware_topology_ambiguous");
assert.equal(payload.dispatchStatus, null);
assert.equal(payload.exitCode, null);
assert.match(payload.reply.content, /拓扑不唯一/u);
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(dispatchCalled, false);
});
test("Code Agent PC gateway route uses cloud-api internal JSON-RPC dispatch and gateway result", async () => {
const gatewayRegistry = createGatewayDemoRegistry({
staleMs: 30000,
dispatchTimeoutMs: 1000
});
gatewayRegistry.updateSession({
projectId: "prj_mvp_topology",
gatewayId: "gtw_DESKTOP-1MHOD9I",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
resourceId: "res_windows_host",
capabilities: [{
projectId: "prj_mvp_topology",
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
name: "shell.exec"
}]
});
const registry = createCodeAgentSessionRegistry({
idFactory: () => "ses_pc_gateway_internal_rpc"
});
const chat = handleCodeAgentChat(
{
conversationId: "cnv_pc_gateway_internal_rpc",
traceId: "trc_pc_gateway_internal_rpc",
projectId: "prj_mvp_topology",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
message: "请通过 PC gateway shell.exec 执行 Windows cmdcmd /c echo HWLAB-INTERNAL-RPC && hostname"
},
{
now: () => "2026-05-23T00:08:45.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_WORKSPACE: process.cwd()
},
sessionRegistry: registry,
gatewayRegistry,
invokeHardwareShellRpc: createCodeAgentHardwareInvokeShellRpc({
handleJsonRpcRequest,
gatewayRegistry
}),
callProvider: async () => {
throw new Error("internal hardware route must not use text fallback");
},
codexStdioManager: {
describe() {
throw new Error("internal hardware route must not inspect Codex stdio fallback");
},
async probe() {
throw new Error("internal hardware route must not probe Codex stdio fallback");
},
async chat() {
throw new Error("internal hardware route must not call Codex stdio fallback");
},
cancel() {},
reapIdle() {}
}
}
);
const outbound = await waitForGatewayRequest(gatewayRegistry, "gws_DESKTOP-1MHOD9I");
assert.equal(outbound.method, "hardware.invoke.shell");
assert.equal(outbound.meta.serviceId, "hwlab-cloud-api");
assert.equal(outbound.params.input.command, "cmd /c echo HWLAB-INTERNAL-RPC && hostname");
const completion = gatewayRegistry.complete({
response: {
jsonrpc: "2.0",
id: outbound.id,
result: {
accepted: true,
status: "succeeded",
operationId: "op_pc_gateway_internal_rpc",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
shellExecuted: true,
dispatchStatus: "succeeded",
exitCode: 0,
stdout: "HWLAB-INTERNAL-RPC\r\nDESKTOP-1MHOD9I\r\n",
stderr: "",
auditId: "aud_pc_gateway_internal_rpc",
evidenceId: "evd_pc_gateway_internal_rpc",
gateway: {
gatewayId: "gtw_DESKTOP-1MHOD9I",
gatewaySessionId: "gws_DESKTOP-1MHOD9I"
}
}
}
});
assert.equal(completion.accepted, true);
const payload = await chat;
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-hardware-capability");
assert.equal(payload.toolCalls[0].name, "hardware.invoke.shell");
assert.equal(payload.toolCalls[0].status, "completed");
assert.equal(payload.toolCalls[0].dispatchStatus, "succeeded");
assert.equal(payload.toolCalls[0].exitCode, 0);
assert.match(payload.toolCalls[0].stdoutSummary, /HWLAB-INTERNAL-RPC/u);
assert.match(payload.toolCalls[0].stdoutSummary, /DESKTOP-1MHOD9I/u);
assert.equal(payload.operationId, "op_pc_gateway_internal_rpc");
assert.equal(payload.hardware.path.cloudApiOnly, true);
assert.equal(payload.runnerTrace.eventLabels.includes("tool:hardware.invoke.shell:completed"), true);
assert.equal(payload.providerTrace.transport, "internal-json-rpc");
assert.equal(payload.providerTrace.serviceId, "hwlab-cloud-api");
});
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForGatewayRequest(gatewayRegistry, gatewaySessionId) {
for (let attempt = 0; attempt < 40; attempt += 1) {
const outbound = gatewayRegistry.nextRequest(gatewaySessionId);
if (outbound) return outbound;
await delay(5);
}
throw new Error(`gateway request was not queued for ${gatewaySessionId}`);
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}
+30
View File
@@ -96,6 +96,36 @@ test("system.health includes the redacted DEV DB env contract", async () => {
}
});
test("JSON-RPC invalid meta reports unknown serviceId reason without leaking secret material", async () => {
const response = await handleJsonRpcRequest({
jsonrpc: "2.0",
id: "req_01J00000000000000000000003",
method: "hardware.invoke.shell",
params: {
projectId: "prj_mvp_topology",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
input: {
command: "cmd /c echo should-not-dispatch"
}
},
meta: {
traceId: "trc_01J00000000000000000000003",
actorId: "usr_01J00000000000000000000003",
serviceId: "hwlab-code-agent-hotfix",
environment: "dev"
}
});
validateResponse(response);
assert.equal(response.error.code, ERROR_CODES.invalidRequest);
assert.equal(response.error.message, "Invalid JSON-RPC request");
assert.match(response.error.data.reason, /unknown serviceId "hwlab-code-agent-hotfix"/u);
assert.equal(JSON.stringify(response).includes("should-not-dispatch"), false);
assert.equal(JSON.stringify(response).includes("sk-"), false);
});
test("cloud-api runtime accepts register/report/invoke and exposes audit/evidence records", async () => {
const runtimeStore = createCloudRuntimeStore({
now: () => "2026-05-21T00:00:00.000Z"
+11 -1
View File
@@ -26,6 +26,7 @@ import {
describeCodeAgentAvailability,
handleCodeAgentChat
} from "./code-agent-chat.mjs";
import { createCodeAgentHardwareInvokeShellRpc } from "./code-agent-hardware-runner.mjs";
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.mjs";
import { createCodeAgentSessionRegistry } from "./code-agent-session-registry.mjs";
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.mjs";
@@ -1256,7 +1257,16 @@ async function handleCodeAgentChatHttp(request, response, options) {
sessionRegistry: options.sessionRegistry,
m3IoSkillRequestJson: options.m3IoSkillRequestJson ?? createCodeAgentM3HwlabApiRequestJson(options),
codexStdioManager: options.codexStdioManager,
traceStore: options.traceStore
traceStore: options.traceStore,
gatewayRegistry: options.gatewayRegistry,
hardwareCapabilityTopology: options.hardwareCapabilityTopology,
invokeHardwareShellRpc: options.invokeHardwareShellRpc ?? createCodeAgentHardwareInvokeShellRpc({
handleJsonRpcRequest,
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe,
gatewayRegistry: options.gatewayRegistry
})
}
);
+42
View File
@@ -1936,6 +1936,48 @@ test("cloud api /v1/agent/chat refuses provider stub when Codex stdio is unavail
}
});
test("cloud api REST JSON-RPC bridge exposes unknown serviceId reason for Code Agent internal calls", async () => {
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio"
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/rpc/hardware.invoke.shell`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": "trc_server-test-invalid-service-id",
"x-source-service-id": "hwlab-code-agent-hotfix"
},
body: JSON.stringify({
projectId: "prj_mvp_topology",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
input: {
command: "cmd /c echo should-not-dispatch"
}
})
});
assert.equal(response.status, 400);
const payload = await response.json();
assert.equal(payload.error.code, -32600);
assert.equal(payload.error.message, "Invalid JSON-RPC request");
assert.match(payload.error.data.reason, /unknown serviceId "hwlab-code-agent-hotfix"/u);
assert.equal(JSON.stringify(payload).includes("should-not-dispatch"), false);
assert.equal(JSON.stringify(payload).includes("sk-"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("OpenAI fallback text chat is not used when Codex stdio is unavailable", async () => {
let providerCalled = false;
const server = createCloudApiServer({
+1 -1
View File
File diff suppressed because one or more lines are too long