feat: expose M3 Code Agent route evidence

This commit is contained in:
unidesk-code-queue-runner
2026-05-23 15:21:55 +00:00
parent 43a6059b78
commit 9296c28bff
10 changed files with 295 additions and 6 deletions
+22
View File
@@ -1247,29 +1247,39 @@ function m3IoSkillRunnerResult({
exitCode: skillResult.ok ? 0 : 2,
stdout: boundToolOutput(JSON.stringify({
route: skillResult.route,
method: skillResult.method ?? "POST",
status: skillResult.status,
accepted: skillResult.accepted,
traceId: skillResult.traceId,
operationId: skillResult.operationId,
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null,
audit: skillResult.audit,
evidence: skillResult.evidence,
durable: skillResult.durable,
blocker: skillResult.blocker,
result: skillResult.result,
readback: skillResult.readback ?? skillResult.result?.targetReadback ?? null,
safety: skillResult.safety
})).text,
stderrSummary: skillResult.blocker?.code ?? "",
outputTruncated: false,
route: HWLAB_M3_IO_API_ROUTE,
method: skillResult.method ?? "POST",
hwlabApi: skillResult.hwlabApi,
capabilityLevel,
controlReady: skillResult.controlReady === true,
accepted: skillResult.accepted,
operationStatus: skillResult.status,
apiStatus: skillResult.status,
operationId: skillResult.operationId,
traceId: skillResult.traceId,
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null,
audit: skillResult.audit,
evidence: skillResult.evidence,
durable: skillResult.durable,
readback: skillResult.readback ?? skillResult.result?.targetReadback ?? null,
blocker: skillResult.blocker,
capabilityBlocker: skillResult.capabilityBlocker ?? null,
trustBlocker: skillResult.trustBlocker ?? null,
@@ -1342,8 +1352,12 @@ function m3IoSkillRunnerResult({
runnerKind: M3_IO_SKILL_RUNNER_KIND,
skill: HWLAB_M3_IO_SKILL_NAME,
route: HWLAB_M3_IO_API_ROUTE,
method: skillResult.method ?? "POST",
traceId: skillResult.traceId,
operationId: skillResult.operationId,
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null,
readback: skillResult.readback ?? skillResult.result?.targetReadback ?? null,
capabilityLevel,
controlReady: skillResult.controlReady === true,
accepted: skillResult.accepted,
@@ -1359,6 +1373,7 @@ function m3IoSkillMissingApiBaseUrlResult({ traceId, requestId, actorId, blocker
service: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route: HWLAB_M3_IO_API_ROUTE,
method: "POST",
hwlabApi: {
route: HWLAB_M3_IO_API_ROUTE,
redactedUrl: null,
@@ -1384,6 +1399,8 @@ function m3IoSkillMissingApiBaseUrlResult({ traceId, requestId, actorId, blocker
requestId,
actorId,
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written",
@@ -1422,6 +1439,7 @@ function m3IoSkillMissingApiBaseUrlResult({ traceId, requestId, actorId, blocker
value: null,
targetReadback: null
},
readback: null,
controlPath: {
cloudApi: false,
gatewaySimu: false,
@@ -2257,10 +2275,14 @@ function m3IoSkillRunnerTrace({ traceId, events, startedAt, finishedAt = started
finishedAt,
events,
route: HWLAB_M3_IO_API_ROUTE,
method: skillResult?.method ?? "POST",
skill: HWLAB_M3_IO_SKILL_NAME,
capabilityLevel,
controlReady: skillResult?.controlReady === true,
operationId: skillResult?.operationId ?? null,
auditId: skillResult?.auditId ?? skillResult?.audit?.auditId ?? null,
evidenceId: skillResult?.evidenceId ?? skillResult?.evidence?.evidenceId ?? null,
readback: skillResult?.readback ?? skillResult?.result?.targetReadback ?? null,
accepted: skillResult?.accepted ?? false,
status: skillResult?.status ?? "blocked",
blocker: skillResult?.blocker ?? null,
@@ -391,12 +391,21 @@ test("Code Agent M3 DO write uses Skill CLI to call only HWLAB API /v1/m3/io", a
assert.equal(payload.toolCalls[0].name, "hwlab-agent-runtime.m3-io");
assert.equal(payload.toolCalls[0].status, "completed");
assert.equal(payload.toolCalls[0].route, HWLAB_M3_IO_API_ROUTE);
assert.equal(payload.toolCalls[0].method, "POST");
assert.equal(payload.toolCalls[0].capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
assert.equal(payload.toolCalls[0].controlReady, true);
assert.equal(payload.toolCalls[0].accepted, true);
assert.equal(payload.toolCalls[0].operationId, "op_m3_do_write_skill");
assert.equal(payload.toolCalls[0].traceId, "trc_m3_skill_write");
assert.equal(payload.toolCalls[0].auditId, "aud_m3_do_write_skill_succeeded");
assert.equal(payload.toolCalls[0].evidenceId, "evd_m3_do_write_skill_succeeded");
assert.equal(payload.toolCalls[0].readback.value, true);
assert.equal(payload.runnerTrace.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(payload.runnerTrace.method, "POST");
assert.equal(payload.runnerTrace.accepted, true);
assert.equal(payload.runnerTrace.auditId, "aud_m3_do_write_skill_succeeded");
assert.equal(payload.runnerTrace.evidenceId, "evd_m3_do_write_skill_succeeded");
assert.equal(payload.runnerTrace.readback.value, true);
assert.equal(payload.runnerTrace.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
assert.equal(payload.runnerTrace.controlReady, true);
assert.equal(payload.runner.writeCapable, true);
@@ -405,6 +414,10 @@ test("Code Agent M3 DO write uses Skill CLI to call only HWLAB API /v1/m3/io", a
assert.equal(payload.runner.safety.directBoxSimuCallsAllowed, false);
assert.equal(payload.runner.safety.directPatchPanelCallsAllowed, false);
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(payload.providerTrace.method, "POST");
assert.equal(payload.providerTrace.auditId, "aud_m3_do_write_skill_succeeded");
assert.equal(payload.providerTrace.evidenceId, "evd_m3_do_write_skill_succeeded");
assert.equal(payload.providerTrace.readback.value, true);
assert.equal(payload.providerTrace.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
assert.equal(payload.providerTrace.controlReady, true);
assert.equal(payload.skills.blockers.some((blocker) => blocker.code === "runtime_durable_not_green"), true);
@@ -537,6 +550,7 @@ test("Code Agent M3 Skill CLI missing API base returns structured config blocker
assert.equal(payload.provider, "hwlab-skill-cli");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(payload.blocker.code, "m3_readiness_blocked");
assert.equal(payload.toolCalls[0].method, "POST");
assert.equal(payload.toolCalls[0].blocker.code, "skill_cli_api_base_missing");
assert.equal(payload.toolCalls[0].blocker.layer, "skill-cli-config");
assert.equal(payload.toolCalls[0].blocker.retryable, false);
@@ -579,10 +593,110 @@ test("Code Agent M3 Skill CLI HWLAB API unavailable returns retryable structured
assert.equal(payload.blocker.layer, "hwlab-api");
assert.equal(payload.blocker.retryable, true);
assert.equal(payload.blocker.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(payload.toolCalls[0].method, "POST");
assert.equal(payload.toolCalls[0].blocker.code, "hwlab_api_unavailable");
assert.equal(payload.toolCalls[0].blocker.retryable, true);
});
test("Code Agent M3 DO false write exposes readback and identifier contract", async () => {
const calls = [];
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_m3_skill_write_false",
traceId: "trc_m3_skill_write_false",
message: "通过 HWLAB API 把 res_boxsimu_1 DO1 写成 false 并读取 res_boxsimu_2 DI1"
},
{
now: () => "2026-05-23T00:06:30.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
OPENAI_API_KEY: "must-not-be-used"
},
callProvider: async () => {
throw new Error("OpenAI fallback must not be used for M3 IO control");
},
m3IoSkillRequestJson: async (url, request) => {
calls.push({ url, request });
return {
ok: true,
status: 200,
body: {
serviceId: "hwlab-cloud-api",
contractVersion: "m3-io-control-v1",
status: "succeeded",
accepted: true,
action: "do.write",
traceId: "trc_m3_skill_write_false",
operationId: "op_m3_do_write_skill_false",
auditId: "aud_m3_do_write_skill_false_succeeded",
evidenceId: "evd_m3_do_write_skill_false_succeeded",
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: false,
targetReadback: {
status: "succeeded",
value: false,
resourceId: "res_boxsimu_2",
port: "DI1"
}
},
controlPath: {
status: "succeeded",
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: true,
frontendBypass: false
}
}
};
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-skill-cli");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
const tool = payload.toolCalls[0];
assert.equal(tool.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(tool.method, "POST");
assert.equal(tool.accepted, true);
assert.equal(tool.status, "completed");
assert.equal(tool.operationId, "op_m3_do_write_skill_false");
assert.equal(tool.traceId, "trc_m3_skill_write_false");
assert.equal(tool.auditId, "aud_m3_do_write_skill_false_succeeded");
assert.equal(tool.evidenceId, "evd_m3_do_write_skill_false_succeeded");
assert.equal(tool.readback.value, false);
assert.equal(tool.readback.resourceId, "res_boxsimu_2");
assert.equal(tool.readback.port, "DI1");
assert.equal(payload.runnerTrace.method, "POST");
assert.equal(payload.runnerTrace.readback.value, false);
assert.equal(payload.providerTrace.readback.value, false);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_IO_API_ROUTE}`);
assert.equal(calls[0].request.body.action, "do.write");
assert.equal(calls[0].request.body.value, false);
assert.equal(calls[0].url.includes("gateway"), false);
assert.equal(calls[0].url.includes("box-simu"), false);
assert.equal(calls[0].url.includes("patch-panel"), false);
});
test("Code Agent blocks direct gateway or patch-panel requests instead of using M3 Skill CLI", async () => {
const direct = await handleCodeAgentChat(
{
+8
View File
@@ -1142,8 +1142,16 @@ test("cloud api /v1/agent/chat routes M3 IO through Skill CLI to /v1/m3/io only"
assert.equal(payload.backend, "hwlab-cloud-api/hwlab-agent-runtime-skill-cli");
assert.equal(payload.toolCalls[0].name, "hwlab-agent-runtime.m3-io");
assert.equal(payload.toolCalls[0].route, "/v1/m3/io");
assert.equal(payload.toolCalls[0].method, "POST");
assert.equal(payload.toolCalls[0].accepted, true);
assert.equal(payload.toolCalls[0].operationId, "op_m3_do_write_server_skill");
assert.equal(payload.toolCalls[0].traceId, "trc_server-test-m3-skill");
assert.equal(payload.toolCalls[0].auditId, "aud_m3_do_write_server_skill_succeeded");
assert.equal(payload.toolCalls[0].evidenceId, "evd_m3_do_write_server_skill_succeeded");
assert.equal(payload.toolCalls[0].readback.value, false);
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(payload.providerTrace.method, "POST");
assert.equal(payload.providerTrace.readback.value, false);
assert.equal(payload.runner.safety.directGatewayCallsAllowed, false);
assert.equal(payload.runner.safety.directBoxSimuCallsAllowed, false);
assert.equal(payload.runner.safety.directPatchPanelCallsAllowed, false);
+7
View File
@@ -419,7 +419,14 @@ async function runLocalContractSmoke() {
assert.equal(m3SkillWrite.provider, "hwlab-skill-cli");
assert.equal(m3SkillWrite.toolCalls[0].name, "hwlab-agent-runtime.m3-io");
assert.equal(m3SkillWrite.toolCalls[0].route, "/v1/m3/io");
assert.equal(m3SkillWrite.toolCalls[0].method, "POST");
assert.equal(m3SkillWrite.toolCalls[0].operationId, "op_m3_do_write_smoke");
assert.equal(m3SkillWrite.toolCalls[0].auditId, "aud_m3_do_write_smoke_succeeded");
assert.equal(m3SkillWrite.toolCalls[0].evidenceId, "evd_m3_do_write_smoke_succeeded");
assert.equal(m3SkillWrite.toolCalls[0].readback.value, true);
assert.equal(m3SkillWrite.providerTrace.fallbackUsed, false);
assert.equal(m3SkillWrite.providerTrace.method, "POST");
assert.equal(m3SkillWrite.providerTrace.readback.value, true);
assert.equal(m3SkillWrite.runner.safety.directGatewayCallsAllowed, false);
assert.equal(m3SkillCalls.length, 1);
assert.equal(m3SkillCalls[0].url, "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/m3/io");
+33 -2
View File
@@ -486,10 +486,12 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
) ?? null;
const providerTrace = payload?.providerTrace && typeof payload.providerTrace === "object" ? payload.providerTrace : null;
const route = skillTool?.route ?? providerTrace?.route ?? payload?.skills?.items?.find((item) => item?.route)?.route ?? null;
const method = skillTool?.method ?? providerTrace?.method ?? null;
const operationId = skillTool?.operationId ?? providerTrace?.operationId ?? null;
const traceId = skillTool?.traceId ?? providerTrace?.traceId ?? payload?.traceId ?? null;
const auditId = skillTool?.audit?.auditId ?? null;
const evidenceId = skillTool?.evidence?.evidenceId ?? null;
const auditId = skillTool?.auditId ?? skillTool?.audit?.auditId ?? providerTrace?.auditId ?? null;
const evidenceId = skillTool?.evidenceId ?? skillTool?.evidence?.evidenceId ?? providerTrace?.evidenceId ?? null;
const readback = skillTool?.readback ?? providerTrace?.readback ?? skillTool?.result?.targetReadback ?? null;
const blockers = normalizeM3RouteBlockers(payload, skillTool);
const safety = {
directGatewayCalls: Boolean(skillTool?.directGatewayCalls ?? payload?.runnerTrace?.directGatewayCalls ?? false),
@@ -510,6 +512,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
skillTool &&
["completed", "blocked"].includes(skillTool.status) &&
routeEvidenceReady &&
method === "POST" &&
safety.directGatewayCalls === false &&
safety.directBoxCalls === false &&
safety.directPatchPanelCalls === false &&
@@ -523,6 +526,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
if (payload?.provider !== "hwlab-skill-cli") missing.push("provider=hwlab-skill-cli");
if (!skillTool) missing.push(`${HWLAB_M3_IO_SKILL_NAME} toolCall`);
if (!routeEvidenceReady) missing.push(`route=${HWLAB_M3_IO_API_ROUTE}`);
if (method !== "POST") missing.push("method=POST");
if (safety.directGatewayCalls || safety.directBoxCalls || safety.directPatchPanelCalls || safety.directTargetMentioned) {
missing.push("no direct gateway/box/patch-panel target");
}
@@ -544,6 +548,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
backend: payload?.backend ?? null,
route,
expectedRoute: HWLAB_M3_IO_API_ROUTE,
method,
toolCallStatus: skillTool?.status ?? null,
accepted,
controlReady,
@@ -552,6 +557,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
traceId,
auditId,
evidenceId,
readback,
idsPresent,
structuredBlocker,
blockers,
@@ -1277,6 +1283,22 @@ function buildCommanderSummary(report) {
auditId: route.auditId ?? null,
evidenceId: route.evidenceId ?? null
};
const routeEvidenceContract = {
route: route.route ?? null,
expectedRoute: route.expectedRoute ?? HWLAB_M3_IO_API_ROUTE,
method: route.method ?? null,
operationId: ids.operationId,
traceId: ids.traceId,
auditId: ids.auditId,
evidenceId: ids.evidenceId,
accepted: route.accepted ?? null,
status: route.toolCallStatus ?? null,
controlReady: route.controlReady ?? null,
readback: route.readback ?? null,
visible: route.route === HWLAB_M3_IO_API_ROUTE &&
route.method === "POST" &&
Boolean(ids.operationId && ids.traceId && ids.auditId && ids.evidenceId)
};
return {
reportClass: "live-mutating",
allowedLiveMutationUsed: report.liveMutationUsed === true,
@@ -1307,10 +1329,15 @@ function buildCommanderSummary(report) {
status: report.dimensions.skillCliM3Route?.status ?? "not_run",
route: route.route ?? null,
expectedRoute: route.expectedRoute ?? HWLAB_M3_IO_API_ROUTE,
method: route.method ?? null,
structuredBlocker: route.structuredBlocker === true,
idsPresent: route.idsPresent === true,
accepted: route.accepted ?? null,
controlReady: route.controlReady ?? null,
readback: route.readback ?? null,
safety: route.safety ?? null
},
exactRouteEvidenceContract: routeEvidenceContract,
evidenceIds: Object.fromEntries(Object.entries(ids).filter(([, value]) => Boolean(value))),
blockers: report.blockers.map((blocker) => ({
scope: blocker.scope,
@@ -1383,6 +1410,10 @@ export function renderMarkdown(report) {
`- liveMutationUsed: ${report.liveMutationUsed === true}`,
`- allowM3Write: ${report.runPolicy?.allowM3Write === true}`,
`- skillCliRoute: ${report.commanderSummary?.m3RouteResult?.route ?? "not_run"}`,
`- skillCliMethod: ${report.commanderSummary?.m3RouteResult?.method ?? "not_run"}`,
`- operationId: ${report.commanderSummary?.evidenceIds?.operationId ?? "none"}`,
`- traceId: ${report.commanderSummary?.evidenceIds?.traceId ?? "none"}`,
`- auditId: ${report.commanderSummary?.evidenceIds?.auditId ?? "none"}`,
`- evidenceId: ${report.commanderSummary?.evidenceIds?.evidenceId ?? "none"}`,
"",
"| Dimension | Status | Summary |",
+47 -1
View File
@@ -291,7 +291,11 @@ test("Skill CLI M3 route passes with operation/audit/evidence ids and no direct
assert.equal(result.status, "pass");
assert.equal(result.observations.route, "/v1/m3/io");
assert.equal(result.observations.method, "POST");
assert.equal(result.observations.idsPresent, true);
assert.equal(result.observations.accepted, true);
assert.equal(result.observations.controlReady, true);
assert.equal(result.observations.readback.value, true);
assert.equal(result.observations.safety.directGatewayCalls, false);
assert.equal(result.observations.safety.fallbackUsed, false);
});
@@ -327,6 +331,14 @@ test("Skill CLI M3 route blocks missing route and direct gateway evidence", () =
assert.equal(result.observations.safety.directGatewayCalls, true);
});
test("Skill CLI M3 route requires explicit POST method evidence", () => {
const result = classifySkillCliM3Route(skillCliPayload({ method: null }), { httpStatus: 200, httpOk: true });
assert.equal(result.status, "blocked");
assert.match(result.summary, /method=POST/u);
assert.ok(result.observations.missing.includes("method=POST"));
});
test("RPT-004 live report defaults to read-only M3 route checks and records no live mutation", async () => {
const report = await buildRpt004Report({
...parseArgs([
@@ -387,6 +399,20 @@ test("RPT-004 live report defaults to read-only M3 route checks and records no l
assert.equal(report.commanderSummary.sessionResult.status, "pass");
assert.equal(report.commanderSummary.skillCliResult.status, "pass");
assert.equal(report.commanderSummary.m3RouteResult.route, "/v1/m3/io");
assert.equal(report.commanderSummary.m3RouteResult.method, "POST");
assert.equal(report.commanderSummary.m3RouteResult.accepted, true);
assert.equal(report.commanderSummary.m3RouteResult.controlReady, true);
assert.equal(report.commanderSummary.m3RouteResult.readback.value, true);
assert.equal(report.commanderSummary.exactRouteEvidenceContract.visible, true);
assert.equal(report.commanderSummary.exactRouteEvidenceContract.route, "/v1/m3/io");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.method, "POST");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.operationId, "op_rpt004_skill_m3");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.traceId, "trc_rpt004_skill");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.auditId, "aud_rpt004_skill_m3");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.evidenceId, "evd_rpt004_skill_m3");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.accepted, true);
assert.equal(report.commanderSummary.exactRouteEvidenceContract.status, "completed");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.readback.value, true);
assert.equal(report.commanderSummary.evidenceIds.evidenceId, "evd_rpt004_skill_m3");
assert.equal(report.dimensions.skillCliM3Route.status, "pass");
assert.equal(report.dimensions.m3TrustedLoop.observations.skippedWriteLoop, true);
@@ -597,6 +623,7 @@ function openAiFallbackPayload({ reply = "text fallback" } = {}) {
function skillCliPayload({
route = "/v1/m3/io",
method = "POST",
accepted = true,
status = "succeeded",
operationId = "op_rpt004_skill_m3",
@@ -632,6 +659,7 @@ function skillCliPayload({
runnerTrace: {
traceId: "trc_rpt004_skill",
runnerKind: "hwlab-m3-io-skill-cli",
method,
route,
fallbackUsed: false,
directGatewayCalls,
@@ -641,22 +669,34 @@ function skillCliPayload({
providerTrace: {
runnerKind: "hwlab-m3-io-skill-cli",
route,
method,
traceId: "trc_rpt004_skill",
operationId,
auditId,
evidenceId,
accepted,
controlReady: accepted,
fallbackUsed: false
fallbackUsed: false,
readback: {
status: "succeeded",
value: true,
resourceId: "res_boxsimu_2",
port: "DI1"
}
},
toolCalls: [{
name: "hwlab-agent-runtime.m3-io",
status: accepted ? "completed" : "blocked",
type: "skill-cli",
route,
method,
accepted,
controlReady: accepted,
capabilityLevel: accepted ? "hwlab-api-control-ready" : "hwlab-api-control-blocked",
operationId,
traceId: "trc_rpt004_skill",
auditId,
evidenceId,
audit: {
auditId,
status
@@ -672,6 +712,12 @@ function skillCliPayload({
status: "degraded",
blocker: blocker?.code ?? "runtime_durable_not_green"
},
readback: {
status: "succeeded",
value: true,
resourceId: "res_boxsimu_2",
port: "DI1"
},
blocker,
blockers: blocker ? [blocker] : [{
code: "runtime_durable_not_green",
@@ -84,14 +84,18 @@ test("M3 Skill CLI posts only to HWLAB API /v1/m3/io for DO1 writes", async () =
assert.equal(result.ok, true);
assert.equal(result.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(result.method, "POST");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
assert.equal(result.controlReady, true);
assert.equal(result.readiness.status, "ready");
assert.equal(result.accepted, true);
assert.equal(result.status, "succeeded");
assert.equal(result.operationId, "op_m3_do_write_cli");
assert.equal(result.auditId, "aud_m3_do_write_cli_succeeded");
assert.equal(result.evidenceId, "evd_m3_do_write_cli_succeeded");
assert.equal(result.audit.auditId, "aud_m3_do_write_cli_succeeded");
assert.equal(result.evidence.evidenceId, "evd_m3_do_write_cli_succeeded");
assert.equal(result.readback.value, false);
assert.equal(result.durable.blocker, "runtime_durable_not_green");
assert.equal(result.trustBlocker.code, "runtime_durable_not_green");
assert.equal(result.blockers.some((blocker) => blocker.code === "runtime_durable_not_green"), true);
@@ -128,6 +132,7 @@ test("M3 Skill CLI rejects direct gateway, box, and patch-panel targets before r
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.method, "POST");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(result.controlReady, false);
assert.equal(result.readiness.status, "blocked");
@@ -169,6 +174,8 @@ test("M3 Skill CLI reports missing API base as safe structured config blocker",
);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.method, "POST");
assert.equal(result.accepted, false);
assert.equal(result.blocker.code, "skill_cli_api_base_missing");
assert.equal(result.blocker.layer, "skill-cli-config");
assert.equal(result.blocker.retryable, false);
@@ -376,6 +376,7 @@ function normalizeSkillResponse({ response, command, payload, apiTarget, traceId
service: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route: HWLAB_M3_IO_API_ROUTE,
method: "POST",
hwlabApi: publicCloudApiTarget(apiTarget),
capabilityLevel,
controlReady,
@@ -386,6 +387,8 @@ function normalizeSkillResponse({ response, command, payload, apiTarget, traceId
requestId,
actorId,
operationId: body.operationId ?? null,
auditId: body.auditId ?? body.auditState?.auditId ?? null,
evidenceId: body.evidenceId ?? body.evidenceState?.evidenceId ?? null,
audit: {
auditId: body.auditId ?? body.auditState?.auditId ?? null,
status: body.auditState?.status ?? null,
@@ -418,6 +421,7 @@ function normalizeSkillResponse({ response, command, payload, apiTarget, traceId
value: body.result?.value ?? body.value ?? null,
targetReadback: body.result?.targetReadback ?? null
},
readback: normalizeReadback(body, command),
controlPath: body.controlPath ?? {
cloudApi: true,
gatewaySimu: false,
@@ -472,6 +476,7 @@ function blockedPayload({ traceId, requestId, apiTarget, code, message, missingC
service: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route: HWLAB_M3_IO_API_ROUTE,
method: "POST",
hwlabApi: publicCloudApiTarget(apiTarget),
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
controlReady: false,
@@ -480,6 +485,8 @@ function blockedPayload({ traceId, requestId, apiTarget, code, message, missingC
traceId,
requestId,
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written",
@@ -683,6 +690,21 @@ function evidenceSummary(body) {
return body.evidenceState?.reason ?? "evidence not written";
}
function normalizeReadback(body, command) {
if (command.action === "do.write") {
return body.result?.targetReadback ?? null;
}
if (command.action === "di.read") {
return {
status: body.status ?? null,
value: body.result?.value ?? body.value ?? null,
resourceId: command.resourceId,
port: command.port
};
}
return null;
}
function helpPayload() {
return {
ok: true,
+6 -2
View File
@@ -163,10 +163,13 @@ export function hwlabApiFactsFromMessage(message) {
facts.push({
tool: firstText(toolCall.name, "skill-cli"),
route,
method: firstText(toolCall.method, parsed?.method, "POST"),
operationId: nullableText(toolCall.operationId ?? parsed?.operationId),
traceId: nullableText(toolCall.traceId ?? parsed?.traceId ?? message?.traceId),
auditId: nullableText(toolCall.audit?.auditId ?? parsed?.audit?.auditId ?? parsed?.auditId),
evidenceId: nullableText(toolCall.evidence?.evidenceId ?? parsed?.evidence?.evidenceId ?? parsed?.evidenceId),
auditId: nullableText(toolCall.auditId ?? toolCall.audit?.auditId ?? parsed?.auditId ?? parsed?.audit?.auditId),
evidenceId: nullableText(toolCall.evidenceId ?? toolCall.evidence?.evidenceId ?? parsed?.evidenceId ?? parsed?.evidence?.evidenceId),
accepted: toolCall.accepted ?? parsed?.accepted ?? null,
readback: toolCall.readback ?? parsed?.readback ?? parsed?.result?.targetReadback ?? null,
blocker: nullableText(blocker),
status: firstText(toolCall.status, parsed?.status, "unknown")
});
@@ -178,6 +181,7 @@ export function compactHwlabApiFact(fact) {
if (!fact) return "";
return compactFields([
field("route", fact.route),
field("method", fact.method),
field("operationId", fact.operationId),
field("traceId", fact.traceId),
field("auditId", fact.auditId),
+29 -1
View File
@@ -205,18 +205,38 @@ test("HWLAB Skill CLI toolCall shows compact copyable /v1/m3/io facts", () => {
type: "skill-cli",
status: "completed",
route: "/v1/m3/io",
method: "POST",
operationId: "op_m3_do_write_cli",
traceId: "trc_m3_skill_cli",
auditId: "aud_m3_do_write_cli_succeeded",
evidenceId: "evd_m3_do_write_cli_succeeded",
audit: { auditId: "aud_m3_do_write_cli_succeeded" },
evidence: { evidenceId: "evd_m3_do_write_cli_succeeded" },
accepted: true,
readback: {
status: "succeeded",
value: false,
resourceId: "res_boxsimu_2",
port: "DI1"
},
blocker: { code: "runtime_durable_not_green" },
stdout: JSON.stringify({
route: "/v1/m3/io",
method: "POST",
status: "succeeded",
accepted: true,
traceId: "trc_m3_skill_cli",
operationId: "op_m3_do_write_cli",
auditId: "aud_m3_do_write_cli_succeeded",
evidenceId: "evd_m3_do_write_cli_succeeded",
audit: { auditId: "aud_m3_do_write_cli_succeeded" },
evidence: { evidenceId: "evd_m3_do_write_cli_succeeded" },
readback: {
status: "succeeded",
value: false,
resourceId: "res_boxsimu_2",
port: "DI1"
},
durable: { blocker: "runtime_durable_not_green" }
})
}
@@ -242,15 +262,23 @@ test("HWLAB Skill CLI toolCall shows compact copyable /v1/m3/io facts", () => {
assert.deepEqual(facts.hwlabApiFacts[0], {
tool: "hwlab-agent-runtime.m3-io",
route: "/v1/m3/io",
method: "POST",
operationId: "op_m3_do_write_cli",
traceId: "trc_m3_skill_cli",
auditId: "aud_m3_do_write_cli_succeeded",
evidenceId: "evd_m3_do_write_cli_succeeded",
accepted: true,
readback: {
status: "succeeded",
value: false,
resourceId: "res_boxsimu_2",
port: "DI1"
},
blocker: "runtime_durable_not_green",
status: "completed"
});
assert.equal(
compactHwlabApiFact(facts.hwlabApiFacts[0]),
"route=/v1/m3/io / operationId=op_m3_do_write_cli / traceId=trc_m3_skill_cli / auditId=aud_m3_do_write_cli_succeeded / evidenceId=evd_m3_do_write_cli_succeeded / blocker=runtime_durable_not_green"
"route=/v1/m3/io / method=POST / operationId=op_m3_do_write_cli / traceId=trc_m3_skill_cli / auditId=aud_m3_do_write_cli_succeeded / evidenceId=evd_m3_do_write_cli_succeeded / blocker=runtime_durable_not_green"
);
});