From 24f818adaea5c3dbaac66ccf42b497bc64631e46 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sat, 23 May 2026 14:56:08 +0000 Subject: [PATCH] feat: show code agent M3 evidence metadata --- package.json | 2 +- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 27 +- web/hwlab-cloud-web/app.mjs | 92 ++++- .../code-agent-m3-evidence.mjs | 369 ++++++++++++++++++ .../code-agent-m3-evidence.test.mjs | 239 ++++++++++++ web/hwlab-cloud-web/package.json | 2 +- web/hwlab-cloud-web/scripts/check.mjs | 87 +++++ web/hwlab-cloud-web/scripts/dist-contract.mjs | 1 + web/hwlab-cloud-web/styles.css | 98 ++++- 9 files changed, 908 insertions(+), 9 deletions(-) create mode 100644 web/hwlab-cloud-web/code-agent-m3-evidence.mjs create mode 100644 web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs diff --git a/package.json b/package.json index c519c546..2f54bcea 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "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/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 --test scripts/artifact-runtime-readiness-guard.test.mjs", - "check": "node --check internal/protocol/index.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/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/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-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-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/dev-artifact-publish.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/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 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/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 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 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/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 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/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 --check internal/protocol/index.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/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/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-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-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/dev-artifact-publish.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/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 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 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/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 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/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", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs", "m1:smoke": "node scripts/m1-contract-smoke.mjs", diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index eb68c68f..905dbfa9 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -75,6 +75,7 @@ const requiredWebAssets = Object.freeze([ "app.mjs", "code-agent-facts.mjs", "code-agent-status.mjs", + "code-agent-m3-evidence.mjs", "live-status.mjs", "runtime.mjs", "gate-summary.mjs", @@ -252,7 +253,10 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([ "untrusted_completion", "SOURCE fixture", "Code Agent SOURCE 回复", - "message-evidence-chip" + "message-evidence-chip", + "hwlab-skill-cli", + "hwlab-m3-io-skill-cli", + "message-m3-evidence" ]); const blockedCodeAgentUiLabels = Object.freeze([ @@ -1652,6 +1656,7 @@ function readStaticFiles() { wiringStatus: readText("web/hwlab-cloud-web/wiring-status.mjs"), runtime: readText("web/hwlab-cloud-web/runtime.mjs"), panel: readText("web/hwlab-cloud-web/workbench-hardware-panel.mjs"), + codeAgentM3Evidence: readText("web/hwlab-cloud-web/code-agent-m3-evidence.mjs"), help: readText("web/hwlab-cloud-web/help.md") }; } @@ -2411,12 +2416,13 @@ function hasCodeAgentReadinessVisibility({ html, app, liveStatus = "" }) { ); } -function hasCodeAgentCompletedEvidenceVisibility({ app }) { +function hasCodeAgentCompletedEvidenceVisibility({ app, codeAgentM3Evidence }) { const messageEvidenceBody = functionBody(app, "messageEvidenceFields"); const realEvidenceBody = functionBody(app, "hasRealCodeAgentEvidence"); const runnerEvidenceBody = functionBody(app, "isCodexRunnerCapableEvidence"); + const codeAgentEvidenceSource = `${app}\n${codeAgentM3Evidence}`; return ( - requiredCodeAgentEvidenceTerms.every((term) => app.includes(term)) && + requiredCodeAgentEvidenceTerms.every((term) => codeAgentEvidenceSource.includes(term)) && /conversationId:\s*result\.conversationId \|\| result\.sessionId \|\| state\.conversationId/u.test(app) && /providerTrace:\s*result\.providerTrace/u.test(app) && /function\s+providerTraceSummary\s*\(/u.test(app) && @@ -2441,6 +2447,18 @@ function hasCodeAgentCompletedEvidenceVisibility({ app }) { /recordField\("trace",\s*message\.traceId\)/u.test(messageEvidenceBody) && /recordField\("message",\s*message\.messageId\)/u.test(messageEvidenceBody) && /providerTraceSummary\(message\.providerTrace\)/u.test(messageEvidenceBody) && + /extractCodeAgentM3Evidence\(result\)/u.test(app) && + /messageM3EvidencePanel/u.test(app) && + /m3EvidenceRows/u.test(app) && + /copyButton/u.test(app) && + /isCodeAgentM3SkillCompletion\(value\)/u.test(realEvidenceBody) && + /export function extractCodeAgentM3Evidence/u.test(codeAgentM3Evidence) && + /operationId/u.test(codeAgentM3Evidence) && + /auditId/u.test(codeAgentM3Evidence) && + /evidenceId/u.test(codeAgentM3Evidence) && + /readback-mismatch/u.test(codeAgentM3Evidence) && + /direct-path-invalid/u.test(codeAgentM3Evidence) && + /未产生\/不可证明/u.test(codeAgentM3Evidence) && /isTrustedCodeAgentProvider\(value\?\.provider\)/u.test(realEvidenceBody) && /isCodexRunnerCapableEvidence\(value\)/u.test(realEvidenceBody) && /CODEX_RUNNER_CAPABLE_PROVIDERS/u.test(app) && @@ -2529,6 +2547,9 @@ function hasCodeAgentConversationUxStates({ app, styles }) { /status-source/u.test(styles) && /message-user/u.test(styles) && /message-agent/u.test(styles) && + /message-m3-evidence\s*\{/u.test(styles) && + /message-m3-rows\s*\{/u.test(styles) && + /copy-chip\s*\{/u.test(styles) && /message-evidence\s*\{/u.test(styles) && /message-evidence-chip/u.test(styles) && /width:\s*min\(100%,\s*780px\)/u.test(styles) && diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 9e44facd..0975a769 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -10,6 +10,12 @@ import { classifyCodeAgentStatusSummary, codeAgentAvailabilityFromLive } from ". import { classifyWorkbenchLiveStatus } from "./live-status.mjs"; import { marked } from "./third_party/marked/marked.esm.js"; import { wiringPresentationFromSummary } from "./wiring-status.mjs"; +import { + extractCodeAgentM3Evidence, + isCodeAgentM3SkillCompletion, + m3EvidenceRows, + m3EvidenceSummaryText +} from "./code-agent-m3-evidence.mjs"; const DEFAULT_API_TIMEOUT_MS = 4500; const DEFAULT_CODE_AGENT_TIMEOUT_MS = 150000; @@ -62,7 +68,7 @@ const M3_TRUSTED_ROUTE = Object.freeze({ const LIVE_M3_ID_FIELDS = Object.freeze(["operationId", "traceId", "auditId", "evidenceId"]); const LIVE_M3_PASS_STATUSES = Object.freeze(["pass", "passed", "verified", "succeeded", "trusted", "completed"]); const NON_LIVE_ID_VALUES = Object.freeze(["", "n/a", "none", "null", "undefined", "not_observed", "not-observed"]); -const TRUSTED_CODE_AGENT_PROVIDERS = Object.freeze(["openai-responses", "codex-cli", "codex-readonly-runner", "codex-stdio"]); +const TRUSTED_CODE_AGENT_PROVIDERS = Object.freeze(["openai-responses", "codex-cli", "codex-readonly-runner", "codex-stdio", "hwlab-skill-cli"]); const CODEX_RUNNER_CAPABLE_PROVIDERS = Object.freeze(["codex-stdio"]); const CODEX_READONLY_PARTIAL_PROVIDERS = Object.freeze(["codex-readonly-runner"]); const CODEX_STDIO_BLOCKER_MARKERS = Object.freeze([ @@ -689,6 +695,7 @@ function initCommandBar() { skills: result.skills, runner: result.runner, runnerTrace: result.runnerTrace, + m3Evidence: extractCodeAgentM3Evidence(result), capabilityLevel: result.capabilityLevel, sourceKind: completion.sourceKind, providerTrace: result.providerTrace, @@ -2559,6 +2566,8 @@ function messageCard(message) { article.append(textSpan(message.text, "message-copy")); const capabilityFacts = codeAgentCapabilityFactsPanel(message); if (capabilityFacts) article.append(capabilityFacts); + const m3Evidence = messageM3EvidencePanel(message); + if (m3Evidence) article.append(m3Evidence); const messageEvidence = messageEvidencePanel(message); if (messageEvidence) article.append(messageEvidence); return article; @@ -2606,6 +2615,42 @@ function codeAgentFactsHeader(facts) { return header; } +function messageM3EvidencePanel(message) { + const evidence = message.m3Evidence ?? extractCodeAgentM3Evidence(message); + if (!evidence) return null; + const section = document.createElement("section"); + section.className = `message-m3-evidence tone-border-${toneClass(evidence.verdict.tone)}`; + section.dataset.m3Verdict = evidence.verdict.key; + section.append(m3EvidenceHeader(evidence)); + const rows = document.createElement("div"); + rows.className = "message-m3-rows"; + rows.append(...m3EvidenceRows(evidence).map(m3EvidenceRowElement)); + section.append(rows); + return section; +} + +function m3EvidenceHeader(evidence) { + const header = document.createElement("div"); + header.className = "message-m3-head"; + header.append( + badge(evidence.verdict.label, evidence.verdict.tone), + textSpan(m3EvidenceSummaryText(evidence), "message-m3-summary") + ); + return header; +} + +function m3EvidenceRowElement(row) { + const item = document.createElement("div"); + item.className = `message-m3-row tone-border-${toneClass(row.tone)}`; + item.append(textSpan(row.label, "message-m3-key")); + const value = row.href ? safeLink(row.href, row.value, "message-m3-value mono") : textSpan(row.value, "message-m3-value mono"); + item.append(value); + if (row.copyable) { + item.append(copyButton(row.value, row.label)); + } + return item; +} + function messageEvidencePanel(message) { const fields = messageEvidenceFields(message).map(boundedEvidenceField); if (fields.length === 0) return null; @@ -2753,6 +2798,48 @@ function badge(text, tone = text) { return span; } +function copyButton(value, label = "value") { + const button = document.createElement("button"); + button.className = "copy-chip"; + button.type = "button"; + button.textContent = "复制"; + button.title = `复制 ${label}`; + button.addEventListener("click", async () => { + await copyTextToClipboard(value); + button.textContent = "已复制"; + window.setTimeout(() => { + button.textContent = "复制"; + }, 1200); + }); + return button; +} + +async function copyTextToClipboard(value) { + const text = String(value ?? ""); + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return; + } + const input = document.createElement("textarea"); + input.value = text; + input.setAttribute("readonly", ""); + input.style.position = "fixed"; + input.style.left = "-9999px"; + document.body.append(input); + input.select(); + document.execCommand("copy"); + input.remove(); +} + +function safeLink(href, text, className) { + const anchor = document.createElement("a"); + anchor.href = href; + anchor.textContent = text ?? href; + anchor.rel = "noreferrer"; + if (className) anchor.className = className; + return anchor; +} + function textSpan(text, className) { const span = document.createElement("span"); span.textContent = text ?? "无"; @@ -3026,6 +3113,9 @@ function isSourceFixtureCompletedChatMessage(message) { } function hasRealCodeAgentEvidence(value) { + if (isCodeAgentM3SkillCompletion(value)) { + return true; + } if (isCodexRunnerCapableEvidence(value)) { return true; } diff --git a/web/hwlab-cloud-web/code-agent-m3-evidence.mjs b/web/hwlab-cloud-web/code-agent-m3-evidence.mjs new file mode 100644 index 00000000..fdd5f24c --- /dev/null +++ b/web/hwlab-cloud-web/code-agent-m3-evidence.mjs @@ -0,0 +1,369 @@ +export const CODE_AGENT_M3_IO_ROUTE = "/v1/m3/io"; +export const CODE_AGENT_M3_TRUSTED_ROUTE = Object.freeze({ + fromResourceId: "res_boxsimu_1", + fromPort: "DO1", + patchPanelServiceId: "hwlab-patch-panel", + toResourceId: "res_boxsimu_2", + toPort: "DI1" +}); +export const CODE_AGENT_M3_SKILL_PROVIDER = "hwlab-skill-cli"; +export const CODE_AGENT_M3_SKILL_RUNNER_KIND = "hwlab-m3-io-skill-cli"; +export const CODE_AGENT_M3_SKILL_NAME = "hwlab-agent-runtime.m3-io"; + +const missingProofLabel = "未产生/不可证明"; +const nonProofIds = new Set(["", "n/a", "none", "null", "undefined", "not_observed", "not-observed"]); +const directPathFields = Object.freeze([ + "directGatewayCalls", + "directBoxCalls", + "directPatchPanelCalls", + "directGatewayCallsAllowed", + "directBoxCallsAllowed", + "directBoxSimuCallsAllowed", + "directPatchPanelCallsAllowed" +]); +const safeInternalLinkPattern = /^\/(?:v1\/(?:m3|audit|evidence|operations?)|json-rpc\b|gate\b|diagnostics\/gate\b)/u; + +export function codeAgentM3EvidenceContractSummary() { + return [ + "Code Agent M3 evidence renderer consumes Skill CLI / /v1/m3/io facts from providerTrace, runnerTrace, and toolCalls.", + "It renders operation/audit/evidence metadata: operationId, traceId, auditId, evidenceId, route, target, readback, accepted/status/blocker, and direct-path validity.", + "Missing proof IDs render as 未产生/不可证明; direct gateway/box/patch-panel paths never render as trusted pass." + ].join(" "); +} + +export function isCodeAgentM3SkillCompletion(value) { + const evidence = extractCodeAgentM3Evidence(value); + return Boolean( + evidence && + value?.status === "completed" && + evidence.route === CODE_AGENT_M3_IO_ROUTE && + evidence.directPathInvalid !== true && + evidence.provider === CODE_AGENT_M3_SKILL_PROVIDER && + evidence.runnerKind === CODE_AGENT_M3_SKILL_RUNNER_KIND + ); +} + +export function extractCodeAgentM3Evidence(message) { + const toolCall = findM3ToolCall(message); + const source = toolCall ?? message?.providerTrace ?? message?.runnerTrace ?? null; + if (!source && !isM3ProviderMessage(message)) return null; + + const providerTrace = objectOrNull(message?.providerTrace); + const runnerTrace = objectOrNull(message?.runnerTrace); + const route = firstNonEmpty(source?.route, providerTrace?.route, runnerTrace?.route, CODE_AGENT_M3_IO_ROUTE); + const provider = firstNonEmpty(message?.provider, source?.provider, providerTrace?.provider); + const runnerKind = firstNonEmpty(message?.runner?.kind, source?.runnerKind, providerTrace?.runnerKind, runnerTrace?.runnerKind); + const command = objectOrNull(source?.command) ?? objectOrNull(message?.command); + const result = objectOrNull(source?.result) ?? objectOrNull(message?.result); + const response = parseToolStdout(source?.stdout); + const blocker = firstBlocker(source, response, providerTrace, runnerTrace, message); + const target = normalizeTarget(source, response, command); + const readback = normalizeReadback(source, response, result); + const ids = { + operationId: firstNonEmpty(source?.operationId, response?.operationId, providerTrace?.operationId, runnerTrace?.operationId, message?.operationId), + traceId: firstNonEmpty(source?.traceId, response?.traceId, providerTrace?.traceId, runnerTrace?.traceId, message?.traceId), + auditId: firstNonEmpty(source?.audit?.auditId, response?.audit?.auditId, message?.auditId, message?.audit?.auditId), + evidenceId: firstNonEmpty(source?.evidence?.evidenceId, response?.evidence?.evidenceId, message?.evidenceId, message?.evidence?.evidenceId) + }; + const directPathInvalid = classifyDirectPathInvalid(source, response, message); + const readbackMismatch = classifyReadbackMismatch({ + action: firstNonEmpty(source?.action, response?.action, command?.action), + target, + readback, + blocker + }); + const accepted = normalizeBoolean(firstDefined(source?.accepted, response?.accepted, providerTrace?.accepted)); + const status = firstNonEmpty(source?.status, response?.status, providerTrace?.status, runnerTrace?.status, message?.status); + const links = normalizeEvidenceLinks(source, response, providerTrace, message); + + if (!isM3EvidenceShape({ route, provider, runnerKind, source })) return null; + + return { + kind: "m3-io", + provider, + runnerKind, + route, + status, + accepted, + blocker, + directPathInvalid, + readbackMismatch, + target, + readback, + ids, + links, + verdict: classifyM3Verdict({ + accepted, + status, + blocker, + directPathInvalid, + readbackMismatch, + ids + }), + source + }; +} + +export function m3EvidenceRows(evidence) { + if (!evidence) return []; + const idRows = [ + evidenceValueRow("operationId", evidence.ids.operationId, { copyable: true, href: evidence.links.operationId }), + evidenceValueRow("traceId", evidence.ids.traceId, { copyable: true, href: evidence.links.traceId }), + evidenceValueRow("auditId", evidence.ids.auditId, { copyable: true, href: evidence.links.auditId }), + evidenceValueRow("evidenceId", evidence.ids.evidenceId, { copyable: true, href: evidence.links.evidenceId }) + ]; + const target = evidence.target; + const readback = evidence.readback; + return [ + evidenceValueRow("状态", evidence.verdict.label, { tone: evidence.verdict.tone }), + evidenceValueRow("accepted", evidence.accepted === null ? "unknown" : String(evidence.accepted), { tone: evidence.accepted === true ? "source" : "blocked" }), + evidenceValueRow("status", evidence.status ?? missingProofLabel, { tone: evidence.status === "blocked" ? "blocked" : "source" }), + evidenceValueRow("route", evidence.route ?? missingProofLabel, { copyable: true, href: evidence.links.route }), + evidenceValueRow("target", resourcePortValueLabel(target), { copyable: false }), + evidenceValueRow("readback", resourcePortValueLabel(readback), { tone: evidence.readbackMismatch ? "blocked" : "source" }), + ...idRows, + evidenceValueRow("blocker", blockerLabel(evidence.blocker), { tone: evidence.blocker ? "blocked" : "source" }) + ]; +} + +export function m3EvidenceSummaryText(evidence) { + if (!evidence) return ""; + return `${evidence.verdict.label} / route=${evidence.route ?? missingProofLabel} / operation=${proofLabel(evidence.ids.operationId)} / evidence=${proofLabel(evidence.ids.evidenceId)}`; +} + +export function proofLabel(value) { + const text = stringOrNull(value); + return text && !nonProofIds.has(text.toLowerCase()) ? text : missingProofLabel; +} + +function evidenceValueRow(label, value, options = {}) { + const proofValue = label.endsWith("Id") ? proofLabel(value) : String(value ?? missingProofLabel); + return { + label, + value: proofValue, + tone: options.tone ?? (proofValue === missingProofLabel ? "blocked" : "source"), + copyable: options.copyable === true && proofValue !== missingProofLabel, + href: safeInternalHref(options.href) + }; +} + +function classifyM3Verdict({ accepted, status, blocker, directPathInvalid, readbackMismatch, ids }) { + if (directPathInvalid) { + return { + key: "direct-path-invalid", + tone: "blocked", + label: "direct-path-invalid", + summary: "直接 gateway/box/patch-panel 路径无效,不能显示为可信闭环通过。" + }; + } + if (readbackMismatch) { + return { + key: "readback-mismatch", + tone: "blocked", + label: "readback-mismatch", + summary: "DI1 readback 与 DO1 target value 不一致,不能证明闭环。" + }; + } + if (blocker || accepted === false || String(status ?? "").toLowerCase() === "blocked") { + return { + key: "blocked", + tone: "blocked", + label: "blocked", + summary: blockerLabel(blocker) + }; + } + if (accepted === true && ["succeeded", "completed", "accepted"].includes(String(status ?? "").toLowerCase())) { + const missingIds = Object.entries(ids).filter(([, value]) => proofLabel(value) === missingProofLabel); + if (missingIds.length > 0) { + return { + key: "accepted-missing-proof", + tone: "degraded", + label: "accepted / 缺少证据ID", + summary: `accepted 但缺少 ${missingIds.map(([key]) => key).join(", ")},不能证明完整可信闭环。` + }; + } + return { + key: "accepted", + tone: "dev-live", + label: "accepted", + summary: "受控 Skill CLI 已通过 HWLAB API /v1/m3/io 返回 operation/audit/evidence facts。" + }; + } + return { + key: "unknown", + tone: "blocked", + label: "未产生/不可证明", + summary: "没有足够的 M3 IO 事实证明该回复执行了受控路径。" + }; +} + +function classifyReadbackMismatch({ action, target, readback, blocker }) { + if (String(action ?? "").toLowerCase() !== "do.write") return false; + if (target.value === undefined || target.value === null || readback.value === undefined || readback.value === null) return false; + if (normalizeBoolean(target.value) === normalizeBoolean(readback.value)) return false; + return true; +} + +function classifyDirectPathInvalid(...sources) { + for (const source of sources) { + if (!source || typeof source !== "object") continue; + const safety = objectOrNull(source.safety) ?? {}; + const hwlabApi = objectOrNull(source.hwlabApi) ?? {}; + const controlPath = objectOrNull(source.controlPath) ?? {}; + const blockerText = [ + source.blocker?.code, + source.blocker?.message, + source.blocker?.zh, + source.capabilityBlocker?.code, + source.capabilityBlocker?.message, + source.capabilityBlocker?.zh, + source.error?.code, + source.error?.message + ].filter(Boolean).join(" "); + if (/direct[_-]?(?:hardware|gateway|box|patch)/iu.test(blockerText)) return true; + if (controlPath.frontendBypass === true || hwlabApi.cloudApiOnly === false) return true; + for (const field of directPathFields) { + if (source[field] === true || safety[field] === true || hwlabApi[field] === true) return true; + } + } + return false; +} + +function normalizeTarget(source, response, command) { + const target = objectOrNull(source?.target) ?? objectOrNull(response?.target) ?? {}; + const commandObject = objectOrNull(command) ?? objectOrNull(response?.command) ?? {}; + return { + resourceId: firstNonEmpty(target.resourceId, commandObject.resourceId, source?.resourceId, response?.resourceId, CODE_AGENT_M3_TRUSTED_ROUTE.fromResourceId), + port: firstNonEmpty(target.port, commandObject.port, source?.port, response?.port, CODE_AGENT_M3_TRUSTED_ROUTE.fromPort), + value: firstDefined(target.value, commandObject.value, source?.value, response?.value) + }; +} + +function normalizeReadback(source, response, result) { + const targetReadback = objectOrNull(result?.targetReadback) ?? + objectOrNull(response?.result?.targetReadback) ?? + objectOrNull(source?.result?.targetReadback) ?? + objectOrNull(source?.targetReadback) ?? + {}; + return { + resourceId: firstNonEmpty(targetReadback.resourceId, CODE_AGENT_M3_TRUSTED_ROUTE.toResourceId), + port: firstNonEmpty(targetReadback.port, CODE_AGENT_M3_TRUSTED_ROUTE.toPort), + value: firstDefined(targetReadback.value, result?.value) + }; +} + +function resourcePortValueLabel(item) { + const value = item.value === undefined || item.value === null ? "value=未产生/不可证明" : `value=${String(item.value)}`; + return `${item.resourceId ?? missingProofLabel}:${item.port ?? missingProofLabel} ${value}`; +} + +function firstBlocker(...sources) { + for (const source of sources) { + if (!source || typeof source !== "object") continue; + const candidate = objectOrNull(source.blocker) ?? objectOrNull(source.capabilityBlocker) ?? objectOrNull(source.trustBlocker); + if (candidate) return candidate; + if (Array.isArray(source.blockers)) { + const blocker = source.blockers.find((item) => item?.code || item?.message || item?.zh); + if (blocker) return blocker; + } + } + return null; +} + +function blockerLabel(blocker) { + if (!blocker) return "none"; + return [blocker.code, blocker.zh ?? blocker.message ?? blocker.reason].filter(Boolean).join(": "); +} + +function normalizeEvidenceLinks(...sources) { + const links = {}; + for (const source of sources) { + if (!source || typeof source !== "object") continue; + const candidates = [ + source.links, + source.urls, + source.detailUrls, + source.internalRoutes, + source.evidence?.links, + source.audit?.links + ].filter((item) => item && typeof item === "object"); + for (const candidate of candidates) { + for (const key of ["operationId", "traceId", "auditId", "evidenceId", "route"]) { + links[key] ??= safeInternalHref(candidate[key] ?? candidate[key.replace(/Id$/u, "")]); + } + } + } + return links; +} + +function safeInternalHref(value) { + const text = stringOrNull(value); + if (!text || !safeInternalLinkPattern.test(text)) return null; + return text; +} + +function parseToolStdout(stdout) { + const text = stringOrNull(stdout); + if (!text) return null; + try { + const payload = JSON.parse(text); + return payload && typeof payload === "object" ? payload : null; + } catch { + return null; + } +} + +function isM3EvidenceShape({ route, provider, runnerKind, source }) { + if (route !== CODE_AGENT_M3_IO_ROUTE) return false; + if (provider === CODE_AGENT_M3_SKILL_PROVIDER) return true; + if (runnerKind === CODE_AGENT_M3_SKILL_RUNNER_KIND) return true; + if (source?.name === CODE_AGENT_M3_SKILL_NAME) return true; + return false; +} + +function findM3ToolCall(message) { + const calls = Array.isArray(message?.toolCalls) ? message.toolCalls : []; + return calls.find((tool) => + tool?.name === CODE_AGENT_M3_SKILL_NAME || + tool?.route === CODE_AGENT_M3_IO_ROUTE || + tool?.runnerKind === CODE_AGENT_M3_SKILL_RUNNER_KIND || + tool?.type === "skill-cli" && tool?.hwlabApi?.route === CODE_AGENT_M3_IO_ROUTE + ) ?? null; +} + +function isM3ProviderMessage(message) { + return message?.provider === CODE_AGENT_M3_SKILL_PROVIDER || + message?.runner?.kind === CODE_AGENT_M3_SKILL_RUNNER_KIND || + message?.providerTrace?.route === CODE_AGENT_M3_IO_ROUTE || + message?.runnerTrace?.route === CODE_AGENT_M3_IO_ROUTE; +} + +function firstNonEmpty(...values) { + for (const value of values) { + const text = stringOrNull(value); + if (text) return text; + } + return null; +} + +function firstDefined(...values) { + return values.find((value) => value !== undefined && value !== null); +} + +function normalizeBoolean(value) { + if (typeof value === "boolean") return value; + if (typeof value === "number") return value === 1 ? true : value === 0 ? false : null; + const text = stringOrNull(value)?.toLowerCase(); + if (text === "true") return true; + if (text === "false") return false; + return null; +} + +function objectOrNull(value) { + return value && typeof value === "object" && !Array.isArray(value) ? value : null; +} + +function stringOrNull(value) { + const text = String(value ?? "").trim(); + return text ? text : null; +} diff --git a/web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs b/web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs new file mode 100644 index 00000000..a547ef42 --- /dev/null +++ b/web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs @@ -0,0 +1,239 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + CODE_AGENT_M3_IO_ROUTE, + codeAgentM3EvidenceContractSummary, + extractCodeAgentM3Evidence, + isCodeAgentM3SkillCompletion, + m3EvidenceRows +} from "./code-agent-m3-evidence.mjs"; + +test("renders accepted M3 Skill CLI operation/audit/evidence metadata", () => { + const evidence = extractCodeAgentM3Evidence(m3Message({ + status: "succeeded", + accepted: true, + operationId: "op_m3_cli_accepted", + traceId: "trc_m3_cli_accepted", + auditId: "aud_m3_cli_accepted_succeeded", + evidenceId: "evd_m3_cli_accepted_succeeded", + value: true, + readbackValue: true + })); + + assert.equal(evidence.verdict.key, "accepted"); + assert.equal(evidence.route, CODE_AGENT_M3_IO_ROUTE); + assert.equal(evidence.target.resourceId, "res_boxsimu_1"); + assert.equal(evidence.target.port, "DO1"); + assert.equal(evidence.target.value, true); + assert.equal(evidence.readback.resourceId, "res_boxsimu_2"); + assert.equal(evidence.readback.port, "DI1"); + assert.equal(evidence.readback.value, true); + assert.equal(evidence.ids.operationId, "op_m3_cli_accepted"); + assert.equal(evidence.ids.traceId, "trc_m3_cli_accepted"); + assert.equal(evidence.ids.auditId, "aud_m3_cli_accepted_succeeded"); + assert.equal(evidence.ids.evidenceId, "evd_m3_cli_accepted_succeeded"); + assert.equal(isCodeAgentM3SkillCompletion(m3Message()), true); + assert.equal(rowValue(evidence, "evidenceId"), "evd_m3_cli_accepted_succeeded"); +}); + +test("renders blocker state from Skill CLI without trusted pass", () => { + const evidence = extractCodeAgentM3Evidence(m3Message({ + status: "blocked", + accepted: false, + operationId: "op_m3_blocked", + blocker: { + code: "m3_wiring_missing", + zh: "hwlab-patch-panel 未确认 active DO1 -> DI1 接线" + } + })); + + assert.equal(evidence.verdict.key, "blocked"); + assert.equal(evidence.verdict.tone, "blocked"); + assert.match(rowValue(evidence, "blocker"), /m3_wiring_missing/u); + assert.equal(isCodeAgentM3SkillCompletion({ ...m3Message(), status: "failed" }), false); +}); + +test("shows missing evidenceId as unavailable proof instead of fabricating one", () => { + const evidence = extractCodeAgentM3Evidence(m3Message({ + status: "succeeded", + accepted: true, + evidenceId: null + })); + + assert.equal(evidence.verdict.key, "accepted-missing-proof"); + assert.equal(rowValue(evidence, "evidenceId"), "未产生/不可证明"); + assert.equal(row(evidence, "evidenceId").copyable, false); +}); + +test("classifies DO1 target and DI1 readback mismatch as blocked proof", () => { + const evidence = extractCodeAgentM3Evidence(m3Message({ + status: "blocked", + accepted: false, + operationId: "op_m3_readback_mismatch", + value: true, + readbackValue: false, + blocker: { + code: "m3_readback_mismatch", + zh: "DI1 回读值 false 与 DO1 写入值 true 不一致" + } + })); + + assert.equal(evidence.readbackMismatch, true); + assert.equal(evidence.verdict.key, "readback-mismatch"); + assert.match(rowValue(evidence, "readback"), /res_boxsimu_2:DI1 value=false/u); +}); + +test("classifies direct gateway/box/patch-panel path as invalid and never trusted", () => { + const evidence = extractCodeAgentM3Evidence(m3Message({ + status: "succeeded", + accepted: true, + operationId: "op_m3_direct_path", + controlPath: { + cloudApi: false, + gatewaySimu: true, + boxSimu: true, + patchPanel: true, + frontendBypass: true + }, + blocker: { + code: "direct_hardware_target_blocked", + zh: "Skill CLI target must be HWLAB cloud-api, not gateway/box/patch-panel." + } + })); + + assert.equal(evidence.directPathInvalid, true); + assert.equal(evidence.verdict.key, "direct-path-invalid"); + assert.equal(evidence.verdict.tone, "blocked"); +}); + +test("contract summary documents operation/audit/evidence visibility", () => { + assert.match(codeAgentM3EvidenceContractSummary(), /operationId/u); + assert.match(codeAgentM3EvidenceContractSummary(), /auditId/u); + assert.match(codeAgentM3EvidenceContractSummary(), /evidenceId/u); + assert.match(codeAgentM3EvidenceContractSummary(), /direct gateway\/box\/patch-panel/u); +}); + +function row(evidence, label) { + const found = m3EvidenceRows(evidence).find((item) => item.label === label); + assert.ok(found, `missing row ${label}`); + return found; +} + +function rowValue(evidence, label) { + return row(evidence, label).value; +} + +function m3Message(overrides = {}) { + const operationId = overrides.operationId ?? "op_m3_cli_accepted"; + const traceId = overrides.traceId ?? "trc_m3_cli_accepted"; + const auditId = Object.hasOwn(overrides, "auditId") ? overrides.auditId : "aud_m3_cli_accepted_succeeded"; + const evidenceId = Object.hasOwn(overrides, "evidenceId") ? overrides.evidenceId : "evd_m3_cli_accepted_succeeded"; + const value = Object.hasOwn(overrides, "value") ? overrides.value : true; + const readbackValue = Object.hasOwn(overrides, "readbackValue") ? overrides.readbackValue : value; + const status = overrides.status ?? "succeeded"; + const accepted = Object.hasOwn(overrides, "accepted") ? overrides.accepted : true; + const toolCall = { + id: "tool_m3_fixture", + type: "skill-cli", + name: "hwlab-agent-runtime.m3-io", + status: accepted ? "completed" : "blocked", + route: CODE_AGENT_M3_IO_ROUTE, + accepted, + operationId, + traceId, + audit: { + auditId + }, + evidence: { + evidenceId, + status: evidenceId ? "green" : "blocked", + sourceKind: evidenceId ? "DEV-LIVE" : "BLOCKED" + }, + command: { + action: "do.write", + resourceId: "res_boxsimu_1", + port: "DO1", + value + }, + result: { + value, + targetReadback: { + resourceId: "res_boxsimu_2", + port: "DI1", + value: readbackValue + } + }, + blocker: overrides.blocker ?? null, + blockers: overrides.blocker ? [overrides.blocker] : [], + controlPath: overrides.controlPath ?? { + cloudApi: true, + gatewaySimu: true, + boxSimu: true, + patchPanel: true, + frontendBypass: false + }, + safety: { + cloudApiRouteOnly: true, + allowedRoute: CODE_AGENT_M3_IO_ROUTE, + directGatewayCalls: false, + directBoxCalls: false, + directPatchPanelCalls: false, + fallbackUsed: false + }, + stdout: JSON.stringify({ + route: CODE_AGENT_M3_IO_ROUTE, + status, + accepted, + traceId, + operationId, + audit: { auditId }, + evidence: { evidenceId }, + blocker: overrides.blocker ?? null, + result: { + value, + targetReadback: { + resourceId: "res_boxsimu_2", + port: "DI1", + value: readbackValue + } + } + }) + }; + + return { + status: "completed", + provider: "hwlab-skill-cli", + model: "controlled-m3-io", + backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli", + traceId, + conversationId: "conv_m3_fixture", + sessionId: "sess_m3_fixture", + messageId: "msg_m3_fixture", + runner: { + kind: "hwlab-m3-io-skill-cli" + }, + runnerTrace: { + runnerKind: "hwlab-m3-io-skill-cli", + route: CODE_AGENT_M3_IO_ROUTE, + status, + accepted, + operationId, + traceId + }, + providerTrace: { + runnerKind: "hwlab-m3-io-skill-cli", + skill: "hwlab-agent-runtime.m3-io", + route: CODE_AGENT_M3_IO_ROUTE, + status, + accepted, + operationId, + traceId, + fallbackUsed: false + }, + toolCalls: [toolCall], + reply: { + content: "M3 IO Skill CLI result fixture." + } + }; +} diff --git a/web/hwlab-cloud-web/package.json b/web/hwlab-cloud-web/package.json index 4801b663..2dd970a0 100644 --- a/web/hwlab-cloud-web/package.json +++ b/web/hwlab-cloud-web/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "m3-readonly": "node scripts/m3-readonly-contract.mjs", - "check": "node --test code-agent-facts.test.mjs code-agent-status.test.mjs wiring-status.test.mjs && node scripts/check.mjs", + "check": "node --test code-agent-facts.test.mjs code-agent-status.test.mjs wiring-status.test.mjs code-agent-m3-evidence.test.mjs && node scripts/check.mjs", "build": "node scripts/build.mjs", "layout": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --static --report ../../reports/dev-gate/dev-cloud-workbench-layout.json", "layout:build": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --build --report ../../reports/dev-gate/dev-cloud-workbench-layout-build.json", diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 99c68730..25c96e8f 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -12,6 +12,11 @@ import { } from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs"; import { gateSummary } from "../gate-summary.mjs"; import { runtime } from "../runtime.mjs"; +import { + codeAgentM3EvidenceContractSummary, + extractCodeAgentM3Evidence, + m3EvidenceRows +} from "../code-agent-m3-evidence.mjs"; import { runM3ControlPanelGuard } from "./m3-control-panel-guard.mjs"; import { runCloudWebM3ReadonlyContract } from "./m3-readonly-contract.mjs"; import { runWorkbenchHardwarePanelContract } from "./workbench-hardware-panel-contract.mjs"; @@ -24,6 +29,7 @@ const requiredFiles = [ "app.mjs", "code-agent-facts.mjs", "code-agent-status.mjs", + "code-agent-m3-evidence.mjs", "live-status.mjs", "gate-summary.mjs", "runtime.mjs", @@ -46,6 +52,7 @@ const styles = fs.readFileSync(path.resolve(rootDir, "styles.css"), "utf8"); const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8"); const codeAgentFacts = fs.readFileSync(path.resolve(rootDir, "code-agent-facts.mjs"), "utf8"); const codeAgentStatus = fs.readFileSync(path.resolve(rootDir, "code-agent-status.mjs"), "utf8"); +const codeAgentM3Evidence = fs.readFileSync(path.resolve(rootDir, "code-agent-m3-evidence.mjs"), "utf8"); const liveStatus = fs.readFileSync(path.resolve(rootDir, "live-status.mjs"), "utf8"); const wiringStatus = fs.readFileSync(path.resolve(rootDir, "wiring-status.mjs"), "utf8"); const helpMarkdown = fs.readFileSync(path.resolve(rootDir, "help.md"), "utf8"); @@ -70,6 +77,65 @@ const requiredTrustedRecordTerms = Object.freeze([ "只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定", "当前显示 SOURCE 回退,不能冒充 DEV-LIVE" ]); +const m3EvidenceFixture = { + status: "completed", + provider: "hwlab-skill-cli", + model: "controlled-m3-io", + backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli", + traceId: "trc_check_m3_evidence", + conversationId: "conv_check_m3_evidence", + sessionId: "sess_check_m3_evidence", + messageId: "msg_check_m3_evidence", + runner: { + kind: "hwlab-m3-io-skill-cli" + }, + providerTrace: { + runnerKind: "hwlab-m3-io-skill-cli", + skill: "hwlab-agent-runtime.m3-io", + route: "/v1/m3/io", + status: "succeeded", + accepted: true, + operationId: "op_check_m3_evidence", + traceId: "trc_check_m3_evidence" + }, + toolCalls: [ + { + type: "skill-cli", + name: "hwlab-agent-runtime.m3-io", + route: "/v1/m3/io", + status: "completed", + accepted: true, + operationId: "op_check_m3_evidence", + traceId: "trc_check_m3_evidence", + audit: { + auditId: "aud_check_m3_evidence" + }, + evidence: { + evidenceId: "evd_check_m3_evidence" + }, + command: { + action: "do.write", + resourceId: "res_boxsimu_1", + port: "DO1", + value: true + }, + result: { + targetReadback: { + resourceId: "res_boxsimu_2", + port: "DI1", + value: true + } + }, + safety: { + cloudApiRouteOnly: true, + allowedRoute: "/v1/m3/io", + directGatewayCalls: false, + directBoxCalls: false, + directPatchPanelCalls: false + } + } + ] +}; const workbenchSmoke = runDevCloudWorkbenchStaticSmoke(); const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract"); const outerScrollSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "outer-scroll-contract"); @@ -495,12 +561,28 @@ assert.doesNotMatch(styles, /\.trace-list\s*{/); for (const trustedRecordTerm of requiredTrustedRecordTerms) { assert.match(app, new RegExp(escapeRegExp(trustedRecordTerm)), `missing trusted-record term: ${trustedRecordTerm}`); } +assert.match(codeAgentM3Evidence, /export function extractCodeAgentM3Evidence/); +assert.match(codeAgentM3Evidence, /operationId/); +assert.match(codeAgentM3Evidence, /auditId/); +assert.match(codeAgentM3Evidence, /evidenceId/); +assert.match(codeAgentM3Evidence, /readback-mismatch/); +assert.match(codeAgentM3Evidence, /direct-path-invalid/); +assert.match(codeAgentM3Evidence, /未产生\/不可证明/); +assert.match(codeAgentM3EvidenceContractSummary(), /operation\/audit\/evidence/u); +assert.equal(extractCodeAgentM3Evidence(m3EvidenceFixture).verdict.key, "accepted"); +assert.equal(m3EvidenceRows(extractCodeAgentM3Evidence(m3EvidenceFixture)).find((row) => row.label === "auditId")?.value, "aud_check_m3_evidence"); +assert.match(app, /extractCodeAgentM3Evidence\(result\)/); +assert.match(app, /function messageM3EvidencePanel/); +assert.match(app, /function m3EvidenceRowElement/); +assert.match(app, /function copyButton/); +assert.match(app, /isCodeAgentM3SkillCompletion\(value\)/); assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.fromResourceId/); assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.patchPanelServiceId/); assert.doesNotMatch(functionBody(app, "renderWiringList"), /activeConnections\.map/u); assert.doesNotMatch(functionBody(app, "renderWiringList"), /link\.fromResourceId/u); assert.match(buildScript, /buildCloudWebDist/); assert.match(distContractScript, /"help\.md"/); +assert.match(distContractScript, /"code-agent-m3-evidence\.mjs"/); assert.match(distContractScript, /"third_party\/marked\/marked\.esm\.js"/); assert.match(distContractScript, /"third_party\/marked\/LICENSE"/); assert.match(distContractScript, /"live-status\.mjs"/); @@ -684,6 +766,10 @@ assert.match(styles, /\.message-card\.status-completed\s*{/); assert.match(styles, /\.message-card\.status-source\s*{/); assert.match(styles, /\.message-user\s*{/); assert.match(styles, /\.message-agent,\s*\n\.message-system\s*{/); +assert.match(styles, /\.message-m3-evidence\s*{/); +assert.match(styles, /\.message-m3-rows\s*{/); +assert.match(styles, /\.message-m3-row\s*{/); +assert.match(styles, /\.copy-chip\s*{/); assert.match(styles, /\.message-evidence\s*{/); assert.match(styles, /\.message-evidence-chip\s*{/); assert.match(styles, /\.code-agent-facts\s*{/); @@ -699,6 +785,7 @@ assert.match(styles, /\.side-tab\s*{[^}]*min-width:\s*0;[^}]*min-height:\s*34px; assert.match(styles, /\.(?:status-dot|state-tag|badge)[^{]*{[^}]*max-width:\s*100%;[^}]*line-height:\s*1\.25;[^}]*white-space:\s*normal;[^}]*overflow-wrap:\s*anywhere;/s); assert.match(styles, /\.probe-card\s*{[^}]*min-width:\s*0;/s); assert.match(styles, /\.probe-card strong\s*{[^}]*line-height:\s*1\.2;[^}]*overflow-wrap:\s*anywhere;/s); +assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-m3-rows\s*{[\s\S]*?grid-template-columns:\s*1fr;/); assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.message-evidence\s*{[\s\S]*?grid-column:\s*1;/); assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.code-agent-fact-grid\s*{[\s\S]*?grid-template-columns:\s*1fr;/); for (const userFacingFunctionName of [ diff --git a/web/hwlab-cloud-web/scripts/dist-contract.mjs b/web/hwlab-cloud-web/scripts/dist-contract.mjs index dcfb43dc..b11994c3 100644 --- a/web/hwlab-cloud-web/scripts/dist-contract.mjs +++ b/web/hwlab-cloud-web/scripts/dist-contract.mjs @@ -11,6 +11,7 @@ export const cloudWebDistRuntimeFiles = Object.freeze([ "app.mjs", "code-agent-facts.mjs", "code-agent-status.mjs", + "code-agent-m3-evidence.mjs", "live-status.mjs", "gate-summary.mjs", "runtime.mjs", diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index 74f76f21..d42eb7bd 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -759,17 +759,25 @@ h3 { min-width: 0; } -.code-agent-facts { +.code-agent-facts, +.message-m3-evidence { grid-column: 2; min-width: 0; display: grid; gap: 7px; padding: 8px; - background: rgba(15, 17, 16, 0.72); border: 1px solid var(--line); +} + +.code-agent-facts { + background: rgba(15, 17, 16, 0.72); border-left-width: 3px; } +.message-m3-evidence { + background: rgba(15, 17, 16, 0.44); +} + .code-agent-facts-head { min-width: 0; display: grid; @@ -785,11 +793,31 @@ h3 { overflow-wrap: anywhere; } -.code-agent-fact-grid { +.message-m3-head { + min-width: 0; + display: flex; + align-items: center; + gap: 7px; + flex-wrap: wrap; +} + +.message-m3-summary { + min-width: 0; + color: var(--muted); + font-family: var(--mono); + font-size: 10px; + overflow-wrap: anywhere; +} + +.code-agent-fact-grid, +.message-m3-rows { min-width: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; +} + +.code-agent-fact-grid { margin: 0; } @@ -802,6 +830,17 @@ h3 { border: 1px solid var(--line); } +.message-m3-row { + min-width: 0; + display: grid; + grid-template-columns: minmax(70px, auto) minmax(0, 1fr) auto; + gap: 5px; + align-items: center; + padding: 4px 5px; + background: var(--surface); + border: 1px solid var(--line); +} + .code-agent-fact-key, .code-agent-fact-value { min-width: 0; @@ -852,6 +891,47 @@ h3 { user-select: all; } +.message-m3-key { + min-width: 0; + color: var(--dim); + font-family: var(--mono); + font-size: 10px; + overflow-wrap: anywhere; +} + +.message-m3-value { + min-width: 0; + color: var(--text); + font-size: 10px; + overflow-wrap: anywhere; + word-break: break-word; +} + +.message-m3-value[href] { + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 2px; +} + +.copy-chip { + min-width: 42px; + min-height: 24px; + padding: 2px 6px; + border: 1px solid var(--line-strong); + background: var(--surface-2); + color: var(--muted); + cursor: pointer; + font-family: var(--mono); + font-size: 10px; +} + +.copy-chip:hover, +.copy-chip:focus-visible { + border-color: var(--accent); + color: var(--text); + outline: 0; +} + .message-evidence summary { cursor: pointer; list-style-position: inside; @@ -1824,6 +1904,18 @@ tbody tr:last-child td { } .code-agent-facts, + .message-m3-evidence { + grid-column: 1; + } + + .message-m3-rows { + grid-template-columns: 1fr; + } + + .message-m3-row { + grid-template-columns: minmax(0, 0.36fr) minmax(0, 1fr) auto; + } + .message-evidence { grid-column: 1; }