Merge pull request #457 from pikasTech/fix/code-agent-m3-controlled-io
feat: route Chinese M3 IO through controlled API
This commit is contained in:
@@ -1118,6 +1118,46 @@ async function callM3IoSkillRunner({ intent, conversationId, sessionId, traceId,
|
||||
}
|
||||
|
||||
async function executeM3IoSkillIntent({ intent, traceId, requestId, actorId, env, now, requestJson }) {
|
||||
if (intent?.blocker) {
|
||||
const blocker = m3IoIntentBlocker(intent.blocker, { traceId });
|
||||
return {
|
||||
commandArgs: [
|
||||
"m3",
|
||||
"io",
|
||||
"--trace-id",
|
||||
traceId,
|
||||
"--request-id",
|
||||
requestId,
|
||||
"--actor-id",
|
||||
actorId
|
||||
],
|
||||
skillResult: m3IoSkillBlockedBeforeRequestResult({
|
||||
traceId,
|
||||
requestId,
|
||||
actorId,
|
||||
blocker,
|
||||
route: HWLAB_M3_IO_API_ROUTE,
|
||||
method: "POST",
|
||||
action: "m3.io.blocked",
|
||||
hwlabApi: {
|
||||
route: HWLAB_M3_IO_API_ROUTE,
|
||||
redactedUrl: null,
|
||||
source: "intent-validation",
|
||||
baseUrlConfigured: Boolean(configuredCloudApiBaseUrl(env)),
|
||||
requiredEnv: [...HWLAB_M3_IO_API_BASE_URL_ENVS],
|
||||
recommendedEnv: HWLAB_M3_IO_API_BASE_URL_ENV,
|
||||
missingConfig: [],
|
||||
cloudApiOnly: true,
|
||||
directGatewayCalls: false,
|
||||
directBoxCalls: false,
|
||||
directPatchPanelCalls: false
|
||||
},
|
||||
startedAt: nowIso(now),
|
||||
finishedAt: nowIso(now)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
if (!m3IoIntentHasSupportedAction(intent)) {
|
||||
const blocker = m3IoIntentActionMissingBlocker({ traceId });
|
||||
return {
|
||||
@@ -1213,6 +1253,31 @@ function m3IoIntentActionMissingBlocker({ traceId }) {
|
||||
};
|
||||
}
|
||||
|
||||
function m3IoIntentBlocker(blocker, { traceId } = {}) {
|
||||
const code = blocker?.code ?? "m3_io_scope_blocked";
|
||||
const layer = blocker?.layer ?? "intent";
|
||||
return {
|
||||
code,
|
||||
layer,
|
||||
category: blocker?.category ?? "scope_blocked",
|
||||
retryable: false,
|
||||
source: "code-agent-m3-skill-cli",
|
||||
summary: blocker?.summary ?? "M3 IO request is outside the allowed controlled virtual IO surface.",
|
||||
message: blocker?.message ?? "Code Agent only supports the M3 virtual IO controlled cloud-api path.",
|
||||
zh: blocker?.zh ?? "当前只支持 M3 虚拟 IO 受控链路。",
|
||||
userMessage: blocker?.userMessage ?? "当前只支持 M3 虚拟 IO 受控链路:box-simu-1 DO1=true/false 写入,或 box-simu-2 DI1 读取。",
|
||||
traceId,
|
||||
route: HWLAB_M3_IO_API_ROUTE,
|
||||
toolName: HWLAB_M3_IO_SKILL_NAME,
|
||||
allowed: {
|
||||
read: `${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}`,
|
||||
write: `${M3_IO_TOPOLOGY.sourceResourceId}:${M3_IO_TOPOLOGY.sourcePort}=true|false`,
|
||||
route: HWLAB_M3_IO_API_ROUTE
|
||||
},
|
||||
requested: blocker?.requested ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function m3IoSkillBlockedBeforeRequestResult({
|
||||
traceId,
|
||||
requestId,
|
||||
@@ -1745,6 +1810,18 @@ function m3IoStructuredResult({ skillResult = {}, stdioResult = {}, commandArgs
|
||||
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
|
||||
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null
|
||||
},
|
||||
zh: m3IoChineseResult({
|
||||
action,
|
||||
isBlocker,
|
||||
blocker,
|
||||
route,
|
||||
method,
|
||||
do1Value,
|
||||
di1Value,
|
||||
trusted,
|
||||
durable,
|
||||
skillResult
|
||||
}),
|
||||
session: {
|
||||
sessionId: stdioResult.session?.sessionId ?? null,
|
||||
sessionMode: stdioResult.sessionMode ?? stdioResult.runner?.sessionMode ?? null,
|
||||
@@ -1772,7 +1849,9 @@ function m3IoStructuredResult({ skillResult = {}, stdioResult = {}, commandArgs
|
||||
trustBlocker: skillResult.trustBlocker ?? null,
|
||||
note: trusted
|
||||
? "operation/audit/evidence 已有可信持久化记录;仍不是 M3 DEV-LIVE 验收结论。"
|
||||
: "本次不伪造 DEV-LIVE:控制链路结果与可信持久化状态分开展示。"
|
||||
: skillResult.controlReady === true
|
||||
? "控制链路可达,但可信记录未 green,不能作为 DEV-LIVE 可信闭环通过。"
|
||||
: "本次不伪造 DEV-LIVE:控制链路结果与可信持久化状态分开展示。"
|
||||
},
|
||||
blocker,
|
||||
blockers: skillResult.blockers ?? (blocker ? [blocker] : []),
|
||||
@@ -1832,6 +1911,62 @@ function m3IoStructuredSummary({ action, isBlocker, blocker, route, do1Value, di
|
||||
return `M3 IO 结果:${actionText};trusted=${trusted ? "true" : "false"};durable=${durable ? "true" : "false"}。`;
|
||||
}
|
||||
|
||||
function m3IoChineseResult({ action, isBlocker, blocker, route, method, do1Value, di1Value, trusted, durable, skillResult = {} }) {
|
||||
const expectedValue = action === "do.write" ? do1Value : null;
|
||||
const targetResource = action === "do.write"
|
||||
? `${M3_IO_TOPOLOGY.sourceResourceId}:${M3_IO_TOPOLOGY.sourcePort}`
|
||||
: `${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}`;
|
||||
const actualResult = action === "do.write"
|
||||
? `${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}=${formatM3Value(di1Value)}`
|
||||
: action === "di.read"
|
||||
? `${M3_IO_TOPOLOGY.targetResourceId}:${M3_IO_TOPOLOGY.targetPort}=${formatM3Value(di1Value)}`
|
||||
: "未执行";
|
||||
const controlReachable = skillResult.controlReady === true;
|
||||
const trustedGreen = trusted === true;
|
||||
const durableGreen = durable?.durable === true && !durable?.blocker;
|
||||
return {
|
||||
status: isBlocker ? "blocked" : trustedGreen && durableGreen ? "succeeded" : "degraded",
|
||||
targetResource,
|
||||
action: action === "do.write"
|
||||
? "写入 DO1 并回读 DI1"
|
||||
: action === "di.read"
|
||||
? "读取 DI1"
|
||||
: "M3 IO 请求",
|
||||
expectedValue,
|
||||
readValue: action === "di.read" ? di1Value : null,
|
||||
actualResult,
|
||||
source: `HWLAB cloud-api 受控链路 ${method} ${route}`,
|
||||
traceId: skillResult.traceId ?? null,
|
||||
operationId: skillResult.operationId ?? null,
|
||||
auditId: skillResult.auditId ?? skillResult.audit?.auditId ?? null,
|
||||
evidenceId: skillResult.evidenceId ?? skillResult.evidence?.evidenceId ?? null,
|
||||
controlPath: {
|
||||
reachable: controlReachable,
|
||||
cloudApiRouteOnly: true,
|
||||
route,
|
||||
directGatewayCalls: false,
|
||||
directBoxCalls: false,
|
||||
directPatchPanelCalls: false
|
||||
},
|
||||
runtime: {
|
||||
durableGreen,
|
||||
trustedGreen,
|
||||
durableStatus: durable?.status ?? null,
|
||||
durableBlocker: durable?.blocker ?? null,
|
||||
note: controlReachable && !trustedGreen
|
||||
? "控制链路可达,但可信记录未 green,不能作为 DEV-LIVE 可信闭环通过。"
|
||||
: trustedGreen
|
||||
? "控制链路和可信记录均为 green;仍不在本接口内声明 live 已更新。"
|
||||
: "控制链路或可信记录未 green。"
|
||||
},
|
||||
blocker: blocker?.code ? {
|
||||
code: blocker.code,
|
||||
layer: blocker.layer ?? null,
|
||||
message: blocker.userMessage ?? blocker.zh ?? blocker.message ?? blocker.code
|
||||
} : null
|
||||
};
|
||||
}
|
||||
|
||||
function redactM3IoCommandArgs(commandArgs = []) {
|
||||
const redacted = [];
|
||||
for (let index = 0; index < commandArgs.length; index += 1) {
|
||||
@@ -2040,12 +2175,30 @@ function isSessionContextRequest(text) {
|
||||
|
||||
function detectM3IoIntent(text) {
|
||||
const normalized = String(text ?? "");
|
||||
if (!/\bM3\b|DO1|DI1|数字输出|数字输入|回读|读回|状态|status/u.test(normalized)) {
|
||||
if (!/\bM\d+\b|\bM3\b|DO\s*\d+|DI\s*\d+|数字输出|数字输入|box[-_ ]?simu|res_boxsimu|回读|读回|状态|status|串口|serial|PWM|DAP/u.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
if (isDirectM3HardwareAccessRequest(normalized)) {
|
||||
return null;
|
||||
}
|
||||
const scopeBlocker = detectM3IoScopeBlocker(normalized);
|
||||
if (scopeBlocker) {
|
||||
return {
|
||||
kind: "m3_io",
|
||||
toolName: HWLAB_M3_IO_SKILL_NAME,
|
||||
action: null,
|
||||
blocker: scopeBlocker
|
||||
};
|
||||
}
|
||||
const explicitTargetBlocker = detectM3IoTargetBlocker(normalized, "any");
|
||||
if (explicitTargetBlocker) {
|
||||
return {
|
||||
kind: "m3_io",
|
||||
toolName: HWLAB_M3_IO_SKILL_NAME,
|
||||
action: null,
|
||||
blocker: explicitTargetBlocker
|
||||
};
|
||||
}
|
||||
if (/DEV-LIVE|验收|acceptance|(?:M3|M4|M5).{0,20}(?:通过|pass|green|accept)/iu.test(normalized)) {
|
||||
return null;
|
||||
}
|
||||
@@ -2075,6 +2228,15 @@ function detectM3IoIntent(text) {
|
||||
}
|
||||
|
||||
if (wantsRead && !wantsWrite) {
|
||||
const targetBlocker = detectM3IoTargetBlocker(normalized, "di.read");
|
||||
if (targetBlocker) {
|
||||
return {
|
||||
kind: "m3_io",
|
||||
toolName: HWLAB_M3_IO_SKILL_NAME,
|
||||
action: null,
|
||||
blocker: targetBlocker
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "m3_io",
|
||||
toolName: HWLAB_M3_IO_SKILL_NAME,
|
||||
@@ -2082,6 +2244,15 @@ function detectM3IoIntent(text) {
|
||||
};
|
||||
}
|
||||
|
||||
const targetBlocker = detectM3IoTargetBlocker(normalized, "do.write");
|
||||
if (targetBlocker) {
|
||||
return {
|
||||
kind: "m3_io",
|
||||
toolName: HWLAB_M3_IO_SKILL_NAME,
|
||||
action: null,
|
||||
blocker: targetBlocker
|
||||
};
|
||||
}
|
||||
const value = parseM3WriteValue(normalized);
|
||||
if (typeof value !== "boolean") {
|
||||
return {
|
||||
@@ -2098,6 +2269,79 @@ function detectM3IoIntent(text) {
|
||||
};
|
||||
}
|
||||
|
||||
function detectM3IoScopeBlocker(text) {
|
||||
const value = String(text ?? "");
|
||||
const unsupportedStage = value.match(/\bM(?!3\b)\d+\b/iu)?.[0] ?? null;
|
||||
if (unsupportedStage && /(?:IO|DI|DO|控制|读取|读|写|状态|硬件|hardware)/iu.test(value)) {
|
||||
return m3ScopeBlocker({
|
||||
code: "m3_io_scope_blocked",
|
||||
summary: `Unsupported hardware stage ${unsupportedStage} requested.`,
|
||||
zh: `当前 Code Agent 只支持 M3 虚拟 IO,暂不支持 ${unsupportedStage} IO。`,
|
||||
requested: unsupportedStage
|
||||
});
|
||||
}
|
||||
const dangerous = value.match(/真实硬件|物理硬件|real\s+hardware|production\s+hardware|PROD|任意\s*shell|shell|bash|sh\s+-c|kubectl|数据库写入|写入数据库|database\s+write|SQL\s+write|绕过\s*(?:patch-panel|cloud-api|HWLAB API)|bypass\s+(?:patch-panel|cloud-api|HWLAB API)|批量脚本|脚本批量|batch\s+script|DAP|串口|serial|PWM/iu)?.[0] ?? null;
|
||||
if (dangerous && /(?:M3|IO|DI|DO|box[-_ ]?simu|res_boxsimu|硬件|hardware|patch-panel|cloud-api|HWLAB API)/iu.test(value)) {
|
||||
return m3ScopeBlocker({
|
||||
code: "m3_io_scope_blocked",
|
||||
summary: `Dangerous or out-of-scope M3 IO request was blocked: ${dangerous}.`,
|
||||
zh: "当前只支持 M3 虚拟 IO 受控链路;不能操作真实硬件、shell、数据库、串口/DAP/PWM、批量脚本,或绕过 cloud-api/patch-panel。",
|
||||
requested: dangerous
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectM3IoTargetBlocker(text, action) {
|
||||
const value = String(text ?? "");
|
||||
const unsupportedPort = value.match(/\b(?:DO|DI)\s*(?!1\b)\d+\b|数字(?:输出|输入)\s*(?!1\b)\d+|PWM|DAP|串口|serial/iu)?.[0] ?? null;
|
||||
if (unsupportedPort) {
|
||||
return m3ScopeBlocker({
|
||||
code: "m3_io_target_unsupported",
|
||||
category: "target_unsupported",
|
||||
summary: `Unsupported M3 IO target ${unsupportedPort} requested.`,
|
||||
zh: "当前只支持 M3 阶段允许的 DO1/DI1,不能执行 DO2/DI2、DAP、串口或 PWM 等请求。",
|
||||
requested: unsupportedPort
|
||||
});
|
||||
}
|
||||
|
||||
const mentionsBox1 = /(?:box[-_ ]?simu[-_ ]?1|boxsimu[_-]?1|res_boxsimu_1)/iu.test(value);
|
||||
const mentionsBox2 = /(?:box[-_ ]?simu[-_ ]?2|boxsimu[_-]?2|res_boxsimu_2)/iu.test(value);
|
||||
if (action === "do.write" && mentionsBox2 && !mentionsBox1) {
|
||||
return m3ScopeBlocker({
|
||||
code: "m3_io_target_unsupported",
|
||||
category: "target_unsupported",
|
||||
summary: "DO write target is not the allowed res_boxsimu_1:DO1 endpoint.",
|
||||
zh: "DO 写入仅允许目标 box-simu-1 DO1;不能把 box-simu-2 当作 DO 写入目标。",
|
||||
requested: "box-simu-2 DO1"
|
||||
});
|
||||
}
|
||||
if (action === "di.read" && mentionsBox1 && !mentionsBox2) {
|
||||
return m3ScopeBlocker({
|
||||
code: "m3_io_target_unsupported",
|
||||
category: "target_unsupported",
|
||||
summary: "DI read target is not the allowed res_boxsimu_2:DI1 endpoint.",
|
||||
zh: "DI 读取仅允许目标 box-simu-2 DI1;不能把 box-simu-1 当作 DI 读取目标。",
|
||||
requested: "box-simu-1 DI1"
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function m3ScopeBlocker({ code, category = "scope_blocked", summary, zh, requested }) {
|
||||
return {
|
||||
code,
|
||||
layer: "intent",
|
||||
category,
|
||||
retryable: false,
|
||||
summary,
|
||||
message: "M3 IO request is outside the allowed controlled virtual IO surface.",
|
||||
zh,
|
||||
userMessage: `${zh} 当前只支持 box-simu-1 DO1=true/false 写入和 box-simu-2 DI1 读取,且必须走 cloud-api ${HWLAB_M3_IO_API_ROUTE}。`,
|
||||
requested
|
||||
};
|
||||
}
|
||||
|
||||
function isDirectM3HardwareAccessRequest(text) {
|
||||
const value = String(text ?? "");
|
||||
if (/(?:https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel|hwlab-patch-panel)|\/invoke\b|\/sync\/tick\b|:7101\b|:7201\b|:7301\b)/iu.test(value)) {
|
||||
@@ -2116,8 +2360,10 @@ function isM3TopologyRouteMention(text) {
|
||||
}
|
||||
|
||||
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;
|
||||
if (/\bfalse\b|\boff\b|关闭|置低|低电平|拉低|断开/iu.test(text)) return false;
|
||||
if (/\btrue\b|\bon\b|打开|置高|高电平|拉高|闭合/iu.test(text)) return true;
|
||||
if (/(?:设置为|设为|写成|写入|置为|set\s+to|=|:)\s*0\b/iu.test(text)) return false;
|
||||
if (/(?:设置为|设为|写成|写入|置为|set\s+to|=|:)\s*1\b/iu.test(text)) return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2267,6 +2513,9 @@ function m3IoSkillReply(skillResult, m3Io = null) {
|
||||
if (m3Io?.blocker?.code) {
|
||||
lines.push(`Blocker: ${m3Io.blocker.code};blocker=${m3Io.blocker.code};layer=${m3Io.blocker.layer ?? "unknown"}。`);
|
||||
}
|
||||
if (m3Io?.trust?.note) {
|
||||
lines.push(`可信记录:${m3Io.trust.note}`);
|
||||
}
|
||||
lines.push(`可信状态:trusted=${m3Io?.trust?.trusted === true ? "true" : "false"};durable=${m3Io?.trust?.durable === true ? "true" : "false"};这不是 M3 DEV-LIVE 验收结论。`);
|
||||
return boundToolOutput(lines.join("\n"), READONLY_TOOL_OUTPUT_LIMIT).text;
|
||||
}
|
||||
@@ -2411,7 +2660,7 @@ function structuredCompletionBlocker(result, context = {}) {
|
||||
}
|
||||
|
||||
function m3CompletionBlockerCode(primary = {}) {
|
||||
if (["m3_gateway_session_unavailable", "runtime_durable_not_green", "skill_cli_api_base_missing", "m3_io_intent_action_missing"].includes(primary?.code)) {
|
||||
if (["m3_gateway_session_unavailable", "runtime_durable_not_green", "skill_cli_api_base_missing", "m3_io_intent_action_missing", "m3_io_scope_blocked", "m3_io_target_unsupported"].includes(primary?.code)) {
|
||||
return "m3_readiness_blocked";
|
||||
}
|
||||
return primary?.code ?? "m3_io_blocker";
|
||||
|
||||
@@ -1319,6 +1319,162 @@ test("Code Agent M3 true/false/read requests bypass Codex stdio and use Skill CL
|
||||
assert.equal(calls.length, cases.length);
|
||||
});
|
||||
|
||||
test("Code Agent parses exact Chinese M3 IO requests and returns structured Chinese result", async () => {
|
||||
const calls = [];
|
||||
const cases = [
|
||||
{
|
||||
suffix: "read_di1",
|
||||
message: "读取 box-simu-2 DI1 状态",
|
||||
action: "di.read",
|
||||
value: true,
|
||||
zhAction: "读取 DI1",
|
||||
targetResource: "res_boxsimu_2:DI1"
|
||||
},
|
||||
{
|
||||
suffix: "write_do1_true",
|
||||
message: "把 box-simu-1 DO1 设置为 true",
|
||||
action: "do.write",
|
||||
value: true,
|
||||
zhAction: "写入 DO1 并回读 DI1",
|
||||
targetResource: "res_boxsimu_1:DO1"
|
||||
},
|
||||
{
|
||||
suffix: "write_do1_false",
|
||||
message: "把 box-simu-1 DO1 设置为 false",
|
||||
action: "do.write",
|
||||
value: false,
|
||||
zhAction: "写入 DO1 并回读 DI1",
|
||||
targetResource: "res_boxsimu_1:DO1"
|
||||
}
|
||||
];
|
||||
|
||||
for (const item of cases) {
|
||||
const payload = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: `cnv_m3_exact_zh_${item.suffix}`,
|
||||
traceId: `trc_m3_exact_zh_${item.suffix}`,
|
||||
message: item.message
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-24T10:00:00.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
OPENAI_API_KEY: "must-not-be-used"
|
||||
},
|
||||
callProvider: async () => {
|
||||
throw new Error("OpenAI fallback must not be used for exact Chinese M3 IO");
|
||||
},
|
||||
m3IoSkillRequestJson: async (url, request) => {
|
||||
calls.push({ suffix: item.suffix, url, request });
|
||||
assert.equal(new URL(url).pathname, HWLAB_M3_IO_API_ROUTE);
|
||||
assert.equal(new URL(url).hostname, "hwlab-cloud-api.hwlab-dev.svc.cluster.local");
|
||||
assert.equal(request.method, "POST");
|
||||
assert.equal(request.body.action, item.action);
|
||||
assert.equal(request.body.source, "hwlab-agent-runtime.m3-io");
|
||||
if (item.action === "do.write") {
|
||||
assert.equal(request.body.resourceId, "res_boxsimu_1");
|
||||
assert.equal(request.body.port, "DO1");
|
||||
assert.equal(request.body.value, item.value);
|
||||
} else {
|
||||
assert.equal(request.body.resourceId, "res_boxsimu_2");
|
||||
assert.equal(request.body.port, "DI1");
|
||||
assert.equal(Object.hasOwn(request.body, "value"), false);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: {
|
||||
serviceId: "hwlab-cloud-api",
|
||||
contractVersion: "m3-io-control-v1",
|
||||
status: "succeeded",
|
||||
accepted: true,
|
||||
action: item.action,
|
||||
traceId: `trc_m3_exact_zh_${item.suffix}`,
|
||||
operationId: `op_m3_exact_zh_${item.suffix}`,
|
||||
auditId: `aud_m3_exact_zh_${item.suffix}`,
|
||||
evidenceId: `evd_m3_exact_zh_${item.suffix}`,
|
||||
auditState: {
|
||||
status: item.action === "do.write" ? "written_non_durable" : "read"
|
||||
},
|
||||
evidenceState: {
|
||||
status: "blocked",
|
||||
sourceKind: "BLOCKED",
|
||||
blocker: "runtime_durable_not_green",
|
||||
writeStatus: item.action === "do.write" ? "written_non_durable" : "not_written"
|
||||
},
|
||||
durableStatus: {
|
||||
status: "degraded",
|
||||
durable: false,
|
||||
blocker: "runtime_durable_not_green"
|
||||
},
|
||||
result: item.action === "do.write"
|
||||
? {
|
||||
value: item.value,
|
||||
targetReadback: {
|
||||
status: "succeeded",
|
||||
value: item.value,
|
||||
resourceId: "res_boxsimu_2",
|
||||
port: "DI1"
|
||||
}
|
||||
}
|
||||
: {
|
||||
status: "succeeded",
|
||||
value: item.value,
|
||||
resourceId: "res_boxsimu_2",
|
||||
port: "DI1"
|
||||
},
|
||||
controlPath: {
|
||||
status: "succeeded",
|
||||
cloudApi: true,
|
||||
gatewaySimu: true,
|
||||
boxSimu: true,
|
||||
patchPanel: true,
|
||||
frontendBypass: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
validateCodeAgentChatSchema(payload);
|
||||
assert.equal(payload.status, "completed");
|
||||
assert.equal(payload.provider, "hwlab-skill-cli");
|
||||
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
|
||||
assert.equal(payload.responseType, "m3_io_result");
|
||||
assert.equal(payload.m3Io.zh.status, "degraded");
|
||||
assert.equal(payload.m3Io.zh.targetResource, item.targetResource);
|
||||
assert.equal(payload.m3Io.zh.action, item.zhAction);
|
||||
assert.equal(payload.m3Io.zh.traceId, `trc_m3_exact_zh_${item.suffix}`);
|
||||
assert.equal(payload.m3Io.zh.operationId, `op_m3_exact_zh_${item.suffix}`);
|
||||
assert.equal(payload.m3Io.zh.auditId, `aud_m3_exact_zh_${item.suffix}`);
|
||||
assert.equal(payload.m3Io.zh.evidenceId, `evd_m3_exact_zh_${item.suffix}`);
|
||||
assert.equal(payload.m3Io.zh.controlPath.reachable, true);
|
||||
assert.equal(payload.m3Io.zh.controlPath.cloudApiRouteOnly, true);
|
||||
assert.equal(payload.m3Io.zh.runtime.durableGreen, false);
|
||||
assert.equal(payload.m3Io.zh.runtime.trustedGreen, false);
|
||||
assert.equal(payload.m3Io.zh.runtime.durableBlocker, "runtime_durable_not_green");
|
||||
assert.match(payload.m3Io.zh.runtime.note, /控制链路可达,但可信记录未 green/u);
|
||||
assert.match(payload.reply.content, /控制链路可达,但可信记录未 green/u);
|
||||
assert.equal(payload.toolCalls[0].route, HWLAB_M3_IO_API_ROUTE);
|
||||
assert.equal(payload.toolCalls[0].method, "POST");
|
||||
assert.equal(payload.toolCalls[0].hwlabApi.cloudApiOnly, true);
|
||||
assert.equal(payload.toolCalls[0].directGatewayCalls, false);
|
||||
assert.equal(payload.toolCalls[0].directBoxCalls, false);
|
||||
assert.equal(payload.toolCalls[0].directPatchPanelCalls, false);
|
||||
assert.equal(payload.providerTrace.fallbackUsed, false);
|
||||
}
|
||||
|
||||
assert.equal(calls.length, cases.length);
|
||||
for (const call of calls) {
|
||||
assert.equal(call.url.includes("gateway"), false);
|
||||
assert.equal(call.url.includes("box-simu"), false);
|
||||
assert.equal(call.url.includes("patch-panel"), false);
|
||||
}
|
||||
});
|
||||
|
||||
test("Code Agent ambiguous M3 IO control request returns structured blocker without Codex stdio", async () => {
|
||||
const codexStdioCalls = [];
|
||||
const payload = await handleCodeAgentChat(
|
||||
@@ -1385,6 +1541,84 @@ test("Code Agent ambiguous M3 IO control request returns structured blocker with
|
||||
assert.equal(codexStdioCalls.length, 0);
|
||||
});
|
||||
|
||||
test("Code Agent blocks out-of-scope or dangerous Chinese M3 IO requests before HWLAB API call", async () => {
|
||||
const messages = [
|
||||
{
|
||||
suffix: "real_hardware",
|
||||
message: "把真实硬件 M3 DO1 设置为 true",
|
||||
code: "m3_io_scope_blocked"
|
||||
},
|
||||
{
|
||||
suffix: "shell",
|
||||
message: "用任意 shell 批量脚本绕过 patch-panel 写入 M3 DO1 true",
|
||||
code: "m3_io_scope_blocked"
|
||||
},
|
||||
{
|
||||
suffix: "di2",
|
||||
message: "读取 box-simu-2 DI2 状态",
|
||||
code: "m3_io_target_unsupported"
|
||||
},
|
||||
{
|
||||
suffix: "wrong_box",
|
||||
message: "把 box-simu-2 DO1 设置为 true",
|
||||
code: "m3_io_target_unsupported"
|
||||
},
|
||||
{
|
||||
suffix: "serial_pwm",
|
||||
message: "通过串口读取 M3 PWM 状态",
|
||||
code: "m3_io_scope_blocked"
|
||||
}
|
||||
];
|
||||
|
||||
for (const item of messages) {
|
||||
const payload = await handleCodeAgentChat(
|
||||
{
|
||||
conversationId: `cnv_m3_dangerous_zh_${item.suffix}`,
|
||||
traceId: `trc_m3_dangerous_zh_${item.suffix}`,
|
||||
message: item.message
|
||||
},
|
||||
{
|
||||
now: () => "2026-05-24T10:01:00.000Z",
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
OPENAI_API_KEY: "must-not-be-used"
|
||||
},
|
||||
callProvider: async () => {
|
||||
throw new Error("OpenAI fallback must not be used for dangerous M3 IO");
|
||||
},
|
||||
m3IoSkillRequestJson: async () => {
|
||||
throw new Error("dangerous or out-of-scope M3 IO must block before HWLAB API request");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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.responseType, "m3_io_blocker");
|
||||
assert.equal(payload.blocker.code, "m3_readiness_blocked");
|
||||
assert.equal(payload.toolCalls.length, 1);
|
||||
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, item.code);
|
||||
assert.equal(payload.toolCalls[0].hwlabApi.source, "intent-validation");
|
||||
assert.equal(payload.toolCalls[0].accepted, false);
|
||||
assert.equal(payload.toolCalls[0].operationId, null);
|
||||
assert.equal(payload.toolCalls[0].directGatewayCalls, false);
|
||||
assert.equal(payload.toolCalls[0].directBoxCalls, false);
|
||||
assert.equal(payload.toolCalls[0].directPatchPanelCalls, false);
|
||||
assert.equal(payload.providerTrace.fallbackUsed, false);
|
||||
assert.equal(payload.m3Io.zh.status, "blocked");
|
||||
assert.equal(payload.m3Io.zh.controlPath.cloudApiRouteOnly, true);
|
||||
assert.match(payload.m3Io.zh.blocker.message, /当前只支持/u);
|
||||
assert.match(payload.reply.content, /M3 IO 阻塞/u);
|
||||
assert.match(payload.reply.content, /Blocker:/u);
|
||||
}
|
||||
});
|
||||
|
||||
test("Code Agent M3 status uses Skill CLI GET /v1/m3/status and keeps route evidence", async () => {
|
||||
const calls = [];
|
||||
const payload = await handleCodeAgentChat(
|
||||
|
||||
Reference in New Issue
Block a user