Merge pull request #468 from pikasTech/fix/code-agent-pc-gateway-capability
源码化 Code Agent PC gateway 能力路由
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 cmd:cmd /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 cmd:cmd /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 cmd:cmd /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, "\\$&");
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user