feat: expose M3 Code Agent route evidence
This commit is contained in:
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 |",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user