Merge pull request #360 from pikasTech/fix/code-agent-m3-evidence-visible

feat: show Code Agent M3 evidence metadata
This commit is contained in:
Lyon
2026-05-23 23:24:09 +08:00
committed by GitHub
9 changed files with 908 additions and 9 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+24 -3
View File
@@ -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) &&
+91 -1
View File
@@ -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;
}
@@ -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;
}
@@ -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."
}
};
}
+1 -1
View File
@@ -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",
+87
View File
@@ -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 [
@@ -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",
+95 -3
View File
@@ -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;
}