Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/ZCODE_RUNTIME.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# ZCode 运行配置边界

> Status: AUTHORITATIVE
> Last updated: 2026-10-15
> Last verified: 2026-10-15,最新上游整合分支的验证结果见[审查说明](reports/2026-10-15-browser-and-transport-fixes.md);旧版本实测与当前分支回归分别记录。
> Verified against: 上游fae74e45033a50fb560c0014d1a6d60a7dc771d1 + 本分支修复;受测源码指纹见审查说明。
> Last updated: 2026-10-16
> Last verified: 2026-10-16,独立验证 Windows/Linux worker 环境转发、Windows 自动发现及真实浏览器后端(见[修复报告](reports/2026-10-16-environment-portability-fixes.md#codex-独立验收2026-10-16));其余条目沿用 2026-10-15 核对范围。
> Verified against: 0e1e319561edf94e0f0cd481ac455ec784eb19dd + 本分支 F1–F3 修复;受测产物指纹见修复报告。

当前生产路径是 `node zcode.cjs app-server --stdio`,不是历史 CLI `--prompt --json`。解析入口为 `NodeRuntimeResolver`,配置项与用户设置方法见仓库 README。

Expand All @@ -13,7 +13,7 @@ Bridge 只读取官方 builtin/personal provider 配置,不复制、不改写

persisted runtime-config 中已知字段只能是字符串或 null;缺文件允许环境/发现回退,存在但损坏、非 object 或超过 64 KiB 会报配置错误。默认 mode 仍为已披露的 yolo;不能把坏配置当成首次未设置而回退到该模式。

浏览器宿主配置(ADR-007):`ZCODE_BRIDGE_BROWSER_USE` 取 `off`(默认,含空值/未设置/非法值回退为 off)或 `headless`;`ZCODE_BRIDGE_BROWSER_EXECUTABLE` 可选,须为已存在文件的绝对路径,缺省按官方实现解析已安装 Chromium/Edge(不下载浏览器)。两项均为普通字符串配置键,遵循配置优先于环境变量的既有规则;非法 USE 值不阻断任务,仅保持关闭并由 doctor 给出 warning。不向 app-server 传递任何 headless CLI 参数;浏览器由 worker 内的 managed CDP runtime 以独立临时 profile 启动,生命周期见 ARCHITECTURE.md。
浏览器宿主配置(ADR-007):`ZCODE_BRIDGE_BROWSER_USE` 取 `off`(默认,含空值/未设置/非法值回退为 off)或 `headless`;`ZCODE_BRIDGE_BROWSER_EXECUTABLE` 可选,须为已存在文件的绝对路径,缺省按官方实现解析已安装 Chromium/Edge(不下载浏览器)。两项均为普通字符串配置键,遵循配置优先于环境变量的既有规则;非法 USE 值不阻断任务,仅保持关闭并由 doctor 给出 warning。detached worker 进程按显式键清单转发这两项非秘密设置(2026-10-16 修复:此前 env-only 配置在真实 spawn 边界丢失),其余任意父进程环境与秘密仍不透传。Windows worker 环境保留 `ProgramFiles` / `ProgramFiles(x86)`(接受常见大小写)等非敏感系统目录变量供自动发现使用,发现解析同样不区分大小写键;显式 `ZCODE_BRIDGE_BROWSER_EXECUTABLE` 优先级不变。不向 app-server 传递任何 headless CLI 参数;浏览器由 worker 内的 managed CDP runtime 以独立临时 profile 启动,生命周期见 ARCHITECTURE.md。

执行和模型目录 RPC 客户端都是私有集成实现,当前假运行时测试覆盖调用选择、事件、交互和清理。真实协议随安装版本变化;本次已新建真实会话验证记录版本的浏览器链路,不能将这次 smoke 扩展为全协议或其他版本认证。

Expand Down
81 changes: 81 additions & 0 deletions docs/reports/2026-10-16-environment-portability-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 跨环境审查 F1–F3 环境可移植性修复报告

> Status: RESEARCH
> Date: 2026-10-16
> Base: `0e1e319561edf94e0f0cd481ac455ec784eb19dd`(上游 1.2.5,PR #43 合并后的 master)
> 执行:Bridge worker(实现与本机 Windows 自测);末尾独立验收部分由 Codex 据实补录,前面的 worker 记录保留原验证范围。

## 来源与范围

本次 Codex 独立审查(2026-10-16,审查记录与原始证据由 Codex 保存,只读引用)复现了三个缺陷:F1 env-only 浏览器设置未传给真实 detached worker;F2 Windows 系统浏览器自动发现所需的 `ProgramFiles` / `ProgramFiles(x86)` 被过滤;F3 仓库位于短目录(如容器 `/app`)时 `validate:plugin` 把仓库路径当无边界子串,在 JSON Schema URL 片段(`/vocab/applicator` 等)上误报"开发目录泄漏"。本分支在 1.2.5 基线上修复三项根因,版本号不变;不修改 MCP/schema/账号或权限合同、不放宽秘密边界、不改变依赖锁与 CI/Node 版本。

首轮实现后,Codex 对 F3 的独立探针又发现两类漏检:JS 字符串转义形式(双反斜杠)的 Windows 绝对路径、以及注释中前导空格之后的绝对路径未被拒绝;返修期间进一步收束为按路径结构判定边界(见下)。本报告描述最终实现;首轮与返修的红/绿日志均按任务记录另行保存,未改写。

## 修改

- `src/runtime/child-env.ts`:`createWorkerEnv` 显式键清单加入 `ZCODE_BRIDGE_BROWSER_USE` 与 `ZCODE_BRIDGE_BROWSER_EXECUTABLE`(非秘密字符串,worker 内 adapter 从自身环境读取);Windows `createMinimalOsEnv` 白名单加入 `PROGRAMFILES`、`PROGRAMFILES(X86)`(按 `toUpperCase` 比较,保留原始键大小写)。其余任意环境与秘密仍不透传。(F1/F2,首轮实现,返修未动。)
- `src/browser/executable.ts`:Windows 浏览器发现改用大小写不敏感的环境键读取(`windowsEnvValue`),接受 `ProgramFiles` / `programfiles` 等常见形式;空值跳过、显式 `executablePath` 优先与候选顺序不变。(F2,首轮实现,返修未动。)
- `scripts/validate-plugin.mjs` + 新增 `scripts/lib/repo-root-pattern.mjs`:泄漏检查的根路径正则抽出为可回归测试的 helper。最终语义按路径结构而非名称字符黑名单判定:
- 右界:根之后只允许路径分隔符、字符串/行终止符或文本结束;空格、`%20`、`@`、`+`、`(`、字母、`.` 等一律视为更长路径段的延续(`/app Backup`、`/app@2`、`/app.git`、`file:///app%20Backup` 均不是本仓库根)。
- 左界:根必须出现在绝对路径可以开始的位置(文本起始、空白、引号或 `=`/`:`/`(`/`,`/`;`/`/`/`\` 等记号分隔符之后);名称字符或 URL authority 尾部(`example.test/app`、`[::1]/app`)说明是更长名称或普通 HTTP URL 的一部分。原始路径分支上,同一记号内更早出现的 `http(s)://` 抑制匹配(覆盖 `https://…/docs/app/…` 等深层 URL 段)。
- 每个原始分隔符位置匹配一到两个 `/` 或 `\`:bundle 中 Windows 绝对路径既有原生单分隔符形式,也有 JS 字符串转义的双反斜杠形式,二者是同一路径。
- 完整标准 file URI(Node 内置 `pathToFileURL`,含 scheme)作为独立备选,不受上述 HTTP 抑制——嵌在 HTTP URL 里的 `file:` 引用(如 redirect target)仍是本地开发路径泄漏;POSIX 风格根同时纳入 `pathToFileURL(root, { windows: false })` 的跨平台拼写,避免验证平台差异。该备选沿用相同左右界(编码同级后缀 `%20Backup` 不误报)。
- 旧实现的可比行为:其分隔符字符类按"替换后成对"生效,因此能命中 JS 转义等双分隔符形式,但漏掉原生单分隔符绝对路径(Windows 常见形态);POSIX 短根上则退化为无边界子串导致误报。新实现两类形式均覆盖。
- 路径泄漏检查本身保留,未取消、未硬编码跳过 `/app`;runtime 依赖与资源检查(vendored package/browsers.json、playwright-core 静态导入禁令)原样保留。
- 提交物 `plugins/codex-zcode-bridge/dist/bridge.mjs`、`worker/worker-main.mjs`、`worker/vendor/pkg/browser-host.cjs` 由 `npm run build` 重建,与源码同步,无手改(返修轮未触及生产源码,产物与首轮一致)。

## 回归(红/绿均有旧代码失败证据)

测试分四组:`test/worker-env-portability.test.ts`(真实 `createWorkerSpawner` 跨进程探针:两项浏览器设置到达、任意秘密不透传、未设置时键缺席且无 `ZCODE_BRIDGE_*` 整体转发)、`test/browser-executable.test.ts`(fixture 目录 + 假 playwright,无真实浏览器)、`test/child-env.test.ts`(函数级转发与 Windows 白名单)、`test/validate-plugin-root-pattern.test.ts`(helper 语义 + 真实 CLI 注入)。

- 首轮红相:旧生产代码上 16 项中 9 项失败(含 F1 跨进程 `null !== 'headless'`、F2 大小写/白名单、F3 `/app` 误报与 Windows 单分隔符漏检)。
- 返修红相:在首轮实现上,evaluator 输入的三个 helper 用例(注释前导空格、JS 转义双反斜杠、Windows 注释)与真实 CLI 注入(转义字符串、注释、斜杠、标准 file URI)均未被拒绝;返修后全部通过。另有审查补充的 `/app@2`、`/app+backup`、`/app(copy)`、`[::1]/app`、`file:///app%20Backup`、`pathToFileURL` 本机盘符同级后缀、深层 URL 段、HTTP 内嵌 `file:` URI 等负例/正例。
- 真实 CLI 回归在临时 fixture 仓库(根名含空格、Unicode 与正则字符)中复制现行 validator + helper 运行:baseline(含 schema URL、同段 URL、同级目录与编码同级目录负例)必须 exit 0;追加泄漏行(转义字符串、注释、斜杠、未编码与标准 file URI、HTTP 内嵌 file URI)必须被"embeds the development repository's absolute path"诊断拒绝。

## Worker 本机验证(Windows,2026-10-16)

| 检查 | 版本与结果 |
|---|---|
| 首轮红/绿(旧生产代码) | Node 22.18.0:红 16 项中 9 失败;绿 16/16 通过 |
| 首轮完整验证 | Node 22.18.0:typecheck、完整隔离测试 358 项(357 通过/0 失败/1 既有 Windows POSIX 权限跳过)、build、validate:plugin、`git diff --check` 全部 exit 0 |
| 返修红/绿(首轮实现) | Node 22.18.0:红相 3 项失败(注释/转义/CLI 注入);绿相 22/22 通过 |
| 返修 typecheck / build / validate:plugin / diff-check | Node 22.18.0,均 exit 0;产物与首轮一致 |
| 返修后未重跑全仓测试 | 按分工由 Codex 在最终验收独立执行;首轮全套记录保留 |
| 本次相关回归 | Node 24.19.0:22/22 通过,exit 0 |

命令清单、真实退出码与完整 stdout/stderr 按任务约定保存于统一日志目录(本地交付物,不进仓库);evaluator 探针只读复现,未在 evaluator 目录写入。

## Worker 交付时未运行项(现由下述独立验收补齐)

- 本机为 Windows,无法在允许路径内把仓库放到真实 `/app` 根复现容器场景;F3 的端到端 `/app` 验收由 Codex Linux 沙盒执行(语义已由 helper 与真实 CLI 回归覆盖)。
- 真实浏览器自动发现(系统安装 Edge/Chrome + `ZCODE_BRIDGE_BROWSER_USE=headless` 无显式路径)按任务约定由 Codex 在 Windows 验收;本报告只覆盖 fixture 回归。
- 沙盒/环境清理、提交、PR 更新与发布不在 worker 职责内。

## Codex 独立验收(2026-10-16)

在 worker 第二次交付后冻结代码,独立审查完整差异及额外正反探针,再在原生 Windows 和 rootless Linux 容器验证。首轮未通过审查及返修日志保留;以下结果来自最终交付,不能用首轮结果代替。

| 独立检查 | 结果 |
|---|---|
| Windows / Node 22.18.0 / `npm test` | 364 项:363 通过、0 失败、0 取消、1 跳过(既有 POSIX 权限测试) |
| Linux / Node 22.18.0 / `npm test` | 365 项:363 通过、0 失败、0 取消、2 跳过(Windows 启动 hook 与进程树语义);总数差异来自平台条件子测试 |
| 两平台 Node 22.18.0 | `npm run typecheck`、`npm run build`、`npm run validate:plugin`、`npm run test:clean-entrypoints` 全部 exit 0;clean-entrypoints 各 4/4 通过 |
| Node 24 相关回归 | Windows 24.19.0、Linux 24.20.0,各 36/36 通过(四个本次相关文件 + browser-host 原始回归) |
| 真实 detached worker | 两平台均收到 env-only 浏览器配置、任意测试秘密仍被过滤、worker 退出;Windows 无显式路径时自动发现系统 Edge |
| 短目录校验 | Linux 仓库实际置于 `/app`,完整 build 与 validator 通过;追加绝对注释、路径字符串、file URI 后分别被校验器拒绝 |
| 独立 F3 探针 | 19 个路径/URL 边界用例全部通过;Windows 真实 CLI 基线通过,JS 转义绝对路径与注释注入均被拒绝 |
| 独立插件包及真实浏览器 | Windows Edge / Linux Chromium,各 16 项通过:MCP 初始化和 1.2.5 版本、缺浏览器错误、vendored 后端、导航、填写、单次点击、页面确认、服务器恰好一次接收、截图、关闭 |
| 产物一致性 | 三个提交 JS bundle 的 Windows 交付与 Linux 干净重建 SHA256 一致(统一 LF 后比较);Windows 验证期间 217 个交付文件均未变化;随后只补录本报告和运行文档的验收状态 |
| 清理 | 两个任务容器和一个派生镜像移除,原有 8 个镜像保留;浏览器及 worker 无残留、测试端口关闭;Windows 临时目录、下载输入与探针 fixture 共 8 个目录移入回收站,原路径不存在;证据与原始失败日志保留 |

Linux 验收环境使用非 root UID 1000、无网络、无宿主挂载、移除 capabilities、no-new-privileges;依赖准备阶段使用公开源联网安装,随后在独立断网容器运行。Windows 使用仓库之外含空格/中文的独立插件包和最小环境变量,属于目录与进程环境隔离,**不是 OS 沙箱**。两平台真实浏览器演示均未读取账户或调用模型服务。

受测产物 SHA256(LF 规范化):

| 提交物 | SHA256 |
|---|---|
| `dist/bridge.mjs` | `6d7cf97a5c9f386f5b743b258d7ea6d0c823b75cb96b4a74e439a5e25542f8d4` |
| `worker/worker-main.mjs` | `a2b2cef48d6df746f345447c1e0082b02ff045da573287db9a664dced3404d84` |
| `worker/vendor/pkg/browser-host.cjs` | `b750aeff5cae48d6cdb71e068b0b8d96592cba52b57a514fedad515615c55b4e` |

结论:F1–F3 在上述范围内独立验收通过,适合提交维护者审查。此结论不包含 macOS/ARM 实机、真实模型端到端协议、所有浏览器版本或未运行的 Node 版本认证;既有 Windows 锁并发抖动本轮未复现,也未据此宣称根治。远端 CI 与维护者合并决定另行记录在 PR 中。本次未更改本机已安装插件,未发布版本。
10 changes: 9 additions & 1 deletion plugins/codex-zcode-bridge/dist/bridge.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23565,7 +23565,11 @@ var WINDOWS_OS_ENV = /* @__PURE__ */ new Set([
"PROGRAMDATA",
"OS",
"PROCESSOR_ARCHITECTURE",
"NUMBER_OF_PROCESSORS"
"NUMBER_OF_PROCESSORS",
// Non-sensitive system install directories; browser auto-discovery resolves
// system Chrome/Edge under these roots (src/browser/executable.ts).
"PROGRAMFILES",
"PROGRAMFILES(X86)"
]);
var POSIX_OS_ENV = /* @__PURE__ */ new Set([
"PATH",
Expand Down Expand Up @@ -23600,6 +23604,10 @@ function createWorkerEnv(source) {
"ZCODE_BRIDGE_DEFAULT_REASONING_LEVEL",
"ZCODE_BRIDGE_MODE",
"ZCODE_BRIDGE_TIMEOUT_MS",
// Env-only browser configuration (ADR-007): non-secret strings the worker's
// adapter reads from its own environment when no persisted setting exists.
"ZCODE_BRIDGE_BROWSER_USE",
"ZCODE_BRIDGE_BROWSER_EXECUTABLE",
"ZCODE_HOME",
"ZCODE_BUILTIN_PROVIDER_CONFIG_FILE",
"ZCODE_PERSONAL_PROVIDER_CONFIG_FILE",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion plugins/codex-zcode-bridge/worker/worker-main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,11 @@ var WINDOWS_OS_ENV = /* @__PURE__ */ new Set([
"PROGRAMDATA",
"OS",
"PROCESSOR_ARCHITECTURE",
"NUMBER_OF_PROCESSORS"
"NUMBER_OF_PROCESSORS",
// Non-sensitive system install directories; browser auto-discovery resolves
// system Chrome/Edge under these roots (src/browser/executable.ts).
"PROGRAMFILES",
"PROGRAMFILES(X86)"
]);
var POSIX_OS_ENV = /* @__PURE__ */ new Set([
"PATH",
Expand Down
57 changes: 57 additions & 0 deletions scripts/lib/repo-root-pattern.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Builds the pattern validate-plugin.mjs uses to detect whether a bundle
// embeds the development repository's absolute path. Kept in scripts/lib so
// the boundary semantics can be regression-tested directly.
//
// A genuine path reference is delimited by path/string structure, not by a
// blacklist of "name characters":
// - On the right, the root may only be followed by a path separator, a
// string/line terminator, or the end of the input. Anything else — a space,
// `%20`, `@`, `+`, `(`, a letter, a dot — continues a longer path segment
// ("/app Backup", "/app@2", "/app.git" are sibling names, not this root).
// - On the left, the root must start in a context where an absolute path can
// begin: start of input, whitespace (a comment like "// /app/src"), a
// quote, or a token separator like `=`, `:`, `(`, `,`, `;`, `/`, `\`.
// A name character or a URL authority tail ("example.test/app",
// "[::1]/app") means the hit is the middle of a longer name or an ordinary
// HTTP URL path, not a local absolute path. Deeper URL path segments are
// covered by the scheme lookbehind on the raw branch: any http(s) URL
// opened earlier in the same token (no whitespace/quote/backtick in
// between) suppresses the raw match.
// Each raw separator position matches one or two separators (`/` or `\`):
// bundles embed Windows absolute paths both raw (C:\repo\src) and
// JS-string-escaped (C:\\repo\\src with literal doubled backslashes), and
// both spellings name the same development path.
//
// The second alternative is the root's complete standard file URI
// (pathToFileURL, scheme included): spaces and non-ASCII segments are
// percent-encoded there, so the raw pattern cannot see them. It is NOT
// subject to the http(s) suppression — a file: URI embedded inside an HTTP
// URL (a redirect target) is still a reference to the local development
// path. The delimiters still apply: "file:///app%20Backup/src" extends the
// segment and must not match, while "file:///app/src" must. Scheme-less
// percent-encoded spellings are not covered by this check.
import { pathToFileURL } from "node:url";

export function buildRepoRootLeakPattern(root) {
const escapedRoot = root
.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")
.replace(/\\\\/gu, "[/\\\\]{1,2}");
// pathToFileURL is platform-dependent for drive-less roots (Windows
// prepends the current drive), so a POSIX-style root also contributes its
// cross-platform spelling; hosts and tests agree regardless of platform.
const encodedCandidates = new Set([pathToFileURL(root).href]);
if (root.startsWith("/")) encodedCandidates.add(pathToFileURL(root, { windows: false }).href);
const encodedRoot = [...encodedCandidates]
.map((candidate) => candidate.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"))
.join("|");
// Complement classes: "previous char (if any) must be an allowed start
// context" and "next char (if any) must be an allowed terminator". The
// backtick is expressed as \u0060 so it can live inside these templates.
const startContext = String.raw`[^\s"'=(:,;\\/\u0060]`;
const terminator = String.raw`[^/\\'"\n\r\t\u0060]`;
const httpUrlToken = String.raw`(?<!https?://[^\s"'\u0060]*)`;
return new RegExp(
`${httpUrlToken}(?<!${startContext})${escapedRoot}(?!${terminator})|(?<!${startContext})(?:${encodedRoot})(?!${terminator})`,
"iu",
);
}
Loading
Loading