Merge pull request #360 from pikasTech/fix/code-agent-m3-evidence-visible
feat: show Code Agent M3 evidence metadata
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -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) &&
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user