feat: add code agent M3 skill control path

Adds the Code Agent -> Skill CLI -> HWLAB API /v1/m3/io controlled M3 IO path with route/audit/evidence/blocker payloads and tests proving no direct gateway/box/patch-panel or OpenAI fallback hardware control.
This commit is contained in:
Lyon
2026-05-23 20:52:40 +08:00
committed by GitHub
parent ff91f77427
commit ecddd9a630
10 changed files with 1577 additions and 7 deletions
+432 -5
View File
@@ -15,6 +15,12 @@ import {
DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS,
createCodeAgentSessionRegistry
} from "./code-agent-session-registry.mjs";
import {
HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
HWLAB_M3_IO_API_ROUTE,
HWLAB_M3_IO_SKILL_NAME,
runM3IoSkillCommand
} from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 120000;
const DEFAULT_CODEX_COMMAND = "codex";
@@ -28,6 +34,21 @@ const READONLY_RUNNER_SANDBOX = "read-only";
const READONLY_SESSION_MODE = "controlled-readonly-session-registry";
const READONLY_IMPLEMENTATION_TYPE = "controlled-readonly-session-registry";
const READONLY_SESSION_CAPABILITY_LEVEL = "read-only-session-tools";
const M3_IO_SKILL_PROVIDER = "hwlab-skill-cli";
const M3_IO_SKILL_BACKEND = "hwlab-cloud-api/hwlab-agent-runtime-skill-cli";
const M3_IO_SKILL_MODEL = "controlled-m3-io";
const M3_IO_SKILL_RUNNER_KIND = "hwlab-m3-io-skill-cli";
const M3_IO_SKILL_SANDBOX = "hwlab-api-route-only";
const M3_IO_SKILL_SESSION_MODE = "controlled-m3-io-skill-cli";
const M3_IO_SKILL_IMPLEMENTATION_TYPE = "skill-cli-hwlab-api-adapter";
const M3_IO_SKILL_CAPABILITY_LEVEL = "controlled-m3-io-skill-cli";
const M3_IO_SKILL_LIMITATION_FLAGS = Object.freeze([
"m3-io-only",
"hwlab-api-route-only",
"not-generic-hardware-control",
"not-codex-stdio",
"not-durable-session"
]);
const OPENAI_FALLBACK_RUNNER_KIND = "openai-responses-fallback";
const CODEX_CLI_ONE_SHOT_RUNNER_KIND = "codex-cli-one-shot-ephemeral";
const READONLY_TOOL_OUTPUT_LIMIT = 4000;
@@ -91,6 +112,51 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
try {
const message = normalizeUserMessage(params.message);
const runnerIntent = detectReadOnlyRunnerIntent(message);
if (runnerIntent.kind === "m3_io") {
const runnerResult = await callM3IoSkillRunner({
intent: runnerIntent,
conversationId,
traceId,
sessionId: requestedSessionId,
env: options.env ?? process.env,
now: options.now,
workspace: options.workspace,
sessionRegistry: options.sessionRegistry,
requestJson: options.m3IoSkillRequestJson
});
const completedAt = nowIso(options.now);
return {
...base,
sessionId: runnerResult.session?.sessionId ?? base.sessionId,
status: "completed",
updatedAt: completedAt,
provider: runnerResult.provider,
model: runnerResult.model,
backend: runnerResult.backend,
workspace: runnerResult.workspace,
sandbox: runnerResult.sandbox,
session: runnerResult.session,
sessionMode: runnerResult.sessionMode,
sessionReuse: runnerResult.sessionReuse,
implementationType: runnerResult.implementationType,
runnerLimitations: runnerResult.runnerLimitations,
codexStdioFeasibility: runnerResult.codexStdioFeasibility,
longLivedSessionGate: runnerResult.longLivedSessionGate,
toolCalls: runnerResult.toolCalls,
skills: runnerResult.skills,
runner: runnerResult.runner,
runnerTrace: runnerResult.runnerTrace,
capabilityLevel: runnerResult.capabilityLevel,
reply: {
messageId,
role: "assistant",
content: runnerResult.content,
createdAt: completedAt
},
usage: null,
providerTrace: runnerResult.providerTrace
};
}
if (runnerIntent.kind !== "none") {
const runnerResult = await callReadOnlyRunner({
intent: runnerIntent,
@@ -334,8 +400,8 @@ export function describeCodeAgentAvailability(env = process.env, options = {}) {
blocker: blocked && !runnerAvailability.ready ? (providerPlan.mode === "openai" ? providerContract.blocker : "凭证缺口") : null,
reason: blocked && !runnerAvailability.ready ? "provider_unavailable" : null,
summary: blocked
? `受控只读 runner 可用于 pwd/skills 等工作区能力;OpenAI Responses fallback 当前受 DEV provider Secret ${CODE_AGENT_PROVIDER_SECRET_REF} 或 DEV egress/base-url contract 影响。`
: "真实后端已接入;pwd/skills 走受控只读 runner,普通聊天可走 OpenAI Responses fallback。OpenAI fallback 不满足 Codex runner capability gate。",
? `受控只读 runner 可用于 pwd/skills 等工作区能力M3 IO 只允许 Skill CLI 调用 ${HWLAB_M3_IO_API_ROUTE}OpenAI Responses fallback 当前受 DEV provider Secret ${CODE_AGENT_PROVIDER_SECRET_REF} 或 DEV egress/base-url contract 影响。`
: `真实后端已接入;pwd/skills 走受控只读 runnerM3 IO 走 Skill CLI -> HWLAB API ${HWLAB_M3_IO_API_ROUTE}普通聊天可走 OpenAI Responses fallback。OpenAI fallback 不满足 Codex runner capability gate。`,
missingEnv,
secretRefs: blocked ? providerContract.secretRefs : [],
egress: providerContract.egress,
@@ -441,6 +507,17 @@ function inspectReadOnlyRunnerAvailability(env, options = {}) {
sessionRegistry,
skillsDirs,
skillsDirsPresent,
m3IoSkill: {
status: "available",
service: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route: HWLAB_M3_IO_API_ROUTE,
capabilityLevel: M3_IO_SKILL_CAPABILITY_LEVEL,
directGatewayCallsAllowed: false,
directBoxCallsAllowed: false,
directPatchPanelCallsAllowed: false,
fallbackAllowed: false
},
safety: runnerSafetyContract()
};
}
@@ -772,6 +849,172 @@ async function callReadOnlyRunner({ intent, conversationId, sessionId, traceId,
});
}
async function callM3IoSkillRunner({ intent, conversationId, sessionId, traceId, env, now, workspace, sessionRegistry, requestJson }) {
const resolvedWorkspace = resolveRunnerWorkspace(env, { workspace });
const registry = resolveCodeAgentSessionRegistry({ sessionRegistry });
const sessionAcquire = registry.acquire({
conversationId,
sessionId,
workspace: resolvedWorkspace,
sandbox: M3_IO_SKILL_SANDBOX,
runnerKind: M3_IO_SKILL_RUNNER_KIND,
capabilityLevel: M3_IO_SKILL_CAPABILITY_LEVEL,
implementationType: M3_IO_SKILL_IMPLEMENTATION_TYPE,
traceId,
now
});
const startedAt = nowIso(now);
const codexStdioFeasibility = inspectCodexStdioFeasibility(envForFeasibility(env));
if (!sessionAcquire.ok) {
const blockedSession = sessionAcquire.session;
throw runnerError(sessionAcquire.code, sessionAcquire.message, {
provider: M3_IO_SKILL_PROVIDER,
model: M3_IO_SKILL_MODEL,
backend: M3_IO_SKILL_BACKEND,
workspace: resolvedWorkspace ?? null,
sandbox: M3_IO_SKILL_SANDBOX,
session: blockedSession,
toolCalls: [],
skills: notRequestedSkills(),
runner: m3IoSkillRunnerDescriptor({ workspace: resolvedWorkspace, session: blockedSession }),
runnerTrace: m3IoSkillRunnerTrace({
traceId,
workspace: resolvedWorkspace ?? repoRoot,
session: blockedSession,
events: [`blocked:${sessionAcquire.code}`],
startedAt,
outputTruncated: false
}),
capabilityLevel: "blocked",
sessionMode: M3_IO_SKILL_SESSION_MODE,
sessionReuse: sessionReuseEvidence(blockedSession),
implementationType: M3_IO_SKILL_IMPLEMENTATION_TYPE,
runnerLimitations: [...M3_IO_SKILL_LIMITATION_FLAGS],
codexStdioFeasibility,
longLivedSessionGate: longLivedSessionGate({
provider: M3_IO_SKILL_PROVIDER,
runnerKind: M3_IO_SKILL_RUNNER_KIND,
session: blockedSession,
sessionMode: M3_IO_SKILL_SESSION_MODE,
implementationType: M3_IO_SKILL_IMPLEMENTATION_TYPE,
codexStdioFeasibility
}),
blockers: [sessionAcquire.blocker]
});
}
let session = sessionAcquire.session;
const commandArgs = m3IoSkillArgsForIntent(intent, { env, traceId });
const skillResult = await runM3IoSkillCommand(commandArgs, {
env,
now,
requestJson
});
const finishedAt = nowIso(now);
const toolCall = {
id: `tool_${randomUUID()}`,
type: "skill-cli",
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(" ")}`),
exitCode: skillResult.ok ? 0 : 2,
stdout: boundToolOutput(JSON.stringify({
route: skillResult.route,
status: skillResult.status,
accepted: skillResult.accepted,
traceId: skillResult.traceId,
operationId: skillResult.operationId,
audit: skillResult.audit,
evidence: skillResult.evidence,
durable: skillResult.durable,
blocker: skillResult.blocker,
result: skillResult.result,
safety: skillResult.safety
})).text,
stderrSummary: skillResult.blocker?.code ?? "",
outputTruncated: false,
traceId,
route: HWLAB_M3_IO_API_ROUTE,
hwlabApi: skillResult.hwlabApi,
accepted: skillResult.accepted,
operationId: skillResult.operationId,
audit: skillResult.audit,
evidence: skillResult.evidence,
blocker: skillResult.blocker,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false
};
session = releaseReadOnlySession(registry, session, { now, traceId, conversationId });
const runner = m3IoSkillRunnerDescriptor({ workspace: resolvedWorkspace, session });
const runnerTracePayload = m3IoSkillRunnerTrace({
traceId,
workspace: resolvedWorkspace,
session,
events: [
"intent:m3_io",
"tool:skill-cli:started",
`route:${HWLAB_M3_IO_API_ROUTE}`,
`tool:${toolCall.status}`
],
startedAt,
finishedAt,
outputTruncated: toolCall.outputTruncated,
skillResult
});
return {
provider: M3_IO_SKILL_PROVIDER,
model: M3_IO_SKILL_MODEL,
backend: M3_IO_SKILL_BACKEND,
content: m3IoSkillReply(skillResult),
workspace: resolvedWorkspace,
sandbox: M3_IO_SKILL_SANDBOX,
session,
sessionMode: M3_IO_SKILL_SESSION_MODE,
sessionReuse: sessionReuseEvidence(session),
implementationType: M3_IO_SKILL_IMPLEMENTATION_TYPE,
runnerLimitations: [...M3_IO_SKILL_LIMITATION_FLAGS],
codexStdioFeasibility,
longLivedSessionGate: longLivedSessionGate({
provider: M3_IO_SKILL_PROVIDER,
runnerKind: M3_IO_SKILL_RUNNER_KIND,
session,
sessionMode: M3_IO_SKILL_SESSION_MODE,
implementationType: M3_IO_SKILL_IMPLEMENTATION_TYPE,
codexStdioFeasibility
}),
toolCalls: [toolCall],
skills: {
status: "used",
items: [{
name: HWLAB_M3_IO_SKILL_NAME,
summary: `Controlled M3 DO1/DI1 adapter for HWLAB API ${HWLAB_M3_IO_API_ROUTE}.`,
route: HWLAB_M3_IO_API_ROUTE,
version: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION
}],
count: 1,
blockers: skillResult.blocker ? [skillResult.blocker] : []
},
runner,
runnerTrace: runnerTracePayload,
capabilityLevel: M3_IO_SKILL_CAPABILITY_LEVEL,
providerTrace: {
runnerKind: M3_IO_SKILL_RUNNER_KIND,
skill: HWLAB_M3_IO_SKILL_NAME,
route: HWLAB_M3_IO_API_ROUTE,
traceId: skillResult.traceId,
operationId: skillResult.operationId,
accepted: skillResult.accepted,
status: skillResult.status,
fallbackUsed: false
}
};
}
function detectReadOnlyRunnerIntent(message) {
const text = String(message ?? "").trim();
const lower = text.toLowerCase();
@@ -783,11 +1026,15 @@ function detectReadOnlyRunnerIntent(message) {
reason: "只读 runner 不读取或输出 secret、token、kubeconfig、密码、私钥或环境变量原文。"
};
}
const m3IoIntent = detectM3IoIntent(text);
if (m3IoIntent) {
return m3IoIntent;
}
if (isHardwareWriteOrAcceptanceRequest(text)) {
return {
kind: "security",
toolName: "security.hardware-boundary",
reason: "只读 runner 不直接调用 gateway/box-simu/patch-panel、硬件写接口或宣称 M3/M4/M5 验收通过。"
reason: `只读 runner 不直接调用 gateway/box-simu/patch-panel、泛化硬件写接口或宣称 M3/M4/M5 验收通过M3 DO1/DI1 只能通过 Skill CLI -> HWLAB API ${HWLAB_M3_IO_API_ROUTE}`
};
}
if (/\bpwd\b/u.test(lower) || /当前.*(?:工作目录|目录)|工作目录|当前路径|workspace path|工作区路径/iu.test(text)) {
@@ -816,6 +1063,54 @@ function detectReadOnlyRunnerIntent(message) {
return { kind: "none" };
}
function detectM3IoIntent(text) {
const normalized = String(text ?? "");
if (!/\bM3\b|DO1|DI1|数字输出|数字输入|回读|读回/u.test(normalized)) {
return null;
}
if (/(?:直接|direct|gateway-simu|box-simu|patch-panel|hwlab-patch-panel|\/invoke|\/sync\/tick)/iu.test(normalized)) {
return null;
}
if (/DEV-LIVE|验收|acceptance|(?:M3|M4|M5).{0,20}(?:通过|pass|green|accept)/iu.test(normalized)) {
return null;
}
const wantsRead = /(?:DI1|数字输入).{0,16}(?:read|readback|读取|回读|读回|查询)|(?:read|readback|读取|回读|读回|查询).{0,16}(?:DI1|数字输入)/iu.test(normalized);
const wantsWrite = /(?:DO1|数字输出).{0,20}(?:write|set|置|写|打开|关闭|true|false|高|低)|(?:write|set|置|写|打开|关闭).{0,20}(?:DO1|数字输出)/iu.test(normalized);
if (!wantsRead && !wantsWrite) {
return null;
}
if (wantsRead && !wantsWrite) {
return {
kind: "m3_io",
toolName: HWLAB_M3_IO_SKILL_NAME,
action: "di.read"
};
}
const value = parseM3WriteValue(normalized);
if (typeof value !== "boolean") {
return {
kind: "unsupported",
toolName: HWLAB_M3_IO_SKILL_NAME,
reason: "M3 DO1 write requires an explicit true/false value."
};
}
return {
kind: "m3_io",
toolName: HWLAB_M3_IO_SKILL_NAME,
action: "do.write",
value
};
}
function parseM3WriteValue(text) {
if (/\btrue\b|\b1\b|\bon\b|打开|置高|高电平|拉高|闭合/iu.test(text)) return true;
if (/\bfalse\b|\b0\b|\boff\b|关闭|置低|低电平|拉低|断开/iu.test(text)) return false;
return null;
}
function extractReadOnlyTarget(text, { defaultTarget = null, preferFile = false } = {}) {
const value = String(text ?? "");
const quoted = value.match(/[`"']([^`"']{1,240})[`"']/u)?.[1];
@@ -845,7 +1140,7 @@ function isSecretReadRequest(text) {
}
function isHardwareWriteOrAcceptanceRequest(text) {
const hardwareTarget = /(?:hardware\.operation\.request|hardware\.invoke\.shell|audit\.event\.write|evidence\.record\.write|gateway-simu|box-simu|patch-panel|hwlab-patch-panel|硬件写|直接调用)/iu.test(text);
const hardwareTarget = /(?:hardware\.operation\.request|hardware\.invoke\.shell|audit\.event\.write|evidence\.record\.write|gateway-simu|box-simu|patch-panel|hwlab-patch-panel|硬件写|直接调用|\/invoke|\/sync\/tick)/iu.test(text);
const mutationVerb = /(?:call|invoke|write|mutate|apply|rollout|accept|pass|验收|通过|写入|调用|变更|操作)/iu.test(text);
const acceptanceClaim = /(?:M3|M4|M5).{0,20}(?:pass|accept|green|通过|验收|完成)/iu.test(text);
return (hardwareTarget && mutationVerb) || acceptanceClaim;
@@ -1378,6 +1673,133 @@ function readOnlyRunnerResult({ content, workspace, toolCalls, skills, runner, r
};
}
function m3IoSkillArgsForIntent(intent, { env, traceId }) {
const args = [
"m3",
"io",
"--action",
intent.action,
"--trace-id",
traceId,
"--actor-id",
"usr_code_agent"
];
const apiBaseUrl = firstNonEmpty(
env.HWLAB_CODE_AGENT_HWLAB_API_BASE_URL,
env.HWLAB_API_BASE_URL,
env.HWLAB_CLOUD_API_BASE_URL,
null
);
if (apiBaseUrl) {
args.push("--api-base-url", apiBaseUrl);
}
if (intent.action === "do.write") {
args.push("--value", String(intent.value));
}
return args;
}
function m3IoSkillReply(skillResult) {
const lines = [
`M3 IO Skill CLI result: status=${skillResult.status}; accepted=${skillResult.accepted}; route=${skillResult.route}; traceId=${skillResult.traceId}; operationId=${skillResult.operationId ?? "null"}.`
];
if (skillResult.audit?.summary) {
lines.push(`Audit: ${skillResult.audit.summary}.`);
}
if (skillResult.evidence?.summary) {
lines.push(`Evidence: ${skillResult.evidence.summary}.`);
}
if (skillResult.blocker) {
lines.push(`Blocker: ${skillResult.blocker.code}; ${skillResult.blocker.zh ?? skillResult.blocker.message}.`);
}
if (skillResult.action === "do.write") {
lines.push(`DO1 requested value=${String(skillResult.command?.value)}; DI1 readback=${String(skillResult.result?.targetReadback?.value ?? "unknown")}.`);
} else if (skillResult.action === "di.read") {
lines.push(`DI1 value=${String(skillResult.result?.value ?? "unknown")}.`);
}
lines.push("Boundary: Code Agent -> Skill CLI -> HWLAB API /v1/m3/io; no direct gateway/box/patch-panel call; no OpenAI fallback used. This is source/artifact control capability, not DEV-LIVE verification.");
return boundToolOutput(lines.join("\n"), READONLY_TOOL_OUTPUT_LIMIT).text;
}
function m3IoSkillRunnerDescriptor({ workspace, session = null } = {}) {
return {
kind: M3_IO_SKILL_RUNNER_KIND,
provider: M3_IO_SKILL_PROVIDER,
backend: M3_IO_SKILL_BACKEND,
workspace: workspace ?? repoRoot,
sandbox: M3_IO_SKILL_SANDBOX,
session: M3_IO_SKILL_SESSION_MODE,
sessionMode: M3_IO_SKILL_SESSION_MODE,
sessionId: session?.sessionId ?? null,
turn: session?.turn ?? null,
sessionReused: session?.reused ?? false,
implementationType: M3_IO_SKILL_IMPLEMENTATION_TYPE,
codexStdio: false,
longLivedSession: false,
durableSession: false,
writeCapable: true,
readOnly: false,
capabilityLevel: M3_IO_SKILL_CAPABILITY_LEVEL,
skill: {
name: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route: HWLAB_M3_IO_API_ROUTE
},
toolPolicy: {
allowed: [`POST ${HWLAB_M3_IO_API_ROUTE}`],
blocked: ["gateway-direct-call", "box-simu-direct-call", "patch-panel-direct-call", "generic-hardware-rpc", "OpenAI-hardware-fallback", "M3/M4/M5-acceptance-claim"]
},
runnerLimitations: [...M3_IO_SKILL_LIMITATION_FLAGS],
safety: {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
cloudApiRouteOnly: true,
allowedRoute: HWLAB_M3_IO_API_ROUTE,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
openAiFallbackAllowed: false,
m3m4m5AcceptanceClaimsAllowed: false,
outputLimitBytes: READONLY_TOOL_OUTPUT_LIMIT
}
};
}
function m3IoSkillRunnerTrace({ traceId, events, startedAt, finishedAt = startedAt, outputTruncated, workspace = repoRoot, session = null, skillResult = null }) {
return {
traceId,
runnerKind: M3_IO_SKILL_RUNNER_KIND,
workspace,
sandbox: M3_IO_SKILL_SANDBOX,
sessionMode: M3_IO_SKILL_SESSION_MODE,
sessionId: session?.sessionId ?? null,
sessionStatus: session?.status ?? null,
idleTimeoutMs: session?.idleTimeoutMs ?? null,
lastTraceId: session?.lastTraceId ?? null,
turn: session?.turn ?? null,
sessionReused: session?.reused ?? false,
implementationType: M3_IO_SKILL_IMPLEMENTATION_TYPE,
limitations: [...M3_IO_SKILL_LIMITATION_FLAGS],
startedAt,
finishedAt,
events,
route: HWLAB_M3_IO_API_ROUTE,
skill: HWLAB_M3_IO_SKILL_NAME,
operationId: skillResult?.operationId ?? null,
accepted: skillResult?.accepted ?? false,
status: skillResult?.status ?? "blocked",
blocker: skillResult?.blocker ?? null,
outputTruncated: Boolean(outputTruncated),
valuesPrinted: false,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false,
note: `Controlled M3 IO uses Skill CLI -> HWLAB API ${HWLAB_M3_IO_API_ROUTE}; it is not OpenAI fallback and does not directly call gateway, box, or patch-panel.`
};
}
function sessionReuseEvidence(session) {
return {
conversationId: session.conversationId,
@@ -1424,6 +1846,9 @@ function resolveCodeAgentSessionRegistry(options = {}) {
}
function runnerDescriptor({ workspace, kind = READONLY_RUNNER_KIND, session = null } = {}) {
const allowed = kind === READONLY_RUNNER_KIND
? ["pwd", "skills.discover", "ls", "rg --files", "cat", `m3.io.skill-cli:${HWLAB_M3_IO_API_ROUTE}`]
: ["pwd", "skills.discover", "ls", "rg --files", "cat"];
return {
kind,
provider: kind === READONLY_RUNNER_KIND ? READONLY_RUNNER_PROVIDER : "codex-cli",
@@ -1443,7 +1868,7 @@ function runnerDescriptor({ workspace, kind = READONLY_RUNNER_KIND, session = nu
readOnly: true,
capabilityLevel: kind === READONLY_RUNNER_KIND ? READONLY_SESSION_CAPABILITY_LEVEL : "text-chat-only",
toolPolicy: {
allowed: ["pwd", "skills.discover", "ls", "rg --files", "cat"],
allowed,
blocked: ["secret-read", "kubeconfig-read", "hardware-write", "gateway-direct-call", "box-simu-direct-call", "patch-panel-direct-call", "M3/M4/M5-acceptance-claim"]
},
runnerLimitations: kind === READONLY_RUNNER_KIND ? [...READONLY_LIMITATION_FLAGS] : ["one-shot", "not-durable-session"],
@@ -1486,6 +1911,8 @@ function runnerSafetyContract() {
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
m3IoSkillCliAllowedRoute: HWLAB_M3_IO_API_ROUTE,
openAiHardwareFallbackAllowed: false,
m3m4m5AcceptanceClaimsAllowed: false,
outputLimitBytes: READONLY_TOOL_OUTPUT_LIMIT
};
@@ -7,6 +7,7 @@ import {
classifyCodexRunnerCapability,
classifyCodeAgentChatReadiness
} from "../../scripts/src/code-agent-response-contract.mjs";
import { HWLAB_M3_IO_API_ROUTE } from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
test("code agent session registry creates, reuses, and expires sessions without storing secrets", () => {
const registry = createCodeAgentSessionRegistry({
@@ -279,3 +280,224 @@ test("OpenAI fallback and codex one-shot do not pass the long-lived session gate
assert.equal(classifyCodexRunnerCapability(oneShot, { httpStatus: 200 }).capabilityPass, false);
assert.equal(classifyCodeAgentChatReadiness(oneShot, { realDevLive: true, httpStatus: 200 }).devLiveReplyPass, true);
});
test("Code Agent M3 DO write uses Skill CLI to call only HWLAB API /v1/m3/io", async () => {
const calls = [];
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_m3_skill_write",
traceId: "trc_m3_skill_write",
message: "请通过 M3 DO1 写入 true,并回读 DI1"
},
{
now: () => "2026-05-23T00:05:00.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
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 });
const parsed = new URL(url);
assert.equal(parsed.pathname, HWLAB_M3_IO_API_ROUTE);
assert.equal(parsed.hostname, "hwlab-cloud-api.hwlab-dev.svc.cluster.local");
assert.equal(request.method, "POST");
assert.equal(request.body.action, "do.write");
assert.equal(request.body.resourceId, "res_boxsimu_1");
assert.equal(request.body.port, "DO1");
assert.equal(request.body.value, true);
assert.equal(request.body.source, "hwlab-agent-runtime.m3-io");
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",
operationId: "op_m3_do_write_skill",
auditId: "aud_m3_do_write_skill_succeeded",
evidenceId: "evd_m3_do_write_skill_succeeded",
auditState: {
status: "written_non_durable",
durableStatus: {
status: "degraded"
}
},
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: {
status: "succeeded",
value: true,
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.backend, "hwlab-cloud-api/hwlab-agent-runtime-skill-cli");
assert.equal(payload.capabilityLevel, "controlled-m3-io-skill-cli");
assert.equal(payload.toolCalls.length, 1);
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].accepted, true);
assert.equal(payload.toolCalls[0].operationId, "op_m3_do_write_skill");
assert.equal(payload.runnerTrace.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(payload.runnerTrace.accepted, true);
assert.equal(payload.runner.writeCapable, true);
assert.deepEqual(payload.runner.toolPolicy.allowed, [`POST ${HWLAB_M3_IO_API_ROUTE}`]);
assert.equal(payload.runner.safety.directGatewayCallsAllowed, false);
assert.equal(payload.runner.safety.directBoxSimuCallsAllowed, false);
assert.equal(payload.runner.safety.directPatchPanelCallsAllowed, false);
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.match(payload.reply.content, /Skill CLI -> HWLAB API \/v1\/m3\/io/u);
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].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 M3 DI read returns structured blocker from HWLAB API without fallback", async () => {
const calls = [];
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_m3_skill_read_blocked",
traceId: "trc_m3_skill_read_blocked",
message: "读取 M3 DI1 readback"
},
{
now: () => "2026-05-23T00:06:00.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
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 read");
},
m3IoSkillRequestJson: async (url, request) => {
calls.push({ url, request });
return {
ok: true,
status: 200,
body: {
serviceId: "hwlab-cloud-api",
contractVersion: "m3-io-control-v1",
status: "blocked",
accepted: false,
action: "di.read",
traceId: "trc_m3_skill_read_blocked",
operationId: "op_m3_di_read_blocked",
auditId: "aud_m3_di_read_blocked_failed",
evidenceId: "evd_m3_di_read_blocked_failed",
blocker: {
code: "m3_gateway_session_unavailable",
message: "gateway unavailable",
zh: "gateway 未注册/不可用"
},
blockerClassification: {
category: "gateway_session"
},
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"
},
controlPath: {
cloudApi: true,
gatewaySimu: false,
boxSimu: false,
patchPanel: false,
frontendBypass: false
}
}
};
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-skill-cli");
assert.equal(payload.toolCalls[0].status, "blocked");
assert.equal(payload.toolCalls[0].route, HWLAB_M3_IO_API_ROUTE);
assert.equal(payload.toolCalls[0].blocker.code, "m3_gateway_session_unavailable");
assert.equal(payload.skills.blockers[0].code, "m3_gateway_session_unavailable");
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(calls.length, 1);
assert.equal(new URL(calls[0].url).pathname, HWLAB_M3_IO_API_ROUTE);
assert.equal(calls[0].request.body.action, "di.read");
assert.equal(calls[0].request.body.resourceId, "res_boxsimu_2");
assert.equal(calls[0].request.body.port, "DI1");
});
test("Code Agent blocks direct gateway or patch-panel requests instead of using M3 Skill CLI", async () => {
const direct = await handleCodeAgentChat(
{
conversationId: "cnv_m3_direct_blocked",
traceId: "trc_m3_direct_blocked",
message: "请直接调用 gateway-simu /invoke 写入 M3 DO1 true"
},
{
now: () => "2026-05-23T00:07:00.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667"
},
m3IoSkillRequestJson: async () => {
throw new Error("direct gateway request must not reach the M3 skill CLI");
}
}
);
validateCodeAgentChatSchema(direct);
assert.equal(direct.status, "failed");
assert.equal(direct.error.code, "security_blocked");
assert.equal(direct.toolCalls[0].name, "security.hardware-boundary");
assert.match(direct.error.message, /Skill CLI -> HWLAB API \/v1\/m3\/io/u);
});
+2 -1
View File
@@ -344,7 +344,8 @@ async function handleCodeAgentChatHttp(request, response, options) {
workspace: options.workspace,
skillsDirs: options.skillsDirs,
skillsDirsExact: options.skillsDirsExact,
sessionRegistry: options.sessionRegistry
sessionRegistry: options.sessionRegistry,
m3IoSkillRequestJson: options.m3IoSkillRequestJson
}
);
+102
View File
@@ -1021,6 +1021,108 @@ test("cloud api /v1/agent/chat runs read-only runner pwd with workspace evidence
}
});
test("cloud api /v1/agent/chat routes M3 IO through Skill CLI to /v1/m3/io only", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-skill-"));
const calls = [];
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: workspace,
HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
OPENAI_API_KEY: "must-not-be-used"
},
callCodeAgentProvider: async () => {
throw new Error("OpenAI fallback must not handle M3 IO");
},
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_server-test-m3-skill",
operationId: "op_m3_do_write_server_skill",
auditId: "aud_m3_do_write_server_skill_succeeded",
evidenceId: "evd_m3_do_write_server_skill_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
}
},
controlPath: {
status: "succeeded",
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: true,
frontendBypass: false
}
}
};
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": "trc_server-test-m3-skill"
},
body: JSON.stringify({
conversationId: "cnv_server-test-m3-skill",
message: "请通过 M3 DO1 写入 false,并回读 DI1"
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-skill-cli");
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].accepted, true);
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(payload.runner.safety.directGatewayCallsAllowed, false);
assert.equal(payload.runner.safety.directBoxSimuCallsAllowed, false);
assert.equal(payload.runner.safety.directPatchPanelCallsAllowed, 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");
assert.equal(calls[0].request.body.value, false);
assert.equal(calls[0].url.includes("gateway-simu"), false);
assert.equal(calls[0].url.includes("box-simu"), false);
assert.equal(calls[0].url.includes("patch-panel"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat reuses controlled read-only session and exposes bounded file tools", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-file-tools-"));
await writeFile(path.join(workspace, "alpha.txt"), "alpha\nbeta\n", "utf8");
+1 -1
View File
File diff suppressed because one or more lines are too long
+78
View File
@@ -350,6 +350,84 @@ async function runLocalContractSmoke() {
assert.equal(classifyCodexRunnerCapability(securityBlocked, { httpStatus: 200 }).capabilityPass, false);
logOk("security blocked path is structured");
const m3SkillCalls = [];
const m3SkillWrite = await handleCodeAgentChat(
{
conversationId: "cnv_code-agent-chat-m3-skill",
traceId: "trc_code-agent-chat-m3-skill",
message: "请通过 M3 DO1 写入 true,并回读 DI1"
},
{
now: () => "2026-05-22T00:04:55.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
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");
},
m3IoSkillRequestJson: async (url, request) => {
m3SkillCalls.push({ url, request });
return {
ok: true,
status: 200,
body: {
status: "succeeded",
accepted: true,
action: "do.write",
traceId: "trc_code-agent-chat-m3-skill",
operationId: "op_m3_do_write_smoke",
auditId: "aud_m3_do_write_smoke_succeeded",
evidenceId: "evd_m3_do_write_smoke_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: true,
targetReadback: {
status: "succeeded",
value: true
}
},
controlPath: {
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: true,
frontendBypass: false
}
}
};
}
}
);
validateCodeAgentChatSchema(m3SkillWrite);
assert.equal(m3SkillWrite.status, "completed");
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.providerTrace.fallbackUsed, false);
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");
assert.equal(m3SkillCalls[0].url.includes("gateway-simu"), false);
assert.equal(m3SkillCalls[0].url.includes("box-simu"), false);
assert.equal(m3SkillCalls[0].url.includes("patch-panel"), false);
logOk("M3 Skill CLI routes through HWLAB API only");
const completedHttp200Readiness = classifyCodeAgentChatReadiness(completedLivePayload, { realDevLive: true, httpStatus: 200 });
assert.equal(completedHttp200Readiness.status, "pass");
assert.equal(completedHttp200Readiness.devLiveReplyPass, true);
+22
View File
@@ -5,6 +5,8 @@ description: Build and validate the HWLAB agent-mgr and agent-worker runtime ske
# HWLAB Agent Runtime
Skill(cli-spec)
Use this skill when working on HWLAB agent runtime skeletons.
- Keep the control plane long-lived and worker execution session-scoped.
@@ -13,6 +15,10 @@ Use this skill when working on HWLAB agent runtime skeletons.
- Use repo-local skills artifacts only when `commitId` and `version` are
supplied explicitly.
- Do not infer or fall back to external skill sources.
- For M3 hardware control, use only the repo-owned Skill CLI below. It calls
HWLAB cloud-api `POST /v1/m3/io`; it must not call gateway, box, or
patch-panel URLs directly, and it must not use an OpenAI/text fallback as
hardware capability.
## Local CLI Contract
@@ -25,6 +31,9 @@ All commands emit JSON and stay local:
- `node cmd/hwlab-agent-mgr/main.mjs evidence --agent-session-id ID`
- `node cmd/hwlab-agent-mgr/main.mjs cleanup --agent-session-id ID`
- `node cmd/hwlab-agent-worker/main.mjs dry-run --agent-session-id ID`
- `node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action do.write --value true --api-base-url URL`
- `node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action do.write --value false --api-base-url URL`
- `node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action di.read --api-base-url URL`
Use `--state-dir DIR` to isolate local state. The default state directory is
`.state/agent-runtime`, which is intended for local runtime state only.
@@ -32,3 +41,16 @@ Use `--state-dir DIR` to isolate local state. The default state directory is
Provide both `--skill-commit-id COMMIT` and `--skill-version VERSION` for a
ready health result. Missing fields must produce `degraded` health and degraded
evidence metadata; there is no silent fallback.
## M3 IO Control Contract
The M3 IO CLI is the controlled Code Agent -> Skill CLI -> HWLAB API adapter.
`--api-base-url` must be a cloud-api base such as `http://127.0.0.1:6667`,
`http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667`, or the public DEV
API endpoint. The CLI appends `/v1/m3/io` itself and rejects direct
gateway/box/patch-panel targets.
The JSON response includes `route`, `traceId`, `operationId`, `audit`,
`evidence`, `accepted`, `status`, `blocker`, and direct-call safety flags.
Durable runtime blockers are preserved as structured `durable` and `evidence`
state; a local/source result must not be called DEV-LIVE.
@@ -0,0 +1,17 @@
#!/usr/bin/env node
import { runM3IoSkillCommand } from "./src/m3-io-skill-client.mjs";
try {
const result = await runM3IoSkillCommand(process.argv.slice(2));
const pretty = process.argv.includes("--pretty");
process.stdout.write(`${JSON.stringify(result, null, pretty ? 2 : 0)}\n`);
process.exitCode = result.ok ? 0 : 2;
} catch (error) {
process.stdout.write(`${JSON.stringify({
ok: false,
status: "failed",
code: "skill_cli_failed",
message: error instanceof Error ? error.message : String(error)
})}\n`);
process.exitCode = 1;
}
@@ -0,0 +1,137 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
HWLAB_M3_IO_API_ROUTE,
runM3IoSkillCommand,
validateCloudApiTarget
} from "./src/m3-io-skill-client.mjs";
test("M3 Skill CLI posts only to HWLAB API /v1/m3/io for DO1 writes", async () => {
const calls = [];
const result = await runM3IoSkillCommand(
[
"m3",
"io",
"--action",
"do.write",
"--value",
"false",
"--api-base-url",
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"--trace-id",
"trc_skill_cli_write_false",
"--request-id",
"req_skill_cli_write_false"
],
{
now: () => "2026-05-23T00:08:00.000Z",
requestJson: async (url, request) => {
calls.push({ url, request });
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_IO_API_ROUTE}`);
assert.equal(request.method, "POST");
assert.equal(request.body.action, "do.write");
assert.equal(request.body.resourceId, "res_boxsimu_1");
assert.equal(request.body.boxId, "boxsimu_1");
assert.equal(request.body.port, "DO1");
assert.equal(request.body.value, false);
assert.equal(request.body.source, "hwlab-agent-runtime.m3-io");
return {
ok: true,
status: 200,
body: {
status: "succeeded",
accepted: true,
traceId: "trc_skill_cli_write_false",
operationId: "op_m3_do_write_cli",
auditId: "aud_m3_do_write_cli_succeeded",
evidenceId: "evd_m3_do_write_cli_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: {
value: false
}
},
controlPath: {
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: true,
frontendBypass: false
}
}
};
}
}
);
assert.equal(result.ok, true);
assert.equal(result.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(result.accepted, true);
assert.equal(result.status, "succeeded");
assert.equal(result.operationId, "op_m3_do_write_cli");
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.durable.blocker, "runtime_durable_not_green");
assert.equal(result.safety.directGatewayCalls, false);
assert.equal(result.safety.directBoxCalls, false);
assert.equal(result.safety.directPatchPanelCalls, false);
assert.equal(result.safety.fallbackUsed, false);
assert.equal(calls.length, 1);
});
test("M3 Skill CLI rejects direct gateway, box, and patch-panel targets before request", async () => {
for (const url of [
"http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101",
"http://hwlab-box-simu-1.hwlab-dev.svc.cluster.local:7201",
"http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301"
]) {
const result = await runM3IoSkillCommand(
[
"m3",
"io",
"--action",
"di.read",
"--api-base-url",
url,
"--trace-id",
"trc_skill_cli_direct_blocked"
],
{
requestJson: async () => {
throw new Error("direct hardware target must be blocked before network call");
}
}
);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.accepted, false);
assert.equal(result.blocker.code, "direct_hardware_target_blocked");
assert.equal(result.operationId, null);
assert.equal(result.safety.directGatewayCalls, false);
assert.equal(result.safety.directBoxCalls, false);
assert.equal(result.safety.directPatchPanelCalls, false);
}
});
test("M3 Skill CLI validates exact API route contract", () => {
const invalid = validateCloudApiTarget({
url: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/rpc/m3.io.di.read"
});
assert.equal(invalid.code, "invalid_hwlab_api_route");
});
@@ -0,0 +1,564 @@
import { randomUUID } from "node:crypto";
export const HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION = "hwlab-agent-runtime-skill-cli-v1";
export const HWLAB_M3_IO_API_ROUTE = "/v1/m3/io";
export const HWLAB_M3_IO_SKILL_NAME = "hwlab-agent-runtime.m3-io";
const allowedActions = new Set(["do.write", "di.read"]);
const forbiddenDirectTarget = /(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel|hwlab-patch-panel|\/invoke\b|\/sync\/tick\b|:7101\b|:7201\b|:7301\b)/iu;
const defaultTimeoutMs = 30000;
export async function runM3IoSkillCommand(argv = [], options = {}) {
const env = options.env ?? process.env;
const parsed = parseM3IoArgs(argv, env);
if (parsed.help) {
return helpPayload();
}
const traceId = parsed.traceId || `trc_${randomUUID()}`;
const requestId = parsed.requestId || `req_${randomUUID()}`;
const actorId = parsed.actorId || "usr_code_agent";
const apiTarget = resolveCloudApiTarget(parsed.apiBaseUrl, env);
const validation = validateCloudApiTarget(apiTarget);
if (validation) {
return blockedPayload({
traceId,
requestId,
apiTarget,
code: validation.code,
message: validation.message
});
}
const command = normalizeCommand(parsed);
if (!allowedActions.has(command.action)) {
return blockedPayload({
traceId,
requestId,
apiTarget,
code: "invalid_action",
message: "M3 Skill CLI only allows do.write or di.read."
});
}
if (command.action === "do.write" && typeof command.value !== "boolean") {
return blockedPayload({
traceId,
requestId,
apiTarget,
code: "invalid_boolean_value",
message: "M3 DO1 write requires --value true or --value false."
});
}
const payload = requestPayload(command, { traceId, requestId, actorId });
const startedAt = nowIso(options.now);
const response = await postJson(apiTarget.url, payload, {
requestJson: options.requestJson,
timeoutMs: parsed.timeoutMs,
headers: {
"x-trace-id": traceId,
"x-request-id": requestId,
"x-actor-id": actorId
}
});
const finishedAt = nowIso(options.now);
return normalizeSkillResponse({
response,
command,
payload,
apiTarget,
traceId,
requestId,
actorId,
startedAt,
finishedAt
});
}
export function parseM3IoArgs(argv = [], env = process.env) {
const args = [...argv];
const command = args.shift() ?? "help";
const subcommand = args.shift() ?? "";
const parsed = {
command,
subcommand,
action: null,
value: undefined,
apiBaseUrl: null,
traceId: null,
requestId: null,
actorId: null,
timeoutMs: defaultTimeoutMs,
pretty: false,
help: command === "help" || command === "--help" || command === "-h"
};
if (command !== "m3" || subcommand !== "io") {
parsed.help = true;
return parsed;
}
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--help" || arg === "-h") {
parsed.help = true;
} else if (arg === "--pretty") {
parsed.pretty = true;
} else if (arg === "--action") {
parsed.action = readOption(args, ++index, arg);
} else if (arg === "--value") {
parsed.value = parseBoolean(readOption(args, ++index, arg));
} else if (arg === "--api-base-url") {
parsed.apiBaseUrl = readOption(args, ++index, arg);
} else if (arg === "--trace-id") {
parsed.traceId = readOption(args, ++index, arg);
} else if (arg === "--request-id") {
parsed.requestId = readOption(args, ++index, arg);
} else if (arg === "--actor-id") {
parsed.actorId = readOption(args, ++index, arg);
} else if (arg === "--timeout-ms") {
parsed.timeoutMs = parseTimeout(readOption(args, ++index, arg));
} else if (arg === "write") {
parsed.action = "do.write";
} else if (arg === "read") {
parsed.action = "di.read";
} else {
throw new Error(`unknown argument ${arg}`);
}
}
parsed.apiBaseUrl = parsed.apiBaseUrl || firstNonEmpty(
env.HWLAB_CODE_AGENT_HWLAB_API_BASE_URL,
env.HWLAB_API_BASE_URL,
env.HWLAB_CLOUD_API_BASE_URL,
loopbackCloudApiBaseUrl(env)
);
return parsed;
}
export function resolveCloudApiTarget(apiBaseUrl, env = process.env) {
const source = apiBaseUrl
? "explicit"
: firstNonEmpty(env.HWLAB_CODE_AGENT_HWLAB_API_BASE_URL, null)
? "env:HWLAB_CODE_AGENT_HWLAB_API_BASE_URL"
: firstNonEmpty(env.HWLAB_API_BASE_URL, null)
? "env:HWLAB_API_BASE_URL"
: firstNonEmpty(env.HWLAB_CLOUD_API_BASE_URL, null)
? "env:HWLAB_CLOUD_API_BASE_URL"
: "loopback-cloud-api-self";
const base = firstNonEmpty(apiBaseUrl, env.HWLAB_CODE_AGENT_HWLAB_API_BASE_URL, env.HWLAB_API_BASE_URL, env.HWLAB_CLOUD_API_BASE_URL, loopbackCloudApiBaseUrl(env));
const url = new URL(HWLAB_M3_IO_API_ROUTE, ensureTrailingSlash(base));
return {
route: HWLAB_M3_IO_API_ROUTE,
url: url.toString(),
redactedUrl: redactUrl(url.toString()),
source,
cloudApiOnly: true,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false
};
}
export function validateCloudApiTarget(apiTarget) {
let url;
try {
url = new URL(apiTarget.url);
} catch {
return {
code: "invalid_hwlab_api_url",
message: "HWLAB API base URL is not a valid URL."
};
}
if (url.pathname !== HWLAB_M3_IO_API_ROUTE) {
return {
code: "invalid_hwlab_api_route",
message: `Skill CLI must call exactly ${HWLAB_M3_IO_API_ROUTE}.`
};
}
const targetText = `${url.hostname}${url.pathname}${url.port ? `:${url.port}` : ""}`;
if (forbiddenDirectTarget.test(targetText)) {
return {
code: "direct_hardware_target_blocked",
message: "Skill CLI target must be HWLAB cloud-api, not gateway/box/patch-panel."
};
}
return null;
}
function normalizeCommand(parsed) {
const action = String(parsed.action ?? "").trim().toLowerCase();
if (action === "do.write") {
return {
action,
gatewayId: "gwsimu_1",
resourceId: "res_boxsimu_1",
boxId: "boxsimu_1",
port: "DO1",
value: parsed.value
};
}
return {
action,
gatewayId: "gwsimu_2",
resourceId: "res_boxsimu_2",
boxId: "boxsimu_2",
port: "DI1"
};
}
function requestPayload(command, meta) {
return {
action: command.action,
gatewayId: command.gatewayId,
resourceId: command.resourceId,
boxId: command.boxId,
port: command.port,
...(Object.hasOwn(command, "value") ? { value: command.value } : {}),
traceId: meta.traceId,
requestId: meta.requestId,
actorId: meta.actorId,
source: HWLAB_M3_IO_SKILL_NAME
};
}
async function postJson(url, body, { requestJson, timeoutMs, headers }) {
if (typeof requestJson === "function") {
try {
const result = await requestJson(url, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
...headers
},
body,
timeoutMs
});
return normalizeJsonResponse(result);
} catch (error) {
return {
ok: false,
status: 0,
body: null,
error: error instanceof Error ? error.message : String(error)
};
}
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
...headers
},
body: JSON.stringify(body),
signal: controller.signal
});
const text = await response.text();
const parsed = parseJsonOrNull(text);
return {
ok: response.ok,
status: response.status,
body: parsed,
error: response.ok ? null : parsed?.error?.message ?? parsed?.message ?? response.statusText
};
} catch (error) {
return {
ok: false,
status: 0,
body: null,
error: error.name === "AbortError" ? `HWLAB API request timed out after ${timeoutMs}ms` : error.message
};
} finally {
clearTimeout(timer);
}
}
function normalizeJsonResponse(result = {}) {
if (Object.hasOwn(result, "ok") || Object.hasOwn(result, "body")) {
return {
ok: result.ok ?? (Number(result.status ?? 200) >= 200 && Number(result.status ?? 200) < 300),
status: result.status ?? 200,
body: result.body ?? result.json ?? null,
error: result.error ?? result.body?.error?.message ?? null
};
}
const status = result.status ?? 200;
return {
ok: status >= 200 && status < 300,
status,
body: result.json ?? result,
error: result.error ?? null
};
}
function normalizeSkillResponse({ response, command, payload, apiTarget, traceId, requestId, actorId, startedAt, finishedAt }) {
const body = response.body ?? {};
const blocker = normalizeBlocker(body, response);
const accepted = body.accepted === true;
const status = body.status ?? (response.ok && accepted ? "succeeded" : "blocked");
const durable = durableSummary(body);
return {
ok: response.ok && accepted && status !== "blocked",
service: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route: HWLAB_M3_IO_API_ROUTE,
hwlabApi: apiTarget,
action: command.action,
accepted,
status,
traceId: body.traceId ?? traceId,
requestId,
actorId,
operationId: body.operationId ?? null,
audit: {
auditId: body.auditId ?? body.auditState?.auditId ?? null,
status: body.auditState?.status ?? null,
durableStatus: body.auditState?.durableStatus?.status ?? body.durableStatus?.status ?? null,
summary: auditSummary(body)
},
evidence: {
evidenceId: body.evidenceId ?? body.evidenceState?.evidenceId ?? null,
status: body.evidenceState?.status ?? null,
sourceKind: body.evidenceState?.sourceKind ?? null,
blocker: body.evidenceState?.blocker ?? null,
writeStatus: body.evidenceState?.writeStatus ?? null,
summary: evidenceSummary(body)
},
durable,
blocker,
command,
result: {
value: body.result?.value ?? body.value ?? null,
targetReadback: body.result?.targetReadback ?? null
},
controlPath: body.controlPath ?? {
cloudApi: true,
gatewaySimu: false,
boxSimu: false,
patchPanel: false,
frontendBypass: false
},
safety: {
cloudApiRouteOnly: true,
allowedRoute: HWLAB_M3_IO_API_ROUTE,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false,
openAiFallbackUsed: false
},
httpStatus: response.status,
error: response.ok ? body.error ?? null : {
code: "hwlab_api_unavailable",
message: response.error ?? "HWLAB API request failed"
},
rawStatus: body.status ?? null,
startedAt,
finishedAt,
response: body
};
}
function blockedPayload({ traceId, requestId, apiTarget, code, message }) {
return {
ok: false,
service: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
route: HWLAB_M3_IO_API_ROUTE,
hwlabApi: apiTarget ?? null,
accepted: false,
status: "blocked",
traceId,
requestId,
operationId: null,
audit: {
auditId: null,
status: "not_written",
durableStatus: null,
summary: "blocked before HWLAB API request"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED",
blocker: code,
writeStatus: "not_written",
summary: "blocked before HWLAB API request"
},
durable: {
status: "blocked",
blocker: code,
summary: message
},
blocker: {
code,
message,
zh: message,
category: blockerCategory(code)
},
safety: {
cloudApiRouteOnly: true,
allowedRoute: HWLAB_M3_IO_API_ROUTE,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false,
openAiFallbackUsed: false
}
};
}
function normalizeBlocker(body, response) {
const raw = body.blocker ?? body.error ?? null;
if (raw) {
const code = raw.code ?? body.blockerClassification?.code ?? "m3_io_blocked";
return {
code,
message: raw.message ?? raw.reason ?? raw.zh ?? body.blockerClassification?.reason ?? "M3 IO request was blocked.",
zh: raw.zh ?? raw.message ?? raw.reason ?? "M3 IO request was blocked.",
category: body.blockerClassification?.category ?? blockerCategory(code)
};
}
if (!response.ok) {
return {
code: "hwlab_api_unavailable",
message: response.error ?? `HWLAB API HTTP ${response.status}`,
zh: response.error ?? `HWLAB API HTTP ${response.status}`,
category: "hwlab_api"
};
}
return null;
}
function blockerCategory(code) {
const value = String(code ?? "");
if (/readiness|control_disabled|not_ready/u.test(value)) return "readiness_blocked";
if (/gateway/u.test(value)) return "gateway_unavailable";
if (/box|resource/u.test(value)) return "box_unavailable";
if (/wiring|patch_panel|patch-panel/u.test(value)) return "patch_panel_wiring_missing";
if (/durable|runtime/u.test(value)) return "durable_blocked";
if (/direct_hardware/u.test(value)) return "direct_hardware_blocked";
return "m3_io_blocked";
}
function durableSummary(body) {
const status = body.durableStatus?.status ?? body.evidenceState?.status ?? null;
const blocker = body.durableStatus?.blocker ?? body.evidenceState?.blocker ?? null;
return {
status,
durable: body.durableStatus?.durable ?? false,
blocker,
category: blocker ? blockerCategory(blocker) : null,
summary: blocker
? `durable evidence blocked: ${blocker}`
: status
? `durable status: ${status}`
: "durable status not reported"
};
}
function auditSummary(body) {
if (body.auditId) {
return `${body.auditId}; auditState=${body.auditState?.status ?? "unknown"}`;
}
return body.auditState?.reason ?? "audit not written";
}
function evidenceSummary(body) {
if (body.evidenceId) {
return `${body.evidenceId}; evidenceState=${body.evidenceState?.status ?? "unknown"}`;
}
return body.evidenceState?.reason ?? "evidence not written";
}
function helpPayload() {
return {
ok: true,
service: HWLAB_M3_IO_SKILL_NAME,
contractVersion: HWLAB_AGENT_RUNTIME_SKILL_CLI_VERSION,
usage: [
"node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action do.write --value true --api-base-url http://127.0.0.1:6667",
"node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action do.write --value false --api-base-url http://127.0.0.1:6667",
"node skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs m3 io --action di.read --api-base-url http://127.0.0.1:6667"
],
route: HWLAB_M3_IO_API_ROUTE,
safety: {
cloudApiRouteOnly: true,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false
}
};
}
function readOption(args, index, name) {
const value = args[index];
if (!value || value.startsWith("--")) {
throw new Error(`${name} requires a value`);
}
return value;
}
function parseBoolean(value) {
const normalized = String(value ?? "").trim().toLowerCase();
if (["true", "1", "on", "high"].includes(normalized)) return true;
if (["false", "0", "off", "low"].includes(normalized)) return false;
return value;
}
function parseTimeout(value) {
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 60000) {
throw new Error("--timeout-ms must be an integer between 1 and 60000");
}
return parsed;
}
function loopbackCloudApiBaseUrl(env = process.env) {
const port = env.HWLAB_CLOUD_API_PORT || env.PORT || "6667";
return `http://127.0.0.1:${port}`;
}
function ensureTrailingSlash(value) {
return String(value ?? "").endsWith("/") ? String(value) : `${value}/`;
}
function firstNonEmpty(...values) {
return values.find((value) => typeof value === "string" && value.trim())?.trim() ?? null;
}
function redactUrl(value) {
try {
const url = new URL(value);
url.username = "";
url.password = "";
return url.toString();
} catch {
return String(value ?? "").replace(/\/\/[^/@]+@/u, "//***@");
}
}
function parseJsonOrNull(value) {
try {
return JSON.parse(value);
} catch {
return null;
}
}
function nowIso(now) {
return typeof now === "function" ? now() : new Date().toISOString();
}