merge main updates into G14

This commit is contained in:
Codex
2026-05-26 01:05:32 +08:00
34 changed files with 1347 additions and 8793 deletions
+1 -4
View File
@@ -500,10 +500,7 @@ export function classifyCodeAgentRuntimeBlock(payload, { httpStatus = null } = {
"codex_stdio_failed",
"codex_stdio_protocol_blocked",
"codex_stdio_empty_response",
"codex_stdio_command_probe_failed",
"external_network_blocked",
"network_tool_unavailable",
"network_timeout"
"codex_stdio_command_probe_failed"
].includes(errorCode)) {
return {
blocked: true,
+78 -391
View File
@@ -46,26 +46,26 @@ const codeAgentExternalNetworkPrompt = Object.freeze({
const codeAgentQuickPromptFixtures = Object.freeze([
{
id: "pwd",
label: "Skill CLIpwd",
prompt: "通过 Skill CLI 执行 pwd,列出当前工作目录。",
label: "Codexpwd",
prompt: "交给 Codex 执行 pwd,列出当前工作目录。",
writable: false
},
{
id: "skills",
label: "Skill CLI:列出 skill",
prompt: "通过 Skill CLI 列出你能使用的所有 skill。",
label: "Codex:列出 skill",
prompt: "交给 Codex 列出你能使用的所有 skill。",
writable: false
},
{
id: "do1-true-di1",
label: "HWLAB APIDO1=true 后读 DI1",
prompt: "通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
label: "CodexDO1=true 后读 DI1",
prompt: "交给 Codex 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
writable: true
},
{
id: "do1-false-di1",
label: "HWLAB APIDO1=false 复核 DI1",
prompt: "通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
label: "CodexDO1=false 复核 DI1",
prompt: "交给 Codex 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
writable: true
}
]);
@@ -326,10 +326,7 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
"echo/mock/stub",
"untrusted_completion",
"SOURCE fixture",
"Code Agent SOURCE 回复",
"hwlab-skill-cli",
"hwlab-m3-io-skill-cli",
"message-m3-evidence"
"Code Agent SOURCE 回复"
]);
const blockedCodeAgentUiLabels = Object.freeze([
@@ -562,11 +559,6 @@ function runStaticSmoke() {
evidence: requiredTrustedRecordTerms
});
addCheck(checks, blockers, "hardware-status-rich-structure", hardwareStatusStructureContract(files), "Right hardware status renders Gateway/Box, DI/DO/AI/AO/FREQ, patch-panel, and DO1 -> DI1 source-labeled link state.", {
blocker: "contract_blocker",
evidence: requiredHardwareStatusTerms
});
addCheck(checks, blockers, "m3-control-user-flow", m3ControlUserFlowContract(files), "Right control panel exposes an operator-readable DO1 -> patch-panel -> DI1 action summary, current action, DO1 target, DI1 observation, trusted/durable state, and recent trace navigation.", {
blocker: "contract_blocker",
evidence: requiredM3ControlUserFlowTerms
@@ -601,22 +593,22 @@ function runStaticSmoke() {
]
});
addCheck(checks, blockers, "same-origin-readonly-boundary", hasSameOriginReadOnlyBoundary(files.app), "Workbench data access stays same-origin and JSON-RPC diagnostics stay read-only.", {
blocker: "safety_blocker",
evidence: ["/health/live", "/v1", "/v1/live-builds", "/v1/diagnostics/gate", "/v1/agent/chat", "/json-rpc", ...readOnlyRpcMethods]
});
addCheck(checks, blockers, "api-live-status-attribution", hasApiLiveStatusAttribution(files), "Default workbench status maps raw API health into pass, error, unverified, or read-only with concrete service/API attribution.", {
blocker: "observability_blocker",
evidence: ["API 正常", "API 错误", "等待验证", "只读模式", "hwlab-cloud-api /health/live", "/v1/agent/chat", "/v1/m3/io", "raw degraded kept as internalRawStatuses"]
});
addCheck(checks, blockers, "workbench-live-surface-timeout-contract", hasWorkbenchLiveSurfaceTimeoutContract(files), "Workbench live status uses a dedicated live-surface timeout instead of the 4500ms light API budget.", {
blocker: "runtime_blocker",
evidence: ["DEFAULT_LIVE_SURFACE_TIMEOUT_MS=12000", "liveSurfaceFetch", "liveSurfaceRpc", "/v1/m3/status", "system.health"]
});
addCheck(checks, blockers, "code-agent-chat-primary-flow", hasCodeAgentChatContract(files), "Center Agent conversation sends to the controlled Code Agent chat endpoint and no longer uses 添加草稿 as the primary flow.", {
blocker: "runtime_blocker",
evidence: ["command-send", "agent-chat-status", "/v1/agent/chat", "conversationId/messageId/status/timestamps/error.message"]
});
addCheck(checks, blockers, "code-agent-quick-prompts-contract", hasCodeAgentQuickPromptContract(files), "Code Agent quick prompts fill the input with Skill CLI / HWLAB API examples while hardware-write prompts require explicit send confirmation.", {
addCheck(checks, blockers, "code-agent-quick-prompts-contract", hasCodeAgentQuickPromptContract(files), "Code Agent quick prompts fill the input with Codex examples while hardware-write prompts require explicit send confirmation.", {
blocker: "safety_blocker",
evidence: codeAgentQuickPromptFixtures.flatMap((prompt) => [prompt.id, prompt.label, prompt.prompt])
});
@@ -626,9 +618,9 @@ function runStaticSmoke() {
evidence: ["DEFAULT_API_TIMEOUT_MS=4500 for light probes", `DEFAULT_CODE_AGENT_TIMEOUT_MS=${codeAgentLongTimeoutMs}`, "sendAgentMessage passes timeoutMs"]
});
addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels disclose full trace access and preserve open/scroll state while live trace updates.", {
addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels auto-replay full trace, avoid compressed-window UI, and preserve open/scroll state while live trace updates.", {
blocker: "observability_blocker",
evidence: ["显示全部 N / 原始 N", "复制 JSON", "下载 trace", "traceDetailsOpen", "traceScrollPositions", "internal scroll for full trace"]
evidence: ["显示全部可读事件 / 完整 trace 回放中", "复制 JSON", "下载 trace", "traceDetailsOpen", "traceScrollPositions", "internal scroll for full trace"]
});
addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider/stdio blockers without exposing credential internals and only long-lived Codex stdio replies can become full Code Agent completion.", {
@@ -636,14 +628,13 @@ function runStaticSmoke() {
evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard", "default workspace hides credential internals"]
});
addCheck(checks, blockers, "code-agent-status-summary", hasCodeAgentStatusSummaryContract(files), "Code Agent area has a compact collapsible status summary for fallback, one-shot, read-only session, skill CLI control, blockers, trace, and current deployed revision.", {
addCheck(checks, blockers, "code-agent-status-summary", hasCodeAgentStatusSummaryContract(files), "Code Agent area has a compact collapsible status summary for Codex stdio, blockers, trace, and current deployed revision.", {
blocker: "observability_blocker",
evidence: [
"code-agent-summary",
"fallback-text-chat-only",
"stateless-one-shot",
"read-only-session-tools",
"skill-cli-api-control",
"当前部署 revision"
]
});
@@ -1977,6 +1968,8 @@ function hasDefaultLoginEntry({ html, app, auth, styles }, artifactPublisher = "
/DEFAULT_AUTH_PASSWORD\s*=\s*["']hwlab2026["']/u.test(auth) &&
/HWLAB_CLOUD_WEB_CONFIG\?\.auth/u.test(auth) &&
/AUTH_STORAGE_KEY\s*=\s*["']hwlab\.cloudWorkbench\.auth\.v1["']/u.test(auth) &&
/const localSession = readLocalSession\(config\)/u.test(auth) &&
/writeLocalSession\(config\)/u.test(functionBody(auth, "attemptLogin")) &&
/账号或密码不正确,请重新输入。/u.test(auth) &&
/ensureWorkbenchAuth/u.test(app) &&
/await\s+ensureWorkbenchAuth/u.test(app) &&
@@ -2498,14 +2491,14 @@ function hasCodeAgentQuickPromptContract({ html, app, styles }) {
/我点击发送后才确认执行这次写入请求/u.test(tag)
);
});
const promptCopyUsesApiBoundary = codeAgentQuickPromptFixtures.every((prompt) =>
/通过 (?:Skill CLI|HWLAB API \/ Skill CLI)/u.test(prompt.prompt) &&
!/(?:\bgateway\b|\bbox\b|\bpatch-panel\b|PROD|真实硬件直控)/iu.test(prompt.prompt)
const promptCopyUsesCodexRoute = codeAgentQuickPromptFixtures.every((prompt) =>
/交给 Codex/u.test(prompt.prompt) &&
!/PROD/iu.test(prompt.prompt)
);
return (
labels &&
writeButtonsRequireExplicitSend &&
promptCopyUsesApiBoundary &&
promptCopyUsesCodexRoute &&
/data-agent-quick-prompt/u.test(app) &&
/function\s+fillAgentQuickPrompt\s*\(/u.test(app) &&
/el\.commandInput\.value = prompt/u.test(app) &&
@@ -2645,13 +2638,42 @@ function hasCodeAgentLongTimeoutContract(files) {
);
}
function hasWorkbenchLiveSurfaceTimeoutContract(files) {
const app = files.app;
return (
/DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) &&
/DEFAULT_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/u.test(app) &&
/LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/u.test(app) &&
/timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "liveSurfaceFetch")) &&
/timeoutName:\s*`工作台实况 \$\{path\}`/u.test(functionBody(app, "liveSurfaceFetch")) &&
/callRpc\(method,\s*params,\s*\{/u.test(functionBody(app, "liveSurfaceRpc")) &&
/timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "liveSurfaceRpc")) &&
/timeoutMs:\s*options\?\.timeoutMs \?\? API_TIMEOUT_MS/u.test(functionBody(app, "callRpc")) &&
/liveSurfaceFetch\("\/health\/live"\)/u.test(functionBody(app, "loadLiveSurface")) &&
/liveSurfaceFetch\("\/v1\/m3\/status"\)/u.test(functionBody(app, "loadLiveSurface")) &&
/liveSurfaceRpc\("system\.health"\)/u.test(functionBody(app, "loadLiveSurface")) &&
/timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "loadGateDiagnostics")) &&
/timeoutName:\s*"工作台 M3 状态刷新"/u.test(functionBody(app, "runM3IoAction"))
);
}
function hasCodeAgentTraceReplayDisclosure({ app, styles }) {
const tracePanelBody = functionBody(app, "messageTracePanel");
const traceToolbarBody = functionBody(app, "messageTraceToolbar");
const renderConversationBody = functionBody(app, "renderConversation");
return (
/function\s+messageTraceCountText\s*\(/u.test(app) &&
/显示全部\s+\$\{displayTotal\}\s+\/\s+原始\s+\$\{rawTotal\}/u.test(app) &&
/显示全部可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/u.test(app) &&
/完整 trace 回放中\s+\/\s+当前可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/u.test(app) &&
!/压缩窗口\s+\$\{readableTotal\}/u.test(app) &&
/CODE_AGENT_SESSION_STORAGE_KEY\s*=\s*"hwlab\.workbench\.codeAgentSession\.v1"/u.test(app) &&
/function\s+restoreCodeAgentSessionState\s*\(/u.test(app) &&
/function\s+persistCodeAgentSessionState\s*\(/u.test(app) &&
/window\.localStorage\?\.setItem\(CODE_AGENT_SESSION_STORAGE_KEY/u.test(app) &&
/window\.localStorage\?\.removeItem\(CODE_AGENT_SESSION_STORAGE_KEY/u.test(app) &&
/refreshRestoredCodeAgentTraces/u.test(app) &&
/function\s+maybeReplayFullTraceForMessage\s*\(/u.test(app) &&
/function\s+replayFullTrace\s*\(/u.test(app) &&
/function\s+renderTraceEventList\s*\(/u.test(app) &&
/messageTraceToolbar\(message,\s*trace,\s*events,\s*rows\)/u.test(tracePanelBody) &&
/list\.dataset\.traceMode\s*=\s*"all"/u.test(tracePanelBody) &&
@@ -2690,7 +2712,6 @@ function hasCodeAgentStatusSummaryContract({ html, app, styles, codeAgentStatus
/fallback-text-chat-only/u.test(codeAgentStatus) &&
/stateless-one-shot/u.test(codeAgentStatus) &&
/read-only-session-tools/u.test(codeAgentStatus) &&
/skill-cli-api-control/u.test(codeAgentStatus) &&
/currentDeploymentRevision/u.test(codeAgentStatus) &&
/unsafeGreenForNonReady/u.test(codeAgentStatus) &&
/provider_config_blocked/u.test(codeAgentStatus) &&
@@ -4407,7 +4428,7 @@ async function inspectJourneyUi(page) {
actions,
eventCount: events.length,
traceMode: panel.querySelector(".message-trace-events")?.dataset.traceMode ?? "",
allTraceVisible: /显示全部\s+\d+\s*\/\s*原始\s+\d+/u.test(countText),
allTraceVisible: /显示全部可读事件\s+\d+\s*\/\s*已载入原始\s+\d+\s*\/\s*后端原始\s+\d+/u.test(countText),
copyJsonVisible: actions.includes("复制 JSON"),
downloadTraceVisible: actions.includes("下载 trace"),
fixedAllMode: panel.querySelector(".message-trace-events")?.dataset.traceMode === "all"
@@ -4473,8 +4494,6 @@ async function inspectJourneyUi(page) {
runtimePathShowsProviderFields: /provider|runnerKind|protocol|implementationType|providerTrace\.command|providerTrace\.terminalStatus/u.test(runtimePathDialogText),
runtimePathSummaryStaysCompact: !/providerTrace\.command|providerTrace\.terminalStatus|protocol/u.test(runtimePathText),
runtimePathFallbackNotFull: !/(OpenAI text fallback|source-fixture|SOURCE)[\s\S]{0,120}(真实 runner|repo-owned Codex app-server stdio)/u.test(runtimePathText),
runtimePathMcpNotFull: !/(MCP|codex-mcp-stdio-runner|mcp-jsonrpc-stdio)/u.test(runtimePathText) ||
(/DEGRADEDMCP|MCP\/其他 runner/u.test(runtimePathText) && !/使用 Responses wire API|wire_api=responses|真实 runnerCodex app-server stdio/u.test(runtimePathText)),
noMainAttributionNoise: document.querySelectorAll(".message-card.message-agent .message-attribution, .message-card.message-agent .message-evidence, .message-card.message-agent .code-agent-facts").length === 0,
attributionFallbackNotRunnerControl: !/openai-responses-fallback[\s\S]{0,80}(真实 runner|Codex stdio 长会话|workspace-write)/u.test(
tracePanels
@@ -4611,24 +4630,6 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
});
}
export async function runDevCloudWorkbenchSlowBlockerFixtureSmoke(options = {}) {
return runLocalAgentFixtureSmoke({
mode: "local-agent-slow-blocker-fixture-browser",
responseDelayMs: options.responseDelayMs ?? localAgentFixtureDelayMs,
agentFixtureMode: "slow-blocker",
expectTimeout: false
});
}
export async function runDevCloudWorkbenchExternalNetworkFixtureSmoke(options = {}) {
return runLocalAgentFixtureSmoke({
mode: "local-agent-external-network-fixture-browser",
responseDelayMs: options.responseDelayMs ?? localAgentFixtureDelayMs,
agentFixtureMode: "external-network-blocker",
expectTimeout: false
});
}
export async function runDevCloudWorkbenchSessionContinuityFixtureSmoke(options = {}) {
let chromium;
try {
@@ -4888,7 +4889,7 @@ export async function runDevCloudWorkbenchQuickPromptsFixtureSmoke() {
{
id: "quick-prompts-copy-boundary",
status: promptResults.every((result) => result.copyBoundaryOk) ? "pass" : "blocked",
summary: "Quick prompt labels and filled text say HWLAB API / Skill CLI and avoid direct gateway/box/patch-panel or PROD claims.",
summary: "Quick prompt labels and filled text say Codex and avoid PROD claims.",
observations: promptResults.map(({ viewport, id, label, inputValue, copyBoundaryOk, forbiddenHits }) => ({
viewport,
id,
@@ -4963,7 +4964,6 @@ async function runLocalAgentFixtureSmoke({
mode,
responseDelayMs = 0,
timeoutConfigMs = null,
agentFixtureMode = "success",
expectTimeout = false
} = {}) {
let chromium;
@@ -5003,7 +5003,7 @@ async function runLocalAgentFixtureSmoke({
};
}
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs, agentFixtureMode });
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs, agentFixtureMode: "success" });
let browser;
try {
browser = await chromium.launch({ headless: true });
@@ -5014,6 +5014,9 @@ async function runLocalAgentFixtureSmoke({
...(globalThis.HWLAB_CLOUD_WEB_CONFIG ?? {}),
timeouts: {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? {}),
codeAgentSubmitTimeoutMsMinMs: 1,
codeAgentSubmitTimeoutMs: timeoutMs,
codeAgentTimeoutMsMinMs: 1,
codeAgentTimeoutMs: timeoutMs
}
};
@@ -5060,24 +5063,17 @@ async function runLocalAgentFixtureSmoke({
{ timeout: 8000 }
);
} else {
const prompts = agentFixtureMode === "slow-blocker"
? [codeAgentE2ePrompts[1]]
: agentFixtureMode === "external-network-blocker"
? [codeAgentExternalNetworkPrompt]
: codeAgentE2ePrompts;
for (const prompt of prompts) {
for (const prompt of codeAgentE2ePrompts) {
promptResults.push(await runCodeAgentPromptJourney(page, prompt, { sourceFixture: true }));
if (agentFixtureMode === "success") {
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
}
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
}
}
const ui = await inspectJourneyUi(page);
const mobilePending = !expectTimeout && agentFixtureMode === "success"
const mobilePending = !expectTimeout
? await inspectLocalAgentPendingViewport(browser, server.url, {
width: 390,
height: 844,
@@ -5097,32 +5093,7 @@ async function runLocalAgentFixtureSmoke({
ui.timeoutActionPanelContained &&
!ui.completedMessageVisible
)
: agentFixtureMode === "slow-blocker" || agentFixtureMode === "external-network-blocker"
? (
promptResults.length === 1 &&
promptResults[0]?.status === "blocked" &&
promptResults[0]?.classification?.blocker === "runner-blocked" &&
promptResults[0]?.legacyWindow?.permanentFailureAround4500ms === false &&
promptResults[0]?.legacyWindow?.pendingChineseVisible === true &&
promptResults[0]?.legacyWindow?.pendingTraceShowsEvents === true &&
(agentFixtureMode === "external-network-blocker"
? ["能力未开放", "Runner 受阻", "服务受阻"].includes(ui.agentChatStatus)
: ui.agentChatStatus === "Runner 受阻") &&
ui.failedMessageVisible &&
ui.failedMessageHasTrace &&
ui.retryInputPreserved &&
ui.failedRetryActionVisible &&
ui.failedTraceActionVisible &&
ui.failedActionPanelContained &&
ui.runtimePathVisible &&
ui.runtimePathCompactSummaryVisible &&
ui.runtimePathUsesCompactDialog &&
ui.runtimePathShowsProviderFields &&
ui.runtimePathSummaryStaysCompact &&
ui.runtimePathMcpNotFull &&
!ui.completedMessageVisible
)
: (
: (
promptResults.every((result) =>
result.status === "pass" &&
result.response?.status === "completed" &&
@@ -5182,8 +5153,6 @@ async function runLocalAgentFixtureSmoke({
status: pass ? "pass" : "blocked",
summary: expectTimeout
? "Local SOURCE fixture proves a bounded Code Agent timeout stays user-visible/BLOCKED in the UI, preserves trace context, and keeps the user's input for retry."
: agentFixtureMode === "slow-blocker" || agentFixtureMode === "external-network-blocker"
? "Local SOURCE fixture returns a delayed structured blocker and the conversation shows the Chinese reason with trace evidence while preserving the input."
: "Local SOURCE fixture sends input and reaches a user-visible replied state without marking the fixture as DEV-LIVE.",
observations: expectTimeout ? {
response: responseSummary,
@@ -5197,7 +5166,7 @@ async function runLocalAgentFixtureSmoke({
}
}
];
if (!expectTimeout && agentFixtureMode === "success") {
if (!expectTimeout) {
checks.push({
id: "local-agent-fixture-no-4500ms-permanent-failure",
status: promptResults.every((result) =>
@@ -5233,22 +5202,6 @@ async function runLocalAgentFixtureSmoke({
summary: "390x844 mobile view shows Chinese Code Agent pending status, trace/session context, and no pending-card text overflow before the delayed response arrives.",
observations: mobilePending
});
} else if (!expectTimeout && (agentFixtureMode === "slow-blocker" || agentFixtureMode === "external-network-blocker")) {
checks.push({
id: agentFixtureMode === "external-network-blocker"
? "local-agent-fixture-external-network-blocker"
: "local-agent-fixture-slow-structured-blocker",
status: pass ? "pass" : "blocked",
summary: agentFixtureMode === "external-network-blocker"
? "External network blocker for a GitHub prompt displays Runner 受阻 with network trace, keeps the user prompt, and does not restore main evidence/facts noise."
: "Delayed structured blocker arrives after the legacy 4500ms window, displays Runner 受阻 instead of a generic 后端失败 state, keeps trace visible, and leaves the user prompt for continuing the same conversation.",
observations: {
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
prompt: promptResults[0] ?? null,
ui
}
});
}
const blockers = checks
.filter((check) => check.status !== "pass")
@@ -5274,7 +5227,7 @@ async function runLocalAgentFixtureSmoke({
safety: {
...staticSafety(),
localFixtureOnly: true,
agentFixtureMode,
agentFixtureMode: "success",
fixtureTrustBoundary: "SOURCE fixture replies render as SOURCE 回复 and never as DEV-LIVE 回复.",
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
@@ -5323,6 +5276,9 @@ async function inspectLocalAgentPendingViewport(browser, url, { width, height, r
...(globalThis.HWLAB_CLOUD_WEB_CONFIG ?? {}),
timeouts: {
...(globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? {}),
codeAgentSubmitTimeoutMsMinMs: 1,
codeAgentSubmitTimeoutMs: timeoutMs,
codeAgentTimeoutMsMinMs: 1,
codeAgentTimeoutMs: timeoutMs
}
};
@@ -5729,7 +5685,7 @@ async function inspectQuickPromptsViewport(browser, url, viewport) {
const button = document.querySelector(`[data-agent-quick-prompt="${id}"]`);
const input = document.querySelector("#command-input");
const text = `${button?.textContent ?? ""}\n${button?.getAttribute("title") ?? ""}\n${button?.dataset.promptText ?? ""}`;
const forbiddenPattern = /\b(?:PROD|gateway|box|patch-panel|真实硬件直控)\b/iu;
const forbiddenPattern = /\bPROD\b/iu;
const forbiddenHits = [...new Set(text.match(forbiddenPattern) ?? [])];
return {
label: button?.textContent?.replace(/\s+/gu, " ").trim() ?? "",
@@ -5739,7 +5695,7 @@ async function inspectQuickPromptsViewport(browser, url, viewport) {
focused: document.activeElement === input,
explicitSendRequired: button?.dataset.requiresExplicitSend === "true",
copyBoundaryOk:
/(?:HWLAB API|Skill CLI)/u.test(text) &&
/Codex/u.test(text) &&
!forbiddenPattern.test(text) &&
(button?.dataset.requiresExplicitSend !== "true" || /只填充输入框|点击发送后才确认/u.test(text)),
forbiddenHits
@@ -5988,7 +5944,7 @@ function sessionContinuityFixturePayload({ body, traceId, conversationId, messag
status: "idle"
},
implementationType: "repo-owned-codex-app-server-stdio-session",
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
runnerLimitations: ["secret-values-redacted"],
codexStdioFeasibility: {
ready: true,
canStartLongLivedCodexStdio: true,
@@ -6190,8 +6146,8 @@ function startLocalAgentTrace(agentTraceStore, { traceId, conversationId, delayM
function localAgentTraceSnapshot({ traceId, conversationId = null, startedAt = Date.now(), events = [], lastEvent = null }) {
return {
traceId,
runnerKind: "codex-mcp-stdio-runner",
sessionMode: "codex-mcp-stdio-long-lived",
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
sessionId: conversationId,
sessionStatus: events.length > 0 ? "running" : "pending",
status: events.length > 0 ? "running" : "pending",
@@ -6890,275 +6846,6 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
}));
return true;
}
if (options.agentFixtureMode === "external-network-blocker") {
const blocker = {
code: "external_network_blocked",
category: "capability_unavailable",
layer: "network",
userMessage: "当前 Code Agent 运行策略禁止外部网络访问;未访问外网,也不会伪造成功。",
retryable: false,
traceId,
sessionId: conversationId,
conversationId,
stage: "policy",
elapsedMs: options.agentDelayMs ?? 0,
lastEvent: { seq: 4, traceId, type: "network", stage: "policy", status: "blocked", label: "network:blocked", errorCode: "external_network_blocked" }
};
jsonResponse(response, 200, {
conversationId,
sessionId: conversationId,
messageId,
status: "failed",
sourceKind: "SOURCE",
evidenceLevel: "SOURCE",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider: "codex-stdio",
model: "codex-stdio",
backend: "local-source-fixture/codex-stdio-external-network-blocker",
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
runner: {
kind: "codex-mcp-stdio-runner",
writeCapable: true,
codexStdio: true,
longLivedSession: true,
durableSession: true
},
capabilityLevel: "blocked",
sessionMode: "codex-mcp-stdio-long-lived",
session: {
sessionId: conversationId,
conversationId,
status: "idle",
turn: 1,
lastTraceId: traceId
},
sessionReuse: {
conversationId,
sessionId: conversationId,
mapped: true,
reused: true,
turn: 1,
status: "idle"
},
error: {
code: "external_network_blocked",
category: "capability_unavailable",
layer: "network",
message: "SOURCE external network blocker: runtime policy disabled external network checks.",
userMessage: blocker.userMessage,
retryable: false,
blocker
},
blocker,
blockers: [blocker],
toolCalls: [
{
name: "external.network.check",
type: "network-check",
status: "blocked",
exitCode: 2,
traceId,
stderrSummary: "external_network_blocked",
targetUrl: "https://github.com/",
blocker
}
],
skills: {
status: "not_requested",
items: [],
count: 0,
totalCount: 0,
blockers: []
},
runnerTrace: {
traceId,
runnerKind: "codex-mcp-stdio-runner",
sessionMode: "codex-mcp-stdio-long-lived",
sessionId: conversationId,
sessionStatus: "idle",
status: "blocked",
elapsedMs: options.agentDelayMs ?? 0,
waitingFor: "network-policy",
events: [
{ seq: 1, traceId, type: "session", stage: "created", status: "completed", label: "session:created" },
{ seq: 2, traceId, type: "prompt", stage: "sent", status: "completed", label: "prompt:sent", toolName: "external.network.check" },
{ seq: 3, traceId, type: "network", stage: "policy", status: "started", label: "network:started", toolName: "external.network.check" },
blocker.lastEvent,
{ seq: 5, traceId, type: "tool_call", stage: "tool_call", status: "blocked", label: "tool:external.network.check:blocked", toolName: "external.network.check", errorCode: "external_network_blocked" }
],
lastEvent: { seq: 5, traceId, type: "tool_call", stage: "tool_call", status: "blocked", label: "tool:external.network.check:blocked", toolName: "external.network.check", errorCode: "external_network_blocked" }
},
conversationFacts: {
conversationId,
sessionId: conversationId,
sessionStatus: "idle",
sessionMode: "codex-mcp-stdio-long-lived",
capabilityLevel: "blocked",
runnerKind: "codex-mcp-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
turnCount: 1,
latestTraceId: traceId,
traceIds: [traceId],
latestSkills: null,
recentToolCalls: [{ name: "external.network.check", status: "blocked" }],
facts: [],
valuesRedacted: true,
secretMaterialStored: false
},
providerTrace: {
source: "SOURCE-local-browser-fixture",
sourceKind: "SOURCE",
transport: "stdio+controlled-network",
responseId: "rsp_source_fixture_external_network_blocker",
redacted: true
}
});
return true;
}
if (options.agentFixtureMode === "slow-blocker") {
jsonResponse(response, 200, {
conversationId,
sessionId: conversationId,
messageId,
status: "failed",
sourceKind: "SOURCE",
evidenceLevel: "SOURCE",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider: "codex-stdio",
model: "codex-stdio",
backend: "local-source-fixture/codex-stdio-slow-structured-blocker",
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
workspace: "/workspace/hwlab",
sandbox: "read-only",
runner: {
kind: "codex-mcp-stdio-runner",
writeCapable: true,
codexStdio: true,
longLivedSession: true,
durableSession: true
},
capabilityLevel: "blocked",
sessionMode: "codex-mcp-stdio-long-lived",
session: {
sessionId: conversationId,
conversationId,
status: "idle",
turn: 1,
lastTraceId: traceId
},
sessionReuse: {
conversationId,
sessionId: conversationId,
mapped: true,
reused: true,
turn: 1,
status: "idle"
},
error: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
message: "SOURCE slow structured blocker: no readable SKILL.md files were found.",
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
retryable: true,
blocker: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
retryable: true
}
},
blocker: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
retryable: true
},
blockers: [
{
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
retryable: true
}
],
toolCalls: [
{
name: "skills.discover",
status: "blocked",
exitCode: 2,
traceId,
stderrSummary: "skills_unavailable",
blocker: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli"
}
}
],
skills: {
status: "blocked",
items: [],
count: 0,
totalCount: 0,
blockers: [
{
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli"
}
]
},
runnerTrace: {
traceId,
runnerKind: "codex-mcp-stdio-runner",
sessionMode: "codex-mcp-stdio-long-lived",
status: "blocked",
events: [
{ seq: 1, traceId, type: "session", stage: "created", status: "completed", label: "session:created" },
{ seq: 2, traceId, type: "prompt", stage: "sent", status: "completed", label: "prompt:sent" },
{ seq: 3, traceId, type: "error", stage: "blocked", status: "blocked", label: "codex-stdio:blocked", errorCode: "skills_unavailable", waitingFor: "codex-stdio" }
],
lastEvent: { seq: 3, traceId, type: "error", stage: "blocked", status: "blocked", label: "codex-stdio:blocked", errorCode: "skills_unavailable", waitingFor: "codex-stdio" }
},
conversationFacts: {
conversationId,
sessionId: conversationId,
sessionStatus: "idle",
sessionMode: "codex-mcp-stdio-long-lived",
turnCount: 1,
latestTraceId: traceId,
traceIds: [traceId],
latestSkills: {
status: "blocked",
count: 0,
totalCount: 0,
names: [],
blockers: ["skills_unavailable"]
},
recentToolCalls: [{ name: "skills.discover", status: "blocked" }],
facts: [],
valuesRedacted: true,
secretMaterialStored: false
},
providerTrace: {
source: "SOURCE-local-browser-fixture",
sourceKind: "SOURCE",
responseId: "rsp_source_fixture_slow_blocker",
redacted: true
}
});
return true;
}
const fixtureSkills = isSkillListPrompt
? {
status: "ready",
+3 -3
View File
@@ -301,7 +301,7 @@ export function classifyCodeAgentSkillPath(payload, { httpStatus = null, httpOk
);
const runnerKind = payload?.runner?.kind ?? null;
const controlledSessionRunner =
runnerKind === "codex-mcp-stdio-runner" ||
runnerKind === "codex-app-server-stdio-runner" ||
runnerKind === "hwlab-readonly-runner";
const missing = [];
if (!httpOk) missing.push(`HTTP ${httpStatus ?? "not_observed"}`);
@@ -507,7 +507,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
const routeEvidenceReady = route === HWLAB_M3_IO_API_ROUTE || route === M3_IO_CONTROL_ROUTE;
const routePass = httpOk === true &&
payload?.status === "completed" &&
payload?.provider === "hwlab-skill-cli" &&
payload?.provider === "legacy-skill-cli" &&
skillTool &&
["completed", "blocked"].includes(skillTool.status) &&
routeEvidenceReady &&
@@ -522,7 +522,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
const missing = [];
if (!httpOk) missing.push(`HTTP ${httpStatus ?? "not_observed"}`);
if (payload?.status !== "completed") missing.push("payload.status=completed");
if (payload?.provider !== "hwlab-skill-cli") missing.push("provider=hwlab-skill-cli");
if (payload?.provider !== "legacy-skill-cli") missing.push("provider=legacy-skill-cli");
if (!skillTool) missing.push(`${HWLAB_M3_IO_SKILL_NAME} toolCall`);
if (!routeEvidenceReady) missing.push(`route=${HWLAB_M3_IO_API_ROUTE}`);
if (method !== "POST") missing.push("method=POST");
+16 -271
View File
@@ -142,12 +142,12 @@ test("Code Agent Codex stdio skill discovery passes with completed toolCalls and
traceId: "trc_test",
provider: "codex-stdio",
model: "gpt-5.5",
backend: "hwlab-cloud-api/codex-mcp-stdio-runner",
backend: "hwlab-cloud-api/codex-app-server-stdio",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
session: {
sessionId: "ses_test",
status: "idle",
@@ -161,17 +161,17 @@ test("Code Agent Codex stdio skill discovery passes with completed toolCalls and
turn: 1
},
runner: {
kind: "codex-mcp-stdio-runner",
kind: "codex-app-server-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
codexStdio: true,
writeCapable: true,
durableSession: true
},
runnerTrace: {
runnerKind: "codex-mcp-stdio-runner"
runnerKind: "codex-app-server-stdio-runner"
},
longLivedSessionGate: {
status: "pass",
@@ -286,139 +286,6 @@ test("Code Agent fallback-only turns cannot satisfy session checks", () => {
assert.ok(result.observations.blockers.includes("fallback-only"));
});
test("Skill CLI M3 route passes with operation/audit/evidence ids and no direct path", () => {
const result = classifySkillCliM3Route(skillCliPayload(), { httpStatus: 200, httpOk: true });
assert.equal(result.status, "pass");
assert.equal(result.observations.route, "/v1/m3/io");
assert.equal(result.observations.method, "POST");
assert.equal(result.observations.idsPresent, true);
assert.equal(result.observations.accepted, true);
assert.equal(result.observations.controlReady, true);
assert.equal(result.observations.readback.value, true);
assert.equal(result.observations.safety.directGatewayCalls, false);
assert.equal(result.observations.safety.fallbackUsed, false);
});
test("Skill CLI M3 route passes with structured blocker and no direct path", () => {
const payload = skillCliPayload({
accepted: false,
status: "blocked",
operationId: null,
auditId: null,
evidenceId: null,
blocker: {
code: "hwlab_api_unavailable",
message: "HWLAB API unavailable"
}
});
const result = classifySkillCliM3Route(payload, { httpStatus: 200, httpOk: true });
assert.equal(result.status, "pass");
assert.equal(result.observations.structuredBlocker, true);
assert.equal(result.observations.idsPresent, false);
});
test("Skill CLI M3 route blocks missing route and direct gateway evidence", () => {
const payload = skillCliPayload({
route: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101/invoke",
directGatewayCalls: true
});
const result = classifySkillCliM3Route(payload, { httpStatus: 200, httpOk: true });
assert.equal(result.status, "blocked");
assert.match(result.summary, /route=\/v1\/m3\/io/u);
assert.equal(result.observations.safety.directGatewayCalls, true);
});
test("Skill CLI M3 route requires explicit POST method evidence", () => {
const result = classifySkillCliM3Route(skillCliPayload({ method: null }), { httpStatus: 200, httpOk: true });
assert.equal(result.status, "blocked");
assert.match(result.summary, /method=POST/u);
assert.ok(result.observations.missing.includes("method=POST"));
});
test("RPT-004 live report defaults to read-only M3 route checks and records no live mutation", async () => {
const report = await buildRpt004Report({
...parseArgs([
"--live",
"--url",
"http://74.48.78.17:16666/",
"--api-url",
"http://74.48.78.17:16667/",
"--expected-commit",
fixtureCommit
]),
writeReport: false
}, {
repoRoot: "/fixture-rpt004",
fsJson: fixtureArtifactJson(),
now: () => "2026-05-23T00:00:00.000Z",
httpGetJson: async (url) => ({ ok: true, status: 200, json: healthPayload(url) }),
httpGetText: async () => ({
ok: true,
status: 200,
contentType: "text/html; charset=utf-8",
body: workbenchHtml()
}),
layoutRunner: async () => ({ status: "pass", summary: "layout ok", artifacts: { screenshots: [] } }),
codeAgentRunner: async ({ turn, conversationId, sessionId }) => ({
ok: true,
status: 200,
json: codexSessionPayload({
conversationId,
sessionId,
toolName: turn.kind === "skills" ? "skills.discover" : "pwd",
turn: turn.kind === "pwd" ? 1 : turn.kind === "skills" ? 2 : 3,
reused: turn.kind !== "pwd",
skills: turn.kind === "skills" ? {
status: "ready",
items: [{
name: "hwlab-agent-runtime",
source: "/workspace/hwlab/skills/hwlab-agent-runtime/SKILL.md"
}],
count: 1
} : undefined,
reply: turn.kind === "context-pwd"
? "前文第一轮你问的是 pwd;当前工作目录仍是 /workspace/hwlab。"
: "当前工作目录是 /workspace/hwlab。"
})
}),
skillCliRunner: async () => ({
ok: true,
status: 200,
json: skillCliPayload()
})
});
assert.equal(report.status, "pass");
assert.equal(report.liveMutationUsed, false);
assert.equal(report.safety.liveMutationUsed, false);
assert.equal(report.commanderSummary.allowedLiveMutationUsed, false);
assert.equal(report.commanderSummary.sessionResult.status, "pass");
assert.equal(report.commanderSummary.skillCliResult.status, "pass");
assert.equal(report.commanderSummary.m3RouteResult.route, "/v1/m3/io");
assert.equal(report.commanderSummary.m3RouteResult.method, "POST");
assert.equal(report.commanderSummary.m3RouteResult.accepted, true);
assert.equal(report.commanderSummary.m3RouteResult.controlReady, true);
assert.equal(report.commanderSummary.m3RouteResult.readback.value, true);
assert.equal(report.commanderSummary.exactRouteEvidenceContract.visible, true);
assert.equal(report.commanderSummary.exactRouteEvidenceContract.route, "/v1/m3/io");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.method, "POST");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.operationId, "op_rpt004_skill_m3");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.traceId, "trc_rpt004_skill");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.auditId, "aud_rpt004_skill_m3");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.evidenceId, "evd_rpt004_skill_m3");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.accepted, true);
assert.equal(report.commanderSummary.exactRouteEvidenceContract.status, "completed");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.readback.value, true);
assert.equal(report.commanderSummary.evidenceIds.evidenceId, "evd_rpt004_skill_m3");
assert.equal(report.dimensions.skillCliM3Route.status, "pass");
assert.equal(report.dimensions.m3TrustedLoop.observations.skippedWriteLoop, true);
assert.equal(report.dimensions.durableEvidence.observations.notApplicableInDefaultReadOnlyMode, true);
});
test("RPT-004 live report emits NOT_CURRENT and skips downstream checks", async () => {
const report = await buildRpt004Report({
...parseArgs([
@@ -530,12 +397,12 @@ function codexSessionPayload({
traceId: `trc_rpt004_${turn}`,
provider: "codex-stdio",
model: "gpt-5.5",
backend: "hwlab-cloud-api/codex-mcp-stdio-runner",
backend: "hwlab-cloud-api/codex-app-server-stdio",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
session: {
sessionId,
status: "idle",
@@ -555,20 +422,20 @@ function codexSessionPayload({
status: "idle"
},
runner: {
kind: "codex-mcp-stdio-runner",
kind: "codex-app-server-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
session: "codex-mcp-stdio-long-lived",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
session: "codex-app-server-stdio-long-lived",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
codexStdio: true,
writeCapable: true,
durableSession: true
},
runnerTrace: {
traceId: `trc_rpt004_${turn}`,
runnerKind: "codex-mcp-stdio-runner",
sessionMode: "codex-mcp-stdio-long-lived",
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
sessionId,
turn,
sessionReused: reused,
@@ -620,128 +487,6 @@ function openAiFallbackPayload({ reply = "text fallback" } = {}) {
}
};
}
function skillCliPayload({
route = "/v1/m3/io",
method = "POST",
accepted = true,
status = "succeeded",
operationId = "op_rpt004_skill_m3",
auditId = "aud_rpt004_skill_m3",
evidenceId = "evd_rpt004_skill_m3",
blocker = null,
directGatewayCalls = false,
directBoxCalls = false,
directPatchPanelCalls = false
} = {}) {
return {
status: "completed",
conversationId: "cnv_rpt004_skill",
sessionId: "ses_rpt004_skill",
messageId: "msg_rpt004_skill",
traceId: "trc_rpt004_skill",
provider: "hwlab-skill-cli",
model: "controlled-m3-io",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
workspace: "/workspace/hwlab",
sandbox: "hwlab-api-route-only",
capabilityLevel: accepted ? "hwlab-api-control-ready" : "hwlab-api-control-blocked",
sessionMode: "controlled-m3-io-skill-cli",
implementationType: "skill-cli-hwlab-api-adapter",
runner: {
kind: "hwlab-m3-io-skill-cli",
safety: {
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false
}
},
runnerTrace: {
traceId: "trc_rpt004_skill",
runnerKind: "hwlab-m3-io-skill-cli",
method,
route,
fallbackUsed: false,
directGatewayCalls,
directBoxCalls,
directPatchPanelCalls
},
providerTrace: {
runnerKind: "hwlab-m3-io-skill-cli",
route,
method,
traceId: "trc_rpt004_skill",
operationId,
auditId,
evidenceId,
accepted,
controlReady: accepted,
fallbackUsed: false,
readback: {
status: "succeeded",
value: true,
resourceId: "res_boxsimu_2",
port: "DI1"
}
},
toolCalls: [{
name: "hwlab-agent-runtime.m3-io",
status: accepted ? "completed" : "blocked",
type: "skill-cli",
route,
method,
accepted,
controlReady: accepted,
capabilityLevel: accepted ? "hwlab-api-control-ready" : "hwlab-api-control-blocked",
operationId,
traceId: "trc_rpt004_skill",
auditId,
evidenceId,
audit: {
auditId,
status
},
evidence: {
evidenceId,
status: accepted ? "blocked" : "blocked",
sourceKind: accepted ? "BLOCKED" : "BLOCKED",
blocker: blocker?.code ?? "runtime_durable_not_green",
writeStatus: "written_non_durable"
},
durable: {
status: "degraded",
blocker: blocker?.code ?? "runtime_durable_not_green"
},
readback: {
status: "succeeded",
value: true,
resourceId: "res_boxsimu_2",
port: "DI1"
},
blocker,
blockers: blocker ? [blocker] : [{
code: "runtime_durable_not_green",
message: "durable trust is not green"
}],
directGatewayCalls,
directBoxCalls,
directPatchPanelCalls,
fallbackUsed: false
}],
skills: {
status: "used",
items: [{
name: "hwlab-agent-runtime.m3-io",
route
}],
blockers: blocker ? [blocker] : []
},
reply: {
content: "M3 IO Skill CLI result"
}
};
}
function healthPayload(url, { revision = fixtureCommit } = {}) {
const isWeb = String(url).includes(":16666");
return {