fix: route code agent chat through codex

This commit is contained in:
lyon
2026-05-25 20:14:05 +08:00
parent a713b73d63
commit 303ff8fafd
9 changed files with 279 additions and 4504 deletions
+36 -37
View File
@@ -12,7 +12,7 @@ Secret 或 token。
同源聊天入口的真实回复证据。
- `internal/cloud/code-agent-chat.mjs``/v1/agent/chat` 的后端处理入口。
- `scripts/code-agent-chat-smoke.mjs` 是 Code Agent chat schema 与 readiness
gate
合同检查
- `scripts/dev-cloud-workbench-smoke.mjs --static` 只验证 Workbench 源码合同和
`/v1/agent/chat` 前端接线;它不是 DEV-LIVE 回复证明。
@@ -41,7 +41,7 @@ report、issue、PR 或截图。
| 观测结果 | readiness |
| --- | --- |
| `status: "failed"``error.code: "provider_unavailable"`,且 `error.missingEnv` 包含 `OPENAI_API_KEY` | `BLOCKED/credential`;provider 凭证缺失,不能标真实回复通过。 |
| `provider: "codex-readonly-runner"` `sessionMode: "controlled-readonly-session-registry"``capabilityLevel: "read-only-session-tools"``session.status``idle/ready/busy``session.idleTimeoutMs``session.lastTraceId` 存在、`session.longLivedSession: true``longLivedSessionGate.status: "blocked"``runnerLimitations` 包含 `not-codex-stdio` / `not-write-capable` / `process-local-session-registry` | 可标为 #317 read-only long-lived session registry pass;不能关闭 full Codex stdio/session capability blocker。 |
| `provider: "codex-readonly-runner"` `sessionMode: "controlled-readonly-session-registry"` | 历史只读状态,只能作为 `BLOCKED/not-codex-stdio` 诊断;不能满足当前自然语言单一路由或 DEV-LIVE reply pass。 |
| `codexStdioFeasibility.status: "blocked"`,或 blocker 包含 `codex_cli_binary_missing``codex_cli_not_executable``codex_cli_native_dependency_missing``runner_lifecycle_missing``stdio_protocol_not_wired``workspace_mount_missing``workspace_write_boundary_blocked``codex_home_missing``codex_home_write_blocked``provider_token_boundary` | 真实 Codex stdio / 等价 long-lived runner 未具备;必须按 blocker 处理,不能表述为完整 Codex session。 |
| `status: "completed"`,但来自 mock、fixture、本地 stub、source-only smoke、浏览器本地回显或人工拼接 | 不是 DEV-LIVE reply pass。 |
| 真实 DEV `POST /v1/agent/chat` 返回 `status: "completed"`,且 `reply.content` 是非空 assistant 回复 | 可标 DEV-LIVE reply pass。 |
@@ -50,45 +50,32 @@ report、issue、PR 或截图。
只有“真实 DEV 路由 + `completed` + 非空 assistant reply”能作为 DEV-LIVE 回复通过依据。
不得把 mock、fixture、本地 echo、source report、静态检查或前端状态当作通过。
## Runner 能力边界
## 自然语言单一路由
`/v1/agent/chat` 可以先落地受控只读能力,但必须诚实区分:
`/v1/agent/chat` 的自然语言请求唯一执行路径是 repo-owned Codex stdio long-lived
session。cloud-api 不再把自然语言预分类到 M3 Skill CLI、`/v1/m3/io`
`external.network.check``session_context``security.hardware-boundary`
`hardware.invoke.shell` shortcut 或 OpenAI text fallback。
- `controlled-readonly-session-registry`:由 cloud-api 进程内 registry 保存
`conversationId/sessionId` 映射、`status`、workspace、sandbox、`createdAt``updatedAt`
`idleTimeoutMs``lastTraceId``turn` 计数与只读工具 trace。它可以覆盖 `pwd`
`skills.discover``ls``rg --files` 和 bounded `cat`,输出必须限长和脱敏。该模式是
read-only long-lived session,但 `longLivedSessionGate` 必须保持 `blocked`,直到 Codex
stdio 或等价 full Code Agent 协议通道真实接通。
- 该模式必须同时标记 `not-codex-stdio``not-write-capable``process-local-session-registry`。它不是
Codex stdio / workspace-write session,不提供写文件、任意 shell、硬件写、Secret/kubeconfig/DB URL
读取,也不证明 M3/M4/M5 trusted green。
- OpenAI Responses fallback 只能标记为 `openai-responses-fallback` /
`text-chat-only`,不得满足 Codex runner capability gate。默认路径不得用 fallback
冒充完整 Code Agent;仅明确允许 fallback 时才可作为普通文本备用通道返回。
- 已登记 PC gateway `shell.exec` capability 是受控硬件能力例外:当请求能从显式
`projectId/gatewaySessionId/resourceId/capabilityId` 或唯一 DEV MVP topology 解析时,
`/v1/agent/chat` 可以选择 `hwlab-hardware-capability` runner,经 cloud-api 进程内
JSON-RPC helper 调用 `hardware.invoke.shell`。该 helper 必须使用冻结的
`hwlab-cloud-api` service id 常量,返回 payload 需包含 `toolCalls[]``operationId`
`dispatchStatus``exitCode`、stdout/stderr 摘要、`runnerTrace.eventLabels`
`capabilityLevel`。拓扑缺失、不唯一、gateway offline、session/capability missing、
dispatch failed 或 JSON-RPC meta/serviceId 无效时必须返回结构化 blocker,不能转入
Codex stdio 或 OpenAI 文本 fallback 冒充成功。
- `security.hardware-boundary` 仍然阻断直接 gateway/box-simu/patch-panel URL、`/invoke`
`/sync/tick` 和泛化硬件写绕过;受控 `hardware.invoke.shell` capability route 只允许
已登记、可解析的 cloud-api 调用,不开放任意 shell。JSON-RPC/REST bridge 拒绝未知
`serviceId` 时,错误原因应可见为 `unknown serviceId ...` 或等价结构化 reason,且不得泄露
Secret/token。
自然语言里即使出现 M3、DO/DI、DAP、PWM、gateway、box-simu、patch-panel、Keil、
serial-monitor、Windows skill、串口、下载、烧录、启动日志等词,也必须把完整请求交给
Codex stdio turn。Codex turn 自己根据仓库、skill 文档和可用工具决定调用 repo wrapper
Windows skill CLI、项目脚本或其他真实可达路径;cloud-api 只负责 session 生命周期、trace、
result 轮询和 schema 化返回。
当前 DEV/runtime 若要升级为完整 #275 runner,至少需要 repo-owned 的 Codex CLI/stdio 或等价
runner 二进制/协议适配、session supervisor 生命周期、workspace mount 与 sandbox 合同、token/Secret
注入边界、trace/cancel/reap 机制,以及与 cloud-api/workbench 的持久 session 映射。缺任一项时,必须
`codexStdioFeasibility` 中报告 blocker
如果 Codex stdio 不具备运行条件,`/v1/agent/chat` 只能返回 Codex stdio readiness
blocker,不能降级到 M3 Skill CLI、受控硬件 shortcut、外网专用检查或普通 OpenAI 文本回复。
显式 `/v1/m3/io` 控制面可以作为独立 API 或 UI 控制面继续存在,但聊天自然语言不得自动路由
到该 API,也不得保留要求自然语言先满足 M3 白名单的源码检查或测试
持久 session 是默认合同:同一个 `conversationId/sessionId` 必须映射到 repo-owned Codex
thread 和固定 workspace,刷新前端、重新打开页面或短连接 result 轮询不得创建新的短期 runner。
除非 Pod 重建或 Codex supervisor 明确重启,workspace、thread/session 绑定和可见 trace 应持续
存在。
## PC Gateway Windows Skill 调用
Code Agent 通过已登记 PC gateway 执行 Windows 侧命令时,必须让 Codex turn 自己调用仓库 wrapper,不能由 cloud-api 字符串匹配短路到 gateway,也不能直接访问 gateway URL
Code Agent 通过已登记 PC gateway 执行 Windows 侧命令时,必须让 Codex turn 自己调用仓库 wrapper,不能由 cloud-api 字符串匹配短路到 gateway
```sh
node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms <ms> --powershell-stdin <<'PS1'
@@ -102,7 +89,7 @@ Workbench 会把“Gateway 命令超时”控件的毫秒值随 `/v1/agent/chat`
调大 timeout 不能替代正确的长任务控制语义。Gateway poll loop 必须支持后台 in-flight 执行,长 Keil/UV4 命令运行期间仍能处理短 `job-status`、state/log 读取和健康探测;如果 trace 出现 `shellExecuted=false` 的 dispatch timeout,优先检查 gateway 是否队头阻塞或离线,而不是把所有 wrapper 调用改成长等待。
Workbench trace 对已知 JSON-RPC gateway 响应应按普通 tool call 展示:前端首行用中性 `tool gateway.shell status=<status> op=<operationId> exit=<exitCode> s=<duration>`,正文展示 request、gateway/resource/capability、dispatch、command、audit/evidence 以及有界 stdout/stderr。后端 capability 仍可记录 `hardware.invoke.shell` 路由事实,但 Cloud Web 源码不得把它暴露成可点击或可误解的直接硬件写入口。不要把整段 JSON 原样刷屏;复制/下载完整 trace 仍保留原始 JSON。
Workbench trace 对已知 JSON-RPC gateway 响应应按普通 tool call 展示:前端首行用中性 `tool gateway.shell status=<status> op=<operationId> exit=<exitCode> s=<duration>`,正文展示 request、gateway/resource/capability、dispatch、command、audit/evidence 以及有界 stdout/stderr。不要把整段 JSON 原样刷屏;复制/下载完整 trace 仍保留原始 JSON。
## 短连接 result 轮询
@@ -141,7 +128,19 @@ py -3 keil-cli.py job-status <job_id>
对 build/download 这类长任务,Code Agent 应优先使用 skill 自带的异步 job 语义:启动命令用短 wrapper timeout 拿到 job id 或明确的启动失败,再用短 `job-status`、state 文件和日志读取轮询进展。除非用户明确要求同步等待并设置了足够大的 Gateway 命令超时,不要通过 gateway 执行 `--wait` 长轮询;同步等待会占用一个 in-flight 槽位,旧 gateway 还会造成队头阻塞。
## Smoke Gate
串口启动日志请求必须优先使用 Windows 侧 `serial-monitor` skill,而不是在 cloud-api 新增串口专用 route
```sh
cd C:\Users\liang\.agents\skills\serial-monitor
npm run cli -- server status
npm run cli -- server start
npm run cli -- monitor start -p <COMx> -b <baud>
npm run cli -- fetch --session-only --no-dedup
```
Keil 下载后的启动日志抓取应和 build/download 共用同一个 Codex stdio session 与 gateway wrapper trace。71-FREQ 类项目的串口参数以 Windows 侧 `serial-monitor\SKILL.md` 和实时设备枚举为准;需要轮询时用短 wrapper 调用读取 session/state/log,而不是新增聊天层白名单或 blocker。
## Smoke Checks
本地合同检查:
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -93,14 +93,14 @@ const CODEX_STDIO_BOUNDARY_INSTRUCTIONS = [
"You are the HWLAB Cloud Workbench Code Agent.",
"Use the provided workspace and repo-owned Codex stdio session only.",
"Do not read or print secrets, tokens, kubeconfig files, DB URLs, private keys, or raw environment values.",
"Do not call gateway, box-simu, or patch-panel directly.",
"Hardware control requests must go through cloud-api/HWLAB API/skill CLI controlled paths.",
"For hardware, gateway, box-simu, patch-panel, DAP, PWM, Keil, serial, and Windows skill requests, execute the requested work through the repo-owned Codex stdio session with the available wrapper, skill CLI, or project tool that actually reaches the target.",
"For registered PC gateway Windows command or skill requests, invoke the repo-owned wrapper with the Codex exec tool: node /app/tools/hwlab-gateway-shell.mjs --json --timeout-ms <gatewayShellTimeoutMs> --powershell-stdin. Put scripts on stdin and set working directory with wrapper --cwd or PowerShell Set-Location -LiteralPath; do not hand-build compound cmd /c, cd &&, pipes, or nested quotes.",
"For PowerShell through the PC gateway, do not hand-escape cmd pipes or quotes; use a single-quoted heredoc with --powershell-stdin. The wrapper sends powershell.exe -EncodedCommand and injects UTF-8 helper functions for Unicode-safe execution.",
"For Windows text files and skill manifests, avoid raw Get-Content or Select-String objects in JSON. Use Read-HwlabText, Select-HwlabText, and ConvertTo-HwlabJson from the wrapper prologue, or explicitly project plain scalar fields before ConvertTo-Json.",
"For Windows filesystem inventory, start with one small bounded PowerShell stdin script: use -LiteralPath, Select-Object -First, ConvertTo-HwlabJson, and keep stdout under about 12 KB. Do not dump full directory JSON and then retry.",
"For Windows-side skills under C:\\Users\\liang\\.agents\\skills\\, read the skill manifest when needed and call its CLI from a PowerShell stdin script using explicit paths or argument arrays. Do not reimplement skill logic, and do not use shell working-directory tricks like cd &&.",
"For F:\\work or F:\\work\\ConStart discovery, first list top-level project markers and *.uvprojx candidates with bounded output. For Keil build/program requests, use the Windows skill CLI at C:\\Users\\liang\\.agents\\skills\\keil with py -3 keil-cli.py from the generic PowerShell stdin wrapper path; do not reimplement Keil build logic.",
"For boot logs and UART capture, use the Windows skill CLI at C:\\Users\\liang\\.agents\\skills\\serial-monitor with npm run cli -- server status/start, monitor start, and fetch from the generic PowerShell stdin wrapper path; for 71-FREQ prefer the skill-documented baud rate unless live evidence says otherwise.",
"For long Keil build/program/download jobs, prefer the skill CLI async job flow: start the job with a short bounded wrapper call, then poll job-status, state files, or logs with short wrapper calls. Do not use gateway --wait long polling unless the user explicitly asks for synchronous waiting and sets a sufficient gateway timeout.",
"If a Windows gateway command reaches the gateway but fails due script syntax or output size, simplify once and report the failed operationId plus the corrected bounded command evidence. Do not spend multiple turns on exploratory rewrites.",
"Use the Windows-side skill CLIs under C:\\Users\\liang\\.agents\\skills\\ from that cmd command when they exist; do not reimplement those tools in the prompt.",
File diff suppressed because it is too large Load Diff
+2 -111
View File
@@ -7,7 +7,6 @@ import {
classifyLiveDeploymentIdentity,
classifyLiveWebAssetIdentity,
parseSmokeArgs,
runDevCloudWorkbenchExternalNetworkFixtureSmoke,
runDevCloudWorkbenchLayoutSmoke,
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
runDevCloudWorkbenchSessionContinuityFixtureSmoke,
@@ -257,8 +256,8 @@ test("source/default smoke covers Code Agent quick prompt fill-only contract", (
const report = runDevCloudWorkbenchStaticSmoke();
const check = report.checks.find((item) => item.id === "code-agent-quick-prompts-contract");
assert.equal(check?.status, "pass");
assert.equal(check.evidence.includes("HWLAB APIDO1=true 后读 DI1"), true);
assert.equal(check.evidence.includes("HWLAB APIDO1=false 复核 DI1"), true);
assert.equal(check.evidence.includes("CodexDO1=true 后读 DI1"), true);
assert.equal(check.evidence.includes("CodexDO1=false 复核 DI1"), true);
assert.equal(check.evidence.some((item) => /我点击发送后才确认执行这次写入请求/u.test(item)), true);
});
@@ -493,87 +492,6 @@ test("Code Agent browser classifier distinguishes runner busy, session blocked,
assert.equal(apiError.category, "api_error");
});
test("Code Agent browser classifier consumes structured blocker taxonomy", () => {
const base = {
status: "failed",
provider: "hwlab-skill-cli",
model: "controlled-m3-io",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
traceId: "trc_structured_blocker"
};
for (const [code, expected] of [
["skill_cli_api_base_missing", ["needs-config", "needs_config"]],
["hwlab_api_unavailable", ["retryable", "retryable"]],
["m3_readiness_blocked", ["capability-unavailable", "capability_unavailable"]],
["text_chat_only_fallback", ["text-chat-only-fallback", "fallback"]]
]) {
const [blocker, category] = expected;
const classification = classifyCodeAgentBrowserJourney({
responseSummary: sanitizeAgentChatBody({
...base,
error: {
code,
layer: code === "hwlab_api_unavailable" ? "hwlab-api" : "m3-readiness",
category,
blocker: {
code,
layer: code === "hwlab_api_unavailable" ? "hwlab-api" : "m3-readiness",
category,
retryable: category === "retryable",
userMessage: "结构化中文提示"
},
retryable: category === "retryable",
userMessage: "结构化中文提示",
route: "/v1/m3/io",
toolName: "hwlab-agent-runtime.m3-io"
}
}, { httpStatus: 200 }),
httpOk: true,
httpStatus: 200,
ui: {
agentChatStatus: "服务受阻",
completedMessageVisible: false,
failedMessageVisible: true
}
});
assert.equal(classification.blocker, blocker);
assert.equal(classification.category, category);
}
});
test("Code Agent browser classifier consumes completed blocked M3 payload blocker", () => {
const classification = classifyCodeAgentBrowserJourney({
responseSummary: sanitizeAgentChatBody({
status: "completed",
provider: "hwlab-skill-cli",
model: "controlled-m3-io",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
traceId: "trc_completed_m3_blocked",
capabilityLevel: "hwlab-api-control-blocked",
reply: {
content: "M3 IO Skill CLI result blocked."
},
blocker: {
code: "m3_readiness_blocked",
layer: "m3-readiness",
category: "capability_unavailable",
retryable: false,
userMessage: "M3 控制链路尚未就绪。"
}
}, { httpStatus: 200 }),
httpOk: true,
httpStatus: 200,
ui: {
agentChatStatus: "能力未开放",
completedMessageVisible: false,
failedMessageVisible: true
}
});
assert.equal(classification.status, "blocked");
assert.equal(classification.blocker, "capability-unavailable");
assert.equal(classification.category, "capability_unavailable");
});
test("Code Agent readiness classifier blocks completed payloads over any non-2xx HTTP status", () => {
const completedPayload = {
conversationId: "cnv_completed_non_2xx",
@@ -723,33 +641,6 @@ test("local slow structured blocker fixture preserves trace, pending state, and
assert.equal(blockerCheck.observations.prompt.classification.blocker, "runner-blocked");
});
test("local external network fixture renders GitHub blocker with trace and without main evidence noise", async () => {
const report = await runDevCloudWorkbenchExternalNetworkFixtureSmoke({
responseDelayMs: 5200
});
if (report.status === "skip") {
assert.match(report.summary, /Playwright is unavailable/u);
return;
}
assert.equal(report.status, "pass", JSON.stringify(report.blockers, null, 2));
assert.equal(report.evidenceLevel, "SOURCE");
assert.equal(report.devLive, false);
const blockerCheck = report.checks.find((check) => check.id === "local-agent-fixture-external-network-blocker");
assert.equal(blockerCheck?.status, "pass");
assert.equal(blockerCheck.observations.ui.failedMessageVisible, true);
assert.equal(blockerCheck.observations.ui.failedMessageHasTrace, true);
assert.equal(blockerCheck.observations.ui.retryInputPreserved, true);
assert.equal(blockerCheck.observations.ui.retryInputValue, "访问一下github看看");
assert.match(blockerCheck.observations.ui.traceText, /network started/u);
assert.match(blockerCheck.observations.ui.traceText, /tool:external\.network\.check:blocked/u);
assert.equal(blockerCheck.observations.ui.noMainAttributionNoise, true);
assert.equal(blockerCheck.observations.prompt.response.error.code, "external_network_blocked");
assert.equal(blockerCheck.observations.prompt.response.toolCalls.some((tool) => tool.name === "external.network.check" && tool.status === "blocked"), true);
assert.equal(blockerCheck.observations.prompt.classification.blocker, "runner-blocked");
});
test("local session continuity fixture reuses Code Agent context and retries with degraded-copy guard", async () => {
const report = await runDevCloudWorkbenchSessionContinuityFixtureSmoke();
if (report.status === "skip") {
+23 -173
View File
@@ -46,26 +46,26 @@ const codeAgentExternalNetworkPrompt = Object.freeze({
const codeAgentQuickPromptFixtures = Object.freeze([
{
id: "pwd",
label: "Skill CLIpwd",
prompt: "通过 Skill CLI 执行 pwd,列出当前工作目录。",
label: "Codexpwd",
prompt: "交给 Codex 执行 pwd,列出当前工作目录。",
writable: false
},
{
id: "skills",
label: "Skill CLI:列出 skill",
prompt: "通过 Skill CLI 列出你能使用的所有 skill。",
label: "Codex:列出 skill",
prompt: "交给 Codex 列出你能使用的所有 skill。",
writable: false
},
{
id: "do1-true-di1",
label: "HWLAB APIDO1=true 后读 DI1",
prompt: "通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
label: "CodexDO1=true 后读 DI1",
prompt: "交给 Codex 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
writable: true
},
{
id: "do1-false-di1",
label: "HWLAB APIDO1=false 复核 DI1",
prompt: "通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
label: "CodexDO1=false 复核 DI1",
prompt: "交给 Codex 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
writable: true
}
]);
@@ -326,10 +326,7 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
"echo/mock/stub",
"untrusted_completion",
"SOURCE fixture",
"Code Agent SOURCE 回复",
"hwlab-skill-cli",
"hwlab-m3-io-skill-cli",
"message-m3-evidence"
"Code Agent SOURCE 回复"
]);
const blockedCodeAgentUiLabels = Object.freeze([
@@ -616,7 +613,7 @@ function runStaticSmoke() {
evidence: ["command-send", "agent-chat-status", "/v1/agent/chat", "conversationId/messageId/status/timestamps/error.message"]
});
addCheck(checks, blockers, "code-agent-quick-prompts-contract", hasCodeAgentQuickPromptContract(files), "Code Agent quick prompts fill the input with Skill CLI / HWLAB API examples while hardware-write prompts require explicit send confirmation.", {
addCheck(checks, blockers, "code-agent-quick-prompts-contract", hasCodeAgentQuickPromptContract(files), "Code Agent quick prompts fill the input with Codex examples while hardware-write prompts require explicit send confirmation.", {
blocker: "safety_blocker",
evidence: codeAgentQuickPromptFixtures.flatMap((prompt) => [prompt.id, prompt.label, prompt.prompt])
});
@@ -2498,14 +2495,14 @@ function hasCodeAgentQuickPromptContract({ html, app, styles }) {
/我点击发送后才确认执行这次写入请求/u.test(tag)
);
});
const promptCopyUsesApiBoundary = codeAgentQuickPromptFixtures.every((prompt) =>
/通过 (?:Skill CLI|HWLAB API \/ Skill CLI)/u.test(prompt.prompt) &&
!/(?:\bgateway\b|\bbox\b|\bpatch-panel\b|PROD|真实硬件直控)/iu.test(prompt.prompt)
const promptCopyUsesCodexRoute = codeAgentQuickPromptFixtures.every((prompt) =>
/交给 Codex/u.test(prompt.prompt) &&
!/PROD/iu.test(prompt.prompt)
);
return (
labels &&
writeButtonsRequireExplicitSend &&
promptCopyUsesApiBoundary &&
promptCopyUsesCodexRoute &&
/data-agent-quick-prompt/u.test(app) &&
/function\s+fillAgentQuickPrompt\s*\(/u.test(app) &&
/el\.commandInput\.value = prompt/u.test(app) &&
@@ -4623,15 +4620,6 @@ export async function runDevCloudWorkbenchSlowBlockerFixtureSmoke(options = {})
});
}
export async function runDevCloudWorkbenchExternalNetworkFixtureSmoke(options = {}) {
return runLocalAgentFixtureSmoke({
mode: "local-agent-external-network-fixture-browser",
responseDelayMs: options.responseDelayMs ?? localAgentFixtureDelayMs,
agentFixtureMode: "external-network-blocker",
expectTimeout: false
});
}
export async function runDevCloudWorkbenchSessionContinuityFixtureSmoke(options = {}) {
let chromium;
try {
@@ -4891,7 +4879,7 @@ export async function runDevCloudWorkbenchQuickPromptsFixtureSmoke() {
{
id: "quick-prompts-copy-boundary",
status: promptResults.every((result) => result.copyBoundaryOk) ? "pass" : "blocked",
summary: "Quick prompt labels and filled text say HWLAB API / Skill CLI and avoid direct gateway/box/patch-panel or PROD claims.",
summary: "Quick prompt labels and filled text say Codex and avoid PROD claims.",
observations: promptResults.map(({ viewport, id, label, inputValue, copyBoundaryOk, forbiddenHits }) => ({
viewport,
id,
@@ -5068,8 +5056,6 @@ async function runLocalAgentFixtureSmoke({
} else {
const prompts = agentFixtureMode === "slow-blocker"
? [codeAgentE2ePrompts[1]]
: agentFixtureMode === "external-network-blocker"
? [codeAgentExternalNetworkPrompt]
: codeAgentE2ePrompts;
for (const prompt of prompts) {
promptResults.push(await runCodeAgentPromptJourney(page, prompt, { sourceFixture: true }));
@@ -5103,7 +5089,7 @@ async function runLocalAgentFixtureSmoke({
ui.timeoutActionPanelContained &&
!ui.completedMessageVisible
)
: agentFixtureMode === "slow-blocker" || agentFixtureMode === "external-network-blocker"
: agentFixtureMode === "slow-blocker"
? (
promptResults.length === 1 &&
promptResults[0]?.status === "blocked" &&
@@ -5111,9 +5097,7 @@ async function runLocalAgentFixtureSmoke({
promptResults[0]?.legacyWindow?.permanentFailureAround4500ms === false &&
promptResults[0]?.legacyWindow?.pendingChineseVisible === true &&
promptResults[0]?.legacyWindow?.pendingTraceShowsEvents === true &&
(agentFixtureMode === "external-network-blocker"
? ["能力未开放", "Runner 受阻", "服务受阻"].includes(ui.agentChatStatus)
: ui.agentChatStatus === "Runner 受阻") &&
ui.agentChatStatus === "Runner 受阻" &&
ui.failedMessageVisible &&
ui.failedMessageHasTrace &&
ui.retryInputPreserved &&
@@ -5188,7 +5172,7 @@ async function runLocalAgentFixtureSmoke({
status: pass ? "pass" : "blocked",
summary: expectTimeout
? "Local SOURCE fixture proves a bounded Code Agent timeout stays user-visible/BLOCKED in the UI, preserves trace context, and keeps the user's input for retry."
: agentFixtureMode === "slow-blocker" || agentFixtureMode === "external-network-blocker"
: agentFixtureMode === "slow-blocker"
? "Local SOURCE fixture returns a delayed structured blocker and the conversation shows the Chinese reason with trace evidence while preserving the input."
: "Local SOURCE fixture sends input and reaches a user-visible replied state without marking the fixture as DEV-LIVE.",
observations: expectTimeout ? {
@@ -5239,15 +5223,11 @@ async function runLocalAgentFixtureSmoke({
summary: "390x844 mobile view shows Chinese Code Agent pending status, trace/session context, and no pending-card text overflow before the delayed response arrives.",
observations: mobilePending
});
} else if (!expectTimeout && (agentFixtureMode === "slow-blocker" || agentFixtureMode === "external-network-blocker")) {
} else if (!expectTimeout && agentFixtureMode === "slow-blocker") {
checks.push({
id: agentFixtureMode === "external-network-blocker"
? "local-agent-fixture-external-network-blocker"
: "local-agent-fixture-slow-structured-blocker",
id: "local-agent-fixture-slow-structured-blocker",
status: pass ? "pass" : "blocked",
summary: agentFixtureMode === "external-network-blocker"
? "External network blocker for a GitHub prompt displays Runner 受阻 with network trace, keeps the user prompt, and does not restore main evidence/facts noise."
: "Delayed structured blocker arrives after the legacy 4500ms window, displays Runner 受阻 instead of a generic 后端失败 state, keeps trace visible, and leaves the user prompt for continuing the same conversation.",
summary: "Delayed structured blocker arrives after the legacy 4500ms window, displays Runner 受阻 instead of a generic 后端失败 state, keeps trace visible, and leaves the user prompt for continuing the same conversation.",
observations: {
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
@@ -5738,7 +5718,7 @@ async function inspectQuickPromptsViewport(browser, url, viewport) {
const button = document.querySelector(`[data-agent-quick-prompt="${id}"]`);
const input = document.querySelector("#command-input");
const text = `${button?.textContent ?? ""}\n${button?.getAttribute("title") ?? ""}\n${button?.dataset.promptText ?? ""}`;
const forbiddenPattern = /\b(?:PROD|gateway|box|patch-panel|真实硬件直控)\b/iu;
const forbiddenPattern = /\bPROD\b/iu;
const forbiddenHits = [...new Set(text.match(forbiddenPattern) ?? [])];
return {
label: button?.textContent?.replace(/\s+/gu, " ").trim() ?? "",
@@ -5748,7 +5728,7 @@ async function inspectQuickPromptsViewport(browser, url, viewport) {
focused: document.activeElement === input,
explicitSendRequired: button?.dataset.requiresExplicitSend === "true",
copyBoundaryOk:
/(?:HWLAB API|Skill CLI)/u.test(text) &&
/Codex/u.test(text) &&
!forbiddenPattern.test(text) &&
(button?.dataset.requiresExplicitSend !== "true" || /只填充输入框|点击发送后才确认/u.test(text)),
forbiddenHits
@@ -6899,136 +6879,6 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
}));
return true;
}
if (options.agentFixtureMode === "external-network-blocker") {
const blocker = {
code: "external_network_blocked",
category: "capability_unavailable",
layer: "network",
userMessage: "当前 Code Agent 运行策略禁止外部网络访问;未访问外网,也不会伪造成功。",
retryable: false,
traceId,
sessionId: conversationId,
conversationId,
stage: "policy",
elapsedMs: options.agentDelayMs ?? 0,
lastEvent: { seq: 4, traceId, type: "network", stage: "policy", status: "blocked", label: "network:blocked", errorCode: "external_network_blocked" }
};
jsonResponse(response, 200, {
conversationId,
sessionId: conversationId,
messageId,
status: "failed",
sourceKind: "SOURCE",
evidenceLevel: "SOURCE",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider: "codex-stdio",
model: "codex-stdio",
backend: "local-source-fixture/codex-stdio-external-network-blocker",
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
runner: {
kind: "codex-mcp-stdio-runner",
writeCapable: true,
codexStdio: true,
longLivedSession: true,
durableSession: true
},
capabilityLevel: "blocked",
sessionMode: "codex-mcp-stdio-long-lived",
session: {
sessionId: conversationId,
conversationId,
status: "idle",
turn: 1,
lastTraceId: traceId
},
sessionReuse: {
conversationId,
sessionId: conversationId,
mapped: true,
reused: true,
turn: 1,
status: "idle"
},
error: {
code: "external_network_blocked",
category: "capability_unavailable",
layer: "network",
message: "SOURCE external network blocker: runtime policy disabled external network checks.",
userMessage: blocker.userMessage,
retryable: false,
blocker
},
blocker,
blockers: [blocker],
toolCalls: [
{
name: "external.network.check",
type: "network-check",
status: "blocked",
exitCode: 2,
traceId,
stderrSummary: "external_network_blocked",
targetUrl: "https://github.com/",
blocker
}
],
skills: {
status: "not_requested",
items: [],
count: 0,
totalCount: 0,
blockers: []
},
runnerTrace: {
traceId,
runnerKind: "codex-mcp-stdio-runner",
sessionMode: "codex-mcp-stdio-long-lived",
sessionId: conversationId,
sessionStatus: "idle",
status: "blocked",
elapsedMs: options.agentDelayMs ?? 0,
waitingFor: "network-policy",
events: [
{ seq: 1, traceId, type: "session", stage: "created", status: "completed", label: "session:created" },
{ seq: 2, traceId, type: "prompt", stage: "sent", status: "completed", label: "prompt:sent", toolName: "external.network.check" },
{ seq: 3, traceId, type: "network", stage: "policy", status: "started", label: "network:started", toolName: "external.network.check" },
blocker.lastEvent,
{ seq: 5, traceId, type: "tool_call", stage: "tool_call", status: "blocked", label: "tool:external.network.check:blocked", toolName: "external.network.check", errorCode: "external_network_blocked" }
],
lastEvent: { seq: 5, traceId, type: "tool_call", stage: "tool_call", status: "blocked", label: "tool:external.network.check:blocked", toolName: "external.network.check", errorCode: "external_network_blocked" }
},
conversationFacts: {
conversationId,
sessionId: conversationId,
sessionStatus: "idle",
sessionMode: "codex-mcp-stdio-long-lived",
capabilityLevel: "blocked",
runnerKind: "codex-mcp-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
turnCount: 1,
latestTraceId: traceId,
traceIds: [traceId],
latestSkills: null,
recentToolCalls: [{ name: "external.network.check", status: "blocked" }],
facts: [],
valuesRedacted: true,
secretMaterialStored: false
},
providerTrace: {
source: "SOURCE-local-browser-fixture",
sourceKind: "SOURCE",
transport: "stdio+controlled-network",
responseId: "rsp_source_fixture_external_network_blocker",
redacted: true
}
});
return true;
}
if (options.agentFixtureMode === "slow-blocker") {
jsonResponse(response, 200, {
conversationId,
+8 -8
View File
@@ -175,31 +175,31 @@
data-agent-quick-prompt="pwd"
data-prompt-action="fill"
title="只填充输入框,点击发送后交给 Code Agent"
data-prompt-text="通过 Skill CLI 执行 pwd,列出当前工作目录。"
>Skill CLIpwd</button>
data-prompt-text="交给 Codex 执行 pwd,列出当前工作目录。"
>Codexpwd</button>
<button
type="button"
data-agent-quick-prompt="skills"
data-prompt-action="fill"
title="只填充输入框,点击发送后交给 Code Agent"
data-prompt-text="通过 Skill CLI 列出你能使用的所有 skill。"
>Skill CLI:列出 skill</button>
data-prompt-text="交给 Codex 列出你能使用的所有 skill。"
>Codex:列出 skill</button>
<button
type="button"
data-agent-quick-prompt="do1-true-di1"
data-prompt-action="fill"
data-requires-explicit-send="true"
title="只填充输入框;必须再点击发送才确认写入请求"
data-prompt-text="通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。"
>HWLAB APIDO1=true 后读 DI1</button>
data-prompt-text="交给 Codex 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。"
>CodexDO1=true 后读 DI1</button>
<button
type="button"
data-agent-quick-prompt="do1-false-di1"
data-prompt-action="fill"
data-requires-explicit-send="true"
title="只填充输入框;必须再点击发送才确认写入请求"
data-prompt-text="通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。"
>HWLAB APIDO1=false 复核 DI1</button>
data-prompt-text="交给 Codex 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。"
>CodexDO1=false 复核 DI1</button>
</div>
<label class="agent-timeout-control" for="code-agent-timeout">
<span>无新事件超时</span>
+2
View File
@@ -1235,6 +1235,8 @@ assert.match(codexStdioSession, /Windows filesystem inventory/);
assert.match(codexStdioSession, /Select-Object -First/);
assert.match(codexStdioSession, /ConvertTo-Json -Compress/);
assert.match(codexStdioSession, /C:\\\\Users\\\\liang\\\\\.agents\\\\skills\\\\keil/);
assert.match(codexStdioSession, /C:\\\\Users\\\\liang\\\\\.agents\\\\skills\\\\serial-monitor/);
assert.match(codexStdioSession, /monitor start/);
assert.match(codexStdioSession, /async job flow/);
assert.match(gatewayShellTool, /function powershellEncodedCommand/);
assert.match(gatewayShellTool, /DEFAULT_GATEWAY_SHELL_TIMEOUT_MS\s*=\s*120000/);