diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index 26f7ea34..d3dc8e12 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -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 diff --git a/internal/cloud/code-agent-chat.mjs b/internal/cloud/code-agent-chat.mjs index ebe9d546..e9f21354 100644 --- a/internal/cloud/code-agent-chat.mjs +++ b/internal/cloud/code-agent-chat.mjs @@ -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; } diff --git a/internal/cloud/code-agent-hardware-runner.mjs b/internal/cloud/code-agent-hardware-runner.mjs new file mode 100644 index 00000000..8f2bac82 --- /dev/null +++ b/internal/cloud/code-agent-hardware-runner.mjs @@ -0,0 +1,1182 @@ +import { randomUUID } from "node:crypto"; + +import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs"; + +export const HARDWARE_INVOKE_SHELL_METHOD = "hardware.invoke.shell"; +export const CODE_AGENT_HARDWARE_PROVIDER = "hwlab-hardware-capability"; +export const CODE_AGENT_HARDWARE_MODEL = "controlled-hardware-capability"; +export const CODE_AGENT_HARDWARE_BACKEND = "hwlab-cloud-api/hardware.invoke.shell"; +export const CODE_AGENT_HARDWARE_RUNNER_KIND = "hwlab-hardware-capability-runner"; +export const CODE_AGENT_HARDWARE_SANDBOX = "cloud-api-capability-route-only"; +export const CODE_AGENT_HARDWARE_SESSION_MODE = "controlled-hardware-capability-route"; +export const CODE_AGENT_HARDWARE_IMPLEMENTATION_TYPE = "cloud-api-hardware-capability-runner"; +export const CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED = "pc-gateway-shell-executed"; +export const CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED = "blocked"; + +const OUTPUT_LIMIT = 4000; +const COMMAND_SUMMARY_LIMIT = 320; +const DEFAULT_DEV_MVP_PC_GATEWAY_TOPOLOGY = Object.freeze([Object.freeze({ + projectId: "prj_mvp_topology", + gatewaySessionId: "gws_DESKTOP-1MHOD9I", + resourceId: "res_windows_host", + capabilityId: "cap_windows_cmd_exec", + capabilityName: "shell.exec", + gatewayLabel: "DESKTOP-1MHOD9I", + topologySource: "dev-mvp-pc-gateway-topology" +})]); + +const HARDWARE_LIMITATION_FLAGS = Object.freeze([ + "cloud-api-hardware-capability-only", + "no-direct-gateway-link", + "no-direct-box-link", + "no-direct-patch-panel-link", + "no-openai-text-fallback", + "registered-capability-required" +]); + +export function detectHardwareCapabilityIntent(message, { params = {}, env = process.env, topology } = {}) { + const text = String(message ?? "").trim(); + if (!mentionsPcGatewayShellCapability(text, params)) return null; + + const command = normalizeWindowsCommand( + params.input?.command ?? + params.command ?? + extractWindowsCommand(text) + ); + const explicit = { + projectId: cleanProtocolId(params.projectId, "prj") ?? extractProtocolField(text, "projectId", "prj"), + gatewaySessionId: cleanProtocolId(params.gatewaySessionId, "gws") ?? extractProtocolField(text, "gatewaySessionId", "gws") ?? extractProtocolField(text, "session", "gws"), + resourceId: cleanProtocolId(params.resourceId, "res") ?? extractProtocolField(text, "resourceId", "res"), + capabilityId: cleanProtocolId(params.capabilityId, "cap") ?? extractProtocolField(text, "capabilityId", "cap") + }; + const resolution = resolveHardwareTopology(explicit, { topology, env }); + + if (resolution.blocker) { + return { + kind: "hardware_shell", + toolName: HARDWARE_INVOKE_SHELL_METHOD, + action: "shell.exec", + command, + request: null, + topology: resolution, + blocker: resolution.blocker + }; + } + + if (!command) { + return { + kind: "hardware_shell", + toolName: HARDWARE_INVOKE_SHELL_METHOD, + action: "shell.exec", + request: resolution.request, + topology: resolution, + blocker: hardwareBlocker({ + code: "hardware_command_missing", + layer: "intent", + category: "needs_command", + summary: "PC gateway shell.exec request did not include a Windows cmd command.", + userMessage: "PC gateway shell.exec 请求缺少 Windows cmd 命令;请明确给出 cmd /c ...。", + retryable: false + }) + }; + } + + if (!isAllowedWindowsCmd(command)) { + return { + kind: "hardware_shell", + toolName: HARDWARE_INVOKE_SHELL_METHOD, + action: "shell.exec", + command, + request: resolution.request, + topology: resolution, + blocker: hardwareBlocker({ + code: "hardware_command_unsupported", + layer: "security", + category: "security_boundary", + summary: "Only explicit Windows cmd /c commands are allowed for the registered PC gateway shell.exec capability.", + userMessage: "该能力只允许已登记 PC gateway 的 Windows cmd /c 命令;不会执行 bash、sh、kubectl、PowerShell 或任意 shell。", + retryable: false + }) + }; + } + + return { + kind: "hardware_shell", + toolName: HARDWARE_INVOKE_SHELL_METHOD, + action: "shell.exec", + command, + request: resolution.request, + topology: resolution + }; +} + +export async function callCodeAgentHardwareRunner({ + intent, + conversationId, + sessionId, + traceId, + now, + workspace, + sessionRegistry, + gatewayRegistry, + invokeHardwareShellRpc, + traceRecorder +} = {}) { + const startedAt = nowIso(now); + const resolvedWorkspace = workspace ?? process.cwd(); + const request = intent?.request ?? null; + const requestId = `req_code_agent_hw_shell_${randomUUID()}`; + const actorId = "usr_code_agent"; + const acquire = acquireHardwareSession(sessionRegistry, { + conversationId, + sessionId, + traceId, + workspace: resolvedWorkspace, + now + }); + let session = acquire.session; + const preflightBlocker = intent?.blocker ?? acquire.blocker ?? hardwareRegistryBlocker({ request, gatewayRegistry, traceId }); + + traceRecorder?.append({ + type: "tool_call", + status: "started", + label: "tool:hardware.invoke.shell:started", + toolName: HARDWARE_INVOKE_SHELL_METHOD, + waitingFor: HARDWARE_INVOKE_SHELL_METHOD + }); + + if (preflightBlocker) { + session = releaseHardwareSession(sessionRegistry, session, { now, traceId, conversationId }); + const finishedAt = nowIso(now); + const runnerResult = hardwareBlockedRunnerResult({ + request, + command: intent?.command, + blocker: preflightBlocker, + traceId, + requestId, + actorId, + startedAt, + finishedAt, + workspace: resolvedWorkspace, + session, + topology: intent?.topology + }); + appendHardwareTraceCompletion(traceRecorder, runnerResult); + return runnerResult; + } + + if (typeof invokeHardwareShellRpc !== "function") { + session = releaseHardwareSession(sessionRegistry, session, { now, traceId, conversationId }); + const finishedAt = nowIso(now); + const runnerResult = hardwareBlockedRunnerResult({ + request, + command: intent?.command, + blocker: hardwareBlocker({ + code: "hardware_dispatch_unavailable", + layer: "dispatch", + category: "needs_config", + summary: "Code Agent hardware runner has no cloud-api JSON-RPC dispatcher.", + userMessage: "Code Agent 硬件能力 runner 缺少 cloud-api 内部 dispatch 入口;本次未执行。", + retryable: false, + traceId + }), + traceId, + requestId, + actorId, + startedAt, + finishedAt, + workspace: resolvedWorkspace, + session, + topology: intent?.topology + }); + appendHardwareTraceCompletion(traceRecorder, runnerResult); + return runnerResult; + } + + let rpcResponse; + try { + rpcResponse = await invokeHardwareShellRpc({ + method: HARDWARE_INVOKE_SHELL_METHOD, + params: { + projectId: request.projectId, + gatewaySessionId: request.gatewaySessionId, + resourceId: request.resourceId, + capabilityId: request.capabilityId, + input: { + command: intent.command + } + }, + traceId, + requestId, + actorId, + serviceId: CLOUD_API_SERVICE_ID + }); + } catch (error) { + rpcResponse = { + error: { + code: "hardware_dispatch_exception", + message: "hardware.invoke.shell dispatch threw before returning JSON-RPC", + data: { + reason: error?.message ?? "dispatch exception" + } + } + }; + } + + session = releaseHardwareSession(sessionRegistry, session, { now, traceId, conversationId }); + const finishedAt = nowIso(now); + const runnerResult = hardwareRunnerResultFromRpc({ + request, + command: intent.command, + rpcResponse, + traceId, + requestId, + actorId, + startedAt, + finishedAt, + workspace: resolvedWorkspace, + session, + topology: intent?.topology + }); + appendHardwareTraceCompletion(traceRecorder, runnerResult); + return runnerResult; +} + +export function createCodeAgentHardwareInvokeShellRpc({ handleJsonRpcRequest, runtimeStore, env, dbProbe, gatewayRegistry } = {}) { + if (typeof handleJsonRpcRequest !== "function") return null; + return async ({ method = HARDWARE_INVOKE_SHELL_METHOD, params = {}, traceId, requestId, actorId, serviceId = CLOUD_API_SERVICE_ID } = {}) => handleJsonRpcRequest( + { + jsonrpc: "2.0", + id: requestId || `req_code_agent_hw_shell_${randomUUID()}`, + method, + params, + meta: { + traceId: traceId || `trc_code_agent_hw_shell_${randomUUID()}`, + actorId, + serviceId, + environment: "dev" + } + }, + { + adapter: "code-agent-internal", + runtimeStore, + env, + dbProbe, + gatewayRegistry + } + ); +} + +export function codeAgentHardwareRunnerDescriptor({ workspace, session = null, capabilityLevel = CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED } = {}) { + return { + kind: CODE_AGENT_HARDWARE_RUNNER_KIND, + provider: CODE_AGENT_HARDWARE_PROVIDER, + backend: CODE_AGENT_HARDWARE_BACKEND, + workspace: workspace ?? process.cwd(), + sandbox: CODE_AGENT_HARDWARE_SANDBOX, + session: CODE_AGENT_HARDWARE_SESSION_MODE, + sessionMode: CODE_AGENT_HARDWARE_SESSION_MODE, + sessionId: session?.sessionId ?? null, + turn: session?.turn ?? null, + sessionReused: session?.reused ?? false, + implementationType: CODE_AGENT_HARDWARE_IMPLEMENTATION_TYPE, + codexStdio: false, + longLivedSession: false, + durableSession: false, + writeCapable: true, + readOnly: false, + capabilityLevel, + toolPolicy: { + allowed: [`internal-json-rpc ${HARDWARE_INVOKE_SHELL_METHOD}`], + blocked: ["direct-gateway-link", "direct-box-link", "direct-patch-panel-link", "openai-text-fallback", "unregistered-shell"] + }, + runnerLimitations: [...HARDWARE_LIMITATION_FLAGS], + safety: { + secretsRead: false, + secretValuesPrinted: false, + kubeconfigRead: false, + cloudApiRouteOnly: true, + sourceServiceId: CLOUD_API_SERVICE_ID, + directGatewayCallsAllowed: false, + directBoxCallsAllowed: false, + directPatchPanelCallsAllowed: false, + openAiFallbackAllowed: false, + outputLimitBytes: OUTPUT_LIMIT + } + }; +} + +function hardwareRunnerResultFromRpc({ + request, + command, + rpcResponse, + traceId, + requestId, + actorId, + startedAt, + finishedAt, + workspace, + session, + topology +}) { + const rpcError = rpcResponse?.error ?? null; + if (rpcError) { + const reason = rpcError.data?.reason ?? rpcError.message ?? "JSON-RPC dispatch failed"; + return hardwareBlockedRunnerResult({ + request, + command, + blocker: hardwareBlocker({ + code: String(reason).includes("unknown serviceId") ? "hardware_rpc_service_id_invalid" : "hardware_json_rpc_invalid", + layer: "json-rpc", + category: "json_rpc_meta", + summary: reason, + userMessage: String(reason).includes("unknown serviceId") + ? "cloud-api 内部 JSON-RPC serviceId 不在冻结列表中,本次未执行硬件能力。" + : "cloud-api 内部 JSON-RPC 请求未通过协议校验,本次未执行硬件能力。", + retryable: false, + traceId, + reason + }), + traceId, + requestId, + actorId, + startedAt, + finishedAt, + workspace, + session, + topology, + rpcError + }); + } + + const result = rpcResponse?.result ?? {}; + const dispatch = result.dispatch ?? {}; + const dispatchStatus = dispatch.dispatchStatus ?? result.status ?? "unknown"; + const exitCode = Number.isInteger(dispatch.exitCode) ? dispatch.exitCode : null; + const stdout = safeOutput(dispatch.stdout ?? dispatch.output?.stdout ?? ""); + const stderr = safeOutput(dispatch.stderr ?? dispatch.output?.stderr ?? ""); + const shellExecuted = dispatch.shellExecuted === true; + const succeeded = shellExecuted && exitCode === 0 && ["succeeded", "completed"].includes(String(dispatchStatus).toLowerCase()); + const operationId = result.operationId ?? dispatch.operationId ?? null; + + if (!succeeded) { + return hardwareBlockedRunnerResult({ + request, + command, + blocker: hardwareFailureBlocker({ dispatchStatus, exitCode, result, dispatch, traceId }), + traceId, + requestId, + actorId, + startedAt, + finishedAt, + workspace, + session, + topology, + result, + dispatch, + operationId, + dispatchStatus, + exitCode, + stdout, + stderr + }); + } + + const stdoutSummary = summarizeOutput(stdout); + const stderrSummary = summarizeOutput(stderr); + const toolCall = hardwareToolCall({ + request, + command, + status: "completed", + traceId, + requestId, + actorId, + operationId, + dispatchStatus, + exitCode, + stdout, + stderr, + stdoutSummary, + stderrSummary, + capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED, + startedAt, + finishedAt + }); + const runnerTrace = hardwareRunnerTrace({ + traceId, + startedAt, + finishedAt, + workspace, + session, + request, + operationId, + dispatchStatus, + exitCode, + status: "completed", + capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED, + events: ["request:accepted", "runner:hardware-capability:selected", "tool:hardware.invoke.shell:started", "tool:hardware.invoke.shell:completed"] + }); + + return { + provider: CODE_AGENT_HARDWARE_PROVIDER, + model: CODE_AGENT_HARDWARE_MODEL, + backend: CODE_AGENT_HARDWARE_BACKEND, + content: hardwareReply({ request, operationId, dispatchStatus, exitCode, stdoutSummary, stderrSummary, blocked: false }), + workspace, + sandbox: CODE_AGENT_HARDWARE_SANDBOX, + session, + sessionMode: CODE_AGENT_HARDWARE_SESSION_MODE, + sessionReuse: session ? sessionReuseEvidence(session) : null, + implementationType: CODE_AGENT_HARDWARE_IMPLEMENTATION_TYPE, + runnerLimitations: [...HARDWARE_LIMITATION_FLAGS], + codexStdioFeasibility: skippedCodexStdioFeasibility({ workspace, reason: "hardware_capability_deterministic_route" }), + longLivedSessionGate: hardwareLongLivedGate(session), + toolCalls: [toolCall], + skills: hardwareSkills({ status: "used", blockers: [] }), + runner: codeAgentHardwareRunnerDescriptor({ workspace, session, capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED }), + runnerTrace, + capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED, + blockers: [], + route: HARDWARE_INVOKE_SHELL_METHOD, + toolName: HARDWARE_INVOKE_SHELL_METHOD, + operationId, + dispatchStatus, + exitCode, + stdoutSummary, + stderrSummary, + hardware: hardwareStructuredResult({ request, command, result, dispatch, operationId, dispatchStatus, exitCode, stdoutSummary, stderrSummary, topology }), + providerTrace: { + runnerKind: CODE_AGENT_HARDWARE_RUNNER_KIND, + codexStdio: false, + transport: "internal-json-rpc", + serviceId: CLOUD_API_SERVICE_ID, + method: HARDWARE_INVOKE_SHELL_METHOD, + traceId, + operationId, + dispatchStatus, + exitCode, + capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED, + fallbackUsed: false + } + }; +} + +function hardwareBlockedRunnerResult({ + request, + command, + blocker, + traceId, + requestId, + actorId, + startedAt, + finishedAt, + workspace, + session, + topology, + result = null, + dispatch = {}, + operationId = null, + dispatchStatus = null, + exitCode = null, + stdout = "", + stderr = "", + rpcError = null +}) { + const normalizedBlocker = { + ...blocker, + traceId, + route: HARDWARE_INVOKE_SHELL_METHOD, + toolName: HARDWARE_INVOKE_SHELL_METHOD + }; + const stdoutSummary = summarizeOutput(stdout); + const stderrSummary = summarizeOutput(stderr || normalizedBlocker.summary || normalizedBlocker.code); + const toolCall = hardwareToolCall({ + request, + command, + status: "blocked", + traceId, + requestId, + actorId, + operationId, + dispatchStatus, + exitCode, + stdout, + stderr, + stdoutSummary, + stderrSummary, + capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED, + blocker: normalizedBlocker, + startedAt, + finishedAt + }); + const runnerTrace = hardwareRunnerTrace({ + traceId, + startedAt, + finishedAt, + workspace, + session, + request, + operationId, + dispatchStatus, + exitCode, + status: "blocked", + capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED, + blocker: normalizedBlocker, + events: ["request:accepted", "runner:hardware-capability:selected", "tool:hardware.invoke.shell:blocked", `blocker:${normalizedBlocker.code}`] + }); + + return { + provider: CODE_AGENT_HARDWARE_PROVIDER, + model: CODE_AGENT_HARDWARE_MODEL, + backend: CODE_AGENT_HARDWARE_BACKEND, + content: hardwareReply({ request, operationId, dispatchStatus, exitCode, stdoutSummary, stderrSummary, blocker: normalizedBlocker, blocked: true }), + workspace, + sandbox: CODE_AGENT_HARDWARE_SANDBOX, + session, + sessionMode: CODE_AGENT_HARDWARE_SESSION_MODE, + sessionReuse: session ? sessionReuseEvidence(session) : null, + implementationType: CODE_AGENT_HARDWARE_IMPLEMENTATION_TYPE, + runnerLimitations: [...HARDWARE_LIMITATION_FLAGS], + codexStdioFeasibility: skippedCodexStdioFeasibility({ workspace, reason: "hardware_capability_deterministic_route" }), + longLivedSessionGate: hardwareLongLivedGate(session), + toolCalls: [toolCall], + skills: hardwareSkills({ status: "blocked", blockers: [normalizedBlocker] }), + runner: codeAgentHardwareRunnerDescriptor({ workspace, session, capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED }), + runnerTrace, + capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED, + blockers: [normalizedBlocker], + blocker: normalizedBlocker, + route: HARDWARE_INVOKE_SHELL_METHOD, + toolName: HARDWARE_INVOKE_SHELL_METHOD, + operationId, + dispatchStatus, + exitCode, + stdoutSummary, + stderrSummary, + hardware: hardwareStructuredResult({ request, command, result, dispatch, operationId, dispatchStatus, exitCode, stdoutSummary, stderrSummary, topology, blocker: normalizedBlocker, rpcError }), + providerTrace: { + runnerKind: CODE_AGENT_HARDWARE_RUNNER_KIND, + codexStdio: false, + transport: "internal-json-rpc", + serviceId: CLOUD_API_SERVICE_ID, + method: HARDWARE_INVOKE_SHELL_METHOD, + traceId, + operationId, + dispatchStatus, + exitCode, + capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED, + fallbackUsed: false, + blocker: normalizedBlocker + } + }; +} + +function hardwareToolCall({ + request, + command, + status, + traceId, + requestId, + actorId, + operationId, + dispatchStatus, + exitCode, + stdout, + stderr, + stdoutSummary, + stderrSummary, + capabilityLevel, + blocker = null, + startedAt, + finishedAt +}) { + return { + id: `tool_${randomUUID()}`, + type: "cloud-api-rpc", + name: HARDWARE_INVOKE_SHELL_METHOD, + status, + method: HARDWARE_INVOKE_SHELL_METHOD, + route: HARDWARE_INVOKE_SHELL_METHOD, + serviceId: CLOUD_API_SERVICE_ID, + sourceServiceId: CLOUD_API_SERVICE_ID, + requestId, + actorId, + traceId, + projectId: request?.projectId ?? null, + gatewaySessionId: request?.gatewaySessionId ?? null, + resourceId: request?.resourceId ?? null, + capabilityId: request?.capabilityId ?? null, + capabilityName: "shell.exec", + command: command ? redactCommand(command) : null, + operationId, + dispatchStatus, + exitCode, + stdout: boundOutput(stdout).text, + stdoutSummary, + stderrSummary, + outputTruncated: boundOutput(stdout).truncated || boundOutput(stderr).truncated, + capabilityLevel, + blocker, + blockers: blocker ? [blocker] : [], + startedAt, + finishedAt, + safety: { + cloudApiRouteOnly: true, + directGatewayCalls: false, + directBoxCalls: false, + directPatchPanelCalls: false, + openAiFallbackUsed: false, + serviceIdFrozen: true + } + }; +} + +function hardwareRunnerTrace({ + traceId, + startedAt, + finishedAt, + workspace, + session, + request, + operationId, + dispatchStatus, + exitCode, + status, + capabilityLevel, + events, + blocker = null +}) { + return { + traceId, + runnerKind: CODE_AGENT_HARDWARE_RUNNER_KIND, + workspace, + sandbox: CODE_AGENT_HARDWARE_SANDBOX, + sessionMode: CODE_AGENT_HARDWARE_SESSION_MODE, + sessionId: session?.sessionId ?? null, + sessionStatus: session?.status ?? null, + turn: session?.turn ?? null, + sessionReused: session?.reused ?? false, + implementationType: CODE_AGENT_HARDWARE_IMPLEMENTATION_TYPE, + limitations: [...HARDWARE_LIMITATION_FLAGS], + startedAt, + finishedAt, + events, + eventLabels: events, + method: HARDWARE_INVOKE_SHELL_METHOD, + serviceId: CLOUD_API_SERVICE_ID, + projectId: request?.projectId ?? null, + gatewaySessionId: request?.gatewaySessionId ?? null, + resourceId: request?.resourceId ?? null, + capabilityId: request?.capabilityId ?? null, + operationId, + dispatchStatus, + exitCode, + accepted: status === "completed", + status, + capabilityLevel, + blocker, + blockers: blocker ? [blocker] : [], + outputTruncated: false, + valuesPrinted: false, + directGatewayCalls: false, + directBoxCalls: false, + directPatchPanelCalls: false, + fallbackUsed: false, + note: "Controlled hardware capability route uses cloud-api internal JSON-RPC hardware.invoke.shell with frozen serviceId; Code Agent does not call gateway directly." + }; +} + +function hardwareStructuredResult({ request, command, result, dispatch = {}, operationId, dispatchStatus, exitCode, stdoutSummary, stderrSummary, topology, blocker = null, rpcError = null }) { + return { + type: blocker ? "hardware_capability_blocker" : "hardware_capability_result", + status: blocker ? "blocked" : "succeeded", + method: HARDWARE_INVOKE_SHELL_METHOD, + serviceId: CLOUD_API_SERVICE_ID, + projectId: request?.projectId ?? null, + gatewaySessionId: request?.gatewaySessionId ?? null, + resourceId: request?.resourceId ?? null, + capabilityId: request?.capabilityId ?? null, + capabilityName: "shell.exec", + capabilityLevel: blocker ? CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED : CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED, + operationId, + dispatchStatus, + exitCode, + stdoutSummary, + stderrSummary, + command: command ? redactCommand(command) : null, + topology: { + source: topology?.source ?? null, + fallbackUsed: topology?.fallbackUsed === true, + candidates: topology?.candidateCount ?? null + }, + dispatch, + result, + rpcError: rpcError ? { + code: rpcError.code, + message: rpcError.message, + reason: rpcError.data?.reason ?? null + } : null, + blocker, + path: { + summary: "Code Agent -> cloud-api internal JSON-RPC -> hardware.invoke.shell -> registered PC gateway", + segments: ["Code Agent", "cloud-api", "hardware.invoke.shell", "PC gateway"], + cloudApiOnly: true, + directGatewayCalls: false, + directBoxCalls: false, + directPatchPanelCalls: false, + openAiFallbackUsed: false + } + }; +} + +function hardwareReply({ request, operationId, dispatchStatus, exitCode, stdoutSummary, stderrSummary, blocker = null, blocked }) { + const lines = []; + if (blocked) { + lines.push(`PC gateway shell.exec 阻塞:${blocker.userMessage ?? blocker.summary ?? blocker.code}。`); + lines.push(`Blocker: ${blocker.code};layer=${blocker.layer ?? "unknown"};reason=${blocker.reason ?? blocker.summary ?? blocker.code}。`); + } else { + lines.push("PC gateway shell.exec 已通过 cloud-api 受控能力路由返回。"); + } + lines.push(`路径:Code Agent -> cloud-api internal JSON-RPC -> hardware.invoke.shell;未直连 gateway/box/patch-panel,未使用 OpenAI text fallback。`); + lines.push(`目标:projectId=${request?.projectId ?? "null"};gatewaySessionId=${request?.gatewaySessionId ?? "null"};resourceId=${request?.resourceId ?? "null"};capabilityId=${request?.capabilityId ?? "null"}。`); + lines.push(`结果:operationId=${operationId ?? "null"};dispatchStatus=${dispatchStatus ?? "null"};exitCode=${exitCode ?? "null"}。`); + if (stdoutSummary) lines.push(`stdout 摘要:${stdoutSummary}`); + if (stderrSummary) lines.push(`stderr 摘要:${stderrSummary}`); + return boundOutput(lines.join("\n")).text; +} + +function hardwareFailureBlocker({ dispatchStatus, exitCode, result, dispatch, traceId }) { + const status = String(dispatchStatus ?? "").toLowerCase(); + if (status === "not_connected") { + return hardwareBlocker({ + code: "hardware_gateway_offline", + layer: "gateway", + category: "gateway_offline", + summary: dispatch.message ?? "gateway is not connected", + userMessage: "目标 PC gateway 当前不在线或已过期,本次未执行。", + retryable: true, + traceId + }); + } + if (status === "timed_out") { + return hardwareBlocker({ + code: "hardware_dispatch_failed", + layer: "dispatch", + category: "timeout", + summary: dispatch.message ?? "gateway dispatch timed out", + userMessage: "cloud-api 已派发到 gateway,但等待回传超时。", + retryable: true, + traceId + }); + } + if (Number.isInteger(exitCode) && exitCode !== 0) { + return hardwareBlocker({ + code: "hardware_shell_failed", + layer: "gateway-shell", + category: "command_failed", + summary: `Windows cmd exited with ${exitCode}`, + userMessage: `Windows cmd 已执行但退出码为 ${exitCode}。`, + retryable: true, + traceId + }); + } + return hardwareBlocker({ + code: "hardware_dispatch_failed", + layer: "dispatch", + category: "dispatch_failed", + summary: dispatch.message ?? result?.status ?? "hardware.invoke.shell dispatch did not report a successful shell execution", + userMessage: "hardware.invoke.shell 未返回成功执行结果,本次按 dispatch failed 处理。", + retryable: true, + traceId + }); +} + +function hardwareRegistryBlocker({ request, gatewayRegistry, traceId }) { + if (!request || !gatewayRegistry || typeof gatewayRegistry.getSession !== "function") return null; + const session = gatewayRegistry.getSession(request.gatewaySessionId); + if (!session) { + return hardwareBlocker({ + code: "hardware_session_missing", + layer: "gateway-registry", + category: "session_missing", + summary: `gateway session ${request.gatewaySessionId} is not registered in the cloud-api gateway registry`, + userMessage: "目标 gateway session 未在 cloud-api gateway registry 中登记。", + retryable: true, + traceId + }); + } + if (typeof gatewayRegistry.isOnline === "function" && !gatewayRegistry.isOnline(request.gatewaySessionId)) { + return hardwareBlocker({ + code: "hardware_gateway_offline", + layer: "gateway-registry", + category: "gateway_offline", + summary: `gateway session ${request.gatewaySessionId} is registered but offline or stale`, + userMessage: "目标 gateway 已登记但当前离线或心跳过期。", + retryable: true, + traceId + }); + } + if (session.resourceId && session.resourceId !== request.resourceId) { + return hardwareBlocker({ + code: "hardware_resource_missing", + layer: "gateway-registry", + category: "resource_missing", + summary: `resource ${request.resourceId} is not registered under gateway session ${request.gatewaySessionId}`, + userMessage: "目标 resourceId 不属于当前 gateway session。", + retryable: false, + traceId + }); + } + if (Array.isArray(session.capabilities) && session.capabilities.length > 0) { + const found = session.capabilities.some((capability) => { + if (typeof capability === "string") return capability === request.capabilityId || capability === "shell.exec"; + return capability?.capabilityId === request.capabilityId || capability?.id === request.capabilityId || capability?.name === "shell.exec"; + }); + if (!found) { + return hardwareBlocker({ + code: "hardware_capability_missing", + layer: "gateway-registry", + category: "capability_missing", + summary: `capability ${request.capabilityId} is not registered on gateway session ${request.gatewaySessionId}`, + userMessage: "目标 shell.exec capability 未在该 gateway session 上登记。", + retryable: false, + traceId + }); + } + } + return null; +} + +function resolveHardwareTopology(explicit, { topology, env = process.env } = {}) { + const explicitComplete = ["projectId", "gatewaySessionId", "resourceId", "capabilityId"].every((field) => explicit[field]); + const candidates = hardwareTopologyCandidates({ topology, env }); + const matches = candidates.filter((candidate) => topologyMatchesExplicit(candidate, explicit)); + + if (explicitComplete && (candidates.length === 0 || matches.length <= 1)) { + const matched = matches[0] ?? {}; + return { + ok: true, + source: Object.keys(explicit).every((key) => explicit[key] === matched[key]) ? matched.topologySource ?? "configured-topology" : "explicit-fields", + fallbackUsed: false, + candidateCount: matches.length, + request: { + projectId: explicit.projectId, + gatewaySessionId: explicit.gatewaySessionId, + resourceId: explicit.resourceId, + capabilityId: explicit.capabilityId + } + }; + } + + if (matches.length === 1) { + const match = matches[0]; + return { + ok: true, + source: match.topologySource ?? "configured-topology", + fallbackUsed: !explicitComplete, + candidateCount: 1, + request: { + projectId: explicit.projectId ?? match.projectId, + gatewaySessionId: explicit.gatewaySessionId ?? match.gatewaySessionId, + resourceId: explicit.resourceId ?? match.resourceId, + capabilityId: explicit.capabilityId ?? match.capabilityId + } + }; + } + + const missing = Object.entries(explicit).filter(([, value]) => !value).map(([key]) => key); + if (matches.length > 1) { + return { + ok: false, + source: "ambiguous-topology", + fallbackUsed: false, + candidateCount: matches.length, + blocker: hardwareBlocker({ + code: "hardware_topology_ambiguous", + layer: "topology", + category: "topology_ambiguous", + summary: `PC gateway shell.exec topology is not unique; ${matches.length} candidates match the request.`, + userMessage: "PC gateway shell.exec 拓扑不唯一,请显式提供 projectId/gatewaySessionId/resourceId/capabilityId。", + retryable: false, + missing + }) + }; + } + + const explicitAny = Object.values(explicit).some(Boolean); + return { + ok: false, + source: "missing-topology", + fallbackUsed: false, + candidateCount: 0, + blocker: hardwareBlocker({ + code: explicitAny ? "hardware_capability_missing" : "hardware_topology_missing", + layer: "topology", + category: explicitAny ? "capability_missing" : "topology_missing", + summary: explicitAny + ? "No registered PC gateway shell.exec topology matches the explicit request." + : "PC gateway shell.exec request does not include enough identifiers and no unique topology is available.", + userMessage: explicitAny + ? "显式请求的 PC gateway shell.exec capability 未登记或不匹配。" + : "缺少 PC gateway shell.exec 拓扑字段,且当前没有唯一可解析拓扑。", + retryable: false, + missing + }) + }; +} + +function hardwareTopologyCandidates({ topology, env = process.env } = {}) { + if (Array.isArray(topology)) return topology.map(normalizeTopologyCandidate).filter(Boolean); + if (Array.isArray(topology?.candidates)) return topology.candidates.map(normalizeTopologyCandidate).filter(Boolean); + if (Array.isArray(topology?.capabilities)) return topology.capabilities.map(normalizeTopologyCandidate).filter(Boolean); + if (topology?.pcGatewayShell) return [normalizeTopologyCandidate(topology.pcGatewayShell)].filter(Boolean); + + const envCandidate = normalizeTopologyCandidate({ + projectId: env.HWLAB_CODE_AGENT_PC_GATEWAY_PROJECT_ID, + gatewaySessionId: env.HWLAB_CODE_AGENT_PC_GATEWAY_SESSION_ID, + resourceId: env.HWLAB_CODE_AGENT_PC_GATEWAY_RESOURCE_ID, + capabilityId: env.HWLAB_CODE_AGENT_PC_GATEWAY_CAPABILITY_ID, + topologySource: "env:HWLAB_CODE_AGENT_PC_GATEWAY_*" + }); + if (envCandidate) return [envCandidate]; + + return DEFAULT_DEV_MVP_PC_GATEWAY_TOPOLOGY.map(normalizeTopologyCandidate).filter(Boolean); +} + +function normalizeTopologyCandidate(candidate = {}) { + const normalized = { + projectId: cleanProtocolId(candidate.projectId, "prj"), + gatewaySessionId: cleanProtocolId(candidate.gatewaySessionId, "gws"), + resourceId: cleanProtocolId(candidate.resourceId, "res"), + capabilityId: cleanProtocolId(candidate.capabilityId, "cap"), + capabilityName: candidate.capabilityName ?? candidate.name ?? "shell.exec", + topologySource: candidate.topologySource ?? candidate.source ?? "configured-topology" + }; + if (!normalized.projectId || !normalized.gatewaySessionId || !normalized.resourceId || !normalized.capabilityId) return null; + if (normalized.capabilityName !== "shell.exec" && normalized.capabilityId !== "cap_windows_cmd_exec") return null; + return normalized; +} + +function topologyMatchesExplicit(candidate, explicit) { + return Object.entries(explicit).every(([field, value]) => !value || candidate[field] === value); +} + +function mentionsPcGatewayShellCapability(text, params = {}) { + if (params.capabilityId || params.gatewaySessionId || params.resourceId || params.input?.command || params.command) { + return true; + } + return /PC\s*gateway|Windows\s+cmd|cmd(?:\.exe)?\s*\/c|shell\.exec|hardware\.invoke\.shell|cap_windows_cmd_exec|res_windows_host|gws_DESKTOP/iu.test(text); +} + +function extractProtocolField(text, field, prefix) { + const aliases = field === "gatewaySessionId" + ? "(?:gatewaySessionId|gatewaySession|session)" + : field; + const match = String(text ?? "").match(new RegExp(`${aliases}\\s*[:=]\\s*([A-Za-z][A-Za-z0-9._:-]+)`, "iu")); + return cleanProtocolId(match?.[1], prefix); +} + +function extractWindowsCommand(text) { + const match = String(text ?? "").match(/\bcmd(?:\.exe)?\s*\/c\s+[\s\S]+$/iu); + if (!match) return null; + return trimCommandTail(match[0]); +} + +function trimCommandTail(command) { + return String(command ?? "") + .replace(/(?:。|;)\s*(?:返回|请返回|输出|并返回|$)[\s\S]*$/u, "") + .replace(/\s+(?:返回|请返回|输出|并返回)\s+(?:stdout|stderr|exitCode|退出码)[\s\S]*$/iu, "") + .replace(/[。;]\s*$/u, "") + .trim(); +} + +function normalizeWindowsCommand(command) { + if (typeof command !== "string") return null; + const trimmed = trimCommandTail(command); + return trimmed || null; +} + +function isAllowedWindowsCmd(command) { + const value = String(command ?? "").trim(); + if (!/^cmd(?:\.exe)?\s+\/c\s+\S/iu.test(value)) return false; + return !/\b(?:bash|sh|kubectl|powershell|pwsh)\b|sh\s+-c/iu.test(value); +} + +function acquireHardwareSession(sessionRegistry, { conversationId, sessionId, traceId, workspace, now } = {}) { + if (!sessionRegistry || typeof sessionRegistry.acquire !== "function") return { session: null, blocker: null }; + const acquired = sessionRegistry.acquire({ + conversationId, + sessionId, + workspace, + sandbox: CODE_AGENT_HARDWARE_SANDBOX, + runnerKind: CODE_AGENT_HARDWARE_RUNNER_KIND, + sessionMode: CODE_AGENT_HARDWARE_SESSION_MODE, + capabilityLevel: CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED, + implementationType: CODE_AGENT_HARDWARE_IMPLEMENTATION_TYPE, + traceId, + now + }); + if (acquired.ok) return { session: acquired.session, blocker: null }; + return { + session: acquired.session ?? null, + blocker: hardwareBlocker({ + code: acquired.code ?? "hardware_session_blocked", + layer: "session", + category: "session_blocked", + summary: acquired.message ?? "Code Agent hardware capability session could not be acquired.", + userMessage: "Code Agent 硬件能力 session 当前不可用。", + retryable: true, + traceId + }) + }; +} + +function releaseHardwareSession(sessionRegistry, session, { now, traceId, conversationId } = {}) { + if (!session || !sessionRegistry || typeof sessionRegistry.release !== "function") return session; + return sessionRegistry.release(session.sessionId, { + now, + traceId, + conversationId, + reused: session.reused, + status: "idle" + }) ?? session; +} + +function appendHardwareTraceCompletion(traceRecorder, runnerResult) { + traceRecorder?.append({ + type: "tool_call", + status: runnerResult.capabilityLevel === CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED ? "completed" : "blocked", + label: runnerResult.capabilityLevel === CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED + ? "tool:hardware.invoke.shell:completed" + : "tool:hardware.invoke.shell:blocked", + toolName: HARDWARE_INVOKE_SHELL_METHOD, + outputSummary: `dispatchStatus=${runnerResult.dispatchStatus ?? "null"}; exitCode=${runnerResult.exitCode ?? "null"}; operationId=${runnerResult.operationId ?? "null"}`, + terminal: false + }); +} + +function skippedCodexStdioFeasibility({ workspace, reason }) { + return { + checked: true, + skipped: true, + reason, + sourceIssue: "pikasTech/HWLAB#460", + status: "skipped", + ready: false, + canStartLongLivedCodexStdio: false, + commandProbe: { + ready: false, + skipped: true, + reason + }, + workspace: workspace ?? process.cwd(), + sandbox: CODE_AGENT_HARDWARE_SANDBOX, + blockers: [], + blockerCodes: [], + summary: "Registered hardware capability requests use deterministic cloud-api JSON-RPC route; Codex stdio and text fallback are not part of this control decision." + }; +} + +function hardwareLongLivedGate(session) { + return { + status: "skipped", + ready: false, + provider: CODE_AGENT_HARDWARE_PROVIDER, + runnerKind: CODE_AGENT_HARDWARE_RUNNER_KIND, + sessionId: session?.sessionId ?? null, + sessionMode: CODE_AGENT_HARDWARE_SESSION_MODE, + implementationType: CODE_AGENT_HARDWARE_IMPLEMENTATION_TYPE, + blockers: [], + reason: "hardware_capability_deterministic_route" + }; +} + +function hardwareSkills({ status, blockers }) { + return { + status, + items: [{ + name: HARDWARE_INVOKE_SHELL_METHOD, + summary: "Controlled cloud-api hardware capability route for registered PC gateway shell.exec.", + capabilityLevel: status === "used" ? CODE_AGENT_HARDWARE_CAPABILITY_EXECUTED : CODE_AGENT_HARDWARE_CAPABILITY_BLOCKED + }], + count: 1, + blockers + }; +} + +function hardwareBlocker({ code, layer, category, summary, userMessage, retryable, traceId = null, reason = null, missing = [] }) { + return { + code, + layer, + category, + retryable: Boolean(retryable), + summary: redactText(summary), + message: redactText(summary), + userMessage, + reason: reason ? redactText(reason) : undefined, + traceId, + route: HARDWARE_INVOKE_SHELL_METHOD, + toolName: HARDWARE_INVOKE_SHELL_METHOD, + missing + }; +} + +function sessionReuseEvidence(session) { + return { + conversationId: session.conversationId, + sessionId: session.sessionId, + mapped: true, + reused: session.reused, + turn: session.turn, + previousTurns: Math.max(0, session.turn - 1), + workspace: session.workspace, + createdAt: session.createdAt, + updatedAt: session.updatedAt, + idleTimeoutMs: session.idleTimeoutMs, + expiresAt: session.expiresAt, + lastTraceId: session.lastTraceId, + status: session.status + }; +} + +function safeOutput(value) { + return redactText(value); +} + +function summarizeOutput(value) { + const text = String(value ?? "").trim().replace(/\s+/gu, " "); + return text.length > COMMAND_SUMMARY_LIMIT ? `${text.slice(0, COMMAND_SUMMARY_LIMIT)}...` : text; +} + +function boundOutput(value, maxLength = OUTPUT_LIMIT) { + const text = String(value ?? ""); + if (text.length <= maxLength) return { text, truncated: false }; + return { + text: `${text.slice(0, maxLength)}\n[output truncated at ${maxLength} bytes]`, + truncated: true + }; +} + +function redactCommand(value) { + return summarizeOutput(redactText(value)); +} + +function redactText(value) { + return String(value ?? "") + .replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gu, "Bearer ***") + .replace(/sk-[A-Za-z0-9._-]+/gu, "sk-***") + .replace(/([A-Za-z0-9_]*TOKEN[A-Za-z0-9_]*=)[^\s]+/giu, "$1***") + .replace(/([A-Za-z0-9_]*KEY[A-Za-z0-9_]*=)[^\s]+/giu, "$1***"); +} + +function cleanProtocolId(value, prefix) { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + if (!trimmed) return null; + if (/^[a-z][a-z0-9]*_[A-Za-z0-9._:-]+$/u.test(trimmed)) return trimmed; + return `${prefix}_${trimmed.replace(/[^A-Za-z0-9._:-]/gu, "-").slice(0, 80)}`; +} + +function nowIso(now) { + return typeof now === "function" ? now() : new Date().toISOString(); +} diff --git a/internal/cloud/code-agent-session-registry.test.mjs b/internal/cloud/code-agent-session-registry.test.mjs index 2c7bf5a9..893a8513 100644 --- a/internal/cloud/code-agent-session-registry.test.mjs +++ b/internal/cloud/code-agent-session-registry.test.mjs @@ -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, "\\$&"); } diff --git a/internal/cloud/json-rpc.test.mjs b/internal/cloud/json-rpc.test.mjs index 17616ef6..a299cec0 100644 --- a/internal/cloud/json-rpc.test.mjs +++ b/internal/cloud/json-rpc.test.mjs @@ -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" diff --git a/internal/cloud/server.mjs b/internal/cloud/server.mjs index 6dd96dfd..11bd021e 100644 --- a/internal/cloud/server.mjs +++ b/internal/cloud/server.mjs @@ -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 + }) } ); diff --git a/internal/cloud/server.test.mjs b/internal/cloud/server.test.mjs index 29de6fb8..3edbfb90 100644 --- a/internal/cloud/server.test.mjs +++ b/internal/cloud/server.test.mjs @@ -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({ diff --git a/package.json b/package.json index 17636dca..70050adc 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "node scripts/repo-reports-guard.mjs && node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/dev-runtime-hotfix-audit.mjs && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs scripts/src/dev-runtime-hotfix-audit.test.mjs", - "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/cloud-web-routes.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.test.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.test.mjs && node --test scripts/src/dev-runtime-hotfix-audit.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cicd-jobs.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test web/hwlab-cloud-web/code-agent-facts.test.mjs web/hwlab-cloud-web/code-agent-status.test.mjs web/hwlab-cloud-web/wiring-status.test.mjs web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/refresh-artifact-catalog.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs tools/hwlab-cli/lib/cli.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/cloud-web-proxy.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/cloud-web-routes.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.mjs && node --check internal/dev-entrypoint/cloud-web-proxy.test.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-hardware-runner.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.mjs && node --check scripts/src/dev-runtime-hotfix-audit.test.mjs && node --test scripts/src/dev-runtime-hotfix-audit.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cicd-jobs.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test web/hwlab-cloud-web/code-agent-facts.test.mjs web/hwlab-cloud-web/code-agent-status.test.mjs web/hwlab-cloud-web/wiring-status.test.mjs web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/refresh-artifact-catalog.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs tools/hwlab-cli/lib/cli.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/cloud-web-proxy.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "dev-runtime-base:build": "node scripts/dev-runtime-base-image.mjs", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs",