fix: 收敛 Code Agent M3 IO 结构化结果

This commit is contained in:
Code Queue Review
2026-05-24 04:22:17 +00:00
parent c9ec9029b6
commit 0b5311e676
8 changed files with 750 additions and 76 deletions
+236 -29
View File
@@ -63,6 +63,13 @@ const M3_IO_SKILL_LIMITATION_FLAGS = Object.freeze([
"not-generic-hardware-control",
"not-durable-session"
]);
const M3_IO_TOPOLOGY = Object.freeze({
sourceResourceId: "res_boxsimu_1",
sourcePort: "DO1",
patchPanelServiceId: "hwlab-patch-panel",
targetResourceId: "res_boxsimu_2",
targetPort: "DI1"
});
const OPENAI_FALLBACK_RUNNER_KIND = "openai-responses-fallback";
const DIRECT_HARDWARE_TARGET_PATTERN = /https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel|hwlab-patch-panel)[^\s"']*|\/invoke\b|\/sync\/tick\b|:7101\b|:7201\b|:7301\b/iu;
const READONLY_TOOL_OUTPUT_LIMIT = 4000;
@@ -375,6 +382,8 @@ function completedRunnerPayload({ base, runnerResult, messageId, now, sessionReg
runner: runnerResult.runner,
runnerTrace: runnerResult.runnerTrace,
capabilityLevel: runnerResult.capabilityLevel,
...(runnerResult.responseType ? { responseType: runnerResult.responseType } : {}),
...(runnerResult.m3Io ? { m3Io: runnerResult.m3Io } : {}),
reply: {
messageId,
role: "assistant",
@@ -1039,13 +1048,21 @@ function codexStdioM3IoSkillRunnerResult({ stdioResult, skillResult, commandArgs
);
const route = m3SkillRoute(skillResult);
const method = m3SkillMethod(skillResult);
const m3Io = m3IoStructuredResult({
skillResult,
stdioResult,
commandArgs,
route,
method
});
const toolCall = m3IoSkillToolCall({
skillResult,
commandArgs,
cwd: stdioResult.workspace,
capabilityLevel,
route,
method
method,
responseType: m3Io.type
});
const blockers = skillResult.blockers ?? (skillResult.blocker ? [skillResult.blocker] : []);
const runnerTrace = {
@@ -1096,7 +1113,9 @@ function codexStdioM3IoSkillRunnerResult({ stdioResult, skillResult, commandArgs
return {
...stdioResult,
content: `${stdioResult.content}\n\n${m3IoSkillReply(skillResult)}`,
content: m3IoSkillReply(skillResult, m3Io),
responseType: m3Io.type,
m3Io,
toolCalls: [
...(stdioResult.toolCalls ?? []),
toolCall
@@ -1118,7 +1137,7 @@ function codexStdioM3IoSkillRunnerResult({ stdioResult, skillResult, commandArgs
toolName: HWLAB_M3_IO_SKILL_NAME,
providerTrace: {
...(stdioResult.providerTrace ?? {}),
...m3IoSkillProviderTrace({ skillResult, route, method, capabilityLevel }),
...m3IoSkillProviderTrace({ skillResult, route, method, capabilityLevel, responseType: m3Io.type }),
runnerKind: stdioResult.runner?.kind ?? CODEX_STDIO_RUNNER_KIND,
codexStdio: true,
transport: "stdio+skill-cli"
@@ -1126,11 +1145,12 @@ function codexStdioM3IoSkillRunnerResult({ stdioResult, skillResult, commandArgs
};
}
function m3IoSkillToolCall({ skillResult, commandArgs, cwd, capabilityLevel, route, method }) {
function m3IoSkillToolCall({ skillResult, commandArgs, cwd, capabilityLevel, route, method, responseType }) {
return {
id: `tool_${randomUUID()}`,
type: "skill-cli",
name: HWLAB_M3_IO_SKILL_NAME,
responseType,
status: skillResult.status === "succeeded" || skillResult.accepted === true ? "completed" : "blocked",
cwd,
command: redactText(`node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs ${redactM3IoCommandArgs(commandArgs).join(" ")}`),
@@ -1191,10 +1211,11 @@ function m3IoSkillItem({ capabilityLevel, route }) {
};
}
function m3IoSkillProviderTrace({ skillResult, route, method, capabilityLevel }) {
function m3IoSkillProviderTrace({ skillResult, route, method, capabilityLevel, responseType }) {
return {
runnerKind: M3_IO_SKILL_RUNNER_KIND,
skill: HWLAB_M3_IO_SKILL_NAME,
responseType,
route,
method,
traceId: skillResult.traceId,
@@ -1210,6 +1231,181 @@ function m3IoSkillProviderTrace({ skillResult, route, method, capabilityLevel })
};
}
function m3IoStructuredResult({ skillResult = {}, stdioResult = {}, commandArgs = [], route = HWLAB_M3_IO_API_ROUTE, method = "POST" }) {
const blocker = m3IoPrimaryBlocker(skillResult);
const isBlocker = Boolean(
blocker ||
skillResult.ok === false ||
skillResult.accepted === false ||
String(skillResult.status ?? "").toLowerCase() === "blocked"
);
const action = skillResult.action ?? skillResult.command?.action ?? m3IoActionFromArgs(commandArgs) ?? (route === HWLAB_M3_STATUS_API_ROUTE ? "status" : "m3.io");
const do1Value = action === "do.write"
? firstDefined(skillResult.command?.value, skillResult.result?.value, skillResult.value) ?? null
: null;
const di1Value = action === "di.read"
? firstDefined(skillResult.result?.value, skillResult.readback?.value) ?? null
: firstDefined(skillResult.result?.targetReadback?.value, skillResult.readback?.value) ?? null;
const durable = skillResult.durable ?? {};
const evidenceSourceKind = skillResult.evidence?.sourceKind ?? null;
const auditStatus = skillResult.audit?.status ?? null;
const evidenceStatus = skillResult.evidence?.status ?? null;
const trusted = durable.durable === true &&
evidenceSourceKind === "DEV-LIVE" &&
!skillResult.trustBlocker &&
["persisted", "green"].includes(String(auditStatus ?? evidenceStatus ?? "").toLowerCase());
const pathSummary = `Code Agent -> Skill CLI -> HWLAB API ${route}`;
return {
type: isBlocker ? "m3_io_blocker" : "m3_io_result",
status: skillResult.status ?? (isBlocker ? "blocked" : "succeeded"),
action,
accepted: skillResult.accepted === true,
controlReady: skillResult.controlReady === true,
capabilityLevel: skillResult.capabilityLevel ?? (isBlocker ? HWLAB_M3_IO_CAPABILITY_LEVELS.blocked : HWLAB_M3_IO_CAPABILITY_LEVELS.ready),
summary: m3IoStructuredSummary({
action,
isBlocker,
blocker,
route,
do1Value,
di1Value,
trusted,
durable: durable.durable === true
}),
do1: {
resourceId: M3_IO_TOPOLOGY.sourceResourceId,
port: M3_IO_TOPOLOGY.sourcePort,
targetValue: do1Value
},
di1: {
resourceId: M3_IO_TOPOLOGY.targetResourceId,
port: M3_IO_TOPOLOGY.targetPort,
observedValue: di1Value,
status: skillResult.result?.targetReadback?.status ?? skillResult.readback?.status ?? null
},
wiring: {
from: `${M3_IO_TOPOLOGY.sourceResourceId}:${M3_IO_TOPOLOGY.sourcePort}`,
via: M3_IO_TOPOLOGY.patchPanelServiceId,
to: `${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}`,
label: `${M3_IO_TOPOLOGY.sourceResourceId}:${M3_IO_TOPOLOGY.sourcePort} -> ${M3_IO_TOPOLOGY.patchPanelServiceId} -> ${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}`
},
path: {
summary: pathSummary,
segments: ["Code Agent", "Skill CLI", "HWLAB API"],
skillCli: {
provider: M3_IO_SKILL_PROVIDER,
runnerKind: M3_IO_SKILL_RUNNER_KIND,
name: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION
},
hwlabApi: {
route,
method,
source: skillResult.hwlabApi?.source ?? null,
redactedUrl: skillResult.hwlabApi?.redactedUrl ?? null,
baseUrlConfigured: skillResult.hwlabApi?.baseUrlConfigured ?? null,
cloudApiOnly: skillResult.hwlabApi?.cloudApiOnly !== false
},
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
openAiFallbackUsed: false
},
operation: {
operationId: skillResult.operationId ?? null,
status: skillResult.status ?? null,
accepted: skillResult.accepted === true,
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null
},
session: {
sessionId: stdioResult.session?.sessionId ?? null,
sessionMode: stdioResult.sessionMode ?? stdioResult.runner?.sessionMode ?? null,
sessionReused: stdioResult.sessionReuse?.reused ?? stdioResult.runner?.sessionReused ?? false,
turn: stdioResult.sessionReuse?.turn ?? stdioResult.runner?.turn ?? null,
codexStdio: stdioResult.runner?.codexStdio === true,
durableSession: stdioResult.runner?.durableSession === true
},
trace: {
traceId: skillResult.traceId ?? stdioResult.traceId ?? null,
requestId: skillResult.requestId ?? null,
actorId: skillResult.actorId ?? null,
runnerKind: M3_IO_SKILL_RUNNER_KIND,
route,
method
},
trust: {
trusted,
durable: durable.durable === true,
durableStatus: durable.status ?? null,
durableBlocker: durable.blocker ?? null,
auditStatus,
evidenceStatus,
evidenceSourceKind,
trustBlocker: skillResult.trustBlocker ?? null,
note: trusted
? "operation/audit/evidence 已有可信持久化记录;仍不是 M3 DEV-LIVE 验收结论。"
: "本次不伪造 DEV-LIVE:控制链路结果与可信持久化状态分开展示。"
},
blocker,
blockers: skillResult.blockers ?? (blocker ? [blocker] : []),
safety: {
cloudApiRouteOnly: true,
allowedRoute: route,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false,
openAiFallbackUsed: false,
acceptanceClaimed: false
}
};
}
function m3IoActionFromArgs(commandArgs = []) {
const actionIndex = commandArgs.indexOf("--action");
if (actionIndex >= 0 && typeof commandArgs[actionIndex + 1] === "string") {
return commandArgs[actionIndex + 1];
}
if (commandArgs.includes("status")) return "status";
return null;
}
function m3IoPrimaryBlocker(skillResult = {}) {
const capabilityCandidates = [
skillResult.blocker,
skillResult.capabilityBlocker,
skillResult.error?.blocker
];
const primary = capabilityCandidates.find((item) => item?.code || item?.message || item?.zh);
if (primary) return primary;
if (
skillResult.ok !== false &&
skillResult.accepted !== false &&
String(skillResult.status ?? "").toLowerCase() !== "blocked"
) {
return null;
}
const fallbackCandidates = [
...(Array.isArray(skillResult.blockers) ? skillResult.blockers : [])
];
return fallbackCandidates.find((item) => item?.code || item?.message || item?.zh) ?? null;
}
function m3IoStructuredSummary({ action, isBlocker, blocker, route, do1Value, di1Value, trusted, durable }) {
if (isBlocker) {
const reason = blocker?.userMessage ?? blocker?.zh ?? blocker?.message ?? blocker?.code ?? "M3 IO 链路受阻";
return `M3 IO 阻塞:${reason};路径 Code Agent -> Skill CLI -> HWLAB API ${route}`;
}
const actionText = action === "do.write"
? `DO1 目标值=${formatM3Value(do1Value)}DI1 观测值=${formatM3Value(di1Value)}`
: action === "di.read"
? `DI1 观测值=${formatM3Value(di1Value)}`
: `状态读取 DI1=${formatM3Value(di1Value)}`;
return `M3 IO 结果:${actionText}trusted=${trusted ? "true" : "false"}durable=${durable ? "true" : "false"}`;
}
function redactM3IoCommandArgs(commandArgs = []) {
const redacted = [];
for (let index = 0; index < commandArgs.length; index += 1) {
@@ -1591,27 +1787,29 @@ function m3IoApiBaseUrlMissingBlocker({ route = HWLAB_M3_IO_API_ROUTE } = {}) {
};
}
function m3IoSkillReply(skillResult) {
const lines = [
`M3 IO Skill CLI result: 已通过 Code Agent -> Skill CLI -> HWLAB API ${skillResult.route} 处理请求;status=${skillResult.status}; accepted=${skillResult.accepted}; traceId=${skillResult.traceId}; operationId=${skillResult.operationId ?? "null"}`
];
if (skillResult.audit?.summary) {
lines.push(`Audit: ${skillResult.audit.summary}audit 摘要)`);
}
if (skillResult.evidence?.summary) {
lines.push(`Evidence: ${skillResult.evidence.summary}evidence 摘要)。`);
}
if (skillResult.blocker) {
lines.push(`Blocker: ${skillResult.blocker.code}; ${skillResult.blocker.zh ?? skillResult.blocker.message}blocker)。`);
function m3IoSkillReply(skillResult, m3Io = null) {
const route = m3Io?.trace?.route ?? skillResult.route ?? HWLAB_M3_IO_API_ROUTE;
const lines = [];
if (m3Io?.type === "m3_io_blocker") {
const blocker = m3Io.blocker ?? skillResult.blocker ?? skillResult.capabilityBlocker ?? {};
lines.push(`M3 IO 阻塞:${blocker.userMessage ?? blocker.zh ?? blocker.message ?? blocker.code ?? "M3 控制链路仍受阻"}`);
} else {
lines.push("M3 IO 结果:受控请求已返回。");
}
if (skillResult.action === "do.write") {
lines.push(`DO1 写入请求:res_boxsimu_1:DO1=${String(skillResult.command?.value)}DI1 readbackres_boxsimu_2:DI1=${String(skillResult.result?.targetReadback?.value ?? "unknown")}`);
lines.push(`DO1 目标值:${M3_IO_TOPOLOGY.sourceResourceId}:${M3_IO_TOPOLOGY.sourcePort}=${formatM3Value(m3Io?.do1?.targetValue ?? skillResult.command?.value)}DI1 观测值:${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}=${formatM3Value(m3Io?.di1?.observedValue ?? skillResult.result?.targetReadback?.value)}`);
} else if (skillResult.action === "di.read") {
lines.push(`DI1 读取结果:res_boxsimu_2:DI1=${String(skillResult.result?.value ?? "unknown")}`);
} else if (skillResult.readonly === true || skillResult.route === HWLAB_M3_STATUS_API_ROUTE) {
lines.push(`M3 status readbackres_boxsimu_2:DI1=${String(skillResult.readback?.value ?? "unknown")}readiness=${skillResult.readiness?.status ?? "unknown"}`);
lines.push(`DI1 读取结果:${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}=${formatM3Value(m3Io?.di1?.observedValue ?? skillResult.result?.value)}`);
} else if (skillResult.readonly === true || route === HWLAB_M3_STATUS_API_ROUTE) {
lines.push(`M3 状态读取:${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}=${formatM3Value(m3Io?.di1?.observedValue ?? skillResult.readback?.value)}readiness=${skillResult.readiness?.status ?? "unknown"}`);
}
lines.push(`Boundary/边界:本次只走 Code Agent -> Skill CLI -> HWLAB API ${skillResult.route};没有直连 gateway/box/patch-panel,也没有使用 OpenAI fallback 冒充 toolCall。这是受控操作结果,不是 M3 DEV-LIVE 验收结论`);
lines.push(`接线关系:${M3_IO_TOPOLOGY.sourceResourceId}:${M3_IO_TOPOLOGY.sourcePort} -> ${M3_IO_TOPOLOGY.patchPanelServiceId} -> ${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}`);
lines.push(`执行路径:Code Agent -> Skill CLI -> HWLAB API ${route};没有使用 OpenAI fallback,也没有由 Code Agent 直连硬件服务。`);
lines.push(`排查字段:traceId=${skillResult.traceId ?? "null"}operationId=${skillResult.operationId ?? "null"}auditId=${skillResult.auditId ?? skillResult.audit?.auditId ?? "null"}evidenceId=${skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? "null"}`);
if (m3Io?.blocker?.code) {
lines.push(`blocker=${m3Io.blocker.code}layer=${m3Io.blocker.layer ?? "unknown"}`);
}
lines.push(`可信状态:trusted=${m3Io?.trust?.trusted === true ? "true" : "false"}durable=${m3Io?.trust?.durable === true ? "true" : "false"};这不是 M3 DEV-LIVE 验收结论。`);
return boundToolOutput(lines.join("\n"), READONLY_TOOL_OUTPUT_LIMIT).text;
}
@@ -1730,22 +1928,22 @@ function structuredCompletionBlocker(result, context = {}) {
const m3SkillTool = Array.isArray(result.toolCalls)
? result.toolCalls.find((toolCall) => toolCall?.name === HWLAB_M3_IO_SKILL_NAME)
: null;
if ((result.provider === M3_IO_SKILL_PROVIDER || m3SkillTool) && result.capabilityLevel === HWLAB_M3_IO_CAPABILITY_LEVELS.blocked) {
const primary = m3SkillTool?.blocker ?? result.skills?.blockers?.[0] ?? result.runnerTrace?.blocker ?? null;
if ((result.provider === M3_IO_SKILL_PROVIDER || m3SkillTool || result.m3Io?.type === "m3_io_blocker") && result.capabilityLevel === HWLAB_M3_IO_CAPABILITY_LEVELS.blocked) {
const primary = result.m3Io?.blocker ?? m3SkillTool?.blocker ?? result.skills?.blockers?.[0] ?? result.runnerTrace?.blocker ?? null;
return structuredBlocker({
code: primary?.code === "hwlab_api_unavailable" ? "hwlab_api_unavailable" : "m3_readiness_blocked",
layer: primary?.code === "hwlab_api_unavailable" ? "hwlab-api" : "m3-readiness",
code: primary?.code ?? "m3_io_blocker",
layer: primary?.layer ?? (primary?.code === "hwlab_api_unavailable" ? "hwlab-api" : "m3-readiness"),
message: primary?.message ?? "M3 IO Skill CLI did not reach a ready HWLAB API control path.",
userMessage: primary?.zh ?? "M3 控制链路仍受阻,前端应显示为能力未就绪,而不是发送失败。",
userMessage: primary?.userMessage ?? primary?.zh ?? "M3 控制链路仍受阻,前端应显示为能力未就绪,而不是发送失败。",
retryable: primary?.code === "hwlab_api_unavailable",
traceId: context.traceId,
provider: result.provider,
backend: result.backend,
runner: result.runner,
capabilityLevel: result.capabilityLevel,
route: HWLAB_M3_IO_API_ROUTE,
route: result.m3Io?.trace?.route ?? m3SkillTool?.route ?? HWLAB_M3_IO_API_ROUTE,
toolName: HWLAB_M3_IO_SKILL_NAME,
blockers: result.skills?.blockers
blockers: result.m3Io?.blockers ?? result.skills?.blockers
});
}
return null;
@@ -2359,6 +2557,15 @@ function nowIso(now) {
return typeof now === "function" ? now() : new Date().toISOString();
}
function firstDefined(...values) {
return values.find((value) => value !== undefined && value !== null);
}
function formatM3Value(value) {
if (value === undefined || value === null) return "未产生/不可证明";
return String(value);
}
function firstNonEmpty(...values) {
for (const value of values) {
if (typeof value === "string" && value.trim()) return value.trim();
+166 -1
View File
@@ -2642,6 +2642,18 @@ test("cloud api /v1/agent/chat recognizes M3 patch-panel topology text before Co
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.backend, "hwlab-cloud-api/codex-mcp-stdio");
assert.equal(payload.responseType, "m3_io_result");
assert.equal(payload.m3Io.type, "m3_io_result");
assert.equal(payload.m3Io.action, "do.write");
assert.equal(payload.m3Io.do1.targetValue, true);
assert.equal(payload.m3Io.di1.observedValue, true);
assert.equal(payload.m3Io.wiring.label, "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1");
assert.equal(payload.m3Io.path.summary, "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io");
assert.equal(payload.m3Io.operation.operationId, "op_m3_do_write_topology_skill");
assert.equal(payload.m3Io.trace.traceId, "trc_server-test-m3-topology");
assert.equal(payload.m3Io.session.sessionId, "ses_server_m3_topology_skill");
assert.equal(payload.m3Io.trust.trusted, false);
assert.equal(payload.m3Io.trust.durable, false);
const skillTool = payload.toolCalls.find((toolCall) => toolCall.name === "hwlab-agent-runtime.m3-io");
assert.ok(skillTool);
assert.equal(skillTool.route, "/v1/m3/io");
@@ -2732,6 +2744,13 @@ test("cloud api /v1/agent/chat returns Skill CLI blocker for M3 topology without
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(payload.responseType, "m3_io_blocker");
assert.equal(payload.m3Io.type, "m3_io_blocker");
assert.equal(payload.m3Io.action, "do.write");
assert.equal(payload.m3Io.do1.targetValue, null);
assert.equal(payload.m3Io.di1.observedValue, null);
assert.equal(payload.m3Io.path.summary, "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io");
assert.equal(payload.m3Io.blocker.code, "invalid_boolean_value");
const skillTool = payload.toolCalls.find((toolCall) => toolCall.name === "hwlab-agent-runtime.m3-io");
assert.ok(skillTool);
assert.equal(skillTool.status, "blocked");
@@ -2739,8 +2758,10 @@ test("cloud api /v1/agent/chat returns Skill CLI blocker for M3 topology without
assert.equal(skillTool.blocker.code, "invalid_boolean_value");
assert.equal(skillTool.accepted, false);
assert.equal(skillTool.operationId, null);
assert.equal(payload.blocker.code, "m3_readiness_blocked");
assert.equal(payload.blocker.code, "invalid_boolean_value");
assert.match(payload.reply.content, /^M3 IO 阻塞:/u);
assert.match(payload.reply.content, /M3 DO1 写入需要 --value true 或 --value false/u);
assert.match(payload.reply.content, /Code Agent -> Skill CLI -> HWLAB API \/v1\/m3\/io/u);
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(payload.providerTrace.transport, "stdio+skill-cli");
assert.equal(calls.length, 0);
@@ -2753,6 +2774,134 @@ test("cloud api /v1/agent/chat returns Skill CLI blocker for M3 topology without
}
});
test("cloud api /v1/agent/chat promotes /v1/m3/io blocker into Chinese M3 IO reply", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-api-blocker-"));
const codexHome = path.join(workspace, "codex-home");
const fakeCodex = await createFakeCodexCommand();
await mkdir(codexHome, { recursive: true });
const calls = [];
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_server_m3_api_blocker",
createRpcClient: async () => ({
async initialize() {
return { tools: ["codex", "codex-reply"] };
},
async listTools() {
return ["codex", "codex-reply"];
},
async callTool() {
return {
structuredContent: {
threadId: "thread_server_m3_api_blocker",
content: "stdio session ready for M3 API blocker fixture."
}
};
},
close() {}
})
});
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_WORKSPACE: workspace,
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667"
},
codexStdioManager: manager,
m3IoSkillRequestJson: async (url, request) => {
calls.push({ url, request });
return {
ok: true,
status: 200,
body: {
serviceId: "hwlab-cloud-api",
contractVersion: "m3-io-control-v1",
route: "/v1/m3/io",
method: "POST",
status: "blocked",
accepted: false,
action: "do.write",
traceId: "trc_server-test-m3-api-blocker",
operationId: "op_m3_do_write_api_blocker",
auditId: "aud_m3_do_write_api_blocker_failed",
evidenceId: "evd_m3_do_write_api_blocker_failed",
blocker: {
code: "m3_wiring_missing",
layer: "hwlab-patch-panel",
zh: "hwlab-patch-panel 未确认 active res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 接线"
},
auditState: {
status: "written_non_durable"
},
evidenceState: {
status: "blocked",
sourceKind: "BLOCKED",
blocker: "runtime_durable_not_green",
writeStatus: "written_non_durable"
},
durableStatus: {
status: "degraded",
durable: false,
blocker: "runtime_durable_not_green"
},
result: {
value: true,
targetReadback: null
},
controlPath: {
status: "blocked",
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: false,
frontendBypass: false
}
}
};
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const payload = await postAgent(port, {
conversationId: "cnv_server-test-m3-api-blocker",
traceId: "trc_server-test-m3-api-blocker",
message: "把 M3 DO1 打开,然后读取 DI1。"
});
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.responseType, "m3_io_blocker");
assert.equal(payload.m3Io.type, "m3_io_blocker");
assert.equal(payload.m3Io.action, "do.write");
assert.equal(payload.m3Io.do1.targetValue, true);
assert.equal(payload.m3Io.di1.observedValue, null);
assert.equal(payload.m3Io.blocker.code, "m3_wiring_missing");
assert.equal(payload.blocker.code, "m3_wiring_missing");
assert.match(payload.reply.content, /^M3 IO 阻塞:/u);
assert.match(payload.reply.content, /hwlab-patch-panel 未确认 active/u);
assert.match(payload.reply.content, /res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1/u);
assert.match(payload.reply.content, /trusted=falsedurable=false/u);
assert.equal(calls.length, 1);
assert.equal(new URL(calls[0].url).pathname, "/v1/m3/io");
assert.equal(calls[0].request.body.action, "do.write");
assert.equal(calls[0].request.body.value, true);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(fakeCodex.root, { recursive: true, force: true });
}
});
test("cloud api /v1/agent/chat routes M3 through Codex stdio then in-process HWLAB API handler", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-m3-skill-"));
const fakeCodex = await createFakeCodexCommand();
@@ -2822,6 +2971,16 @@ test("cloud api /v1/agent/chat routes M3 through Codex stdio then in-process HWL
assert.equal(payload.runner.codexStdio, true);
assert.equal(payload.codexStdioFeasibility.ready, true);
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
assert.equal(payload.responseType, "m3_io_result");
assert.equal(payload.m3Io.type, "m3_io_result");
assert.equal(payload.m3Io.action, "do.write");
assert.equal(payload.m3Io.do1.targetValue, true);
assert.equal(payload.m3Io.di1.observedValue, true);
assert.equal(payload.m3Io.operation.operationId.startsWith("op_m3_do_write_"), true);
assert.equal(payload.m3Io.session.sessionId, "ses_server_stdio_m3_skill");
assert.equal(payload.m3Io.path.summary, "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io");
assert.equal(payload.m3Io.trust.trusted, false);
assert.equal(payload.m3Io.trust.durable, false);
const skillTool = payload.toolCalls.find((toolCall) => toolCall.name === "hwlab-agent-runtime.m3-io");
assert.ok(skillTool);
assert.equal(skillTool.type, "skill-cli");
@@ -2922,6 +3081,9 @@ test("cloud api /v1/agent/chat blocks direct M3 API base targets only after Code
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(payload.responseType, "m3_io_blocker");
assert.equal(payload.m3Io.type, "m3_io_blocker");
assert.equal(payload.m3Io.blocker.code, "direct_hardware_target_blocked");
assert.equal(payload.toolCalls[0].name, "codex");
assert.equal(payload.toolCalls[0].status, "completed");
const skillTool = payload.toolCalls.find((toolCall) => toolCall.name === "hwlab-agent-runtime.m3-io");
@@ -2998,6 +3160,9 @@ test("cloud api /v1/agent/chat reports M3 Skill CLI missing API base after Codex
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(payload.responseType, "m3_io_blocker");
assert.equal(payload.m3Io.type, "m3_io_blocker");
assert.equal(payload.m3Io.blocker.code, "skill_cli_api_base_missing");
assert.equal(payload.skills.blockers[0].code, "skill_cli_api_base_missing");
assert.equal(payload.toolCalls[0].name, "codex");
assert.equal(payload.toolCalls[0].status, "completed");
+11 -2
View File
@@ -2522,7 +2522,9 @@ function hasCodeAgentCompletedEvidenceVisibility({ app, codeAgentM3Evidence }) {
/isRealCompletedChatResult\(result\)/u.test(app) &&
/classifyCodeAgentCompletion\(result\)/u.test(app) &&
/status:\s*"completed"/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
/sourceKind:\s*"DEV-LIVE"/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
/m3_io_result/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
/sourceKind:\s*result\?\.m3Io\?\.type === "m3_io_result"[\s\S]*?"CONTROLLED"[\s\S]*?"DEV-LIVE"/u.test(functionBody(app, "classifyCodeAgentCompletion")) &&
/m3_io_blocker/u.test(app) &&
/Code Agent 完成证据不足/u.test(app) &&
/本次不会标记为真实完成/u.test(app)
);
@@ -2573,9 +2575,15 @@ function hasCodeAgentStatusSummaryContract({ html, app, styles, codeAgentStatus
function hasCodeAgentConversationUxStates({ app, styles }) {
const submitBody = `${functionBody(app, "initCommandBar")}\n${functionBody(app, "submitAgentMessage")}`;
const agentToneBody = functionBody(app, "agentStatusTone");
const statusToneContract =
/status === "completed" \? "dev-live" : status === "failed" \|\| status === "blocked" \? "blocked" : status === "running" \? "pending" : "source"/u.test(app) ||
/status === "completed" \? "dev-live" : \["failed",\s*"blocked",\s*"timeout",\s*"canceled",\s*"error"\]\.includes\(status\) \? "blocked" : status === "running" \? "pending" : "source"/u.test(app);
/status === "completed" \? "dev-live" : \["failed",\s*"blocked",\s*"timeout",\s*"canceled",\s*"error"\]\.includes\(status\) \? "blocked" : status === "running" \? "pending" : "source"/u.test(app) ||
(
/function\s+agentStatusTone\s*\(/u.test(app) &&
/m3_io_result/u.test(agentToneBody) &&
/\["failed",\s*"blocked",\s*"timeout",\s*"canceled",\s*"error"\]\.includes\(status\)/u.test(agentToneBody)
);
return (
/function\s+classifyCodeAgentCompletion\s*\(/u.test(app) &&
/function\s+isSourceFixtureChatResult\s*\(/u.test(app) &&
@@ -2614,6 +2622,7 @@ function hasCodeAgentConversationUxStates({ app, styles }) {
!/message-evidence\s*\{/u.test(styles) &&
/width:\s*min\(100%,\s*780px\)/u.test(styles) &&
statusToneContract &&
/m3_io_blocker/u.test(functionBody(app, "agentStatusLabel")) &&
!/sourceKind:\s*"SOURCE"[\s\S]{0,160}status:\s*"completed"/u.test(app)
);
}
+94 -14
View File
@@ -694,16 +694,17 @@ async function submitAgentMessage(value, options = {}) {
const completion = classifyCodeAgentCompletion(result);
const status = completion.status;
const structuredBlockedError = structuredBlockedErrorFromResult(result);
const m3Presentation = m3IoPresentation(result);
state.chatMessages[index] = {
...pendingMessage,
title: completion.title,
text: completion.replied
title: m3Presentation?.title ?? completion.title,
text: m3Presentation?.text ?? (completion.replied
? result.reply?.content || "Code Agent 没有返回文本。"
: structuredBlockedError
? failureMessage({ ...result, error: structuredBlockedError })
: result.status === "completed"
? untrustedCompletionMessage(result)
: failureMessage(result),
: failureMessage(result)),
status,
traceId: result.traceId,
conversationId: result.conversationId || result.sessionId || state.conversationId,
@@ -2490,7 +2491,7 @@ function renderAgentChatStatus(status, result = null) {
blocked: "服务受阻"
};
el.agentChatStatus.textContent = agentStatusLabel(status, result, labels);
el.agentChatStatus.className = `state-tag tone-${toneClass(status === "completed" ? "dev-live" : ["failed", "blocked", "timeout", "canceled", "error"].includes(status) ? "blocked" : status === "running" ? "pending" : "source")}`;
el.agentChatStatus.className = `state-tag tone-${toneClass(agentStatusTone(status, result))}`;
el.commandInput.disabled = status === "running";
el.commandSend.disabled = status === "running";
el.commandSend.textContent = status === "running" ? "发送中" : "发送";
@@ -2558,7 +2559,11 @@ function sessionSummaryTone(summary) {
}
function agentStatusLabel(status, result, labels) {
if (status === "completed" && result?.m3Io?.type === "m3_io_result") {
return result.m3Io.trust?.trusted === true ? "M3 IO 可信回复" : "M3 IO 受控结果";
}
if (status !== "failed") return labels[status] ?? statusLabel(status);
if (result?.m3Io?.type === "m3_io_blocker") return "M3 IO 阻塞";
const presentation = agentFailurePresentation(result?.error, { result });
return (
{
@@ -2578,6 +2583,16 @@ function agentStatusLabel(status, result, labels) {
);
}
function agentStatusTone(status, result) {
if (status === "completed" && result?.m3Io?.type === "m3_io_result") {
return result.m3Io.trust?.trusted === true ? "dev-live" : "warn";
}
if (status === "completed") return "dev-live";
if (["failed", "blocked", "timeout", "canceled", "error"].includes(status)) return "blocked";
if (status === "running") return "pending";
return "source";
}
function sourceTitle(result) {
return `Code Agent 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`;
}
@@ -2615,6 +2630,14 @@ function classifyCodeAgentCompletion(result) {
title: "Code Agent 返回错误"
};
}
if (result?.m3Io?.type === "m3_io_blocker") {
return {
status: "failed",
replied: false,
sourceKind: "BLOCKED",
title: "M3 IO 阻塞"
};
}
if (isStructuredBlockedChatResult(result)) {
return {
status: "failed",
@@ -2627,7 +2650,7 @@ function classifyCodeAgentCompletion(result) {
return {
status: "completed",
replied: true,
sourceKind: "DEV-LIVE",
sourceKind: result?.m3Io?.type === "m3_io_result" && result.m3Io.trust?.trusted !== true ? "CONTROLLED" : "DEV-LIVE",
title: sourceTitle(result)
};
}
@@ -2662,6 +2685,8 @@ function isStructuredBlockedChatResult(result) {
typeof result === "object" &&
(
result.blocker?.code ||
result.m3Io?.type === "m3_io_blocker" ||
result.m3Io?.blocker?.code ||
error.blocker?.code ||
result.capabilityLevel === "hwlab-api-control-blocked"
)
@@ -2672,19 +2697,44 @@ function structuredBlockedErrorFromResult(result) {
if (!isStructuredBlockedChatResult(result)) return null;
const error = result?.error && typeof result.error === "object" ? result.error : {};
const blocker = error.blocker ?? result?.blocker ?? {};
const code = normalizeErrorCode(error.code ?? blocker.code ?? "code_agent_blocked");
const m3Blocker = result?.m3Io?.blocker ?? {};
const code = normalizeErrorCode(error.code ?? m3Blocker.code ?? blocker.code ?? "code_agent_blocked");
const selectedBlocker = result?.m3Io?.type === "m3_io_blocker" && m3Blocker.code ? m3Blocker : blocker;
return {
...error,
code,
layer: error.layer ?? blocker.layer,
category: error.category ?? blocker.category,
blocker,
retryable: error.retryable ?? blocker.retryable,
userMessage: error.userMessage ?? blocker.userMessage,
message: error.message ?? blocker.summary ?? code,
layer: error.layer ?? selectedBlocker.layer,
category: error.category ?? selectedBlocker.category,
blocker: selectedBlocker,
retryable: error.retryable ?? selectedBlocker.retryable,
userMessage: error.userMessage ?? selectedBlocker.userMessage,
message: error.message ?? selectedBlocker.summary ?? selectedBlocker.zh ?? selectedBlocker.message ?? code,
traceId: error.traceId ?? result?.traceId,
route: error.route ?? blocker.route,
toolName: error.toolName ?? blocker.toolName
route: error.route ?? selectedBlocker.route,
toolName: error.toolName ?? selectedBlocker.toolName
};
}
function m3IoPresentation(result) {
const m3 = result?.m3Io && typeof result.m3Io === "object" ? result.m3Io : null;
if (!m3) return null;
const do1 = m3.do1?.targetValue === undefined || m3.do1?.targetValue === null ? "未产生/不可证明" : String(m3.do1.targetValue);
const di1 = m3.di1?.observedValue === undefined || m3.di1?.observedValue === null ? "未产生/不可证明" : String(m3.di1.observedValue);
const fields = `traceId=${m3.trace?.traceId ?? result?.traceId ?? "null"}operationId=${m3.operation?.operationId ?? "null"}`;
const trust = `trusted=${m3.trust?.trusted === true ? "true" : "false"}durable=${m3.trust?.durable === true ? "true" : "false"}`;
const path = m3.path?.summary ?? "Code Agent -> Skill CLI -> HWLAB API";
const wiring = m3.wiring?.label ?? "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1";
if (m3.type === "m3_io_blocker") {
const blocker = m3.blocker ?? {};
const reason = blocker.userMessage ?? blocker.zh ?? blocker.message ?? blocker.code ?? "M3 控制链路仍受阻";
return {
title: "M3 IO 阻塞",
text: `M3 IO 阻塞:${reason}\nDO1 目标值:${do1}DI1 观测值:${di1}\n路径:${path}。接线:${wiring}\n排查字段:${fields}blocker=${blocker.code ?? "unknown"}\n可信状态:${trust};未伪造 DEV-LIVE,也未使用 OpenAI fallback 冒充执行。`
};
}
return {
title: "M3 IO 结果",
text: `M3 IO 结果:DO1 目标值=${do1}DI1 观测值=${di1}\n路径:${path}。接线:${wiring}\n排查字段:${fields}auditId=${m3.operation?.auditId ?? "null"}evidenceId=${m3.operation?.evidenceId ?? "null"}\n可信状态:${trust};这是受控操作返回,不是 M3 DEV-LIVE 验收结论。`
};
}
@@ -3074,6 +3124,7 @@ function messageM3EvidencePanel(message) {
rows.className = "message-m3-rows";
rows.append(...m3EvidenceRows(evidence).map(m3EvidenceRowElement));
section.append(rows);
section.append(m3EvidenceRawDetails(evidence, message));
return section;
}
@@ -3099,6 +3150,35 @@ function m3EvidenceRowElement(row) {
return item;
}
function m3EvidenceRawDetails(evidence, message) {
const details = document.createElement("details");
details.className = "message-m3-raw";
const summary = document.createElement("summary");
summary.textContent = "原始字段";
const pre = document.createElement("pre");
pre.textContent = boundedJson({
responseType: message.responseType,
m3Io: message.m3Io ?? null,
toolCall: evidence.source ?? null,
providerTrace: message.providerTrace ?? null,
runnerTrace: message.runnerTrace ?? null,
blocker: message.blocker ?? null,
blockers: message.blockers ?? null
}, 6000);
details.append(summary, pre);
return details;
}
function boundedJson(value, maxLength = 6000) {
let text = "";
try {
text = JSON.stringify(value, null, 2);
} catch {
text = String(value ?? "");
}
return text.length > maxLength ? `${text.slice(0, maxLength)}\n...字段已截断` : text;
}
function messageTracePanel(message) {
const trace = message.runnerTrace && typeof message.runnerTrace === "object" ? message.runnerTrace : null;
if (!trace && !message.traceId) return null;
+125 -27
View File
@@ -9,6 +9,7 @@ export const CODE_AGENT_M3_TRUSTED_ROUTE = Object.freeze({
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";
export const CODE_AGENT_M3_PATH_LABEL = "Code Agent -> Skill CLI -> HWLAB API";
const missingProofLabel = "未产生/不可证明";
const nonProofIds = new Set(["", "n/a", "none", "null", "undefined", "not_observed", "not-observed"]);
@@ -45,57 +46,71 @@ export function isCodeAgentM3SkillCompletion(value) {
export function extractCodeAgentM3Evidence(message) {
const toolCall = findM3ToolCall(message);
const m3Io = objectOrNull(message?.m3Io);
const source = toolCall ?? message?.providerTrace ?? message?.runnerTrace ?? null;
if (!source && !isM3ProviderMessage(message)) return null;
if (!source && !m3Io && !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 route = firstNonEmpty(source?.route, m3Io?.trace?.route, m3Io?.path?.hwlabApi?.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 trustBlocker = firstTrustBlocker(m3Io, source, response, providerTrace, runnerTrace, message);
const target = normalizeTarget(source, response, command, m3Io);
const readback = normalizeReadback(source, response, result, m3Io);
const action = firstNonEmpty(m3Io?.action, source?.action, response?.action, command?.action);
const trust = normalizeTrust(m3Io, source, response, trustBlocker);
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)
operationId: firstNonEmpty(m3Io?.operation?.operationId, source?.operationId, response?.operationId, providerTrace?.operationId, runnerTrace?.operationId, message?.operationId),
traceId: firstNonEmpty(m3Io?.trace?.traceId, source?.traceId, response?.traceId, providerTrace?.traceId, runnerTrace?.traceId, message?.traceId),
auditId: firstNonEmpty(m3Io?.operation?.auditId, source?.audit?.auditId, response?.audit?.auditId, message?.auditId, message?.audit?.auditId),
evidenceId: firstNonEmpty(m3Io?.operation?.evidenceId, 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),
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 accepted = normalizeBoolean(firstDefined(m3Io?.accepted, source?.accepted, response?.accepted, providerTrace?.accepted));
const status = firstNonEmpty(m3Io?.status, source?.status, response?.status, providerTrace?.status, runnerTrace?.status, message?.status);
const links = normalizeEvidenceLinks(source, response, providerTrace, message);
const path = normalizePath(m3Io, route);
const wiring = normalizeWiring(m3Io);
if (!isM3EvidenceShape({ route, provider, runnerKind, source })) return null;
if (!isM3EvidenceShape({ route, provider, runnerKind, source, m3Io })) return null;
return {
kind: "m3-io",
responseType: firstNonEmpty(m3Io?.type, source?.responseType, message?.responseType),
provider,
runnerKind,
route,
action,
status,
accepted,
blocker,
trustBlocker,
trust,
directPathInvalid,
readbackMismatch,
target,
readback,
path,
wiring,
ids,
links,
verdict: classifyM3Verdict({
accepted,
status,
blocker,
trustBlocker,
trust,
directPathInvalid,
readbackMismatch,
ids
@@ -116,11 +131,17 @@ export function m3EvidenceRows(evidence) {
const readback = evidence.readback;
return [
evidenceValueRow("状态", evidence.verdict.label, { tone: evidence.verdict.tone }),
evidenceValueRow("类型", evidence.responseType ?? evidence.kind, { tone: evidence.responseType === "m3_io_blocker" ? "blocked" : "source" }),
evidenceValueRow("动作", evidence.action ?? missingProofLabel),
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("路径", evidence.path.summary, { copyable: true }),
evidenceValueRow("接线", evidence.wiring.label, { copyable: true }),
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" }),
evidenceValueRow("DO1", resourcePortValueLabel(target), { copyable: false }),
evidenceValueRow("DI1", resourcePortValueLabel(readback), { tone: evidence.readbackMismatch ? "blocked" : "source" }),
evidenceValueRow("trusted", booleanStateLabel(evidence.trust.trusted), { tone: evidence.trust.trusted === true ? "dev-live" : evidence.trust.trusted === false ? "warn" : "source" }),
evidenceValueRow("durable", booleanStateLabel(evidence.trust.durable), { tone: evidence.trust.durable === true ? "dev-live" : evidence.trust.durable === false ? "warn" : "source" }),
...idRows,
evidenceValueRow("blocker", blockerLabel(evidence.blocker), { tone: evidence.blocker ? "blocked" : "source" })
];
@@ -128,7 +149,7 @@ export function m3EvidenceRows(evidence) {
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)}`;
return `${evidence.verdict.label} / ${evidence.path.summary} / DO1=${formatValue(evidence.target.value)} / DI1=${formatValue(evidence.readback.value)} / operation=${proofLabel(evidence.ids.operationId)}`;
}
export function proofLabel(value) {
@@ -147,7 +168,7 @@ function evidenceValueRow(label, value, options = {}) {
};
}
function classifyM3Verdict({ accepted, status, blocker, directPathInvalid, readbackMismatch, ids }) {
function classifyM3Verdict({ accepted, status, blocker, trustBlocker, trust, directPathInvalid, readbackMismatch, ids }) {
if (directPathInvalid) {
return {
key: "direct-path-invalid",
@@ -173,6 +194,14 @@ function classifyM3Verdict({ accepted, status, blocker, directPathInvalid, readb
};
}
if (accepted === true && ["succeeded", "completed", "accepted"].includes(String(status ?? "").toLowerCase())) {
if (trustBlocker || trust?.trusted === false) {
return {
key: "accepted-untrusted",
tone: "degraded",
label: "accepted / 可信持久化未通过",
summary: trustBlocker ? blockerLabel(trustBlocker) : "受控路径已返回,但 trusted/durable 未 green。"
};
}
const missingIds = Object.entries(ids).filter(([, value]) => proofLabel(value) === missingProofLabel);
if (missingIds.length > 0) {
return {
@@ -229,26 +258,66 @@ function classifyDirectPathInvalid(...sources) {
return false;
}
function normalizeTarget(source, response, command) {
function normalizeTarget(source, response, command, m3Io = null) {
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)
resourceId: firstNonEmpty(m3Io?.do1?.resourceId, target.resourceId, commandObject.resourceId, source?.resourceId, response?.resourceId, CODE_AGENT_M3_TRUSTED_ROUTE.fromResourceId),
port: firstNonEmpty(m3Io?.do1?.port, target.port, commandObject.port, source?.port, response?.port, CODE_AGENT_M3_TRUSTED_ROUTE.fromPort),
value: firstDefined(m3Io?.do1?.targetValue, target.value, commandObject.value, source?.value, response?.value)
};
}
function normalizeReadback(source, response, result) {
function normalizeReadback(source, response, result, m3Io = null) {
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)
resourceId: firstNonEmpty(m3Io?.di1?.resourceId, targetReadback.resourceId, CODE_AGENT_M3_TRUSTED_ROUTE.toResourceId),
port: firstNonEmpty(m3Io?.di1?.port, targetReadback.port, CODE_AGENT_M3_TRUSTED_ROUTE.toPort),
value: firstDefined(m3Io?.di1?.observedValue, targetReadback.value, result?.value)
};
}
function normalizePath(m3Io, route) {
return {
summary: firstNonEmpty(m3Io?.path?.summary, `${CODE_AGENT_M3_PATH_LABEL} ${route ?? CODE_AGENT_M3_IO_ROUTE}`),
segments: Array.isArray(m3Io?.path?.segments) && m3Io.path.segments.length > 0
? m3Io.path.segments
: ["Code Agent", "Skill CLI", "HWLAB API"]
};
}
function normalizeWiring(m3Io) {
return {
from: firstNonEmpty(m3Io?.wiring?.from, `${CODE_AGENT_M3_TRUSTED_ROUTE.fromResourceId}:${CODE_AGENT_M3_TRUSTED_ROUTE.fromPort}`),
via: firstNonEmpty(m3Io?.wiring?.via, CODE_AGENT_M3_TRUSTED_ROUTE.patchPanelServiceId),
to: firstNonEmpty(m3Io?.wiring?.to, `${CODE_AGENT_M3_TRUSTED_ROUTE.toResourceId}:${CODE_AGENT_M3_TRUSTED_ROUTE.toPort}`),
label: firstNonEmpty(
m3Io?.wiring?.label,
`${CODE_AGENT_M3_TRUSTED_ROUTE.fromResourceId}:${CODE_AGENT_M3_TRUSTED_ROUTE.fromPort} -> ${CODE_AGENT_M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${CODE_AGENT_M3_TRUSTED_ROUTE.toResourceId}:${CODE_AGENT_M3_TRUSTED_ROUTE.toPort}`
)
};
}
function normalizeTrust(m3Io, source, response, trustBlocker) {
const trust = objectOrNull(m3Io?.trust) ?? {};
const durableValue = firstDefined(
trust.durable,
source?.durable?.durable,
response?.durable?.durable
);
const trustedValue = firstDefined(trust.trusted, source?.trusted, response?.trusted);
const durable = normalizeBoolean(durableValue);
const trusted = normalizeBoolean(trustedValue);
return {
trusted: trusted === null ? (trustBlocker ? false : null) : trusted,
durable: durable === null ? null : durable,
blocker: trustBlocker,
sourceKind: firstNonEmpty(trust.evidenceSourceKind, source?.evidence?.sourceKind, response?.evidence?.sourceKind),
durableStatus: firstNonEmpty(trust.durableStatus, source?.durable?.status, response?.durable?.status)
};
}
@@ -260,16 +329,34 @@ function resourcePortValueLabel(item) {
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);
const candidate = objectOrNull(source.blocker) ?? objectOrNull(source.capabilityBlocker) ?? objectOrNull(source.error?.blocker);
if (candidate) return candidate;
if (Array.isArray(source.blockers)) {
const blocker = source.blockers.find((item) => item?.code || item?.message || item?.zh);
const blocker = source.blockers.find((item) => (item?.code || item?.message || item?.zh) && !isTrustOnlyBlocker(item));
if (blocker) return blocker;
}
}
return null;
}
function firstTrustBlocker(...sources) {
for (const source of sources) {
if (!source || typeof source !== "object") continue;
const candidate = objectOrNull(source.trustBlocker) ?? objectOrNull(source.trust?.trustBlocker);
if (candidate) return candidate;
if (Array.isArray(source.blockers)) {
const blocker = source.blockers.find((item) => (item?.code || item?.message || item?.zh) && isTrustOnlyBlocker(item));
if (blocker) return blocker;
}
}
return null;
}
function isTrustOnlyBlocker(blocker) {
const text = `${blocker?.code ?? ""} ${blocker?.layer ?? ""} ${blocker?.category ?? ""}`;
return /durable|runtime|trust|evidence/i.test(text);
}
function blockerLabel(blocker) {
if (!blocker) return "none";
return [blocker.code, blocker.zh ?? blocker.message ?? blocker.reason].filter(Boolean).join(": ");
@@ -313,8 +400,9 @@ function parseToolStdout(stdout) {
}
}
function isM3EvidenceShape({ route, provider, runnerKind, source }) {
function isM3EvidenceShape({ route, provider, runnerKind, source, m3Io }) {
if (route !== CODE_AGENT_M3_IO_ROUTE) return false;
if (m3Io?.type === "m3_io_result" || m3Io?.type === "m3_io_blocker") return true;
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;
@@ -367,3 +455,13 @@ function stringOrNull(value) {
const text = String(value ?? "").trim();
return text ? text : null;
}
function formatValue(value) {
return value === undefined || value === null ? missingProofLabel : String(value);
}
function booleanStateLabel(value) {
if (value === true) return "true";
if (value === false) return "false";
return "unknown";
}
@@ -22,6 +22,7 @@ test("renders accepted M3 Skill CLI operation/audit/evidence metadata", () => {
}));
assert.equal(evidence.verdict.key, "accepted");
assert.equal(evidence.responseType, "m3_io_result");
assert.equal(evidence.route, CODE_AGENT_M3_IO_ROUTE);
assert.equal(evidence.target.resourceId, "res_boxsimu_1");
assert.equal(evidence.target.port, "DO1");
@@ -34,6 +35,10 @@ test("renders accepted M3 Skill CLI operation/audit/evidence metadata", () => {
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, "路径"), "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io");
assert.equal(rowValue(evidence, "接线"), "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1");
assert.match(rowValue(evidence, "DO1"), /res_boxsimu_1:DO1 value=true/u);
assert.match(rowValue(evidence, "DI1"), /res_boxsimu_2:DI1 value=true/u);
assert.equal(rowValue(evidence, "evidenceId"), "evd_m3_cli_accepted_succeeded");
});
@@ -49,11 +54,38 @@ test("renders blocker state from Skill CLI without trusted pass", () => {
}));
assert.equal(evidence.verdict.key, "blocked");
assert.equal(evidence.responseType, "m3_io_blocker");
assert.equal(evidence.verdict.tone, "blocked");
assert.match(rowValue(evidence, "blocker"), /m3_wiring_missing/u);
assert.equal(isCodeAgentM3SkillCompletion({ ...m3Message(), status: "failed" }), false);
});
test("renders accepted but non-durable M3 IO result as degraded, not DEV-LIVE trusted", () => {
const evidence = extractCodeAgentM3Evidence(m3Message({
status: "succeeded",
accepted: true,
value: false,
readbackValue: false,
trust: {
trusted: false,
durable: false,
durableStatus: "degraded",
durableBlocker: "runtime_durable_not_green"
},
trustBlocker: {
code: "runtime_durable_not_green",
layer: "runtime-durable",
zh: "runtime durable 未 green"
}
}));
assert.equal(evidence.verdict.key, "accepted-untrusted");
assert.equal(evidence.verdict.tone, "degraded");
assert.equal(rowValue(evidence, "trusted"), "false");
assert.equal(rowValue(evidence, "durable"), "false");
assert.match(rowValue(evidence, "DI1"), /value=false/u);
});
test("shows missing evidenceId as unavailable proof instead of fabricating one", () => {
const evidence = extractCodeAgentM3Evidence(m3Message({
status: "succeeded",
@@ -81,7 +113,7 @@ test("classifies DO1 target and DI1 readback mismatch as blocked proof", () => {
assert.equal(evidence.readbackMismatch, true);
assert.equal(evidence.verdict.key, "readback-mismatch");
assert.match(rowValue(evidence, "readback"), /res_boxsimu_2:DI1 value=false/u);
assert.match(rowValue(evidence, "DI1"), /res_boxsimu_2:DI1 value=false/u);
});
test("classifies direct gateway/box/patch-panel path as invalid and never trusted", () => {
@@ -133,10 +165,18 @@ function m3Message(overrides = {}) {
const readbackValue = Object.hasOwn(overrides, "readbackValue") ? overrides.readbackValue : value;
const status = overrides.status ?? "succeeded";
const accepted = Object.hasOwn(overrides, "accepted") ? overrides.accepted : true;
const responseType = overrides.responseType ?? (accepted && status !== "blocked" ? "m3_io_result" : "m3_io_blocker");
const trust = overrides.trust ?? {
trusted: responseType === "m3_io_result",
durable: responseType === "m3_io_result",
durableStatus: responseType === "m3_io_result" ? "green" : "blocked",
durableBlocker: responseType === "m3_io_result" ? null : "m3_io_blocked"
};
const toolCall = {
id: "tool_m3_fixture",
type: "skill-cli",
name: "hwlab-agent-runtime.m3-io",
responseType,
status: accepted ? "completed" : "blocked",
route: CODE_AGENT_M3_IO_ROUTE,
accepted,
@@ -165,7 +205,8 @@ function m3Message(overrides = {}) {
}
},
blocker: overrides.blocker ?? null,
blockers: overrides.blocker ? [overrides.blocker] : [],
trustBlocker: overrides.trustBlocker ?? null,
blockers: [overrides.blocker, overrides.trustBlocker].filter(Boolean),
controlPath: overrides.controlPath ?? {
cloudApi: true,
gatewaySimu: true,
@@ -203,6 +244,50 @@ function m3Message(overrides = {}) {
return {
status: "completed",
responseType,
m3Io: {
type: responseType,
status,
action: "do.write",
accepted,
do1: {
resourceId: "res_boxsimu_1",
port: "DO1",
targetValue: value
},
di1: {
resourceId: "res_boxsimu_2",
port: "DI1",
observedValue: readbackValue
},
wiring: {
from: "res_boxsimu_1:DO1",
via: "hwlab-patch-panel",
to: "res_boxsimu_2:DI1",
label: "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1"
},
path: {
summary: "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io",
segments: ["Code Agent", "Skill CLI", "HWLAB API"],
hwlabApi: {
route: CODE_AGENT_M3_IO_ROUTE,
method: "POST"
}
},
operation: {
operationId,
auditId,
evidenceId
},
trace: {
traceId,
route: CODE_AGENT_M3_IO_ROUTE,
method: "POST"
},
trust,
blocker: overrides.blocker ?? null,
blockers: [overrides.blocker, overrides.trustBlocker].filter(Boolean)
},
provider: "hwlab-skill-cli",
model: "controlled-m3-io",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
@@ -224,6 +309,7 @@ function m3Message(overrides = {}) {
providerTrace: {
runnerKind: "hwlab-m3-io-skill-cli",
skill: "hwlab-agent-runtime.m3-io",
responseType,
route: CODE_AGENT_M3_IO_ROUTE,
status,
accepted,
+2 -1
View File
@@ -841,7 +841,8 @@ assert.match(codeAgentStatus, /unsafeGreenForNonReady/);
assert.match(codeAgentStatus, /provider_config_blocked/);
assert.doesNotMatch(codeAgentStatus, /sourceMain|SOURCE main|待部署 commit|targetCommit/u);
assert.match(app, /sourceKind:\s*completion\.sourceKind/);
assert.match(app, /status === "completed" \? "dev-live"/);
assert.match(app, /function\s+agentStatusTone\s*\(/);
assert.match(app, /m3_io_result/);
assert.doesNotMatch(app, /status === "failed" \? "dev-live"/);
assert.doesNotMatch(`${html}\n${app}`, /provider_unavailable[\s\S]{0,120}tone-[\w-]*dev-live/iu);
assert.doesNotMatch(app, /tone:\s*state\.conversationId\s*\?\s*"dev-live"/);
+28
View File
@@ -1135,6 +1135,34 @@ h3 {
text-underline-offset: 2px;
}
.message-m3-raw {
min-width: 0;
padding: 6px;
background: rgba(20, 24, 23, 0.72);
border: 1px solid var(--line-soft);
}
.message-m3-raw summary {
cursor: pointer;
color: var(--dim);
font-family: var(--mono);
font-size: 10px;
}
.message-m3-raw pre {
min-width: 0;
max-height: 240px;
margin: 7px 0 0;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
line-height: 1.4;
}
.copy-chip {
min-width: 42px;
min-height: 24px;