test: guard M3 IO Code Agent contract

This commit is contained in:
unidesk-code-queue-runner
2026-05-23 16:09:15 +00:00
parent d7231a0c54
commit 9974c259e3
7 changed files with 286 additions and 21 deletions
+81 -6
View File
@@ -64,6 +64,7 @@ const M3_IO_SKILL_LIMITATION_FLAGS = Object.freeze([
]);
const OPENAI_FALLBACK_RUNNER_KIND = "openai-responses-fallback";
const CODEX_CLI_ONE_SHOT_RUNNER_KIND = "codex-cli-one-shot-ephemeral";
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;
const READONLY_FILE_READ_LIMIT = 64 * 1024;
const READONLY_FILE_ENTRY_LIMIT = 200;
@@ -237,7 +238,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
runner: providerRunner,
capabilityLevel: providerResult.capabilityLevel ?? "text-chat-only"
});
return {
return finalizeCodeAgentChatPayload({
...base,
status: "completed",
updatedAt: completedAt,
@@ -281,7 +282,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
usage: providerResult.usage ?? null,
providerTrace: providerResult.providerTrace ?? null,
...(providerCompletionBlocker ? { blocker: providerCompletionBlocker, blockers: [providerCompletionBlocker] } : {})
};
});
} catch (error) {
const failedAt = nowIso(options.now);
const payload = {
@@ -338,7 +339,7 @@ function completedRunnerPayload({ base, runnerResult, messageId, now }) {
runner: runnerResult.runner,
capabilityLevel: runnerResult.capabilityLevel
});
return {
return finalizeCodeAgentChatPayload({
...base,
sessionId: runnerResult.session?.sessionId ?? base.sessionId,
status: "completed",
@@ -369,7 +370,7 @@ function completedRunnerPayload({ base, runnerResult, messageId, now }) {
...(blocker ? { blocker, blockers: [blocker, ...(runnerResult.blockers ?? [])].filter(Boolean) } : {}),
usage: null,
providerTrace: runnerResult.providerTrace
};
});
}
export function validateCodeAgentChatSchema(payload) {
@@ -402,6 +403,56 @@ export function validateCodeAgentChatSchema(payload) {
if (payload.status === "completed" && (typeof payload.reply?.content !== "string" || !payload.reply.content.trim())) {
throw new Error("completed code agent chat response must include non-empty reply.content");
}
validateCodeAgentToolCallContract(payload);
}
function validateCodeAgentToolCallContract(payload) {
if (!Array.isArray(payload.toolCalls)) return;
for (const [index, toolCall] of payload.toolCalls.entries()) {
if (!toolCall || typeof toolCall !== "object") {
throw new Error(`code agent toolCalls[${index}] must be an object`);
}
const directTarget = findDirectHardwareTarget(toolCall);
if (directTarget) {
throw new Error(`code agent toolCalls[${index}] must not include direct gateway/box/patch-panel target ${directTarget}`);
}
if (!isM3IoToolCall(toolCall, payload)) continue;
const structuredBlocked = toolCall.status === "blocked" && Boolean(toolCall.blocker?.code || toolCall.capabilityBlocker?.code || toolCall.error?.code);
if (toolCall.route !== HWLAB_M3_IO_API_ROUTE && !structuredBlocked) {
throw new Error(`code agent M3 toolCalls[${index}] must show route ${HWLAB_M3_IO_API_ROUTE} or structured blocked`);
}
if (toolCall.route === HWLAB_M3_IO_API_ROUTE && (toolCall.method ?? "POST") !== "POST") {
throw new Error(`code agent M3 toolCalls[${index}] must use POST ${HWLAB_M3_IO_API_ROUTE}`);
}
}
}
function isM3IoToolCall(toolCall, payload) {
return toolCall.name === HWLAB_M3_IO_SKILL_NAME ||
toolCall.route === HWLAB_M3_IO_API_ROUTE ||
payload.provider === M3_IO_SKILL_PROVIDER ||
payload.runner?.kind === M3_IO_SKILL_RUNNER_KIND ||
payload.runnerTrace?.runnerKind === M3_IO_SKILL_RUNNER_KIND;
}
function findDirectHardwareTarget(value, seen = new Set()) {
if (typeof value === "string") {
const match = value.match(DIRECT_HARDWARE_TARGET_PATTERN);
return match?.[0] ?? null;
}
if (!value || typeof value !== "object") return null;
if (seen.has(value)) return null;
seen.add(value);
for (const child of Object.values(value)) {
const match = findDirectHardwareTarget(child, seen);
if (match) return match;
}
return null;
}
function finalizeCodeAgentChatPayload(payload) {
validateCodeAgentChatSchema(payload);
return payload;
}
export function describeCodeAgentAvailability(env = process.env, options = {}) {
@@ -1247,7 +1298,7 @@ function m3IoSkillRunnerResult({
name: HWLAB_M3_IO_SKILL_NAME,
status: skillResult.status === "succeeded" || skillResult.accepted === true ? "completed" : "blocked",
cwd: resolvedWorkspace,
command: redactText(`node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs ${commandArgs.join(" ")}`),
command: redactText(`node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs ${redactM3IoCommandArgs(commandArgs).join(" ")}`),
exitCode: skillResult.ok ? 0 : 2,
stdout: boundToolOutput(JSON.stringify({
route: skillResult.route,
@@ -1270,7 +1321,7 @@ function m3IoSkillRunnerResult({
outputTruncated: false,
route: HWLAB_M3_IO_API_ROUTE,
method: skillResult.method ?? "POST",
hwlabApi: skillResult.hwlabApi,
hwlabApi: m3IoToolCallApiTarget(skillResult.hwlabApi),
capabilityLevel,
controlReady: skillResult.controlReady === true,
accepted: skillResult.accepted,
@@ -1371,6 +1422,30 @@ function m3IoSkillRunnerResult({
};
}
function redactM3IoCommandArgs(commandArgs = []) {
const redacted = [];
for (let index = 0; index < commandArgs.length; index += 1) {
redacted.push(commandArgs[index]);
if (commandArgs[index] === "--api-base-url" && index + 1 < commandArgs.length) {
redacted.push("<hwlab-api-base-url>");
index += 1;
}
}
return redacted;
}
function m3IoToolCallApiTarget(hwlabApi = {}) {
const target = {
...hwlabApi,
route: hwlabApi?.route ?? HWLAB_M3_IO_API_ROUTE
};
delete target.url;
if (findDirectHardwareTarget(target.redactedUrl)) {
target.redactedUrl = null;
}
return target;
}
function m3IoSkillMissingApiBaseUrlResult({ traceId, requestId, actorId, blocker, startedAt, finishedAt }) {
return {
ok: false,
+64 -12
View File
@@ -1774,9 +1774,21 @@ async function successControlResult(input) {
reason: null
});
const target = controlTarget(input.command, input.operation.gatewaySessionId);
const audit = auditState(records.auditWrite, records.runtimeAfter);
const evidence = evidenceState(records.runtimeAfter, records);
const readback = input.command.action === "di.read"
? {
status: "succeeded",
value: readValueFromGatewayResult(input.dispatch.body),
resourceId: input.command.resourceId,
port: input.command.port
}
: input.targetReadback ?? null;
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "succeeded",
accepted: true,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.ready,
@@ -1795,11 +1807,19 @@ async function successControlResult(input) {
traceId: input.meta.traceId,
auditId: records.auditEvent.auditId,
evidenceId: records.evidenceRecord.evidenceId,
audit: {
auditId: records.auditEvent.auditId,
...audit
},
evidence: {
evidenceId: records.evidenceRecord.evidenceId,
...evidence
},
operationState: {
status: "succeeded",
reachable: true
},
auditState: auditState(records.auditWrite, records.runtimeAfter),
auditState: audit,
operation: {
...input.operation,
status: "succeeded",
@@ -1814,6 +1834,7 @@ async function successControlResult(input) {
patchPanel: input.patchPanel?.body ?? null,
targetReadback: input.targetReadback ?? null
},
readback,
controlPath: {
status: "succeeded",
cloudApi: true,
@@ -1824,7 +1845,7 @@ async function successControlResult(input) {
},
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidenceState(records.runtimeAfter, records),
evidenceState: evidence,
durableStatus: durableStatus(records.runtimeAfter),
trustBlocker: trustBlocker(records.runtimeAfter),
blockerClassification: redactedBlockerClassification({
@@ -1844,9 +1865,16 @@ async function blockedControlResult(input) {
reason: input.reason
});
const target = controlTarget(input.command, input.operation.gatewaySessionId);
const audit = auditState(records.auditWrite, records.runtimeAfter);
const evidence = evidenceState(records.runtimeAfter, records);
const readback = input.command.action === "di.read"
? null
: input.targetReadback ?? null;
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
@@ -1865,6 +1893,14 @@ async function blockedControlResult(input) {
traceId: input.meta.traceId,
auditId: records.auditEvent.auditId,
evidenceId: records.evidenceRecord.evidenceId,
audit: {
auditId: records.auditEvent.auditId,
...audit
},
evidence: {
evidenceId: records.evidenceRecord.evidenceId,
...evidence
},
blocker: {
code: input.code,
message: input.reason,
@@ -1880,7 +1916,7 @@ async function blockedControlResult(input) {
reachable: false,
blocker: input.code
},
auditState: auditState(records.auditWrite, records.runtimeAfter),
auditState: audit,
operation: {
...input.operation,
status: "failed",
@@ -1896,6 +1932,7 @@ async function blockedControlResult(input) {
patchPanel: input.patchPanel?.body ?? null,
targetReadback: input.targetReadback ?? null
},
readback,
controlPath: {
status: "blocked",
cloudApi: true,
@@ -1906,7 +1943,7 @@ async function blockedControlResult(input) {
},
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidenceState(records.runtimeAfter, records),
evidenceState: evidence,
durableStatus: durableStatus(records.runtimeAfter),
trustBlocker: trustBlocker(records.runtimeAfter),
persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records),
@@ -1916,9 +1953,21 @@ async function blockedControlResult(input) {
function blockedResult({ action, command = null, meta, runtime, code, reason, httpStatus, now, details = {} }) {
const target = controlTarget(command, command?.gatewaySessionId);
const audit = {
auditId: null,
status: "not_written",
durableStatus: durableStatus(runtime),
reason: "operation was blocked before audit write"
};
const evidence = evidenceState(runtime, {
auditWrite: { written: false, reason: "operation was blocked before audit write" },
evidenceWrite: { written: false, reason: "operation was blocked before evidence write" }
});
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
@@ -1937,6 +1986,11 @@ function blockedResult({ action, command = null, meta, runtime, code, reason, ht
traceId: meta.traceId,
auditId: null,
evidenceId: null,
audit,
evidence: {
evidenceId: null,
...evidence
},
blocker: {
code,
message: reason,
@@ -1952,11 +2006,12 @@ function blockedResult({ action, command = null, meta, runtime, code, reason, ht
reachable: false,
blocker: code
},
auditState: {
status: "not_written",
durableStatus: durableStatus(runtime),
reason: "operation was blocked before audit write"
auditState: audit,
result: {
value: null,
targetReadback: null
},
readback: null,
controlPath: {
status: "blocked",
cloudApi: true,
@@ -1965,10 +2020,7 @@ function blockedResult({ action, command = null, meta, runtime, code, reason, ht
patchPanel: false,
frontendBypass: false
},
evidenceState: evidenceState(runtime, {
auditWrite: { written: false, reason: "operation was blocked before audit write" },
evidenceWrite: { written: false, reason: "operation was blocked before evidence write" }
}),
evidenceState: evidence,
durableStatus: durableStatus(runtime),
trustBlocker: trustBlocker(runtime),
persistence: {
+20
View File
@@ -318,6 +318,8 @@ test("M3 DO write dispatches through gateway-simu, ticks patch-panel, reads DI,
assert.equal(result.status, "succeeded");
assert.equal(result.accepted, true);
assert.equal(result.route, "/v1/m3/io");
assert.equal(result.method, "POST");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
assert.equal(result.controlReady, true);
assert.equal(result.action, "do.write");
@@ -341,6 +343,11 @@ test("M3 DO write dispatches through gateway-simu, ticks patch-panel, reads DI,
assert.equal(result.traceId, "trc_m3_control_write_true");
assert.match(result.auditId, /^aud_m3_do_write_/u);
assert.match(result.evidenceId, /^evd_m3_do_write_/u);
assert.equal(result.audit.auditId, result.auditId);
assert.equal(result.audit.status, "written_non_durable");
assert.equal(result.evidence.evidenceId, result.evidenceId);
assert.equal(result.evidence.status, "blocked");
assert.equal(result.readback.value, true);
assert.equal(result.operationState.status, "succeeded");
assert.equal(result.auditState.status, "written_non_durable");
assert.equal(result.auditState.durableStatus.durable, false);
@@ -468,6 +475,8 @@ test("M3 DI read returns structured value through gateway-simu without patch-pan
assert.equal(result.status, "succeeded");
assert.equal(result.accepted, true);
assert.equal(result.route, "/v1/m3/io");
assert.equal(result.method, "POST");
assert.equal(result.action, "di.read");
assert.equal(result.command.port, "DI1");
assert.equal(result.gatewayId, "gwsimu_2");
@@ -479,6 +488,10 @@ test("M3 DI read returns structured value through gateway-simu without patch-pan
assert.equal(result.operationState.status, "succeeded");
assert.equal(result.auditState.status, "written_non_durable");
assert.equal(result.result.value, true);
assert.equal(result.readback.status, "succeeded");
assert.equal(result.readback.value, true);
assert.equal(result.readback.resourceId, "res_boxsimu_2");
assert.equal(result.readback.port, "DI1");
assert.equal(result.controlPath.patchPanel, false);
assert.deepEqual(fixture.calls.map((call) => call.path), ["/status", "/invoke"]);
});
@@ -514,10 +527,17 @@ test("M3 IO control blocks with Chinese reason when gateway session is unavailab
assert.equal(result.status, "blocked");
assert.equal(result.accepted, false);
assert.equal(result.route, "/v1/m3/io");
assert.equal(result.method, "POST");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(result.controlReady, false);
assert.equal(result.blocker.code, M3_IO_BLOCKER_CODES.gatewayUnavailable);
assert.equal(result.blockerClassification.category, "gateway_session");
assert.equal(result.audit.auditId, null);
assert.equal(result.audit.status, "not_written");
assert.equal(result.evidence.evidenceId, null);
assert.equal(result.evidence.status, "blocked");
assert.equal(result.readback, null);
assert.equal(result.auditState.status, "not_written");
assert.equal(result.durableStatus.redacted, true);
assert.match(result.blocker.zh, /gateway 未注册\/不可用/u);
+45
View File
@@ -509,9 +509,24 @@ async function handleM3IoControlHttp(request, response, options) {
sendJson(response, 400, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "m3-io-control-v1",
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED"
},
readback: null,
error: {
code: "parse_error",
message: "Invalid JSON body",
@@ -525,9 +540,24 @@ async function handleM3IoControlHttp(request, response, options) {
sendJson(response, 400, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "m3-io-control-v1",
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED"
},
readback: null,
error: {
code: "invalid_params",
message: "M3 IO control body must be a JSON object"
@@ -559,9 +589,24 @@ async function handleM3IoControlHttp(request, response, options) {
sendJson(response, protocolCode === ERROR_CODES.invalidParams ? 400 : 200, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "m3-io-control-v1",
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
traceId: getHeader(request, "x-trace-id") || params.traceId || "trc_unassigned",
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED"
},
readback: null,
error: {
code: protocolCode,
message: error.message,
+52
View File
@@ -7,6 +7,7 @@ import path from "node:path";
import test from "node:test";
import { createCloudApiServer } from "./server.mjs";
import { validateCodeAgentChatSchema } from "./code-agent-chat.mjs";
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
import {
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
@@ -1018,6 +1019,7 @@ test("cloud api /v1/agent/chat runs read-only runner pwd with workspace evidence
});
assert.equal(response.status, 200);
const payload = await response.json();
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "codex-readonly-runner");
assert.equal(payload.backend, "hwlab-cloud-api/codex-readonly-runner");
@@ -1137,6 +1139,7 @@ test("cloud api /v1/agent/chat routes M3 IO through Skill CLI to /v1/m3/io only"
});
assert.equal(response.status, 200);
const payload = await response.json();
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-skill-cli");
assert.equal(payload.backend, "hwlab-cloud-api/hwlab-agent-runtime-skill-cli");
@@ -1155,6 +1158,7 @@ test("cloud api /v1/agent/chat routes M3 IO through Skill CLI to /v1/m3/io only"
assert.equal(payload.runner.safety.directGatewayCallsAllowed, false);
assert.equal(payload.runner.safety.directBoxSimuCallsAllowed, false);
assert.equal(payload.runner.safety.directPatchPanelCallsAllowed, false);
assert.equal(/https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel)|:7101\b|:7201\b|:7301\b|\/invoke\b|\/sync\/tick\b/iu.test(JSON.stringify(payload.toolCalls)), false);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/m3/io");
assert.equal(calls[0].request.body.action, "do.write");
@@ -1169,6 +1173,49 @@ test("cloud api /v1/agent/chat routes M3 IO through Skill CLI to /v1/m3/io only"
}
});
test("cloud api /v1/agent/chat blocks direct M3 API base targets before request and without toolCall URL leakage", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-direct-target-"));
const calls = [];
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: workspace,
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101"
},
m3IoSkillRequestJson: async (url, request) => {
calls.push({ url, request });
throw new Error("direct gateway target must be blocked before requestJson");
}
});
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-direct-target",
traceId: "trc_server-test-m3-direct-target",
message: "读取 M3 DI1"
});
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-skill-cli");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(payload.toolCalls[0].name, "hwlab-agent-runtime.m3-io");
assert.equal(payload.toolCalls[0].status, "blocked");
assert.equal(payload.toolCalls[0].route, "/v1/m3/io");
assert.equal(payload.toolCalls[0].method, "POST");
assert.equal(payload.toolCalls[0].blocker.code, "direct_hardware_target_blocked");
assert.equal(payload.toolCalls[0].hwlabApi.redactedUrl, null);
assert.equal(payload.blocker.code, "m3_readiness_blocked");
assert.equal(calls.length, 0);
assert.equal(/https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel)|:7101\b|:7201\b|:7301\b|\/invoke\b|\/sync\/tick\b/iu.test(JSON.stringify(payload.toolCalls)), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat reports M3 Skill CLI missing API base as structured blocker", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-api-base-missing-"));
const server = createCloudApiServer({
@@ -2123,10 +2170,15 @@ test("cloud api /v1/m3/io returns structured blocked payload when gateway is una
const payload = await response.json();
assert.equal(payload.status, "blocked");
assert.equal(payload.accepted, false);
assert.equal(payload.route, "/v1/m3/io");
assert.equal(payload.method, "POST");
assert.equal(payload.traceId, "trc_http_m3_gateway_missing");
assert.equal(payload.operationId, null);
assert.equal(payload.auditId, null);
assert.equal(payload.evidenceId, null);
assert.equal(payload.audit.status, "not_written");
assert.equal(payload.evidence.status, "blocked");
assert.equal(payload.readback, null);
assert.match(payload.blocker.zh, /gateway 未注册\/不可用/u);
assert.equal(payload.controlPath.cloudApi, true);
assert.equal(payload.controlPath.frontendBypass, false);
+20
View File
@@ -435,6 +435,26 @@ async function runLocalContractSmoke() {
assert.equal(m3SkillCalls[0].url.includes("patch-panel"), false);
logOk("M3 Skill CLI routes through HWLAB API only");
const directToolCallPayload = JSON.parse(JSON.stringify(m3SkillWrite));
directToolCallPayload.toolCalls[0].hwlabApi = {
route: "/v1/m3/io",
redactedUrl: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101/v1/m3/io"
};
assert.throws(
() => validateCodeAgentChatSchema(directToolCallPayload),
/direct gateway\/box\/patch-panel target/u
);
const missingRouteToolCallPayload = JSON.parse(JSON.stringify(m3SkillWrite));
delete missingRouteToolCallPayload.toolCalls[0].route;
missingRouteToolCallPayload.toolCalls[0].blocker = null;
missingRouteToolCallPayload.toolCalls[0].status = "completed";
assert.throws(
() => validateCodeAgentChatSchema(missingRouteToolCallPayload),
/must show route \/v1\/m3\/io/u
);
logOk("M3 toolCall schema rejects direct hardware URLs and missing route evidence");
const completedHttp200Readiness = classifyCodeAgentChatReadiness(completedLivePayload, { realDevLive: true, httpStatus: 200 });
assert.equal(completedHttp200Readiness.status, "blocked");
assert.equal(completedHttp200Readiness.devLiveReplyPass, false);
+4 -3
View File
@@ -55,9 +55,10 @@ gateway/box/patch-panel targets. If no API base URL is configured, the CLI
returns `skill_cli_api_base_missing` and does not fall back to `127.0.0.1` or
any direct hardware service.
The JSON response includes `route`, `traceId`, `operationId`, `audit`,
`evidence`, `accepted`, `status`, `capabilityLevel`, `readiness`, `blocker`,
`trustBlocker`, and direct-call safety flags. `capabilityLevel` is
The JSON response includes `route`, `method`, `traceId`, `operationId`,
`audit`, `evidence`, `accepted`, `status`, `readback`, `capabilityLevel`,
`readiness`, `blocker`, `trustBlocker`, and direct-call safety flags.
`capabilityLevel` is
`hwlab-api-control-ready` only after the HWLAB API accepts the request through
`/v1/m3/io`; direct target rejection, gateway unavailability, missing wiring, or
other control blockers return `hwlab-api-control-blocked`. Durable runtime