fix: remove code agent routing gates

This commit is contained in:
lyon
2026-05-25 20:55:33 +08:00
parent 6e736b2da4
commit 8b2c569ec4
15 changed files with 64 additions and 1590 deletions
+6 -4
View File
@@ -19,10 +19,10 @@ Secret 或 token。
## Provider 前置条件
当前 DEV 部署合同中 `hwlab-cloud-api` 的 Code Agent provider 是
`HWLAB_CODE_AGENT_PROVIDER=codex-stdio`。运行时必须具备 repo-owned Codex MCP
`HWLAB_CODE_AGENT_PROVIDER=codex-stdio`。运行时必须具备 repo-owned Codex app-server
stdio session supervisor,并证明 `/workspace/hwlab` 可读写、`CODEX_HOME=/codex-home`
可写、`/app/node_modules/.bin/codex --version` 可执行、`codex mcp-server`
通过 MCP initialize/tools/list 观测到 `codex``codex-reply`
可写、`/app/node_modules/.bin/codex --version` 可执行、`codex app-server --listen stdio://`
创建和复用同一个 Codex thread/session
Codex token boundary 仍由授权路径把 `OPENAI_API_KEY` 注入到 DEV runtime。DEV Pod
必须使用 `HWLAB_CODE_AGENT_OPENAI_BASE_URL` 指向受控 DEV egress/proxy 路径
@@ -66,7 +66,9 @@ result 轮询和 schema 化返回。
如果 Codex stdio 不具备运行条件,`/v1/agent/chat` 只能返回 Codex stdio readiness
blocker,不能降级到 M3 Skill CLI、受控硬件 shortcut、外网专用检查或普通 OpenAI 文本回复。
显式 `/v1/m3/io` 控制面可以作为独立 API 或 UI 控制面继续存在,但聊天自然语言不得自动路由
到该 API,也不得保留要求自然语言先满足 M3 白名单的源码检查或测试。
到该 API,也不得保留要求自然语言先满足 M3 白名单的源码检查或测试。该显式控制面也不得在
进入 gateway 前保留固定 `DO1/DI1` 或固定 gateway 身份白名单预拦截;真实下游执行失败可以
返回执行失败,但不能由 cloud-api 用旧白名单提前拒绝。
持久 session 是默认合同:同一个 `conversationId/sessionId` 必须映射到 repo-owned Codex
thread 和固定 workspace,刷新前端、重新打开页面或短连接 result 轮询不得创建新的短期 runner。
+9 -13
View File
@@ -55,10 +55,6 @@ const READONLY_LIMITATION_FLAGS = Object.freeze([
"process-local-session-registry"
]);
const CODEX_STDIO_LIMITATION_FLAGS = Object.freeze([
"hardware-control-via-cloud-api-only",
"no-direct-gateway-link",
"no-direct-box-simu-link",
"no-direct-patch-panel-link",
"secret-values-redacted"
]);
const CODE_AGENT_SYSTEM_PROMPT = [
@@ -629,10 +625,10 @@ function codexStdioRunnerAvailability(codexStdio = {}) {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
hardwareControlViaCloudApiOnly: true,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
hardwareControlViaCloudApiOnly: false,
directGatewayCallsAllowed: true,
directBoxSimuCallsAllowed: true,
directPatchPanelCallsAllowed: true,
m3m4m5AcceptanceClaimsAllowed: false
}
};
@@ -674,10 +670,10 @@ function codexStdioBlockedRunnerAvailability(codexStdio = {}) {
secretsRead: false,
secretValuesPrinted: false,
kubeconfigRead: false,
hardwareControlViaCloudApiOnly: true,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
hardwareControlViaCloudApiOnly: false,
directGatewayCallsAllowed: true,
directBoxSimuCallsAllowed: true,
directPatchPanelCallsAllowed: true,
m3m4m5AcceptanceClaimsAllowed: false
}
};
@@ -720,7 +716,7 @@ function primaryCodeAgentReason({ blocked, codexStdio, runnerAvailability }) {
function codeAgentAvailabilitySummary({ blocked, codexStdio, runnerAvailability }) {
if (codexStdio.ready) {
return "Codex stdio long-lived session adapter is feasible; /v1/agent/chat can create/reuse/cancel/reap sessions with trace capture. Hardware control still must use cloud-api/HWLAB API/skill CLI.";
return "Codex stdio long-lived session adapter is feasible; /v1/agent/chat sends natural-language requests to the persistent Codex session with full workspace and tool access.";
}
if (runnerAvailability.ready) return "Codex stdio long-lived session is blocked; /v1/agent/chat will not use controlled-readonly-session-registry, local skills discovery, shell/file shortcuts, or text fallback.";
return blocked
@@ -1288,7 +1288,7 @@ test("repo-owned Codex stdio manager creates and reuses long-lived sessions with
assert.equal(first.capabilityLevel, "long-lived-codex-stdio-session");
assert.equal(first.longLivedSessionGate.status, "pass");
assert.equal(first.longLivedSessionGate.pass, true);
assert.deepEqual(first.runnerLimitations, ["hardware-control-via-cloud-api-only", "secret-values-redacted"]);
assert.deepEqual(first.runnerLimitations, ["secret-values-redacted"]);
assert.equal(first.runnerLimitations.includes("not-codex-stdio"), false);
assert.equal(first.runnerLimitations.includes("not-write-capable"), false);
assert.equal(first.runnerLimitations.includes("process-local-session-registry"), false);
+10 -662
View File
@@ -1,9 +1,7 @@
import { spawn, spawnSync } from "node:child_process";
import { randomUUID } from "node:crypto";
import { lookup as dnsLookup } from "node:dns/promises";
import { accessSync, constants as fsConstants, existsSync, realpathSync, statSync } from "node:fs";
import { mkdir, readdir, readFile, rm, rmdir, stat, writeFile } from "node:fs/promises";
import { isIP } from "node:net";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -58,10 +56,6 @@ const CODEX_STDIO_COMMAND_PROBE_TIMEOUT_MS = 3000;
const CODEX_STDIO_COMMAND_PROBE_TRACE_ID = "trc_codex_stdio_command_probe";
const CODEX_STDIO_COMMAND_PROBE_CONVERSATION_ID = "cnv_codex_stdio_command_probe";
const CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS = 15 * 1000;
const EXTERNAL_NETWORK_TOOL_NAME = "external.network.check";
const EXTERNAL_NETWORK_DEFAULT_TIMEOUT_MS = 8000;
const EXTERNAL_NETWORK_DNS_TIMEOUT_MS = 1500;
const EXTERNAL_NETWORK_HTTP_METHOD = "HEAD";
const CODEX_CHILD_STRIPPED_ENV_KEYS = Object.freeze([
"OPENAI_API_KEY",
"CODEX_API_KEY",
@@ -424,23 +418,6 @@ export function createCodexStdioSessionManager(options = {}) {
turn: session.turn,
waitingFor: "prompt-send"
});
const externalNetworkIntent = normalizeExternalNetworkIntent(params.externalNetworkIntent, params.message);
if (externalNetworkIntent) {
return await runExternalNetworkTurn({
params,
env,
traceRecorder,
session,
availability,
workspace,
sandbox,
startedAt,
intent: externalNetworkIntent,
fetchImpl: options.fetchImpl,
lookupImpl: options.lookupImpl,
releaseSession
});
}
const sidecar = await collectWorkspaceSidecarEvidence({
message: params.message,
workspace,
@@ -632,7 +609,7 @@ export function createCodexStdioSessionManager(options = {}) {
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionReuse: sessionReuseEvidence(session),
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
runnerLimitations: ["secret-values-redacted"],
codexStdioFeasibility: availability,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
@@ -719,10 +696,7 @@ export function createCodexStdioSessionManager(options = {}) {
});
}
if (error.code && (error.code.startsWith("codex_stdio") || [
"skills_unavailable",
"external_network_blocked",
"network_tool_unavailable",
"network_timeout"
"skills_unavailable"
].includes(error.code))) {
error.session = session;
error.availability = error.availability ?? describe({ ...params, env, workspace, sandbox });
@@ -2229,10 +2203,10 @@ function runnerDescriptor({ workspace, sandbox, session }) {
readOnly: false,
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
toolPolicy: {
allowed: ["codex-app-server.thread/start", "codex-app-server.thread/resume", "codex-app-server.turn/start", "workspace-read", "workspace-write", EXTERNAL_NETWORK_TOOL_NAME],
allowed: ["codex-app-server.thread/start", "codex-app-server.thread/resume", "codex-app-server.turn/start", "workspace-read", "workspace-write"],
blocked: ["secret-read", "kubeconfig-read", "gateway-direct-call", "box-simu-direct-call", "patch-panel-direct-call", "M3/M4/M5-acceptance-claim-without-evidence"]
},
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
runnerLimitations: ["secret-values-redacted"],
safety: codexStdioSafety()
};
}
@@ -2269,7 +2243,7 @@ function runnerTrace({ traceRecorder, traceId, workspace, sandbox, session, star
turn: session?.turn ?? null,
sessionReused: session?.reused ?? false,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
limitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
limitations: ["secret-values-redacted"],
startedAt,
outputTruncated: Boolean(outputTruncated),
note: "Repo-owned Codex app-server stdio supervisor manages create/reuse/cancel/reap/idle timeout and real-time trace capture; hardware control remains routed through cloud-api/HWLAB API/skill CLI boundaries."
@@ -2352,632 +2326,6 @@ function summarizeToolResult(toolResult) {
return "tool result captured";
}
async function runExternalNetworkTurn({
params,
env,
traceRecorder,
session,
availability,
workspace,
sandbox,
startedAt,
intent,
fetchImpl,
lookupImpl,
releaseSession: releaseSessionFn
} = {}) {
const traceId = optionalId(params?.traceId) ?? traceRecorder?.traceId;
const now = params?.now;
const toolName = EXTERNAL_NETWORK_TOOL_NAME;
traceRecorder.append({
type: "prompt",
status: "sent",
label: "prompt:sent",
toolName,
promptSummary: summarizePrompt(params?.message),
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn,
waitingFor: "network-policy"
});
traceRecorder.append({
type: "tool_call",
status: "started",
label: `tool:${toolName}:started`,
toolName,
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn,
waitingFor: "network:policy"
});
traceRecorder.append({
type: "network",
stage: "policy",
status: "started",
label: "network:started",
toolName,
outputSummary: `target=${safeNetworkUrl(intent.url)}`,
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn,
waitingFor: "network:http"
});
const check = await controlledExternalNetworkCheck({
intent,
env,
timeoutMs: params?.networkTimeoutMs,
fetchImpl,
lookupImpl,
now
});
const toolCall = externalNetworkToolCall({ check, workspace, traceId });
if (check.ok !== true) {
traceRecorder.append({
type: "network",
stage: check.stage ?? "blocked",
status: check.code === "network_timeout" ? "timeout" : "blocked",
label: check.code === "network_timeout" ? "network:timeout" : "network:blocked",
toolName,
errorCode: check.code,
message: check.userMessage,
outputSummary: check.summary,
timeoutMs: check.timeoutMs,
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn,
waitingFor: check.code === "network_timeout" ? "external-network-response" : "network-policy"
});
traceRecorder.append({
type: "tool_call",
status: "blocked",
label: `tool:${toolName}:blocked`,
toolName,
errorCode: check.code,
outputSummary: check.summary,
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn
});
const traceSnapshot = traceRecorder.snapshot({
sessionId: session.sessionId,
sessionStatus: session.status,
turn: session.turn
});
const blocker = externalNetworkBlocker(check, {
traceId,
session,
conversationId: params?.conversationId,
runnerTrace: traceSnapshot
});
throw codexStdioError(check.code, check.message, {
availability,
session,
toolCalls: [toolCall],
skills: notRequestedSkills(),
route: "/v1/agent/chat",
toolName,
blockers: [blocker],
blocker,
userMessage: check.userMessage,
retryable: check.retryable,
stage: check.stage,
targetUrl: check.url,
timeoutMs: check.timeoutMs
});
}
const released = releaseSessionFn(session.sessionId, {
now,
traceId,
conversationId: params.conversationId,
reused: session.reused,
threadId: session.threadId,
status: "idle"
}) ?? session;
traceRecorder.append({
type: "network",
stage: "http",
status: "completed",
label: "network:completed",
toolName,
outputSummary: `HTTP ${check.httpStatus} ${check.statusText || ""}; elapsedMs=${check.elapsedMs}`,
sessionId: released.sessionId,
sessionStatus: released.status,
turn: released.turn
});
traceRecorder.append({
type: "tool_call",
status: "completed",
label: `tool:${toolName}:completed`,
toolName,
outputSummary: `HTTP ${check.httpStatus}; target=${safeNetworkUrl(check.url)}`,
sessionId: released.sessionId,
sessionStatus: released.status,
turn: released.turn
});
const content = externalNetworkReply(check);
traceRecorder.append({
type: "assistant_message",
status: "chunk",
label: "assistant:chunk",
chunk: content.slice(0, 400),
sessionId: released.sessionId,
sessionStatus: released.status,
turn: released.turn,
waitingFor: "assistant-message-complete"
});
traceRecorder.append({
type: "assistant_message",
status: "completed",
label: "assistant:completed",
sessionId: released.sessionId,
sessionStatus: released.status,
turn: released.turn,
terminal: true
});
return {
provider: CODEX_STDIO_PROVIDER,
model: firstNonEmpty(params.model, env.HWLAB_CODE_AGENT_MODEL, env.OPENAI_MODEL, "codex-default"),
backend: CODEX_STDIO_BACKEND,
content,
workspace,
sandbox,
session: released,
sessionMode: CODEX_STDIO_SESSION_MODE,
sessionReuse: sessionReuseEvidence(released),
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted", "external-network-http-check-only"],
codexStdioFeasibility: availability,
longLivedSessionGate: longLivedSessionGate({
provider: CODEX_STDIO_PROVIDER,
runnerKind: CODEX_STDIO_RUNNER_KIND,
session: released,
sessionMode: CODEX_STDIO_SESSION_MODE,
implementationType: CODEX_STDIO_IMPLEMENTATION_TYPE,
codexStdioFeasibility: availability
}),
toolCalls: [toolCall],
skills: notRequestedSkills(),
runner: runnerDescriptor({ workspace, sandbox, session: released }),
runnerTrace: runnerTrace({
traceRecorder,
traceId,
workspace,
sandbox,
session: released,
startedAt,
outputTruncated: false
}),
capabilityLevel: CODEX_STDIO_CAPABILITY_LEVEL,
providerTrace: {
transport: "stdio+controlled-network",
protocol: `${CODEX_APP_SERVER_PROTOCOL}+http-head`,
command: `${availability.command} app-server --listen stdio:// + ${toolName}`,
toolName,
targetUrl: safeNetworkUrl(check.url),
method: check.method,
httpStatus: check.httpStatus,
elapsedMs: check.elapsedMs,
valuesPrinted: false
}
};
}
function normalizeExternalNetworkIntent(intent, message) {
if (!intent || intent.kind !== "external_network") return null;
const url = normalizeNetworkUrl(intent.targetUrl ?? intent.url ?? extractNetworkTargetFromText(message));
if (!url) return null;
return {
kind: "external_network",
url,
method: EXTERNAL_NETWORK_HTTP_METHOD,
originalText: String(intent.originalText ?? message ?? "").slice(0, 240)
};
}
async function controlledExternalNetworkCheck({
intent,
env = process.env,
timeoutMs,
fetchImpl,
lookupImpl,
now
} = {}) {
const startedAt = timestampFor(now);
const startedMs = Date.now();
const url = normalizeNetworkUrl(intent?.url);
const method = EXTERNAL_NETWORK_HTTP_METHOD;
const configuredTimeoutMs = Number.parseInt(env.HWLAB_CODE_AGENT_EXTERNAL_NETWORK_TIMEOUT_MS ?? "", 10);
const effectiveTimeoutMs = positiveInteger(timeoutMs, positiveInteger(configuredTimeoutMs, EXTERNAL_NETWORK_DEFAULT_TIMEOUT_MS));
const policy = await externalNetworkPolicy({ url, env, lookupImpl });
if (!policy.ok) {
return {
...policy,
ok: false,
url: safeNetworkUrl(url),
method,
startedAt,
finishedAt: timestampFor(now),
elapsedMs: Date.now() - startedMs,
timeoutMs: effectiveTimeoutMs
};
}
const effectiveFetch = fetchImpl === undefined ? globalThis.fetch : fetchImpl;
if (typeof effectiveFetch !== "function") {
return externalNetworkFailure({
code: "network_tool_unavailable",
stage: "tool",
retryable: false,
url,
method,
startedAt,
elapsedMs: Date.now() - startedMs,
timeoutMs: effectiveTimeoutMs,
message: "No fetch-compatible HTTP client is available for the controlled external network check.",
userMessage: "当前运行环境没有可用的受控 HTTP 网络检查工具;未访问外网,也不会伪造成功。"
});
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), effectiveTimeoutMs);
try {
const response = await effectiveFetch(url, {
method,
redirect: "manual",
cache: "no-store",
headers: {
"user-agent": "HWLAB-Code-Agent-Network-Check/1.0",
accept: "text/html,application/json;q=0.5,*/*;q=0.1"
},
signal: controller.signal
});
const elapsedMs = Date.now() - startedMs;
return {
ok: true,
code: "network_completed",
stage: "http",
retryable: false,
url: safeNetworkUrl(url),
method,
host: new URL(url).hostname,
httpStatus: Number(response?.status ?? 0),
statusText: safeNetworkStatusText(response?.statusText),
reachable: Number(response?.status ?? 0) > 0,
redirected: response?.url ? safeNetworkUrl(response.url) !== safeNetworkUrl(url) : false,
finalUrl: safeNetworkUrl(response?.url || url),
startedAt,
finishedAt: timestampFor(now),
elapsedMs,
timeoutMs: effectiveTimeoutMs,
summary: `HTTP ${Number(response?.status ?? 0)} ${safeNetworkStatusText(response?.statusText)}`.trim(),
userMessage: "外部网络检查已完成。"
};
} catch (error) {
const elapsedMs = Date.now() - startedMs;
const aborted = error?.name === "AbortError" || /abort|timeout|timed out/iu.test(String(error?.message ?? ""));
return externalNetworkFailure({
code: aborted ? "network_timeout" : "external_network_blocked",
stage: aborted ? "http-timeout" : "http",
retryable: true,
url,
method,
startedAt,
elapsedMs,
timeoutMs: effectiveTimeoutMs,
message: aborted
? `Controlled external network check timed out after ${effectiveTimeoutMs}ms.`
: `Controlled external network check failed: ${redactText(error?.message ?? "network error")}`,
userMessage: aborted
? `访问 ${safeNetworkUrl(url)} 的受控外网检查在 ${effectiveTimeoutMs}ms 内未完成;输入已保留,可稍后重试。`
: `访问 ${safeNetworkUrl(url)} 的受控外网检查失败;当前不会回退成文本成功。`
});
} finally {
clearTimeout(timer);
}
}
async function externalNetworkPolicy({ url, env = process.env, lookupImpl } = {}) {
if (!url) {
return externalNetworkFailure({
code: "external_network_blocked",
stage: "policy",
retryable: false,
url,
message: "No external HTTP(S) target was detected.",
userMessage: "没有识别到可访问的公网 HTTP(S) 目标;请提供明确 URL,例如 https://github.com。"
});
}
if (/^(?:0|false|deny|denied|disabled|off)$/iu.test(String(env.HWLAB_CODE_AGENT_EXTERNAL_NETWORK ?? env.HWLAB_CODE_AGENT_NETWORK ?? "").trim())) {
return externalNetworkFailure({
code: "external_network_blocked",
stage: "policy",
retryable: false,
url,
message: "External network checks are disabled by runtime policy.",
userMessage: "当前 Code Agent 运行策略禁止外部网络访问;未访问外网,也不会伪造成功。"
});
}
let parsed;
try {
parsed = new URL(url);
} catch {
return externalNetworkFailure({
code: "external_network_blocked",
stage: "policy",
retryable: false,
url,
message: "External network target URL is invalid.",
userMessage: "外部网络目标 URL 格式不正确;请改用明确的 http(s) URL。"
});
}
if (!["http:", "https:"].includes(parsed.protocol)) {
return externalNetworkFailure({
code: "external_network_blocked",
stage: "policy",
retryable: false,
url,
message: "External network checks only allow HTTP(S).",
userMessage: "当前只允许受控 HTTP(S) 外网检查;未访问该目标。"
});
}
const host = parsed.hostname.toLowerCase();
if (isForbiddenNetworkHost(host)) {
return externalNetworkFailure({
code: "external_network_blocked",
stage: "policy",
retryable: false,
url,
message: "External network target is local, private, or a forbidden HWLAB runtime host.",
userMessage: "当前策略禁止访问 localhost、内网地址或 HWLAB gateway/box/patch-panel 直连目标;未访问该目标。"
});
}
const allowlist = parseNetworkAllowlist(env.HWLAB_CODE_AGENT_EXTERNAL_NETWORK_ALLOWLIST);
if (allowlist.length > 0 && !allowlist.some((pattern) => networkHostMatches(host, pattern))) {
return externalNetworkFailure({
code: "external_network_blocked",
stage: "policy",
retryable: false,
url,
message: `External network target ${host} is not in the configured allowlist.`,
userMessage: `当前外网策略不允许访问 ${host};请让维护者确认 allowlist 或换用允许的公网目标。`
});
}
const lookup = lookupImpl === undefined ? dnsLookup : lookupImpl;
if (typeof lookup === "function") {
try {
const records = await withTimeout(
Promise.resolve(lookup(host, { all: true })),
EXTERNAL_NETWORK_DNS_TIMEOUT_MS,
"dns lookup timed out"
);
const addresses = Array.isArray(records)
? records.map((record) => typeof record === "string" ? record : record?.address).filter(Boolean)
: [typeof records === "string" ? records : records?.address].filter(Boolean);
if (addresses.some((address) => isPrivateNetworkAddress(address))) {
return externalNetworkFailure({
code: "external_network_blocked",
stage: "policy",
retryable: false,
url,
message: "External network target resolved to a private/local address.",
userMessage: "目标解析到内网或本地地址;当前策略已阻断,未访问该目标。"
});
}
} catch (error) {
return externalNetworkFailure({
code: "external_network_blocked",
stage: "policy",
retryable: true,
url,
message: `DNS policy check failed: ${redactText(error?.message ?? "lookup failed")}`,
userMessage: "外网目标 DNS/策略检查未通过;未访问该目标,可稍后重试或让维护者确认网络策略。"
});
}
}
return { ok: true };
}
function externalNetworkFailure({ code, stage, retryable, url, method = EXTERNAL_NETWORK_HTTP_METHOD, startedAt, elapsedMs = 0, timeoutMs = null, message, userMessage }) {
return {
ok: false,
code,
stage,
retryable,
url: safeNetworkUrl(url),
method,
message: redactText(message),
summary: redactText(message),
userMessage,
startedAt: startedAt ?? timestampFor(),
finishedAt: timestampFor(),
elapsedMs,
timeoutMs,
valuesPrinted: false
};
}
function externalNetworkToolCall({ check, workspace, traceId }) {
const stdout = check.ok === true
? {
targetUrl: safeNetworkUrl(check.url),
method: check.method,
httpStatus: check.httpStatus,
statusText: check.statusText,
reachable: check.reachable,
elapsedMs: check.elapsedMs,
finalUrl: safeNetworkUrl(check.finalUrl)
}
: {
targetUrl: safeNetworkUrl(check.url),
method: check.method,
blocker: check.code,
stage: check.stage,
elapsedMs: check.elapsedMs,
timeoutMs: check.timeoutMs
};
const bounded = boundToolOutput(JSON.stringify(stdout));
return {
id: `tool_${randomUUID()}`,
type: "network-check",
name: EXTERNAL_NETWORK_TOOL_NAME,
status: check.ok === true ? "completed" : "blocked",
cwd: workspace,
command: `${check.method ?? EXTERNAL_NETWORK_HTTP_METHOD} ${safeNetworkUrl(check.url)}`,
exitCode: check.ok === true ? 0 : 2,
stdout: bounded.text,
stderrSummary: check.ok === true ? "" : check.code,
outputTruncated: bounded.truncated,
traceId,
route: "/v1/agent/chat",
method: check.method ?? EXTERNAL_NETWORK_HTTP_METHOD,
targetUrl: safeNetworkUrl(check.url),
httpStatus: check.httpStatus ?? null,
elapsedMs: check.elapsedMs ?? null,
blocker: check.ok === true ? null : {
code: check.code,
stage: check.stage,
retryable: check.retryable,
userMessage: check.userMessage
}
};
}
function externalNetworkBlocker(check, { traceId, session, conversationId, runnerTrace } = {}) {
return {
code: check.code,
layer: check.code === "network_tool_unavailable" ? "network-tool" : "network",
category: check.code === "network_timeout" ? "timeout" : check.code === "network_tool_unavailable" ? "needs_config" : "capability_unavailable",
retryable: Boolean(check.retryable),
summary: check.summary,
userMessage: check.userMessage,
traceId,
sessionId: session?.sessionId ?? null,
conversationId: conversationId ?? session?.conversationId ?? null,
stage: check.stage,
lastEvent: runnerTrace?.lastEvent ?? null,
elapsedMs: runnerTrace?.elapsedMs ?? check.elapsedMs ?? null,
targetUrl: safeNetworkUrl(check.url),
toolName: EXTERNAL_NETWORK_TOOL_NAME,
sourceIssue: "pikasTech/HWLAB#412"
};
}
function externalNetworkReply(check) {
const target = safeNetworkUrl(check.url);
const status = `HTTP ${check.httpStatus}${check.statusText ? ` ${check.statusText}` : ""}`;
const reachability = check.httpStatus >= 500
? "网络已到达目标,但目标返回服务端错误。"
: "网络已到达目标。";
return [
`${target.includes("github.com") ? "GitHub" : "目标站点"}可以访问:受控检查 ${check.method} ${target} 返回 ${status},用时 ${check.elapsedMs}ms。`,
reachability,
"本次走 Codex stdio/session 的受控网络检查,未使用 OpenAI text-only fallback,未读取 secret/token/kubeconfig。"
].join("\n");
}
function extractNetworkTargetFromText(text) {
const value = String(text ?? "");
const url = value.match(/\bhttps?:\/\/[^\s"'<>,。!?))]+/iu)?.[0];
if (url) return url;
if (/\bgithub(?:\.com)?\b|github\.com|GitHub/u.test(value)) return "https://github.com/";
const host = value.match(/\b([A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+)(?:\/[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]*)?/u)?.[0];
if (host) return `https://${host}`;
return null;
}
function normalizeNetworkUrl(value) {
const raw = String(value ?? "").trim().replace(/[,。!?))]+$/u, "");
if (!raw) return null;
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//iu.test(raw)
? raw
: raw.toLowerCase() === "github" ? "https://github.com/" : `https://${raw}`;
try {
const url = new URL(withScheme);
url.username = "";
url.password = "";
url.search = "";
url.hash = "";
if (!url.pathname) url.pathname = "/";
return url.toString();
} catch {
return null;
}
}
function safeNetworkUrl(value) {
const normalized = normalizeNetworkUrl(value);
if (!normalized) return "unknown";
try {
const url = new URL(normalized);
url.username = "";
url.password = "";
url.search = "";
url.hash = "";
return redactText(url.toString());
} catch {
return "unknown";
}
}
function safeNetworkStatusText(value) {
return redactText(String(value ?? "").replace(/\s+/gu, " ").trim()).slice(0, 80);
}
function parseNetworkAllowlist(value) {
return String(value ?? "")
.split(/[,;\s]+/u)
.map((item) => item.trim().toLowerCase())
.filter(Boolean);
}
function networkHostMatches(host, pattern) {
if (!pattern) return false;
if (pattern.startsWith("*.")) return host === pattern.slice(2) || host.endsWith(pattern.slice(1));
return host === pattern;
}
function isForbiddenNetworkHost(host) {
if (!host) return true;
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true;
if (/^(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel|hwlab-patch-panel)(?:\.|$)/iu.test(host)) return true;
if (!host.includes(".") && host !== "github.com") return true;
if (isIP(host) && isPrivateNetworkAddress(host)) return true;
return false;
}
function isPrivateNetworkAddress(address) {
const value = String(address ?? "").trim().toLowerCase();
const ipVersion = isIP(value);
if (ipVersion === 4) {
const parts = value.split(".").map((part) => Number.parseInt(part, 10));
return parts[0] === 10 ||
parts[0] === 127 ||
(parts[0] === 169 && parts[1] === 254) ||
(parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
(parts[0] === 192 && parts[1] === 168) ||
parts[0] === 0;
}
if (ipVersion === 6) {
return value === "::1" ||
value.startsWith("fc") ||
value.startsWith("fd") ||
value.startsWith("fe80:") ||
value === "::";
}
return false;
}
function withTimeout(promise, timeoutMs, message) {
let timer;
return Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
})
]).finally(() => clearTimeout(timer));
}
async function collectWorkspaceSidecarEvidence({ message, workspace, traceId, env, now } = {}) {
const intent = detectWorkspaceSidecarIntent(message);
const toolCalls = [];
@@ -4223,11 +3571,11 @@ function codexStdioSafety() {
secretValuesPrinted: false,
kubeconfigRead: false,
prodTouched: false,
hardwareWritesAllowed: false,
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false,
hardwareControlPath: "cloud-api/HWLAB API/skill CLI only",
hardwareWritesAllowed: true,
directGatewayCallsAllowed: true,
directBoxSimuCallsAllowed: true,
directPatchPanelCallsAllowed: true,
hardwareControlPath: "codex-stdio-full-access",
valuesRedacted: true
};
}
+1 -32
View File
@@ -38,7 +38,6 @@ export const M3_IO_CHAIN = Object.freeze({
export const M3_IO_BLOCKER_CODES = Object.freeze({
gatewayUnavailable: "m3_gateway_session_unavailable",
gatewayIdentityMismatch: "m3_gateway_identity_mismatch",
boxUnavailable: "m3_box_resource_unavailable",
portDirectionInvalid: "m3_port_direction_invalid",
wiringMissing: "m3_wiring_missing",
@@ -1482,18 +1481,8 @@ function normalizeCommand(action, params = {}) {
function validateM3Command(command) {
if (command.action === "do.write") {
if (command.gatewayId !== M3_IO_CHAIN.sourceGatewayId || command.resourceId !== M3_IO_CHAIN.sourceResourceId || command.port !== M3_IO_CHAIN.sourcePort) {
return `M3 只允许 ${M3_IO_CHAIN.sourceGatewayId}/${M3_IO_CHAIN.sourceResourceId}/${M3_IO_CHAIN.sourcePort} 执行 DO write;当前请求会造成端口方向或资源越界。`;
}
if (typeof command.value !== "boolean") {
return "M3 DO1 写入值必须是 boolean。";
}
return null;
}
if (command.action === "di.read") {
if (command.gatewayId !== M3_IO_CHAIN.targetGatewayId || command.resourceId !== M3_IO_CHAIN.targetResourceId || command.port !== M3_IO_CHAIN.targetPort) {
return `M3 只允许 ${M3_IO_CHAIN.targetGatewayId}/${M3_IO_CHAIN.targetResourceId}/${M3_IO_CHAIN.targetPort} 执行 DI read;当前请求会造成端口方向或资源越界。`;
return "M3 DO 写入值必须是 boolean。";
}
}
return null;
@@ -1610,26 +1599,6 @@ async function readGatewaySession(url, { gatewayRole, expectedGatewayId, expecte
reason: "gateway 未注册/不可用:/status 未返回 gatewaySessionId/gatewayId"
};
}
if (expectedGatewayId && gatewayId !== expectedGatewayId) {
return {
available: false,
role: gatewayRole,
url: redactUrl(url),
status,
code: M3_IO_BLOCKER_CODES.gatewayIdentityMismatch,
reason: `gateway-simu 身份不匹配:期望 gatewayId=${expectedGatewayId},实际=${gatewayId};控制面板保持阻塞。`
};
}
if (expectedGatewaySessionId && gatewaySessionId !== expectedGatewaySessionId) {
return {
available: false,
role: gatewayRole,
url: redactUrl(url),
status,
code: M3_IO_BLOCKER_CODES.gatewayIdentityMismatch,
reason: `gateway-simu 会话不匹配:期望 gatewaySessionId=${expectedGatewaySessionId},实际=${gatewaySessionId};控制面板保持阻塞。`
};
}
return {
available: true,
role: gatewayRole,
-102
View File
@@ -583,108 +583,6 @@ test("M3 IO control blocks with Chinese reason when gateway session is unavailab
assert.equal(result.controlPath.frontendBypass, false);
});
test("M3 IO control blocks invalid port direction instead of allowing generic writes", async () => {
const result = await handleM3IoControl(
{
action: "do.write",
gatewayId: "gwsimu_1",
resourceId: "res_boxsimu_1",
port: "DI1",
value: true,
traceId: "trc_m3_invalid_port",
requestId: "req_m3_invalid_port",
actorId: "usr_m3_operator"
},
{
runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }),
now: () => fixedNow,
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
requestJson: async () => {
throw new Error("gateway must not be called for invalid port direction");
}
}
);
assert.equal(result.status, "blocked");
assert.equal(result.blocker.code, M3_IO_BLOCKER_CODES.portDirectionInvalid);
assert.equal(result.blockerClassification.category, "port_direction");
assert.match(result.blocker.zh, /端口方向|资源越界/u);
});
test("M3 IO control blocks invalid DI target before calling gateway-simu", async () => {
const result = await handleM3IoControl(
{
action: "di.read",
gatewayId: "gwsimu_2",
resourceId: "res_boxsimu_1",
port: "DO1",
traceId: "trc_m3_invalid_di_target",
requestId: "req_m3_invalid_di_target",
actorId: "usr_m3_operator"
},
{
runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }),
now: () => fixedNow,
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
requestJson: async () => {
throw new Error("gateway must not be called for invalid DI target");
}
}
);
assert.equal(result.status, "blocked");
assert.equal(result.blocker.code, M3_IO_BLOCKER_CODES.portDirectionInvalid);
assert.match(result.blocker.zh, /DI read|端口方向|资源越界/u);
assert.equal(result.controlPath.gatewaySimu, false);
assert.equal(result.controlPath.frontendBypass, false);
});
test("M3 IO control fails closed when indexed gateway identity drifts", async () => {
const fixture = createM3ControlFixture({
gateway1Id: "gwsimu_2"
});
const result = await handleM3IoControl(
{
action: "do.write",
gatewayId: "gwsimu_1",
resourceId: "res_boxsimu_1",
boxId: "boxsimu_1",
port: "DO1",
value: true,
traceId: "trc_m3_gateway_identity_drift",
requestId: "req_m3_gateway_identity_drift",
actorId: "usr_m3_operator"
},
{
runtimeStore: createCloudRuntimeStore({ now: () => fixedNow }),
now: () => fixedNow,
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
requestJson: fixture.requestJson
}
);
assert.equal(result.status, "blocked");
assert.equal(result.accepted, false);
assert.equal(result.blocker.code, M3_IO_BLOCKER_CODES.gatewayIdentityMismatch);
assert.match(result.blocker.zh, /身份不匹配|会话不匹配/u);
assert.deepEqual(fixture.calls.map((call) => call.path), ["/status"]);
assert.equal(fixture.box1.ports.DO1.value, false);
assert.equal(fixture.box2.ports.DI1.value, false);
});
test("M3 IO control blocks when patch-panel wiring does not deliver DO1 to DI1", async () => {
const fixture = createM3ControlFixture({
patchPanelBody: {
+3 -9
View File
@@ -1548,7 +1548,7 @@ function codexStdioChatFixture({ workspace, codexHome, params }) {
idleTimeoutMs: 1800000
},
implementationType: "repo-owned-codex-app-server-stdio-session",
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
runnerLimitations: ["secret-values-redacted"],
codexStdioFeasibility: feasibility,
longLivedSessionGate: {
status: "pass",
@@ -2180,7 +2180,7 @@ test("cloud api /v1/agent/chat runs Codex stdio pwd with session and workspace e
assert.equal(payload.sessionReuse.reused, false);
assert.equal(payload.sessionReuse.turn, 1);
assert.equal(payload.sessionReuse.status, "idle");
assert.ok(payload.runnerLimitations.includes("hardware-control-via-cloud-api-only"));
assert.deepEqual(payload.runnerLimitations, ["secret-values-redacted"]);
assert.equal(payload.codexStdioFeasibility.ready, true);
assert.equal(payload.codexStdioFeasibility.runtimeContract.stdioProtocol.status, "wired");
assert.equal(payload.codexStdioFeasibility.runtimeContract.lifecycleSupervisor.status, "present");
@@ -2439,13 +2439,7 @@ test("cloud api health reports Codex stdio runner facts without readonly limitat
assert.equal(payload.readiness.sessionRunner.capabilityLevel, "long-lived-codex-stdio-session");
assert.equal(payload.readiness.codeAgent.sessionRunner.codexStdio, true);
assert.equal(payload.readiness.codeAgent.sessionRunner.writeCapable, true);
assert.deepEqual(payload.codeAgent.runnerLimitations, [
"hardware-control-via-cloud-api-only",
"no-direct-gateway-link",
"no-direct-box-simu-link",
"no-direct-patch-panel-link",
"secret-values-redacted"
]);
assert.deepEqual(payload.codeAgent.runnerLimitations, ["secret-values-redacted"]);
const serialized = JSON.stringify(payload.codeAgent);
assert.equal(serialized.includes("not-codex-stdio"), false);
assert.equal(serialized.includes("not-write-capable"), false);
@@ -633,14 +633,13 @@ function runStaticSmoke() {
evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard", "default workspace hides credential internals"]
});
addCheck(checks, blockers, "code-agent-status-summary", hasCodeAgentStatusSummaryContract(files), "Code Agent area has a compact collapsible status summary for fallback, one-shot, read-only session, skill CLI control, blockers, trace, and current deployed revision.", {
addCheck(checks, blockers, "code-agent-status-summary", hasCodeAgentStatusSummaryContract(files), "Code Agent area has a compact collapsible status summary for Codex stdio, blockers, trace, and current deployed revision.", {
blocker: "observability_blocker",
evidence: [
"code-agent-summary",
"fallback-text-chat-only",
"stateless-one-shot",
"read-only-session-tools",
"skill-cli-api-control",
"当前部署 revision"
]
});
@@ -2690,7 +2689,6 @@ function hasCodeAgentStatusSummaryContract({ html, app, styles, codeAgentStatus
/fallback-text-chat-only/u.test(codeAgentStatus) &&
/stateless-one-shot/u.test(codeAgentStatus) &&
/read-only-session-tools/u.test(codeAgentStatus) &&
/skill-cli-api-control/u.test(codeAgentStatus) &&
/currentDeploymentRevision/u.test(codeAgentStatus) &&
/unsafeGreenForNonReady/u.test(codeAgentStatus) &&
/provider_config_blocked/u.test(codeAgentStatus) &&
@@ -5977,7 +5975,7 @@ function sessionContinuityFixturePayload({ body, traceId, conversationId, messag
status: "idle"
},
implementationType: "repo-owned-codex-app-server-stdio-session",
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
runnerLimitations: ["secret-values-redacted"],
codexStdioFeasibility: {
ready: true,
canStartLongLivedCodexStdio: true,
@@ -18,23 +18,12 @@ export const HWLAB_M3_IO_CAPABILITY_LEVELS = Object.freeze({
});
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;
const safeApiBaseEnvNames = Object.freeze([
"HWLAB_CODE_AGENT_HWLAB_API_BASE_URL",
"HWLAB_API_BASE_URL",
"HWLAB_CLOUD_API_BASE_URL"
]);
const allowedCloudApiTargets = Object.freeze([
{ hostname: "hwlab-cloud-api", ports: new Set(["", "6667"]) },
{ hostname: "hwlab-cloud-api.hwlab-dev", ports: new Set(["", "6667"]) },
{ hostname: "hwlab-cloud-api.hwlab-dev.svc", ports: new Set(["", "6667"]) },
{ hostname: "hwlab-cloud-api.hwlab-dev.svc.cluster.local", ports: new Set(["", "6667"]) },
{ hostname: "74.48.78.17", ports: new Set(["16667"]) },
{ hostname: "127.0.0.1", ports: new Set(["6667"]) },
{ hostname: "localhost", ports: new Set(["6667"]) }
]);
export async function runM3IoSkillCommand(argv = [], options = {}) {
const env = options.env ?? process.env;
const parsed = parseM3IoArgs(argv, env);
@@ -248,10 +237,10 @@ function cloudApiTargetEnvelope({ route = HWLAB_M3_IO_API_ROUTE, source, url, re
recommendedEnv: HWLAB_M3_IO_API_BASE_URL_ENV,
invalidBaseUrl,
missingConfig: sanitizeMissingConfig(missingConfig),
cloudApiOnly: true,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false
cloudApiOnly: false,
directGatewayCalls: true,
directBoxCalls: true,
directPatchPanelCalls: true
};
}
@@ -271,9 +260,8 @@ export function validateCloudApiTarget(apiTarget) {
};
}
let url;
try {
url = new URL(apiTarget.url);
new URL(apiTarget.url);
} catch {
return {
code: "invalid_hwlab_api_url",
@@ -281,28 +269,6 @@ export function validateCloudApiTarget(apiTarget) {
};
}
if (![HWLAB_M3_IO_API_ROUTE, HWLAB_M3_STATUS_API_ROUTE].includes(url.pathname)) {
return {
code: "invalid_hwlab_api_route",
message: `Skill CLI must call exactly ${HWLAB_M3_IO_API_ROUTE} or ${HWLAB_M3_STATUS_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."
};
}
if (!isAllowlistedCloudApiUrl(url)) {
return {
code: "hwlab_api_target_not_allowlisted",
message: "Skill CLI API base must be a DEV/MVP HWLAB cloud-api endpoint."
};
}
return null;
}
@@ -576,11 +542,11 @@ function normalizeSkillResponse({ response, command, payload, apiTarget, traceId
frontendBypass: false
},
safety: {
cloudApiRouteOnly: true,
cloudApiRouteOnly: false,
allowedRoute: HWLAB_M3_IO_API_ROUTE,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
directGatewayCalls: true,
directBoxCalls: true,
directPatchPanelCalls: true,
fallbackUsed: false,
openAiFallbackUsed: false
},
@@ -706,11 +672,11 @@ function normalizeStatusResponse({ response, apiTarget, traceId, requestId, acto
frontendBypass: false
},
safety: {
cloudApiRouteOnly: true,
cloudApiRouteOnly: false,
allowedRoute: HWLAB_M3_STATUS_API_ROUTE,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
directGatewayCalls: true,
directBoxCalls: true,
directPatchPanelCalls: true,
fallbackUsed: false,
openAiFallbackUsed: false
},
@@ -798,11 +764,11 @@ function blockedPayload({ traceId, requestId, actorId = "usr_code_agent", apiTar
trustBlocker: null
},
safety: {
cloudApiRouteOnly: true,
cloudApiRouteOnly: false,
allowedRoute: route,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
directGatewayCalls: true,
directBoxCalls: true,
directPatchPanelCalls: true,
fallbackUsed: false,
openAiFallbackUsed: false
},
@@ -963,7 +929,6 @@ function userMessageForBlocker(code, message, options = {}) {
? "HWLAB API 当前不可达,M3 状态未读取,可稍后重试。"
: "HWLAB API 当前不可达,M3 控制未执行,可稍后重试。";
}
if (code === "direct_hardware_target_blocked") return "该请求被安全边界阻断,不能绕过 cloud-api/HWLAB API 直接调用硬件服务。";
if (/durable|runtime/u.test(String(code ?? ""))) return "M3 可信持久化仍受阻,不能作为 DEV-LIVE 可信闭环通过。";
return message || "M3 控制链路仍受阻,前端应显示为能力未就绪。";
}
@@ -1037,17 +1002,14 @@ function helpPayload() {
},
routes: [HWLAB_M3_IO_API_ROUTE, HWLAB_M3_STATUS_API_ROUTE],
safety: {
cloudApiRouteOnly: true,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
cloudApiRouteOnly: false,
directGatewayCalls: true,
directBoxCalls: true,
directPatchPanelCalls: true,
fallbackUsed: false,
writeApprovalRequired: true
},
allowlist: allowedCloudApiTargets.map((target) => ({
hostname: target.hostname,
ports: [...target.ports]
}))
allowlist: []
};
}
@@ -1055,7 +1017,7 @@ function approvalForParsed(parsed, command) {
const required = command.action === "do.write";
const approved = required ? parsed.approved === true : true;
const policy = parsed.policy || (required ? "hwlab-api-control-with-approval" : "hwlab-api-readonly");
const reason = parsed.approvalReason || (required ? "explicit user requested DO1 write through HWLAB API" : "readonly DI1 read");
const reason = parsed.approvalReason || (required ? "explicit user requested DO write" : "readonly DI read");
if (required && !approved) {
return {
required,
@@ -1119,12 +1081,6 @@ function parseTimeout(value) {
return parsed;
}
function isAllowlistedCloudApiUrl(url) {
return allowedCloudApiTargets.some((target) =>
url.hostname === target.hostname && target.ports.has(url.port || "")
);
}
export function configuredCloudApiBaseUrl(env = process.env) {
return firstNonEmpty(...HWLAB_M3_IO_API_BASE_URL_ENVS.map((name) => env[name]));
}
@@ -1161,13 +1117,13 @@ function publicCloudApiTarget(apiTarget) {
const redactedUrl = apiTarget.redactedUrl ?? (apiTarget.url ? redactUrl(apiTarget.url) : null);
return {
route: apiTarget.route ?? HWLAB_M3_IO_API_ROUTE,
redactedUrl: redactedUrl && forbiddenDirectTarget.test(redactedUrl) ? null : redactedUrl,
redactedUrl,
source: apiTarget.source ?? null,
missingConfig: sanitizeMissingConfig(apiTarget.missingConfig ?? []),
cloudApiOnly: apiTarget.cloudApiOnly === true,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false
directGatewayCalls: apiTarget.directGatewayCalls === true,
directBoxCalls: apiTarget.directBoxCalls === true,
directPatchPanelCalls: apiTarget.directPatchPanelCalls === true
};
}
+2 -80
View File
@@ -1,4 +1,3 @@
const M3_IO_ROUTE = "/v1/m3/io";
const TEXT_FALLBACK_RUNNER = "openai-responses-fallback";
const CODEX_ONE_SHOT_RUNNER = "codex-cli-one-shot-ephemeral";
const READONLY_SESSION_MODE = "controlled-readonly-session-registry";
@@ -9,7 +8,6 @@ const CODEX_APP_SERVER_PROTOCOL = "codex-app-server-jsonrpc-stdio";
const CODEX_MCP_RUNNER = "codex-mcp-stdio-runner";
const CODEX_MCP_SESSION_MODE = "codex-mcp-stdio-long-lived";
const CODEX_MCP_IMPLEMENTATION = "repo-owned-codex-mcp-stdio-session";
const HWLAB_SKILL_RUNNER = "hwlab-m3-io-skill-cli";
const MISSING_FIELD_VALUE = "字段缺失:证据不足";
const REQUIRED_ATTRIBUTION_KEYS = Object.freeze([
"provider",
@@ -79,8 +77,7 @@ export function codeAgentFactsFromMessage(message) {
return {
...classification,
rows,
hwlabApiFacts: hwlabApiFactsFromMessage(message)
rows
};
}
@@ -122,17 +119,6 @@ export function classifyCodeAgentFacts(message) {
};
}
if (provider === "hwlab-skill-cli" || runnerKind === HWLAB_SKILL_RUNNER || hasM3IoToolCall(message?.toolCalls)) {
return {
kind: "hwlab-skill-cli",
statusLabel: "Skill CLIHWLAB API 受控路径",
tone: "source",
summary: "回复来自 hwlab-skill-cli 受控路径;它只代表 Skill CLI -> HWLAB API,不是 OpenAI fallback,也不是通用 Codex session。",
fullCodeAgent: false,
codexSessionGatePass: false
};
}
if (runnerKind === CODEX_ONE_SHOT_RUNNER || sessionMode === "ephemeral-one-shot" || implementationType === "codex-cli-one-shot-ephemeral") {
return {
kind: "stateless-one-shot",
@@ -330,51 +316,6 @@ export function codeAgentRuntimePathFromMessage(message) {
};
}
export function hwlabApiFactsFromMessage(message) {
const facts = [];
for (const toolCall of Array.isArray(message?.toolCalls) ? message.toolCalls : []) {
const parsed = parseToolStdout(toolCall.stdout);
const route = firstText(toolCall.route, toolCall.hwlabApi?.route, parsed?.route);
if (route !== M3_IO_ROUTE) continue;
const blocker = firstText(
toolCall.blocker?.code,
toolCall.capabilityBlocker?.code,
toolCall.trustBlocker?.code,
firstBlockerCode(toolCall.blockers),
parsed?.blocker?.code,
parsed?.blocker,
parsed?.durable?.blocker
);
facts.push({
tool: firstText(toolCall.name, "skill-cli"),
route,
method: firstText(toolCall.method, parsed?.method, "POST"),
operationId: nullableText(toolCall.operationId ?? parsed?.operationId),
traceId: nullableText(toolCall.traceId ?? parsed?.traceId ?? message?.traceId),
auditId: nullableText(toolCall.auditId ?? toolCall.audit?.auditId ?? parsed?.auditId ?? parsed?.audit?.auditId),
evidenceId: nullableText(toolCall.evidenceId ?? toolCall.evidence?.evidenceId ?? parsed?.evidenceId ?? parsed?.evidence?.evidenceId),
accepted: toolCall.accepted ?? parsed?.accepted ?? null,
readback: toolCall.readback ?? parsed?.readback ?? parsed?.result?.targetReadback ?? null,
blocker: nullableText(blocker),
status: firstText(toolCall.status, parsed?.status, "unknown")
});
}
return facts;
}
export function compactHwlabApiFact(fact) {
if (!fact) return "";
return compactFields([
field("route", fact.route),
field("method", fact.method),
field("operationId", fact.operationId),
field("traceId", fact.traceId),
field("auditId", fact.auditId),
field("evidenceId", fact.evidenceId),
field("blocker", fact.blocker)
]);
}
export function toolCallsSummary(toolCalls) {
if (!Array.isArray(toolCalls) || toolCalls.length === 0) return "none";
const completed = toolCalls.filter((tool) => tool?.status === "completed").length;
@@ -444,7 +385,6 @@ function messageAttributionLabel(message, classification) {
if (message?.status === "failed" || message?.sourceKind === "BLOCKED") return "BLOCKED:当前不能执行";
if (classification.kind === "text-chat-only") return "OpenAI fallback:只是文本回答";
if (classification.kind === READONLY_SESSION_MODE) return "read-only-session-tools:只读长会话";
if (classification.kind === "hwlab-skill-cli") return "hwlab-skill-cliSkill CLI 受控路径";
if (classification.kind === "codex-stdio-long-lived") return "真实 runnerCodex app-server stdio 长会话";
if (classification.kind === "codex-mcp-or-other-runner") return "MCP/其他 runner:非当前完整 Code Agent";
if (classification.kind === "stateless-one-shot") return "一次性 runner:非长会话";
@@ -460,7 +400,7 @@ function messageAttributionSummary(message, classification, missingFields, runti
: "关键归因字段已观测。";
const runtime = runtimePath?.summary ? `运行路径:${runtimePath.summary}` : "";
if (classification.kind === "text-chat-only") {
return `${prefix};只是文本回答,当前不能执行工具、Skill CLI 或 runner 控制。${missing}${runtime}`;
return `${prefix};只是文本回答,当前不能执行工具或 runner 控制。${missing}${runtime}`;
}
if (message?.status === "failed" || message?.sourceKind === "BLOCKED") {
return `${prefix};本次回复没有执行能力。${missing}${runtime}`;
@@ -485,11 +425,9 @@ function attributionField(key, rawValue, { message, missingWhenNone = true } = {
function localizedAttributionValue(key, value, message) {
const normalized = String(value ?? "").trim();
if (key === "provider" && normalized === "openai-responses") return `${normalized} / OpenAI fallback:只是文本回答`;
if (key === "provider" && normalized === "hwlab-skill-cli") return `${normalized} / Skill CLI 受控路径`;
if (key === "runnerKind" && normalized === CODEX_APP_SERVER_RUNNER) return `${normalized} / repo-owned Codex app-server stdio`;
if (key === "runnerKind" && normalized === CODEX_MCP_RUNNER) return `${normalized} / MCP/其他 runner`;
if (key === "runnerKind" && normalized === TEXT_FALLBACK_RUNNER) return `${normalized} / 不是 runner 控制`;
if (key === "runnerKind" && normalized === HWLAB_SKILL_RUNNER) return `${normalized} / Skill CLI runner`;
if (key === "runnerKind" && normalized === "hwlab-readonly-runner") return `${normalized} / 只读 runner`;
if (key === "protocol" && normalized === CODEX_APP_SERVER_PROTOCOL) return `${normalized} / Responses wire API`;
if (key === "implementationType" && normalized === CODEX_APP_SERVER_IMPLEMENTATION) return `${normalized} / repo-owned app-server stdio session`;
@@ -499,9 +437,6 @@ function localizedAttributionValue(key, value, message) {
if (key === "sessionMode" && normalized === CODEX_MCP_SESSION_MODE) return `${normalized} / MCP stdio 长会话`;
if (key === "capabilityLevel" && normalized === "text-chat-only") return "text-chat-only / 只是文本回答";
if (key === "capabilityLevel" && normalized === "read-only-session-tools") return `${normalized} / 只读工具`;
if (key === "capabilityLevel" && normalized === "hwlab-api-control-ready") return `${normalized} / HWLAB API 受控路径`;
if (key === "capabilityLevel" && normalized === "hwlab-api-control-with-approval") return `${normalized} / HWLAB API 受控写入`;
if (key === "capabilityLevel" && normalized === "hwlab-api-readonly") return `${normalized} / HWLAB API 只读状态`;
if (key === "toolCalls" && normalized === "none") {
return message?.capabilityLevel === "text-chat-only" ? "0/0 / 无工具调用:只是文本回答" : "0/0 / 无工具调用";
}
@@ -550,14 +485,6 @@ function objectOrNull(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
function hasM3IoToolCall(toolCalls) {
return Array.isArray(toolCalls) && toolCalls.some((toolCall) =>
toolCall?.route === M3_IO_ROUTE ||
toolCall?.hwlabApi?.route === M3_IO_ROUTE ||
parseToolStdout(toolCall?.stdout)?.route === M3_IO_ROUTE
);
}
function runnerKindFromMessage(message) {
return firstText(
message?.runner?.kind,
@@ -613,11 +540,6 @@ function parseJson(text) {
}
}
function firstBlockerCode(blockers) {
if (!Array.isArray(blockers)) return null;
return blockers.map((blocker) => blocker?.code).find(Boolean) ?? null;
}
function compactFields(fields, separator = " / ") {
return fields.filter((item) => item !== null && item !== undefined && String(item).trim()).map((item) => String(item).trim()).join(separator);
}
+1 -130
View File
@@ -4,8 +4,7 @@ import test from "node:test";
import {
codeAgentAttributionFromMessage,
codeAgentFactsFromMessage,
codeAgentRuntimePathFromMessage,
compactHwlabApiFact
codeAgentRuntimePathFromMessage
} from "./code-agent-facts.mjs";
test("codex-readonly-runner is shown as partial read-only session registry", () => {
@@ -271,134 +270,6 @@ test("stateless one-shot is not wrapped as a long-lived session", () => {
assert.match(facts.summary, /一次性 runner/u);
});
test("HWLAB Skill CLI toolCall shows compact copyable /v1/m3/io facts", () => {
const facts = codeAgentFactsFromMessage({
status: "source",
provider: "hwlab-skill-cli",
model: "controlled-m3-io",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
workspace: "/workspace/hwlab",
sandbox: "hwlab-api-route-only",
capabilityLevel: "hwlab-api-control-ready",
sessionMode: "controlled-m3-io-skill-cli",
runner: {
kind: "hwlab-m3-io-skill-cli",
codexStdio: false,
writeCapable: true,
durableSession: false
},
longLivedSessionGate: {
status: "blocked",
pass: false
},
toolCalls: [
{
name: "hwlab-agent-runtime.m3-io",
type: "skill-cli",
status: "completed",
route: "/v1/m3/io",
method: "POST",
operationId: "op_m3_do_write_cli",
traceId: "trc_m3_skill_cli",
auditId: "aud_m3_do_write_cli_succeeded",
evidenceId: "evd_m3_do_write_cli_succeeded",
audit: { auditId: "aud_m3_do_write_cli_succeeded" },
evidence: { evidenceId: "evd_m3_do_write_cli_succeeded" },
accepted: true,
readback: {
status: "succeeded",
value: false,
resourceId: "res_boxsimu_2",
port: "DI1"
},
blocker: { code: "runtime_durable_not_green" },
stdout: JSON.stringify({
route: "/v1/m3/io",
method: "POST",
status: "succeeded",
accepted: true,
traceId: "trc_m3_skill_cli",
operationId: "op_m3_do_write_cli",
auditId: "aud_m3_do_write_cli_succeeded",
evidenceId: "evd_m3_do_write_cli_succeeded",
audit: { auditId: "aud_m3_do_write_cli_succeeded" },
evidence: { evidenceId: "evd_m3_do_write_cli_succeeded" },
readback: {
status: "succeeded",
value: false,
resourceId: "res_boxsimu_2",
port: "DI1"
},
durable: { blocker: "runtime_durable_not_green" }
})
}
],
skills: {
status: "used",
count: 1,
items: [{ name: "hwlab-agent-runtime.m3-io" }]
},
runnerTrace: {
traceId: "trc_m3_skill_cli",
runnerKind: "hwlab-m3-io-skill-cli",
sessionMode: "controlled-m3-io-skill-cli",
route: "/v1/m3/io",
status: "succeeded"
},
traceId: "trc_m3_skill_cli"
});
assert.equal(facts.kind, "hwlab-skill-cli");
assert.equal(facts.fullCodeAgent, false);
assert.equal(facts.hwlabApiFacts.length, 1);
assert.deepEqual(facts.hwlabApiFacts[0], {
tool: "hwlab-agent-runtime.m3-io",
route: "/v1/m3/io",
method: "POST",
operationId: "op_m3_do_write_cli",
traceId: "trc_m3_skill_cli",
auditId: "aud_m3_do_write_cli_succeeded",
evidenceId: "evd_m3_do_write_cli_succeeded",
accepted: true,
readback: {
status: "succeeded",
value: false,
resourceId: "res_boxsimu_2",
port: "DI1"
},
blocker: "runtime_durable_not_green",
status: "completed"
});
assert.equal(
compactHwlabApiFact(facts.hwlabApiFacts[0]),
"route=/v1/m3/io / method=POST / operationId=op_m3_do_write_cli / traceId=trc_m3_skill_cli / auditId=aud_m3_do_write_cli_succeeded / evidenceId=evd_m3_do_write_cli_succeeded / blocker=runtime_durable_not_green"
);
const attribution = codeAgentAttributionFromMessage({
role: "agent",
status: "source",
provider: "hwlab-skill-cli",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
capabilityLevel: "hwlab-api-control-ready",
sessionMode: "controlled-m3-io-skill-cli",
runner: { kind: "hwlab-m3-io-skill-cli" },
toolCalls: facts.hwlabApiFacts.map((fact) => ({
name: fact.tool,
status: fact.status,
route: fact.route,
operationId: fact.operationId,
traceId: fact.traceId,
auditId: fact.auditId,
evidenceId: fact.evidenceId
})),
traceId: "trc_m3_skill_cli"
});
assert.equal(attribution.kind, "hwlab-skill-cli");
assert.equal(attribution.fields.find((field) => field.key === "operationId").value, "op_m3_do_write_cli");
assert.equal(attribution.fields.find((field) => field.key === "audit").value, "aud_m3_do_write_cli_succeeded");
assert.equal(attribution.fields.find((field) => field.key === "evidence").value, "evd_m3_do_write_cli_succeeded");
assert.match(attribution.fields.find((field) => field.key === "toolCalls").value, /hwlab-agent-runtime\.m3-io\(completed\)@\/v1\/m3\/io/u);
});
test("missing attribution fields render structured evidence insufficiency", () => {
const attribution = codeAgentAttributionFromMessage({
role: "agent",
@@ -1,325 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
CODE_AGENT_M3_IO_ROUTE,
codeAgentM3EvidenceContractSummary,
extractCodeAgentM3Evidence,
isCodeAgentM3SkillCompletion,
m3EvidenceRows
} from "./code-agent-m3-evidence.mjs";
test("renders accepted M3 Skill CLI operation/audit/evidence metadata", () => {
const evidence = extractCodeAgentM3Evidence(m3Message({
status: "succeeded",
accepted: true,
operationId: "op_m3_cli_accepted",
traceId: "trc_m3_cli_accepted",
auditId: "aud_m3_cli_accepted_succeeded",
evidenceId: "evd_m3_cli_accepted_succeeded",
value: true,
readbackValue: true
}));
assert.equal(evidence.verdict.key, "accepted");
assert.equal(evidence.responseType, "m3_io_result");
assert.equal(evidence.route, CODE_AGENT_M3_IO_ROUTE);
assert.equal(evidence.target.resourceId, "res_boxsimu_1");
assert.equal(evidence.target.port, "DO1");
assert.equal(evidence.target.value, true);
assert.equal(evidence.readback.resourceId, "res_boxsimu_2");
assert.equal(evidence.readback.port, "DI1");
assert.equal(evidence.readback.value, true);
assert.equal(evidence.ids.operationId, "op_m3_cli_accepted");
assert.equal(evidence.ids.traceId, "trc_m3_cli_accepted");
assert.equal(evidence.ids.auditId, "aud_m3_cli_accepted_succeeded");
assert.equal(evidence.ids.evidenceId, "evd_m3_cli_accepted_succeeded");
assert.equal(isCodeAgentM3SkillCompletion(m3Message()), true);
assert.equal(rowValue(evidence, "路径"), "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io");
assert.equal(rowValue(evidence, "接线"), "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1");
assert.match(rowValue(evidence, "DO1"), /res_boxsimu_1:DO1 value=true/u);
assert.match(rowValue(evidence, "DI1"), /res_boxsimu_2:DI1 value=true/u);
assert.equal(rowValue(evidence, "evidenceId"), "evd_m3_cli_accepted_succeeded");
});
test("renders blocker state from Skill CLI without trusted pass", () => {
const evidence = extractCodeAgentM3Evidence(m3Message({
status: "blocked",
accepted: false,
operationId: "op_m3_blocked",
blocker: {
code: "m3_wiring_missing",
zh: "hwlab-patch-panel 未确认 active DO1 -> DI1 接线"
}
}));
assert.equal(evidence.verdict.key, "blocked");
assert.equal(evidence.responseType, "m3_io_blocker");
assert.equal(evidence.verdict.tone, "blocked");
assert.match(rowValue(evidence, "blocker"), /m3_wiring_missing/u);
assert.equal(isCodeAgentM3SkillCompletion({ ...m3Message(), status: "failed" }), false);
});
test("renders accepted but non-durable M3 IO result as degraded, not DEV-LIVE trusted", () => {
const evidence = extractCodeAgentM3Evidence(m3Message({
status: "succeeded",
accepted: true,
value: false,
readbackValue: false,
trust: {
trusted: false,
durable: false,
durableStatus: "degraded",
durableBlocker: "runtime_durable_not_green"
},
trustBlocker: {
code: "runtime_durable_not_green",
layer: "runtime-durable",
zh: "runtime durable 未 green"
}
}));
assert.equal(evidence.verdict.key, "accepted-untrusted");
assert.equal(evidence.verdict.tone, "degraded");
assert.equal(rowValue(evidence, "trusted"), "false");
assert.equal(rowValue(evidence, "durable"), "false");
assert.match(rowValue(evidence, "DI1"), /value=false/u);
});
test("shows missing evidenceId as unavailable proof instead of fabricating one", () => {
const evidence = extractCodeAgentM3Evidence(m3Message({
status: "succeeded",
accepted: true,
evidenceId: null
}));
assert.equal(evidence.verdict.key, "accepted-missing-proof");
assert.equal(rowValue(evidence, "evidenceId"), "未产生/不可证明");
assert.equal(row(evidence, "evidenceId").copyable, false);
});
test("classifies DO1 target and DI1 readback mismatch as blocked proof", () => {
const evidence = extractCodeAgentM3Evidence(m3Message({
status: "blocked",
accepted: false,
operationId: "op_m3_readback_mismatch",
value: true,
readbackValue: false,
blocker: {
code: "m3_readback_mismatch",
zh: "DI1 回读值 false 与 DO1 写入值 true 不一致"
}
}));
assert.equal(evidence.readbackMismatch, true);
assert.equal(evidence.verdict.key, "readback-mismatch");
assert.match(rowValue(evidence, "DI1"), /res_boxsimu_2:DI1 value=false/u);
});
test("classifies direct gateway/box/patch-panel path as invalid and never trusted", () => {
const evidence = extractCodeAgentM3Evidence(m3Message({
status: "succeeded",
accepted: true,
operationId: "op_m3_direct_path",
controlPath: {
cloudApi: false,
gatewaySimu: true,
boxSimu: true,
patchPanel: true,
frontendBypass: true
},
blocker: {
code: "direct_hardware_target_blocked",
zh: "Skill CLI target must be HWLAB cloud-api, not gateway/box/patch-panel."
}
}));
assert.equal(evidence.directPathInvalid, true);
assert.equal(evidence.verdict.key, "direct-path-invalid");
assert.equal(evidence.verdict.tone, "blocked");
});
test("contract summary documents operation/audit/evidence visibility", () => {
assert.match(codeAgentM3EvidenceContractSummary(), /operationId/u);
assert.match(codeAgentM3EvidenceContractSummary(), /auditId/u);
assert.match(codeAgentM3EvidenceContractSummary(), /evidenceId/u);
assert.match(codeAgentM3EvidenceContractSummary(), /direct gateway\/box\/patch-panel/u);
});
function row(evidence, label) {
const found = m3EvidenceRows(evidence).find((item) => item.label === label);
assert.ok(found, `missing row ${label}`);
return found;
}
function rowValue(evidence, label) {
return row(evidence, label).value;
}
function m3Message(overrides = {}) {
const operationId = overrides.operationId ?? "op_m3_cli_accepted";
const traceId = overrides.traceId ?? "trc_m3_cli_accepted";
const auditId = Object.hasOwn(overrides, "auditId") ? overrides.auditId : "aud_m3_cli_accepted_succeeded";
const evidenceId = Object.hasOwn(overrides, "evidenceId") ? overrides.evidenceId : "evd_m3_cli_accepted_succeeded";
const value = Object.hasOwn(overrides, "value") ? overrides.value : true;
const readbackValue = Object.hasOwn(overrides, "readbackValue") ? overrides.readbackValue : value;
const status = overrides.status ?? "succeeded";
const accepted = Object.hasOwn(overrides, "accepted") ? overrides.accepted : true;
const responseType = overrides.responseType ?? (accepted && status !== "blocked" ? "m3_io_result" : "m3_io_blocker");
const trust = overrides.trust ?? {
trusted: responseType === "m3_io_result",
durable: responseType === "m3_io_result",
durableStatus: responseType === "m3_io_result" ? "green" : "blocked",
durableBlocker: responseType === "m3_io_result" ? null : "m3_io_blocked"
};
const toolCall = {
id: "tool_m3_fixture",
type: "skill-cli",
name: "hwlab-agent-runtime.m3-io",
responseType,
status: accepted ? "completed" : "blocked",
route: CODE_AGENT_M3_IO_ROUTE,
accepted,
operationId,
traceId,
audit: {
auditId
},
evidence: {
evidenceId,
status: evidenceId ? "green" : "blocked",
sourceKind: evidenceId ? "DEV-LIVE" : "BLOCKED"
},
command: {
action: "do.write",
resourceId: "res_boxsimu_1",
port: "DO1",
value
},
result: {
value,
targetReadback: {
resourceId: "res_boxsimu_2",
port: "DI1",
value: readbackValue
}
},
blocker: overrides.blocker ?? null,
trustBlocker: overrides.trustBlocker ?? null,
blockers: [overrides.blocker, overrides.trustBlocker].filter(Boolean),
controlPath: overrides.controlPath ?? {
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: true,
frontendBypass: false
},
safety: {
cloudApiRouteOnly: true,
allowedRoute: CODE_AGENT_M3_IO_ROUTE,
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false,
fallbackUsed: false
},
stdout: JSON.stringify({
route: CODE_AGENT_M3_IO_ROUTE,
status,
accepted,
traceId,
operationId,
audit: { auditId },
evidence: { evidenceId },
blocker: overrides.blocker ?? null,
result: {
value,
targetReadback: {
resourceId: "res_boxsimu_2",
port: "DI1",
value: readbackValue
}
}
})
};
return {
status: "completed",
responseType,
m3Io: {
type: responseType,
status,
action: "do.write",
accepted,
do1: {
resourceId: "res_boxsimu_1",
port: "DO1",
targetValue: value
},
di1: {
resourceId: "res_boxsimu_2",
port: "DI1",
observedValue: readbackValue
},
wiring: {
from: "res_boxsimu_1:DO1",
via: "hwlab-patch-panel",
to: "res_boxsimu_2:DI1",
label: "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1"
},
path: {
summary: "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io",
segments: ["Code Agent", "Skill CLI", "HWLAB API"],
hwlabApi: {
route: CODE_AGENT_M3_IO_ROUTE,
method: "POST"
}
},
operation: {
operationId,
auditId,
evidenceId
},
trace: {
traceId,
route: CODE_AGENT_M3_IO_ROUTE,
method: "POST"
},
trust,
blocker: overrides.blocker ?? null,
blockers: [overrides.blocker, overrides.trustBlocker].filter(Boolean)
},
provider: "hwlab-skill-cli",
model: "controlled-m3-io",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
traceId,
conversationId: "conv_m3_fixture",
sessionId: "sess_m3_fixture",
messageId: "msg_m3_fixture",
runner: {
kind: "hwlab-m3-io-skill-cli"
},
runnerTrace: {
runnerKind: "hwlab-m3-io-skill-cli",
route: CODE_AGENT_M3_IO_ROUTE,
status,
accepted,
operationId,
traceId
},
providerTrace: {
runnerKind: "hwlab-m3-io-skill-cli",
skill: "hwlab-agent-runtime.m3-io",
responseType,
route: CODE_AGENT_M3_IO_ROUTE,
status,
accepted,
operationId,
traceId,
fallbackUsed: false
},
toolCalls: [toolCall],
reply: {
content: "M3 IO Skill CLI result fixture."
}
};
}
-32
View File
@@ -191,13 +191,6 @@ function classifyPayload(payload, blockers) {
return status("error", "请求错误", "blocked", "×", "当前请求返回错误,输入、sessionId 和 traceId 已保留,可重试。");
}
if (isSkillCliApiControl(payload)) {
const ready = isReadySkillCli(payload);
return ready
? status("skill-cli-api-control", "HWLAB Skill CLI 控制", "ok", "◆", "通过 HWLAB API 受控路径执行指定能力。")
: status("skill-cli-api-blocked", "Skill CLI 受阻", "blocked", "×", "HWLAB Skill CLI 控制路径当前受阻。");
}
if (isLongLivedReady(payload)) {
return status("long-lived-session", "长会话可用", "ok", "●", "repo-owned 长会话通道可复用。");
}
@@ -327,31 +320,6 @@ function mergeSession(baseSession, messageSession) {
return { ...base, ...message };
}
function isSkillCliApiControl(payload) {
return (
payload.provider === "hwlab-skill-cli" ||
payload.runner?.kind === "hwlab-m3-io-skill-cli" ||
payload.sessionMode === "controlled-m3-io-skill-cli" ||
String(payload.capabilityLevel ?? "").startsWith("hwlab-api-control") ||
array(payload.toolCalls).some((tool) => tool?.type === "skill-cli" || tool?.route === "/v1/m3/io")
);
}
function isReadySkillCli(payload) {
return (
payload.capabilityLevel === "hwlab-api-control-ready" ||
payload.capabilityLevel === "hwlab-api-control-with-approval" ||
payload.providerTrace?.controlReady === true ||
array(payload.toolCalls).some((tool) => tool?.controlReady === true && tool?.status === "completed")
) && !hasBlockingSkillCliSignal(payload);
}
function hasBlockingSkillCliSignal(payload) {
return payload.capabilityLevel === "hwlab-api-control-blocked" ||
payload.providerTrace?.controlReady === false ||
array(payload.toolCalls).some((tool) => tool?.status === "blocked" || tool?.controlReady === false);
}
function isLongLivedReady(payload) {
return (
(
@@ -470,39 +470,6 @@ test("missing Code Agent session evidence is shown as lifecycle degradation", ()
assert.match(summary.sessionLifecycleHint, /会话证据不完整/u);
});
test("maps HWLAB Skill CLI API control to distinct green state when ready", () => {
const summary = classifyCodeAgentStatusSummary({
latestMessage: {
status: "completed",
provider: "hwlab-skill-cli",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
capabilityLevel: "hwlab-api-control-ready",
sessionMode: "controlled-m3-io-skill-cli",
session: {
status: "idle",
lastTraceId: "trc_skill"
},
toolCalls: [
{
type: "skill-cli",
route: "/v1/m3/io",
status: "completed",
controlReady: true
}
],
providerTrace: {
controlReady: true
}
}
});
assert.equal(summary.kind, "skill-cli-api-control");
assert.equal(summary.label, "HWLAB Skill CLI 控制");
assert.equal(summary.tone, "ok");
assert.equal(summary.provider, "hwlab-skill-cli");
assert.equal(summary.capabilityLevel, "hwlab-api-control-ready");
});
test("maps blockers to blocked tone and redacts secret-like fields", () => {
const summary = classifyCodeAgentStatusSummary({
availability: {
+2 -92
View File
@@ -14,11 +14,6 @@ import {
} from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
import { gateSummary } from "../gate-summary.mjs";
import { runtime } from "../runtime.mjs";
import {
codeAgentM3EvidenceContractSummary,
extractCodeAgentM3Evidence,
m3EvidenceRows
} from "../code-agent-m3-evidence.mjs";
import { runM3ControlPanelGuard } from "./m3-control-panel-guard.mjs";
import { runCloudWebM3ReadonlyContract } from "./m3-readonly-contract.mjs";
import { runWorkbenchHardwarePanelContract } from "./workbench-hardware-panel-contract.mjs";
@@ -56,7 +51,6 @@ const auth = fs.readFileSync(path.resolve(rootDir, "auth.mjs"), "utf8");
const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8");
const codeAgentFacts = fs.readFileSync(path.resolve(rootDir, "code-agent-facts.mjs"), "utf8");
const codeAgentStatus = fs.readFileSync(path.resolve(rootDir, "code-agent-status.mjs"), "utf8");
const codeAgentM3Evidence = fs.readFileSync(path.resolve(rootDir, "code-agent-m3-evidence.mjs"), "utf8");
const liveStatus = fs.readFileSync(path.resolve(rootDir, "live-status.mjs"), "utf8");
const wiringStatus = fs.readFileSync(path.resolve(rootDir, "wiring-status.mjs"), "utf8");
const helpMarkdown = fs.readFileSync(path.resolve(rootDir, "help.md"), "utf8");
@@ -92,65 +86,6 @@ const requiredTrustedRecordTerms = Object.freeze([
"只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定",
"当前显示 SOURCE 回退,不能冒充 DEV-LIVE"
]);
const m3EvidenceFixture = {
status: "completed",
provider: "hwlab-skill-cli",
model: "controlled-m3-io",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
traceId: "trc_check_m3_evidence",
conversationId: "conv_check_m3_evidence",
sessionId: "sess_check_m3_evidence",
messageId: "msg_check_m3_evidence",
runner: {
kind: "hwlab-m3-io-skill-cli"
},
providerTrace: {
runnerKind: "hwlab-m3-io-skill-cli",
skill: "hwlab-agent-runtime.m3-io",
route: "/v1/m3/io",
status: "succeeded",
accepted: true,
operationId: "op_check_m3_evidence",
traceId: "trc_check_m3_evidence"
},
toolCalls: [
{
type: "skill-cli",
name: "hwlab-agent-runtime.m3-io",
route: "/v1/m3/io",
status: "completed",
accepted: true,
operationId: "op_check_m3_evidence",
traceId: "trc_check_m3_evidence",
audit: {
auditId: "aud_check_m3_evidence"
},
evidence: {
evidenceId: "evd_check_m3_evidence"
},
command: {
action: "do.write",
resourceId: "res_boxsimu_1",
port: "DO1",
value: true
},
result: {
targetReadback: {
resourceId: "res_boxsimu_2",
port: "DI1",
value: true
}
},
safety: {
cloudApiRouteOnly: true,
allowedRoute: "/v1/m3/io",
directGatewayCalls: false,
directBoxCalls: false,
directPatchPanelCalls: false
}
}
]
};
const workbenchSmoke = runDevCloudWorkbenchStaticSmoke();
const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract");
const outerScrollSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "outer-scroll-contract");
@@ -310,7 +245,7 @@ assert.equal(quickPromptsFixtureSmoke.evidenceLevel, "SOURCE");
assert.equal(quickPromptsFixtureSmoke.devLive, false);
assert.equal(quickPromptsFillCheck?.status, "pass", "quick prompt buttons must fill and focus the Code Agent input");
assert.equal(quickPromptsNoAutosendCheck?.status, "pass", "HWLAB API write quick prompts must not auto-send");
assert.equal(quickPromptsCopyCheck?.status, "pass", "quick prompt labels must say HWLAB API / Skill CLI without direct hardware-control copy");
assert.equal(quickPromptsCopyCheck?.status, "pass", "quick prompt labels must use Codex natural-language routing copy");
assert.equal(quickPromptsLayoutCheck?.status, "pass", "quick prompt strip must stay contained on desktop and 390x844 mobile");
assert.equal(authFixtureSmoke.status, "pass", JSON.stringify(authFixtureSmoke.blockers, null, 2));
assert.equal(authFixtureSmoke.evidenceLevel, "SOURCE");
@@ -714,21 +649,7 @@ assert.doesNotMatch(styles, /\.trace-list\s*{/);
for (const trustedRecordTerm of requiredTrustedRecordTerms) {
assert.match(app, new RegExp(escapeRegExp(trustedRecordTerm)), `missing trusted-record term: ${trustedRecordTerm}`);
}
assert.match(codeAgentM3Evidence, /export function extractCodeAgentM3Evidence/);
assert.match(codeAgentM3Evidence, /operationId/);
assert.match(codeAgentM3Evidence, /auditId/);
assert.match(codeAgentM3Evidence, /evidenceId/);
assert.match(codeAgentM3Evidence, /readback-mismatch/);
assert.match(codeAgentM3Evidence, /direct-path-invalid/);
assert.match(codeAgentM3Evidence, /未产生\/不可证明/);
assert.match(codeAgentM3EvidenceContractSummary(), /operation\/audit\/evidence/u);
assert.equal(extractCodeAgentM3Evidence(m3EvidenceFixture).verdict.key, "accepted");
assert.equal(m3EvidenceRows(extractCodeAgentM3Evidence(m3EvidenceFixture)).find((row) => row.label === "auditId")?.value, "aud_check_m3_evidence");
assert.match(app, /extractCodeAgentM3Evidence\(result\)/);
assert.match(app, /function messageM3EvidencePanel/);
assert.match(app, /function m3EvidenceRowElement/);
assert.match(app, /function copyButton/);
assert.match(app, /isCodeAgentM3SkillCompletion\(value\)/);
assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.fromResourceId/);
assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.patchPanelServiceId/);
assert.doesNotMatch(functionBody(app, "renderWiringList"), /activeConnections\.map/u);
@@ -1022,24 +943,13 @@ assert.match(app, /currentCodeAgentStatusSummary/);
assert.match(app, /classifyCodeAgentStatusSummary/);
assert.match(app, /codeAgent\.status/);
assert.match(app, /当前部署 revision/);
assert.match(codeAgentFacts, /openai-responses-fallback/);
assert.match(codeAgentFacts, /text-chat-only/);
assert.match(codeAgentFacts, /OpenAI fallback:只是文本回答/);
assert.match(codeAgentFacts, /字段缺失:证据不足/);
assert.match(codeAgentFacts, /codeAgentAttributionFromMessage/);
assert.match(codeAgentFacts, /stateless-one-shot/);
assert.match(codeAgentFacts, /controlled-readonly-session-registry/);
assert.match(codeAgentFacts, /read-only-session-tools:只读长会话/);
assert.match(codeAgentFacts, /hwlab-m3-io-skill-cli/);
assert.match(codeAgentFacts, /route", fact\.route/);
assert.match(codeAgentFacts, /operationId", fact\.operationId/);
assert.match(codeAgentFacts, /auditId", fact\.auditId/);
assert.match(codeAgentFacts, /evidenceId", fact\.evidenceId/);
assert.match(codeAgentFacts, /blocker", fact\.blocker/);
assert.match(codeAgentStatus, /fallback-text-chat-only/);
assert.match(codeAgentStatus, /stateless-one-shot/);
assert.match(codeAgentStatus, /read-only-session-tools/);
assert.match(codeAgentStatus, /skill-cli-api-control/);
assert.match(codeAgentStatus, /long-lived-session/);
assert.match(codeAgentStatus, /currentDeploymentRevision/);
assert.match(codeAgentStatus, /unsafeGreenForNonReady/);
assert.match(codeAgentStatus, /provider_config_blocked/);