Files
pikasTech-HWLAB/skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs
T
2026-05-23 14:06:11 +00:00

639 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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";
export const HWLAB_M3_IO_CAPABILITY_LEVELS = Object.freeze({
blocked: "hwlab-api-control-blocked",
ready: "hwlab-api-control-ready"
});
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);
const controlReady = response.ok && accepted && status !== "blocked" && body.controlPath?.cloudApi !== false && body.controlPath?.frontendBypass !== true;
const capabilityLevel = controlReady
? HWLAB_M3_IO_CAPABILITY_LEVELS.ready
: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked;
const capabilityBlocker = controlReady ? null : normalizeCapabilityBlocker(blocker, response);
const trustBlocker = normalizeTrustBlocker(body);
const blockers = dedupeBlockers([capabilityBlocker, trustBlocker]);
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,
capabilityLevel,
controlReady,
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,
capabilityBlocker,
trustBlocker,
blockers,
readiness: {
status: controlReady ? "ready" : "blocked",
controlReady,
capabilityLevel,
route: HWLAB_M3_IO_API_ROUTE,
blocker: capabilityBlocker,
trustBlocker
},
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 }) {
const capabilityBlocker = {
code,
message,
zh: message,
category: blockerCategory(code)
};
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,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
controlReady: false,
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: capabilityBlocker,
capabilityBlocker,
trustBlocker: null,
blockers: [capabilityBlocker],
readiness: {
status: "blocked",
controlReady: false,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
route: HWLAB_M3_IO_API_ROUTE,
blocker: capabilityBlocker,
trustBlocker: null
},
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 normalizeCapabilityBlocker(blocker, response) {
if (blocker) return blocker;
return {
code: response.ok ? "hwlab_api_control_blocked" : "hwlab_api_unavailable",
message: response.ok ? "HWLAB API did not accept the M3 IO control request." : response.error ?? "HWLAB API request failed",
zh: response.ok ? "HWLAB API 未接受 M3 IO 控制请求。" : response.error ?? "HWLAB API 请求失败",
category: response.ok ? "readiness_blocked" : "hwlab_api"
};
}
function normalizeTrustBlocker(body) {
const code = body.evidenceState?.blocker ?? body.durableStatus?.blocker ?? null;
if (!code) return null;
return {
code,
message: `Durable trust is blocked: ${code}`,
zh: `可信持久化仍 blocked${code}`,
category: blockerCategory(code),
layer: "runtime-durable",
source: body.evidenceState?.blocker ? "evidenceState.blocker" : "durableStatus.blocker"
};
}
function dedupeBlockers(blockers) {
const seen = new Set();
const result = [];
for (const blocker of blockers) {
if (!blocker?.code) continue;
const key = `${blocker.code}:${blocker.layer ?? ""}:${blocker.category ?? ""}`;
if (seen.has(key)) continue;
seen.add(key);
result.push(blocker);
}
return result;
}
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();
}