370 lines
15 KiB
JavaScript
370 lines
15 KiB
JavaScript
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;
|
|
}
|