Skip to content

feat(review): run the reviewed repository's own commands behind a container (#9556) - #9723

Merged
wenshao merged 23 commits into
mainfrom
feat/review-sandbox-ci
Aug 25, 2026
Merged

feat(review): run the reviewed repository's own commands behind a container (#9556)#9723
wenshao merged 23 commits into
mainfrom
feat/review-sandbox-ci

Conversation

@wenshao

@wenshao wenshao commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

A review executes the code it is reviewing. This puts that execution behind a container boundary, and makes it a policy the operator sets rather than a property of what happens to be installed.

Two call sites run the reviewed repository's own commands:

point what runs
build-test.ts whatever the reviewed repo's package.json names — npm ci with its preinstall/postinstall, the build, the suite
test-efficacy.ts that suite again, once per baseline / control / mutant / hunk probe / revert

Both now route through lib/sandboxed-exec.ts, which either returns a container argv or null for the direct spawn that has always been there.

Why it's needed

Both call sites handed the PR's code process.env entirebuildRunEnv() spreads it, and the probe spawn passes no env key at all so it inherits. On CI that environment carries OPENAI_API_KEY and GH_TOKEN. A postinstall script reading process.env is one line, and it needs none of the git-config machinery the pipeline's twenty rounds of threat findings were built on.

That is checkable rather than argued: read the two call sites at the parent commit.

The boundary goes around the executions, not around the review agent. Wrapping the agent was tried first and is the wrong shape — its secrets do not survive the container's env allowlist (sandbox.ts forwards by name, and OPENAI_*/GH_TOKEN are not on the list), its timeout reaps the host-side docker client rather than the container, its CLI version stops matching the runner's @latest — and after paying all of that the mount is process.cwd(), the whole checkout, so <repo>/.git stays writable anyway.

Reviewer Test Plan

How to verify

cd packages/cli && npx vitest run src/commands/review4 324 passed, 1 skipped, 0 failed. The default policy is off, so every existing test exercises the unchanged direct path; the new file pins the argv.

Evidence (Before & After)

Three decisions carry this design, each measured rather than assumed, each pinned by a test that goes red when that decision alone is reverted:

decision why mutation → result
mount the review temp dir, not the tree the dependency farm links OUT of every tree: each package in the probe tree's node_modules points at the review worktree's copy — 1 722 links, 0 failed on a live CI review (run 32423107998). Mounting one tree leaves every link dangling mount the tree → red
env allowlist, not the inherited env the finding above add an inherited secret → red
network only for an install a build and a suite do not need the registry; --network none keeps loopback, so a suite standing up a local fixture server still runs give every kind the network → red

One ephemeral container per command, not one per phase. Measured on the ECS pool from a real sandboxed autofix job (96386571484): Resolve sandbox image is 2 s once, warm; a docker run on a warm image adds a few hundred ms. Against TOTAL_BUDGET_MS = 540_000 and at most ~15 suite runs that is 1–2 %. The stronger reason is not cost: a long-lived container per phase would re-introduce the cross-run state #9221 spent rounds closing (tracked-file carryover, ignored-plant carryover). --rm is isolation by construction.

Tested on

macOS 26.6 (Darwin 25.6.0), Node 24, packages/cli vitest 3.2.4. No container runtime on this machine — which is why the argv is what the tests pin, and why an integration exercise against a real runtime is named below as not done.

Risk & Scope

Off by default. review.sandbox is off | auto | required; off is today's behaviour exactly. Containerising a build by surprise changes what native modules compile against, so nobody gets it without asking.

Read through operatorReviewSettings, which skips the workspace scope — a repository cannot ship a .qwen/settings.json that switches off the containment that exists to contain it. QWEN_REVIEW_SANDBOX outranks the setting so CI can require containment without depending on a settings file the runner may not carry.

required makes the execution-dependent evidence unavailable for that run; it does not end the review. That is the rule this repository already established one level down — a probe that cannot get an isolated tree reports inconclusive rather than falling back to the shared worktree.

Not done in this PR, and I would rather name it than let it read as done:

  • No integration run against a real container runtime. This machine has none. The argv is pinned; that a container starts, resolves the farm links through the mount, and produces the same verdicts is not.
  • Native builds. npm ci inside the image must produce the same native deps the host does; this repo already trips on packages/audio-capture/node-gyp.
  • Wiring CI. The workflow change that sets QWEN_REVIEW_SANDBOX=required and copies autofix's docker info preflight is deliberately a separate change, so this one can land and be exercised with auto first.
  • What this does not close: contamination inside the worktree (mounted writable by necessity — that is /review: verification probes mutate the shared worktree while reverse auditors read it #9207/fix(review): run verifier probes in a private scratch worktree (#9207) #9221's class), the review's own git operations (host-side by design), and a malicious image or container escape.
  • Host-trusted state that lives under the mount, named explicitly. The bind mount is the review temp dir, so anything the pipeline keeps there and later trusts is reachable from inside the container: each pipeline tree's .git gitfile (rewrite it and the host-side git that follows the redirect runs a planted filter), and the worktree lease files, which cleanupReviewWorktreeLeases matches by session ids alone before force-removing whatever worktree and branch they name. Neither is opened by this change — on main the reviewed repository's commands already run as the host user with no filesystem restriction at all, so both are reachable there and reachable more widely; containment narrows what such a script can touch rather than widening it. But it does not close them, and a required that claims containment should say so rather than imply otherwise. Closing them is a mount-geometry change (keep host-trusted state out of the writable surface), tracked as its own work.

Linked Issues

Implements the decision on #9556, with the design, the measurements and the correction to my own earlier framing recorded in that thread.

中文说明

这个 PR 做了什么

审查会执行它所审查的代码。本 PR 把那次执行放到容器边界之后,并让它成为操作者设定的策略,而不是"恰好装了什么"的副产品。

两个调用点在运行被审仓库自己的命令:build-test.ts(被审仓库 package.json 指定的一切——npm ci 连同 preinstall/postinstall、构建、测试套件)与 test-efficacy.ts(该套件再跑很多遍:基线/对照/每个突变体/每个 hunk 探针/回退)。两者现在都经由 lib/sandboxed-exec.ts,它要么返回容器 argv,要么返回 null 走一直以来的直接 spawn。

为什么需要

两个调用点都把 process.env 整个交给了 PR 的代码——buildRunEnv() 展开它,而探针 spawn 干脆没有 env 键因而继承。在 CI 上那里面有 OPENAI_API_KEYGH_TOKEN。一个读 process.envpostinstall 就是一行,且完全不需要二十轮威胁 finding 所依赖的那套 git 配置机关。

这可核查而非可辩论:读父提交上的那两个调用点即可。

边界围住的是执行,而不是审查 agent。 先试的是包住 agent,形状不对——它的密钥过不了容器的 env 白名单(sandbox.ts 按名字转发,OPENAI_*/GH_TOKEN 不在名单上)、它的 timeout 收割的是宿主侧 docker 客户端而非容器、它的 CLI 版本与 runner 的 @latest 不再一致;而付完这些代价之后,挂载仍是 process.cwd()(整个检出目录),<repo>/.git 照样可写。

审查者验证方案

如何验证

cd packages/cli && npx vitest run src/commands/review4 324 通过、1 跳过、0 失败。默认策略是 off,因此所有既有测试跑的都是未改变的直接路径;新文件钉的是 argv。

证据(Before & After)

三条决定支撑本设计,每条都经过测量而非假设,且每条都有测试钉住(单独回退该决定即变红):

决定 理由 变异 → 结果
挂载 review 临时目录,而非那棵树 依赖 farm 的链接指向树外:探针树 node_modules 里每个包都指向 review 工作树的副本——真实 CI 审查实测 1 722 条链接、0 失败(run 32423107998)。只挂一棵树会让每条链接悬空 改成挂树 → 红
env 白名单,而非继承环境 即上文那条发现 加入一个继承来的密钥 → 红
只有 install 给网络 构建与套件不需要 registry;--network none 保留回环,因此"起本地夹具服务器"的套件照常工作 所有类别都给网络 → 红

每命令一个一次性容器,而非每阶段一个。在 ECS 池上、从一次真实沙箱化 autofix 作业(96386571484)实测:Resolve sandbox image 预热下 2 秒、每作业一次;对预热镜像 docker run 额外数百毫秒。对上 TOTAL_BUDGET_MS = 540_000 与至多约 15 次套件运行,占 1–2%。更强的理由不是成本:每阶段长驻容器会把 #9221 花了若干轮才关掉的跨运行状态重新引进来(已跟踪文件残留、被忽略投毒残留)。--rm 是构造上的隔离。

测试环境

macOS 26.6(Darwin 25.6.0)、Node 24、packages/cli vitest 3.2.4。本机没有容器运行时——这正是"测试钉的是 argv"的原因,也是下面把"针对真实运行时的集成验证"明确列为未做的原因。

风险与范围

默认关闭。 review.sandboxoff | auto | requiredoff 与今天的行为完全一致。让构建在无人要求的情况下进容器,会改变原生模块编译所依赖的对象。

它经由 operatorReviewSettings 读取,而该函数跳过 workspace 作用域——仓库无法通过 .qwen/settings.json 关掉那道正是为了约束它自己而存在的containmentQWEN_REVIEW_SANDBOX 优先级高于该设置,使 CI 无需依赖 runner 上未必存在的设置文件即可强制要求。

required 会让依赖执行的证据在该次运行中不可用,但不会终止审查。这正是本仓库低一层已经立好的规矩——拿不到隔离树的探针报 inconclusive,而不是退回共享工作树。

本 PR 未做、且我宁愿点名也不愿让人读成已做的:

  • 没有针对真实容器运行时的集成运行。 本机没有。argv 已被钉住;但"容器真的起来、farm 链接经由挂载解析得到、产出相同判定"没有。
  • 原生构建。 镜像内 npm ci 必须产出与宿主相同的原生依赖;本仓库已在 packages/audio-capture/node-gyp 上踩过。
  • 接入 CI。 设置 QWEN_REVIEW_SANDBOX=required 并照抄 autofix docker info 前置的那次工作流改动,有意留作另一个 PR,以便本 PR 先落地并用 auto 实际磨合。
  • 本 PR 关不掉的: 工作树内部的污染(必须以可写方式挂载——那是 /review: verification probes mutate the shared worktree while reverse auditors read it #9207/fix(review): run verifier probes in a private scratch worktree (#9207) #9221 那一类)、审查自身的 git 操作(按设计跑在宿主)、恶意镜像或容器逃逸。
  • 挂载之下的宿主可信状态,逐项点名。 绑定挂载的就是 review 临时目录,所以流水线放在那里、事后又去信任的东西,都能从容器内够到:每棵流水线树的 .git gitfile(改写它,跟随重定向的宿主侧 git 就会执行植入的 filter),以及工作树 lease 文件——cleanupReviewWorktreeLeases 仅凭会话标识匹配,随后对 lease 里写的工作树和分支执行强制删除。这两条都不是本改动打开的:在 main 上,被审仓库的命令本来就以宿主用户身份、在毫无文件系统限制的情况下运行,因此那里同样够得到、而且够得更宽;容器化是把这类脚本能碰到的范围收窄,不是扩大。但它确实没有关掉这两条,而一个声称容器化的 required 应当把这件事说出来,而不是让人以为相反。关掉它们属于挂载几何的改动(把宿主可信状态移出可写面),另行跟踪。

关联 Issue

实现 #9556 上的决策;设计、测量,以及我对自己先前表述的更正,都记录在该讨论串中。

…tainer (#9556)

A review executes the code it is reviewing. `build-test` runs whatever the
reviewed repository's `package.json` names — `npm ci` with its `preinstall` and
`postinstall` scripts, the build, the suite — and `test-efficacy` runs that
suite again once per baseline, control, mutant, hunk probe and revert. Both did
it as the invoking identity, and both handed the PR's code `process.env`
entire: on CI that carries `OPENAI_API_KEY` and `GH_TOKEN`. Reading them is one
line in a `postinstall`, and it needs none of the git-config machinery the
pipeline's threat findings are built on.

The boundary goes around the executions, not around the review agent. Wrapping
the agent was tried first and is the wrong shape: its secrets do not survive
the container's env allowlist, its `timeout` reaps the host-side client rather
than the container, its CLI version stops matching the runner's — and after all
of that the mount is the whole checkout, so `<repo>/.git` stays writable
anyway.

Three decisions the argv encodes, each measured rather than assumed:

- **The mount is the review temp dir, not the tree the command runs in.** The
  dependency farm links OUT of every tree — `exposeDependencies` points each
  package in the probe tree's `node_modules` at the review worktree's copy, 1
  722 of them on a live CI review. Mounting one tree would leave every link
  dangling. Every tree the pipeline builds is a sibling under `.qwen/tmp`, so
  one mount covers both ends while `<repo>/.git` stays outside it.
- **The environment is an allowlist**, not the inherited one.
- **The network is per command kind.** An install needs the registry; a build
  and a suite do not, and `--network none` keeps loopback so a suite that
  stands up a local fixture server still runs.

One ephemeral container per command. A long-lived one per phase would save
about 1–2% of the 540-second efficacy budget and would re-introduce exactly the
cross-run state #9221 spent rounds closing.

Off by default: containerising a build by surprise changes what native modules
compile against. `review.sandbox` is `off` | `auto` | `required`, read through
`operatorReviewSettings` — which skips the workspace scope, so a repository
cannot ship a `.qwen/settings.json` that switches off the containment existing
to contain it. `QWEN_REVIEW_SANDBOX` outranks it so CI can require containment
without depending on a settings file the runner may not carry.

Each of the three decisions is pinned by a test that goes red when that
decision alone is reverted.
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Re-run after 22 commits since the first pass; the branch merged main at 5d2a126.

  • Template: looks good ✓
  • Problem: real and verified at the base commit, not theoretical. buildRunEnv() spreads the entire process.env into every install/build/test run of the reviewed repository, and the probe-suite spawn passes no env key at all — it inherits everything, and runs the host's own process.execPath. On CI that environment carries the model and GitHub credentials. review: decide whether the pipeline should keep granting code execution as the invoking user #9556 is labeled category/security.
  • Direction: aligned — this implements the decision recorded on review: decide whether the pipeline should keep granting code execution as the invoking user #9556 by the same maintainer who opened it. It does touch sandbox machinery and the core settings schema, so per this gate's rules the final call stays with a human maintainer; that was true in the first pass and has not changed.
  • Size: core paths touched (packages/cli/src/config/**): ~1 418 production lines, ~1 062 test lines, 9 generated/schema lines. Maintainer-authored, so the two-tier gate does not apply. The 1 000+ line advisory is informational only — the bulk is the new boundary module and the tests pinning it.
  • Approach: scope matches the goal. Each piece maps to a named property (mount geometry, env allowlist, per-kind network, refusal wiring), and the PR deletes a duplicated run() in test-delta instead of re-synchronising the copy — the right direction. The CI-wiring change is deliberately held back as a separate PR.
  • Risk: no elevated risk signals — the Stage 1e revert-prone path patterns do not match (sandboxed-exec.ts is not sandbox.ts).

Moving on to code review. 🔍

中文说明

首轮之后又落了 22 个提交,分支在 5d2a126 合并了 main,本次为重跑。

  • 模板:完整 ✓
  • 问题:真实存在,且已在基线提交上核实,不是理论问题。buildRunEnv() 把整个 process.env 展开进被审仓库的每一次 install/build/test;探针套件的 spawn 干脆没有 env 键——完整继承,且直接跑宿主机的 process.execPath。在 CI 上,那里面有模型与 GitHub 凭据。review: decide whether the pipeline should keep granting code execution as the invoking user #9556 挂着 category/security
  • 方向:对齐——实现的是 review: decide whether the pipeline should keep granting code execution as the invoking user #9556 上记录的决策,开 issue 与写 PR 的是同一位维护者。改动触及 sandbox 机制与核心设置 schema,按本门禁的规则,最终决定权在人类维护者;首轮如此,本次不变。
  • 规模:触及核心路径(packages/cli/src/config/**):约 1418 行生产代码、约 1062 行测试、9 行生成/schema。维护者本人的 PR,两级门禁不适用。1000+ 行提示仅作信息——体量主要是新的边界模块及其钉住测试。
  • 方案:范围与目标相符。每一块都对应一个点名的性质(挂载几何、env 白名单、按类别给网络、拒绝逻辑的接线);PR 还删掉了 test-delta 里重复的 run() 而不是再同步一份副本——方向正确。接入 CI 的改动有意留作单独 PR。
  • 风险:无升级风险信号——Stage 1e 的高回滚路径模式没有命中(sandboxed-exec.ts 不是 sandbox.ts)。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 5d2a126de0109fcce82908c2cbf365e1f6f00c4b · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Code review

I wrote down my own design before reading the diff: operator-scope policy setting (off/auto/required, default off), one ephemeral --rm container per command, mount the review temp dir so the dependency farm's outward links resolve, env allowlist instead of the inherited environment, network only for installs. The PR matches that and goes past it in the places that matter:

  • Env provenance, not name lists. isFileSourcedEnvKey keeps repository-shipped .env values from deciding anything the operator's shell decides — the policy can only ever be tightened by the environment, never loosened, and QWEN_REVIEW_SANDBOX_IMAGE from the reviewed checkout is ignored outright because the image IS the code. I verified the loader only records a key as file-sourced when the real environment had nothing, so the scrub restores rather than approximates.
  • required now actually refuses. The first pass's main blocker is gone: refuseUnsandboxedPhase is consumed at the top of all three phases (build-test, test-efficacy, test-delta), including the agent-shell hand-off route the spawn gates can't see, and the refusal on --resume throws instead of clobbering the in-flight report. An unmountable tree refuses under required and falls back under auto — the contract is spelled correctly in both directions.
  • The probe suite runs node off the image's PATH, not the host's process.execPath — the first pass's ENOENT prediction is closed, with the vitest entry and probe paths quoted through shellQuotePath.
  • Reuse: test-delta's careful copy of run() is deleted and the shared one used, which is also why the base-side rerun crosses the same boundary as the PR side — the two halves of a delta can no longer drift in shape.

I also checked the base-side dependencies the module leans on: shell-quote, REVIEW_TMP_DIR, redirectedAncestor, CUSTOM_SANDBOX_IMAGE_ENV_VAR, and the file-sourced tracking sets all exist at the parent commit with the semantics the comments claim.

The two open Criticals from the review loop (R1-3, R19-1). I verified both premises at the base commit rather than taking either side's word: the worktree leases live at join(repositoryRoot, REVIEW_TMP_DIR) and cleanupReviewWorktreeLeases matches by session ids with no provenance check; and on main the reviewed repository's commands already run as the host user with the full environment and no filesystem restriction at all. So the gitfile and lease classes are reachable today, from any review, before this PR exists — containment narrows the reachable surface to the one mount rather than widening it. That makes them real problems and not regressions, and the fix (keep host-trusted state out of the writable mount) is a geometry change the PR description explicitly tracks as separate work. Whether that must land before or after this PR is a scope call for a maintainer, not a gate call — flagged in the reflection below.

CI evidence at 5d2a126

All pull-request-event workflow runs are complete: Qwen Code CI — success, Security Checks — success. The in-flight review-pr check is the bot's own review job, not this PR's CI. The macOS/Windows unit lanes and the CLI integration lane are skipped on this commit; the ubuntu suite is the one carrying the review tests.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Dependency CVE audit ✅ success
Secret scan (TruffleHog) ✅ success
Classify PR ✅ success
review-pr (bot orchestration) 🔄 in progress

Sandboxed verification would settle the one claim static review cannot: @qwen-code /verify — that the pinned argv actually starts a container against a real runtime, that the farm's ~1 700 links resolve through the mount, and that verdicts match the direct path. The author's machine has no container runtime, so every behavioural statement about the contained path rests on the pinned argv and the author's account of earlier live measurements; the suite itself exercises only the default off path. Not verified here: real-runtime behaviour (no runtime available to this review, and this run never executes PR code).

中文说明

代码审查

读 diff 之前我先写下了自己的设计:操作者作用域的策略设置(off/auto/required,默认 off)、每命令一个一次性 --rm 容器、挂载 review 临时目录以便依赖 farm 向外的链接可解析、白名单环境替代继承环境、仅 install 给网络。PR 与此一致,并在要紧处走得更远:

  • 按来源而非按名单处理 env。 isFileSourcedEnvKey 让仓库自带的 .env 无法决定任何本应由操作者 shell 决定的事——环境只能收紧策略、不能放松;来自被审仓库的 QWEN_REVIEW_SANDBOX_IMAGE 被直接忽略,因为镜像就是代码本身。已核实 loader 只在真实环境本无该键时才记为文件来源,所以擦除是精确还原而非近似。
  • required 现在真的会拒绝。 首轮的主要阻断已消除:refuseUnsandboxedPhase 在三个阶段(build-test、test-efficacy、test-delta)的顶部被消费,包括 spawn 门禁看不见的 agent shell 移交路线;--resume 上的拒绝是 throw 而不是覆盖在途报告。不可挂载的树在 required 下拒绝、在 auto 下回退——两个方向的契约都写对了。
  • 探针套件改用镜像 PATH 上的 node,不再是宿主机的 process.execPath——首轮预言的 ENOENT 已关闭,vitest 入口与探针路径经 shellQuotePath 加引号。
  • 复用:test-delta 里那份小心复制的 run() 被删除,改用共享实现——这也是 base 侧重跑与 PR 侧跨越同一边界的原因,delta 的两半不会再形状漂移。

另核实了模块依赖的基线侧设施:shell-quoteREVIEW_TMP_DIRredirectedAncestorCUSTOM_SANDBOX_IMAGE_ENV_VAR 与文件来源追踪集合都存在于父提交,且语义与注释所述一致。

审查循环遗留的两条 Critical(R1-3、R19-1)。 两边说法我都没有直接采信,而是在基线提交上亲自核实:工作树 lease 确实落在 join(repositoryRoot, REVIEW_TMP_DIR)cleanupReviewWorktreeLeases 确实只按会话标识匹配、无来源校验;而 main 上,被审仓库的命令今天就以宿主用户身份、带着完整环境、在毫无文件系统限制的情况下运行。所以 gitfile 与 lease 这两类资产在本 PR 存在之前就已可从任何一次审查中够到——容器化把可达面收窄到一个挂载,而不是扩大。它们是真问题,但不是本 PR 引入的回归;修复(把宿主可信状态移出可写挂载)属于几何改动,PR 描述已明确将其另行跟踪。该修复必须先于还是后于本 PR 落地,是维护者的范围决策,不是门禁的决策——已在反思部分点名。

5d2a126 上的 CI 证据

所有 pull_request 事件的工作流运行均已完成:Qwen Code CI — 成功Security Checks — 成功。仍在运行的 review-pr 检查是机器人自己的审查作业,不属于本 PR 的 CI。macOS/Windows 单测与 CLI 集成本提交上跳过;承载 review 测试的是 ubuntu 套件。表格见英文部分。

沙箱验证可以了结静态审查看不到的唯一论断:@qwen-code /verify——钉住的 argv 能否在真实运行时上真正起容器、farm 的约 1700 条链接能否经由挂载解析、判定是否与直接路径一致。作者机器上没有容器运行时,所以关于容器化路径的一切行为论断都建立在钉住的 argv 与作者对早前实测的转述之上;套件本身只跑默认 off 路径。本次未验证:真实运行时行为(本审查无运行时可用,且本运行从不执行 PR 代码)。

Qwen Code · qwen3.8-max

Reviewed at 5d2a126de0109fcce82908c2cbf365e1f6f00c4b · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the code is ready; the reason this doesn't end in an approval is a human decision this gate is not allowed to make, not a defect in the diff.

Stepping back: the branch that arrived is not the one I sent back in the first pass. All three things that kept it at 2/5 are fixed and pinned — the schema is regenerated and CI has been green since, required genuinely refuses (at the phase gates, including the agent-shell hand-off, with the resume case throwing instead of clobbering a report), and the probe suite runs the image's node instead of a host path the container doesn't have. Nineteen review rounds then converged the hard way: seven consecutive rounds produced no new finding before R19-1, and when R19-1 landed I checked its premises at the base commit myself — the lease files and gitfiles it names are reachable from any review today, on main, before this PR exists. The author verified the same, named both classes in the PR description's "does not close" section, and handed the fix to a separate mount-geometry change. I agree with that shape: bolting a lease-relocation into an already +2 400-line security boundary is exactly how a landable PR becomes an unlandable one.

What keeps this at 3/5 is honesty about what I can and cannot attest. The default off path is today's behaviour exactly, and CI exercises it green. The contained path has never run against a real runtime — by the author's own statement, and by construction of this review, which never executes PR code. And the change lands squarely on sandbox machinery and the core settings schema, which this gate escalates to a human maintainer regardless of how clean the review reads.

⏸️ Deferring to @pomelo-nwu / @yiliang114 — two calls need a maintainer, and neither can be made from the diff:

  1. Scope of the containment boundary. Land this PR now — default-off, opt-in, strictly narrowing today's exposure — and track the mount-geometry fix (host-trusted state: pipeline gitfiles, worktree leases) as follow-up work; or hold this PR until that fix is in. The review loop's two remaining Criticals are pre-existing by my own base-commit check, so this is a sequencing judgment, not a correctness one.
  2. Real-runtime integration. The argv is pinned but no container has ever started from it. One pass on a docker-equipped runner (or @qwen-code /verify) before QWEN_REVIEW_SANDBOX=required gets wired into CI — which the PR deliberately holds back as a separate change.

No approval and no request-changes from me this round: the branch is green, the earlier blockers are closed, and the remaining questions are policy, not code.

中文说明

置信度:3/5 —— 代码已经就绪;本轮不以批准收尾,是因为摆着一个本门禁无权代答的人类决策,而不是 diff 里还有什么缺陷。

退一步看:抵达这里的分支已不是首轮被我退回的那个。首轮压在 2/5 上的三件事都已修好且被测试钉住:schema 已重新生成、CI 自此常绿;required 真的会拒绝(在阶段门禁处、包括 agent shell 移交路线、resume 场景以 throw 代替覆盖在途报告);探针套件跑的是镜像里的 node,不再是容器里不存在的宿主路径。随后十九轮审查以最笨的方式收敛:连续七轮没有新发现,直到 R19-1;R19-1 落地时我亲自在基线提交上核了它的前提——它点名的 lease 文件与 gitfile,今天在 main 上、在本 PR 存在之前,就已可从任何一次审查中够到。作者核实了同样的事实,把这两类资产写进 PR 描述的「关不掉的」一节,并把修复交给一次单独的挂载几何改动。我认可这个形状:把 lease 迁移硬塞进一个已经 +2400 行的安全边界,正是「可落地的 PR」变成「落不了地的 PR」的标准路径。

停在 3/5 是对自己能证明与不能证明之事的诚实。默认 off 路径就是今天的行为,CI 跑它是绿的。容器化路径从未在真实运行时上跑过——作者自己这么说,本审查的构造也如此(从不执行 PR 代码)。而改动恰好落在 sandbox 机制与核心设置 schema 上,无论审查读起来多干净,本门禁都把这类改动升级给人类维护者。

⏸️ 转交 @pomelo-nwu / @yiliang114 —— 有两个决定需要维护者来做,且都无法从 diff 中得出:

  1. 容器化边界的范围。 现在落地本 PR——默认关闭、显式开启、相对今天的暴露面严格收窄——把挂载几何修复(宿主可信状态:流水线 gitfile、工作树 lease)另行跟踪;或者扣住本 PR,直到该修复完成。审查循环遗留的两条 Critical 按我自己在基线提交上的核查属于既有暴露,所以这是排序判断,不是正确性判断。
  2. 真实运行时集成。 argv 已被钉住,但还没有任何容器真正由它启动。在把 QWEN_REVIEW_SANDBOX=required 接入 CI 之前(该工作流改动被有意留作单独变更),在有 docker 的 runner 上跑一次(或 @qwen-code /verify)。

本轮我不给批准、也不请求修改:分支是绿的,早先的阻断项已关闭,遗留问题是策略问题,不是代码问题。

Qwen Code · qwen3.8-max

Reviewed at 5d2a126de0109fcce82908c2cbf365e1f6f00c4b · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs some rethinking — see my notes above. The short version: regenerate packages/vscode-ide-companion/schemas/settings.schema.json (CI is red on this commit), and either enforce the refused verdict at the two call sites or hold the required option until that lands — as it stands, required silently runs the reviewed code unsandboxed when no runtime answers. 🙏

The generated JSON Schema is checked in and CI diffs it against a fresh run
(`npm run generate:settings-schema`). Adding `review.sandbox` to
`settingsSchema.ts` without regenerating left the two out of step, which is
what the "settings.schema.json is out of date" gate is for.

The second red check, `Post Coverage Comment`, failed at "Download coverage
reports artifact" — a consequence of the test job dying before it uploaded
one, not an independent failure.
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI 85.35% 85.35% 90.69% 84.37%
Core 88.52% 88.52% 90.24% 87.03%
CLI Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   85.35 |    84.37 |   90.69 |   85.35 |                   
 src               |   85.82 |    81.89 |   88.03 |   85.82 |                   
  cli.ts           |   95.68 |    84.11 |     100 |   95.68 | ...60-561,565-566 
  gemini.tsx       |   73.34 |    78.04 |   80.76 |   73.34 | ...1336-1340,1467 
  ...ractiveCli.ts |   88.26 |    82.64 |   88.88 |   88.26 | ...3135,3141,3207 
  ...liCommands.ts |   88.93 |    83.21 |      80 |   88.93 | ...97-599,615,721 
  ...ActiveAuth.ts |     100 |     87.5 |     100 |     100 | 66-80             
 ...cp-integration |    73.5 |    76.25 |   93.17 |    73.5 |                   
  acpAgent.ts      |   72.37 |    75.94 |   92.27 |   72.37 | ...74,12285,12331 
  ...k-reporter.ts |     100 |       80 |     100 |     100 | 81,84,119,141     
  authMethods.ts   |      92 |       60 |     100 |      92 | 33-34             
  ...heap-probe.ts |   97.39 |    96.66 |     100 |   97.39 | 243,264-265       
  errorCodes.ts    |     100 |      100 |     100 |     100 |                   
  ...ion-skills.ts |     100 |    88.23 |     100 |     100 | 17,32             
  generation.ts    |    97.1 |    81.25 |     100 |    97.1 | 109,112           
  ...figuration.ts |     100 |     91.3 |     100 |     100 | 73,124            
  ...DirContext.ts |     100 |      100 |     100 |     100 |                   
  ...ersistence.ts |   94.95 |    92.24 |     100 |   94.95 | ...13-118,227-228 
  ...management.ts |   74.75 |     66.3 |     100 |   74.75 | ...92-496,505-509 
  ...e-download.ts |    64.7 |    62.24 |    87.5 |    64.7 | ...08-609,615-619 
 ...tegration/live |    97.5 |       88 |   92.85 |    97.5 |                   
  ...en-context.ts |   95.74 |    82.35 |     100 |   95.74 | ...0,66-67,99-100 
  ...structions.ts |     100 |      100 |     100 |     100 |                   
  ...ak-to-user.ts |   96.66 |      100 |    87.5 |   96.66 | 37-38             
  ...task-tools.ts |   98.97 |      100 |   88.88 |   98.97 | 201-202           
 ...ration/service |    97.1 |    95.89 |   93.75 |    97.1 |                   
  filesystem.ts    |    97.1 |    95.89 |   93.75 |    97.1 | ...22-123,246-247 
 ...ration/session |   91.05 |    86.57 |   95.75 |   91.05 |                   
  Session.ts       |    90.4 |    85.32 |   95.13 |    90.4 | ...50,12677-12681 
  ...entTracker.ts |   96.81 |    89.36 |      90 |   96.81 | 137-143,222       
  ...projection.ts |   98.85 |    91.59 |     100 |   98.85 | 234,250,262       
  ...stop-guard.ts |     100 |    98.07 |     100 |     100 | 37,127            
  ...eplay-page.ts |   94.16 |    86.36 |     100 |   94.16 | ...43,347,427,431 
  ...y-replayer.ts |   83.41 |    93.22 |   94.11 |   83.41 | ...29-147,265-267 
  index.ts         |       0 |        0 |       0 |       0 | 1-40              
  ...ssionUtils.ts |   89.76 |    87.32 |     100 |   89.76 | ...54-270,326-328 
  ...oal-update.ts |   98.61 |    97.29 |     100 |   98.61 | 64                
  ...lure-guard.ts |   98.32 |    97.72 |     100 |   98.32 | 294-295,340-341   
  tasksSnapshot.ts |    94.3 |     87.5 |     100 |    94.3 | 65-71             
  ...on-tracker.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...ssion/emitters |   95.62 |    92.73 |   97.05 |   95.62 |                   
  ...ageEmitter.ts |   95.25 |    93.54 |     100 |   95.25 | ...08-115,128-129 
  PlanEmitter.ts   |     100 |       90 |     100 |     100 | 66                
  base-emitter.ts  |   78.26 |    77.77 |     100 |   78.26 | 23-24,26-28       
  index.ts         |       0 |        0 |       0 |       0 | 1-10              
  ...ll-emitter.ts |   98.57 |    94.84 |     100 |   98.57 | 75-76,394-395     
 ...ession/rewrite |    91.8 |    89.13 |   94.44 |    91.8 |                   
  LlmRewriter.ts   |    82.4 |     86.2 |     100 |    82.4 | ...,88-89,166-170 
  ...Middleware.ts |   96.96 |    88.09 |     100 |   96.96 | 144,152-154       
  TurnBuffer.ts    |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 src/agent-view    |      89 |    81.59 |   91.53 |      89 |                   
  attach-lease.ts  |     100 |    96.96 |     100 |     100 | 173               
  ...t-cli-argv.ts |     100 |      100 |     100 |     100 |                   
  ...ged-detach.ts |     100 |     90.9 |     100 |     100 | 40,64             
  protocol.ts      |     100 |      100 |     100 |     100 |                   
  pty-host-env.ts  |     100 |      100 |     100 |     100 |                   
  ...st-process.ts |   87.99 |     77.6 |   94.28 |   87.99 | ...1219,1309-1311 
  pty-host.ts      |   84.51 |    85.04 |   90.69 |   84.51 | ...14-516,531-532 
  ...sor-client.ts |   80.38 |    72.81 |   77.41 |   80.38 | ...22-626,652-656 
  ...or-process.ts |   96.61 |    89.47 |   84.61 |   96.61 | 129-130,150-151   
  ...sor-runner.ts |    84.9 |     75.6 |      85 |    84.9 | ...44,468,471-481 
  ...sor-server.ts |   85.71 |    83.06 |   95.45 |   85.71 | ...67-468,471-488 
  ...isor-store.ts |   97.73 |    81.16 |     100 |   97.73 | ...92,594,607,643 
  ...nal-bridge.ts |   93.98 |    91.54 |   83.33 |   93.98 | 228-238           
  ...r-sideband.ts |   95.37 |    86.44 |     100 |   95.37 | 203-204,228-233   
 src/commands      |   90.66 |    78.53 |   65.62 |   90.66 |                   
  auth.ts          |     100 |    83.33 |     100 |     100 | 11,14             
  channel.ts       |   55.55 |      100 |       0 |   55.55 | 18-22,30-40       
  extensions.tsx   |   96.77 |      100 |      50 |   96.77 | 39                
  hooks.tsx        |   66.66 |      100 |       0 |   66.66 | 20-24             
  mcp.ts           |   95.45 |      100 |      50 |   95.45 | 31                
  review.ts        |   98.85 |      100 |      50 |   98.85 | 98                
  serve.ts         |   89.46 |    76.02 |     100 |   89.46 | ...12-915,927,938 
  sessions.ts      |     100 |      100 |      50 |     100 |                   
  update.ts        |   98.13 |    94.44 |   66.66 |   98.13 | 82-83             
 ...mmands/channel |   89.06 |    88.56 |   90.64 |   89.06 |                   
  channel-cwd.ts   |     100 |      100 |     100 |     100 |                   
  ...l-registry.ts |   94.88 |    95.49 |      90 |   94.88 | ...20-323,368-371 
  ...entry-path.ts |      75 |       50 |     100 |      75 | 8-9               
  config-utils.ts  |   95.83 |    96.35 |     100 |   95.83 | ...03-208,266-269 
  configure.ts     |    14.7 |      100 |       0 |    14.7 | 18-21,23-84       
  daemon-worker.ts |   93.91 |    85.55 |   94.33 |   93.91 | ...1264,1271-1272 
  loop-runtime.ts  |   91.66 |      100 |      50 |   91.66 | 15,22             
  ...classifier.ts |   98.53 |    96.66 |     100 |   98.53 | 115-116,161       
  ...tact-store.ts |   93.51 |    87.65 |     100 |   93.51 | ...71,288-289,337 
  pairing.ts       |      75 |      100 |      50 |      75 | 22-28,59-70       
  pidfile.ts       |   95.55 |       90 |     100 |   95.55 | ...50-251,315-316 
  proxy.ts         |     100 |      100 |     100 |     100 |                   
  reload.ts        |    77.5 |    86.95 |      75 |    77.5 | 72-84,93-97       
  runtime.ts       |   82.43 |    86.44 |     100 |   82.43 | ...87-191,251-253 
  set.ts           |   75.72 |    85.71 |      50 |   75.72 | 65-83,111-116     
  start.ts         |    85.8 |    82.17 |      88 |    85.8 | ...85,591-594,606 
  ...ure-format.ts |   93.65 |    82.45 |     100 |   93.65 | ...42,48-49,74-75 
  status.ts        |   78.57 |    59.25 |   66.66 |   78.57 | ...36-137,150-161 
  stop.ts          |   57.83 |    82.35 |      50 |   57.83 | ...3,74-76,85-111 
 ...nds/extensions |   88.85 |    87.73 |   87.09 |   88.85 |                   
  consent.ts       |   72.53 |    90.32 |   42.85 |   72.53 | ...86-142,157-163 
  disable.ts       |     100 |       90 |     100 |     100 | 30                
  enable.ts        |     100 |    91.66 |     100 |     100 | 38                
  install.ts       |   82.95 |    81.57 |      75 |   82.95 | ...96-199,202-211 
  link.ts          |     100 |      100 |     100 |     100 |                   
  list.ts          |     100 |     87.5 |     100 |     100 | 18                
  new.ts           |     100 |      100 |     100 |     100 |                   
  settings.ts      |   99.15 |      100 |   83.33 |   99.15 | 151               
  sources.ts       |   93.42 |    87.09 |   92.85 |   93.42 | ...4-66,96-98,167 
  uninstall.ts     |   74.57 |       40 |   66.66 |   74.57 | 45-47,60-67,70-73 
  update.ts        |   96.71 |    97.05 |     100 |   96.71 | 114-118           
  utils.ts         |   75.63 |    55.55 |     100 |   75.63 | ...30-134,136-140 
 ...les/mcp-server |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-60              
 ...amples/starter |       0 |        0 |       0 |       0 |                   
  example.ts       |       0 |        0 |       0 |       0 | 1-64              
 src/commands/mcp  |   90.25 |    84.61 |   83.33 |   90.25 |                   
  add.ts           |    99.3 |    96.07 |     100 |    99.3 | 154-155           
  approve.ts       |   76.19 |     87.5 |   66.66 |   76.19 | ...,89-99,114-124 
  list.ts          |    92.9 |    84.84 |      80 |    92.9 | ...79-181,199-200 
  reconnect.ts     |   78.85 |    66.66 |   85.71 |   78.85 | 42-55,169-191     
  remove.ts        |     100 |       80 |     100 |     100 | 21-25             
 ...ommands/review |   91.63 |    89.98 |   93.01 |   91.63 |                   
  agent-prompt.ts  |   94.89 |    92.99 |   97.95 |   94.89 | ...3286,3621-3701 
  base-tree.ts     |   77.02 |    80.76 |   77.77 |   77.02 | ...63-384,386-399 
  capture-local.ts |   73.58 |     90.9 |      75 |   73.58 | 112-116,163-186   
  ...k-coverage.ts |   50.71 |       35 |   66.66 |   50.71 | ...40-245,279-289 
  cleanup.ts       |   92.18 |    89.69 |    90.9 |   92.18 | ...1061,1063-1064 
  comment-body.ts  |   67.85 |    87.09 |   66.66 |   67.85 | ...30,157,159-164 
  ...ent-status.ts |   94.22 |    87.32 |    90.9 |   94.22 | ...96,462,738-758 
  ...ose-review.ts |   97.17 |    93.76 |   98.52 |   97.17 | ...5563-5607,5882 
  cost-ledger.ts   |   94.58 |     94.4 |   81.25 |   94.58 | ...53-654,694-704 
  drive.ts         |    94.1 |    92.85 |   92.85 |    94.1 | ...80-782,787-789 
  extract-step.ts  |   91.36 |    90.62 |   88.88 |   91.36 | ...90-707,714-729 
  fetch-diff.ts    |   73.75 |      100 |   66.66 |   73.75 | 77-97             
  fetch-pr.ts      |   97.29 |    92.25 |     100 |   97.29 | ...1566,1724-1729 
  findings.ts      |   96.02 |    92.15 |     100 |   96.02 | ...1249,1258-1259 
  issue-context.ts |   88.15 |     93.1 |   85.71 |   88.15 | 249-276           
  load-rules.ts    |   26.41 |      100 |   16.66 |   26.41 | ...41-153,155-156 
  match-remote.ts  |   85.55 |     92.3 |   66.66 |   85.55 | 74-79,144-150     
  meta.ts          |   79.43 |    93.75 |   66.66 |   79.43 | 123-128,147-162   
  mock-provider.ts |   95.44 |    90.25 |   89.47 |   95.44 | 145,690-709       
  parse-args.ts    |   99.49 |    95.66 |     100 |   99.49 | 585,856,912       
  plan-diff.ts     |   71.42 |      100 |   66.66 |   71.42 | 162-197           
  pr-context.ts    |   96.03 |    87.58 |     100 |   96.03 | ...2233,2333-2349 
  presubmit.ts     |   94.32 |    90.83 |   94.11 |   94.32 | ...1214,1249-1280 
  ...ish-assets.ts |    81.3 |    82.22 |   85.71 |    81.3 | ...75-479,506-552 
  ...r-findings.ts |   90.74 |    83.75 |     100 |   90.74 | ...17-422,429-430 
  repo-context.ts  |   94.62 |    90.75 |     100 |   94.62 | ...66-467,482-487 
  ...ve-anchors.ts |   78.34 |    89.28 |      75 |   78.34 | ...83-188,200-217 
  run.ts           |   82.66 |    88.54 |   94.11 |   82.66 | ...22,638-692,706 
  save-artifact.ts |    94.2 |    92.46 |   94.11 |    94.2 | ...14-617,710-713 
  scratch-tree.ts  |   95.93 |       86 |     100 |   95.93 | ...91-392,461-464 
  script-lint.ts   |   83.78 |    78.57 |   88.88 |   83.78 | ...69-783,785-807 
  submit.ts        |   94.11 |    89.38 |   94.44 |   94.11 | ...1673,1701-1738 
  test-delta.ts    |   95.75 |     92.3 |      75 |   95.75 | 470-478           
  test-efficacy.ts |   84.03 |    80.48 |   96.07 |   84.03 | ...3249,3257-3277 
  test-plan.ts     |   94.61 |    91.79 |      95 |   94.61 | ...29-832,873-874 
 ...w/__fixtures__ |     100 |      100 |     100 |     100 |                   
  ...r-default.mjs |     100 |      100 |     100 |     100 |                   
  ...der-empty.mjs |     100 |      100 |     100 |     100 |                   
  ...der-named.mjs |     100 |      100 |     100 |     100 |                   
 ...nds/review/lib |   97.29 |    94.74 |   98.61 |   97.29 |                   
  agent-briefs.ts  |   99.08 |      100 |      50 |   99.08 | 822-823           
  ...t-identity.ts |     100 |      100 |     100 |     100 |                   
  anchors.ts       |     100 |    97.04 |     100 |     100 | ...39,175,184,231 
  assets.ts        |     100 |      100 |     100 |     100 |                   
  audit-layers.ts  |   98.67 |    96.15 |     100 |   98.67 | 288-290           
  authorization.ts |   93.48 |    93.45 |     100 |   93.48 | ...79-385,583-584 
  budget.ts        |     100 |    97.95 |     100 |     100 | 887,940           
  build-budget.ts  |     100 |      100 |     100 |     100 |                   
  certification.ts |     100 |      100 |     100 |     100 |                   
  convergence.ts   |   99.46 |    97.17 |    90.9 |   99.46 | 590,808           
  coverage.ts      |   98.97 |    95.11 |     100 |   98.97 | ...1103,1648-1649 
  deadline.ts      |   98.03 |    91.66 |     100 |   98.03 | ...20,752,820,837 
  diff-flags.ts    |     100 |        0 |     100 |     100 | 75                
  diff-plan.ts     |   98.77 |    93.26 |     100 |   98.77 | ...78,301,327-328 
  disk.ts          |     100 |      100 |     100 |     100 |                   
  effort.ts        |     100 |      100 |     100 |     100 |                   
  failing-files.ts |     100 |    93.33 |     100 |     100 | 41                
  gh.ts            |   89.53 |    95.52 |   78.94 |   89.53 | ...47,384-385,412 
  git.ts           |   96.77 |    93.93 |     100 |   96.77 | 234-235,272-273   
  heavy.ts         |     100 |      100 |     100 |     100 |                   
  import-graph.ts  |   96.68 |     95.4 |     100 |   96.68 | 180-182,211-212   
  ...ntal-scope.ts |     100 |      100 |     100 |     100 |                   
  inline-counts.ts |     100 |      100 |     100 |     100 |                   
  ...audit-gate.ts |     100 |     97.5 |     100 |     100 | 135               
  ledger.ts        |     100 |      100 |     100 |     100 |                   
  local-diff.ts    |   84.86 |    90.38 |     100 |   84.86 | ...63-473,475-483 
  ...ry-context.ts |   96.61 |    95.48 |     100 |   96.61 | ...47-450,496-499 
  md-field.ts      |     100 |      100 |     100 |     100 |                   
  merge-base.ts    |     100 |      100 |     100 |     100 |                   
  narrow-diff.ts   |     100 |      100 |     100 |     100 |                   
  npm-toolchain.ts |   98.23 |    95.29 |     100 |   98.23 | ...,822,1203,1220 
  path-rules.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   96.96 |       95 |     100 |   96.96 | 32-33             
  prompt-record.ts |   98.03 |    94.23 |     100 |   98.03 | 293-294,300       
  receipt.ts       |     100 |      100 |     100 |     100 |                   
  remote-match.ts  |   98.03 |    94.73 |     100 |   98.03 | 109-110           
  report.ts        |   92.92 |    86.66 |     100 |   92.92 | 213-214,216-220   
  ...ry-context.ts |     100 |    98.66 |     100 |     100 | 187               
  resume.ts        |     100 |      100 |     100 |     100 |                   
  retirement.ts    |     100 |    94.36 |     100 |     100 | ...58-559,760,917 
  review-footer.ts |   99.55 |     98.1 |     100 |   99.55 | 548-549           
  ...w-settings.ts |     100 |    96.42 |     100 |     100 | 99                
  roster.ts        |     100 |    97.14 |     100 |     100 | 177,222           
  round-model.ts   |     100 |      100 |     100 |     100 |                   
  run-ledger.ts    |    98.2 |    93.87 |     100 |    98.2 | ...23,541,647,670 
  same-file.ts     |     100 |    94.11 |     100 |     100 | 35                
  ...boxed-exec.ts |   94.55 |    92.39 |   95.45 |   94.55 | ...91-492,542-543 
  shell-quote.ts   |     100 |      100 |     100 |     100 |                   
  stale-bundle.ts  |   98.18 |    94.04 |     100 |   98.18 | 431,472,512-513   
  test-utils.ts    |   99.04 |    91.66 |     100 |   99.04 | 75                
  toolchain.ts     |     100 |      100 |     100 |     100 |                   
  transcripts.ts   |   98.09 |    95.07 |     100 |   98.09 | ...92,438,707-708 
  ...pace-scope.ts |     100 |    96.96 |     100 |     100 | 186               
  workspaces.ts    |     100 |    96.85 |     100 |     100 | 222,452,499,512   
  ...ree-reader.ts |     100 |      100 |     100 |     100 |                   
  worktree.ts      |   89.39 |    81.78 |     100 |   89.39 | ...1813-1814,1827 
 ...w/lib/platform |   94.71 |    87.89 |   97.05 |   94.71 |                   
  aone-client.ts   |   94.94 |     87.3 |     100 |   94.94 | ...92-293,299-302 
  aone.ts          |   93.06 |    89.86 |   94.73 |   93.06 | ...34,598-603,655 
  github.ts        |   99.08 |     75.8 |     100 |   99.08 | 249-250           
  registry.ts      |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...mands/sessions |   94.11 |    89.06 |   89.47 |   94.11 |                   
  common.ts        |     100 |      100 |     100 |     100 |                   
  list.ts          |   90.96 |    86.66 |   81.81 |   90.96 | 208-219,221-222   
  ps.ts            |     100 |    94.44 |     100 |     100 | 58                
 src/config        |   94.24 |    90.33 |   95.02 |   94.24 |                   
  ...l-fallback.ts |     100 |      100 |     100 |     100 |                   
  auth.ts          |   93.36 |    88.37 |     100 |   93.36 | ...06-307,330-331 
  ...eMcpImport.ts |   87.91 |    81.52 |     100 |   87.91 | ...63-371,453-454 
  compile-cache.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |   89.54 |    90.29 |   83.78 |   89.54 | ...2497,2499-2507 
  ...cy-monitor.ts |      90 |    77.27 |     100 |      90 | ...72-73,90-92,98 
  ...ust-policy.ts |   83.02 |    88.88 |     100 |   83.02 | ...02-209,232-240 
  ...heme-names.ts |     100 |      100 |     100 |     100 |                   
  ...ScopeUtils.ts |   97.56 |    88.88 |     100 |   97.56 | 67                
  environment.ts   |   94.51 |    92.55 |   95.23 |   94.51 | ...24-625,679-680 
  ...le-watcher.ts |   90.86 |    83.65 |   95.83 |   90.86 | ...23-325,370,418 
  ...resh-state.ts |   90.57 |    97.29 |   93.75 |   90.57 | 137-142,146-152   
  ...ime-reload.ts |     100 |    69.69 |     100 |     100 | ...12-113,122-123 
  hot-reload.ts    |     100 |    89.13 |     100 |     100 | 47,172-178,238    
  keyBindings.ts   |    97.4 |       50 |     100 |    97.4 | 240-243           
  ...ngsAdapter.ts |     100 |    94.11 |     100 |     100 | 64                
  ...ig-watcher.ts |   95.17 |    83.05 |     100 |   95.17 | ...78,200,292-293 
  ...er-secrets.ts |   98.97 |    96.96 |     100 |   98.97 | 85                
  mcpApprovals.ts  |   78.57 |       92 |   86.66 |   78.57 | ...18-319,324-326 
  mcpJson.ts       |     100 |      100 |     100 |     100 |                   
  mcpServers.ts    |   92.85 |     87.5 |     100 |   92.85 | 46-47             
  ...idersScope.ts |      95 |    94.73 |     100 |      95 | 11-12             
  ...abledTools.ts |     100 |      100 |     100 |     100 |                   
  ...comparison.ts |     100 |      100 |     100 |     100 |                   
  ...n-settings.ts |   99.15 |    93.93 |     100 |   99.15 | 63                
  sandboxConfig.ts |   93.33 |    93.33 |     100 |   93.33 | ...42-147,216-217 
  session-id.ts    |     100 |      100 |     100 |     100 |                   
  ...ings-cache.ts |   96.52 |    93.93 |     100 |   96.52 | 90-91,201-202     
  settings.ts      |   91.16 |    92.89 |      90 |   91.16 | ...1027,1029-1030 
  ...ingsSchema.ts |     100 |      100 |     100 |     100 |                   
  settingsUtils.ts |   80.82 |     89.2 |   85.18 |   80.82 | ...85-603,610-618 
  ...ngsWatcher.ts |   95.54 |    88.34 |     100 |   95.54 | ...28,277-278,293 
  ...d-env-keys.ts |     100 |      100 |     100 |     100 |                   
  ...l-settings.ts |     100 |      100 |     100 |     100 |                   
  ...paths-lite.ts |   89.47 |       88 |     100 |   89.47 | 43-44,53-54,56-57 
  ...precedence.ts |   98.79 |     92.3 |     100 |   98.79 | 62                
  ...tedFolders.ts |   92.53 |    93.54 |     100 |   92.53 | ...36-337,373-384 
 ...nfig/migration |   95.23 |    78.94 |   85.71 |   95.23 |                   
  index.ts         |   95.65 |     87.5 |     100 |   95.65 | 117-118           
  scheduler.ts     |   96.55 |       80 |     100 |   96.55 | 19-20             
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...ation/versions |   94.91 |      100 |     100 |   94.91 |                   
  ...-v2-shared.ts |     100 |      100 |     100 |     100 |                   
  v1-to-v2.ts      |   81.75 |      100 |     100 |   81.75 | ...28-229,231-247 
  v2-to-v3.ts      |     100 |      100 |     100 |     100 |                   
  v3-to-v4.ts      |     100 |      100 |     100 |     100 |                   
  v5-to-v4.ts      |      96 |      100 |     100 |      96 | 94-95,99          
 src/core          |     100 |      100 |     100 |     100 |                   
  auth.ts          |     100 |      100 |     100 |     100 |                   
  initializer.ts   |     100 |      100 |     100 |     100 |                   
  theme.ts         |     100 |      100 |     100 |     100 |                   
 src/dualOutput    |   75.08 |    67.64 |   71.42 |   75.08 |                   
  ...tputBridge.ts |   75.33 |    68.18 |   73.68 |   75.33 | ...09-410,418-421 
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/export        |       0 |        0 |       0 |       0 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-7               
 src/generated     |     100 |      100 |     100 |     100 |                   
  git-commit.ts    |     100 |      100 |     100 |     100 |                   
 src/hooks         |     100 |      100 |     100 |     100 |                   
  ...elete-hook.ts |     100 |      100 |     100 |     100 |                   
 src/i18n          |   89.68 |    88.66 |   93.02 |   89.68 |                   
  index.ts         |   73.45 |    77.77 |      90 |   73.45 | ...70-271,294-299 
  languageUtils.ts |   98.88 |    97.01 |     100 |   98.88 | 184-185           
  languages.ts     |   93.07 |     92.3 |   85.71 |   93.07 | ...35,164-169,184 
  ...nslateKeys.ts |     100 |      100 |     100 |     100 |                   
  ...lationDict.ts |   93.33 |    66.66 |     100 |   93.33 | 15                
 src/i18n/locales  |     100 |      100 |     100 |     100 |                   
  ca.js            |     100 |      100 |     100 |     100 |                   
  de.js            |     100 |      100 |     100 |     100 |                   
  en.js            |     100 |      100 |     100 |     100 |                   
  fr.js            |     100 |      100 |     100 |     100 |                   
  ja.js            |     100 |      100 |     100 |     100 |                   
  pt.js            |     100 |      100 |     100 |     100 |                   
  ru.js            |     100 |      100 |     100 |     100 |                   
  zh-TW.js         |     100 |      100 |     100 |     100 |                   
  zh.js            |     100 |      100 |     100 |     100 |                   
 ...nonInteractive |   87.37 |    83.73 |   89.32 |   87.37 |                   
  ...ng-failure.ts |     100 |      100 |     100 |     100 |                   
  ...iveHelpers.ts |   94.95 |    91.05 |     100 |   94.95 | ...30-431,529,542 
  ...uggestions.ts |   84.29 |    70.83 |     100 |   84.29 | 70-76,92-103      
  session.ts       |   84.97 |    76.31 |   96.07 |   84.97 | ...1048,1057-1067 
  ...iagnostics.ts |    95.8 |     87.5 |   93.75 |    95.8 | ...03,277-278,289 
  types.ts         |    42.5 |      100 |   33.33 |    42.5 | ...33-634,637-638 
 ...active/control |   75.54 |    89.83 |      80 |   75.54 |                   
  ...rolContext.ts |    6.06 |        0 |       0 |    6.06 | 57-99             
  ...Dispatcher.ts |   91.95 |    92.98 |   88.88 |   91.95 | ...54-372,392,395 
  ...rolService.ts |    6.89 |        0 |       0 |    6.89 | 46-188            
 ...ol/controllers |   57.47 |     66.3 |   73.68 |   57.47 |                   
  ...Controller.ts |    42.4 |      100 |   83.33 |    42.4 | 101-105,140-223   
  ...Controller.ts |       0 |        0 |       0 |       0 | 1-56              
  ...Controller.ts |   70.04 |    62.92 |   91.66 |   70.04 | ...11-620,635-640 
  ...Controller.ts |   49.23 |       60 |      50 |   49.23 | ...07-108,111-121 
  ...Controller.ts |   53.96 |    67.08 |   66.66 |   53.96 | ...78-690,699-728 
 .../control/types |       0 |        0 |       0 |       0 |                   
  serviceAPIs.ts   |       0 |        0 |       0 |       0 | 1                 
 ...Interactive/io |   98.18 |    94.09 |   95.34 |   98.18 |                   
  ...putAdapter.ts |   98.07 |    93.18 |   98.11 |   98.07 | ...1448,1464-1465 
  ...putAdapter.ts |   96.22 |    91.66 |   85.71 |   96.22 | 52-53             
  ...nputReader.ts |     100 |    94.73 |     100 |     100 | 67                
  ...putAdapter.ts |   98.51 |      100 |   90.47 |   98.51 | 90-91,131-132     
  ...projection.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/patches       |       0 |        0 |       0 |       0 |                   
  is-in-ci.ts      |       0 |        0 |       0 |       0 | 1-17              
 src/remoteInput   |   87.31 |    75.32 |   88.23 |   87.31 |                   
  ...utContext.tsx |     100 |      100 |     100 |     100 |                   
  ...putWatcher.ts |   88.01 |       76 |   93.33 |   88.01 | ...49-350,361-364 
  index.ts         |       0 |        0 |       0 |       0 | 1-8               
 src/runtime       |   99.69 |    96.28 |     100 |   99.69 |                   
  ...livery-ipc.ts |     100 |    91.17 |     100 |     100 | 94,106,134        
  ...l-delivery.ts |     100 |      100 |     100 |     100 |                   
  cpu-percent.ts   |     100 |      100 |     100 |     100 |                   
  ...ion-source.ts |     100 |      100 |     100 |     100 |                   
  ...erver-name.ts |     100 |      100 |     100 |     100 |                   
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  ...-summaries.ts |   86.66 |       50 |     100 |   86.66 | 11,19             
  ...ber-errors.ts |     100 |    95.32 |     100 |     100 | 53,93-94,172,192  
  ...ls-mapping.ts |     100 |      100 |     100 |     100 |                   
 src/serve         |   87.24 |    84.44 |    90.8 |   87.24 |                   
  ...extra-args.ts |     100 |      100 |     100 |     100 |                   
  ...tp-enabled.ts |     100 |      100 |     100 |     100 |                   
  ...ion-bridge.ts |     100 |      100 |     100 |     100 |                   
  auth.ts          |   93.99 |     91.5 |     100 |   93.99 | ...29-430,433-435 
  ...em-adapter.ts |     100 |      100 |     100 |     100 |                   
  capabilities.ts  |     100 |    98.07 |     100 |     100 | 702               
  ...cp-command.ts |     100 |      100 |     100 |     100 |                   
  ...horization.ts |   92.79 |    93.54 |    87.5 |   92.79 | 75-80,135-136     
  ...op-mcp-ipc.ts |   81.06 |    73.68 |   94.11 |   81.06 | ...37-242,267,289 
  ...nt-service.ts |    94.1 |    86.98 |     100 |    94.1 | ...75-477,484,486 
  ...-selection.ts |     100 |      100 |     100 |     100 |                   
  ...ings-store.ts |   89.64 |    94.16 |   96.55 |   89.64 | ...57-269,521-524 
  ...ebhook-ipc.ts |    98.5 |     87.5 |     100 |    98.5 | 47                
  ...iagnostics.ts |     100 |      100 |     100 |     100 |                   
  ...worker-env.ts |     100 |      100 |     100 |     100 |                   
  ...rker-group.ts |   87.27 |     85.2 |     100 |   87.27 | ...10,816-820,838 
  ...er-manager.ts |   89.39 |    83.88 |   93.33 |   89.39 | ...98,711,722-724 
  ...horization.ts |     100 |      100 |     100 |     100 |                   
  ...tartup-ipc.ts |   97.72 |    96.66 |     100 |   97.72 | 88-89             
  ...supervisor.ts |   92.54 |    84.53 |   97.14 |   92.54 | ...1489,1543-1547 
  ...e-grouping.ts |     100 |    94.28 |     100 |     100 | 71,137            
  core-runtime.ts  |     100 |      100 |     100 |     100 |                   
  ...ub-session.ts |    90.9 |     78.6 |   94.73 |    90.9 | ...1001,1022-1027 
  ...tree-guard.ts |   92.89 |    87.55 |     100 |   92.89 | ...2766,2836-2840 
  daemon-logger.ts |   82.82 |    78.68 |   92.04 |   82.82 | ...1775,1802-1808 
  ...y-pressure.ts |     100 |    96.96 |     100 |     100 | 135               
  ...trics-ring.ts |     100 |      100 |     100 |     100 |                   
  ...s-provider.ts |   68.04 |    52.77 |     100 |   68.04 | ...44-249,282-290 
  daemon-status.ts |   98.69 |    91.96 |     100 |   98.69 | ...1590,1592-1593 
  debug-mode.ts    |     100 |      100 |     100 |     100 |                   
  env-snapshot.ts  |   93.37 |    85.18 |     100 |   93.37 | 114-117,195-202   
  ...-scheduler.ts |   87.34 |    83.87 |     100 |   87.34 | 33-36,48-50,79-81 
  ...d-provider.ts |   92.06 |    87.09 |     100 |   92.06 | ...72,287-293,316 
  ...h-settings.ts |   94.94 |    90.45 |     100 |   94.94 | ...30,708,724,734 
  fast-path.ts     |   91.38 |       82 |   95.45 |   91.38 | ...46-555,633-634 
  ...ration-sse.ts |   42.55 |    33.33 |     100 |   42.55 | 23-24,30,33-56    
  health-query.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-149             
  ...e-observer.ts |   89.89 |    83.24 |      96 |   89.89 | ...11-512,541-543 
  ...back-binds.ts |     100 |    88.88 |     100 |     100 | 32                
  ...-workspace.ts |    90.9 |    85.71 |     100 |    90.9 | ...30-131,142-143 
  ...pp-sandbox.ts |   96.72 |    95.23 |     100 |   96.72 | 41-42             
  ...iders-edit.ts |     100 |    82.14 |     100 |     100 | 58-60,65,81       
  ...ory-picker.ts |     100 |    86.95 |     100 |     100 | 36,66,92          
  ...-with-auth.ts |     100 |      100 |     100 |     100 |                   
  ...sion-audit.ts |     100 |      100 |   93.33 |     100 |                   
  ...nal-ledger.ts |    94.9 |    84.78 |     100 |    94.9 | ...81,302,361-362 
  rate-limit.ts    |   92.68 |    88.29 |     100 |   92.68 | ...89-291,303-305 
  ...qwen-serve.ts |   84.04 |    80.69 |   76.06 |   84.04 | ...7995,8013-8017 
  ...tup-errors.ts |     100 |      100 |     100 |     100 |                   
  sandbox.ts       |   45.52 |    59.42 |   76.92 |   45.52 | ...1050,1062-1085 
  ...-keepalive.ts |   94.31 |    88.28 |     100 |   94.31 | ...37,541-542,581 
  ...-lifecycle.ts |     100 |      100 |     100 |     100 |                   
  ...-lifecycle.ts |   89.16 |    90.29 |   86.95 |   89.16 | ...24-325,330-334 
  serve-token.ts   |     100 |      100 |     100 |     100 |                   
  server.ts        |   91.16 |    90.45 |   71.42 |   91.16 | ...3012,3042-3043 
  ...-admission.ts |   99.13 |    95.94 |     100 |   99.13 | 308-309           
  ...on-helpers.ts |     100 |      100 |     100 |     100 |                   
  ...-redaction.ts |     100 |      100 |     100 |     100 |                   
  ...t-event-id.ts |     100 |    95.23 |     100 |     100 | 12                
  ...-admission.ts |   98.71 |    89.65 |     100 |   98.71 | 68                
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...ion-limits.ts |     100 |      100 |     100 |     100 |                   
  ...t-sessions.ts |   93.72 |    77.93 |     100 |   93.72 | ...51,854,867-869 
  ...l-resolver.ts |   90.32 |    66.66 |     100 |   90.32 | 16,45-46          
  ...ell-static.ts |   93.45 |    86.88 |     100 |   93.45 | ...77-280,323-326 
  ...ace-agents.ts |   66.13 |    70.57 |   92.68 |   66.13 | ...2246,2256-2266 
  ...generation.ts |    95.4 |    82.35 |   66.66 |    95.4 | 55-56,78,92       
  ...-git-state.ts |     100 |    91.93 |    90.9 |     100 | 161,172,202,265   
  ...ace-inputs.ts |     100 |      100 |     100 |     100 |                   
  ...ace-memory.ts |      83 |    74.54 |     100 |      83 | ...30-537,597-604 
  ...ers-status.ts |    98.6 |     79.8 |     100 |    98.6 | 108,136,179,182   
  ...tion-store.ts |   89.67 |    88.27 |   92.59 |   89.67 | ...91-400,411-414 
  ...e-registry.ts |   94.98 |    90.55 |     100 |   94.98 | ...67-568,575-576 
  ...e-remember.ts |   98.23 |    92.56 |     100 |   98.23 | ...36,340-345,386 
  ...te-runtime.ts |    89.4 |    90.55 |     100 |    89.4 | ...89-190,258-279 
  ...me-storage.ts |     100 |      100 |     100 |     100 |                   
  ...visibility.ts |     100 |      100 |     100 |     100 |                   
  ...management.ts |   72.63 |    72.83 |   96.15 |   72.63 | ...88-889,896-900 
  ...lls-status.ts |     100 |    95.45 |     100 |     100 | 152               
  ...reconciler.ts |   91.63 |    84.09 |     100 |   91.63 | ...71-273,306-307 
 ...serve/acp-http |   80.34 |    80.22 |    94.5 |   80.34 |                   
  ...r-registry.ts |   96.92 |    94.87 |     100 |   96.92 | 184-187           
  client-mcp-ws.ts |   54.85 |    58.62 |   72.72 |   54.85 | ...99-300,304-305 
  ...n-registry.ts |   93.03 |    84.13 |   98.52 |   93.03 | ...1624,1671-1682 
  dispatch.ts      |   75.51 |    77.21 |   93.33 |   75.51 | ...5509,5566-5572 
  index.ts         |   82.68 |    79.74 |   91.22 |   82.68 | ...2424,2510-2511 
  json-rpc.ts      |     100 |    96.96 |     100 |     100 | 92                
  ...ach-budget.ts |     100 |      100 |     100 |     100 |                   
  safe-ws-send.ts  |   52.94 |    71.42 |     100 |   52.94 | 33-42,47-55       
  sse-stream.ts    |   98.26 |    88.75 |     100 |   98.26 | 87-88,117         
  ...ort-stream.ts |       0 |        0 |       0 |       0 | 1                 
  ws-stream.ts     |   94.06 |    89.09 |     100 |   94.06 | 50,55,134,138-141 
 src/serve/auth    |   86.86 |     79.7 |   93.87 |   86.86 |                   
  device-flow.ts   |   96.35 |    80.57 |   97.61 |   96.35 | ...1358,1453,1519 
  ...w-provider.ts |   44.24 |    74.07 |   71.42 |   44.24 | ...23-284,297,301 
 ...rve/cdp-tunnel |   87.73 |    76.21 |    97.5 |   87.73 |                   
  ...r-emulator.ts |   93.27 |    77.77 |     100 |   93.27 | ...53-256,282-283 
  ...verse-link.ts |      88 |    76.19 |     100 |      88 | ...28-329,420-423 
  ...l-registry.ts |     100 |      100 |     100 |     100 |                   
  cdp-ws.ts        |   76.28 |    61.29 |    87.5 |   76.28 | ...13-217,223-228 
 ...nel/acceptance |    6.12 |    57.89 |   46.15 |    6.12 |                   
  ...helpers.d.mts |       0 |        0 |       0 |       0 | 1                 
  ...e-helpers.mjs |   97.64 |    70.96 |     100 |   97.64 | 22-23             
  ...mcp-smoke.mjs |       0 |        0 |       0 |       0 | 1-124             
  ...cceptance.mjs |       0 |        0 |       0 |       0 | 1-473             
  ...re-server.mjs |       0 |        0 |       0 |       0 | 1-59              
  ...ols-smoke.mjs |       0 |        0 |       0 |       0 | 1-268             
  real-tab.mjs     |       0 |        0 |       0 |       0 | 1-218             
  ...al-chrome.mjs |       0 |        0 |       0 |       0 | 1-223             
 .../conversations |   90.17 |    85.32 |      95 |   90.17 |                   
  ...e-activity.ts |     100 |      100 |     100 |     100 |                   
  ...ime-errors.ts |     100 |      100 |     100 |     100 |                   
  ...me-manager.ts |     100 |      100 |     100 |     100 |                   
  ...-ownership.ts |   87.33 |    83.58 |   88.46 |   87.33 | ...57-558,601-602 
  ...-workspace.ts |   89.09 |    78.66 |     100 |   89.09 | ...91-292,339-340 
 src/serve/fs      |   87.77 |    82.34 |     100 |   87.77 |                   
  audit.ts         |     100 |    96.29 |     100 |     100 | 211               
  errors.ts        |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...x-registry.ts |     100 |      100 |     100 |     100 |                   
  paths.ts         |   77.64 |    74.01 |     100 |   77.64 | ...65,594-598,611 
  policy.ts        |   90.52 |    89.18 |     100 |   90.52 | 172-180           
  text-cursor.ts   |   88.23 |       90 |     100 |   88.23 | 74-77,92-95       
  ...ile-system.ts |   88.02 |    81.85 |     100 |   88.02 | ...3027,3037-3038 
 src/serve/live    |   77.23 |     70.5 |   90.46 |   77.23 |                   
  discovery.ts     |   85.89 |    82.05 |    91.3 |   85.89 | ...73-579,592-593 
  ...oordinator.ts |   82.67 |    76.63 |   97.01 |   82.67 | ...1319,1351-1353 
  ...-installer.ts |    64.3 |    82.35 |   80.76 |    64.3 | ...45-446,460-472 
  ...oordinator.ts |    76.7 |    67.47 |   85.71 |    76.7 | ...1885,1976-1977 
  ...controller.ts |   67.82 |    79.66 |      75 |   67.82 | ...66-278,287-295 
  ...sk-service.ts |   87.45 |    65.93 |   95.65 |   87.45 | ...1186-1187,1215 
  ...redentials.ts |   96.26 |    93.47 |     100 |   96.26 | 91-94             
  ...me-session.ts |   65.63 |    57.24 |   88.88 |   65.63 | ...2270,2275-2282 
  ...up-context.ts |   94.85 |    77.39 |     100 |   94.85 | ...18,327-330,350 
  types.ts         |     100 |      100 |     100 |     100 |                   
 .../local-control |   82.89 |    88.77 |      90 |   82.89 |                   
  credentials.ts   |   96.42 |    95.45 |     100 |   96.42 | 109-110           
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...interfaces.ts |   43.58 |    82.75 |   42.85 |   43.58 | ...09-117,130-142 
  ...r-identity.ts |     100 |    85.71 |     100 |     100 | 61                
  service.ts       |    93.4 |       90 |     100 |    93.4 | ...20-222,313-315 
 src/serve/routes  |   85.87 |    81.03 |   95.27 |   85.87 |                   
  a2ui-action.ts   |   96.84 |     88.5 |    87.5 |   96.84 | ...70-272,309-311 
  capabilities.ts  |   98.73 |    96.15 |     100 |   98.73 | 82                
  ...nel-notify.ts |   79.16 |    85.18 |     100 |   79.16 | ...03-104,120-126 
  ...l-webhooks.ts |   93.56 |    84.09 |     100 |   93.56 | ...42,292,332,334 
  daemon-status.ts |   85.71 |    83.33 |     100 |   85.71 | 101-108           
  goals.ts         |   98.94 |    91.17 |     100 |   98.94 | 143               
  health.ts        |   99.09 |    91.17 |     100 |   99.09 | 147               
  live-setup.ts    |   33.33 |     37.5 |      50 |   33.33 | ...18-123,130-135 
  live.ts          |   84.61 |    76.47 |     100 |   84.61 | ...04,106-111,131 
  permission.ts    |     100 |     92.3 |     100 |     100 | 50,98             
  ...uled-tasks.ts |   87.94 |    85.26 |   93.75 |   87.94 | ...1539,1584-1585 
  ...on-runtime.ts |   91.42 |       90 |     100 |   91.42 | 56-64             
  session.ts       |   86.25 |    82.36 |   93.45 |   86.25 | ...6730,6732-6733 
  sse-events.ts    |   86.85 |    85.64 |   94.11 |   86.85 | ...18-929,932,939 
  usage-stats.ts   |     100 |    95.45 |     100 |     100 | 118               
  ...space-auth.ts |   85.55 |    75.64 |     100 |   85.55 | ...21-326,331,345 
  ...el-control.ts |   86.26 |    78.94 |     100 |   86.26 | ...17-318,339-347 
  ...management.ts |   90.35 |    78.94 |     100 |   90.35 | ...52-553,576-577 
  ...d-contacts.ts |   83.62 |    94.59 |     100 |   83.62 | 123,125-142       
  ...controller.ts |   83.33 |    80.47 |      90 |   83.33 | ...1056,1061,1068 
  ...extensions.ts |    88.8 |    77.83 |   93.84 |    88.8 | ...2329,2374-2375 
  ...-file-read.ts |      91 |    80.91 |     100 |      91 | ...20-621,624-625 
  ...file-write.ts |   89.72 |    79.35 |     100 |   89.72 | ...05,719-726,807 
  ...t-branches.ts |   75.43 |    66.66 |     100 |   75.43 | ...13-618,627-634 
  ...e-git-diff.ts |   97.32 |    90.56 |     100 |   97.32 | 161-162,189-191   
  ...ce-git-log.ts |     100 |    93.18 |     100 |     100 | 52,77,188         
  workspace-git.ts |   77.08 |    89.65 |     100 |   77.08 | 97-118            
  ...github-prs.ts |   88.26 |    63.46 |     100 |   88.26 | ...38-239,264-265 
  ...-lifecycle.ts |   95.23 |    75.75 |     100 |   95.23 | ...50-151,186-187 
  ...al-control.ts |   74.17 |    69.23 |     100 |   74.17 | ...18,220-226,231 
  ...management.ts |   87.47 |       85 |     100 |   87.47 | ...1733,1743-1748 
  ...cp-control.ts |    73.2 |    67.54 |   85.71 |    73.2 | ...27-633,644-645 
  ...ace-models.ts |   95.53 |    89.74 |     100 |   95.53 | ...52-157,296-297 
  ...ermissions.ts |    77.9 |    72.41 |     100 |    77.9 | ...69-277,298-316 
  ...e-settings.ts |   75.67 |       75 |     100 |   75.67 | ...15-726,732-733 
  ...tup-github.ts |   77.97 |    70.58 |   84.21 |   77.97 | ...46-352,397-398 
  ...ace-skills.ts |    76.9 |    87.15 |     100 |    76.9 | ...29-354,360-394 
  ...ace-status.ts |   82.94 |     74.5 |     100 |   82.94 | ...84-486,490-491 
  ...pace-tools.ts |   75.94 |    69.69 |   66.66 |   75.94 | ...59-164,193-194 
  ...pace-trust.ts |   76.92 |     67.1 |      80 |   76.92 | ...38-343,351-352 
  ...pace-voice.ts |   91.33 |    81.02 |     100 |   91.33 | ...70-673,676-678 
 src/serve/server  |   92.75 |    89.98 |   97.22 |   92.75 |                   
  access-log.ts    |   98.73 |    97.26 |     100 |   98.73 | 119,196           
  ...-timestamp.ts |     100 |      100 |     100 |     100 |                   
  ...er-helpers.ts |   63.82 |    78.15 |   81.81 |   63.82 | ...16,330,332-347 
  ...w-registry.ts |    98.8 |    81.81 |     100 |    98.8 | 107               
  ...r-handlers.ts |   97.87 |       80 |     100 |   97.87 | 27                
  ...r-response.ts |   87.73 |    76.19 |     100 |   87.73 | ...97,814,877-886 
  fs-factory.ts    |     100 |    95.52 |     100 |     100 | 77,144,200        
  ...branch-ops.ts |     100 |      100 |     100 |     100 |                   
  ...list-cache.ts |   99.01 |    95.52 |     100 |   99.01 | 184-185           
  ...t-deadline.ts |     100 |      100 |     100 |     100 |                   
  ...iter-setup.ts |      65 |       80 |   33.33 |      65 | 30-35,38-43,47-48 
  ...st-helpers.ts |   95.13 |    95.09 |     100 |   95.13 | ...66-168,423-428 
  self-origin.ts   |   76.19 |       80 |     100 |   76.19 | 45-54             
  ...e-features.ts |      95 |     87.5 |     100 |      95 | 182-188           
  ...on-archive.ts |   91.39 |    87.04 |   97.56 |   91.39 | ...,975,1003-1004 
  ...ion-export.ts |     100 |       95 |     100 |     100 | 64                
  session-list.ts  |      97 |    93.45 |     100 |      97 | ...1068,1273-1277 
  ...ry-context.ts |    87.5 |       50 |     100 |    87.5 | 49-50             
  telemetry.ts     |   99.06 |    97.26 |     100 |   99.06 | ...04,873,952-954 
 src/serve/voice   |    92.7 |    91.53 |   97.72 |    92.7 |                   
  ...ice-config.ts |   84.81 |       30 |     100 |   84.81 | 91-100,104-105    
  voice-ws.ts      |   91.58 |    93.44 |      96 |   91.58 | ...68,483,521-523 
  ...oordinator.ts |     100 |    98.24 |     100 |     100 | 176               
 ...kspace-service |    90.9 |    88.03 |   91.66 |    90.9 |                   
  index.ts         |   90.41 |    87.29 |      90 |   90.41 | ...1505-1509,1512 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services      |   92.69 |    89.67 |   98.13 |   92.69 |                   
  ...mandLoader.ts |     100 |       95 |     100 |     100 | 106               
  ...killLoader.ts |   97.19 |    85.71 |     100 |   97.19 | 142,153-154       
  ...andService.ts |   98.73 |      100 |     100 |   98.73 | 107               
  ...mandLoader.ts |   87.09 |    83.07 |     100 |   87.09 | ...35-340,345-350 
  ...omptLoader.ts |   79.55 |    88.42 |   85.71 |   79.55 | ...48,178,245-246 
  ...mandLoader.ts |   97.77 |     92.3 |     100 |   97.77 | 176,183-184       
  ...nd-factory.ts |   91.42 |    91.66 |     100 |   91.42 | 128,137-144       
  ...ation-tool.ts |     100 |    95.45 |     100 |     100 | 125               
  ...ndMetadata.ts |   98.23 |    96.72 |     100 |   98.23 | 83,87             
  commandUtils.ts  |      96 |     90.9 |     100 |      96 | 48                
  ...and-parser.ts |   90.69 |    85.71 |     100 |   90.69 | 63-66             
  ...ionService.ts |     100 |      100 |     100 |     100 |                   
  prompt-stash.ts  |   96.66 |    92.85 |     100 |   96.66 | 34-35             
  ...tree-lease.ts |   92.14 |    92.42 |     100 |   92.14 | ...91-296,329-330 
  ...low-loader.ts |     100 |    96.29 |     100 |     100 | 88                
  setup-github.ts  |    90.8 |    80.95 |     100 |    90.8 | ...49-450,457-458 
  ...-args-file.ts |   93.93 |    91.66 |    87.5 |   93.93 | 208-210,224-230   
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...e-keyterms.ts |   98.64 |    95.77 |     100 |   98.64 | 116,142-143       
  voice-model.ts   |     100 |      100 |     100 |     100 |                   
  voice-service.ts |    90.4 |    87.87 |     100 |    90.4 | ...81,288,353-358 
  ...e-settings.ts |     100 |    95.23 |     100 |     100 | 19                
  ...ranscriber.ts |   91.77 |    87.11 |   97.22 |   91.77 | ...99-901,904-906 
 ...s/housekeeping |      93 |    88.34 |      95 |      93 |                   
  scheduler.ts     |      93 |    88.34 |      95 |      93 | ...57-359,411-415 
 ...rvices/insight |     100 |      100 |     100 |     100 |                   
  dates.ts         |     100 |      100 |     100 |     100 |                   
 ...ght/generators |   88.94 |    86.86 |   96.29 |   88.94 |                   
  DataProcessor.ts |   88.31 |    86.84 |      95 |   88.31 | ...1368,1372-1379 
  ...tGenerator.ts |   98.24 |    85.71 |     100 |   98.24 | 47                
  ...teRenderer.ts |     100 |      100 |     100 |     100 |                   
 .../insight/types |       0 |       50 |      50 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 |                   
  ...sightTypes.ts |       0 |        0 |       0 |       0 | 1                 
 ...mpt-processors |   97.27 |    94.25 |     100 |   97.27 |                   
  ...tProcessor.ts |     100 |      100 |     100 |     100 |                   
  ...eProcessor.ts |   94.52 |       85 |     100 |   94.52 | 46-47,93-94       
  ...tionParser.ts |     100 |      100 |     100 |     100 |                   
  ...lProcessor.ts |   97.41 |    95.83 |     100 |   97.41 | 96-99             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/services/tips |   97.27 |    84.61 |     100 |   97.27 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  tipHistory.ts    |   92.59 |       70 |     100 |   92.59 | ...24,146,153,162 
  tipRegistry.ts   |     100 |      100 |     100 |     100 |                   
  tipScheduler.ts  |     100 |    91.66 |     100 |     100 | 55                
 src/startup       |   88.99 |    83.47 |    90.9 |   88.99 |                   
  ...p-prefetch.ts |   98.09 |    94.23 |    87.5 |   98.09 | 50,209,225-226    
  ...reeStartup.ts |   80.53 |     74.6 |     100 |   80.53 | ...94,403,409-412 
 src/test-utils    |   94.09 |    79.16 |   77.77 |   94.09 |                   
  ci-env.ts        |      88 |     62.5 |     100 |      88 | 22-23,28          
  ...omMatchers.ts |   69.69 |       50 |      50 |   69.69 | 32-35,37-39,45-47 
  ...mised-lock.ts |     100 |      100 |   66.66 |     100 |                   
  ...andContext.ts |     100 |      100 |     100 |     100 |                   
  render.tsx       |     100 |      100 |     100 |     100 |                   
 src/ui            |   70.84 |    77.49 |   72.04 |   70.84 |                   
  App.tsx          |   33.33 |       75 |   33.33 |   33.33 | 32-86             
  AppContainer.tsx |   76.08 |       72 |   69.44 |   76.08 | ...4298,4414-4420 
  ...tionNudge.tsx |    9.58 |      100 |       0 |    9.58 | 24-94             
  ...ackDialog.tsx |    30.3 |      100 |       0 |    30.3 | 26-76             
  ...tionNudge.tsx |    7.69 |      100 |       0 |    7.69 | 25-103            
  colors.ts        |   63.63 |      100 |   41.17 |   63.63 | ...52,54-55,60-61 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...AutoUpdate.ts |   93.54 |    94.64 |      90 |   93.54 | 126,131,202-213   
  keyMatchers.ts   |   95.91 |    97.14 |     100 |   95.91 | 25-26             
  ...tic-colors.ts |     100 |      100 |     100 |     100 |                   
  ...one-update.ts |   39.81 |    77.44 |   62.16 |   39.81 | ...1193,1196-1215 
  ...ractiveUI.tsx |   71.53 |    75.47 |    62.5 |   71.53 | ...11,338,405-410 
  ...inePresets.ts |   96.27 |    83.87 |     100 |   96.27 | ...97,402,410-412 
  systemInfo.ts    |   95.09 |    90.27 |     100 |   95.09 | ...54-255,260-264 
  ...InfoFields.ts |    87.5 |    65.85 |     100 |    87.5 | ...24-125,146-147 
  textConstants.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...e-relaunch.ts |   89.61 |    86.66 |      50 |   89.61 | 56-61,83-84       
 src/ui/auth       |   58.76 |    66.66 |   51.06 |   58.76 |                   
  AuthDialog.tsx   |   59.01 |     42.1 |   16.66 |   59.01 | ...25,332-354,358 
  ...nProgress.tsx |       0 |        0 |       0 |       0 | 1-64              
  ...etupSteps.tsx |   60.21 |    70.73 |   57.69 |   60.21 | ...90,794,803,806 
  useAuth.ts       |   94.83 |       75 |     100 |   94.83 | ...33-234,253-259 
  ...rSetupFlow.ts |   43.18 |    33.33 |      50 |   43.18 | ...78-399,416-459 
 src/ui/commands   |   84.04 |     84.2 |   91.07 |   84.04 |                   
  aboutCommand.ts  |     100 |      100 |     100 |     100 |                   
  ...or-command.ts |     100 |    95.65 |     100 |     100 | 104,182           
  agentsCommand.ts |   83.78 |      100 |      60 |   83.78 | 30-32,42-44       
  ...odeCommand.ts |    93.1 |    95.23 |     100 |    93.1 | 77-82             
  arenaCommand.ts  |   63.89 |    65.71 |   65.21 |   63.89 | ...01-606,691-699 
  authCommand.ts   |     100 |      100 |     100 |     100 |                   
  branchCommand.ts |     100 |      100 |     100 |     100 |                   
  btwCommand.ts    |   94.32 |    77.41 |     100 |   94.32 | 35-36,114-119     
  bugCommand.ts    |     100 |    77.77 |     100 |     100 | 28,62             
  cdCommand.ts     |    92.3 |    82.75 |     100 |    92.3 | ...,94-99,178,187 
  clearCommand.ts  |    80.9 |    70.83 |     100 |    80.9 | ...28-129,137-146 
  commands.ts      |   97.45 |    96.66 |     100 |   97.45 | 153-155           
  ...essCommand.ts |   68.22 |    54.05 |      75 |   68.22 | ...97-198,212-215 
  ...astCommand.ts |   84.27 |       75 |     100 |   84.27 | ...,91-97,125-130 
  ...ig-command.ts |   93.12 |    88.42 |     100 |   93.12 | ...07-315,321-323 
  ...extCommand.ts |   74.79 |    74.39 |   84.61 |   74.79 | ...89-622,633-634 
  copyCommand.ts   |    98.7 |    96.29 |     100 |    98.7 | 66-67,172,272,323 
  ...or-command.ts |   85.95 |    80.55 |   88.88 |   85.95 | ...68-274,298-309 
  deleteCommand.ts |     100 |      100 |     100 |     100 |                   
  diffCommand.ts   |     100 |    87.87 |     100 |     100 | ...63,231-232,245 
  ...ryCommand.tsx |   90.56 |    87.83 |    90.9 |   90.56 | ...75-280,327-334 
  docsCommand.ts   |     100 |     90.9 |     100 |     100 | 26                
  doctorChecks.ts  |   70.31 |    74.57 |     100 |   70.31 | ...95-301,325-341 
  doctorCommand.ts |   70.16 |    84.61 |      95 |   70.16 | ...29-679,682-816 
  dreamCommand.ts  |   85.45 |    88.88 |     100 |   85.45 | 58-65             
  editorCommand.ts |     100 |      100 |     100 |     100 |                   
  ...rt-command.ts |   80.48 |       75 |     100 |   80.48 | 49-54,69-72,93-98 
  effort-utils.ts  |     100 |      100 |     100 |     100 |                   
  exportCommand.ts |   98.25 |    91.02 |     100 |   98.25 | ...81,198-199,364 
  ...onsCommand.ts |   52.31 |    56.25 |   69.23 |   52.31 | ...09,277-329,390 
  forgetCommand.ts |     100 |       90 |     100 |     100 | 59                
  forkCommand.ts   |     100 |    94.11 |     100 |     100 | 96,147            
  goalCommand.ts   |     100 |    96.49 |     100 |     100 | 139,192           
  helpCommand.ts   |     100 |      100 |     100 |     100 |                   
  ...oryCommand.ts |     100 |      100 |     100 |     100 |                   
  hooksCommand.ts  |   81.25 |    65.71 |   85.71 |   81.25 | ...,86-93,131-132 
  ideCommand.ts    |   60.75 |    64.28 |   41.17 |   60.75 | ...05-306,310-324 
  ...figCommand.ts |   52.83 |    81.25 |      70 |   52.83 | ...74-319,321-330 
  initCommand.ts   |   91.86 |       80 |     100 |   91.86 | 48,83-88          
  ...ghtCommand.ts |   77.87 |    71.42 |     100 |   77.87 | ...44-245,250-272 
  ...ageCommand.ts |   94.44 |    90.14 |     100 |   94.44 | ...13-214,241-251 
  learn-command.ts |     100 |      100 |     100 |     100 |                   
  lspCommand.ts    |     100 |    86.95 |     100 |     100 | 31,102-103        
  mcpCommand.ts    |     100 |      100 |     100 |     100 |                   
  memoryCommand.ts |     100 |      100 |     100 |     100 |                   
  modelCommand.ts  |   86.01 |    85.76 |     100 |   86.01 | ...1093,1127-1132 
  ...onsCommand.ts |     100 |      100 |     100 |     100 |                   
  planCommand.ts   |   78.82 |    76.92 |     100 |   78.82 | 30-35,51-56,68-73 
  quitCommand.ts   |     100 |      100 |     100 |     100 |                   
  recapCommand.ts  |   21.81 |      100 |      50 |   21.81 | 24-73             
  ...ns-command.ts |   98.83 |    81.81 |     100 |   98.83 | 100               
  ...berCommand.ts |     100 |     87.5 |     100 |     100 | 46                
  renameCommand.ts |    89.6 |       90 |     100 |    89.6 | ...72-176,212-219 
  ...oreCommand.ts |   90.96 |    86.04 |     100 |   90.96 | ...41-146,177-178 
  resumeCommand.ts |     100 |      100 |     100 |     100 |                   
  rewindCommand.ts |   81.25 |      100 |      50 |   81.25 | 20-22             
  ...ngsCommand.ts |     100 |      100 |     100 |     100 |                   
  ...hubCommand.ts |   89.47 |       75 |      80 |   89.47 | 54-59             
  skillsCommand.ts |   78.82 |    81.81 |     100 |   78.82 | 37-52,78,97       
  statsCommand.ts  |   90.65 |    76.73 |     100 |   90.65 | ...30-733,825-832 
  ...ineCommand.ts |     100 |      100 |     100 |     100 |                   
  ...aryCommand.ts |   73.04 |     82.3 |      90 |   73.04 | ...20-547,561-565 
  tasksCommand.ts  |   77.33 |    72.13 |     100 |   77.33 | ...46-150,173-178 
  ...tupCommand.ts |     100 |      100 |     100 |     100 |                   
  themeCommand.ts  |     100 |      100 |     100 |     100 |                   
  toolsCommand.ts  |     100 |      100 |     100 |     100 |                   
  trustCommand.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...te-command.ts |     100 |    94.11 |     100 |     100 | 74,148            
  vimCommand.ts    |     100 |      100 |     100 |     100 |                   
  voice-command.ts |   93.63 |       88 |     100 |   93.63 | 36,98-103         
  ...owsCommand.ts |   94.38 |    85.29 |     100 |   94.38 | ...78-183,282-287 
 src/ui/components |    73.1 |    80.02 |   77.58 |    73.1 |                   
  AboutBox.tsx     |     100 |      100 |     100 |     100 |                   
  AnsiOutput.tsx   |   65.57 |      100 |      50 |   65.57 | 69-90             
  ApiKeyInput.tsx  |       0 |        0 |       0 |       0 | 1-97              
  AppHeader.tsx    |    88.7 |       75 |     100 |    88.7 | 36,38-43,45       
  ...odeDialog.tsx |   87.24 |    72.22 |   33.33 |   87.24 | ...85,233-238,245 
  AsciiArt.ts      |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |   95.65 |    66.66 |     100 |   95.65 | 27,52             
  ...TextInput.tsx |   89.06 |    90.78 |     100 |   89.06 | ...87-289,303-305 
  Composer.tsx     |   94.54 |    66.66 |     100 |   94.54 | ...-76,88,143,158 
  ...entPrompt.tsx |     100 |      100 |     100 |     100 |                   
  ...ryDisplay.tsx |   75.89 |    62.06 |     100 |   75.89 | ...,88,93-108,113 
  ...geDisplay.tsx |   68.42 |    57.14 |     100 |   68.42 | 16-17,31-32,42-50 
  CronPill.tsx     |     100 |    93.75 |     100 |     100 | 19                
  ...ification.tsx |      84 |       60 |     100 |      84 | 23-24,40-42       
  ...gProfiler.tsx |       0 |        0 |       0 |       0 | 1-36              
  ...ogManager.tsx |   11.28 |      100 |       0 |   11.28 | 71-598            
  DiffDialog.tsx   |    53.5 |     37.5 |   69.23 |    53.5 | ...32-737,747-760 
  ...ngsDialog.tsx |    8.44 |      100 |       0 |    8.44 | 37-195            
  EffortDialog.tsx |   97.36 |      100 |     100 |   97.36 | 55-56             
  ExitWarning.tsx  |     100 |      100 |     100 |     100 |                   
  ...hProgress.tsx |    87.8 |    33.33 |     100 |    87.8 | 28-31,56          
  ...ustDialog.tsx |     100 |      100 |     100 |     100 |                   
  Footer.tsx       |   81.27 |    69.23 |      50 |   81.27 | ...06,245,267-272 
  ...ngSpinner.tsx |   68.42 |    85.71 |      50 |   68.42 | 35-52,73,80-81    
  GoalPill.tsx     |   93.51 |    81.81 |     100 |   93.51 | 37-38,106-109,123 
  Header.tsx       |   98.65 |    94.73 |     100 |   98.65 | 173,175           
  Help.tsx         |   98.33 |       90 |     100 |   98.33 | ...25,382,448-449 
  ...emDisplay.tsx |   79.69 |    67.61 |     100 |   79.69 | ...17,520,523-529 
  ...ngeDialog.tsx |     100 |      100 |     100 |     100 |                   
  InputPrompt.tsx  |   86.26 |     83.3 |      80 |   86.26 | ...2231,2252,2348 
  ...Shortcuts.tsx |     100 |       88 |     100 |     100 | 98,119            
  ...Indicator.tsx |   98.18 |    97.82 |     100 |   98.18 | 161-162           
  ...firmation.tsx |   91.42 |      100 |      50 |   91.42 | 26-31             
  MainContent.tsx  |   95.88 |    96.03 |   46.15 |   95.88 | ...20,523-527,530 
  MemoryDialog.tsx |   86.59 |    80.15 |     100 |   86.59 | ...34-435,485,553 
  ...geDisplay.tsx |       0 |        0 |       0 |       0 | 1-41              
  ModelDialog.tsx  |   85.22 |    74.17 |     100 |   85.22 | ...1042,1098,1100 
  ...tsDisplay.tsx |     100 |    97.22 |     100 |     100 | 270               
  ...fications.tsx |   16.66 |      100 |       0 |   16.66 | 14-56             
  ...onsDialog.tsx |    2.13 |      100 |       0 |    2.13 | 62-133,148-1004   
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...icePrompt.tsx |   92.64 |    85.71 |     100 |   92.64 | 102-106,134-139   
  PrepareLabel.tsx |   91.66 |    77.27 |     100 |   91.66 | 73-75,77-79,110   
  ...atePrompt.tsx |    8.57 |      100 |       0 |    8.57 | 24-55,58-134      
  ...geDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ngDisplay.tsx |   21.42 |      100 |       0 |   21.42 | 13-39             
  ...hProgress.tsx |   85.25 |    88.46 |     100 |   85.25 | 121-147           
  ...dSelector.tsx |   92.79 |    82.65 |     100 |   92.79 | ...19-323,354-370 
  ...ionPicker.tsx |   83.66 |    72.13 |     100 |   83.66 | ...96,402,444-466 
  ...onPreview.tsx |   93.58 |    83.78 |     100 |   93.58 | ...,70-71,195-197 
  ...ryDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...putPrompt.tsx |   92.06 |    86.36 |   83.33 |   92.06 | ...,70-72,120-123 
  ...tedDialog.tsx |     100 |      100 |     100 |     100 |                   
  ...ngsDialog.tsx |   71.55 |    73.89 |   69.23 |   71.55 | ...1252,1258-1259 
  ...ionDialog.tsx |    92.3 |    96.15 |   33.33 |    92.3 | 60-63,68-75,164   
  ...putPrompt.tsx |    15.9 |      100 |       0 |    15.9 | 20-63             
  ...Indicator.tsx |   57.14 |      100 |       0 |   57.14 | 12-15             
  ...MoreLines.tsx |      28 |      100 |       0 |      28 | 18-40             
  ...iewDialog.tsx |   97.77 |    87.67 |     100 |   97.77 | ...97,305-307,324 
  ...tsDisplay.tsx |   95.86 |       75 |     100 |   95.86 | 67-71             
  ...ionPicker.tsx |       0 |        0 |       0 |       0 | 1-171             
  ...tivityTab.tsx |    3.94 |      100 |       0 |    3.94 | 27-275            
  StatsDialog.tsx  |    8.64 |      100 |       0 |    8.64 | ...76-111,130-322 
  StatsDisplay.tsx |     100 |      100 |     100 |     100 |                   
  ...ciencyTab.tsx |    78.9 |    56.52 |     100 |    78.9 | ...26,213,262-288 
  ...atmapView.tsx |    8.98 |      100 |       0 |    8.98 | 20-107            
  ...essionTab.tsx |      80 |    66.66 |     100 |      80 | ...70-277,283-300 
  ...ineDialog.tsx |    93.9 |    86.88 |     100 |    93.9 | ...20,282,302-304 
  ...yTodoList.tsx |   96.36 |    88.23 |     100 |   96.36 | 138-141           
  ...nsDisplay.tsx |   95.62 |    87.09 |     100 |   95.62 | ...24-125,273-275 
  ...inalImage.tsx |     100 |    93.93 |     100 |     100 | 75,129            
  ThemeDialog.tsx  |   89.95 |    46.15 |      75 |   89.95 | ...71-173,243-245 
  Tips.tsx         |   93.54 |       75 |     100 |   93.54 | 39-40             
  TodoDisplay.tsx  |     100 |      100 |     100 |     100 |                   
  ...tsDisplay.tsx |     100 |     87.5 |     100 |     100 | 31-32             
  TrustDialog.tsx  |     100 |    83.33 |     100 |     100 | 72-87             
  ...ification.tsx |   36.36 |      100 |       0 |   36.36 | 15-22             
  ...Indicator.tsx |    92.5 |     87.5 |     100 |    92.5 | 50-53             
  ...ackDialog.tsx |    7.84 |      100 |       0 |    7.84 | 24-134            
  ...xitDialog.tsx |   80.36 |    43.47 |      60 |   80.36 | ...24-238,248-251 
  ...odeVisuals.ts |   97.22 |    85.71 |     100 |   97.22 | 25                
  ...s-helpers.tsx |   66.25 |    81.25 |      50 |   66.25 | 25-32,46-53,62-72 
 ...nts/agent-view |   58.69 |    70.24 |    62.5 |   58.69 |                   
  ...atContent.tsx |    9.09 |      100 |       0 |    9.09 | 54-275,281-283    
  ...tChatView.tsx |     100 |    81.81 |     100 |     100 | 82                
  ...tComposer.tsx |   69.48 |    33.33 |   66.66 |   69.48 | ...51,269,277-279 
  AgentFooter.tsx  |   15.38 |      100 |       0 |   15.38 | 28-65             
  AgentHeader.tsx  |   15.38 |      100 |       0 |   15.38 | 27-64             
  AgentTabBar.tsx  |    87.9 |    63.88 |     100 |    87.9 | ...88,110-118,136 
  ...oryAdapter.ts |     100 |    91.83 |     100 |     100 | 103,109-110,138   
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
 ...mponents/arena |   45.51 |    70.53 |   60.86 |   45.51 |                   
  ArenaCards.tsx   |   73.06 |    71.79 |   85.71 |   73.06 | ...83-185,321-326 
  ...ectDialog.tsx |   83.48 |    69.86 |   88.88 |   83.48 | ...88-392,409-410 
  ...artDialog.tsx |    9.77 |      100 |       0 |    9.77 | 27-166            
  ...tusDialog.tsx |    5.63 |      100 |       0 |    5.63 | 33-75,80-288      
  ...topDialog.tsx |    6.17 |      100 |       0 |    6.17 | 33-213            
 ...ackground-view |   85.86 |     85.1 |   92.98 |   85.86 |                   
  ...sksDialog.tsx |   82.66 |    83.09 |   85.71 |   82.66 | ...1854,1977-1983 
  ...TasksPill.tsx |   78.84 |    94.28 |     100 |   78.84 | 64,109-129        
  ...gentPanel.tsx |   97.08 |    86.31 |     100 |   97.08 | 132,442-446,520   
  agent-forest.ts  |    99.2 |    93.93 |     100 |    99.2 | 258               
  ...Visibility.ts |     100 |      100 |     100 |     100 |                   
  ...e-overlay.tsx |    88.2 |    76.47 |     100 |    88.2 | ...36-138,140-142 
 ...nts/extensions |   84.32 |    76.78 |   83.33 |   84.32 |                   
  ...gerDialog.tsx |   82.15 |    76.08 |     100 |   82.15 | ...91-198,258,260 
  TabBar.tsx       |   97.29 |    88.88 |     100 |   97.29 | 33                
  index.ts         |       0 |        0 |       0 |       0 | 1-12              
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...tensions/steps |   46.26 |       85 |   58.82 |   46.26 |                   
  ...ctionStep.tsx |   95.12 |    92.85 |   85.71 |   95.12 | 84-86,89          
  ...etailStep.tsx |       0 |        0 |       0 |       0 | 1-145             
  ...nListStep.tsx |   75.26 |    88.37 |   66.66 |   75.26 | ...53,174,203-209 
  ...electStep.tsx |       0 |        0 |       0 |       0 | 1-83              
  ...nfirmStep.tsx |   16.32 |      100 |       0 |   16.32 | 28-74             
  index.ts         |       0 |        0 |       0 |       0 | 1-11              
 ...xtensions/tabs |   71.92 |    68.21 |   70.83 |   71.92 |                   
  DiscoverTab.tsx  |   68.22 |    67.66 |   55.55 |   68.22 | ...93,656-660,664 
  InstalledTab.tsx |   75.49 |    67.44 |   83.33 |   75.49 | ...77,782-783,820 
  SourcesTab.tsx   |   71.67 |    70.47 |   77.77 |   71.67 | ...28,547,621-633 
 ...tensions/views |    50.7 |    52.38 |   20.83 |    50.7 |                   
  ...tionsView.tsx |   73.75 |    56.36 |   66.66 |   73.75 | ...30,353,369-374 
  ...tionsView.tsx |   43.45 |    44.82 |    6.66 |   43.45 | ...98-405,408-420 
  ...etailView.tsx |    9.24 |      100 |       0 |    9.24 | 40-67,70-163      
 ...mponents/hooks |   87.11 |    81.37 |   91.89 |   87.11 |                   
  ...rListBody.tsx |   95.29 |    85.18 |     100 |   95.29 | 95-98             
  ...etailStep.tsx |   75.32 |    71.42 |      60 |   75.32 | ...56-169,173-186 
  ...etailStep.tsx |     100 |      100 |     100 |     100 |                   
  ...rListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entHeader.tsx |     100 |    85.71 |     100 |     100 | 47                
  ...rListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...etailStep.tsx |     100 |      100 |     100 |     100 |                   
  ...abledStep.tsx |     100 |      100 |     100 |     100 |                   
  ...sListStep.tsx |     100 |      100 |     100 |     100 |                   
  ...entDialog.tsx |   72.29 |    70.49 |     100 |   72.29 | ...51,563-568,572 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-13              
  ...erGrouping.ts |     100 |      100 |     100 |     100 |                   
  sourceLabels.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...components/mcp |   40.91 |    63.44 |   70.58 |   40.91 |                   
  ...ealthPill.tsx |     100 |      100 |     100 |     100 |                   
  ...entDialog.tsx |   32.09 |    26.19 |      40 |   32.09 | ...12,914,927-933 
  ...valDialog.tsx |   15.06 |      100 |       0 |   15.06 | 40-109            
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-35              
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |      97 |       95 |     100 |      97 | 24,113-114        
 ...ents/mcp/steps |   53.94 |    73.51 |   57.14 |   53.94 |                   
  ...icateStep.tsx |    5.65 |      100 |       0 |    5.65 | 40-66,69-308      
  ...electStep.tsx |   10.95 |      100 |       0 |   10.95 | 16-88             
  ...etailStep.tsx |     100 |      100 |     100 |     100 |                   
  ...eListStep.tsx |   99.09 |    97.36 |     100 |   99.09 | 71                
  ...etailStep.tsx |   62.83 |       60 |   33.33 |   62.83 | ...87-296,307-332 
  ...rListStep.tsx |   88.53 |    81.25 |     100 |   88.53 | ...64,170,175-180 
  ...etailStep.tsx |    10.3 |      100 |       0 |    10.3 | ...1,67-79,82-140 
  ToolListStep.tsx |   69.29 |       50 |     100 |   69.29 | ...23,126,135-144 
 ...nents/messages |   90.51 |    87.35 |   85.71 |   90.51 |                   
  ...orMessage.tsx |     100 |      100 |     100 |     100 |                   
  ...ionDialog.tsx |   89.23 |     84.9 |   81.81 |   89.23 | ...75,593,611-613 
  BtwMessage.tsx   |     100 |      100 |     100 |     100 |                   
  ...upDisplay.tsx |     100 |    94.73 |     100 |     100 | ...43,289,402,432 
  ...onMessage.tsx |   92.06 |    82.35 |     100 |   92.06 | 58-60,62,64       
  ...nMessages.tsx |   94.11 |    95.91 |   76.92 |   94.11 | ...47-349,352-355 
  DiffRenderer.tsx |   93.17 |    86.02 |     100 |   93.17 | ...07,235-236,302 
  ...tsDisplay.tsx |   97.08 |    77.77 |     100 |   97.08 | 95,97,106         
  ...usMessage.tsx |   81.73 |     65.9 |      75 |   81.73 | ...10-214,222,245 
  ...tsDisplay.tsx |   95.52 |    88.31 |     100 |   95.52 | ...40,142,175-180 
  ...ssMessage.tsx |    12.5 |      100 |       0 |    12.5 | 18-59             
  ...edMessage.tsx |   21.05 |      100 |       0 |   21.05 | 23-39             
  ...sMessages.tsx |   59.04 |       50 |    37.5 |   59.04 | ...21-126,147-159 
  ...ryMessage.tsx |   13.63 |      100 |       0 |   13.63 | 23-64             
  ...onMessage.tsx |   91.87 |    82.51 |     100 |   91.87 | ...49-651,658-660 
  ...upMessage.tsx |   98.38 |    95.38 |     100 |   98.38 | 188-191,422       
  ToolMessage.tsx  |   93.85 |    88.38 |   93.75 |   93.85 | ...1051,1096-1098 
 ...ponents/shared |   86.52 |    82.35 |   86.72 |   86.52 |                   
  ...ctionList.tsx |     100 |      100 |      75 |     100 |                   
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  EnumSelector.tsx |     100 |    96.42 |     100 |     100 | 58                
  ...rBoundary.tsx |     100 |      100 |     100 |     100 |                   
  MaxSizedBox.tsx  |   84.71 |    86.95 |      90 |   84.71 | ...67-568,685-686 
  MultiSelect.tsx  |   93.58 |       75 |     100 |   93.58 | ...43,199-201,211 
  ...tonSelect.tsx |     100 |      100 |     100 |     100 |                   
  ...ontroller.tsx |     100 |    83.33 |     100 |     100 | 73,93-95          
  ...eSelector.tsx |     100 |       60 |     100 |     100 | 40-45             
  ...lableList.tsx |   90.37 |    82.85 |   18.18 |   90.37 | ...60-63,65,73-76 
  StaticRender.tsx |     100 |      100 |     100 |     100 |                   
  TextInput.tsx    |    80.8 |    67.24 |      80 |    80.8 | ...36-240,252-258 
  ...ontroller.tsx |     100 |    81.81 |     100 |     100 | 59-62             
  ...apsedTime.tsx |     100 |      100 |     100 |     100 |                   
  ...Indicator.tsx |     100 |      100 |     100 |     100 |                   
  ...lizedList.tsx |   91.49 |    86.66 |   83.33 |   91.49 | ...18-846,859,959 
  text-buffer.ts   |   85.98 |    81.81 |   97.91 |   85.98 | ...2664,2762-2763 
  ...er-actions.ts |   73.93 |    67.22 |     100 |   73.93 | ...32-733,934-936 
 ...ponents/skills |    3.96 |      100 |       0 |    3.96 |                   
  ...gerDialog.tsx |    3.96 |      100 |       0 |    3.96 | 79-137,140-681    
 ...ents/subagents |   30.87 |        0 |       0 |   30.87 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  index.ts         |       0 |        0 |       0 |       0 | 1-11              
  reducers.tsx     |    12.1 |      100 |       0 |    12.1 | 33-190            
  types.ts         |     100 |      100 |     100 |     100 |                   
  utils.ts         |   10.95 |      100 |       0 |   10.95 | ...1,56-57,60-102 
 ...bagents/create |    9.13 |      100 |       0 |    9.13 |                   
  ...ionWizard.tsx |    7.28 |      100 |       0 |    7.28 | 34-299            
  ...rSelector.tsx |   14.75 |      100 |       0 |   14.75 | 26-85             
  ...onSummary.tsx |    4.26 |      100 |       0 |    4.26 | 27-331            
  ...tionInput.tsx |    8.63 |      100 |       0 |    8.63 | 23-177            
  ...dSelector.tsx |   33.33 |      100 |       0 |   33.33 | 20-21,26-27,36-63 
  ...nSelector.tsx |    37.5 |      100 |       0 |    37.5 | 20-21,26-27,36-58 
  ...EntryStep.tsx |   12.76 |      100 |       0 |   12.76 | 34-78             
  ToolSelector.tsx |    4.16 |      100 |       0 |    4.16 | 31-253            
 ...bagents/manage |    21.6 |    59.52 |   27.27 |    21.6 |                   
  ...ctionStep.tsx |   10.25 |      100 |       0 |   10.25 | 21-103            
  ...eleteStep.tsx |   20.93 |      100 |       0 |   20.93 | 23-62             
  ...tEditStep.tsx |   25.53 |      100 |       0 |   25.53 | ...2,37-38,51-124 
  ...ctionStep.tsx |   35.61 |    59.52 |     100 |   35.61 | ...21-433,438-440 
  ...iewerStep.tsx |   13.72 |      100 |       0 |   13.72 | 18-73             
  ...gerDialog.tsx |    6.74 |      100 |       0 |    6.74 | 35-341            
 ...mponents/views |    70.1 |    72.89 |   61.11 |    70.1 |                   
  ContextUsage.tsx |   71.49 |    64.86 |      80 |   71.49 | ...30-436,473-567 
  DoctorReport.tsx |     9.8 |      100 |       0 |     9.8 | 25-54,57-131      
  ...sionsList.tsx |   88.05 |       75 |     100 |   88.05 | 70-77             
  McpStatus.tsx    |   92.01 |     73.8 |     100 |   92.01 | ...36,175-177,262 
  SkillsList.tsx   |   20.51 |      100 |       0 |   20.51 | 17-20,27-57       
  ToolsList.tsx    |     100 |      100 |     100 |     100 |                   
 src/ui/contexts   |   86.01 |    81.54 |   86.48 |   86.01 |                   
  ...ewContext.tsx |   87.56 |       80 |      75 |   87.56 | ...37-240,246-256 
  AppContext.tsx   |      80 |       50 |     100 |      80 | 19-20             
  ...ewContext.tsx |   93.83 |    68.51 |   42.85 |   93.83 | ...44,281-285,317 
  ...igContext.tsx |   81.81 |       50 |     100 |   81.81 | 15-16             
  ...ssContext.tsx |   85.65 |    84.85 |     100 |   85.65 | ...1612-1614,1620 
  ...owContext.tsx |   91.07 |    81.81 |     100 |   91.07 | 47-48,60-62       
  ...deContext.tsx |     100 |      100 |      50 |     100 |                   
  ...onContext.tsx |   80.77 |    79.56 |    92.3 |   80.77 | ...31-434,443-446 
  ...gsContext.tsx |     100 |      100 |     100 |     100 |                   
  ...usContext.tsx |     100 |      100 |     100 |     100 |                   
  ...ngContext.tsx |   71.42 |       50 |     100 |   71.42 | 17-20             
  ...utContext.tsx |   85.71 |      100 |   66.66 |   85.71 | 13-14             
  ...edContext.tsx |     100 |      100 |      50 |     100 |                   
  ...nsContext.tsx |   88.88 |       50 |     100 |   88.88 | 156-157           
  ...teContext.tsx |   86.66 |       50 |     100 |   86.66 | 237-238           
  ...deContext.tsx |      80 |     87.5 |      75 |      80 | ...11-112,118-120 
  ...rtContext.tsx |     100 |      100 |     100 |     100 |                   
 src/ui/daemon     |   88.45 |    73.87 |   95.45 |   88.45 |                   
  ...ui-adapter.ts |   88.45 |    73.87 |   95.45 |   88.45 | ...81,799-800,886 
 src/ui/editors    |   93.33 |    85.71 |   66.66 |   93.33 |                   
  ...ngsManager.ts |   93.33 |    85.71 |   66.66 |   93.33 | 49,63-64          
 src/ui/hooks      |   85.97 |     83.9 |   87.81 |   85.97 |                   
  ...dProcessor.ts |   85.53 |    85.13 |     100 |   85.53 | ...-970,1017-1018 
  ...ention-ref.ts |   97.72 |       84 |     100 |   97.72 | 65                
  keyToAnsi.ts     |    3.92 |      100 |       0 |    3.92 | 19-77             
  ...esourceRef.ts |     100 |      100 |     100 |     100 |                   
  ...completion.ts |     100 |    95.45 |     100 |     100 | 95                
  ...ention-ref.ts |     100 |      100 |     100 |     100 |                   
  ...dProcessor.ts |   94.62 |    73.58 |     100 |   94.62 | ...87-288,293-294 
  ...dProcessor.ts |   86.79 |    71.86 |   83.33 |   86.79 | ...1529,1558-1562 
  ...rt-command.ts |     100 |      100 |     100 |     100 |                   
  ...sced-flush.ts |     100 |      100 |     100 |     100 |                   
  ...ng-enabled.ts |     100 |      100 |     100 |     100 |                   
  ...oice-input.ts |   92.41 |    82.08 |   66.66 |   92.41 | ...12,514-515,670 
  ...ke-repaint.ts |     100 |      100 |     100 |     100 |                   
  ...amingState.ts |   12.22 |      100 |       0 |   12.22 | 54-157            
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...dScrollbar.ts |     100 |      100 |     100 |     100 |                   
  ...ationFrame.ts |      42 |       75 |     100 |      42 | 42-44,53-59,62-87 
  ...odeCommand.ts |   58.82 |      100 |     100 |   58.82 | 28,33-48          
  ...enaCommand.ts |      85 |      100 |     100 |      85 | 23-24,29          
  ...aInProcess.ts |   27.92 |       80 |      25 |   27.92 | ...69-170,173-175 
  ...Completion.ts |   86.44 |    88.48 |     100 |   86.44 | ...14-515,525-541 
  ...ifications.ts |   87.82 |    96.77 |     100 |   87.82 | 138-152           
  ...tIndicator.ts |   88.28 |    81.57 |     100 |   88.28 | ...66,175,179-187 
  ...waySummary.ts |   96.26 |       75 |     100 |   96.26 | 126-128,170       
  ...ndTaskView.ts |   94.89 |    77.55 |     100 |   94.89 | 164-168,257,263   
  ...chedScroll.ts |     100 |      100 |     100 |     100 |                   
  ...ketedPaste.ts |    23.8 |      100 |       0 |    23.8 | 19-37             
  ...nchCommand.ts |   95.53 |    83.01 |     100 |   95.53 | ...64-165,289-292 
  ...ompletion.tsx |   97.09 |    87.23 |     100 |   97.09 | ...23-324,334-335 
  ...dMigration.ts |    92.1 |    88.88 |     100 |    92.1 | 42-44             
  useCompletion.ts |   96.29 |    90.56 |     100 |   96.29 | ...17-218,222-223 
  ...nitMessage.ts |     100 |      100 |     100 |     100 |                   
  ...extualTips.ts |   78.26 |       50 |     100 |   78.26 | ...2,75-79,96-104 
  ...eteCommand.ts |   89.52 |    90.69 |     100 |   89.52 | ...98-106,114-115 
  ...ialogClose.ts |   36.11 |       10 |     100 |   36.11 | ...89-195,202-207 
  useDiffData.ts   |   11.62 |      100 |       0 |   11.62 | 44-87             
  ...oublePress.ts |   53.12 |       75 |     100 |   53.12 | 33-35,41-54       
  ...orSettings.ts |     100 |      100 |     100 |     100 |                   
  ...Completion.ts |   99.12 |    97.67 |     100 |   99.12 | 182-183           
  ...ionUpdates.ts |   93.72 |    92.98 |     100 |   93.72 | ...87-291,314-320 
  ...agerDialog.ts |   88.88 |      100 |     100 |   88.88 | 21,25             
  ...backDialog.ts |    63.9 |    76.47 |   66.66 |    63.9 | ...66-168,190-191 
  useFocus.ts      |     100 |      100 |     100 |     100 |                   
  ...olderTrust.ts |     100 |    93.33 |     100 |     100 | 62                
  ...ggestions.tsx |   96.47 |    78.94 |     100 |   96.47 | 121,155-156       
  ...miniStream.ts |    87.4 |    84.04 |   78.26 |    87.4 | ...5819-5821,5823 
  ...BranchName.ts |     100 |    94.44 |     100 |     100 | 54                
  ...oryManager.ts |   98.38 |    98.85 |     100 |   98.38 | 141-144           
  ...ooksDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...stListener.ts |     100 |      100 |     100 |     100 |                   
  ...nAuthError.ts |   76.19 |       50 |     100 |   76.19 | 39-40,43-45       
  ...putHistory.ts |   92.59 |    85.71 |     100 |   92.59 | 63-64,72,94-96    
  ...storyStore.ts |     100 |    94.11 |     100 |     100 | 69                
  useKeypress.ts   |     100 |      100 |     100 |     100 |                   
  ...rdProtocol.ts |   36.36 |      100 |       0 |   36.36 | 24-31             
  ...unchEditor.ts |   22.58 |      100 |      50 |   22.58 | 11-32,44-85       
  ...gIndicator.ts |     100 |    96.66 |     100 |     100 | 109               
  useLogger.ts     |      16 |      100 |       0 |      16 | 15-45             
  useMCPHealth.ts  |   10.52 |      100 |       0 |   10.52 | 36-75             
  ...cpApproval.ts |   93.12 |    86.11 |     100 |   93.12 | ...24-127,139-140 
  useMcpDialog.ts  |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...moryDialog.ts |    87.5 |      100 |     100 |    87.5 | 19,23             
  ...oryMonitor.ts |   83.14 |    78.57 |     100 |   83.14 | 54-63,74-79       
  ...ssageQueue.ts |     100 |     97.4 |     100 |     100 | 175,262           
  ...delCommand.ts |     100 |       96 |     100 |     100 | 61                
  ...ouseEvents.ts |   94.89 |       95 |   83.33 |   94.89 | 78-82             
  ...raseCycler.ts |   84.74 |    76.47 |     100 |   84.74 | ...49,52-53,69-71 
  ...rredEditor.ts |   58.33 |    22.22 |     100 |   58.33 | 23-27,29-33       
  ...derUpdates.ts |   85.29 |    80.28 |    92.3 |   85.29 | ...36,351-361,441 
  useQwenAuth.ts   |     100 |      100 |     100 |     100 |                   
  ...lScheduler.ts |   89.13 |     86.9 |     100 |   89.13 | ...61-463,496-506 
  ...oryCommand.ts |       0 |        0 |       0 |       0 | 1-7               
  ...umeCommand.ts |    95.4 |    77.77 |     100 |    95.4 | 133-134,236-241   
  ...ompletion.tsx |   90.67 |    83.33 |     100 |   90.67 | ...02,105,138-141 
  ...ectionList.ts |   97.12 |    96.19 |     100 |   97.12 | ...92-193,247-250 
  ...sionPicker.ts |   92.87 |    90.35 |     100 |   92.87 | ...99-501,503-505 
  ...earchInput.ts |     100 |    97.29 |     100 |     100 | 82                
  ...ngsCommand.ts |   18.75 |      100 |       0 |   18.75 | 10-25             
  ...ellHistory.ts |   93.28 |    80.95 |     100 |   93.28 | ...96,153-154,164 
  ...oryCommand.ts |   85.48 |    58.33 |     100 |   85.48 | 22-28,40,71       
  ...agerDialog.ts |   88.23 |      100 |     100 |   88.23 | 20,24             
  ...Completion.ts |   82.79 |    85.33 |   94.73 |   82.79 | ...86-688,696-732 
  ...tateAndRef.ts |     100 |      100 |     100 |     100 |                   
  ...tatsDialog.ts |     100 |      100 |     100 |     100 |                   
  useStatusLine.ts |   97.32 |    93.93 |     100 |   97.32 | ...18-422,518-525 
  ...eateDialog.ts |   88.23 |      100 |     100 |   88.23 | 14,18             
  ...mInProcess.ts |   27.35 |       80 |      25 |   27.35 | ...82-183,186-188 
  ...tification.ts |     100 |     87.5 |     100 |     100 | 50                
  ...alProgress.ts |   67.34 |    58.82 |   66.66 |   67.34 | 52-53,61-68,79-85 
  ...rminalSize.ts |     100 |      100 |     100 |     100 |                   
  ...emeCommand.ts |    79.2 |    35.29 |     100 |    79.2 | ...15-116,120-121 
  useTimer.ts      |   97.59 |    94.73 |     100 |   97.59 | 17-18             
  ...lMigration.ts |       0 |        0 |       0 |       0 |                   
  ...rustModify.ts |     100 |    90.47 |     100 |     100 | 112,134           
  useTurnDiffs.ts  |   95.12 |    78.57 |     100 |   95.12 | 133-134,156-157   
  ...elcomeBack.ts |   87.36 |     90.9 |     100 |   87.36 | ...,94-96,114-115 
  ...reeSession.ts |   93.75 |       70 |     100 |   93.75 | 47-48,72          
  vim.ts           |      74 |    67.56 |   69.23 |      74 | ...1854-1861,1869 
 src/ui/layouts    |   91.25 |    89.47 |     100 |   91.25 |                   
  ...AppLayout.tsx |   90.99 |     87.5 |     100 |   90.99 | 61-63,111-116,152 
  ...AppLayout.tsx |   91.66 |    92.85 |     100 |   91.66 | 75-80             
 src/ui/models     |   80.72 |       80 |   71.42 |   80.72 |                   
  ...ableModels.ts |   80.72 |       80 |   71.42 |   80.72 | ...,61-71,125-127 
 ...noninteractive |     100 |      100 |    6.66 |     100 |                   
  ...eractiveUi.ts |     100 |      100 |    6.66 |     100 |                   
 src/ui/selection  |   93.56 |    86.19 |     100 |   93.56 |                   
  screen-buffer.ts |   94.73 |    66.66 |     100 |   94.73 | 51-52             
  ...ion-coords.ts |     100 |      100 |     100 |     100 |                   
  ...ction-span.ts |   93.81 |     92.1 |     100 |   93.81 | ...1,45-46,99-100 
  ...tion-state.ts |     100 |      100 |     100 |     100 |                   
  ...ction-text.ts |   93.85 |    93.44 |     100 |   93.85 | 30-34,130-131     
  ...selection.tsx |   91.88 |    78.57 |     100 |   91.88 | ...16-417,446-447 
 src/ui/state      |      95 |    81.81 |     100 |      95 |                   
  extensions.ts    |      95 |    81.81 |     100 |      95 | 69-70,89          
 src/ui/themes     |    98.5 |    73.17 |     100 |    98.5 |                   
  ansi-light.ts    |     100 |      100 |     100 |     100 |                   
  ansi.ts          |     100 |      100 |     100 |     100 |                   
  atom-one-dark.ts |     100 |      100 |     100 |     100 |                   
  ayu-light.ts     |     100 |      100 |     100 |     100 |                   
  ayu.ts           |     100 |      100 |     100 |     100 |                   
  color-utils.ts   |   99.23 |    97.05 |     100 |   99.23 | 277-278           
  default-light.ts |     100 |      100 |     100 |     100 |                   
  default.ts       |     100 |      100 |     100 |     100 |                   
  ...inal-theme.ts |   88.59 |    85.96 |     100 |   88.59 | ...57-261,266-270 
  dracula.ts       |     100 |      100 |     100 |     100 |                   
  github-dark.ts   |     100 |      100 |     100 |     100 |                   
  github-light.ts  |     100 |      100 |     100 |     100 |                   
  googlecode.ts    |     100 |      100 |     100 |     100 |                   
  no-color.ts      |     100 |      100 |     100 |     100 |                   
  qwen-dark.ts     |     100 |      100 |     100 |     100 |                   
  qwen-light.ts    |     100 |      100 |     100 |     100 |                   
  ...tic-tokens.ts |     100 |      100 |     100 |     100 |                   
  ...-of-purple.ts |     100 |      100 |     100 |     100 |                   
  theme-manager.ts |   88.68 |    84.52 |     100 |   88.68 | ...83-392,397-398 
  theme.ts         |     100 |    38.02 |     100 |     100 | ...34-449,457-461 
  xcode.ts         |     100 |      100 |     100 |     100 |                   
 src/ui/utils      |   87.73 |    85.83 |   96.06 |   87.73 |                   
  ...Colorizer.tsx |   80.31 |    85.41 |     100 |   80.31 | ...00-201,313-339 
  ...nRenderer.tsx |   80.07 |     75.6 |     100 |   80.07 | ...70,274,332-333 
  ...wnDisplay.tsx |   92.87 |     93.5 |     100 |   92.87 | ...,955,1002-1020 
  ...idDiagram.tsx |   87.79 |    95.34 |     100 |   87.79 | 156-179           
  ...eRenderer.tsx |   93.63 |    81.77 |   95.23 |   93.63 | ...47-750,803-808 
  ...odeDisplay.ts |   94.28 |    85.71 |     100 |   94.28 | 23,40             
  asciiCharts.ts   |    96.7 |     87.5 |     100 |    96.7 | 170-177,278       
  ...dWorkUtils.ts |     100 |      100 |     100 |     100 |                   
  ...boardUtils.ts |    52.9 |    74.15 |    92.3 |    52.9 | ...29,632-641,644 
  commandUtils.ts  |   98.61 |    93.27 |     100 |   98.61 | 189,217-218,424   
  computeStats.ts  |     100 |      100 |     100 |     100 |                   
  customBanner.ts  |   90.68 |    91.22 |     100 |   90.68 | ...13,324-327,334 
  displayUtils.ts  |   73.84 |    73.91 |     100 |   73.84 | ...34,36-40,42-46 
  formatters.ts    |   94.87 |    98.24 |     100 |   94.87 | 116-119           
  goal-runtime.ts  |   91.42 |       95 |     100 |   91.42 | 32-34             
  gradientUtils.ts |     100 |      100 |     100 |     100 |                   
  highlight.ts     |     100 |      100 |     100 |     100 |                   
  ...gap-notice.ts |     100 |      100 |     100 |     100 |                   
  ...oryMapping.ts |     100 |    95.65 |     100 |     100 | 45,151            
  historyUtils.ts  |   96.07 |     97.1 |     100 |   96.07 | 104-107           
  ...mage-parts.ts |   97.75 |    94.59 |     100 |   97.75 | 82-83             
  inline-math.ts   |   98.48 |    95.23 |     100 |   98.48 | 129-130           
  input-mouse.ts   |     100 |    85.71 |     100 |     100 | 48,93             
  isNarrowWidth.ts |     100 |      100 |     100 |     100 |                   
  ...olDetector.ts |   68.81 |       75 |   66.66 |   68.81 | ...27-132,160-161 
  latexRenderer.ts |   94.95 |     73.8 |     100 |   94.95 | ...76-178,184-187 
  layoutUtils.ts   |     100 |      100 |     100 |     100 |                   
  list-mouse.ts    |     100 |      100 |     100 |     100 |                   
  ...ightLoader.ts |     100 |       95 |     100 |     100 | 81                
  ...nUtilities.ts |   98.72 |    94.36 |     100 |   98.72 | 145-146           
  ...t-position.ts |     100 |     87.5 |     100 |     100 | 85                
  ...geRenderer.ts |   86.51 |    70.16 |   95.12 |   86.51 | ...1286,1326-1332 
  ...alRenderer.ts |   86.69 |     71.9 |     100 |   86.69 | ...1476,1513-1519 
  ...lsBySource.ts |     100 |    95.23 |     100 |     100 | 84                
  mouse.ts         |   92.85 |    74.19 |     100 |   92.85 | ...38,145,149-152 
  osc8.ts          |   91.33 |    79.03 |     100 |   91.33 | ...73,273,277-278 
  ...red-height.ts |   98.38 |    97.14 |     100 |   98.38 | 195-197           
  ...mConstants.ts |     100 |      100 |     100 |     100 |                   
  restoreGoal.ts   |     100 |      100 |     100 |     100 |                   
  ...storyUtils.ts |   83.24 |    80.12 |     100 |   83.24 | ...02-624,755-756 
  ...ickerUtils.ts |     100 |      100 |     100 |     100 |                   
  ...evel-label.ts |   77.77 |    66.66 |     100 |   77.77 | 18,22-24          
  ...are-cursor.ts |   89.47 |    85.71 |     100 |   89.47 | 39-44             
  ...ataService.ts |   93.17 |     79.1 |     100 |   93.17 | ...14,227,254-256 
  suggestions.ts   |     100 |      100 |     100 |     100 |                   
  ...izedOutput.ts |   95.19 |      100 |   88.88 |   95.19 | 121-126           
  ...nal-buffer.ts |     100 |      100 |     100 |     100 |                   
  ...e-renderer.ts |   90.61 |    83.44 |     100 |   90.61 | ...80,482-484,607 
  ...ize-reflow.ts |     100 |     92.3 |     100 |     100 | 57,62,209,217,347 
  ...wOptimizer.ts |     100 |    94.73 |     100 |     100 | 35,78             
  terminalSetup.ts |    4.37 |      100 |       0 |    4.37 | 44-393            
  textUtils.ts     |   98.71 |    95.72 |     100 |   98.71 | 292-293,478-479   
  ...background.ts |     100 |      100 |     100 |     100 |                   
  todoSnapshot.ts  |   90.42 |    92.85 |     100 |   90.42 | ...06-207,240-241 
  ...isplay-map.ts |     100 |      100 |     100 |     100 |                   
  updateCheck.ts   |     100 |    92.75 |     100 |     100 | 227-239,331       
  windowTitle.ts   |   96.55 |    94.73 |     100 |   96.55 | 56-57             
  ...ow-keyword.ts |     100 |      100 |     100 |     100 |                   
 ...i/utils/export |   75.03 |     60.1 |   94.59 |   75.03 |                   
  collect.ts       |   71.27 |    65.81 |      96 |   71.27 | ...90-633,655-656 
  index.ts         |     100 |      100 |     100 |     100 |                   
  normalize.ts     |   80.42 |    51.35 |     100 |   80.42 | ...59-364,376-378 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
  utils.ts         |     100 |      100 |     100 |     100 |                   
 ...ort/formatters |   52.92 |    47.22 |   71.42 |   52.92 |                   
  html.ts          |   84.61 |       50 |     100 |   84.61 | ...53,57-58,62-63 
  json.ts          |     100 |      100 |     100 |     100 |                   
  jsonl.ts         |   82.45 |     37.5 |     100 |   82.45 | ...48,50-51,65-66 
  markdown.ts      |   36.32 |    47.05 |      50 |   36.32 | ...16-219,233-295 
 src/ui/voice      |   81.24 |    79.78 |   81.69 |   81.24 |                   
  ...d-recorder.ts |     6.2 |      100 |       0 |     6.2 | ...33-159,162-163 
  ...o-recorder.ts |   84.61 |    93.33 |   57.14 |   84.61 | ...16-117,131-136 
  ...me-session.ts |   91.09 |     92.1 |     100 |   91.09 | ...99,305,316-319 
  sox-recorder.ts  |    92.7 |    71.87 |     100 |    92.7 | ...34-135,153-154 
  ...ailability.ts |     100 |      100 |     100 |     100 |                   
  ...e-keyterms.ts |     100 |      100 |     100 |     100 |                   
  voice-model.ts   |     100 |      100 |     100 |     100 |                   
  ...e-recorder.ts |   88.29 |    67.74 |   81.81 |   88.29 | ...,98-99,112,115 
  voice-refine.ts  |     100 |    93.33 |     100 |     100 | 92                
  ...ream-retry.ts |   86.79 |       70 |     100 |   86.79 | 16-18,48-49,59-60 
  ...am-session.ts |   88.02 |    66.66 |   84.61 |   88.02 | ...26,343-345,363 
  ...ranscriber.ts |     100 |      100 |     100 |     100 |                   
 src/utils         |   92.25 |    89.67 |   96.39 |   92.25 |                   
  ...p-profiler.ts |   98.39 |    92.59 |     100 |   98.39 | 141,185,235       
  acpModelUtils.ts |   97.36 |    95.14 |     100 |   97.36 | ...09-210,214-215 
  apiPreconnect.ts |   96.74 |    94.59 |     100 |   96.74 | 167-170           
  ...ol-call-id.ts |   84.61 |       60 |     100 |   84.61 | 26-27,37-38       
  checks.ts        |   33.33 |      100 |       0 |   33.33 | 23-28             
  ...-api-error.ts |     100 |    96.42 |     100 |     100 | 14                
  cleanup.ts       |   84.05 |    94.11 |      80 |   84.05 | 80,111-121        
  ...y-identity.ts |   87.06 |    81.91 |     100 |   87.06 | ...70-371,378-379 
  ...Calculator.ts |     100 |      100 |     100 |     100 |                   
  cpuProfiler.ts   |   70.73 |    73.23 |   88.88 |   70.73 | ...27,430-431,438 
  deepMerge.ts     |     100 |       90 |     100 |     100 | 50-52,58          
  ...re-runtime.ts |     100 |      100 |     100 |     100 |                   
  ...putCapture.ts |   90.65 |    86.31 |     100 |   90.65 | ...73,371,373-374 
  ...arResolver.ts |   97.14 |    96.55 |     100 |   97.14 | 125-126           
  errors.ts        |   97.56 |    94.64 |     100 |   97.56 | 69-70,304-305     
  events.ts        |     100 |      100 |     100 |     100 |                   
  ...on-mention.ts |   88.48 |     82.6 |     100 |   88.48 | ...56-160,164-168 
  gitUtils.ts      |   92.85 |    86.66 |     100 |   92.85 | ...13-116,164-167 
  ...tyWarnings.ts |     100 |      100 |     100 |     100 |                   
  ...lationInfo.ts |   97.81 |    94.69 |     100 |   97.81 | ...03,420-421,466 
  ...projection.ts |   95.27 |    95.58 |     100 |   95.27 | 140-145           
  jsonc-editor.ts  |   93.18 |    92.66 |     100 |   93.18 | ...80-381,384-385 
  load-undici.ts   |     100 |      100 |     100 |     100 |                   
  ...npm-update.ts |   86.64 |    77.02 |     100 |   86.64 | ...03-304,335-345 
  math.ts          |       0 |        0 |       0 |       0 | 1-15              
  ...er-mention.ts |     100 |    66.66 |     100 |     100 | 14,30,44-46       
  ...iagnostics.ts |   94.57 |    83.01 |   88.88 |   94.57 | ...05,311,315-317 
  ...serMessage.ts |     100 |      100 |     100 |     100 |                   
  ...onfigUtils.ts |   94.25 |    91.17 |     100 |   94.25 | ...30,436,439-443 
  ...-part-list.ts |     100 |      100 |     100 |     100 |                   
  osc.ts           |   97.18 |      100 |    87.5 |   97.18 | 182-183           
  package.ts       |   88.88 |    85.71 |     100 |   88.88 | 31-32             
  paths.ts         |     100 |      100 |     100 |     100 |                   
  processUtils.ts  |    92.3 |       80 |     100 |    92.3 | 45-46             
  readStdin.ts     |   93.67 |    94.11 |   85.71 |   93.67 | 79-83             
  relaunch.ts      |   95.87 |    89.28 |     100 |   95.87 | 103-105,131       
  resolvePath.ts   |     100 |      100 |     100 |     100 |                   
  runBudget.ts     |   99.35 |    96.77 |     100 |   99.35 | 119               
  sandbox-path.ts  |     100 |      100 |     100 |     100 |                   
  ...xImageName.ts |     100 |    77.77 |     100 |     100 | 10,18             
  sandboxMounts.ts |     100 |      100 |     100 |     100 |                   
  ...-path-argv.ts |     100 |      100 |     100 |     100 |                   
  sessionPaths.ts  |   90.84 |    90.56 |     100 |   90.84 | ...81-182,185-186 
  shell-args.ts    |     100 |      100 |     100 |     100 |                   
  spawnWrapper.ts  |     100 |      100 |     100 |     100 |                   
  ...ate-verify.ts |     100 |      100 |     100 |     100 |                   
  ...upProfiler.ts |   98.47 |    94.66 |     100 |   98.47 | 132-133,308       
  ...upWarnings.ts |     100 |      100 |     100 |     100 |                   
  stdioHelpers.ts  |     100 |       90 |     100 |     100 | 23                
  ...alSequence.ts |     100 |    97.61 |     100 |     100 | 60                
  ...iffPreview.ts |   76.47 |       25 |     100 |   76.47 | 13,17,23-24       
  ...on-handler.ts |    73.8 |       75 |     100 |    73.8 | 17-18,25-26,67-73 
  ...entEmitter.ts |     100 |      100 |     100 |     100 |                   
  ...ansionHook.ts |     100 |      100 |     100 |     100 |                   
  ...upWarnings.ts |   87.75 |       75 |     100 |   87.75 | 47-48,53-54,57-58 
  version.ts       |     100 |    66.66 |     100 |     100 | 11                
  ...ingHandler.ts |     100 |      100 |     100 |     100 |                   
  ...WithBackup.ts |   65.04 |    77.77 |     100 |   65.04 | 97,112,133-172    
 ...s/housekeeping |   94.35 |    94.11 |     100 |   94.35 |                   
  cleanup.ts       |   92.59 |    93.75 |     100 |   92.59 | ...02-205,209-211 
  ...eractionAt.ts |     100 |      100 |     100 |     100 |                   
  throttledOnce.ts |   95.95 |    93.93 |     100 |   95.95 | 77-78,153-154     
-------------------|---------|----------|---------|---------|-------------------
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   88.52 |    87.03 |   90.24 |   88.52 |                   
 src               |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/__mocks__/fs  |       0 |        0 |       0 |       0 |                   
  promises.ts      |       0 |        0 |       0 |       0 | 1-48              
 src/agents        |   90.24 |    84.51 |   94.55 |   90.24 |                   
  ...transcript.ts |   88.49 |    84.09 |     100 |   88.49 | ...32,640,646-650 
  ...ent-resume.ts |   85.64 |       78 |    85.1 |   85.64 | ...1793-1797,1800 
  ...ound-tasks.ts |   95.19 |    90.75 |   96.42 |   95.19 | ...1889,1897-1898 
  forkedAgent.ts   |   93.18 |    83.47 |   94.44 |   93.18 | ...90,698,703-710 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...ent-result.ts |    96.8 |    92.68 |     100 |    96.8 | 106,129-131       
  ...n-registry.ts |   95.27 |    88.23 |   98.33 |   95.27 | ...1478,1492-1494 
  ...w-snapshot.ts |   75.73 |    72.22 |    87.5 |   75.73 | ...21,445,452-454 
  worktree-pin.ts  |     100 |    88.23 |     100 |     100 | 78,99             
 src/agents/arena  |   76.96 |    68.22 |   78.94 |   76.96 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.91 |     65.2 |   78.57 |   75.91 | ...1888,1894-1895 
  arena-events.ts  |   64.44 |      100 |      50 |   64.44 | ...71-175,178-183 
  diff-summary.ts  |    87.5 |    72.34 |     100 |    87.5 | ...32-133,137-138 
  index.ts         |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...gents/backends |   78.07 |    85.19 |   76.12 |   78.07 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |   90.87 |    85.24 |   93.18 |   90.87 | ...83,685,687-688 
  TmuxBackend.ts   |    90.7 |    76.55 |   97.36 |    90.7 | ...87,697,743-747 
  detect.ts        |   31.25 |      100 |       0 |   31.25 | 34-88             
  index.ts         |     100 |      100 |     100 |     100 |                   
  iterm-it2.ts     |     100 |     92.1 |     100 |     100 | 37-38,106         
  tmux-commands.ts |    6.64 |      100 |    3.03 |    6.64 | ...93-363,386-503 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...agents/runtime |   93.21 |    87.65 |   91.18 |   93.21 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   90.27 |    80.44 |   80.95 |   90.27 | ...2525,2571-2573 
  agent-events.ts  |     100 |      100 |     100 |     100 |                   
  ...t-headless.ts |   93.49 |    89.41 |   83.33 |   93.49 | ...96-497,500-501 
  ...nteractive.ts |   81.01 |    82.35 |   76.66 |   81.01 | ...33,535-538,541 
  ...statistics.ts |   98.29 |    82.55 |     100 |   98.29 | 141,165,206,239   
  agent-types.ts   |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...ool-policy.ts |   98.38 |      100 |    92.3 |   98.38 | 85-86             
  ...low-budget.ts |     100 |      100 |     100 |     100 |                   
  ...-scheduler.ts |   97.43 |    96.36 |     100 |   97.43 | 128-130           
  ...ow-journal.ts |   92.78 |    78.12 |     100 |   92.78 | ...49-150,192-194 
  ...ta-literal.ts |   95.96 |    92.63 |     100 |   95.96 | ...78-379,395-396 
  ...chestrator.ts |   93.87 |    90.47 |   91.48 |   93.87 | ...2216,2309-2312 
  ...ow-prompts.ts |     100 |      100 |     100 |     100 |                   
  ...low-runner.ts |   95.47 |    83.47 |   94.44 |   95.47 | ...44,312,332-335 
  ...ow-sandbox.ts |   96.91 |    91.02 |     100 |   96.91 | ...1704,1710-1711 
  ...flow-saved.ts |   96.51 |    94.36 |     100 |   96.51 | 134-135,234-237   
  ...flow-stall.ts |    97.9 |    83.33 |     100 |    97.9 | 170-171,270       
 src/agents/tasks  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/team   |   84.25 |    85.18 |   91.03 |   84.25 |                   
  TeamManager.ts   |   77.21 |    83.04 |   83.87 |   77.21 | ...1832,1855-1856 
  identity.ts      |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...sionBridge.ts |     100 |      100 |     100 |     100 |                   
  mailbox.ts       |   96.02 |    87.23 |     100 |   96.02 | 352-358           
  ...ptAddendum.ts |     100 |      100 |     100 |     100 |                   
  tasks.ts         |   89.29 |       83 |     100 |   89.29 | ...1000,1044-1045 
  team-events.ts   |   73.68 |      100 |   66.66 |   73.68 | 140-144,151-155   
  teamHelpers.ts   |   91.71 |    94.44 |      95 |   91.71 | ...18-319,355-365 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...eam/test-utils |   95.06 |    95.16 |   98.21 |   95.06 |                   
  ...on-harness.ts |   96.49 |       85 |     100 |   96.49 | 128-129,141-142   
  fake-agent.ts    |     100 |    96.77 |     100 |     100 | 158,167           
  fake-backend.ts  |   86.46 |    97.61 |   95.83 |   86.46 | 124-146           
 src/config        |   85.25 |    87.57 |   77.31 |   85.25 |                   
  approval-mode.ts |     100 |      100 |     100 |     100 |                   
  ...xtDefaults.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |    84.1 |    87.09 |   75.28 |    84.1 | ...8990,8997-8998 
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 27                
  models.ts        |     100 |      100 |     100 |     100 |                   
  ...sDiscovery.ts |   97.46 |    93.05 |     100 |   97.46 | ...04,182-183,202 
  storage.ts       |   94.39 |    91.57 |   88.23 |   94.39 | ...45-446,449-450 
 ...nfirmation-bus |   98.27 |    97.22 |     100 |   98.27 |                   
  message-bus.ts   |   98.14 |    97.14 |     100 |   98.14 | 42-43             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/core          |   92.76 |    88.56 |   93.79 |   92.76 |                   
  ...on-restore.ts |   88.23 |    85.41 |     100 |   88.23 | ...60,63-64,67-68 
  baseLlmClient.ts |    88.4 |    83.68 |   81.81 |    88.4 | ...59,672,678-680 
  client.ts        |    92.7 |    88.26 |   91.11 |    92.7 | ...4351,4449-4450 
  ...tGenerator.ts |   87.45 |    88.09 |   88.88 |   87.45 | ...08-509,554-560 
  ...lScheduler.ts |   89.82 |    84.83 |   94.73 |   89.82 | ...6483,6511-6527 
  ...entContext.ts |   96.63 |    90.13 |   96.66 |   96.63 | ...42,444-445,512 
  geminiChat.ts    |   95.19 |    90.72 |   96.69 |   95.19 | ...5651,5696-5697 
  geminiRequest.ts |     100 |      100 |     100 |     100 |                   
  genai-compat.ts  |     100 |      100 |     100 |     100 |                   
  ...MediaLimit.ts |     100 |       96 |     100 |     100 | 96                
  ...htProtocol.ts |    9.09 |      100 |       0 |    9.09 | ...9,62-66,69-110 
  ...ream-error.ts |     100 |      100 |     100 |     100 |                   
  logger.ts        |   87.41 |    87.02 |     100 |   87.41 | ...64-568,614-628 
  ...lay-buffer.ts |     100 |      100 |     100 |     100 |                   
  ...dispatcher.ts |     100 |      100 |     100 |     100 |                   
  ...tyDefaults.ts |     100 |      100 |     100 |     100 |                   
  ...olExecutor.ts |   93.54 |    83.33 |      50 |   93.54 | 46-47             
  output-styles.ts |     100 |      100 |     100 |     100 |                   
  ...on-helpers.ts |   93.49 |    78.57 |     100 |   93.49 | ...10-211,228-229 
  ...issionFlow.ts |   98.98 |    96.96 |     100 |   98.98 | 109               
  ...try-policy.ts |     100 |      100 |     100 |     100 |                   
  ...ell-policy.ts |   94.89 |    88.54 |     100 |   94.89 | ...51-252,297-298 
  prompts.ts       |   93.89 |    91.66 |      85 |   93.89 | ...1272,1475-1476 
  ...ing-effort.ts |     100 |      100 |     100 |     100 |                   
  ...n-recovery.ts |   95.13 |       80 |     100 |   95.13 | ...06-107,142-144 
  ...t-profiler.ts |    97.9 |    81.15 |   88.23 |    97.9 | 117,124-125,130   
  ...port-retry.ts |     100 |      100 |     100 |     100 |                   
  tokenLimits.ts   |     100 |    91.89 |     100 |     100 | 87,122-139        
  ...-arguments.ts |     100 |      100 |     100 |     100 |                   
  ...reparation.ts |     100 |      100 |     100 |     100 |                   
  ...tion-guard.ts |   90.38 |    94.73 |     100 |   90.38 | 83-87             
  ...allIdUtils.ts |   98.81 |    91.22 |     100 |   98.81 | 43,52             
  ...okTriggers.ts |   99.45 |    92.43 |     100 |   99.45 | 182,193           
  ...terruption.ts |     100 |     92.3 |     100 |     100 | 86,104            
  turn.ts          |   99.19 |    94.48 |     100 |   99.19 | 698-699,768       
  ...l-fallback.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   96.59 |    89.11 |   97.43 |   96.59 |                   
  ...tGenerator.ts |   97.67 |    88.91 |   97.43 |   97.67 | ...1497,1526,1537 
  converter.ts     |   96.19 |    89.25 |     100 |   96.19 | ...1334,1555-1557 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
  usage.ts         |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   89.24 |    72.72 |   94.11 |   89.24 |                   
  ...tGenerator.ts |   87.54 |    71.42 |   93.75 |   87.54 | ...93-294,356-362 
  index.ts         |     100 |    85.71 |     100 |     100 | 51                
 ...ntentGenerator |   96.65 |     91.3 |   95.23 |   96.65 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   96.59 |    90.75 |      95 |   96.59 | ...1299-1300,1328 
  ...tDetection.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   92.18 |    90.83 |   96.58 |   92.18 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |   91.26 |    89.66 |   96.87 |   91.26 | ...1948,2117-2132 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   68.25 |    82.35 |      50 |   68.25 | 44-53,74-78,90-94 
  ...tGenerator.ts |      70 |    73.33 |     100 |      70 | ...07-112,121-127 
  pipeline.ts      |   95.36 |    91.52 |     100 |   95.36 | ...1433-1434,1541 
  ...ix-caching.ts |   95.23 |    92.85 |     100 |   95.23 | 45-46,69-70       
  ...ureContext.ts |     100 |      100 |     100 |     100 |                   
  ...ingOptions.ts |       0 |        0 |       0 |       0 | 1                 
  ...CallParser.ts |   92.11 |    92.25 |     100 |   92.11 | ...21-522,542-545 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |    97.2 |    91.81 |   98.63 |    97.2 |                   
  dashscope.ts     |   98.42 |    95.27 |   96.55 |   98.42 | ...51-752,894-895 
  deepseek.ts      |   95.23 |    89.79 |     100 |   95.23 | ...49-150,163-164 
  default.ts       |   98.87 |       96 |     100 |   98.87 | 178,304           
  index.ts         |     100 |      100 |     100 |     100 |                   
  mimo.ts          |   94.11 |    66.66 |     100 |   94.11 | 29,52-53          
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  mistral.ts       |   96.07 |    73.33 |     100 |   96.07 | 32-33             
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 |                   
  utils.ts         |     100 |      100 |     100 |     100 |                   
  zai.ts           |      90 |    76.31 |     100 |      90 | ...,72-73,173-175 
 src/extension     |   88.71 |    86.07 |   93.41 |   88.71 |                   
  ...ive-safety.ts |   97.77 |    93.75 |     100 |   97.77 | 100-101           
  ...-converter.ts |   80.55 |    73.66 |     100 |   80.55 | ...1133,1179-1180 
  corruptFile.ts   |     100 |       50 |     100 |     100 | 40-45             
  ...-converter.ts |     100 |      100 |     100 |     100 |                   
  ...redentials.ts |   95.33 |    89.47 |     100 |   95.33 | ...21-122,173-175 
  ...me-refresh.ts |     100 |      100 |     100 |     100 |                   
  ...sion-store.ts |   92.82 |    89.27 |    98.3 |   92.82 | ...1641-1647,1691 
  ...ionManager.ts |   84.52 |    83.52 |      83 |   84.52 | ...3139,3177-3178 
  ...references.ts |     100 |     90.9 |     100 |     100 | ...05,129,197,200 
  ...onSettings.ts |    92.3 |     94.4 |     100 |    92.3 | ...98-501,570-571 
  ...-converter.ts |    75.9 |    85.71 |   85.71 |    75.9 | ...98,202,214-248 
  github.ts        |    92.7 |     87.7 |     100 |    92.7 | ...1340-1341,1351 
  http-client.ts   |   84.61 |       80 |     100 |   84.61 | 20-21             
  i18n.ts          |   78.26 |       96 |      50 |   78.26 | 104-110,116-123   
  index.ts         |     100 |      100 |     100 |     100 |                   
  marketplace.ts   |   88.39 |    83.11 |     100 |   88.39 | ...08,494,507-508 
  ...ork-policy.ts |   89.72 |    90.16 |     100 |   89.72 | ...36,148-154,156 
  npm.ts           |   89.02 |    81.81 |     100 |   89.02 | ...86-688,695-700 
  override.ts      |   94.11 |    93.54 |     100 |   94.11 | 63-64,81-82       
  ...-converter.ts |   94.89 |    90.41 |     100 |   94.89 | ...50-151,222-224 
  redaction.ts     |     100 |      100 |     100 |     100 |                   
  settings.ts      |   66.26 |      100 |      50 |   66.26 | 81-107,141-146    
  ...ceRegistry.ts |   94.01 |    83.33 |     100 |   94.01 | ...38-344,365-366 
  storage.ts       |     100 |      100 |     100 |     100 |                   
  ...ableSchema.ts |     100 |      100 |     100 |     100 |                   
  variables.ts     |   88.95 |    84.21 |     100 |   88.95 | ...32-235,238-241 
  ...extraction.ts |   85.77 |       81 |   89.47 |   85.77 | ...02-205,260-261 
 ...ent-plugins-v1 |   84.94 |    79.51 |     100 |   84.94 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  manifest.ts      |   81.87 |    84.48 |     100 |   81.87 | ...55-156,161-174 
  mcp.ts           |   84.98 |    79.56 |     100 |   84.98 | ...88-389,419-420 
  paths.ts         |     100 |    94.44 |     100 |     100 | 59                
  skills.ts        |   82.31 |    63.88 |     100 |   82.31 | ...38-141,150-151 
 src/followup      |   84.72 |    81.87 |   86.84 |   84.72 |                   
  followupState.ts |   98.44 |    95.74 |     100 |   98.44 | 236-237           
  index.ts         |     100 |      100 |     100 |     100 |                   
  overlayFs.ts     |   96.29 |    88.88 |     100 |   96.29 | 78,108,122        
  speculation.ts   |   76.36 |     70.4 |   58.33 |   76.36 | ...42-743,750-751 
  ...onToolGate.ts |   97.97 |     87.5 |     100 |   97.97 | 105,110           
  ...nGenerator.ts |   86.11 |    87.17 |     100 |   86.11 | ...39-244,356-358 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/goals         |      93 |    89.39 |   94.67 |      93 |                   
  ...eGoalStore.ts |   87.61 |    88.88 |   86.66 |   87.61 | ...85-188,196-204 
  ...t-verifier.ts |   96.27 |    91.17 |     100 |   96.27 | ...20,143-146,163 
  ...checkpoint.ts |   81.48 |    76.19 |     100 |   81.48 | ...02-105,115-118 
  ...ion-prompt.ts |     100 |      100 |     100 |     100 |                   
  goal-evidence.ts |   88.54 |    87.98 |   97.67 |   88.54 | ...1200,1223-1226 
  ...projection.ts |   66.66 |    72.97 |   33.33 |   66.66 | ...87,190,194-196 
  ...ersistence.ts |   87.36 |    85.71 |    87.5 |   87.36 | ...53-154,185-190 
  goal-protocol.ts |   96.87 |    95.65 |     100 |   96.87 | 215-216           
  goal-reducer.ts  |   95.25 |    92.82 |   97.29 |   95.25 | ...73,552,570-571 
  goal-runtime.ts  |   96.38 |    89.73 |   95.83 |   96.38 | ...1349-1350,1480 
  goal-tools.ts    |   98.38 |    94.17 |   95.83 |   98.38 | ...05-206,307-308 
  ...rn-context.ts |     100 |      100 |     100 |     100 |                   
  goal-verifier.ts |   92.46 |    93.02 |     100 |   92.46 | ...69-172,185-187 
  goal-wire.ts     |       0 |        0 |       0 |       0 | 1-28              
  goalHook.ts      |   96.91 |    92.42 |     100 |   96.91 | 115-120,221-222   
  goalJudge.ts     |   95.84 |    87.09 |     100 |   95.84 | ...55-356,448-449 
  index.ts         |     100 |      100 |     100 |     100 |                   
 src/hooks         |   88.07 |    86.25 |   88.54 |   88.07 |                   
  ...okRegistry.ts |   86.48 |    77.08 |     100 |   86.48 | ...41-344,362-369 
  ...bortSignal.ts |     100 |      100 |     100 |     100 |                   
  context-usage.ts |     100 |      100 |     100 |     100 |                   
  ...terpolator.ts |   96.66 |    93.33 |     100 |   96.66 | 66-67             
  ...HookRunner.ts |   96.68 |    87.23 |     100 |   96.68 | 110-112,231-233   
  ...Aggregator.ts |   96.57 |    91.48 |     100 |   96.57 | ...20-321,402,404 
  ...entHandler.ts |   95.57 |    84.76 |   94.73 |   95.57 | ...1040-1041,1051 
  hookPlanner.ts   |   87.55 |    85.54 |   86.66 |   87.55 | ...22-226,233-244 
  hookRegistry.ts  |   92.53 |    85.43 |     100 |   92.53 | ...39,458,462,466 
  hookRunner.ts    |   62.65 |    72.34 |   66.66 |   62.65 | ...70-771,780-781 
  hookSystem.ts    |   87.64 |     98.5 |   70.83 |   87.64 | ...58-759,765-766 
  ...HookRunner.ts |   79.06 |    66.66 |      80 |   79.06 | ...33-434,452-456 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...edCallback.ts |     100 |      100 |     100 |     100 |                   
  ...HookRunner.ts |   94.19 |    84.37 |   81.81 |   94.19 | ...76-384,458-459 
  ...SkillHooks.ts |   78.75 |       75 |   66.66 |   78.75 | 62-66,137-152     
  ...oksManager.ts |   94.87 |    88.88 |     100 |   94.87 | ...84,325,327-329 
  ssrfGuard.ts     |   86.45 |    87.91 |     100 |   86.45 | ...85,289-295,301 
  stopHookCap.ts   |     100 |      100 |     100 |     100 |                   
  trustedHooks.ts  |      90 |    52.63 |     100 |      90 | ...53,66-67,97-98 
  types.ts         |   94.25 |    96.09 |   88.88 |   94.25 | ...46-547,632-636 
  urlValidator.ts  |     100 |      100 |     100 |     100 |                   
  ...it-context.ts |     100 |      100 |     100 |     100 |                   
 src/ide           |   76.98 |    85.03 |   79.03 |   76.98 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  detect-ide.ts    |     100 |      100 |     100 |     100 |                   
  ide-client.ts    |   69.16 |    84.65 |   68.29 |   69.16 | ...1068,1097-1105 
  ide-installer.ts |   89.06 |    79.31 |     100 |   89.06 | ...36,143-147,160 
  ideContext.ts    |     100 |      100 |     100 |     100 |                   
  process-utils.ts |   84.84 |    71.79 |     100 |   84.84 | ...37,151,193-194 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/lsp           |   58.96 |    70.67 |   66.49 |   58.96 |                   
  ...nfigLoader.ts |   80.55 |    72.22 |   95.65 |   80.55 | ...02-504,508-514 
  ...ionFactory.ts |   42.81 |    73.07 |      50 |   42.81 | ...76-427,433-450 
  ...Normalizer.ts |   23.09 |    13.72 |   30.43 |   23.09 | ...04-905,909-924 
  ...verManager.ts |   75.73 |     80.1 |   79.66 |   75.73 | ...1346,1352-1382 
  ...eLspClient.ts |   32.78 |    81.81 |   21.05 |   32.78 | ...89-293,299-300 
  ...LspService.ts |      60 |    73.36 |   78.26 |      60 | ...1575,1635-1645 
  configHash.ts    |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/mcp           |    82.3 |    77.81 |   78.33 |    82.3 |                   
  configHash.ts    |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...h-provider.ts |   86.95 |      100 |   33.33 |   86.95 | ...,93,97,101-102 
  ...h-provider.ts |   79.31 |    58.06 |     100 |   79.31 | ...26-933,940-942 
  ...en-storage.ts |   98.78 |    97.95 |     100 |   98.78 | 106-107           
  oauth-utils.ts   |   73.61 |    85.48 |    92.3 |   73.61 | ...46-366,392-421 
  ...n-provider.ts |   89.83 |       96 |   45.45 |   89.83 | ...43,147,151-152 
 .../token-storage |   82.12 |    88.48 |   89.28 |   82.12 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   87.08 |    87.71 |   95.23 |   87.08 | ...00-201,214-215 
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   68.14 |    82.35 |   64.28 |   68.14 | ...81-295,298-314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/memory        |   88.97 |    85.08 |   91.31 |   88.97 |                   
  ...y-document.ts |   89.52 |    84.61 |     100 |   89.52 | ...24-325,329-330 
  ...nel-memory.ts |   97.36 |    96.63 |   96.42 |   97.36 | ...91-293,367-368 
  dream.ts         |    64.6 |    72.22 |      50 |    64.6 | ...04-109,124-165 
  ...entPlanner.ts |     100 |    83.33 |     100 |     100 | 135,145           
  entries.ts       |   75.59 |    84.84 |   83.33 |   75.59 | ...56-157,172-180 
  extract.ts       |   93.82 |    84.09 |     100 |   93.82 | 78-83,122,154-157 
  ...entPlanner.ts |   91.55 |    76.74 |     100 |   91.55 | ...05,118-121,296 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |   90.16 |    78.76 |   94.44 |   90.16 | ...06,629,642-648 
  indexer.ts       |   94.14 |       84 |     100 |   94.14 | ...32-233,334,337 
  ...kill-agent.ts |   97.94 |    89.36 |     100 |   97.94 | 82-83,179-180     
  manager.ts       |   78.51 |    83.16 |   77.77 |   78.51 | ...1487,1500-1502 
  ...ent-config.ts |   86.99 |    82.69 |   86.36 |   86.99 | ...69,389,396-402 
  memoryAge.ts     |   90.47 |    83.33 |     100 |   90.47 | 50-51             
  ...yDiscovery.ts |   93.42 |    90.72 |     100 |   93.42 | ...11,370,592-595 
  paths.ts         |     100 |      100 |     100 |     100 |                   
  ...ing-skills.ts |     100 |       72 |     100 |     100 | 31-35,73-78,97    
  prompt.ts        |   97.26 |    86.79 |     100 |   97.26 | ...10-218,222,225 
  recall.ts        |   86.86 |    86.23 |   92.85 |   86.86 | ...33-538,571-582 
  refresh.ts       |   93.58 |    89.58 |     100 |   93.58 | ...75-176,183-184 
  ...ceSelector.ts |    93.2 |    85.71 |     100 |    93.2 | ...45-146,148-149 
  remember.ts      |   98.88 |    90.19 |     100 |   98.88 | 50,70             
  scan.ts          |   93.75 |       80 |     100 |   93.75 | ...08-109,154,157 
  scopes.ts        |     100 |      100 |     100 |     100 |                   
  ...et-scanner.ts |     100 |      100 |     100 |     100 |                   
  ...entPlanner.ts |   76.89 |    74.07 |   72.22 |   76.89 | ...47-451,454,460 
  status.ts        |   10.52 |      100 |       0 |   10.52 | 41-98             
  store.ts         |   92.92 |    81.81 |     100 |   92.92 | ...16-117,147-148 
  ...git-status.ts |     100 |    85.71 |     100 |     100 | 27                
  ...cret-guard.ts |     100 |      100 |     100 |     100 |                   
  ...emory-sync.ts |   94.24 |    82.85 |     100 |   94.24 | ...34-236,246-247 
  types.ts         |     100 |      100 |     100 |     100 |                   
  ...ontextFile.ts |   81.21 |     79.1 |   81.81 |   81.21 | ...66-280,294-299 
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   92.82 |    89.74 |   91.35 |   92.82 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   97.77 |    91.83 |     100 |   97.77 | 155,161,171       
  ...capability.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   79.43 |    68.96 |   85.71 |   79.43 | ...,89-96,131-142 
  ...igResolver.ts |   98.71 |    93.33 |     100 |   98.71 | 166,328,334       
  modelRegistry.ts |     100 |    98.11 |     100 |     100 | 177,262           
  modelsConfig.ts  |   89.36 |    86.93 |   88.09 |   89.36 | ...1407,1436-1437 
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/output        |     100 |      100 |     100 |     100 |                   
  ...-formatter.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/permissions   |   83.79 |    91.18 |   71.07 |   83.79 |                   
  autoMode.ts      |   97.66 |    93.13 |     100 |   97.66 | ...82-589,635,712 
  ...transcript.ts |      98 |       84 |     100 |      98 | 200-201           
  classifier.ts    |      94 |    94.54 |     100 |      94 | 158-165,389-393   
  ...erousRules.ts |     100 |    89.36 |     100 |     100 | 110,133,147,175   
  ...alTracking.ts |     100 |      100 |     100 |     100 |                   
  ...e-commands.ts |   86.77 |     73.8 |     100 |   86.77 | 131-141,210-214   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...on-manager.ts |   86.63 |    89.01 |      80 |   86.63 | ...1111,1217-1221 
  rule-parser.ts   |   94.49 |    92.74 |     100 |   94.49 | ...1447,1481-1483 
  ...-semantics.ts |   70.44 |    91.07 |   46.66 |   70.44 | ...2237,2311-2314 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...sifier-prompts |   99.04 |    95.23 |     100 |   99.04 |                   
  system-prompt.ts |   99.04 |    95.23 |     100 |   99.04 | 220               
 src/prompts       |   83.63 |      100 |    87.5 |   83.63 |                   
  mcp-prompts.ts   |   18.18 |      100 |       0 |   18.18 | 11-19             
  ...t-registry.ts |     100 |      100 |     100 |     100 |                   
 src/providers     |   83.78 |    78.34 |   81.25 |   83.78 |                   
  all-providers.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  install.ts       |   93.11 |     84.5 |     100 |   93.11 | ...56-257,330-331 
  ...der-config.ts |   75.91 |    73.48 |   78.26 |   75.91 | ...74-475,503-504 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...viders/presets |   98.04 |    91.66 |   63.63 |   98.04 |                   
  ...oding-plan.ts |   87.34 |      100 |       0 |   87.34 | 81-83,86-88,90-93 
  ...a-standard.ts |     100 |      100 |     100 |     100 |                   
  ...token-plan.ts |     100 |      100 |     100 |     100 |                   
  ...m-provider.ts |   97.05 |    81.25 |      75 |   97.05 | 118-119           
  deepseek.ts      |     100 |      100 |     100 |     100 |                   
  grok.ts          |     100 |      100 |     100 |     100 |                   
  idealab.ts       |     100 |      100 |     100 |     100 |                   
  minimax.ts       |     100 |      100 |     100 |     100 |                   
  modelscope.ts    |     100 |      100 |     100 |     100 |                   
  moonshot.ts      |     100 |      100 |     100 |     100 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  requesty.ts      |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/qwen          |   85.36 |    78.59 |   95.94 |   85.36 |                   
  ...tGenerator.ts |    98.6 |    98.14 |     100 |    98.6 | 103-104           
  qwenOAuth2.ts    |   82.79 |    73.45 |    90.9 |   82.79 | ...1205-1221,1251 
  ...kenManager.ts |   85.36 |     76.8 |     100 |   85.36 | ...52-757,778-783 
 src/resources     |     100 |      100 |     100 |     100 |                   
  ...e-registry.ts |     100 |      100 |     100 |     100 |                   
 src/services      |   90.61 |    86.34 |   96.78 |   90.61 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |   98.48 |    87.28 |     100 |   98.48 | 81-82,105,474-475 
  branch-points.ts |     100 |    95.23 |     100 |     100 | ...20,211,224,327 
  ...ionService.ts |   97.72 |    96.53 |     100 |   97.72 | ...1081,1224-1232 
  ...ingService.ts |   92.43 |    87.77 |   94.73 |   92.43 | ...2843,2858-2859 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  ...utSlimming.ts |    97.2 |    94.23 |     100 |    97.2 | ...39-340,378-381 
  cronScheduler.ts |   94.17 |    90.45 |      98 |   94.17 | ...1333,1736-1737 
  cronTasksFile.ts |   96.34 |    91.96 |     100 |   96.34 | ...11,336-337,483 
  cronTasksLock.ts |   94.44 |    89.47 |     100 |   94.44 | ...02-103,132-133 
  ...eryService.ts |   96.22 |    93.54 |      90 |   96.22 | 121,155-156,161   
  ...oryService.ts |   88.17 |    79.02 |    92.3 |   88.17 | ...1303,1344-1347 
  fileReadCache.ts |    97.5 |    96.07 |     100 |    97.5 | 349-350,363-364   
  ...temService.ts |    92.8 |    84.68 |   94.11 |    92.8 | ...53,479-486,531 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  ...reeService.ts |   74.75 |    70.76 |   96.07 |   74.75 | ...2296,2325-2326 
  ...on-service.ts |   86.58 |    74.39 |     100 |   86.58 | ...56-460,498-499 
  ...references.ts |   98.57 |    91.42 |     100 |   98.57 | 156-157,217-218   
  ...ionService.ts |   98.26 |    97.23 |     100 |   98.26 | ...65-866,889-890 
  ...ticsDumper.ts |   98.37 |    95.23 |     100 |   98.37 | 185-186           
  ...ureMonitor.ts |   95.82 |    90.52 |   97.05 |   95.82 | ...60,861,875-877 
  ...orRegistry.ts |   97.22 |    90.99 |     100 |   97.22 | ...55-456,609-610 
  ...ttachments.ts |   97.74 |     90.9 |     100 |   97.74 | 298-308,646       
  ...pi-history.ts |   98.94 |    88.88 |     100 |   98.94 | 43                
  ...ersistence.ts |   91.67 |    80.64 |     100 |   91.67 | ...1062-1063,1091 
  ...tory-state.ts |     100 |       95 |     100 |     100 | 31                
  ...on-service.ts |   94.49 |     92.3 |   97.22 |   94.49 | ...98-600,656-664 
  ...pr-service.ts |   96.22 |    89.13 |     100 |   96.22 | 90-93             
  ...ce-service.ts |    98.5 |    94.11 |    90.9 |    98.5 | 64-65             
  ...n-registry.ts |   98.73 |    96.29 |     100 |   98.73 | 584,638-639,692   
  ...ken-counts.ts |     100 |       96 |     100 |     100 | 58                
  ...ipt-reader.ts |    93.7 |    91.22 |    97.8 |    93.7 | ...2791-2792,2869 
  ...turn-state.ts |   94.11 |     90.9 |   91.66 |   94.11 | 108-112,129-130   
  ...est-helper.ts |       0 |        0 |       0 |       0 | 1-65              
  ...iter-lease.ts |   83.14 |    74.47 |   97.61 |   83.14 | ...2433,2445-2448 
  sessionRecap.ts  |   67.56 |    43.47 |     100 |   67.56 | ...60,178,180-183 
  ...ionService.ts |   89.61 |    87.01 |    93.4 |   89.61 | ...3013,3027-3047 
  sessionTitle.ts  |   96.35 |    79.71 |     100 |   96.35 | ...08-311,342-343 
  ...ContextEnv.ts |     100 |    94.73 |     100 |     100 | 76,111            
  ...ionService.ts |   84.43 |    78.45 |   97.18 |   84.43 | ...2496,2502-2507 
  ...pInhibitor.ts |   97.42 |    92.77 |     100 |   97.42 | ...30,169,369-370 
  ...e-encoding.ts |   85.96 |    76.47 |     100 |   85.96 | 58-61,64-65,78-79 
  ...Estimation.ts |     100 |    94.11 |     100 |     100 | 118               
  ...ageService.ts |   97.76 |    91.59 |   93.75 |   97.76 | ...61-262,366,567 
  ...ite-origin.ts |     100 |    93.33 |     100 |     100 | 32                
  ...UseSummary.ts |   94.63 |    88.46 |     100 |   94.63 | ...62-164,214-215 
  ...rd-service.ts |     100 |    88.37 |     100 |     100 | ...29,145-146,241 
  ...oryService.ts |   90.76 |    84.07 |     100 |   90.76 | ...10-513,565-566 
  ...reeCleanup.ts |   14.42 |      100 |   33.33 |   14.42 | 58-186            
  ...ionService.ts |   88.36 |     87.8 |     100 |   88.36 | ...48-449,465-466 
 ...icrocompaction |   98.91 |    95.08 |     100 |   98.91 |                   
  microcompact.ts  |   98.91 |    95.08 |     100 |   98.91 | ...60,769,778-779 
 ...s/visionBridge |    98.8 |    92.12 |     100 |    98.8 |                   
  ...capability.ts |     100 |      100 |     100 |     100 |                   
  ...part-utils.ts |     100 |      100 |     100 |     100 |                   
  ...ion-bridge.ts |   98.72 |    82.35 |     100 |   98.72 | 65,71             
  ...ge-service.ts |   98.61 |     94.7 |     100 |   98.61 | ...06,666,679-680 
 src/skills        |   89.77 |    86.05 |   94.73 |   89.77 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...activation.ts |     100 |    93.33 |     100 |     100 | 93,112            
  skill-curator.ts |   89.71 |    81.54 |     100 |   89.71 | ...01-902,904-907 
  skill-load.ts    |   94.84 |    87.69 |     100 |   94.84 | ...03,223,235-237 
  skill-manager.ts |   86.09 |    85.64 |   86.11 |   86.09 | ...1243,1250-1254 
  skill-paths.ts   |   90.42 |     87.5 |     100 |   90.42 | ...19-120,125-126 
  symlinkScope.ts  |     100 |      100 |     100 |     100 |                   
  types.ts         |   97.91 |    98.07 |     100 |   97.91 | 277-278           
 ...ataviz/scripts |   80.06 |    95.23 |   88.23 |   80.06 |                   
  ...te_palette.js |   80.06 |    95.23 |   88.23 |   80.06 | 261-296,306-328   
 ...s/bundled/loop |   97.48 |    95.77 |     100 |   97.48 |                   
  ...omous-loop.ts |     100 |      100 |     100 |     100 |                   
  ...-task-file.ts |   94.85 |     92.4 |     100 |   94.85 | ...56,367,375-376 
  ...k-resolver.ts |     100 |      100 |     100 |     100 |                   
 src/subagents     |   88.56 |    89.42 |    98.3 |   88.56 |                   
  ...ter-schema.ts |     100 |    98.07 |     100 |     100 | 99                
  ...tin-agents.ts |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nt-manager.ts |   85.51 |    86.52 |   97.43 |   85.51 | ...1583,1660-1661 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 47-52,63-68,71-76 
 src/telemetry     |   82.55 |    84.83 |   85.71 |   82.55 |                   
  ...ty-tracker.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...on-metrics.ts |   99.08 |    80.95 |     100 |   99.08 | 185,199           
  ...on-tracing.ts |   80.71 |    81.91 |   79.16 |   80.71 | ...92,499-501,517 
  ...attributes.ts |   96.98 |    91.37 |     100 |   96.98 | ...47-348,366-367 
  ...ag-metrics.ts |     100 |    77.77 |     100 |     100 | 21,40             
  ...t-loop-lag.ts |   96.85 |    85.71 |     100 |   96.85 | 170-173           
  ...-exporters.ts |   65.38 |    83.33 |      50 |   65.38 | ...08-109,112-113 
  ...ai-content.ts |    74.5 |    66.41 |   91.66 |    74.5 | ...1480,1493-1502 
  ...i-provider.ts |     100 |    99.02 |     100 |     100 | 106               
  ...ai-request.ts |   87.52 |    92.79 |   83.78 |   87.52 | ...55-561,564-570 
  gen-ai-usage.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-111             
  ...-processor.ts |   99.12 |    96.03 |      95 |   99.12 | 150,379-380       
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |   60.73 |    78.01 |   66.66 |   60.73 | ...1507,1524-1544 
  metrics.ts       |   80.37 |    82.35 |   80.95 |   80.37 | ...1150,1153-1164 
  otlp-urls.ts     |     100 |      100 |     100 |     100 |                   
  ...attributes.ts |     100 |      100 |     100 |     100 |                   
  ...ime-config.ts |       0 |        0 |       0 |       0 | 1                 
  sanitize.ts      |      80 |    83.33 |     100 |      80 | 35-36,41-42       
  ...rters-grpc.ts |     100 |      100 |     100 |     100 |                   
  ...rters-http.ts |     100 |      100 |     100 |     100 |                   
  sdk-impl.ts      |   93.95 |    86.44 |      75 |   93.95 | ...41,483-484,500 
  sdk.ts           |    82.7 |     90.9 |   66.66 |    82.7 | ...00-204,242-264 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...ion-events.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |   91.17 |    88.72 |    97.5 |   91.17 | ...1920,1949-1952 
  ...etry-utils.ts |     100 |      100 |     100 |     100 |                   
  ...l-decision.ts |     100 |      100 |     100 |     100 |                   
  trace-context.ts |     100 |      100 |     100 |     100 |                   
  ...e-id-utils.ts |     100 |      100 |     100 |     100 |                   
  tracer.ts        |   98.56 |    88.63 |     100 |   98.56 | 52,101            
  types.ts         |   83.26 |    95.68 |   86.36 |   83.26 | ...1467,1471-1478 
  uiTelemetry.ts   |   97.18 |    93.93 |      88 |   97.18 | ...70,314,461-462 
 ...ry/qwen-logger |   74.23 |     80.7 |      70 |   74.23 |                   
  event-types.ts   |       0 |        0 |       0 |       0 |                   
  qwen-logger.ts   |   74.23 |    80.53 |   69.49 |   74.23 | ...1122,1160-1161 
 src/test-utils    |   96.38 |    98.64 |   84.09 |   96.38 |                   
  config.ts        |     100 |      100 |     100 |     100 |                   
  ...st-helpers.ts |   94.11 |       90 |     100 |   94.11 | 69-70             
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...mised-lock.ts |     100 |      100 |     100 |     100 |                   
  mock-tool.ts     |   94.85 |      100 |      80 |   94.85 | ...53,227-228,241 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   87.25 |    85.68 |   90.06 |   87.25 |                   
  ...erQuestion.ts |   89.71 |    81.13 |    92.3 |   89.71 | ...66-367,374-375 
  ...-registrar.ts |    77.7 |    66.66 |   66.66 |    77.7 | ...72-277,292-294 
  ...ub-session.ts |   89.72 |    91.48 |   83.33 |   89.72 | ...06-307,318-325 
  cron-create.ts   |   90.64 |     93.1 |      75 |   90.64 | ...,73-74,223-231 
  cron-delete.ts   |   97.56 |      100 |   85.71 |   97.56 | 31-32             
  cron-list.ts     |   98.23 |    95.45 |   88.88 |   98.23 | 57-58             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  display-image.ts |   87.42 |    85.71 |    90.9 |   87.42 | ...29-134,194-195 
  edit.ts          |   82.76 |    86.88 |   82.35 |   82.76 | ...45-746,865-915 
  ...r-worktree.ts |   83.14 |    68.42 |   88.88 |   83.14 | ...84-187,278-279 
  enterPlanMode.ts |      85 |       84 |      90 |      85 | ...28-133,161-175 
  exit-worktree.ts |   83.29 |     83.8 |   94.73 |   83.29 | ...14-515,537-538 
  exitPlanMode.ts  |      95 |    85.29 |     100 |      95 | ...21-325,344,378 
  ...permission.ts |     100 |      100 |     100 |     100 |                   
  glob.ts          |   96.33 |     88.5 |     100 |   96.33 | ...24-225,373,376 
  grep.ts          |   90.73 |    86.71 |   86.36 |   90.73 | ...76-677,727-728 
  ...adTracking.ts |     100 |      100 |     100 |     100 |                   
  image-gen.ts     |   91.66 |    78.12 |   91.66 |   91.66 | ...13-214,221-222 
  list-agents.ts   |   94.11 |    83.33 |   85.71 |   94.11 | 31-32,47-48       
  loop-wakeup.ts   |   99.27 |     93.1 |     100 |   99.27 | 45                
  ls.ts            |   96.74 |    90.54 |     100 |   96.74 | 176-181,212,216   
  lsp.ts           |   72.71 |     59.9 |    90.9 |   72.71 | ...1212,1214-1215 
  ...nt-manager.ts |   82.06 |    80.15 |   85.71 |   82.06 | ...3234,3236-3237 
  mcp-client.ts    |   86.08 |     87.5 |   93.93 |   86.08 | ...2483,2487-2490 
  ...ry-timeout.ts |     100 |      100 |     100 |     100 |                   
  mcp-errors.ts    |     100 |      100 |     100 |     100 |                   
  ...pool-entry.ts |   79.21 |    85.71 |   81.57 |   79.21 | ...1341,1349-1350 
  ...ool-events.ts |       8 |        0 |       0 |       8 | 132-158           
  mcp-pool-key.ts  |    97.5 |    93.93 |     100 |    97.5 | 178-179           
  ...ce-content.ts |   96.55 |    91.17 |     100 |   96.55 | 80-82             
  mcp-retry.ts     |   97.67 |    95.65 |     100 |   97.67 | 131-132           
  ...ion-config.ts |     100 |      100 |     100 |     100 |                   
  mcp-status.ts    |     100 |      100 |     100 |     100 |                   
  mcp-tool.ts      |   97.95 |    92.37 |     100 |   97.95 | ...1161,1216-1217 
  ...sport-pool.ts |   83.98 |     80.3 |   88.46 |   83.98 | ...1409,1416-1420 
  ...ace-budget.ts |   87.27 |     82.6 |     100 |   87.27 | ...00-305,340-345 
  memory-config.ts |     100 |      100 |     100 |     100 |                   
  ...iable-tool.ts |     100 |    84.61 |     100 |     100 | 101,108           
  monitor.ts       |   91.82 |    83.09 |   88.46 |   91.82 | ...99,612,810-815 
  notebook-edit.ts |   85.71 |    77.39 |   82.35 |   85.71 | ...96-912,958-959 
  ...escendants.ts |   36.17 |    64.51 |   55.55 |   36.17 | ...46-310,385-390 
  ...nforcement.ts |   83.21 |    90.69 |     100 |   83.21 | 147-158,207-220   
  read-file.ts     |   95.49 |    88.61 |    87.5 |   95.49 | ...49,464,536-537 
  ...p-resource.ts |   96.85 |      100 |   91.66 |   96.85 | 92-96             
  readManyFiles.ts |   95.79 |    81.35 |     100 |   95.79 | ...10,563,573-577 
  ...d-artifact.ts |   85.68 |    81.59 |   94.73 |   85.68 | ...1071,1095-1096 
  ...t-shutdown.ts |    87.2 |    86.66 |   77.77 |    87.2 | ...,75-79,162-165 
  ripGrep.ts       |    94.6 |    87.34 |   95.45 |    94.6 | ...33-734,740-741 
  ...-transport.ts |   71.42 |    55.55 |   71.42 |   71.42 | ...36-137,143-144 
  send-message.ts  |      80 |    89.74 |   66.66 |      80 | ...59-265,333-340 
  ...n-mcp-view.ts |   94.07 |    91.89 |    90.9 |   94.07 | 131-139           
  shell.ts         |   78.96 |    84.29 |      93 |   78.96 | ...5036,5111-5112 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   91.39 |    92.55 |      90 |   91.39 | ...84,488,534-556 
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-create.ts   |    94.4 |    93.75 |   83.33 |    94.4 | 45-49,63-64,95    
  task-list.ts     |   80.43 |    86.95 |   85.71 |   80.43 | ...67,121,125-132 
  task-stop.ts     |   93.14 |    96.29 |    87.5 |   93.14 | 39-40,54-64       
  task-update.ts   |   82.87 |     86.5 |   92.85 |   82.87 | ...54-564,588-599 
  team-create.ts   |   97.24 |    86.36 |   85.71 |   97.24 | 48-49,129-130     
  team-delete.ts   |   86.74 |    84.61 |   85.71 |   86.74 | 37-38,42-48,72-73 
  ...n-approval.ts |   92.14 |    96.96 |   81.81 |   92.14 | 38-39,42-43,93-99 
  todoWrite.ts     |   95.13 |    87.85 |   93.33 |   95.13 | ...23-527,540-545 
  ...repeat-key.ts |     100 |      100 |     100 |     100 |                   
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   80.35 |    81.52 |   85.71 |   80.35 | ...1017,1025-1026 
  ...-finalizer.ts |    98.1 |     92.3 |   93.33 |    98.1 | ...34-235,237-241 
  ...iagnostics.ts |   99.06 |    97.69 |   91.66 |   99.06 | 133-134,205       
  ...-retention.ts |     100 |    95.83 |     100 |     100 | 116               
  tool-search.ts   |   96.19 |    89.79 |   93.75 |   96.19 | ...09,259-264,426 
  tool-utils.ts    |   97.46 |    96.55 |     100 |   97.46 | 26-27             
  tools.ts         |   92.93 |    92.18 |      92 |   92.93 | ...64-565,581-587 
  truncation.ts    |   90.61 |    90.35 |     100 |   90.61 | ...53-461,498-504 
  ...reapproved.ts |   99.27 |    94.11 |     100 |   99.27 | 170               
  web-fetch.ts     |   96.05 |    90.54 |   96.77 |   96.05 | ...85-786,800-801 
  web-search.ts    |   90.58 |    83.57 |      80 |   90.58 | ...1025,1083-1086 
  write-file.ts    |   87.06 |    85.71 |   89.47 |   87.06 | ...29-832,869-904 
  zoom-image.ts    |   95.76 |    93.93 |    90.9 |   95.76 | 54-59,203-204     
 src/tools/agent   |   87.49 |    88.65 |   89.56 |   87.49 |                   
  agent.ts         |   86.18 |    87.83 |   87.36 |   86.18 | ...4385,4419-4429 
  fork-profile.ts  |   93.65 |       90 |     100 |   93.65 | ...33-134,171-174 
  fork-subagent.ts |   98.73 |       95 |     100 |   98.73 | 101-102,173       
 ...tools/artifact |   95.78 |    92.51 |   88.63 |   95.78 |                   
  artifact-tool.ts |   91.46 |    88.46 |   71.42 |   91.46 | ...13-314,322-325 
  ...-publisher.ts |     100 |    85.71 |     100 |     100 | 32                
  ...-publisher.ts |   96.74 |    97.72 |    87.5 |   96.74 | 29-30,156-157     
  html.ts          |     100 |    96.77 |     100 |     100 | 122               
  ...-publisher.ts |     100 |       80 |     100 |     100 | 30                
  oss-publisher.ts |    98.1 |    91.48 |     100 |    98.1 | 43-45             
  publisher.ts     |     100 |      100 |     100 |     100 |                   
 ...tools/workflow |   88.35 |    86.77 |   81.48 |   88.35 |                   
  workflow.ts      |   88.35 |    86.77 |   81.48 |   88.35 | ...35,780,782-783 
 src/utils         |   92.76 |    89.78 |    96.8 |   92.76 |                   
  LruCache.ts      |     100 |      100 |     100 |     100 |                   
  ...Controller.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |      95 |     92.7 |     100 |      95 | ...49-550,657-661 
  auth-type.ts     |     100 |      100 |     100 |     100 |                   
  bareMode.ts      |   81.81 |      100 |      50 |   81.81 | 18-19             
  ...ry-content.ts |   98.45 |    95.79 |     100 |   98.45 | 132-133,159-160   
  browser.ts       |   86.84 |    78.94 |     100 |   86.84 | 34,36-37,65-66    
  btwUtils.ts      |   13.95 |      100 |       0 |   13.95 | 17-31,34-55       
  bundlePaths.ts   |     100 |      100 |     100 |     100 |                   
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...igResolver.ts |     100 |      100 |     100 |     100 |                   
  ...engthError.ts |   91.06 |    89.47 |     100 |   91.06 | ...46-147,154-155 
  ...n-branches.ts |   95.89 |    94.11 |      95 |   95.89 | ...99-500,512-525 
  ...tion-chain.ts |     100 |      100 |     100 |     100 |                   
  cronDisplay.ts   |     100 |    97.61 |     100 |     100 | 46                
  cronParser.ts    |   95.34 |    93.33 |     100 |   95.34 | 41-42,47-48,70-71 
  debugLogger.ts   |     100 |    97.14 |      95 |     100 | 79,86             
  ...qwen-model.ts |     100 |      100 |     100 |     100 |                   
  editHelper.ts    |   93.63 |     83.9 |     100 |   93.63 | ...27-428,462-463 
  editor.ts        |   97.65 |    95.45 |     100 |   97.65 | ...35-336,338-339 
  encoding.ts      |     100 |      100 |     100 |     100 |                   
  env.ts           |     100 |      100 |     100 |     100 |                   
  ...arResolver.ts |   94.28 |    88.88 |     100 |   94.28 | 28-29,125-126     
  errorParsing.ts  |     100 |      100 |     100 |     100 |                   
  ...rReporting.ts |   95.65 |    93.33 |     100 |   95.65 | 37-38             
  errors.ts        |   88.92 |    93.08 |      68 |   88.92 | ...92,394,410-411 
  fetch.ts         |   90.68 |    82.63 |     100 |   90.68 | ...72,483-484,503 
  ...ng-options.ts |     100 |      100 |     100 |     100 |                   
  file-identity.ts |     100 |      100 |     100 |     100 |                   
  fileUtils.ts     |   94.79 |    92.16 |   96.29 |   94.79 | ...2076,2084-2085 
  formatters.ts    |     100 |      100 |     100 |     100 |                   
  ...eUtilities.ts |    92.4 |    86.95 |     100 |    92.4 | ...52-158,168-169 
  ...rStructure.ts |   94.39 |    94.28 |     100 |   94.39 | ...29-132,343-348 
  getPty.ts        |   31.57 |       50 |     100 |   31.57 | 26-38             
  git-branches.ts  |    91.6 |    84.21 |    92.3 |    91.6 | ...90,405-410,570 
  ...fig-safety.ts |   97.01 |       80 |     100 |   97.01 | 53-54             
  git-ignore.ts    |     100 |      100 |     100 |     100 |                   
  gitDiff.ts       |   95.19 |    81.36 |     100 |   95.19 | ...1073,1419-1420 
  gitDirect.ts     |   98.84 |    94.28 |     100 |   98.84 | 234,318           
  ...noreParser.ts |   94.48 |    93.22 |     100 |   94.48 | ...23-124,158-159 
  gitUtils.ts      |   78.83 |    82.35 |    87.5 |   78.83 | ...22-123,164-215 
  github-prs.ts    |   95.74 |    82.27 |     100 |   95.74 | 216,314-322       
  iconvHelper.ts   |     100 |      100 |     100 |     100 |                   
  ...rePatterns.ts |     100 |      100 |     100 |     100 |                   
  image-view.ts    |   95.08 |    93.47 |     100 |   95.08 | ...62-166,234-238 
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  is-tool.ts       |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |   96.15 |    93.63 |     100 |   96.15 | ...86-387,429-432 
  ...-detection.ts |     100 |      100 |     100 |     100 |                   
  ...iconv-lite.ts |     100 |      100 |     100 |     100 |                   
  ...simple-git.ts |   96.77 |    91.66 |     100 |   96.77 | 38                
  ...m-headless.ts |      96 |    88.88 |     100 |      96 | 34                
  ...-constants.ts |   94.28 |     92.3 |     100 |   94.28 | 66-67             
  ...iagnostics.ts |    96.4 |     94.2 |     100 |    96.4 | ...66,293-294,376 
  ...tProcessor.ts |   94.01 |    89.88 |     100 |   94.01 | ...47-353,445-446 
  ...Inspectors.ts |     100 |      100 |     100 |     100 |                   
  modelId.ts       |   98.96 |    98.18 |     100 |   98.96 | 154               
  ...kerChecker.ts |    90.9 |    91.66 |     100 |    90.9 | 73-79             
  notebook.ts      |   94.57 |    89.91 |   95.83 |   94.57 | ...21,333,385-387 
  openaiLogger.ts  |   91.66 |    89.74 |     100 |   91.66 | ...26-228,251-256 
  osc8.ts          |   54.26 |    64.86 |   83.33 |   54.26 | ...72-195,197-257 
  partUtils.ts     |     100 |    98.64 |     100 |     100 | 211               
  pathReader.ts    |     100 |      100 |     100 |     100 |                   
  paths.ts         |   90.88 |    90.66 |     100 |   90.88 | ...28-629,631-633 
  pdf.ts           |   92.17 |    85.81 |     100 |   92.17 | ...64-565,606-611 
  ...s-liveness.ts |     100 |    93.47 |     100 |     100 | 62,72,108         
  projectPath.ts   |     100 |      100 |     100 |     100 |                   
  projectRoot.ts   |   71.73 |    78.57 |     100 |   71.73 | 54-66             
  ...ectSummary.ts |   89.62 |    72.41 |     100 |   89.62 | ...40-145,196-199 
  ...tIdContext.ts |     100 |      100 |     100 |     100 |                   
  proxyUtils.ts    |     100 |      100 |     100 |     100 |                   
  ...rDetection.ts |   71.15 |       86 |     100 |   71.15 | ...-90,96-101,147 
  ...noreParser.ts |   92.63 |    91.66 |     100 |   92.63 | ...77-178,197-198 
  rateLimit.ts     |   93.75 |    89.62 |     100 |   93.75 | ...13,218-219,262 
  ...text-range.ts |   96.98 |    87.36 |     100 |   96.98 | ...87-688,763-764 
  retry.ts         |   96.09 |    92.52 |     100 |   96.09 | ...72,563-564,582 
  retryContext.ts  |     100 |      100 |     100 |     100 |                   
  ...sification.ts |   97.63 |    97.08 |     100 |   97.63 | ...17,251-252,278 
  retryPolicy.ts   |   97.72 |    90.56 |     100 |   97.72 | 130-131           
  ripgrepUtils.ts  |   90.04 |    93.43 |   95.45 |   90.04 | ...55-565,598-599 
  ...iagnostics.ts |   83.08 |     67.5 |   92.59 |   83.08 | ...23,543-544,550 
  ...tchOptions.ts |   84.87 |    86.71 |   96.29 |   84.87 | ...71,696,725-734 
  ...odelPrefix.ts |     100 |      100 |     100 |     100 |                   
  runtimeStatus.ts |   97.77 |    91.48 |     100 |   97.77 | 172-173           
  safe-mode.ts     |     100 |      100 |     100 |     100 |                   
  safeJsonParse.ts |     100 |      100 |     100 |     100 |                   
  ...nStringify.ts |     100 |      100 |     100 |     100 |                   
  ...-child-env.ts |     100 |      100 |     100 |     100 |                   
  ...aConverter.ts |   98.22 |    98.01 |     100 |   98.22 | 100,102-103       
  ...aValidator.ts |   92.09 |    83.65 |   90.47 |   92.09 | ...60,882-883,896 
  ...r-launcher.ts |   96.35 |    93.97 |   85.71 |   96.35 | ...35-336,347-348 
  sedEditParser.ts |   91.78 |    92.18 |     100 |   91.78 | ...66-569,645-646 
  ...nIdContext.ts |     100 |       90 |     100 |     100 | 95                
  ...orageUtils.ts |   96.21 |    85.47 |     100 |   96.21 | ...70,386,466,485 
  ...-pager-env.ts |     100 |      100 |     100 |     100 |                   
  ...fety-rules.ts |     100 |     89.7 |     100 |     100 | ...01,304,309-311 
  shell-utils.ts   |   86.37 |    88.59 |     100 |   86.37 | ...2361,2368-2372 
  ...lAstParser.ts |    98.3 |    91.59 |     100 |    98.3 | ...1340-1342,1352 
  ...nlyChecker.ts |   96.33 |    96.57 |     100 |   96.33 | ...83-284,292-293 
  sideQuery.ts     |   86.82 |    86.66 |     100 |   86.82 | ...79-185,187-193 
  ...pEventSink.ts |     100 |       80 |     100 |     100 | 61                
  ...tGenerator.ts |     100 |      100 |     100 |     100 |                   
  ...ameContext.ts |     100 |      100 |     100 |     100 |                   
  symlink.ts       |   77.77 |    57.14 |     100 |   77.77 | 44,54-59          
  ...emEncoding.ts |   96.36 |    91.17 |     100 |   96.36 | 59-60,124-125     
  terminal-env.ts  |      50 |      100 |       0 |      50 | 18-19             
  terminalSafe.ts  |     100 |      100 |     100 |     100 |                   
  ...Serializer.ts |   98.72 |       90 |     100 |   98.72 | 42-43,134,201-203 
  testUtils.ts     |   53.33 |      100 |   33.33 |   53.33 | ...53,59-64,70-72 
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  textUtils.ts     |      65 |      100 |      75 |      65 | 56-75             
  thoughtUtils.ts  |     100 |    95.65 |     100 |     100 | 99                
  ...-converter.ts |   95.23 |    85.71 |     100 |   95.23 | 36-37             
  ...error-type.ts |     100 |      100 |     100 |     100 |                   
  ...name-utils.ts |     100 |      100 |     100 |     100 |                   
  ...ultCleanup.ts |   54.62 |    60.86 |      75 |   54.62 | ...03-105,108-134 
  ...Compaction.ts |   96.34 |    96.55 |     100 |   96.34 | ...35-340,342-347 
  ...pt-records.ts |   87.61 |    86.23 |     100 |   87.61 | ...80-484,514-529 
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...-directory.ts |    83.7 |    80.95 |    87.5 |    83.7 | ...37-238,252-253 
  ...ifact-path.ts |   94.11 |    92.85 |     100 |   94.11 | 32-33             
  ...aceContext.ts |   95.39 |    89.47 |     100 |   95.39 | ...16-317,321-322 
  xml.ts           |    97.8 |    87.69 |     100 |    97.8 | 98-99             
  yaml-parser.ts   |   83.87 |    77.27 |     100 |   83.87 | ...31-234,239-240 
 ...ils/filesearch |   83.94 |    80.75 |   94.78 |   83.94 |                   
  crawlCache.ts    |     100 |      100 |     100 |     100 |                   
  crawler.ts       |    82.9 |    76.81 |   95.08 |    82.9 | ...1563,1597-1598 
  fileSearch.ts    |   93.78 |    87.67 |     100 |   93.78 | ...71-272,274-275 
  fzfWorker.ts     |       0 |        0 |       0 |       0 | 1-109             
  ...rkerHandle.ts |   84.05 |    75.86 |      90 |   84.05 | ...30-334,340-341 
  ignore.ts        |     100 |    97.36 |     100 |     100 | 187               
  result-cache.ts  |     100 |    93.75 |     100 |     100 | 49                
 ...uest-tokenizer |    92.3 |      100 |   88.88 |    92.3 |                   
  ...ageFormats.ts |   81.81 |      100 |   66.66 |   81.81 | 56-61             
  textTokenizer.ts |     100 |      100 |     100 |     100 |                   
-------------------|---------|----------|---------|---------|-------------------

For detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Plan (not a blocker): 324 passed — this review observed 22989, 495 passed.

中文说明

Test Plan(非阻断):324 passed — this review observed 22989, 495 passed

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +337 to +338
const verdict = sandboxVerdict();
if (verdict.kind !== 'container') return null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The required policy fails open: the refusal this diff promises has no enforcement point anywhere, and the reviewed code runs unsandboxed with the full environment in four demonstrated shapes. Both call sites test only verdict.kind !== 'container' and return null, falling through to the direct spawn with buildRunEnv() — the entire process.env. sandboxVerdict() produces a refused verdict when required meets no answering runtime, but nothing consumes it; the doc comment delegates refusal to "the caller's decision", and no caller ever decides. Three further paths fail open even once that is fixed: the nesting guard returns direct when SANDBOX is set BEFORE policy is consulted — so required inside qwen --sandbox runs with the full inherited environment (the seatbelt spawn inherits the entire process.env), and the PR's own test pins this bypass; a cwd outside .qwen/tmp — the documented local-checkout /review mode — hits at < 0 and returns null even though the verdict IS container; and the npm-toolchain hand-off (yarn/pnpm/bun repos, no lockfile — "the common case" per its own comment) tells the agent to run the PR's install/build/test itself (agent-briefs.ts:540 wires exactly that), never consulting the policy even when a runtime answers. Measured: with QWEN_REVIEW_SANDBOX=required and no runtime answering, a probe drove the real run() — the verdict was {"kind":"refused"}, yet the command executed and a probe secret placed in the review process's env appeared in the PR command's output; a one-line refusal-consumption patch flips the probe to a thrown refusal. Local-checkout arm: real run() with a stub docker and an outside cwd → exit 0, output SECRET-IS:hunter2-credential, docker log shows only docker info. Hand-off arm: yarn.lock fixture under required vs off → reports byte-identical, zero contained executions; a policy-consulting patch flips it. This is the unresolved point of the existing CHANGES_REQUESTED review, and it still stands at this head. Either thread the verdict through to the callers so required produces an evidence-unavailable result instead of spawning/instructing (all four paths), or hold the required option out of the schema until that enforcement exists.

中文说明

required 策略失守时不吭声:本 diff 承诺的拒绝在代码里没有任何执行点,四种已实测的形态下被审代码都会带着完整环境在沙箱外运行。两个调用点都只判 verdict.kind !== 'container' 就返回 null,落入带 buildRunEnv()(整个 process.env)的直接 spawn。required 且无运行时时 sandboxVerdict() 确实产出 refused 判定,但没有任何代码消费它;注释把拒绝推给"调用者的决定",而没有任何调用者做这个决定。即使修掉这一条,还有三条路径照样失守:嵌套守卫在 SANDBOX 被设置时先于策略返回 direct——在 qwen --sandbox 内部设 required 会带着完整继承环境直跑(seatbelt spawn 继承整个 process.env),本 PR 自己的测试钉住了这个绕过;cwd 不在 .qwen/tmp 之内——即文档化的本地检出 /review 模式——即使判定是 container 也会命中 at < 0 返回 null;npm-toolchain 的交接路径(yarn/pnpm/bun 仓库、无 lockfile——按其注释是"常见情况")指示 agent 自行运行 PR 的 install/build/test(agent-briefs.ts:540 正是这样接线的),全程不查询策略,即使有运行时也照跑。实测:QWEN_REVIEW_SANDBOX=required 且无运行时时,探针驱动真实 run()——判定为 {"kind":"refused"},命令却照样执行,放进审查进程环境的探针密钥出现在 PR 命令输出里;一行消费拒绝的补丁即可翻转为抛错拒绝。本地检出臂:stub docker + 外部 cwd 下真实 run() → exit 0,输出 SECRET-IS:hunter2-credential,docker 日志只有 docker info。交接臂:requiredoff 下 yarn.lock 夹具报告逐字节相同、容器化执行次数为零;查询策略的补丁可翻转。这正是既有 CHANGES_REQUESTED 审查中未解决的点,在本 head 上依旧成立。要么把判定贯通到调用者、让 required 产出"证据不可用"而不是 spawn/指示(四条路径都要),要么在强制落地前先从 schema 里拿掉 required 选项。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +1728 to +1730
const suite = `${shellQuotePath(process.execPath)} ${shellQuotePath(
findVitestBin(dependencyRoot),
)} run --reporter=json ${probes.map(shellQuotePath).join(' ')}`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The sandboxed probe suite bakes the host's Node path into the container command, and that path does not exist inside the container — every sandboxed probe run exits 127, so test-efficacy yields zero evidence exactly when containment is on. containerCommand delivers this string as sh -lc '<suite>', but the only mount is the review temp dir and the sandbox image's Node lives at its own path (/usr/local/bin/node in the node:22-slim lineage); /usr/bin/node (here) or /opt/hostedtoolcache/… (GitHub runners) is neither mounted nor present in the image. build-test's npm commands resolve through the container's PATH and are unaffected — this is the only command that hardcodes the host path. Downstream, exit 127 with empty stdout maps every probe — baseline, control, each mutant, hunk probes, revert — to inconclusive/no-output, blaming the runner's output for a sandbox-wiring error. Measured against a live daemon with the module's exact argv shape: sh -lc '/usr/bin/node --version'sh: 1: /usr/bin/node: not found (exit 127), while the control arm (node via the image PATH) in the same container exits 0 printing v22.23.2. Use the image's own toolchain for the boxed branch — node resolves on the image's PATH:

const suite = `node ${shellQuotePath(findVitestBin(dependencyRoot))} run --reporter=json ${probes.map(shellQuotePath).join(' ')}`;

keeping process.execPath for the direct spawn. (This was point 3 of the earlier triage review — still open at this head.)

中文说明

沙箱化探针套件把宿主机的 Node 路径嵌进容器命令,而该路径在容器内不存在——每个沙箱化探针运行都以 127 退出,恰好在开启 containment 时整个 test-efficacy 阶段零证据。containerCommandsh -lc '<suite>' 传递该字符串,但唯一挂载是 review 临时目录,镜像的 Node 在自己的路径(node:22-slim 谱系的 /usr/local/bin/node);/usr/bin/node(本机)或 /opt/hostedtoolcache/…(GitHub runner)既不在挂载里也不在镜像里。build-test 的 npm 命令经容器 PATH 解析、不受影响——只有这条命令硬编码宿主路径。下游 127 + 空 stdout 会把所有探针(基线、对照、每个突变体、hunk 探针、回退)判为 inconclusive/no-output,把沙箱接线错误归咎于运行器输出。对真实守护进程、按模块的 argv 形状实测:sh -lc '/usr/bin/node --version'sh: 1: /usr/bin/node: not found(exit 127);同一容器内走镜像 PATH 的对照臂正常输出 v22.23.2(exit 0)。容器分支改用镜像自带工具链(node 走镜像 PATH),process.execPath 只留给直接 spawn。(这是先前审查意见的第 3 点,在本 head 上依旧成立。)

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +235 to +236
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The RW mount covers every pipeline tree's .git gitfile — containerized PR code can rewrite a gitfile to redirect later HOST-side git invocations at a planted repository, and obtain host code execution through filter.*. All pipeline trees (review worktree, -probe, -base, -scratch-*) are linked worktrees under <repo>/.qwen/tmp, and each tree's .git gitfile sits inside the tree — i.e. inside this RW mount. Code running as root in the container rewrites <tree>/.git to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge = <payload> and a .gitattributes applying it. Two host-side git consumers then follow the redirect: probe-tree creation (git worktree add --detach <probeTree> <sha> with the review worktree as cwd — neither the gitfile verification nor the localFilterCommands gate that scratch-tree.ts has), and restoreProbeTreeTracked after every sandboxed suite run — its backpointer round-trip PASSES because the attacker writes the very gitdir file the check reads, and its inert -c overrides neutralize core.hooksPath/core.fsmonitor but NOT filter.*, so its checkout --force HEAD -- . materializes planted blobs through the planted smudge filter. The module doc's "<repo>/.git stays outside it" is true of the common dir and false of the security property: the per-tree gitfiles that select which gitdir host git consults stay writable inside the mount. Reproduced end-to-end on git 2.43.0 in throwaway trees mirroring the layout: the check sequence passed (backpointer round-trip read the attacker-planted gitdir), checkout exit 0, HOST-SIDE EXECUTION OBSERVED for both the restore checkout and git worktree add; scratch-tree's --git-common-dir identity gate refused the same tree. Treat the per-tree gitfile as attack surface: after each sandboxed run verify every tree's .git still resolves to its own admin entry under <repo>/.git/worktrees/ (outside the mount) and refuse on mismatch; extend scratch-tree's localFilterCommands refusal and gitfile check to probe-tree creation and restoreProbeTreeTracked.

中文说明

RW 挂载覆盖了每棵流水线树的 .git gitfile——容器内的 PR 代码可以改写 gitfile,把之后的宿主侧 git 重定向到挂载内植入的仓库,并借 filter.* 获得宿主代码执行。所有流水线树(review 工作树、-probe-base-scratch-*)都是 <repo>/.qwen/tmp 下的链接工作树,各自的 .git gitfile 位于树内——即在此 RW 挂载之内。容器内以 root 运行的代码把 <tree>/.git 改写为 gitdir: <同一挂载下植入的目录>,其中带 filter.<x>.smudge = <payload> 与应用它的 .gitattributes。两个宿主侧 git 消费者会跟随重定向:探针树创建(以 review 工作树为 cwd 的 git worktree add --detach <probeTree> <sha>——既无 gitfile 校验、也无 localFilterCommands 关卡,而 scratch-tree.ts 两者都有);以及每次沙箱化套件运行后的 restoreProbeTreeTracked——其回指针往返检查会通过,因为攻击者写下的正是该检查读取的 gitdir 文件;其惰性 -c 覆盖能中和 core.hooksPath/core.fsmonitor,却中和不了 filter.*,于是 checkout --force HEAD -- . 会经植入的 smudge filter 物化植入内容。模块文档的"<repo>/.git 留在挂载外"对 common dir 为真,对安全性质为假:决定宿主 git 查哪个 gitdir 的每棵树的 gitfile 仍在挂载内可写。已在 git 2.43.0 上按该布局的一次性树中端到端复现:检查序列通过(回指针往返读到攻击者植入的 gitdir)、checkout exit 0、restore checkout 与 git worktree add 均观测到宿主侧执行;scratch-tree 的 --git-common-dir 身份关卡对同一棵树会拒绝。把每棵树的 gitfile 当作攻击面:每次沙箱化运行后校验各树 .git 仍解析到 <repo>/.git/worktrees/(挂载外)下自己的 admin entry,不一致即拒绝;把 scratch-tree 的 localFilterCommands 拒绝与 gitfile 检查扩展到探针树创建与 restoreProbeTreeTracked

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +1539 to +1540
const marker = `${sep}${REVIEW_TMP_DIR}${sep}`;
const at = resolved.indexOf(marker);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The mount-root marker search uses first-occurrence indexOf: when the checkout itself sits under a .qwen/tmp, the mount widens to the OUTER temp dir and pulls <repo>/.git and every sibling tree into the container — defeating this module's stated property that <repo>/.git stays outside. A review invoked from inside another review's worktree — this pipeline's own dogfood geometry — is exactly such a nested layout. Executing the verbatim slice arithmetic: for probeTree = /srv/.qwen/tmp/checkouts/myrepo/.qwen/tmp/review-pr-1-probe, indexOf yields tmpDir = /srv/.qwen/tmp (mounting the entire outer checkout family), while lastIndexOf yields /srv/.qwen/tmp/checkouts/myrepo/.qwen/tmp (the intended dir); lastIndexOf returned the intended dir on all three layouts executed (normal, nested, and this machine's actual review path). The flat case computes correctly, which is why the new tests pass while this corner stays open; the identical copy in build-test's containerised has the same shape. Tree names cannot contain separators (scratch labels flatten to [A-Za-z0-9._-]), so the deepest occurrence is always the tree's true parent temp dir, and the at < 0 guard still rejects non-temp-dir trees:

Suggested change
const marker = `${sep}${REVIEW_TMP_DIR}${sep}`;
const at = resolved.indexOf(marker);
const at = resolved.lastIndexOf(marker);
中文说明

挂载根的标记查找用了首次出现的 indexOf:当检出目录本身位于某个 .qwen/tmp 之下时,挂载会扩到外层临时目录,把 <repo>/.git 与所有兄弟树一起拉进容器——与本模块声明的"<repo>/.git 留在挂载外"相悖。从另一个 review 工作树内部发起的审查——本流水线自身的 dogfood 几何——正是这种嵌套布局。按原样执行切片算术:对 probeTree = /srv/.qwen/tmp/checkouts/myrepo/.qwen/tmp/review-pr-1-probeindexOf 得到 tmpDir = /srv/.qwen/tmp(挂进整个外层检出家族),lastIndexOf 得到 /srv/.qwen/tmp/checkouts/myrepo/.qwen/tmp(预期目录);在三种布局(常规、嵌套、本机真实 review 路径)上 lastIndexOf 均返回预期目录。扁平情形计算正确,这正是新测试通过而该角落仍敞开的原因;build-test 的 containerised 里的同款副本同病。树名不可能含分隔符(scratch 标签被压平为 [A-Za-z0-9._-]),因此最深一次出现总是该树真正的父临时目录,at < 0 守卫仍会拒绝非临时目录树。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +235 to +238
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,
'--workdir',
opts.cwd,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] No UID/GID handling: under rootful docker the container runs as uid 0 and writes root-owned files into the host-mounted trees, which the non-root host pipeline then cannot modify or delete. The argv carries no --user, and the default image (repo Dockerfile: FROM node:22-slim, no USER directive) runs as root; containerized npm ci/build/suite then create root-owned node_modules and build output inside the mounted trees, and later host-side steps — the install-timeout rmSync of node_modules, discardWorktree, cleanup sweeps, scratch-tree resets — hit EACCES, leaving residue that accumulates across reviews: the cross-run-state class #9221 closed. utils/sandbox.ts maps host UID/GID (SANDBOX_SET_UID_GID / shouldUseCurrentUserInSandbox) for exactly this hazard on the same image lineage, and #9556's risk section names UID/GID as a condition of this design. Observed live during verification: argv has no --user flag; container uid 0 vs host uid 1000; files created by the container read back uid 0; host-side rmSync(recursive) on the container-created dirs failed EACCES: permission denied — cleanup afterwards needed a root-privileged container to remove the residue. Add '--user', \${process.getuid()}:${process.getgid()}`(guarded onprocess.getuid` existing), mirroring sandbox.ts and its opt-out.

中文说明

没有 UID/GID 处理:rootful docker 下容器以 uid 0 运行,会在宿主挂载的树里写入 root 属主文件,宿主侧非 root 流水线随后既改不了也删不掉。argv 没有 --user,默认镜像(仓库 Dockerfile:FROM node:22-slim,无 USER 指令)以 root 运行;沙箱化的 npm ci/构建/套件于是在挂载树内创建 root 属主的 node_modules 与构建产物,之后的宿主步骤——安装超时时 rmSyncnode_modulesdiscardWorktree、清理扫描、scratch-tree 复位——会撞 EACCES,留下跨审查累积的残留:即 #9221 关闭过的那类跨运行状态。utils/sandbox.ts 对同一镜像谱系正是为此映射宿主 UID/GID(SANDBOX_SET_UID_GID/shouldUseCurrentUserInSandbox),#9556 的风险小节也把 UID/GID 列为本设计的条件之一。验证期间实测:argv 无 --user 标志;容器 uid 0 对宿主 uid 1000;容器创建的文件读回 uid 0;宿主侧对容器所建目录的 rmSync(recursive)EACCES: permission denied——事后清理需要 root 权限容器才能删掉残留。加 '--user', \${process.getuid()}:${process.getgid()}`(以 process.getuid` 存在为守卫),与 sandbox.ts 及其豁免开关保持一致。

— qwen3.8-max via Qwen Code /review (v0.21.15)

const marker = `${sep}${REVIEW_TMP_DIR}${sep}`;
const at = resolved.indexOf(marker);
if (at < 0) return null;
const tmpDir = resolved.slice(0, at + marker.length - 1);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The single RW mount gives every containerized command write access to ALL sibling trees and pipeline artifacts — including the merge-base baseline the A/B comparison trusts. The mount's WIDTH is a documented decision (the farm's links dangle otherwise) — write access to every other tree is a different claim and is never named as an accepted cost. Everything lives under this one dir: the -base baseline tree, -probe trees, -scratch-* trees, lease files, report JSON. A containerized npm ci (kind install, network on) with a committed postinstall can enumerate the mount and patch or delete sources in the -base tree; the later test-delta comparison then reads PR-introduced failures back as pre-existing in the corrupted baseline — netNew comes back empty and a real regression is masked. Observed live with the module's exact mount spec: from a container workdir'd in review-pr-9, the sibling trees and lease files were visible; a write into the -base tree succeeded (WRITE-OK; read back CORRUPTED-BY-PR-POSTINSTALL, uid=0). base-tree.ts builds the baseline via runBuildTest in baseWorktreePath(worktree), confirming the consumer chain. Keep the tmpDir mount for link resolution, but overlay the sibling trees read-only (--volume <sibling>:<sibling>:ro — nested bind mounts are honoured over the wider one), leaving .npm-cache and the command's own tree RW.

中文说明

单一 RW 挂载让每个容器化命令都能写入所有兄弟树与流水线产物——包括 A/B 对比所信任的 merge-base 基线。挂载的"宽度"是有文档记载的决定(否则 farm 链接悬空)——但对其他所有树的写权限是另一个命题,且从未被列为接受的代价。一切都在这一个目录下:-base 基线树、-probe 树、-scratch-* 树、租约文件、报告 JSON。带提交 postinstall 的容器化 npm ci(install 类别、有网络)可以枚举挂载并改写或删除 -base 树里的源码;之后的 test-delta 对比会把 PR 引入的失败从被污染的基线里读回为"本就存在"——netNew 为空,真实回归被掩盖。按模块的挂载参数实测:从工作目录在 review-pr-9 的容器里可见兄弟树与租约文件;对 -base 树的写入成功(WRITE-OK;读回 CORRUPTED-BY-PR-POSTINSTALL,uid=0)。base-tree.tsbaseWorktreePath(worktree) 里的 runBuildTest 构建基线,确认了消费链。保留 tmpDir 挂载以解析链接,但把兄弟树以只读叠加挂载(--volume <sibling>:<sibling>:ro——嵌套绑定挂载优先于更宽的挂载生效),.npm-cache 与命令自身的树保持 RW。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +1728 to +1730
const suite = `${shellQuotePath(process.execPath)} ${shellQuotePath(
findVitestBin(dependencyRoot),
)} run --reporter=json ${probes.map(shellQuotePath).join(' ')}`;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The composed sh -lc suite command is untested — removing ALL quoting from it changes no test result. This sandbox branch is the first place probe file paths reach a shell: the direct arm passes an argv array (no shell); the boxed arm interpolates each path into one sh -lc string via shellQuotePath. This diff makes that quoting load-bearing for correct execution, but nothing exercises the composed command — shellQuotePath's escape behavior is pinned only through agent-prompt prompt-string assertions, not through anything that executes it. A quoting regression — or a probe path containing a space or an apostrophe (a checkout under ~/Documents/John's Projects/…) — is misparsed inside the container: the wrong file runs or a probe is silently skipped, and only when sandboxing is on. Mutation: stripping every shellQuotePath call from the composed string → the five relevant test files still pass 283/283; comparator: defanging the apostrophe escape flips two agent-prompt tests — the suite-composition path has no such pin. Add a test asserting the composed suite round-trips to the intended argv for a probe path containing a space and an apostrophe (or a colocated shell-quote test driving the boxed branch).

中文说明

组装出的 sh -lc 套件命令没有测试——把其中所有引号处理都删掉,也没有任何测试结果变化。这个沙箱分支是探针文件路径第一次进入 shell:直接臂传 argv 数组(无 shell);容器臂经 shellQuotePath 把每个路径插进一条 sh -lc 字符串。本 diff 使该引号处理成为正确执行的承重件,但没有任何测试驱动这条组装命令——shellQuotePath 的转义行为只被 agent-prompt 的提示词断言钉住,没有被任何会执行它的东西钉住。引号回归——或含空格/撇号的探针路径(检出在 ~/Documents/John's Projects/… 之类)——会在容器内被错误解析:跑错文件或静默跳过探针,且只在开启沙箱时发生。变异测试:剥掉组装字符串中全部 shellQuotePath 调用 → 相关五个测试文件仍 283/283 全过;对照:弄坏撇号转义会让两个 agent-prompt 测试变红——套件组装路径没有这样的钉子。补一个测试:断言含空格与撇号的探针路径经组装后往返出预期 argv(或放一个驱动容器分支的 shell-quote 同目录测试)。

— qwen3.8-max via Qwen Code /review (v0.21.15)

typeof review?.severityFloor === 'string'
? review.severityFloor
: undefined,
sandbox: typeof review?.sandbox === 'string' ? review.sandbox : undefined,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] review.sandbox — this feature's on/off switch — has no test at either the loader or the schema layer. review-settings.test.ts covers effort/severityFloor/reverseAuditRounds/attribution/comment passthrough and defaults, but never asserts sandbox passthrough; settingsSchema.test.ts has no assertion for the new enum, and the dialog-membership test pins five review.* keys but not review.sandbox despite showInDialog: true. The policy-layer tests in sandboxed-exec.test.ts all inject {sandbox: …} directly, bypassing operatorReviewSettings() — so a loader or schema regression silently changes whether containment engages, and no test fails. Mutations in a scratch tree: loader reading the wrong key (sandboxMode) → 23/23 pass; schema default off→auto + showInDialog false → 55/55 pass; the comparator (mutating a tested setting) fails its test — the harness can detect this class; review.sandbox simply has no coverage. Add cases mirroring the existing effort tests: 'required' passes through raw, a non-string drops to undefined, and the schema exposes the enum with default off.

中文说明

review.sandbox——本功能的总开关——在加载层与 schema 层都没有测试。review-settings.test.ts 覆盖了 effort/severityFloor/reverseAuditRounds/attribution/comment 的透传与默认值,却从未断言 sandbox 透传;settingsSchema.test.ts 对新枚举没有任何断言;对话框成员测试钉了五个 review.* 键,唯独不含 review.sandbox(尽管它 showInDialog: true)。sandboxed-exec.test.ts 里的策略层测试全部直接注入 {sandbox: …},绕过了 operatorReviewSettings()——加载层或 schema 的回归会静默改变 containment 是否生效,而没有任何测试失败。在 scratch 树中变异:加载层读错键(sandboxMode)→ 23/23 全过;schema 默认值 off→autoshowInDialog false → 55/55 全过;对照项(变异一个有测试的设置项)会令测试失败——测试框架能发现这类问题,只是 review.sandbox 没有覆盖。参照现有 effort 测试补用例:'required' 原样透传、非字符串落为 undefined、schema 暴露该枚举且默认 off

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +1525 to +1526
/**
* The container argv for one probe-suite run, or null to spawn it directly.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Orphaned doc comment: probeContainer was inserted between restoreProbeTreeTracked's doc and the function itself. The long comment ending "Refusing is the only answer that is neither." documented restoreProbeTreeTracked's non-obvious refuse-when-.git-absent rationale; JSDoc binds to the immediately-following declaration, so after this insertion it reads as a preamble to probeContainer, while the function whose rationale it carries loses its documentation. (The same defect class at build-test's containerised is reported separately.) Move probeContainer (with its own comment) above the "Put the probe tree's TRACKED files back…" block, or after restoreProbeTreeTracked.

中文说明

孤立的文档注释:probeContainer 被插在 restoreProbeTreeTracked 的文档注释与该函数之间。以"Refusing is the only answer that is neither."结尾的长注释记录的是 restoreProbeTreeTracked.git 缺失时拒绝的非显而易见之理;JSDoc 绑定紧随其后的声明,插入之后这段注释读起来成了 probeContainer 的前言,而真正需要它的函数失去了文档。(同类缺陷也出现在 build-test 的 containerised,另行报告。)把 probeContainer(连同其注释)移到"Put the probe tree's TRACKED files back…"块之上,或 restoreProbeTreeTracked 之后。

— qwen3.8-max via Qwen Code /review (v0.21.15)

Comment on lines +320 to +321
/**
* The container argv for one reviewed-repository command, or null to run it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Orphaned doc comment (second site): containerised was inserted directly after run()'s pre-existing doc comment. The comment above this block — "Exported for the one thing an injected exec cannot cover: that the failing set is measured HERE, off the raw text, and survives a trim…" — documented run(); JSDoc binds to the immediately-following declaration, so run() now silently loses the rationale for its raw-text failingFiles contract, while this new function gains a floating comment that is factually wrong for it (it is not exported and has nothing to do with exec or FAIL lines). A future edit to run()'s trim/parse seam loses the one comment explaining why the parse must read the raw text — the exact divergence that comment was written to prevent. (Mirror of the test-efficacy orphan, reported separately.) Move containerised (with its comment) above the run() doc block, or after run().

中文说明

孤立的文档注释(第二处):containerised 被插在 run() 既有文档注释的正下方。此块上方的注释——"导出是为了那件注入的 exec 覆盖不了的事:失败集合在这里基于原始文本测量,并能在裁掉 FAIL 行后存活……"——记录的是 run();JSDoc 绑定紧随其后的声明,于是 run() 悄悄失去了其原始文本 failingFiles 契约的理据,而这个新函数得到一段对它而言事实错误的悬空注释(它并未导出,也与 exec/FAIL 行无关)。未来对 run() 裁剪/解析接缝的修改会失去唯一解释"为何必须解析原始文本"的注释——那正是该注释要防止的偏离。(与另行报告的 test-efficacy 处为镜像缺陷。)把 containerised(连同注释)移到 run() 的文档块之上,或 run() 之后。

— qwen3.8-max via Qwen Code /review (v0.21.15)

…s the first cut shipped

Five Criticals from the review, all of them real, and the first two would have
made the feature not work at all.

- **`required` failed open.** `sandboxVerdict` produced a `refused` verdict and
  nothing consumed it: both call sites tested `kind !== 'container'` and fell
  through to the direct spawn with the full environment. Refusal is now decided
  ONCE, at the top of each phase, before anything executes — which is also the
  only place that can cover the route that never reaches a spawn: a repo whose
  toolchain cannot be scoped is handed to the AGENT's own shell, and a gate at
  the spawn would leave that wide open under the very policy forbidding it.
- **`SANDBOX` was a shortcut past the policy.** The first cut returned `direct`
  when the session was already sandboxed, reasoning that the outer boundary is
  the one the operator asked for. Wrong for this property: the CLI's own
  sandbox constrains the filesystem and hands the child `process.env` entire,
  and stripping the secrets is half of what `required` promises.
- **The probe suite baked in the host's Node path.** `process.execPath` does
  not exist inside the image, so every sandboxed probe would exit 127 and map
  baseline, control, every mutant, every hunk and the revert to inconclusive —
  zero evidence exactly when containment is on. It uses the image's `node` now;
  the vitest path resolves because it lives under the mount.
- **The mount root took the first `.qwen/tmp`, not the deepest.** A review run
  from inside another review's worktree nests them, and the first occurrence
  widens the mount to the outer temp dir — pulling `<repo>/.git` and every
  sibling checkout in, which is the one property the mount exists for.
- **No UID/GID mapping.** The default image runs as root, so the container's
  writes into the mounted trees were root-owned and every later host-side
  cleanup — the install-timeout `rmSync`, `discardWorktree`, the sweeps — hit
  EACCES, accumulating residue across reviews.

Two things the fixes themselves needed, found by checking them rather than by
being told:

- **Refusing with `toolchain: 'unsupported'` would have caused the regression it
  was closing.** That value has a documented meaning — the brief reads it as
  "build-test could not scope this repo, install and build it yourself" — so a
  refusal routed into it would have sent the agent to run the reviewed code by
  hand, unsandboxed. It is a distinct `refused` now, with a brief rule that says
  the evidence is unavailable and must NOT be reconstructed by hand.
- **A bare `--user uid:gid` resets `$HOME` to `/`**, which the mapped user
  cannot write, so npm fails before the install starts — `utils/sandbox.ts`
  copies the host's `$HOME` for exactly this. The container gets a writable
  HOME inside the mount, with the npm cache under it.

The duplicated mount-root arithmetic became one exported `mountRootFor`, which
is both how the copies stopped drifting and how the nested case got a test.
Every fix above is pinned by a test that goes red when that fix alone is
reverted.
@wenshao

wenshao commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

All five Criticals handled in c3fa33c. Every one of them held — I checked each against the code before touching it, and two would have made the feature not work at all.

required failed open — right, and the doc comment that delegated refusal "to the caller" was the bug: no caller decided. It is decided once now, at the top of each phase, before anything executes. That placement is what your point about the hand-off forced: a repo whose toolchain cannot be scoped never reaches a spawn at all — unsupportedReport hands the install and the suite to the agent's own shell — so a gate at the spawn would have left that route open under the very policy forbidding it. The SANDBOX shortcut is gone for the same reason you gave: the CLI's own sandbox constrains the filesystem and hands the child process.env entire, and stripping the secrets is half of what required promises.

The host Node path — confirmed, and the consequence is worse than "some probes fail": exit 127 with empty stdout maps baseline, control, every mutant, every hunk and the revert to inconclusive, so the phase yields nothing exactly when containment is on. Fixed as you suggested; the vitest path resolves because it lives under the mount.

indexOflastIndexOf — confirmed with your nested layout, and the reasoning that tree names cannot contain a separator is what makes the deepest occurrence always right. Both copies are gone: the arithmetic is one exported mountRootFor now, which is also how the nested case got a test. That folds in your duplication Suggestion.

UID/GID — confirmed, and the residue argument is the one that decided it: root-owned writes make every later host-side cleanup EACCES, which is the cross-run-state class #9221 spent rounds closing.

Two things the fixes themselves needed, which I found by checking them rather than by being told, and which are worth recording because both would have shipped as new defects:

  1. Refusing with toolchain: 'unsupported' would have caused the regression it was closing. That value has a documented meaning — agent-briefs.ts:540 reads it as "build-test could not scope this repo → install dependencies first, then run one build and one test yourself" — so a refusal routed into it would have sent the agent to run the reviewed code by hand with its own shell. It is a distinct 'refused' now, with a brief rule that says the evidence is unavailable and must not be reconstructed by hand. (base-tree is unaffected: it gates on toolchain !== 'npm', so refused falls to "not built".)
  2. A bare --user uid:gid resets $HOME to /, which the mapped user cannot write, so npm fails before the install starts. utils/sandbox.ts copies the host's $HOME for exactly this — it also useradds the host ids so getpwuid resolves, machinery this does not need because what runs here is a shell command rather than the CLI. The container gets a writable HOME inside the mount, with the npm cache under it, and the getpwuid limit is stated in the code rather than papered over.

The gitfile finding I have not fixed, and I want to be exact about why rather than let silence read as agreement. You are right on the mechanism: each tree's .git gitfile sits inside the RW mount, container code can repoint it at a planted gitdir also inside the mount, and the backpointer round-trip passes because the attacker writes both sides. The claim in my module doc — "<repo>/.git stays outside it" — is true of the common dir and false of the security property, exactly as you put it. That correction belongs in the doc and I will make it.

What I am not doing in this PR is the post-run verification, for a reason I would rather state than assume: this is not a hole the PR opens. Unsandboxed — today's behaviour, and the default this PR keeps — the reviewed code runs as the invoking user and can rewrite any of those gitfiles already, with no container involved. The containment narrows the blast radius and does not create this route. Fixing it properly means requiring every tree's gitdir to resolve under <repo>/.git/worktrees/ (outside the mount) at each host-side consumer — probe-tree creation and the restore, per your list — which is a change to the identity gates rather than to the sandbox, overlaps #9742's backpointer work, and deserves its own tests. Filing it against #9556 with your repro rather than bolting it on here.

Local: src/commands/review 4 418 passed, 1 skipped, 0 failed; with src/config, 5 613. Each fix was verified by reverting it alone and watching its own test go red — the phase refusal, the SANDBOX non-shortcut, the uid mapping, the HOME, and the deepest-.qwen/tmp mount root.

中文说明

五个 Critical 已在 c3fa33c 处理完毕。每一条都成立——我在动手前逐条对着代码核过,其中两条会让这个功能根本不工作。

required 失败放行 —— 成立,而那句把拒绝"交给调用方"的文档注释正是 bug:没有任何调用方去决定。现在它在每个阶段的顶部、任何东西执行之前被决定一次。这个位置正是你关于 hand-off 那一点逼出来的:无法被 scope 的仓库根本到不了 spawn——unsupportedReport 把安装与套件交给 agent 自己的 shell——所以把闸门放在 spawn 处,会在"恰恰禁止此事的策略"之下把那条路留着大开。SANDBOX 短路也按你给的理由去掉了:CLI 自己的沙箱约束的是文件系统,却把 process.env 整个交给子进程,而剥离密钥是 required 承诺的一半。

宿主 Node 路径 —— 确认,且后果比"部分探针失败"更糟:exit 127 + 空 stdout 会把基线、对照、每个突变体、每个 hunk 与回退全部映射为 inconclusive——恰恰在容器化开启时该阶段产出为零。已按你的建议修正;vitest 路径能解析,因为它位于挂载之内。

indexOflastIndexOf —— 用你给的嵌套布局确认;而"树名不能含分隔符"正是"最深处那个总是对的"的依据。两份副本都没了:这段算术现在是一个导出的 mountRootFor,嵌套情形也因此有了测试。这同时合并了你那条关于重复的 Suggestion。

UID/GID —— 确认,而决定性的是残留那条论证:root 所有的写入会让其后每一次宿主侧清理 EACCES,那正是 #9221 花了若干轮才关掉的跨运行状态类别。

两件修复本身需要的东西,是我核查修复时发现的、而非被指出的,值得记录,因为两者都会作为新缺陷出厂:

  1. toolchain: 'unsupported' 来拒绝,会造成它本要关闭的那个回归。 该值有文档化的含义——agent-briefs.ts:540 把它读作"build-test 无法 scope 本仓库 → 先自己装依赖,然后自己跑一条 build 和一条 test"——因此把拒绝路由进它,等于让 agent 用自己的 shell 手工运行被审代码。现在它是独立的 'refused',并配有一条简报规则:该证据不可用,且不得手工重建。(base-tree 不受影响:它以 toolchain !== 'npm' 把关,refused 自然落入"未构建"。)
  2. --user uid:gid 会把 $HOME 重置为 /,而被映射的用户写不了它,于是 npm 在安装开始前就失败。utils/sandbox.ts 正是为此复制宿主的 $HOME——它还会 useradd 宿主 id 以便 getpwuid 可解析,那套机制这里不需要,因为这里跑的是 shell 命令而非 CLI。容器现在拿到挂载内一个可写的 HOME,npm 缓存置于其下;getpwuid 这条限制写在代码里,而不是含糊过去。

gitfile 那条我没有修,我宁愿把理由说准,也不愿让沉默读作认同。 机制上你是对的:每棵树的 .git gitfile 位于可写挂载之内,容器内代码可以把它重指向同样位于挂载内的植入 gitdir,而 backpointer 往返会通过——因为攻击者两侧都能写。我模块文档里那句"<repo>/.git 在挂载之外",对 common dir 成立、对安全性质不成立,正如你所述。这条更正属于文档,我会改。

本 PR 不做的是运行后校验,理由我宁可明说而非默认:这不是本 PR 打开的洞。在无沙箱状态下——也就是今天的行为、以及本 PR 保持的默认——被审代码以调用者身份运行,本就能重写那些 gitfile 中的任意一个,与容器无关。容器化缩小了影响面,并没有创造这条路径。要修对,需要在每个宿主侧消费点(按你的清单:探针树创建与恢复)要求每棵树的 gitdir 解析到 <repo>/.git/worktrees/ 之下(挂载之外)——那是对身份门而非对沙箱的改动,与 #9742 的 backpointer 工作重叠,且应有自己的测试。我会连同你的复现归档到 #9556,而不是硬塞进这里。

本地:src/commands/review 4 418 通过、1 跳过、0 失败;连同 src/config 为 5 613。每项修复都以"单独回退该项 → 其对应测试变红"验证——阶段级拒绝、SANDBOX 不再短路、uid 映射、HOME,以及最深处的 .qwen/tmp 挂载根。

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform-fragile assertions bite exactly there.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out assertion arm is environment-dependent.

Not explored to full depth (tool budget reached): "agent test-matrix": none — no check was cut short..

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:165 — [review] R1-6 the disclose strings are produced but never surfaced
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:99 — [review] R1-8 an unrecognized QWEN_REVIEW_SANDBOX value silently falls through to off
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:101 — [review] R1-9 policy parsing asymmetric: env normalized, settings exact-matched
  • packages/cli/src/commands/review/lib/review-settings.ts:17 — [review] R1-10 a settings-load failure silently degrades required to off
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:201 — [review] R1-11 the env allowlist drops the proxy variables
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:228 — [review] R1-12 the container starts with no pids/memory/CPU limits
  • packages/cli/src/commands/review/build-test.ts:346 — [review] R1-13 the single RW mount gives every containerized command write access to all sibling trees
  • packages/cli/src/commands/review/test-efficacy.ts:1730 — [review] R1-14 the composed sh -lc suite command is untested
  • packages/cli/src/commands/review/lib/review-settings.ts:110 — [review] R1-15 review.sandbox has no test at either the loader or the schema layer
  • packages/cli/src/commands/review/test-efficacy.ts:1526 — [review] R1-16 orphaned doc comment (probeContainer inserted between doc and function)
  • packages/cli/src/commands/review/build-test.ts:321 — [review] R1-17 orphaned doc comment (containerised inserted after run()'s doc)

[Critical] R2-4 (packages/cli/src/commands/review/test-delta.ts:223,:315 — file not in this diff, so this blocker cannot be anchored inline): test-delta never crosses the containment choke point. The diff makes build-test's run() the containment choke point and gates two phases at phase-top, but runTestDelta keeps its own private spawnSync(command, { shell: true, env: buildRunEnv(process.env) }) with no refusal gate and no containerised() consultation — newly wrong because of this diff, whose own agent-briefs bullet routes the agent to base-tree + test-delta whenever tests failed. Both arms reachable: with a runtime answering, a containerized build-test records failing tests and the documented flow reruns them BASE-SIDE UNCONTAINED; with no runtime, a stale report from an earlier run/regime does the same. Second consequence under auto: PR-side suites run in the restricted container env while base-side runs in the full host env, so an env-sensitive test can flip on exactly one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. witness: [probe] QWEN_REVIEW_SANDBOX=required with answering docker, runTestDelta driven through the PR's own code: verdict {kind:'container',runtime:'docker'}; DELTA base-side output 'SECRET-IS:[hunter2-credential]' — the recorded repo command ran directly in the host shell with the full environment. Fix direction: route test-delta's rerun through build-test's run() (or export and apply containerised there); a bare refuseUnsandboxedPhase call does not close the answering-runtime arm.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform-fragile assertions bite exactly there。

未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out assertion arm is environment-dependent。

未探索到全部深度(达到工具调用预算):"agent test-matrix"none — no check was cut short.

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 11 条(原文未翻译,列表见上方英文部分)。

[Critical] R2-4 (packages/cli/src/commands/review/test-delta.ts:223,:315 — file not in this diff, so this blocker cannot be anchored inline): test-delta never crosses the containment choke point. The diff makes build-test's run() the containment choke point and gates two phases at phase-top, but runTestDelta keeps its own private spawnSync(command, { shell: true, env: buildRunEnv(process.env) }) with no refusal gate and no containerised() consultation — newly wrong because of this diff, whose own agent-briefs bullet routes the agent to base-tree + test-delta whenever tests failed. Both arms reachable: with a runtime answering, a containerized build-test records failing tests and the documented flow reruns them BASE-SIDE UNCONTAINED; with no runtime, a stale report from an earlier run/regime does the same. Second consequence under auto: PR-side suites run in the restricted container env while base-side runs in the full host env, so an env-sensitive test can flip on exactly one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. witness: [probe] QWEN_REVIEW_SANDBOX=required with answering docker, runTestDelta driven through the PR's own code: verdict {kind:'container',runtime:'docker'}; DELTA base-side output 'SECRET-IS:[hunter2-credential]' — the recorded repo command ran directly in the host shell with the full environment. Fix direction: route test-delta's rerun through build-test's run() (or export and apply containerised there); a bare refuseUnsandboxedPhase call does not close the answering-runtime arm.

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +220 to +222
export function refuseUnsandboxedPhase(
verdict: SandboxVerdict = sandboxVerdict(),
): string | null {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-1: round 1's blocker still stands — the fix covers only the no-runtime arm. refuseUnsandboxedPhase() models "did a runtime answer", never "can this phase actually be contained": with a runtime ANSWERING, the verdict is container, the gate returns null, and every route the container cannot serve still runs the reviewed code unsandboxed with the full environment.

Measured at this head: a local-checkout review (cwd outside .qwen/tmp) falls through containerised()run() and spawns with buildRunEnv() — the entire process.env — with the report indistinguishable from a contained run; test-efficacy's probe tree (<worktree>-probe) is unmountable for a local checkout, so the direct spawn inherits the whole environment; and the unsupported-toolchain hand-off passes the gate on verdict container, instructing the agent to run install/build/test in its own shell, policy never consulted. The settings schema promises operators that required refuses to run them unsandboxed, and #9556's policy table says the execution-dependent steps report UNAVAILABLE — not "run directly".

witness: probe through the real run() with answering docker, policy=required
ARM-A (cwd outside .qwen/tmp): exit 0, output "SECRET-IS:[hunter2-credential]" — ran directly, saw the host secret
ARM-B (cwd inside .qwen/tmp):  exit 0, output "SECRET-IS:[]" — contained, allowlist stripped the secret

Make the phase-top decision containment-aware: under required, refuse when the phase tree cannot be mounted (e.g. thread the phase root through and return a reason when mountRootFor(root) === null), and treat the unsupported-toolchain hand-off as a refusal under required — the phase reports evidence-unavailable instead of spawning or instructing.

中文说明

第 1 轮的阻断项 R1-1 依旧成立——本次修复只覆盖了"无运行时"这一臂。refuseUnsandboxedPhase() 只建模"有没有运行时应答",从不建模"本阶段是否真的能被容器化":当有运行时应答时,判定为 container,关卡放行,凡是容器无法承接的路径依旧带着完整环境在沙箱外运行被审代码。

在本 head 上实测:本地检出审查(cwd 不在 .qwen/tmp 之内)经 containerised() 落空后从 run() 直接 spawn,带 buildRunEnv()——整个 process.env——且报告与容器化运行无法区分;test-efficacy 的探针树(<worktree>-probe)在本地检出下不可挂载,直接 spawn 继承全部环境;不支持工具链的交接路径在 container 判定下过关,指示 agent 在自己的 shell 里运行 install/build/test,全程不查询策略。设置 schema 向操作者承诺 required 拒绝在沙箱外运行,#9556 的策略表也写明依赖执行的步骤"报告不可用"——而不是"直接运行"。

证据:探针驱动真实 run()(有应答的 docker,policy=required)——ARM-A(cwd 在 .qwen/tmp 外):exit 0,输出 SECRET-IS:[hunter2-credential],直接运行、看到了宿主密钥;ARM-B(cwd 在 .qwen/tmp 内):exit 0,输出 SECRET-IS:[],已容器化、白名单剥掉了密钥。

请让阶段顶部的决定具备"可容器化"意识:在 required 下,当阶段树无法挂载时拒绝(例如把阶段根传入,mountRootFor(root) === null 时返回原因),并把不支持工具链的交接也视为 required 下的拒绝——阶段报告"证据不可用",而不是 spawn 或指示。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// file one run rewrote, an ignored plant a sweep honoured. `--rm` is
// isolation by construction rather than by hygiene.
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-3: still stands at this head — deliberately deferred by the author, mechanism unchanged. The RW mount covers every pipeline tree's .git gitfile: it sits inside its tree, i.e. inside this mount, and code in the container can rewrite it to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge. The two host-side consumers that follow the redirect — probe-tree creation (git worktree add with the review worktree as cwd) and restoreProbeTreeTracked after every sandboxed suite run — have neither scratch-tree.ts's gitfile verification nor its localFilterCommands refusal; the restore's backpointer round-trip passes because the attacker writes the very gitdir file the check reads, and its -c overrides neutralize core.hooksPath/core.fsmonitor but NOT filter.*.

witness: round-1 end-to-end reproduction (git 2.43.0, throwaway trees mirroring the layout):
check sequence passed (backpointer round-trip read the attacker-planted gitdir), checkout exit 0,
HOST-SIDE EXECUTION OBSERVED for both the restore checkout and `git worktree add`;
scratch-tree's identity gate refused the same tree.

The module doc's "<repo>/.git stays outside it" is true of the common dir and false of this security property. Treat the per-tree gitfile as attack surface: after each sandboxed run verify every tree's .git still resolves to its own admin entry under <repo>/.git/worktrees/ (outside the mount) and refuse on mismatch; extend scratch-tree's localFilterCommands refusal and gitfile check to probe-tree creation and restoreProbeTreeTracked — or land the documented follow-up before auto/required are turned on in CI. The doc correction committed to in round 1 is also still absent (see the separate comment at the module doc).

中文说明

R1-3 在本 head 上依旧成立——作者有意推迟到后续 PR,机制未变。RW 挂载覆盖了每棵流水线树的 .git gitfile:它位于树内、即在此挂载之内,容器内代码可以把它改写为 gitdir: <同一挂载下植入的目录>,其中带 filter.<x>.smudge。两个会跟随重定向的宿主侧消费者——探针树创建(以 review 工作树为 cwd 的 git worktree add)与每次沙箱化套件运行后的 restoreProbeTreeTracked——既没有 scratch-tree.ts 的 gitfile 校验、也没有其 localFilterCommands 拒绝;restore 的回指针往返检查会通过,因为攻击者写下的正是该检查读取的 gitdir 文件,其 -c 覆盖能中和 core.hooksPath/core.fsmonitor,却中和不了 filter.*

证据:第 1 轮已在 git 2.43.0 上按该布局端到端复现:检查序列通过(回指针往返读到攻击者植入的 gitdir)、checkout exit 0、restore checkout 与 git worktree add 均观测到宿主侧执行;scratch-tree 的身份关卡对同一棵树会拒绝。

模块文档的"<repo>/.git 留在挂载外"对 common dir 为真,对这条安全性质为假。请把每棵树的 gitfile 当作攻击面:每次沙箱化运行后校验各树 .git 仍解析到 <repo>/.git/worktrees/(挂载外)下自己的 admin entry,不一致即拒绝;把 scratch-tree 的 localFilterCommands 拒绝与 gitfile 检查扩展到探针树创建与 restoreProbeTreeTracked——或者在 CI 打开 auto/required 之前先落地已承诺的后续修复。第 1 轮承诺的文档更正也仍未做(见模块文档处的另一条评论)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +812 to +813
const refusal = refuseUnsandboxedPhase();
if (refusal) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] A policy refusal on a --resume call RETURNS a fresh report instead of throwing, so the handler's unconditional writeFileSync(args.out, …) overwrites the in-flight report the call was asked to continue — destroying it and killing the resume chain permanently. The refusal report carries no run identity field, so every later --resume — even after the runtime recovers — fails previousReport's identity check ("records no run identity"), and the round must redo install + build + all suites from scratch. Trigger: policy required, call 1 runs containerized and writes a partial report when the whole-call budget expires mid-suites (the ordinary --resume case); before the resume call the runtime probe fails (daemon restarted, or the 30s probe times out under load). The !adapter branch twenty lines below codifies the exact invariant this violates: "A continuation must never answer with a FRESH report … Throwing reaches the handler's catch, which writes nothing."

witness: three-call sequence through the real runBuildTest (runtime stubbed to fail on call 2):
CALL1 toolchain npm with run identity
CALL2 returned toolchain=refused run=null; OUT after call2: toolchain=refused run=null test entries=0 — original overwritten
CALL3 threw 'records no run identity'
FLIP (throw when previous set): CALL2 threw, OUT preserved (run present=true), CALL3 resumed
Suggested change
const refusal = refuseUnsandboxedPhase();
if (refusal) {
const refusal = refuseUnsandboxedPhase();
if (refusal) {
if (previous) {
throw new Error(
`build-test: --resume cannot continue the run recorded at ${args.out}: ` +
`${refusal} — running the remaining suites unsandboxed is what the policy ` +
`forbids. The report is left untouched; get a container runtime answering, ` +
`then resume again.`,
);
}
中文说明

策略拒绝在 --resume 调用上返回一份全新报告而不是抛错,于是处理器无条件的 writeFileSync(args.out, …) 会覆盖它本应续跑的那份在途报告——证据被毁、续跑链被永久杀死。拒绝报告不带 run 身份字段,因此之后每一次 --resume——即使运行时已恢复——都会撞上 previousReport 的身份检查("records no run identity"),整轮只能从头重做 install + build + 全部套件。触发路径:策略 required,第 1 次调用容器化运行、在整套预算于套件中途耗尽时写下部分报告(正是 --resume 存在的常规场景);续跑调用前运行时探测失败(守护进程重启,或 30s 探测在负载下超时)。下方二十行的 !adapter 分支恰好写明了被此处违反的不变量:"续跑绝不能以一份全新报告作答……抛错会到达处理器的 catch,那里什么都不写。"

证据:对真实 runBuildTest 的三调用序列(第 2 次调用前把运行时置为失败)——第 1 次调用得到 npm 工具链与运行身份;第 2 次调用返回 toolchain=refused、run=null,调用后报告文件变为 toolchain=refused、run=null、test 条目 0(原报告被覆盖);第 3 次调用抛 "records no run identity"。翻转为"有 previous 时抛错"后:第 2 次调用抛错、报告保留(run 仍在)、第 3 次调用成功续跑。

— qwen3.8-max via Qwen Code /review (v0.22.0)

cwd: join(tmpDir, 'review-pr-9'),
kind: 'install',
});
const user = args[args.indexOf('--user') + 1];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Pattern (1 of 3 locations): three assertions in this new file only hold on POSIX with a host-typical environment — the implementations are correct, the tests are platform-fragile, and the Windows merge-queue gate runs them. THIS assertion: containerCommand emits --user only when process.getuid/getgid exist and the documented SANDBOX_SET_UID_GID opt-out is unset, but the test asserts unconditionally. On Windows process.getuid is undefined, args.indexOf('--user') is -1, and args[0] ('run') is compared against 'undefined:undefined'; on POSIX any shell carrying the documented opt-out (docs/users/features/sandbox.md names export SANDBOX_SET_UID_GID=false) fails it spuriously. ci.yml:875 test_windows (merge_group event) runs npm run test:civitest run in packages/cli, so this file red-boards the required Windows check.

witness: SANDBOX_SET_UID_GID=false npx vitest run sandboxed-exec.test.ts
 × containerCommand > maps the host uid … AssertionError: expected 'run' to be '1000:1000' (1 failed | 12 passed)
flip (env unset): 13 passed

Make it hermetic and platform-aware, the utils/sandbox.test.ts convention: it.skipIf(!process.getuid || process.env['SANDBOX_SET_UID_GID']?.toLowerCase().trim() === 'false') plus vi.stubEnv('SANDBOX_SET_UID_GID', 'true') inside.

中文说明

模式(3 处之 1):这个新文件里有三处断言只在"POSIX + 宿主典型环境"下成立——实现是对的,测试是平台脆弱的,而 Windows 合并队列关卡会运行它们。本处断言:containerCommand 只在 process.getuid/getgid 存在且未设置文档化的 SANDBOX_SET_UID_GID 豁免时才发出 --user,但测试无条件断言。Windows 上 process.getuid 为 undefined,args.indexOf('--user') 为 -1,args[0]'run')会与 'undefined:undefined' 比较;POSIX 上任何带文档化豁免变量(docs/users/features/sandbox.md 写明 export SANDBOX_SET_UID_GID=false)的 shell 都会让它假失败。ci.yml:875test_windows(merge_group 事件)运行 npm run test:ci → packages/cli 的 vitest run,本文件会让必需的 Windows 检查变红。

证据:SANDBOX_SET_UID_GID=false 下运行该测试 → × maps the host uid … expected 'run' to be '1000:1000'(1 failed | 12 passed);去掉该变量 → 13 passed。

请改成密封且平台感知,沿用 utils/sandbox.test.ts 的约定:it.skipIf(!process.getuid || process.env['SANDBOX_SET_UID_GID']?.toLowerCase().trim() === 'false'),并在测试内 vi.stubEnv('SANDBOX_SET_UID_GID', 'true')

— qwen3.8-max via Qwen Code /review (v0.22.0)

// HOME inside the mount: forcing a uid resets it to `/`, which the
// mapped user cannot write, and npm fails before the install starts.
'HOME=/home-in-mount',
'npm_config_cache=/home-in-mount/.npm',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Pattern (2 of 3 locations): platform-fragile assertions in this new file (see the comment at line 168 for the pattern and the Windows merge-queue gate). THIS assertion hardcodes the POSIX literal 'npm_config_cache=/home-in-mount/.npm', but containerEnv builds the entry with path.join(homeDir, '.npm') — on Windows node:path IS path.win32, so the implementation emits backslashes and the toEqual fails (the HOME=/home-in-mount entry above it passes — template literal, no join).

witness: node v22.23.0 path.win32 probe:
join('/home-in-mount', '.npm') = '\home-in-mount\.npm'  vs expected '/home-in-mount/.npm'

Build the expected entry with the same primitive the implementation uses: \npm_config_cache=${join('/home-in-mount', '.npm')}``.

中文说明

模式(3 处之 2):本新文件中的平台脆弱断言(模式与 Windows 合并队列关卡见第 168 行的评论)。本处断言硬编码了 POSIX 字面量 'npm_config_cache=/home-in-mount/.npm',但 containerEnvpath.join(homeDir, '.npm') 构造该条目——Windows 上 node:path 就是 path.win32,实现会输出反斜杠,toEqual 因而失败(上面那条 HOME=/home-in-mount 能过,因为它是模板字符串、未经 join)。

证据:node v22.23.0 的 path.win32 探针:join('/home-in-mount', '.npm') = '\home-in-mount\.npm',与期望值 '/home-in-mount/.npm' 不符。

请用与实现相同的原语构造期望值:\npm_config_cache=${join('/home-in-mount', '.npm')}``。

— qwen3.8-max via Qwen Code /review (v0.22.0)

cwd: join(tmpDir, 'review-pr-9'),
kind: 'install',
});
expect(args.slice(-3)).toEqual(['sh', '-lc', 'npm ci && npm test']);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Pattern (5 of 5 locations): under-pinned wiring in this file (see the comment at line 76). HERE: no assertion pins that containerCommand puts the IMAGE in the argv — the fixture image: 'example/image:tag' is only fed in, never checked, and this file is the only suite exercising containerCommand. Deleting opts.image from the final push keeps every assertion passing (slice(-3) still sees ['sh', '-lc', cmd]), while at runtime docker parses the first positional (sh) as the image and every containerized install/build/test fails with "Unable to find image 'sh:latest'" — surfacing only in a live containerized run, which this suite never performs.

witness: mutant 'drop opts.image' → suite 13/13 green; flip pin expect(args.slice(-4)).toEqual([image, 'sh', '-lc', cmd]) → red against mutant (1 failed | 12 passed), green against pristine

Extend the tail assertion: expect(args.slice(-4)).toEqual([base.image, 'sh', '-lc', 'npm ci && npm test']);.

中文说明

模式(5 处之 5):本文件对接线钉得不够(见第 76 行的评论)。本处:没有断言钉住 containerCommand镜像放进 argv——夹具 image: 'example/image:tag' 只被传入、从未被检查,而本文件是唯一运行 containerCommand 的套件。从最终 push 中删掉 opts.image,所有断言依旧通过(slice(-3) 仍看到 ['sh', '-lc', cmd]),而运行时 docker 会把第一个位置参数(sh)当作镜像,所有容器化 install/构建/测试都会以 "Unable to find image 'sh:latest'" 失败——只在真实容器化运行中暴露,而本套件从不做这种运行。

证据:突变体"删掉 opts.image" → 套件 13/13 绿;翻转钉 expect(args.slice(-4)).toEqual([image, 'sh', '-lc', cmd]) → 对突变体红(1 failed | 12 passed)、对原代码绿。

请扩展尾部断言:expect(args.slice(-4)).toEqual([base.image, 'sh', '-lc', 'npm ci && npm test']);

— qwen3.8-max via Qwen Code /review (v0.22.0)

// bought by running that suite unsandboxed. Refusing here rather than at the
// spawn keeps the report's vocabulary intact: the phase produced nothing, and
// says why, instead of a run of probes each blaming the runner.
const sandboxRefusal = refuseUnsandboxedPhase();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Pattern (1 of 4 locations): the diff's production wiring has no coverage in the consumer suites. HERE: the two phase-level refusal gates — the entire round-1 fix ("make required actually refuse") — have zero test coverage at either call site; the only test of refuseUnsandboxedPhase calls it directly, never through a phase. Deleting this early-return (or the twin in build-test.ts:812, or moving either after adapter selection / probe-tree creation) leaves every test green, and under required with no runtime the phases again execute the reviewed repo's commands unsandboxed — the exact regression this commit exists to prevent. Both gates are deterministically testable by mocking ./sandboxed-exec.js.

witness: grep across packages/cli test tree — zero refusal/sandbox references in build-test.test.ts and test-efficacy.test.ts; only sandboxed-exec.test.ts touches the gate, calling it directly

Add one test per phase: mock sandboxed-exec.js so refuseUnsandboxedPhase returns a reason; assert runBuildTest returns toolchain: 'refused', ok: false, the note, and never calls the injected exec; and runTestEfficacy records the note and runs zero probes.

中文说明

模式(4 处之 1):本 diff 的生产接线在消费者套件中没有覆盖。本处:两个阶段级拒绝关卡——第 1 轮修复的全部("让 required 真的拒绝")——在两个调用点都零测试覆盖;refuseUnsandboxedPhase 唯一的测试是直接调用它,从不经过阶段。删除这个提前返回(或 build-test.ts:812 的孪生关卡,或把任一者移到适配器选择/探针树创建之后)所有测试保持绿,而在无运行时的 required 下,两个阶段会再次在沙箱外执行被审仓库的命令——正是本提交要防止的回归。两个关卡都可以通过 mock ./sandboxed-exec.js 做确定性测试。

证据:对 packages/cli 测试树全量 grep——build-test.test.ts 与 test-efficacy.test.ts 中零拒绝/沙箱引用;只有 sandboxed-exec.test.ts 触及该关卡,且是直接调用。

请为每个阶段各加一个测试:mock sandboxed-exec.js 使 refuseUnsandboxedPhase 返回原因;断言 runBuildTest 返回 toolchain: 'refused'ok: false、带 note,且从不调用注入的 exec;断言 runTestEfficacy 记录 note 且零探针运行。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// install scripts the PR committed, its build, its suite — so it is the
// thing #9556 is about. `containerised` returns null when the run is not
// sandboxed, and the direct spawn below is unchanged for that case.
const boxed = containerised(command, cwd, kind);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Pattern (2 of 4 locations): the diff's production wiring has no coverage in the consumer suites (see the comment at test-efficacy.ts:2488). HERE: the wiring between the tested containerCommand argv builder and the actual spawn — containerised()'s three null cases, the boxed/direct ternary, and the boxed branch's env: process.env without shell: true — is exercised by no test; every build-test.test.ts case runs policy off, so containerised() only ever takes its first null path in tests. Removing if (tmpDir === null) return null; stays green: a local-checkout review would then be handed container argv with a mount that doesn't contain the tree; swapping the ternary or restoring shell: true/buildRunEnv() on the boxed branch regresses silently.

witness: mutant 'remove tmpDir===null return' → build-test + sandboxed-exec suites green (118/118);
probe (policy auto, fake docker answering): mutated tree → docker run invoked with null mount (red); correct tree → no docker run (green)

Mock sandboxed-exec.js to a container verdict and assert run() spawns (boxed.file, boxed.args) without a shell for a cwd under a temp dir, and falls back to direct spawn for a cwd outside one.

中文说明

模式(4 处之 2):本 diff 的生产接线在消费者套件中没有覆盖(见 test-efficacy.ts:2488 的评论)。本处:已测试的 containerCommand argv 构造器与实际 spawn 之间的接线——containerised() 的三种 null 情形、容器/直接三元选择、容器分支不带 shell: trueenv: process.env——没有任何测试运行;build-test.test.ts 的所有用例都以策略 off 运行,测试中 containerised() 只走第一个 null 分支。移除 if (tmpDir === null) return null; 仍是绿:本地检出审查随后会拿到挂载不含该树的容器 argv;交换三元、或在容器分支恢复 shell: true/buildRunEnv() 都会静默回归。

证据:突变体"移除 tmpDir===null 返回" → build-test + sandboxed-exec 套件绿(118/118);探针(策略 auto、假 docker 应答):突变树 → 以 null 挂载调用 docker run(红);正确树 → 不调用 docker run(绿)。

请 mock sandboxed-exec.js 为 container 判定,断言对临时目录内的 cwd,run()(boxed.file, boxed.args) 无 shell spawn;对临时目录外的 cwd 回退直接 spawn。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Math.min(perCommandMs, remainingMs()),
// The one command that needs the registry. Everything else this adapter
// runs is offline under the sandbox policy — see `containerCommand`.
'install',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Pattern (3 of 4 locations): the diff's production wiring has no coverage in the consumer suites (see the comment at test-efficacy.ts:2488). HERE: the only call site that grants network under the sandbox policy is unpinned — npm-toolchain.test.ts's okExec accepts (command: string) only, so no test observes the fourth kind argument. Deleting 'install' stays green: run()'s default kind = 'test' then applies --network none to npm ci, and every sandboxed build-test fails at install — read by the pipeline as an install failure of the reviewed repo rather than a policy wiring bug. The inverse mutation (passing 'install' for build/test) likewise stays green and would grant egress to builds the design keeps offline.

witness: mutant 'delete install arg' → npm-toolchain 7/7, sandboxed-exec 13/13, build-test 105/105 all green;
inverse mutant → green; probe capturing the 4th arg: green on correct code (install → 'install'), red on both mutants

Capture okExec's fourth argument and assert the install command receives 'install' while build/test commands receive 'test' or omit it.

中文说明

模式(4 处之 3):本 diff 的生产接线在消费者套件中没有覆盖(见 test-efficacy.ts:2488 的评论)。本处:沙箱策略下唯一授予网络的调用点没有被钉住——npm-toolchain.test.tsokExec 只接受 (command: string),没有测试观察第四个 kind 参数。删掉 'install' 仍是绿:run() 的默认 kind = 'test' 会对 npm ci 施加 --network none,所有沙箱化 build-test 都会在安装时失败——流水线会把这读成被审仓库的安装失败,而不是策略接线 bug。反向突变(给 build/test 传 'install')同样是绿,会把出口网络授予设计上保持离线的构建。

证据:突变体"删除 install 参数" → npm-toolchain 7/7、sandboxed-exec 13/13、build-test 105/105 全绿;反向突变体 → 绿;捕获第 4 参数的探针:对正确代码绿(install → 'install'),对两个突变体都红。

请捕获 okExec 的第四个参数,断言 install 命令收到 'install',而 build/test 命令收到 'test' 或省略。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// maps every probe — baseline, control, each mutant, each hunk, the revert —
// to inconclusive, blaming the runner's output for a wiring error. The vitest
// bin path DOES resolve, because it lives under the mounted temp dir.
const suite = `node ${shellQuotePath(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Pattern (4 of 4 locations): the diff's production wiring has no coverage in the consumer suites (see the comment at test-efficacy.ts:2488). HERE: the sandboxed probe-run path is untested end to end — the probeContainer boxed/direct choice and the image-PATH node decision have no test anywhere (the suite-quoting half of this gap is round-1 ledger entry R1-14, still open). The boxed route is the first place probe paths reach a shell: a regression in probeContainer's null cases or in the node-off-image-PATH choice maps every probe — baseline, control, each mutant, hunk, revert — to inconclusive, silently misattributing test efficacy.

witness: grep across all tests — no coverage of probeContainer, the boxed/direct choice, or the suite string;
test-efficacy.integration.test.ts drives runProbeSuite with an empty probe list ('is not the subject')

Assert via an injected seam (or an exported helper) that the boxed path uses image-PATH node and probeContainer returns null for the three documented cases.

中文说明

模式(4 处之 4):本 diff 的生产接线在消费者套件中没有覆盖(见 test-efficacy.ts:2488 的评论)。本处:沙箱化探针运行路径端到端无测试——probeContainer 的容器/直接选择与"镜像 PATH 上的 node"决定在任何地方都没有测试(此缺口的套件引号部分即第 1 轮账目 R1-14,仍未关闭)。容器分支是探针路径第一次进入 shell 的地方:probeContainer 的 null 情形或"镜像 PATH 的 node"选择一旦回归,会把每个探针——基线、对照、每个突变体、hunk、回退——判为 inconclusive,静默错判测试效力。

证据:对全部测试 grep——probeContainer、容器/直接选择、套件字符串均无覆盖;test-efficacy.integration.test.ts 以空探针列表驱动 runProbeSuite("不是其主题")。

请通过注入缝隙(或导出的辅助函数)断言容器分支使用镜像 PATH 的 node,且 probeContainer 对三种文档化情形返回 null。

— qwen3.8-max via Qwen Code /review (v0.22.0)

…inment (#9556)

Round 2. The sharpest finding is that the guarantee this PR advertises was
true of one route and false through another.

**A repository could switch off the containment that exists to contain it.**
`operatorReviewSettings` skips the workspace scope precisely so a
`.qwen/settings.json` cannot set review policy — but the env layer that
outranked it is repository-controlled too: `loadEnvironment` walks up from cwd
and applies `<repo>/.qwen/.env`, from the very checkout under review, admitted
by default because folder trust starts off. `QWEN_REVIEW_SANDBOX=off` in a
committed `.env` disabled it. Three siblings were worse, because they have no
ordering to fall back on: `QWEN_REVIEW_SANDBOX_IMAGE` chooses the image the
reviewed code runs *inside*; `SANDBOX_SET_UID_GID=false` puts the container
back to root; `DOCKER_HOST` chooses which daemon answers, so `required` reads
as satisfied and whatever that daemon returns is scored as evidence.

`environment.ts` gains `isFileSourcedEnvKey`, and containment now reads only
the operator's settings or a real process variable. The policy additionally
only ever tightens, so even a genuine env value cannot lower a settings
`required` — which, as a mutation showed, is what actually protects the policy;
the file-source check is what protects the other three.

**`required` still failed open where the mount could not be built.** The gate
asked "did a runtime answer", never "can this phase be contained": with a
healthy daemon and a cwd outside `.qwen/tmp` — a `/review` of a local checkout
— the command fell through to the direct spawn with the full environment and a
report indistinguishable from a contained run. It asks the second question now.

**A refusal on `--resume` destroyed the run it was asked to continue.**
Returning a report let the handler's unconditional write overwrite the
in-flight one, and the refusal carries no run identity, so every later resume
failed the identity check even after the runtime recovered. It throws on a
continuation, which is the invariant the `!adapter` branch states in its own
words.

**The HOME added last round was itself cross-run state.** It lived on the
shared mount, and `sh -lc` sources `$HOME/.profile` while npm reads
`$HOME/.npmrc` — so one run's postinstall could plant what the next review's
install executes, with the network on. That contradicted this module's own
`--rm` "isolation by construction" claim, and it arrived with the fix for the
`$HOME` problem rather than in the original. HOME is a tmpfs now: discarded
with the container, never on the host. The npm cache stays on the mount, and
the comment says plainly that npm's integrity check is what stands between a
poisoned cache and a bad install.

**The mount root was lexical.** `resolve` never touches the filesystem, so a
symlink at or above `.qwen/tmp` — committable as mode 120000 — would have
widened a read-write bind mount to wherever it pointed. Every other creating or
destroying path in this pipeline refuses that; this one does now too.

Also: the new tests were platform-fragile in three places (Windows has no
`process.getuid`, and the documented `SANDBOX_SET_UID_GID` opt-out could fail
them on a developer's box), and the secret-leak check asserted against whatever
the runner happened to export rather than a planted canary. Both fixed, and the
mount-root tests now build real directories, which is how the symlink refusal
got pinned at all.

Every fix here is pinned by a test that goes red when that fix alone is
reverted — except the policy's file-source check, which a mutation showed is
redundant with the tightening rule, and which is documented as defence in depth
rather than claimed as load-bearing.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file and the Windows-only case-folding path (R3-21) bite exactly there.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped.

Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:174 — [review] R2-8 JSDoc claims a SANDBOX-set session 'returns direct' — contradicts the NOTE and the code
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:40 — [review] R2-9 the R1-3 doc correction (per-tree gitfiles sit INSIDE the mount) is still absent
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:420 — [review] R2-10 opt-out honours only 'false', not '0', despite the 'same opt-out' comment
  • packages/cli/src/commands/review/test-efficacy.ts:2497 — [review] R2-11 refusal note fires even when the phase had nothing to probe
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:169 — [review] R2-12 (residual) auto-with-no-runtime fall-through is still unpinned
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:40 — [review] R2-13 env-vs-setting merge direction unpinned (expectation now 'required' under tighten-only)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts — [review] R2-14 --workdir unpinned for the install-kind invocations
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:333 — [review] R2-16 image slot in argv unpinned (incl. the DEFAULT_IMAGE-substitution mutant)
  • packages/cli/src/commands/review/test-efficacy.ts:2496 — [review] R2-17 phase-level refusal gates untested in the consumer suites (now incl. the resume-throw arm)
  • packages/cli/src/commands/review/build-test.ts:376 — [review] R2-18 containerised/run wiring untested in the consumer suites
  • packages/cli/src/commands/review/lib/npm-toolchain.ts:803 — [review] R2-19 the 'install' kind argument is unobserved by any test (okExec signature)
  • packages/cli/src/commands/review/test-efficacy.ts:1738 — [review] R2-20 the sandboxed probe-run path is untested end to end
  • packages/cli/src/commands/review/build-test.ts:833 — [review] D3-1 the refused report is cemented as a settled base-build failure by base-tree's marker (anchored on unchanged code — age rule)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:202 — [probe] D3-2 podman runtime passthrough untested — hardcoded-docker mutant ships green (anchored on unchanged code — age rule)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:323 — [probe] D3-3 --init unpinned — deleting it ships green and breaks the timeout kill (anchored on unchanged code — age rule)

Convergence: round 3 posted 16 inline comment(s), 14 of them reported for the first time; the previous round posted 22 (20 new). Findings keep coming back to the same files: packages/cli/src/commands/review/lib/sandboxed-exec.ts (findings in rounds 1, 2; 6 more now); packages/cli/src/commands/review/lib/sandboxed-exec.test.ts (findings in round 2; 5 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179,:315 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment choke point. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on exactly one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. witness: round-2 probe — QWEN_REVIEW_SANDBOX=required with answering docker, runTestDelta driven through the PR's own code: DELTA base-side output 'SECRET-IS:[hunter2-credential]' (the recorded repo command ran directly in the host shell with the full environment); re-verified at 04a6ee1: the private spawnSync is unchanged and the file still has no gate. Fix direction: route test-delta's rerun through build-test's run() (or add the same phase-top gate over the baseline).(中文:R2-21 依旧成立——test-delta 从不经过containment咽喉点:它保留自己的私有 run()(spawnSync shell:true、env:buildRunEnv(process.env))作为默认 exec,文件内没有任何 refuseUnsandboxedPhase/sandboxVerdict/containerised 引用,因此 required 下它会在基础侧以完整环境未沙箱化地重跑记录的失败套件并把归因作为证据发布。第二后果:PR 侧套件在受限容器环境里跑而基础侧在完整宿主环境里跑,环境敏感测试可能恰在一侧翻转,test-delta 会凭容器/宿主环境差异给 PR 制造伪 Critical(或放过真回归)。证据:第 2 轮探针——required+docker 在场,驱动 PR 自身代码的 runTestDelta,基础侧输出 'SECRET-IS:[hunter2-credential]';已在 04a6ee1 复核:私有 spawnSync 未变、仍无门。修复方向:让 test-delta 的重跑经过 build-test 的 run(),或对 baseline 加同样的阶段顶部门。)

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file and the Windows-only case-folding path (R3-21) bite exactly there。

未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped。

收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 15 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 3 轮发布了 16 条行内评论,其中 14 条是首次提出;上一轮发布了 22 条(其中 20 条首次提出)。发现反复回到同一批文件:packages/cli/src/commands/review/lib/sandboxed-exec.ts(第 1、2 轮已出过发现,本轮又有 6 条);packages/cli/src/commands/review/lib/sandboxed-exec.test.ts(第 2 轮已出过发现,本轮又有 5 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179,:315 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment choke point. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on exactly one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. witness: round-2 probe — QWEN_REVIEW_SANDBOX=required with answering docker, runTestDelta driven through the PR's own code: DELTA base-side output 'SECRET-IS:[hunter2-credential]' (the recorded repo command ran directly in the host shell with the full environment); re-verified at 04a6ee1: the private spawnSync is unchanged and the file still has no gate. Fix direction: route test-delta's rerun through build-test's run() (or add the same phase-top gate over the baseline).(中文:R2-21 依旧成立——test-delta 从不经过containment咽喉点:它保留自己的私有 run()(spawnSync shell:true、env:buildRunEnv(process.env))作为默认 exec,文件内没有任何 refuseUnsandboxedPhase/sandboxVerdict/containerised 引用,因此 required 下它会在基础侧以完整环境未沙箱化地重跑记录的失败套件并把归因作为证据发布。第二后果:PR 侧套件在受限容器环境里跑而基础侧在完整宿主环境里跑,环境敏感测试可能恰在一侧翻转,test-delta 会凭容器/宿主环境差异给 PR 制造伪 Critical(或放过真回归)。证据:第 2 轮探针——required+docker 在场,驱动 PR 自身代码的 runTestDelta,基础侧输出 'SECRET-IS:[hunter2-credential]';已在 04a6ee1 复核:私有 spawnSync 未变、仍无门。修复方向:让 test-delta 的重跑经过 build-test 的 run(),或对 baseline 加同样的阶段顶部门。)

— qwen3.8-max via Qwen Code /review (v0.22.0)

// reaches a spawn at all: a repo this adapter cannot scope is handed to the
// AGENT's own shell (`unsupportedReport`), which would otherwise run the
// install and the suite with nothing consulted.
const refusal = refuseUnsandboxedPhase(root);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-1: the round-1/round-2 blocker's hand-off arm is still open at this head. Under required with a runtime ANSWERING and a mountable tree, a repo the npm adapter cannot scope (yarn/pnpm/bun — no package-lock.json — or no package.json) passes this gate (refuseUnsandboxedPhase returns null whenever the verdict is container and the tree is mountable), reaches unsupportedReport with ok: true, and the agent brief's unsupported bullet then instructs the agent to run the reviewed repo's install/build/test in its OWN shell with the full environment — the exact exposure #9556's design exists to remove, under the mode that promises it cannot happen. The gate's own comment below says it sits at phase top exactly because this route "would otherwise run the install and the suite with nothing consulted" — yet under required+runtime+mountable the gate consults nothing about scopeability. The round-3 increment fixed the no-runtime and unmountable arms; this arm remains.

witness: probe (real runtime answering, real mountRootFor, yarn fixture under a real .qwen/tmp layout):
policy in force: required | phase-top gate: null | adapter selected: true
report.toolchain: unsupported | report.ok: true
report.note: 'This is a yarn.lock repo ... Run yarn install --frozen-lockfile first, then fall back to the build/test precedence in your brief' — with exec stubbed to throw, never invoked

Fix: when the verdict is not direct, an unscopeable repo must produce a refusal-shaped report (toolchain: 'refused', ok: false), never an unsupported hand-off; alternatively widen refuseUnsandboxedPhase to model scopeability.

中文说明

R1-1:第 1/2 轮阻断项的交接臂在本 head 上仍然敞开。在 required 且运行时在场、树可挂载时,npm 适配器无法 scope 的仓库(yarn/pnpm/bun——没有 package-lock.json——或没有 package.json)会通过此门(只要判定为 container 且树可挂载,refuseUnsandboxedPhase 就返回 null),到达 ok: trueunsupportedReport,随后 agent 简报的 unsupported 条目指示 agent 在自己的 shell 里以完整环境运行被审仓库的 install/build/test——这正是 #9556 的设计要消除的暴露,发生在承诺不会发生的模式下。下方门自己的注释说它放在阶段顶部正是因为这条路径"否则会在什么都不查询的情况下运行 install 和套件"——可在 required+运行时在场+可挂载下,门对可 scope 性什么都没查。第 3 轮增量修掉了无运行时臂和不可挂载臂;此臂仍在。

证据:探针(真实运行时在场、真实 mountRootFor、真实 .qwen/tmp 布局下的 yarn 夹具):生效策略 required | 阶段顶部门:null | 报告 toolchain: unsupported、ok: true,exec 被置为抛错且从未被调用。

修复:判定不为 direct 时,不可 scope 的仓库必须产出拒绝形态的报告(toolchain: 'refused'ok: false),而不是 unsupported 交接;或让 refuseUnsandboxedPhase 把可 scope 性纳入判定。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// file one run rewrote, an ignored plant a sweep honoured. `--rm` is
// isolation by construction rather than by hygiene.
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-3: still stands at this head — deliberately deferred since round 1, mechanism unchanged, re-verified at this commit. The single RW bind mount covers the whole review temp dir, so it covers every pipeline tree's .git gitfile: containerized PR code can rewrite a gitfile to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge, and the host-side git invocations that follow the redirect (probe-tree creation, scratch trees) execute the planted filter as the reviewing user — host code execution. The module doc's "<repo>/.git stays outside it" is true of the common dir only, not of the per-tree gitfiles inside the mount; the doc correction committed to in round 1 (R2-9) is still absent.

中文说明

R1-3:在本 head 上依旧成立——自第 1 轮起被有意推迟,机制未变,已在本提交复核。唯一的 RW 绑定挂载覆盖整个 review 临时目录,因此覆盖每棵流水线树的 .git gitfile:容器化的 PR 代码可以把 gitfile 改写为 gitdir: <同一挂载内植入的目录> 并带上 filter.<x>.smudge,随后跟随该重定向的宿主侧 git 调用(探针树创建、scratch 树)会以审查用户身份执行植入的 filter——宿主代码执行。模块文档里"<repo>/.git 在挂载之外"只对 common dir 成立,对挂载内的各树 gitfile 不成立;第 1 轮承诺的文档更正(R2-9)仍未落地。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// report is indistinguishable from a contained run. The question the policy
// asks is whether THIS phase can be contained, and the mount is the half
// that can fail while the runtime is healthy.
if (mountRoot(root) === null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] This mount-failure refusal fires under auto too, not only required, and the reason hardcodes review.sandbox is "required". Under auto with a runtime answering, a /review of a local checkout (cwd not under .qwen/tmp) gets verdict container, this branch fires, and both consumers (build-test.ts:814, test-efficacy.ts:2496) refuse the whole phase — zero build/test evidence — blaming a policy the operator never set. That contradicts auto's documented semantics in this same module ("use a container when one is available, run directly when not"), containerised()'s docstring naming the local-checkout case as direct-run, and the asymmetry that auto with NO runtime runs directly with disclosure.

witness: probe through the real module:
verdict(auto, runtime answering) = {"kind":"container","runtime":"docker"}
refuseUnsandboxedPhase(localCheckout, autoVerdict) = 'review.sandbox is "required" and this tree cannot be mounted: ...'
asymmetry arm: verdict(auto, no runtime) = {"kind":"direct",...}; gate(localCheckout, that) = null
Suggested change
if (mountRoot(root) === null) {
if (policy === 'required' && mountRoot(root) === null) {

(pass the policy in, and let the message name the policy actually in force; under auto fall through to the documented direct-with-disclosure path).

中文说明

这个挂载失败拒绝在 auto 下同样触发,而不只是 required,且理由文本硬编码了 review.sandbox is "required"。在 auto 且运行时在场时,对本地检出的 /review(cwd 不在 .qwen/tmp 之内)得到 container 判定,此分支触发,两个消费者(build-test.ts:814、test-efficacy.ts:2496)拒绝整个阶段——零构建/测试证据——并把责任推给操作者从未设置的策略。这与同一模块里 auto 的文档语义("有容器则用容器,否则直接运行")、containerised() 文档把本地检出列为直跑情形、以及"auto 无运行时反而直跑并披露"的不对称相矛盾。

证据:对真实模块的探针:verdict(auto, 运行时在场) = container;refuseUnsandboxedPhase(本地检出, auto 判定) 返回 'review.sandbox is "required" and this tree cannot be mounted...';不对称臂:auto 无运行时为 direct,门返回 null。

修复:仅当策略为 required 时拒绝挂载失败(把策略传入),消息写明实际生效的策略;auto 落回文档化的"直跑+披露"路径。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// isolation by construction rather than by hygiene.
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,
'--workdir',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] --workdir receives the lexical resolve(cwd) while this round's mountRootFor change makes the bind mount use realpathSync(root) (next line's opts.tmpDir). When any ancestor above the repo's parent is a symlink — the span redirectedAncestor deliberately stops before — the lexical workdir does not exist under the realpath'd mount inside the container. Both call sites pass lexical cwds (containerised: cwd: resolve(cwd); probeContainer: resolve(probeTree)). Regression introduced THIS round — the pre-round lexical mount matched the lexical workdir.

witness: fixture base/link -> base/real, repo under link/sub:
mountRootFor() = /tmp/r32.../real/sub/repo/.qwen/tmp
--workdir       = /tmp/r32.../link/sub/repo/.qwen/tmp/review-pr-1
workdir inside mount? false | realpath(cwd) inside mount? true (the implied one-line fix flips it)
live docker arm (exact argv shape): docker auto-created the lexical path as an EMPTY root-owned dir
while the planted package.json is visible only under the real mount path

Consequence: every sandboxed npm ci/build/test runs in the wrong empty directory — fabricated failure evidence attributed to the PR, probes map inconclusive, and with --user uid:gid the mapped uid cannot even write the root-owned dir.

Fix: canonicalise on the same axis as the mount — realpath the cwd before handing it to containerCommand (verifying it stays under tmpDir), or mount the real root at the lexical path.

中文说明

--workdir 拿到的是词法 resolve(cwd),而本轮 mountRootFor 的改动让绑定挂载改用 realpathSync(root)(下一行的 opts.tmpDir)。当仓库父目录之上的任何祖先是符号链接时——正是 redirectedAncestor 有意不检查的那段——词法 workdir 在容器内并不位于 realpath 化后的挂载之下。两个调用点都传词法 cwd(containerisedcwd: resolve(cwd)probeContainerresolve(probeTree))。这是本轮引入的回归——改动前词法挂载与词法 workdir 是一致的。

证据:夹具 base/link -> base/real、仓库在 link/sub 下:mountRootFor() 返回 real 路径,--workdir 是 link 路径,workdir 不在挂载内(realpath(cwd) 在);真实 docker 臂:docker 把词法路径自动创建为空的 root 属主目录,植入的 package.json 只在真实挂载路径下可见。

后果:每个容器化的 npm ci/构建/测试都跑在错误的空目录里——伪造的失败证据被记到 PR 头上,探针全部 inconclusive,且带 --user uid:gid 时映射用户连那个 root 属主目录都写不了。

修复:与挂载同轴规范化——把 cwd realpath 化后再交给 containerCommand(并校验仍在 tmpDir 之下),或把真实路径挂载到词法路径上。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +484 to +485
'DOCKER_CONTEXT',
'CONTAINER_HOST',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The scrub misses the config-INDIRECTION daemon selectors: docker's DOCKER_CONFIG — and, at unit level, podman's CONTAINERS_CONF/CONTAINERS_REGISTRIES_CONF (podman steering not exercised — not installed on this runner). A repo-shipped .qwen/.env with DOCKER_CONFIG=.qwen/dockercfg plus committed config.json (currentContext) and contexts/meta/<sha>/meta.json (attacker endpoint) redirects the availability probe AND every docker run at an attacker-controlled daemon: required reads as satisfied, the RW mount spec is handed over, and all build/test/probe evidence is fabricated. loadEnvironment admits the key (it is in no exclusion list) and marks it file-sourced, but this loop deletes only its five listed keys.

witness: end-to-end probe (planted config dir + fake TLS daemon):
CONTROL (no DOCKER_CONFIG): docker info -> 29.1.3, exit=0; fake.log: (empty)
ATTACK (DOCKER_CONFIG=<planted>): docker info -> 99.9.9-fake, exit=0; fake.log: HEAD /_ping, GET /v1.48/info
docker run arm: fake.log received POST /v1.48/containers/create
unit probe: DOCKER_CONFIG/CONTAINERS_CONF/CONTAINERS_REGISTRIES_CONF all survive runtimeClientEnv();
adding them to this list flips the probe
Suggested change
'DOCKER_CONTEXT',
'CONTAINER_HOST',
'DOCKER_CONTEXT',
'CONTAINER_HOST',
'DOCKER_CONFIG',

(and CONTAINERS_CONF / CONTAINERS_REGISTRIES_CONF for the podman arm — file-sourced-only deletion preserves an operator's genuine values).

中文说明

清除列表漏掉了配置间接类的守护进程选择器:docker 的 DOCKER_CONFIG——以及单元层面 podman 的 CONTAINERS_CONF/CONTAINERS_REGISTRIES_CONF(podman 的操纵未实测——本机未安装)。仓库自带的 .qwen/.env 写入 DOCKER_CONFIG=.qwen/dockercfg,加上提交的 config.jsoncurrentContext)与 contexts/meta/<sha>/meta.json(攻击者端点),就能把可用性探测和每一次 docker run 重定向到攻击者控制的守护进程:required 读作已满足,RW 挂载规格被交出,全部构建/测试/探针证据被伪造。loadEnvironment 会接纳该键(不在任何排除清单)并标记为文件来源,但此循环只删除列出的五个键。

证据:端到端探针(植入配置目录+假 TLS 守护进程):对照(无 DOCKER_CONFIG)docker info -> 29.1.3、fake.log 为空;攻击(植入 DOCKER_CONFIG)docker info -> 99.9.9-fake、fake.log 收到 HEAD /_ping、GET /v1.48/info;docker run 臂收到 POST /v1.48/containers/create。单元探针:三个间接键都穿过 runtimeClientEnv();加入本列表即可翻转。

修复:把 DOCKER_CONFIG(以及 podman 的 CONTAINERS_CONF/CONTAINERS_REGISTRIES_CONF)加入此列表——仅删文件来源值,操作者自己的值保留。

— qwen3.8-max via Qwen Code /review (v0.22.0)

const elsewhere = tmp();
mkdirSync(join(elsewhere, 'tmp', 'review-pr-1'), { recursive: true });
mkdirSync(join(root, '.qwen'), { recursive: true });
symlinkSync(join(elsewhere, 'tmp'), join(root, '.qwen', 'tmp'));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This suite plants the redirect at exactly one of the four positions the walk lstats (mount root, .qwen, repo root, repo's parent); a mutation shrinking the walk to the mount root alone ships all 18 tests green. The production shape the depth exists for: a PR committing .qwen itself as a mode-120000 symlink — a fresh clone materialises it, the pipeline's mkdirSync follows it and builds the layout at the link's target, and the shrunken walk then clears mountRootFor, handing a RW bind mount to the link's target (with .qwen. that target is the clone root, pulling <clone>/.git into the mount — the widening this test's own comment says the guard exists to stop).

witness: mutation redirectedAncestor(root, root) [walk = mount root alone] -> suite 18 passed (18)
probe with the link planted at .qwen: mountRootFor -> '/tmp/r313-.../tmp' (mount proceeds) on the mutant; null on the real walk

Fix: add a case planting the symlink one level up (.qwen → an outside dir containing inner/tmp/review-pr-1) and expect null; optionally one more with the link at the repo-root component.

中文说明

本套件只在遍历会 lstat 的四个位置(挂载根、.qwen、仓库根、仓库父目录)中一个植入了重定向;把遍历收缩到仅挂载根的突变能让全部 18 个测试绿。这个深度存在的产品形态:PR 把 .qwen 本身提交为 mode 120000 的符号链接——新克隆会把它实体化,流水线的 mkdirSync 跟随它并在链接目标处建立布局,收缩后的遍历会让 mountRootFor 放行,把 RW 绑定挂载交给链接目标(.qwen. 时目标就是克隆根,把 <clone>/.git 拉进挂载——正是本测试注释说该守卫要阻止的展宽)。

证据:突变(遍历仅剩挂载根)-> 套件 18 通过;链接植入在 .qwen 的探针:突变下 mountRootFor 返回被重定向的临时目录(挂载继续),真实遍历下为 null。

修复:补一个把符号链接植入上一层(.qwen → 含 inner/tmp/review-pr-1 的外部目录)并期望 null 的用例;可再加一个链接在仓库根组件的用例。

— qwen3.8-max via Qwen Code /review (v0.22.0)

isFileSourcedEnvKey(key) ? undefined : env[key]?.trim();
return (
pick('QWEN_REVIEW_SANDBOX_IMAGE') ||
pick(CUSTOM_SANDBOX_IMAGE_ENV_VAR) ||

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This second pick is untested in both directions — no test anywhere sets QWEN_CODE_CUSTOM_SANDBOX_IMAGE for reviewSandboxImage. Dropping the file-source guard from ONLY this branch ships green: a repo committing that key in .qwen/.env then still names the image the reviewed code executes in — the "the image IS the code" hole this round closed for QWEN_REVIEW_SANDBOX_IMAGE stays open one fallback down. (Dropping the pick entirely also ships green and silently loses the operator's qwen --sandbox image parity.)

witness: mutation (second branch unguarded): suite 18 passed (18)

Fix: add cases — the key set as a process variable is returned when QWEN_REVIEW_SANDBOX_IMAGE is absent; stubbed file-sourced via the spy, it falls through to the default.

中文说明

第二个取值分支双向都无测试——没有任何测试为 reviewSandboxImage 设置过 QWEN_CODE_CUSTOM_SANDBOX_IMAGE。只删掉这个分支的文件来源守卫即可绿通过:仓库在 .qwen/.env 里提交该键就仍能指定被审代码运行所用的镜像——本轮为 QWEN_REVIEW_SANDBOX_IMAGE 关上的"镜像即代码"漏洞在下一级回退上仍敞开。(整个删掉该取值同样绿,且悄悄丢掉操作者 qwen --sandbox 的镜像一致性。)

证据:突变(第二分支去守卫):套件 18 通过。

修复:补用例——QWEN_REVIEW_SANDBOX_IMAGE 缺省时进程变量设置的该键被返回;经 spy 标记为文件来源时落回默认镜像。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// ...and the WIRING, not just `containerEnv` called with a literal: HOME
// must be the tmpfs the argv also declares, or the mapped uid has no
// writable home and npm fails before the install starts.
expect(passed).toContain(`HOME=${CONTAINER_HOME}`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The wiring test pins HOME against the argv but leaves the sibling npm_config_cache wiring unpinned — containerEnv is called directly with a literal in the array-equality test, never with the value containerCommand actually passes (join(opts.tmpDir, '.npm-cache')). Mutating that call argument to a path outside the mount ships 18/18 green: with the pinned --user uid:gid, npm cannot create a cache dir under root-owned / (EACCES on every sandboxed install), or the cache-in-mount property the module documents is silently lost.

witness: mutation containerEnv('/root/.npm-cache'): suite 18 passed
probe asserting the on-mount cache path flipped — Expected 'npm_config_cache=/repo/.qwen/tmp/.npm-cache', Received 'npm_config_cache=/root/.npm-cache'; pristine 19/19

Fix: extend this assertion so the cache is pinned to the mounted tmpDir:

Suggested change
expect(passed).toContain(`HOME=${CONTAINER_HOME}`);
expect(passed).toContain(`HOME=${CONTAINER_HOME}`);
expect(passed).toContain(`npm_config_cache=${join(tmpDir, '.npm-cache')}`);
中文说明

接线测试把 HOME 钉到了 argv 上,但兄弟 npm_config_cache 接线没钉——数组等价测试是用字面量直接调用 containerEnv,从不用 containerCommand 实际传入的值(join(opts.tmpDir, '.npm-cache'))。把那个实参突变为挂载外的路径,18/18 全绿:在已钉住的 --user uid:gid 下,npm 无法在 root 属主的 / 下创建缓存目录(每次容器化安装都 EACCES),或者模块文档所载的"缓存在挂载内"性质被悄悄丢掉。

证据:突变 containerEnv('/root/.npm-cache'):套件 18 通过;钉住挂载内缓存路径的探针翻转;原代码 19/19。

修复:扩展此断言——expect(passed).toContain(``npm_config_cache=${join(tmpDir, '.npm-cache')}``),把缓存钉在挂载的 tmpDir 上。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment thread packages/cli/src/config/environment.ts Outdated
* variable and not file-sourced.
*/
export function isFileSourcedEnvKey(key: string): boolean {
return dotEnvSourcedKeys.has(key) || settingsEnvSourcedKeys.has(key);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This seam cannot distinguish OPERATOR-owned user-level .env files (~/.env, <globalQwenDir>/.env, legacy ~/.qwen/.envfindEnvFiles loads unconditionally, no trust gate) from repository content: the loader marks every key it writes with no file-scope distinction, and the three new security read sites treat "file-sourced" as "repository-sourced". An operator-set QWEN_REVIEW_SANDBOX=required in a user-level .env is silently dropped by sandboxPolicy and the policy degrades to the setting or off with no disclosure; the same marking scrubs an operator's DOCKER_HOST (colima/rootless) and QWEN_REVIEW_SANDBOX_IMAGE set there. Fails SAFE against the repository — silent operator degradation, not an exploit — but the module's own comment ("the operator's routes remain their settings file and their real shell environment") is narrower than the CLI's actual operator routes.

witness: real-loader probe, hermetic HOME, same values on two operator routes:
[arm A: shell export] policy = 'required'; DOCKER_HOST kept; image kept
[arm B: ~/.qwen/.env] loader wrote 'required', isFileSourcedEnvKey = true -> policy = 'off'; DOCKER_HOST scrubbed; image default

Fix: track file scope in the provenance sets (only repository-scoped files feed the security-decision set, or keep a separate home-sourced set); at minimum disclose on stderr when a file-sourced QWEN_REVIEW_SANDBOX is being ignored.

中文说明

这个接缝分不清操作者属主的用户级 .env 文件(~/.env<globalQwenDir>/.env、遗留 ~/.qwen/.env——findEnvFiles 无条件加载、无信任关卡)与仓库内容:加载器对写入的每个键不做文件范围区分地打标,三个新的安全读取点把"文件来源"当作"仓库来源"。操作者在用户级 .env 里设置的 QWEN_REVIEW_SANDBOX=required 会被 sandboxPolicy 悄悄丢弃,策略降级为设置值或 off 且无任何披露;同一标记还会清掉操作者设在那里的 DOCKER_HOST(colima/rootless)与 QWEN_REVIEW_SANDBOX_IMAGE。对仓库方向是安全失败——是操作者意图的静默降级,不是可利用漏洞——但模块自己的注释("操作者的途径是其设置文件和真实 shell 环境")比 CLI 实际的操作者途径要窄。

证据:真实加载器探针、密封 HOME、两条操作者途径同值:[臂 A:shell 导出] 策略 'required'、DOCKER_HOST 保留;[臂 B:~/.qwen/.env] 加载器写入 'required' 且被标记文件来源 -> 策略 'off'、DOCKER_HOST 被清、镜像落回默认。

修复:在来源集合中记录文件范围(仅仓库范围文件进入安全决策集合,或单列家目录来源集合);至少在文件来源的 QWEN_REVIEW_SANDBOX 被忽略时向 stderr 披露。

— qwen3.8-max via Qwen Code /review (v0.22.0)

cwd: join(tmpDir, 'review-pr-9'),
kind: 'install',
});
const passed = args.filter((_, i) => args[i - 1] === '--env');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The canary inspects only the VALUES of the forwarded --env entries, never the SET: a regression to valueless --env KEY forwarding — which docker/podman resolve from the client process's own environment — ships the suite green while the inherited environment still crosses the boundary (--env OPENAI_API_KEY resolves from the client env into the reviewed code's postinstall). That is the natural shape of exactly the regression the adjacent comment says this test guards against.

witness: mutant (valueless forwarding of process.env): suite 18 passed (18)
mutant + set-pinning assertion: 1 failed
pristine + set-pinning: 18 passed

Fix: pin the full set — expect([...passed].sort()).toEqual([...containerEnv(join(tmpDir, '.npm-cache'))].sort()) — or additionally assert every entry matches /^[A-Za-z_][A-Za-z0-9_]*=/ so any valueless forward fails.

中文说明

金丝雀只检查转发 --env 条目的,从不检查集合:退化为无值 --env KEY 转发——docker/podman 会从客户端进程自身环境解析——整套测试绿通过,而继承环境仍然越过边界(--env OPENAI_API_KEY 从客户端环境解析进被审代码的 postinstall)。这正是相邻注释声称本测试要防的退化的自然形态。

证据:突变(无值转发 process.env):套件 18 通过;突变+集合钉住断言:1 失败;原代码+集合钉住:18 通过。

修复:钉住完整集合——expect([...passed].sort()).toEqual([...containerEnv(join(tmpDir, '.npm-cache'))].sort())——或再断言每个条目匹配 /^[A-Za-z_][A-Za-z0-9_]*=/,使任何无值转发失败。

— qwen3.8-max via Qwen Code /review (v0.22.0)

…at their edges

Round 3. Six Criticals, all of them real, and two were introduced by round 2's
own fixes.

- **The hand-off arm of the original blocker was still open.** The phase gate
  refuses when containment is impossible — but a repo the npm adapter cannot
  scope (yarn/pnpm/bun, no lockfile) reaches `unsupportedReport` with the gate
  satisfied: a runtime answered and the tree is mountable. That report tells the
  agent to install and build with its own shell, which nothing here contains. An
  inapplicable adapter is now a refusal under `required`, not a hand-off.
- **The mount-failure refusal fired under `auto` too**, and its message
  hardcoded `required`. Under `auto` the contract is "contain it when that is
  possible", so an unmountable tree falls back to the direct spawn — refusing
  there would have taken build/test and efficacy evidence away from every local
  review the moment a daemon happened to be running. Mine, from round 2.
- **`--workdir` got the lexical path while the mount got the realpath.** Round
  2 made `mountRootFor` canonicalise; the workdir did not follow, so on any
  layout where the two spellings differ — `/var` against `/private/var` is the
  everyday one — the container was handed a directory it does not have and
  every command would fail before starting. Also mine, from round 2.
- **The daemon scrub missed the indirection selectors.** `DOCKER_CONFIG`,
  `CONTAINERS_CONF` and friends name a config file that in turn names the
  daemon, the registries and the runtime: scrubbing the direct selectors and
  leaving these moves the same steering one level down.
- **`isFileSourcedEnvKey` matched case-sensitively.** Windows env lookup is
  case-insensitive, so a `.env` committed as `docker_host=…` reaches the child
  exactly as `DOCKER_HOST` would while the exact-case test answers "not from a
  file" about a value that is. Same class as `sanitizedGitEnv`'s case fold.

The gitfile finding stays deferred with its reasoning in the thread — it is not
a hole this PR opens, and closing it belongs to the identity gates rather than
to the sandbox.

Two mutations came back green on the first pass and were the useful part of
this round: the `DOCKER_CONFIG` scrub had no test because the fixture pinned one
key rather than the set, and the operator's `SANDBOX_SET_UID_GID=false` opt-out
had no test at all — both uid tests asserted the flag was PRESENT. The scrub
test now asserts the whole set, and the opt-out has its own.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R3-8 redirectedAncestor walk climbs one level higher than its siblings — already reported (comment 3837175064)

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform arms and the win32 case-fold path bite exactly there.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped.

Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:161 — [review] D4-1 resetContainerRuntimeProbe has no consumers; the containerRuntime probe is untested

Convergence: round 4 posted 14 inline comment(s), 4 of them reported for the first time; the previous round posted 16 (14 new). Findings keep coming back to the same files: packages/cli/src/commands/review/lib/sandboxed-exec.ts (findings in rounds 1, 3; 2 more now); packages/cli/src/commands/review/build-test.ts (findings in round 1; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on exactly one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. witness: [probe] re-run at this head — QWEN_REVIEW_SANDBOX=required stubbed + PROBE_SECRET in env, real runTestDelta (default exec) reran the recorded failing npm test base-side in a fixture baseline whose test script echoes the secret: baseOutput "... SECRET-IS:[hunter2-credential] ...", baseExitCode 1 — the reviewed repo's code ran in the host shell with the full environment under the mode that forbids it. Fix direction: route test-delta's rerun through build-test's run() (or add the same phase-top gate over the baseline).

中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform arms and the win32 case-fold path bite exactly there。

未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped。

收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 4 轮发布了 14 条行内评论,其中 4 条是首次提出;上一轮发布了 16 条(其中 14 条首次提出)。发现反复回到同一批文件:packages/cli/src/commands/review/lib/sandboxed-exec.ts(第 1、3 轮已出过发现,本轮又有 2 条);packages/cli/src/commands/review/build-test.ts(第 1 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on exactly one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. witness: [probe] re-run at this head — QWEN_REVIEW_SANDBOX=required stubbed + PROBE_SECRET in env, real runTestDelta (default exec) reran the recorded failing npm test base-side in a fixture baseline whose test script echoes the secret: baseOutput "... SECRET-IS:[hunter2-credential] ...", baseExitCode 1 — the reviewed repo's code ran in the host shell with the full environment under the mode that forbids it. Fix direction: route test-delta's rerun through build-test's run() (or add the same phase-top gate over the baseline).

— qwen3.8-max via Qwen Code /review (v0.22.0)

// gate above passes whenever a runtime answers and the tree is mountable,
// which is exactly when a yarn/pnpm/bun repo still reaches this branch. So
// an inapplicable adapter is a refusal under that policy, not a hand-off.
if (!applicable && sandboxPolicy() === 'required') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-1: the round-4 gate added to close the hand-off arm is dead code — applicable is the readonly ReviewToolchainAdapter[] returned by selectToolchainAdapter, so !applicable is false for an empty and a non-empty array alike, and both unscopeable-repo routes still reach the toolchain: 'unsupported', ok: true hand-off under required. Witness: with QWEN_REVIEW_SANDBOX=required, an answering docker and a mountable tree, driving the real runBuildTest returns {"toolchain":"unsupported","ok":true} for both route A (no package.json — applicable is []) and route B (a yarn.lock repo where the adapter applies but concedes internally); patching the condition to applicable.length === 0 flips route A to refused/ok:false while route B stays unsupported — the npm adapter applies to yarn repos and concedes inside runNpmToolchain, so a length-only fix does not close the blocker's witness shape. The brief's unsupported rule then sends the agent to run the reviewed repository's install/build/test in its own shell with the full environment — the exact execution required forbids, reported as a clean hand-off. Fix by intercepting the outcome instead of the selection: under required, convert any returned report with toolchain === 'unsupported' into refusedReport(report.note), and throw rather than return on --resume, per the continuation invariant the adjacent branch states.

中文说明

R1-1:本轮为关闭交接臂新增的门是死代码——applicableselectToolchainAdapter 返回的 readonly ReviewToolchainAdapter[],数组永远为真值,空与非空时 !applicable 都是 false,因此在 required 下两条「适配器无法 scope」的路径仍会到达 toolchain: 'unsupported'ok: true 的交接。证据:QWEN_REVIEW_SANDBOX=required、docker 在场、树可挂载时驱动真实 runBuildTest,路线 A(无 package.json——applicable[])与路线 B(yarn.lock 仓库,适配器适用但在内部让步)都返回 {"toolchain":"unsupported","ok":true};把条件改成 applicable.length === 0 后路线 A 翻转为 refused/ok:false,路线 B 仍是 unsupported——npm 适配器对 yarn 仓库适用、在 runNpmToolchain 内部让步,因此只改长度判断关不掉阻断项的证据形态。随后简报的 unsupported 规则会指示 agent 在自己的 shell 里以完整环境运行被审仓库的 install/build/test——正是 required 所禁止的执行,却被报告为一次干净的交接。修复请拦截结果而非选择:required 下把任何 toolchain === 'unsupported' 的返回报告转成 refusedReport(report.note),并在 --resume 时抛错而不是返回,与相邻分支写明的续跑不变量一致。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// file one run rewrote, an ignored plant a sweep honoured. `--rm` is
// isolation by construction rather than by hygiene.
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-3: still stands at this head — deliberately deferred since round 1, mechanism unchanged, re-checked at this commit. The single RW bind mount covers the whole review temp dir, so it covers every pipeline tree's .git gitfile: containerized PR code can rewrite a gitfile to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge, and the host-side git invocations that follow the redirect (probe-tree restore, scratch-tree reset) give host code execution. The mount spec is byte-identical to what rounds 1–3 probed live, and this round's diff does not touch the mechanism. witness: not run this round — the round-1/round-2 live probes are recorded in the earlier threads and the code they exercised is unchanged. Either close this arm (mask each tree's gitfile from the writable surface, or make the host-side consumers refuse in-mount redirects) or hold it explicitly out of the PR's containment claims.

中文说明

R1-3:在本 head 上依旧成立——自第 1 轮起被有意推迟,机制未变,已在该提交复核。单一 RW 绑定挂载覆盖整个 review 临时目录,因此覆盖每棵流水线树的 .git gitfile:容器内的 PR 代码可以把 gitfile 改写为 gitdir: <同一挂载下的植入目录>,其中携带 filter.<x>.smudge,随后跟随该重定向的宿主侧 git 调用(探针树恢复、scratch 树复位)即形成宿主代码执行。挂载规格与第 1–3 轮活体探针所验证的代码逐字节一致,本轮 diff 未触碰该机制。证据:本轮未重跑——第 1/2 轮的活体探针记录在更早的讨论串中,其验证过的代码未变。要么关闭此臂(把各树 gitfile 从可写面上屏蔽,或让宿主侧消费者拒绝挂载内重定向),要么在 PR 的 containment 声明中明确把它排除在外。

— qwen3.8-max via Qwen Code /review (v0.22.0)

'CONTAINERS_REGISTRIES_CONF',
'CONTAINERS_STORAGE_CONF',
]) {
if (isFileSourcedEnvKey(key)) delete scrubbed[key];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R3-4: only half-fixed — the case-fold added this round answers the query half (a case-variant committed key now reports file-sourced on Windows), but this delete remains exact-case on a plain object: the spread { ...env } carries the file's casing, so for a tracked docker_host the delete of the canonical DOCKER_HOST is a no-op and the value still crosses to the runtime client. Witness: driving the real (platform-free) delete half with a tracked case-variant key, the PR unmodified returns {"docker_host":"tcp://attacker:2375","keys":["PATH","docker_host"]} — the attacker value reaches the docker info probe and every boxed spawn — while deleting the tracked spelling flips it to {"keys":["PATH"]} (the win32 fold branch and Windows' case-insensitive child-env lookup are modeled, declared). A repo committing docker_host=tcp://attacker:2375 in .qwen/.env then steers the availability probe and every container spawn at a daemon it controls on Windows — required reads as satisfied and fabricated evidence comes back scored. Delete the tracked spelling, not the canonical one (on win32 iterate Object.keys(scrubbed) and match case-insensitively when isFileSourcedEnvKey holds — sibling precedent: sanitizedGitEnv in worktree.ts folds case for exactly this reason), and add a test seeding the tracker with a case-variant key.

中文说明

R3-4:只修了一半——本轮新增的大小写折叠解决了查询半边(Windows 上大小写变体的提交键现在会报告为文件来源),但这里的删除仍按规范拼写精确匹配普通对象:展开 { ...env } 携带的是文件里的大小写,因此对已追踪的 docker_host,删除规范形 DOCKER_HOST 是空操作,该值仍会进入运行时客户端。证据:用一个已追踪的大小写变体键驱动真实的(平台无关的)删除半边,未修改的 PR 返回 {"docker_host":"tcp://attacker:2375","keys":["PATH","docker_host"]}——攻击者值到达 docker info 探测与每一次容器化 spawn——改为删除被追踪的拼写后翻转为 {"keys":["PATH"]}(win32 折叠分支与 Windows 大小写不敏感的子进程环境查找为建模,已声明)。仓库在 .qwen/.env 提交 docker_host=tcp://attacker:2375 即可在 Windows 上把可用性探测与所有容器 spawn 指向其控制的守护进程——required 读作已满足,伪造证据被当作结果评分。请删除被追踪的拼写而不是规范拼写(win32 上遍历 Object.keys(scrubbed),在 isFileSourcedEnvKey 成立时大小写不敏感匹配——先例:worktree.ts 的 sanitizedGitEnv 正是为此折叠大小写),并新增一个用大小写变体键填充追踪集合的测试。

— qwen3.8-max via Qwen Code /review (v0.22.0)

for (const entry of containerEnv(join(opts.tmpDir, '.npm-cache'))) {
args.push('--env', entry);
}
args.push(opts.image, 'sh', '-lc', command);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] A boxed run's timeout kills only the runtime client: the container has no identity (no --name, nothing captured) and there is no rm -f/kill anywhere in the pipeline — --rm fires only when the container exits on its own — so a suite that ignores SIGTERM persists with the mounted review temp dir still writable, past the budget and past the end of the review. Witness (live docker 29.1.3, the module's exact argv shape, a TERM-ignoring workload): BEFORE-KILL RUNNING: 8aeb1efdaab2 Up 3 seconds; kill -TERM on the client pid → the client is still alive; AFTER-KILL RUNNING: 8aeb1efdaab2 Up 6 seconds with a heartbeat file advancing inside the RW mount — on this docker version the attached client forwards/waits instead of dying, so spawnSync's deadline does not even return (--init does not help: the suite's own trap ignores the forwarded TERM). A PR committing process.on('SIGTERM', () => {}) in vitest.config.ts plus a suite that never finishes accumulates one orphan per malicious review on persistent runners — the review worktree other agents are reading, sibling probe trees and the shared npm cache stay writable by it. Give each container a unique --name and best-effort rm -f it on timeout/signal in both run() and runProbeSuite() — enforce the deadline at the daemon, not at the client.

中文说明

容器化运行的超时只会杀死运行时客户端:容器没有身份(无 --name,也未捕获任何 ID),整个流水线没有任何 rm -f/kill——--rm 只在容器自行退出时生效——因此一个忽略 SIGTERM 的套件会带着仍然可写的 review 临时目录挂载持续存在,超出预算、也超出审查结束时间。证据(真实 docker 29.1.3,模块的完整 argv 形态,忽略 TERM 的负载):杀死前 RUNNING: 8aeb1efdaab2 Up 3 seconds;对客户端 pid 执行 kill -TERM → 客户端仍存活;杀死后 RUNNING: 8aeb1efdaab2 Up 6 seconds,RW 挂载内的心跳文件持续推进——该 docker 版本下附着客户端会转发/等待而不退出,spawnSync 的截止甚至不会返回(--init 无用:套件自己的 trap 忽略转发的 TERM)。PR 在 vitest.config.ts 提交 process.on('SIGTERM', () => {}) 加一个永不结束的套件,即可在持久 runner 上按恶意审查累积孤儿容器——其他 agent 正在读取的审查工作树、兄弟探针树与共享 npm 缓存都继续可写。请给每个容器唯一 --name,并在超时/信号时于 run()runProbeSuite() 两处尽力 rm -f——截止应在守护进程层面执行,而不是客户端。

— qwen3.8-max via Qwen Code /review (v0.22.0)

const suite = `node ${shellQuotePath(
findVitestBin(dependencyRoot),
)} run --reporter=json ${probes.map(shellQuotePath).join(' ')}`;
const boxed = probeContainer(suite, probeTree);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] Boxed probe runs mount the review temp dir in its CANONICAL (realpath'd) spelling — mountRootFor returns realpathSync(root), the mount and containerPathFor's --workdir use it — but every dependency-farm symlink the probe tree imports through keeps the LEXICAL spelling (exposeDependencies builds the targets from the un-canonicalised worktree argument). Whenever the checkout sits under a symlinked ancestor above redirectedAncestor's stop node — the macOS /tmp → /private/tmp case this module's own comments name — every farm link dangles inside the container and the containerised efficacy phase deterministically yields zero evidence. Witness (fixture alias → real, repo under alias/work/myrepo, real container): MOUNT …/real/work/myrepo/.qwen/tmp (canonical); farm target …/alias/…/node_modules/left-pad (lexical); with the PR's wiring the link DANGLES, and rebuilding the farm via exposeDependencies(probeTree, realpathSync(worktree)) makes it RESOLVE — the flip confirms the fix. As shipped, the baseline vitest run collects only import errors and mutants/hunks are skipped with "every file was red or collected nothing" — a wiring failure published as a statement about the PR's own suite, for every boxed review of such a checkout. Canonicalise what crosses the boundary (pass the realpath'd dependency root into exposeDependencies from the sandboxed path, or symlink to realpathSync(...) targets in farmNodeModules); scratch-tree shares the farm.

中文说明

容器化探针运行把 review 临时目录按其规范(realpath 化)拼写挂载——mountRootFor 返回 realpathSync(root),挂载与 containerPathFor--workdir 都用它——但探针树借以导入依赖的每条依赖 farm 符号链接仍保留词法拼写(exposeDependencies 用未规范化的 worktree 参数构造链接目标)。只要检出位于 redirectedAncestor 停止节点之上的符号链接祖先之下——正是本模块注释点名的 macOS /tmp → /private/tmp 常见情形——容器内每条 farm 链接都悬空,容器化的 efficacy 阶段确定性地零证据。证据(夹具 alias → real,仓库在 alias/work/myrepo 下,真实容器):挂载 …/real/work/myrepo/.qwen/tmp(规范);farm 目标 …/alias/…/node_modules/left-pad(词法);按 PR 接线链接悬空,用 exposeDependencies(probeTree, realpathSync(worktree)) 重建 farm 后链接解析成功——翻转确认修复方向。按现状,基线 vitest 运行只收集到导入错误,mutant/hunk 以 "every file was red or collected nothing" 被跳过——把接线失败发布为对 PR 套件本身的论断,且对此类检出的每次容器化审查都如此。请把跨界的东西规范化(容器化路径向 exposeDependencies 传 realpath 化的依赖根,或在 farmNodeModules 里链接到 realpathSync(...) 目标);scratch-tree 共用该 farm。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// ...and the WIRING, not just `containerEnv` called with a literal: HOME
// must be the tmpfs the argv also declares, or the mapped uid has no
// writable home and npm fails before the install starts.
expect(passed).toContain(`HOME=${CONTAINER_HOME}`);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R3-12: still stands — the wiring test pins HOME against the argv but leaves the sibling npm_config_cache wiring unpinned: containerEnv is called directly with a literal in the array-equality test, never with the value containerCommand actually computes. A mutation dropping or misspelling the cache entry in the argv loop ships green — the container would re-download ~1 700 packages per review or write the cache outside the mount, surfacing as PR-attributed slowness/failure rather than a wiring error. Extend the assertion with the computed value: expect(passed).toContain(\npm_config_cache=${join(tmpDir, '.npm-cache')}`)`.

中文说明

R3-12:依旧成立——接线测试把 HOME 与 argv 对钉,却漏了兄弟 npm_config_cache 的接线:数组相等测试里是直接拿字面量调用 containerEnv,从不是 containerCommand 实际计算出的值。在 argv 循环里删掉或写错缓存条目的变异可以绿着上线——容器将每次审查重新下载约 1 700 个包,或把缓存写到挂载之外,最终以归咎于 PR 的慢/失败显现,而不是接线错误。请把断言扩展到计算值:expect(passed).toContain(\npm_config_cache=${join(tmpDir, '.npm-cache')}`)`。

— qwen3.8-max via Qwen Code /review (v0.22.0)

* variable and not file-sourced.
*/
export function isFileSourcedEnvKey(key: string): boolean {
if (dotEnvSourcedKeys.has(key) || settingsEnvSourcedKeys.has(key)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R3-13: still stands — this seam cannot distinguish OPERATOR-owned user-level .env files (~/.env, <globalQwenDir>/.env — loaded unconditionally, no trust gate) from repository content: both are marked file-sourced, so an operator's own docker_host / containment knobs kept in a user-level .env are scrubbed or ignored exactly like a repo-planted one — the daemon probe fails and auto silently degrades to direct runs (or required refuses), with nothing naming the cause. Record the provenance scope when populating the tracking sets (user-level vs repository-path .env) and answer file-sourced only for repository-reachable files, or document the over-marking as the contract where operators configure these values.

中文说明

R3-13:依旧成立——该接缝无法区分操作者自有的用户级 .env 文件(~/.env<globalQwenDir>/.env——无条件加载、无信任门)与仓库内容:两者都被标记为文件来源,于是操作者自己放在用户级 .env 里的 docker_host/containment 旋钮会像仓库植入的一样被清除或忽略——守护进程探测失败,auto 悄悄降级为直跑(或 required 拒绝),且没有任何东西说明原因。请在填充追踪集合时记录来源作用域(用户级 vs 仓库路径的 .env),只对仓库可达的文件回答「文件来源」;或在操作者配置这些值的地方把这种过度标记写明为契约。

— qwen3.8-max via Qwen Code /review (v0.22.0)

kind: 'install',
});
const passed = args.filter((_, i) => args[i - 1] === '--env');
expect(passed.some((e) => e.includes('canary-should-not-cross'))).toBe(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R3-14: still stands — the canary inspects only the VALUES of the forwarded --env entries, never the SET: the two forwarding forms docker/podman resolve from the client's own environment — bare --env OPENAI_API_KEY (no =value) and --env-file <dump> — cross the boundary without the canary string ever appearing in the argv this assertion inspects. A future edit forwarding process.env in docker's idiomatic passthrough form ships the suite green while OPENAI_API_KEY/GH_TOKEN from the CI reviewer's environment reach the reviewed repository's postinstall inside the container — the exact leak this module exists to prevent, re-opened with the guard reporting green. Pin the set, not the substring: expect(passed).toEqual(containerEnv(join(tmpDir, '.npm-cache'))) (any extra token — a bare key included — fails), plus expect(args).not.toContain('--env-file').

中文说明

R3-14:依旧成立——金丝雀只检查转发的 --env 条目的,从不检查集合:docker/podman 会从客户端自身环境解析的两种转发形态——裸 --env OPENAI_API_KEY(无 =值)与 --env-file <dump>——都能穿过边界,而金丝雀字符串根本不会出现在该断言检查的 argv 里。未来某次按 docker 惯用透传形态转发 process.env 的修改会让套件绿着上线,同时 CI 审查者环境里的 OPENAI_API_KEY/GH_TOKEN 到达容器内被审仓库的 postinstall——正是本模块要防止的泄漏,且守卫报告为绿色地重新打开。请钉住集合而不是子串:expect(passed).toEqual(containerEnv(join(tmpDir, '.npm-cache')))(任何额外 token——包括裸键——都会失败),另加 expect(args).not.toContain('--env-file')

— qwen3.8-max via Qwen Code /review (v0.22.0)

`to fill the gap.`,
});
const refusal = refuseUnsandboxedPhase(root);
if (refusal && args.resume) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The round-4 wiring added here — the refuseUnsandboxedPhase gate, the refusedReport shape (toolchain: 'refused', ok: false), the --resume throw, and the boxed/direct selection in run() — is exercised by no test anywhere: build-test.test.ts (106 runBuildTest calls) has zero references to a sandbox policy, verdict, or refused, and sandboxed-exec.test.ts tests the decisions only with injected arguments. Every mutation that unwinds this round's fix ships green — flipping ok: false to true (the refusal reads as a clean hand-off), deleting the resume throw (a continuation overwrites the partial report with an identity-less refusal), or containerised() returning null unconditionally (every command silently runs direct under required). The dead !applicable condition one hunk down is exactly this class of defect and survived compile, lint and the full suite for the same reason. Pin the seam via vi.spyOn on the sandboxed-exec module: required + no runtime → refused, ok: false, exec never invoked; same with --resume → throws, --out untouched; required + container verdict + mountable → boxed argv; auto + no runtime → direct run unchanged.

中文说明

本轮在此新增的接线——refuseUnsandboxedPhase 门、refusedReport 形态(toolchain: 'refused'ok: false)、--resume 抛错、以及 run() 里的容器化/直跑选择——没有任何测试覆盖:build-test.test.ts(106 次 runBuildTest 调用)对沙箱策略、判定、refused 零引用,sandboxed-exec.test.ts 只用注入参数测试这些决定。任何撤销本轮修复的变异都能绿着上线——把 ok: false 翻成 true(拒绝被读成干净交接)、删掉 resume 抛错(续跑会用无身份的拒绝报告覆盖部分报告)、或让 containerised() 无条件返回 null(required 下每条命令悄悄直跑)。下方 hunk 里那个死掉的 !applicable 条件正是这类缺陷,它通过编译、lint 和全套测试也是同一原因。请用 vi.spyOn 钉住该接缝:required + 无运行时 → refusedok: false、exec 从未被调用;同样状态加 --resume → 抛错且 --out 不被改动;required + 容器判定 + 可挂载 → 容器化 argv;auto + 无运行时 → 直跑行为不变。

— qwen3.8-max via Qwen Code /review (v0.22.0)

* then names a directory the container does not have, and every command
* fails before it starts. Null when the tree is not under a mountable root.
*/
export function containerPathFor(cwd: string): string | null {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] containerPathFor — added this round to decide the --workdir spelling that must match the realpath'd mount — is the only exported decision function the new test file does not import: its parent-fallback branch (leaf not yet created: canonicalise the parent, re-attach the leaf) has zero coverage. A mutation swapping or deleting the fallback leaves every test green, and a wrong spelling there makes every containerised command fail before it starts (--workdir names a directory the container lacks) — read in the report as the PR's build/test failure rather than wiring. Add cases: existing dir under a temp root (realpath), not-yet-created leaf falling back to the parent's realpath, and null when neither exists.

中文说明

containerPathFor——本轮新增、用于决定必须与 realpath 化挂载一致的 --workdir 拼写——是新测试文件唯一没有导入的导出决定函数:其父目录回退分支(叶子尚未创建:规范化父目录、重新拼接叶子)零覆盖。交换或删除该回退的变异可以让所有测试保持绿色,而那里的错误拼写会让每条容器化命令在启动前失败(--workdir 指向容器里不存在的目录)——报告会把这读成 PR 的构建/测试失败而不是接线问题。请补用例:临时根下的已存在目录(realpath)、尚未创建的叶子回退到父目录的 realpath、两者都不存在时返回 null。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Round 4.

- **Last round's hand-off gate never ran.** It tested `!applicable`, and
  `applicable` is the filtered adapter ARRAY returned by
  `selectToolchainAdapter` — never falsy. The gate shipped green and closed
  nothing. It is judged on the RESULT now (`toolchain === 'unsupported'`),
  which also covers the second route to a hand-off — an adapter that applies
  and cannot scope, from inside the npm one — and the predicate is exported and
  tested rather than living inline where no test could see it.
- **The boxed farm dangled under a symlinked ancestor.** Round 3 made the mount
  and `--workdir` canonical; `exposeDependencies` still built its link targets
  from the lexical root, so on the everyday macOS `/tmp` → `/private/tmp`
  layout every farm link resolved to a path the container does not have. The
  phase would then report "every file was red or collected nothing" — a wiring
  failure published as a statement about the PR's own suite. Canonicalised on
  the sandboxed path only; the direct path keeps the caller's spelling.
- **A timed-out boxed run leaked its container.** `--rm` fires only on a
  self-exit, and the deadline kills the runtime CLIENT — so a suite whose own
  trap ignores the forwarded signal keeps running with the review temp dir
  writable, past the budget and past the end of the review. Containers get a
  unique `--name`, and both spawn sites `rm -f` it when the deadline fires.
- **The daemon scrub deleted case-sensitively.** Round 3 taught
  `isFileSourcedEnvKey` to fold case on Windows and left the deletion exact —
  so a `docker_host` written by a repo `.env` was correctly detected and then
  not removed.

Two mutations came back green again, and both were the round's real lesson: the
hand-off refusal had no test (which is how its dead-code predecessor shipped),
and the farm canonicalisation still has none — it needs a symlinked-ancestor
fixture with a live runtime, which this machine cannot provide, and it is
listed with the other integration gaps rather than claimed.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform arms and the win32 case-fold path bite exactly there.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped.

Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:160 — [probe] D5-1 containerRuntime probe is untested; both test seams are dead
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:122 — [probe] D5-2 settings half of sandboxPolicy matched case-sensitively ('Required' silently becomes off)
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:176 — [review] D5-3 sandboxVerdict JSDoc documents the discarded first-cut 'returns direct' behaviour
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:45 — [probe] D5-4 strictest auto/required ordering unpinned (reorder mutant ships green)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:286 — [probe] D5-5 install network pinned only negatively (single-token --network=none mutant ships green)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:391 — [probe] D5-6 --init unpinned — deleting it ships green and degrades deadline enforcement
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:236 — [probe] D5-7 refuseUnsandboxedPhase PROCEED path unpinned (final return-null mutant ships green)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:350 — [probe] D5-8 --user default-on case unpinned (every test stubs SANDBOX_SET_UID_GID)
  • packages/cli/src/commands/review/test-efficacy.ts:2536 — [review] D5-9 refusal note fires even when the phase had nothing to probe
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:209 — [probe] D5-10 auto-with-no-runtime → direct fallback untested (both mutants ship green)
  • packages/cli/src/commands/review/build-test.ts:334 — [review] D5-11 run()'s doc comment orphaned above containerised()
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:180 — [probe] D5-12 SANDBOX-with-answering-probe direction unpinned (shortcut mutant ships green)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:42 — [probe] D5-13 env-side trim/toLowerCase normalization unpinned
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:401 — [probe] D5-14 image slot in argv unpinned (image-dropped mutant ships green)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:269 — [probe] D5-15 podman runtime passthrough unpinned (hardcoded-docker mutant ships green)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:492 — [probe] D5-16 image override pick ordering unpinned (order-swap mutant ships green)

Convergence: round 5 posted 13 inline comment(s), 3 of them reported for the first time; the previous round posted 14 (4 new). Findings keep coming back to the same files: packages/cli/src/commands/review/lib/sandboxed-exec.ts (findings in rounds 1, 3, 4; 1 more now); packages/cli/src/commands/review/build-test.ts (findings in rounds 1, 4; 1 more now); packages/cli/src/commands/review/test-efficacy.ts (findings in round 4; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment choke point. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence, and the shipped agent brief (agent-briefs.ts:539) routes here whenever a test command failed. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. Witness: [probe] re-run at this head — real runTestDelta under QWEN_REVIEW_SANDBOX=required in a fixture baseline whose test script echoes a secret: PROBE-R221-BASE-OUTPUT "...SECRET-IS:[hunter2-credential]..." — the recorded repo command ran directly in the host shell with the full environment under the mode that forbids it. Fix direction: route test-delta's rerun through the same boundary — gate runTestDelta with refuseUnsandboxedPhase(baseline) (reporting the delta as unmeasured, like test-efficacy's refusal note) and/or execute through build-test's containerised run instead of the private direct-spawn copy.(中文:R2-21 依旧成立——test-delta 从不经过 containment 咽喉点:它保留私有 run()(spawnSync shell:true、env:buildRunEnv(process.env))作为默认 exec,文件内没有任何 refuseUnsandboxedPhase/sandboxVerdict/containerised 引用,因此 required 下它会在基础侧以完整环境未沙箱化地重跑记录的失败套件并把归因作为证据发布,且内置简报在任一 test 命令失败时都会路由到这里。第二后果:PR 侧套件在受限容器环境里跑而基础侧在完整宿主环境里跑,环境敏感测试可能恰在一侧翻转,test-delta 会凭容器/宿主环境差异给 PR 制造伪 Critical(或放过真回归)。证据:本 head 上重跑探针——真实 runTestDelta、QWEN_REVIEW_SANDBOX=required、夹具基线的 test 脚本回显密钥:基础侧输出含 SECRET-IS:[hunter2-credential]。修复方向:让 test-delta 的重跑经过同一边界。)

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform arms and the win32 case-fold path bite exactly there。

未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped。

收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 16 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 5 轮发布了 13 条行内评论,其中 3 条是首次提出;上一轮发布了 14 条(其中 4 条首次提出)。发现反复回到同一批文件:packages/cli/src/commands/review/lib/sandboxed-exec.ts(第 1、3、4 轮已出过发现,本轮又有 1 条);packages/cli/src/commands/review/build-test.ts(第 1、4 轮已出过发现,本轮又有 1 条);packages/cli/src/commands/review/test-efficacy.ts(第 4 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment choke point. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence, and the shipped agent brief (agent-briefs.ts:539) routes here whenever a test command failed. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. Witness: [probe] re-run at this head — real runTestDelta under QWEN_REVIEW_SANDBOX=required in a fixture baseline whose test script echoes a secret: PROBE-R221-BASE-OUTPUT "...SECRET-IS:[hunter2-credential]..." — the recorded repo command ran directly in the host shell with the full environment under the mode that forbids it. Fix direction: route test-delta's rerun through the same boundary — gate runTestDelta with refuseUnsandboxedPhase(baseline) (reporting the delta as unmeasured, like test-efficacy's refusal note) and/or execute through build-test's containerised run instead of the private direct-spawn copy.(中文:R2-21 依旧成立——test-delta 从不经过 containment 咽喉点:它保留私有 run()(spawnSync shell:true、env:buildRunEnv(process.env))作为默认 exec,文件内没有任何 refuseUnsandboxedPhase/sandboxVerdict/containerised 引用,因此 required 下它会在基础侧以完整环境未沙箱化地重跑记录的失败套件并把归因作为证据发布,且内置简报在任一 test 命令失败时都会路由到这里。第二后果:PR 侧套件在受限容器环境里跑而基础侧在完整宿主环境里跑,环境敏感测试可能恰在一侧翻转,test-delta 会凭容器/宿主环境差异给 PR 制造伪 Critical(或放过真回归)。证据:本 head 上重跑探针——真实 runTestDelta、QWEN_REVIEW_SANDBOX=required、夹具基线的 test 脚本回显密钥:基础侧输出含 SECRET-IS:[hunter2-credential]。修复方向:让 test-delta 的重跑经过同一边界。)

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines 960 to 961
return {
toolchain: 'unsupported',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-1: still stands — the round-5 result-based gate wraps only two of the unsupported returns. This generic "No supported npm project here to scope" return in the !adapter branch bypasses refusedIfHandedOff, so under review.sandbox: required this route still hands the reviewed repo's commands to the agent's unsandboxed shell. With a runtime answering and a mountable tree, a repo with no root package.json (Python/Go/Rust, or a manifest in a subdirectory) returns {toolchain: 'unsupported', ok: true} with the note "Fall back to the build/test precedence in your brief — installing dependencies first", and the brief's unsupported rule (agent-briefs.ts:540) then sends the agent to run the reviewed repository's install/build/test in its own shell with the full environment — the exact execution required forbids, reported as a clean hand-off. Witness: real runBuildTest with a package.json-less fixture repo, docker answering, QWEN_REVIEW_SANDBOX=required — the bare return yields {"toolchain":"unsupported","ok":true}; wrapping it flips to {"toolchain":"refused","ok":false}.

Suggested change
return {
toolchain: 'unsupported',
return refusedIfHandedOff({
toolchain: 'unsupported',

(also wrap the applicable.length > 1 ambiguity return — unreachable with one adapter today, but it shares the shape — or funnel every unsupported outcome through one exit.)

中文说明

R1-1 依旧成立——第 5 轮基于结果的门只包住了两处 unsupported 返回。!adapter 分支里这个「此处没有可 scope 的 npm 项目」通用返回绕过了 refusedIfHandedOff,因此在 review.sandbox: required 下这条路径仍会把被审仓库的命令交给 agent 未沙箱化的 shell。运行时在场、树可挂载时,一个没有根 package.json 的仓库(Python/Go/Rust,或清单元文件在子目录里)会返回 {toolchain: 'unsupported', ok: true},附注「回退到简报里的 build/test 优先顺序——先安装依赖」,随后简报的 unsupported 规则(agent-briefs.ts:540)会指示 agent 在自己的 shell 里以完整环境运行被审仓库的 install/build/test——正是 required 所禁止、却被报告为干净交接的执行。证据:对真实 runBuildTest、无 package.json 的夹具仓库、docker 在场、QWEN_REVIEW_SANDBOX=required——裸返回得到 {"toolchain":"unsupported","ok":true};包上 refusedIfHandedOff 后翻转为 {"toolchain":"refused","ok":false}。请把 applicable.length > 1 的歧义返回也包上(今天只有一个适配器不可达,但形态相同),或让所有 unsupported 结果走同一个出口。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +484 to +485
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-3: still stands — deliberately deferred since round 1, mechanism unchanged at this commit (this round's hunks do not touch it). The single RW bind mount covers the whole review temp dir, so it covers every pipeline tree's .git gitfile: containerized PR code can rewrite a gitfile to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge, and the host-side git invocations that follow the redirect (probe-tree restore, scratch-tree reset) give host code execution. Witness: not re-run this round — the round-1/round-2 live probes are recorded in the original threads, and the mount spec they exercised is byte-identical at this commit. Either close this arm (mask each tree's gitfile from the writable surface — e.g. a read-only bind over it — or make the host-side consumers refuse in-mount redirects), or hold it explicitly out of the PR's containment claims.

中文说明

R1-3 依旧成立——自第 1 轮起被有意推迟,机制在本提交上未变(本轮 hunk 未触碰它)。单一 RW 绑定挂载覆盖整个 review 临时目录,因此覆盖每棵流水线树的 .git gitfile:容器内的 PR 代码可以把 gitfile 改写为 gitdir: <同一挂载下的植入目录>,其中携带 filter.<x>.smudge,随后跟随该重定向的宿主侧 git 调用(探针树恢复、scratch 树复位)即形成宿主代码执行。证据:本轮未重跑——第 1/2 轮的活体探针记录在原始讨论串中,其验证过的挂载规格与本提交逐字节一致。要么关闭此臂(把各树 gitfile 从可写面上屏蔽——例如在其上叠加只读绑定——或让宿主侧消费者拒绝挂载内重定向),要么在 PR 的 containment 声明中明确把它排除在外。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +1771 to +1773
const r = boxed
? spawnSync(boxed.file, boxed.args, {
cwd: probeTree,

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R4-1: still stands with a narrowed mechanism — the round-5 --name + killContainer machinery is unreachable against a TERM-ignoring workload. spawnSync's timeout sends SIGTERM to the runtime CLIENT and then blocks until that client exits; the attached docker client forwards the signal and waits (this diff's own docker 29.1.3 measurement in containerCommand's --name comment). A container workload that ignores SIGTERM therefore hangs runProbeSuite forever, and this post-spawn killContainer never runs. The twin in build-test.ts's run() has the same shape. Witness (live docker 29.1.3, the module's exact argv): spawnSync('docker', ['run','--rm','--init','--name',…,'sh','-lc','trap "" TERM; sleep 20'], {timeout: 2500}) returned after 20444ms; the same spawn with killSignal: 'SIGKILL' returned at 2504ms, after which docker rm -f reaped the live container (STILL-ALIVE count 0). A PR committing a probe-reachable SIGTERM-ignore (e.g. in vitest globalSetup) plus a never-finishing suite hangs the review indefinitely and keeps the named container alive with the review temp dir mounted writable. Fix: pass killSignal: 'SIGKILL' in the boxed spawn options at both sites, so the client dies at the deadline, spawnSync returns, and the existing killContainer reaps the container through the daemon.

中文说明

R4-1 依旧成立,机制收窄——第 5 轮新增的 --name + killContainer 机制在忽略 TERM 的负载下不可达。spawnSync 的超时向运行时客户端发 SIGTERM,然后阻塞直到该客户端退出;附着的 docker 客户端会转发信号并等待(本 diff 在 containerCommand--name 注释里对 docker 29.1.3 的实测正是如此)。因此忽略 SIGTERM 的容器负载会让 runProbeSuite 永久挂起,此处的 killContainer 永远不会执行。build-test.ts run() 里的孪生点同形。证据(真实 docker 29.1.3、模块的完整 argv):带 timeout: 2500 的上述 spawnSync 20444ms 后才返回;改用 killSignal: 'SIGKILL'2504ms 返回,随后 docker rm -f 清掉存活容器(STILL-ALIVE 计数 0)。PR 只需在探针可达处(如 vitest globalSetup)提交忽略 SIGTERM 的处理再加一个永不结束的套件,即可让审查无限挂起、命名容器带着仍可写的 review 临时目录挂载存活。修复:在两处容器分支的 spawn 选项里加 killSignal: 'SIGKILL',使客户端在截止时刻被杀、spawnSync 返回,既有的 killContainer 再经守护进程清掉容器。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +595 to +598
'DOCKER_CONFIG',
'CONTAINERS_CONF',
'CONTAINERS_REGISTRIES_CONF',
'CONTAINERS_STORAGE_CONF',

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] The scrub misses the proxy family: runtimeClientEnv drops the file-sourced daemon selectors but not HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY, which docker/podman clients honour for the daemon connection — the function's own comment states the rule this violates ("Indirection counts as selection … scrubbing the direct selectors and leaving these would move the same steering one level down"). None of the loader's three exclusion tiers contains the proxy variables, so a committed .qwen/.env sets them and runtimeClientEnv() forwards them to the containerRuntime() probe and every boxed spawn. Witness (docker 29.1.3 against a fake proxy): ARM[http-proxy-nonloop] exit=0 proxy_requests=2 — proxy log HEAD .../_ping, GET .../v1.52/info — the client accepted the fake proxy's stubbed response as a successful docker info; controls noproxy/loopback had 0 proxy requests; loader probe: HTTP_PROXY: admitted=true fileSourced=true / forwarded=true while DOCKER_HOST is dropped. For an operator on a plaintext tcp:// daemon (the remote-engine shape the docstring names), required reads as satisfied against the attacker's proxy and fabricated evidence comes back scored; unix-socket/loopback daemons are unaffected. Note: for plaintext tcp:// the effective keys are HTTP_PROXY/http_proxy (HTTPS_PROXY covers TLS daemons).

Suggested change
'DOCKER_CONFIG',
'CONTAINERS_CONF',
'CONTAINERS_REGISTRIES_CONF',
'CONTAINERS_STORAGE_CONF',
'DOCKER_CONFIG',
'CONTAINERS_CONF',
'CONTAINERS_REGISTRIES_CONF',
'CONTAINERS_STORAGE_CONF',
'HTTP_PROXY',
'HTTPS_PROXY',
'ALL_PROXY',
'NO_PROXY',

(file-sourced-only deletion preserves an operator's own shell-exported proxy, matching the DOCKER_HOST treatment.)

中文说明

清除列表漏掉了代理家族:runtimeClientEnv 删除文件来源的守护进程选择器,却不删 HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY——docker/podman 客户端对守护进程连接同样认这些变量——本函数注释写明的规则正是它违反的(「间接也算选择……只清直接选择器而留下这些,等于把同样的操纵挪低一层」)。加载器的三层排除名单都不含代理变量,因此提交在 .qwen/.env 里的代理变量会被接纳,并经 runtimeClientEnv() 传给 containerRuntime() 探测与每一次容器化 spawn。证据(docker 29.1.3 对假代理):ARM[http-proxy-nonloop] exit=0 proxy_requests=2——代理日志收到 HEAD .../_pingGET .../v1.52/info——客户端把假代理的伪造响应当成成功的 docker info;对照(无代理/回环)代理请求为 0;加载器探针:HTTP_PROXY: admitted=true fileSourced=true / forwarded=true,而 DOCKER_HOST 被删除。对使用明文 tcp:// 守护进程的操作者(注释点名的远程引擎形态),required 会在攻击者代理面前读作已满足,伪造证据被当作结果评分;unix 套接字/回环守护进程不受影响。注意:明文 tcp:// 下生效的是 HTTP_PROXY/http_proxyHTTPS_PROXY 覆盖 TLS 守护进程)。仅删文件来源值,操作者自己 shell 导出的代理保留,与 DOCKER_HOST 的处理一致。

— qwen3.8-max via Qwen Code /review (v0.22.0)

expect(sandboxPolicy({}, {})).toBe('off');
// A garbled value is not a policy — it falls through rather than being
// guessed at.
expect(sandboxPolicy({ QWEN_REVIEW_SANDBOX: 'yes' }, {})).toBe('off');

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R3-5: still stands — the file-sourced branch of sandboxPolicy (!fileSourced('QWEN_REVIEW_SANDBOX')) is never tested; no caller ever passes the injectable third parameter, and the env-can-only-tighten merge is equally unpinned in the distinguishing direction. Both mutations ship green against the real suite (21/21): deleting the clause (the real tracking set is empty in tests, so it is vacuous) and env-always-wins instead of strictest. Under the first, a committed <repo>/.qwen/.env decides the policy; under the second, an operator's required is silently downgraded by a stray QWEN_REVIEW_SANDBOX=off.

expect(
  sandboxPolicy({ QWEN_REVIEW_SANDBOX: 'required' }, {}, () => true),
).toBe('off');
expect(
  sandboxPolicy({ QWEN_REVIEW_SANDBOX: 'off' }, { sandbox: 'required' }),
).toBe('required');
中文说明

R3-5 依旧成立——sandboxPolicy 的文件来源分支(!fileSourced('QWEN_REVIEW_SANDBOX'))从未被测试;没有任何调用方传入可注入的第三个参数,而「环境只能收紧」的合并在判别方向上同样未被钉住。两个突变在真实套件下都绿(21/21):删掉该子句(测试里真实追踪集为空,子句恒真),以及用「环境恒赢」替换 strictest。前者下提交在 <repo>/.qwen/.env 的值即可决定策略;后者下操作者的 required 会被一个偶然的 QWEN_REVIEW_SANDBOX=off 静默降级。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +324 to +326
expect(passed.some((e) => e.includes('canary-should-not-cross'))).toBe(
false,
);

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R3-14: still stands — the canary inspects only the VALUES of the forwarded --env entries, never the SET: a regression to valueless --env KEY forwarding ships the suite green, and docker/podman then resolve the value from the client's own environment. Leak verified against the live daemon: CANARY=secret docker run --rm --env CANARY alpine printenv CANARY printed the client-env value. Mutant (forward every process.env key valuelessly) ships 21/21 green; asserting every forwarded entry contains = flips it red.

for (const entry of passed) {
  expect(entry).toContain('=');
}
中文说明

R3-14 依旧成立——金丝雀只检查转发 --env 条目的,从不检查集合:回归到无值 --env KEY 转发全套件绿,docker/podman 会从客户端自身环境解析该值。泄漏已在真实守护进程上验证:CANARY=secret docker run --rm --env CANARY alpine printenv CANARY 打印出客户端环境的值。突变(把 process.env 每个键无值转发)全套件绿(21/21);断言每条转发条目都含 = 即可翻红。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +841 to +842
const refusedReport = (why: string): BuildTestReport => ({
toolchain: 'refused',

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R4-3: still stands — the wiring added in rounds 4-5 (the refuseUnsandboxedPhase gate, the refusedReport shape, the --resume throw, refusedIfHandedOff, and the boxed/direct selection in run()) is exercised by no test; build-test.test.ts has zero sandbox hits. The round-5 commit message itself records the failure class — "the hand-off refusal had no test (which is how its dead-code predecessor shipped)" — and this round's unwrapped generic return (R1-1) is the live instance. Add build-test-level tests: required + answering runtime + package.json-less root yields refused; a refused report on --resume throws and leaves the report file untouched; the boxed/direct selection follows the verdict.

中文说明

R4-3 依旧成立——第 4-5 轮新增的接线(refuseUnsandboxedPhase 门、refusedReport 形态、--resume 抛错、refusedIfHandedOffrun() 里的容器/直跑选择)没有任何测试覆盖;build-test.test.ts 里沙箱相关命中为零。第 5 轮提交信息自己记录了这一失败类——「交接拒绝没有测试(其死代码前辈正是这样上线的)」——而本轮未包裹的通用返回(R1-1)就是活例。请补 build-test 层测试:required + 运行时在场 + 无 package.json 的根得到 refused;对 refused 报告 --resume 抛错且不触碰报告文件;容器/直跑选择跟随判定。

— qwen3.8-max via Qwen Code /review (v0.22.0)

* then names a directory the container does not have, and every command
* fails before it starts. Null when the tree is not under a mountable root.
*/
export function containerPathFor(cwd: string): string | null {

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] R4-4: still stands — containerPathFor, which decides the --workdir spelling at both spawn sites and whose canonicalisation must agree with mountRootFor's realpath'd mount, is the only exported decision function the new test file does not import. Mutated to return the lexical resolve(cwd), the entire review suite stays green (4421 passed); on a host whose temp path resolves through a symlink (macOS /var/private/var) the bind mount exists under the canonical path only, --workdir names a directory the container does not have, and every containerized command fails before it starts; a null-regression silently unsandboxes via containerised() returning null. Add tests alongside the mountRootFor describe using the same real-directory fixtures: symlinked-ancestor agreement and the not-yet-existing leaf fallback.

中文说明

R4-4 依旧成立——containerPathFor 决定两个 spawn 点的 --workdir 拼写、其规范化必须与 mountRootFor realpath 化的挂载一致,却是新测试文件唯一没有导入的导出决策函数。突变为返回词法 resolve(cwd) 后整个 review 套件仍绿(4421 通过);在临时路径经符号链接解析的宿主上(macOS /var/private/var),绑定挂载只存在于规范路径下,--workdir 指向容器里不存在的目录,每个容器化命令都会在启动前失败;返回 null 的回归则会经 containerised() 悄悄退回未沙箱化。请在 mountRootFor 的 describe 旁用同样的真实目录夹具补测:符号链接祖先下两者拼写一致,以及尚未创建的叶子回退。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// SIGTERM", which is a less useful sentence about the same event. The reason
// tag is derived from the whole result either way, so it does not depend on
// which message wins.
if (boxed && (r.error || r.signal)) {

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The spawn-level wiring added this round — boxed spawn with runtimeClientEnv() and this deadline/error→killContainer cleanup (and its build-test twin) — has no test at any level; the test file's header defers container startup to "an integration harness" this diff does not add. If this block regresses (deleted or inverted), no test turns red and the orphan container keeps the shared .qwen/tmp mount writable past the end of the review — the hazard the --name/killContainer machinery exists to close. Mock node:child_process.spawnSync to simulate a timed-out/errored boxed run and assert rm -f is aimed at the spawned container name; or land the deferred integration coverage and reference it in the header.

中文说明

本轮新增的 spawn 层接线——容器分支以 runtimeClientEnv() 为客户端环境、以及这处「截止/错误→killContainer」清理(连同 build-test 的孪生点)——在任何层面都没有测试;测试文件头部把容器启动推迟给一个本 diff 并未添加的「集成测试架」。若该块回归(被删或条件取反),没有任何测试变红,孤儿容器会在审查结束后继续以可写状态持有共享 .qwen/tmp 挂载——正是 --name/killContainer 机制要关闭的危险。请 mock node:child_process.spawnSync 模拟超时/出错的容器化运行,断言 rm -f 指向所启动容器的名字;或落地被推迟的集成覆盖并在头部注释中引用。

— qwen3.8-max via Qwen Code /review (v0.22.0)

stdio: ['ignore', 'pipe', 'pipe'],
env: buildRunEnv(),
});
if (boxed && spawnTimedOut(r)) {

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The container-kill condition is asymmetric between the twins: this site kills only on spawnTimedOut(r), while test-efficacy's runProbeSuite kills on r.error || r.signal. Settled against the live daemon modeling this boxed run() exactly (same options, 70 MiB output from a TERM-ignoring container): spawn result {"errorCode":"ENOBUFS","signal":"SIGPIPE","status":null}spawnTimedOut(r) is false, so killContainer never fires here while the --rm container keeps running with the mount writable; test-efficacy's twin condition kills it (rm -f reaped the orphan; it is a try/catch no-op when no container exists).

Suggested change
if (boxed && spawnTimedOut(r)) {
if (boxed && (spawnTimedOut(r) || r.error)) {
中文说明

两处孪生的容器清理条件不对称:此处只在 spawnTimedOut(r) 时清理,而 test-efficacy 的 runProbeSuiter.error || r.signal 时清理。按本处容器化 run() 的完整形态在真实守护进程上实测(相同选项、忽略 TERM 的容器输出 70 MiB):spawn 结果 {"errorCode":"ENOBUFS","signal":"SIGPIPE","status":null}——spawnTimedOut(r) 为 false,此处的 killContainer 永不触发,--rm 容器带着可写挂载继续运行;孪生处的条件则能清掉它(rm -f 清除了孤儿容器;无容器时它是 try/catch 空操作)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

…e reaper reachable

Round 5. The first finding is the same one for the third time, and the third
time is the one worth explaining: I had been guarding routes rather than the
exit.

- **Attempt one** tested `!applicable` — the filtered adapter ARRAY, never
  falsy — and was dead code.
- **Attempt two** wrapped the two `adapter.run` returns and missed the
  `!adapter` branch's own `unsupported` report.
- **Now** the conversion sits at the single place a report can reach a caller,
  and it is an exported `applyHandOffPolicy` rather than a branch inside a long
  function, so a test can reach it without a live container runtime. Both
  previous attempts failed the same way — a guard placed where no test could
  see it, in a function with more exits than the author was holding in mind.

**The container reaper added last round was unreachable, not wrong.**
`spawnSync` sends its `killSignal` at the deadline and then WAITS for the child
to exit, so an attached runtime client that forwards SIGTERM to a workload
whose own trap ignores it never returns — and the `killContainer` after it never
runs. The boxed spawns use `killSignal: 'SIGKILL'` now: the client cannot ignore
it, the call returns, and the container is reaped by name at the daemon, which
is where the deadline had to be enforced.

**The scrub missed the proxy family.** `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY`/
`NO_PROXY` (and their lowercase spellings) are honoured by both clients for
every daemon call, so a repo-shipped one interposes on the connection the
direct selectors were scrubbed to protect.

One test was removed rather than added: a `expect(killContainer).toBeTypeOf(
'function')` I wrote to "document" the SIGKILL reasoning. It asserts nothing and
would have read as coverage; the reasoning belongs in the comment where it now
lives alone.

The `killSignal` choice is reasoned but not pinned — it needs a live runtime and
a TERM-ignoring workload — and joins the integration gaps already listed in the
PR body rather than being claimed.

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform arms (--user absence, win32 scrub fold) bite exactly there.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped.

Not explored to full depth (tool budget reached): chunk 2: executing npx vitest run src/commands/review/lib/sandboxed-exec.test.ts — the review worktree has no node_modules installed, and a full monorepo install exc….

Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:434 — [review] D6-1 containerCommand's doc block is attached to let containerSeq , leaving the function undocumented
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:281 — [probe] D6-2 refuseUnsandboxedPhase's success exit (container+required+mountable → null) is pinned by no test; the refusal mutant ships green and the added case flips it red
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:117 — [probe] D6-3 the four lowercase proxy siblings in the scrub are unpinned and load-bearing on posix; the delete-mutant ships green and a file-sourced lowercase https_proxy sur…
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:336 — [probe] D6-4 the tmpfs assertion pins only the path prefix, not rw,mode=1777 ; ro/0755 mutants ship green and docker probes show HOME not writable by the mapped uid
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:83 — [probe] D6-5 the Windows side of the --user branch (flag correctly absent when getuid is undefined) is pinned by no assertion; the guard-drop mutant ships green on every platf…
  • packages/cli/src/commands/review/build-test.ts:1109 — [probe] D6-6 the single-exit wrapper invocation runBuildTest = applyHandOffPolicy(...) is pinned by no test; dropping it ships 129/129 green and restores the hand-off defect under requir…
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:133 — [probe] D6-7 the win32 case-fold loop in runtimeClientEnv's drop() is pinned by no assertion; deleting it ships green and a committed docker_host variant survives on Windows

Convergence: round 6 posted 2 inline comment(s), 1 of them reported for the first time; the previous round posted 13 (3 new). Findings keep coming back to the same files: packages/cli/src/commands/review/build-test.ts (findings in rounds 1, 4, 5; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment boundary. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence, and the shipped agent brief (agent-briefs.ts:539) routes here whenever a test command failed. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. Witness: probe re-run at the round-5 head — real runTestDelta under QWEN_REVIEW_SANDBOX=required in a fixture baseline whose test script echoes a secret: base-side output '...SECRET-IS:[hunter2-credential]...' — the recorded repo command ran directly in the host shell with the full environment under the mode that forbids it; the file is unchanged at this head. Fix direction: route test-delta's rerun through the same boundary — gate runTestDelta with refuseUnsandboxedPhase (reporting the delta as unmeasured, like test-efficacy's refusal note) and/or execute through build-test's containerised run instead of the private direct-spawn copy.(中文:R2-21 依旧成立——test-delta 从不经过 containment 咽喉点:它保留私有 run()(spawnSync shell:true、env:buildRunEnv(process.env))作为默认 exec,文件内没有任何 refuseUnsandboxedPhase/sandboxVerdict/containerised 引用,因此 required 下它会在基础侧以完整环境未沙箱化地重跑记录的失败套件并把归因作为证据发布,且内置简报在任一 test 命令失败时都会路由到这里。第二后果:PR 侧套件在受限容器环境里跑而基础侧在完整宿主环境里跑,环境敏感测试可能恰在一侧翻转,test-delta 会凭容器/宿主环境差异给 PR 制造伪 Critical(或放过真回归)。证据:探针在上一轮 head 上重跑——真实 runTestDelta、QWEN_REVIEW_SANDBOX=required、夹具基线的 test 脚本回显密钥:基础侧输出含 SECRET-IS:[hunter2-credential];该文件在本 head 上未变。修复方向:让 test-delta 的重跑经过同一边界。)

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform arms (--user absence, win32 scrub fold) bite exactly there。

未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped。

未探索到全部深度(达到工具调用预算):chunk 2:executing npx vitest run src/commands/review/lib/sandboxed-exec.test.ts — the review worktree has no node_modules installed, and a full monorepo install exc…

收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 7 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 6 轮发布了 2 条行内评论,其中 1 条是首次提出;上一轮发布了 13 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/cli/src/commands/review/build-test.ts(第 1、4、5 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment boundary. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence, and the shipped agent brief (agent-briefs.ts:539) routes here whenever a test command failed. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. Witness: probe re-run at the round-5 head — real runTestDelta under QWEN_REVIEW_SANDBOX=required in a fixture baseline whose test script echoes a secret: base-side output '...SECRET-IS:[hunter2-credential]...' — the recorded repo command ran directly in the host shell with the full environment under the mode that forbids it; the file is unchanged at this head. Fix direction: route test-delta's rerun through the same boundary — gate runTestDelta with refuseUnsandboxedPhase (reporting the delta as unmeasured, like test-efficacy's refusal note) and/or execute through build-test's containerised run instead of the private direct-spawn copy.(中文:R2-21 依旧成立——test-delta 从不经过 containment 咽喉点:它保留私有 run()(spawnSync shell:true、env:buildRunEnv(process.env))作为默认 exec,文件内没有任何 refuseUnsandboxedPhase/sandboxVerdict/containerised 引用,因此 required 下它会在基础侧以完整环境未沙箱化地重跑记录的失败套件并把归因作为证据发布,且内置简报在任一 test 命令失败时都会路由到这里。第二后果:PR 侧套件在受限容器环境里跑而基础侧在完整宿主环境里跑,环境敏感测试可能恰在一侧翻转,test-delta 会凭容器/宿主环境差异给 PR 制造伪 Critical(或放过真回归)。证据:探针在上一轮 head 上重跑——真实 runTestDelta、QWEN_REVIEW_SANDBOX=required、夹具基线的 test 脚本回显密钥:基础侧输出含 SECRET-IS:[hunter2-credential];该文件在本 head 上未变。修复方向:让 test-delta 的重跑经过同一边界。)

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +484 to +485
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-3: still stands at this head — deliberately deferred since round 1, mechanism unchanged; this round's hunks do not touch it. The single RW bind mount covers the whole review temp dir, so it covers every pipeline tree's .git gitfile: containerized PR code can rewrite a gitfile to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge, and the host-side git invocations that follow the redirect (probe-tree creation, restoreProbeTreeTracked after every sandboxed suite run) give host code execution. Rounds 1-2 reproduced this end-to-end (HOST-SIDE EXECUTION OBSERVED for both the restore checkout and git worktree add; scratch-tree's --git-common-dir identity gate refused the same tree); the mount spec is byte-identical at this commit. Either close this arm (mask each tree's gitfile from the writable surface — e.g. a read-only bind over it — or make the host-side consumers verify every tree's .git still resolves to its own admin entry under <repo>/.git/worktrees/ and refuse in-mount redirects), or hold it explicitly out of the PR's containment claims.

中文说明

R1-3 在本 head 上依旧成立——自第 1 轮起被有意推迟,机制未变,本轮 hunk 未触碰它。单一 RW 绑定挂载覆盖整个 review 临时目录,因此覆盖每棵流水线树的 .git gitfile:容器内的 PR 代码可以把 gitfile 改写为 gitdir: <同一挂载下的植入目录>,其中携带 filter.<x>.smudge,随后跟随该重定向的宿主侧 git 调用(探针树创建、每次沙箱化套件运行后的 restoreProbeTreeTracked)即形成宿主代码执行。第 1/2 轮已端到端复现(restore checkout 与 git worktree add 均观测到宿主侧执行;scratch-tree 的 --git-common-dir 身份关卡对同一棵树会拒绝);挂载规格与本提交逐字节一致。要么关闭此臂(把各树 gitfile 从可写面上屏蔽——例如在其上叠加只读绑定——或让宿主侧消费者校验各树 .git 仍解析到 <repo>/.git/worktrees/ 下自己的 admin entry 并拒绝挂载内重定向),要么在 PR 的 containment 声明中明确把它排除在外。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +1109 to +1111
export function runBuildTest(args: BuildTestArgs): BuildTestReport {
return applyHandOffPolicy(runBuildTestUnguarded(args));
}

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R6-2: the new single-exit wrapper also runs on --resume answers — a resumed report whose toolchain is 'unsupported' under a now-required policy is replaced by a fresh identity-less refusedReport, and the handler's unconditional writeFileSync(args.out, …) destroys the report the call was asked to continue; every later --resume fails the identity check ('records no run identity'), so the chain is permanently dead. The diff's own invariant ('A continuation must never answer with a FRESH report') is enforced by throws on the other two continuation routes (refusal gate, !adapter) but not at this third exit. Verified end-to-end against the real runBuildTest with an answering docker: call 1 (policy off, yarn-shaped repo) writes an identity-stamped unsupported hand-off; call 2 (--resume, QWEN_REVIEW_SANDBOX=required) returns toolchain=refused, run=null — original overwritten; call 3 throws 'records no run identity'. The throw-on-resume arm preserves the report. Trigger: the policy is read per call, so a tightening between call 1 and the resume (env or operator setting) plus an answering runtime on the resume call is enough; the repo shape is the common unscopeable case (yarn/pnpm/bun, no package-lock.json).

Suggested change
export function runBuildTest(args: BuildTestArgs): BuildTestReport {
return applyHandOffPolicy(runBuildTestUnguarded(args));
}
export function runBuildTest(args: BuildTestArgs): BuildTestReport {
const report = runBuildTestUnguarded(args);
if (args.resume && handOffRefused(report.toolchain, sandboxPolicy())) {
throw new Error(
`refusing to continue this run: converting the resumed hand-off to a ` +
`refusal would replace the report at ${args.out} with a fresh one ` +
`that records no run identity. Re-run without --resume under the ` +
`new policy.`,
);
}
return applyHandOffPolicy(report);
}
中文说明

R6-2:新的单出口包装器对 --resume 的应答同样生效——在策略已变为 required 时,一份 toolchain'unsupported' 的续跑报告会被替换成一份没有运行身份的全新 refusedReport,而处理器无条件的 writeFileSync(args.out, …) 会毁掉这次调用本应续跑的那份报告;之后每一次 --resume 都会撞上身份检查('records no run identity'),续跑链被永久杀死。本 diff 自己的不变量("续跑绝不能以一份全新报告作答")在另外两条续跑路径(拒绝门、!adapter)上以抛错落实,唯独这第三个出口没有。已对真实 runBuildTest(docker 在场)端到端验证:第 1 次调用(策略 off、yarn 形态仓库)写下带运行身份的 unsupported 交接报告;第 2 次调用(--resumeQWEN_REVIEW_SANDBOX=required)返回 toolchain=refused, run=null——原报告被覆盖;第 3 次调用抛 'records no run identity'。改为续跑时抛错后报告得以保留。触发条件:策略按调用读取,因此第 1 次调用与续跑之间收紧策略(环境变量或操作者设置)、且续跑时运行时应答即可;仓库形态正是最常见的不可 scope 情形(yarn/pnpm/bun、无 package-lock.json)。修复见上方建议代码块。

— qwen3.8-max via Qwen Code /review (v0.22.0)

…continuing

Round 6, two Criticals: the deferred gitfile one, and this.

Round 5's single-exit conversion runs on `--resume` answers too. A resumed
report whose toolchain is `unsupported`, under a policy that tightened between
the first call and the continuation, was replaced by a fresh refusal — which
the handler writes unconditionally, over the report the call was asked to
continue. That refusal carries no run identity, so every later `--resume` fails
the identity check and the round redoes install, build and every suite.

"A continuation must never answer with a FRESH report" is enforced by a throw
at the refusal gate and at `!adapter`. This conversion was added after both and
did not have it. It does now.

The trigger is ordinary rather than adversarial: the policy is read per call,
so an operator raising it — or a workflow's `env:` — between call one and the
resume is enough, on exactly the unscopeable repo shapes (yarn/pnpm/bun) that
reach a hand-off at all.

**The first test I wrote for this passed without the fix.** It drove
`runBuildTest` with an incomplete argument object and asserted `.toThrow()`;
the throw it saw was `--timeout must be a finite number of seconds`, from
validation long before the code under test. A mutation caught it. Making the
fixture faithful means reproducing the whole resume-identity apparatus, so the
decision is a predicate instead — `resumeWouldDestroyReport`, tested directly,
with the wiring left as one visible line. Same shape as round 5's
`applyHandOffPolicy`, and for the same reason: a guard a test cannot reach is a
guard that has already been wrong twice here.

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the sandboxed-exec platform arms (--user absence, win32 scrub fold) bite exactly there.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped.

Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/build-test.ts:1143 — [review] the new resume-guard throw and single-exit conversion are unpinned at the runBuildTest level
  • packages/cli/src/commands/review/build-test.ts:1086 — [probe] resumeWouldDestroyReport's insertion orphaned applyHandOffPolicy's doc comment
  • packages/cli/src/commands/review/build-test.ts:1144 — [review] the new throw message lacks the build-test: prefix every sibling error carries
  • packages/cli/src/commands/review/build-test.ts:433 — [probe] the timeout-reap wiring is unpinned — deleting the guard ships the suite green
  • packages/cli/src/commands/review/build-test.ts:899 — [probe] the runBuildTest suite depends on the host machine's review.sandbox setting
  • packages/cli/src/commands/review/build-test.ts:399 — [probe] the first sandboxed command burns its deadline pulling the image

Convergence: round 7 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (1 new). Findings keep coming back to the same files: packages/cli/src/commands/review/build-test.ts (findings in round 6; 1 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)

[Critical] R1-3 (packages/cli/src/commands/review/lib/sandboxed-exec.ts:485 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — deliberately deferred since round 1, mechanism unchanged. The single RW bind mount covers every pipeline tree's .git gitfile: containerized PR code can rewrite a gitfile to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge, and the host-side git invocations that follow the redirect (probe-tree creation, restoreProbeTreeTracked after every sandboxed suite run) give host code execution. Witness: rounds 1-2 end-to-end reproduction — HOST-SIDE EXECUTION OBSERVED for both the restore checkout and git worktree add; scratch-tree's identity gate refused the same tree; the mount spec is byte-identical at this commit. Fix direction: mask each tree's gitfile from the writable surface (e.g. a read-only bind over it), or make the host-side consumers verify every tree's .git still resolves to its own admin entry under /.git/worktrees/ and refuse in-mount redirects — or land the documented follow-up before auto/required are turned on in CI.

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment boundary. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. Witness: probe re-run under QWEN_REVIEW_SANDBOX=required in a fixture baseline whose test script echoes a secret — base-side output '...SECRET-IS:[hunter2-credential]...'; the file is unchanged at this head and was re-discovered independently by this round's reverse audit. Fix direction: gate runTestDelta with refuseUnsandboxedPhase(baseline) (reporting the delta as unmeasured, like test-efficacy's refusal note) and/or execute through build-test's exported run() (its new optional kind defaults to the restrictive 'test').

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the sandboxed-exec platform arms (--user absence, win32 scrub fold) bite exactly there。

未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped。

收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 6 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 7 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 1 条首次提出)。发现反复回到同一批文件:packages/cli/src/commands/review/build-test.ts(第 6 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

[Critical] R1-3 (packages/cli/src/commands/review/lib/sandboxed-exec.ts:485 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — deliberately deferred since round 1, mechanism unchanged. The single RW bind mount covers every pipeline tree's .git gitfile: containerized PR code can rewrite a gitfile to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge, and the host-side git invocations that follow the redirect (probe-tree creation, restoreProbeTreeTracked after every sandboxed suite run) give host code execution. Witness: rounds 1-2 end-to-end reproduction — HOST-SIDE EXECUTION OBSERVED for both the restore checkout and git worktree add; scratch-tree's identity gate refused the same tree; the mount spec is byte-identical at this commit. Fix direction: mask each tree's gitfile from the writable surface (e.g. a read-only bind over it), or make the host-side consumers verify every tree's .git still resolves to its own admin entry under /.git/worktrees/ and refuse in-mount redirects — or land the documented follow-up before auto/required are turned on in CI.

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment boundary. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. Witness: probe re-run under QWEN_REVIEW_SANDBOX=required in a fixture baseline whose test script echoes a secret — base-side output '...SECRET-IS:[hunter2-credential]...'; the file is unchanged at this head and was re-discovered independently by this round's reverse audit. Fix direction: gate runTestDelta with refuseUnsandboxedPhase(baseline) (reporting the delta as unmeasured, like test-efficacy's refusal note) and/or execute through build-test's exported run() (its new optional kind defaults to the restrictive 'test').

— qwen3.8-max via Qwen Code /review (v0.22.0)

// The deadline killed the CLIENT; the container outlives it — see the
// `--name` comment in `containerCommand`. Reach the daemon instead, then
// report the timeout exactly as before.
killContainer(boxed.runtime, boxed.name);

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R7-1: the boxed container is reaped only when the spawn TIMED OUT. A spawnSync maxBuffer overflow — the reviewed command writing more than 64 MB to one stdout/stderr stream, trivially produced by a postinstall/build/test — kills the runtime client with error.code === 'ENOBUFS', and spawnTimedOut() returns false for that shape, so killContainer is skipped. The container — holding the RW bind mount of the review temp dir — outlives the per-command deadline and the end of the review: --rm fires only on exit and nothing else stops it, so a workload that never exits leaves one orphan per malicious review on a persistent runner, still mutating the trees later phases read. The command is also misreported as an ordinary failure (timedOut: false, exitCode: null), with no hint that a live container still holds the mount.

witness: live probe with the module's exact argv shape and a 70 MB workload through the 64 MiB maxBuffer (Node v22.22.0, docker 24.0.9):
spawn result: {"errCode":"ENOBUFS","signal":"SIGKILL","status":null}
spawnTimedOut => false  =>  killContainer called? false
docker ps after ENOBUFS kill of client: "qwen-review-verify-c1-45245 Up 31 seconds"
orphan container alive: true

The sibling site in test-efficacy.ts already reaps on the broader r.error || r.signal condition; this site checks only the timeout. Reap whenever the boxed client did not exit normally (status === null covers ETIMEDOUT, ENOBUFS and signal kills; for a client that never spawned, docker rm -f on the absent name fails silently by killContainer's try/catch construction):

if (boxed && r.status === null) {
  // The deadline (or a buffer overflow) killed the CLIENT; the container
  // outlives it. Reach the daemon instead.
  killContainer(boxed.runtime, boxed.name);
}
中文说明

容器化运行只有在 spawn 超时时才会被收割。spawnSyncmaxBuffer 溢出——被审命令向单个 stdout/stderr 流写入超过 64 MB,一个 postinstall/构建/测试即可轻易制造——会以 error.code === 'ENOBUFS' 杀死运行时客户端,而 spawnTimedOut() 对这种形态返回 false,于是 killContainer 被跳过。容器带着 review 临时目录的 RW 绑定挂载存活过每命令截止、也存活过审查结束:--rm 只在自行退出时触发,没有任何别的机制停掉它——一个永不退出的负载会在持久 runner 上按恶意审查累积一个孤儿容器,继续改写后续阶段读取的树。该命令还会被误报为普通失败(timedOut: falseexitCode: null),丝毫不提示仍有活容器握着挂载。

证据:按模块 argv 形态、70 MB 负载穿过 64 MiB maxBuffer 的活体探针(Node v22.22.0、docker 24.0.9):spawn 结果 {"errCode":"ENOBUFS","signal":"SIGKILL","status":null}spawnTimedOut => falsekillContainer 未被调用;客户端被杀后 docker ps 仍显示容器 Up 31 seconds——孤儿容器存活:true。

test-efficacy.ts 的孪生点已按更宽的 r.error || r.signal 条件收割,本处却只判超时。请在容器化客户端未正常退出时一律收割(status === null 覆盖 ETIMEDOUT、ENOBUFS 与信号杀;对从未成功 spawn 的客户端,按 killContainer 的 try/catch 构造,docker rm -f 一个不存在的名字会静默失败)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

…timeout

Round 7, one Critical.

`build-test` reaped the boxed container when `spawnTimedOut(r)`, which is true
for ETIMEDOUT and false for a `maxBuffer` overflow — and a reviewed command
writing 64 MB to one stream is a postinstall away. The client dies with
ENOBUFS, the reap is skipped, and the container keeps the review temp dir
mounted read-write past the per-command deadline and past the end of the
review.

The sibling in `test-efficacy` already reaped on the broader `r.error ||
r.signal`. That the two had drifted to different conditions is how one came to
miss a case the other caught, so they now share one exported predicate:
`status === null` — exactly "the client did not exit normally", covering
ETIMEDOUT, ENOBUFS and signal kills in one condition rather than a list of
causes to keep in sync. A normal exit needs no reaping (`--rm` has fired), and
a client that never spawned has no container, where the reap is a silent no-op
by `killContainer`'s construction.

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the sandboxed-exec platform arms (--user absence, win32 scrub fold) bite exactly there.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped.

Test Plan (not a blocker): 324 passed — this review observed 23009 passed.

Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:129 (+10 locations) — [probe] under-pinned guarantees in the new test file: each named mutation ships the suite green (case-fold scrub, proxy entries, image slot, cache wiring, tmp…
  • packages/cli/src/commands/review/build-test.ts:900 (+3 locations) — [probe] sandbox wiring in the consumer phases is unpinned: gate deletions ship green and containment silently disappears
  • packages/cli/src/commands/review/build-test.ts:1087 (+4 locations) — [review] doc blocks displaced from the declarations they describe by this diff's insertions
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:174 — [review] sandboxVerdict's docstring documents the rejected first-cut behavior (SANDBOX-set 'returns direct'), the implementation and test pin the opposite
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:160 — [review] dead seam: resetContainerRuntimeProbe has no caller and no caller ever passes force=true to containerRuntime
  • packages/cli/src/commands/review/build-test.ts:434 — [probe] the reap keys on status === null only: a client that exits WITH a status while the container keeps running (measured: stream-loss exit 125) is never reaped — orphan container hold…
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:122 — [probe] sandboxPolicy's settings value is not normalized (env half trims/lowercases): a near-miss review.sandbox like "Required" silently resolves to off — fail-open on the centra…
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:273 — [review] the required refusal message names one cause for mountRootFor === null; symlink-redirected/unresolvable trees get a false, unactionable reason
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:502 — [probe] --volume colon interpolation: a colon-containing checkout path makes every containerized command fail (invalid spec: too many colons) although the probe passes and auto ne…

Convergence: round 8 posted 2 inline comment(s), 1 of them reported for the first time; the previous round posted 1 (1 new). The rate of new findings is not falling. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment boundary. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. Witness: probe re-run under QWEN_REVIEW_SANDBOX=required in a fixture baseline whose test script echoes a secret — base-side output '...SECRET-IS:[hunter2-credential]...'; the file is unchanged at this head (verified at HEAD this round). Fix direction: gate runTestDelta with refuseUnsandboxedPhase(baseline) (reporting the delta as unmeasured, like test-efficacy's refusal note) and/or execute through build-test's exported run() (its new optional kind defaults to the restrictive 'test').

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the sandboxed-exec platform arms (--user absence, win32 scrub fold) bite exactly there。

未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the uid/opt-out and symlink-path arms are platform-shaped。

Test Plan(非阻断):324 passed — this review observed 23009 passed

收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改:共 9 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 8 轮发布了 2 条行内评论,其中 1 条是首次提出;上一轮发布了 1 条(其中 1 条首次提出)。新发现的产出速度没有下降。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

[Critical] R2-21 (packages/cli/src/commands/review/test-delta.ts:179 — file not in this diff, so this blocker cannot be anchored inline): still stands at this head — test-delta never crosses the containment boundary. It keeps its own private run() (spawnSync(command, { shell: true, env: buildRunEnv(process.env) })) as the default exec — zero references to refuseUnsandboxedPhase/sandboxVerdict/containerised in the file — so under required it reruns the recorded failing suites BASE-SIDE unsandboxed with the full environment and publishes the attribution as evidence. Second consequence: PR-side suites run in the restricted container env while the base side runs in the full host env, so an env-sensitive test can flip on one side and test-delta manufactures a Critical against the PR (or dismisses a real regression) out of a container/host env difference. Witness: probe re-run under QWEN_REVIEW_SANDBOX=required in a fixture baseline whose test script echoes a secret — base-side output '...SECRET-IS:[hunter2-credential]...'; the file is unchanged at this head (verified at HEAD this round). Fix direction: gate runTestDelta with refuseUnsandboxedPhase(baseline) (reporting the delta as unmeasured, like test-efficacy's refusal note) and/or execute through build-test's exported run() (its new optional kind defaults to the restrictive 'test').

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +603 to +605
for (const key of [
'DOCKER_HOST',
'DOCKER_CERT_PATH',

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] runtimeClientEnv's scrub list misses DOCKER_API_VERSION, so a repository-shipped value survives the scrub and silently switches auto containment off. A reviewed PR that commits DOCKER_API_VERSION=1.9 in .qwen/.env has it admitted by the loader (qwen-scoped file, no exclusion list names it) and tracked file-sourced; the scrub does not drop it, so containerRuntime's docker info probe runs with it and exits 1 ("client version 1.9 is too old"); the probe answers null, and under review.sandbox: auto the verdict becomes direct — every install/build/test/probe then executes the reviewed repository's own commands unsandboxed with the full process.env while the operator believes containment is active, and the fallback is silent because nothing consumes the verdict's disclose string. Under required the same input fails closed (refused), so the exposure is the auto posture — and a repo-induced probe failure is exactly the steering class this scrub list exists to prevent.

witness (A/B probe against the real loader + scrub + docker 24.0.9):

clean repo                          -> containerRuntime "docker", verdictAuto "container"
repo ships DOCKER_API_VERSION=1.9   -> fileSourced true, clientEnvKeepsDAV "1.9",
                                       containerRuntime null, verdictAuto "direct",
                                       verdictRequired "refused"
DOCKER_API_VERSION=1.9 docker info  -> exit 1, "client version 1.9 is too old"
key added to the scrub list         -> verdictAuto "container" restored
Suggested change
for (const key of [
'DOCKER_HOST',
'DOCKER_CERT_PATH',
for (const key of [
'DOCKER_HOST',
'DOCKER_CERT_PATH',
'DOCKER_API_VERSION',
中文说明

[Critical] runtimeClientEnv 的清洗列表漏掉了 DOCKER_API_VERSION,仓库内置的值可以穿过清洗、静默关掉 auto 封装。被审 PR 在 .qwen/.env 里提交 DOCKER_API_VERSION=1.9 时,loader 会接受它(qwen 作用域文件,没有任何排除列表列它)并记为文件来源;清洗不丢弃它,于是 containerRuntimedocker info 探测带着它运行、以退出码 1 结束("client version 1.9 is too old");探测答 null,review.sandbox: auto 下判定变为 direct——每一次 install/build/test/探针都以完整 process.env 在沙箱外执行被审仓库自己的命令,而操作者以为封装生效,且该回退是无声的(判定里的 disclose 字符串没有任何消费者)。required 下同样输入会失败关闭(refused),因此暴露面是 auto 姿态——而「仓库诱导探测失败」正是这份清洗列表要防的转向类。

证据(对真实 loader + 清洗 + docker 24.0.9 的 A/B 探针):干净仓库 → containerRuntime "docker"verdictAuto "container";仓库内置 DOCKER_API_VERSION=1.9fileSourced trueclientEnvKeepsDAV "1.9"containerRuntime nullverdictAuto "direct"verdictRequired "refused"DOCKER_API_VERSION=1.9 docker info → 退出码 1、"client version 1.9 is too old";把该键加入清洗列表 → verdictAuto "container" 恢复。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +501 to +502
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

ghost Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-3: still stands at this head — deliberately deferred since round 1, mechanism unchanged (this round's diff includes this file but does not touch the mount geometry). The single RW bind mount covers every pipeline tree's .git gitfile: it sits inside its tree, i.e. inside this mount, and containerized PR code can rewrite it to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge; the host-side git invocations that follow the redirect (probe-tree creation, restoreProbeTreeTracked after every sandboxed suite run) then give host code execution. The PR description discloses this class under "What this does not close" (in-worktree contamination, host-side git operations), so this is re-posted as a carried blocker for the record rather than a new discovery — it remains the open boundary between this PR's containment and the host.

witness: rounds 1-2 end-to-end reproduction — HOST-SIDE EXECUTION OBSERVED for both the restore checkout and git worktree add; scratch-tree's identity gate refused the same tree; the mount spec is byte-identical at this commit.

Fix direction: mask each tree's gitfile from the writable surface (e.g. a read-only bind over it), or make the host-side consumers verify every tree's .git still resolves to its own admin entry under <repo>/.git/worktrees/ and refuse in-mount redirects — or land the documented follow-up before auto/required are turned on in CI.

中文说明

[Critical] R1-3:在本 head 上依旧成立——自第 1 轮起被有意延后,机制未变(本轮 diff 包含本文件,但未触碰挂载几何)。单一 RW 绑定挂载覆盖每棵流水线树的 .git gitfile:gitfile 位于树内、即挂载之内,容器化的 PR 代码可以把它改写为 gitdir: <同一挂载下种植的目录>,其中携带 filter.<x>.smudge;随后跟随该重定向的宿主侧 git 调用(探针树创建、每次沙箱化套件运行后的 restoreProbeTreeTracked)即获得宿主机代码执行。PR 描述已在「本 PR 关不掉的」一节披露该类(工作树内部污染、宿主侧 git 操作),因此本条作为携带的阻断项重新发布以存档,而非新发现——它仍是本 PR 的封装与宿主之间未关闭的边界。

证据:第 1-2 轮端到端复现——恢复检出与 git worktree add 两条路径均观察到宿主侧执行;scratch-tree 的身份门拒绝了同一棵树;本提交上挂载规格逐字节相同。

修复方向:把各树的 gitfile 从可写面中遮掉(例如在其上加只读绑定挂载),或让宿主侧消费者校验每棵树的 .git 仍解析到 <repo>/.git/worktrees/ 下自己的管理条目并拒绝挂载内重定向——或在 auto/required 于 CI 开启前先落地已记录的后续工作。

— qwen3.8-max via Qwen Code /review (v0.22.0)

The scrub list named the daemon selectors, then the proxy family, and still
missed DOCKER_API_VERSION — which selects no daemon at all. It just makes
every client call fail, and under `sandbox: auto` a failing availability
probe reads as "no runtime here", so a repository that ships one value in
its .env turns containment off without a word.

The class has no last entry. Replace the list with the rule it was trying to
approximate: a key the loader wrote from a file the reviewed checkout
supplies does not reach the process that decides whether containment
happened. The client needs no repository-provided environment for anything,
and the workload's environment is an allowlist either way.

Deleting restores rather than approximates: the loader records a key as
file-sourced only where the real environment had nothing, so a file value
never shadows an inherited one and the client keeps the operator's own
DOCKER_HOST, PATH and HOME.
@wenshao

ghost commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator Author

第 8 轮

DOCKER_API_VERSION — 成立,但按名单补它是错的修法

先确认机制:这个变量不选择任何 daemon,它只是让客户端的每一次调用都失败。而 containerRuntime() 的可用性探测把「客户端调不通」读成「这台机器没有容器运行时」,sandbox: auto 于是安静地退回直接执行。仓库在 .env 里塞一个值,就把容器化关掉了,且不留一句话。是真的。

但这已经是第二次被指出「清单漏了一项」——上一轮补的是代理家族,这一轮是一个连 daemon 都不指向的变量。这说明按名字枚举关不掉这个类:能改变客户端行为的仓库可写变量没有最后一项(不兼容的 API 版本、代理、配置路径、指向另一个 docker 二进制的 PATH……)。

所以换成它一直想逼近的那条规则本身——按来源,不按名字

for (const key of Object.keys(scrubbed)) {
  if (fileSourced(key)) delete scrubbed[key];
}

被审检出提供的文件写进来的键,不进入「决定是否发生了容器化」的那个进程。客户端本来就不需要任何仓库提供的环境;工作负载那一侧的环境两种写法下都是白名单。

删除是精确还原,不是近似

这一点我先去核了才敢这么写。加载器只在真实环境原本没有这个键时才写入并记账(config/environment.ts:593-598630-635isEffectivelyUnset),所以文件值永远不会遮盖继承来的值,删掉它等于把变量还原到加载前的状态。因此这次剥离不会让客户端丢掉操作者 shell 里的 PATHHOMEDOCKER_HOST——那些是有值的,永远不会被记为 file-sourced。

真实代价只有一条,也写进注释了:操作者若把某个值放在 .env 里,isFileSourcedEnvKey 分不清它和仓库自己的,会被一并丢掉,需要改设到 shell。在「谁的值可以决定容器化」这个问题上,保守到这一侧是对的。

两个方向都做了变异

  • 退回按名单剥离(只处理 DOCKER_HOST/HTTPS_PROXY)→ 测试变红
  • 无差别删除(不判来源)→ 测试同样变红

第二条是关键:没有它,这次修复可以退化成「把环境删光」还一路绿灯。测试用例里特意放了一个 SOMETHING_NOBODY_ENUMERATED,就是为了让「按名字」的写法过不去。

关于 auto 静默这件事本身

auto 在探测不到运行时的时候直接执行,这是它被定义出来的语义,不是缺陷。这一轮修掉的是仓库有能力制造这个探测失败。要的是保证而不是尽力,用 required

R1-3(RW 挂载里的 .git gitfile)

依然按此前记录的理由保留,判断没有变化。

全量 5626 通过,tsc / lint / prettier 干净。

附一条与本 PR 无关的观察,供参考不在本 PR 处理:AuthDialog.test.tsx 里的 drives API key provider steps from endpoint options metadata 会读到真实的 ~/.qwen/settings.json。我本机注册过一个自定义 provider,列表被挤偏一行,本地稳定失败而 CI 全绿。这是 main 上既有的测试隔离问题。

@wenshao

ghost commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

第 14 轮

4ddc611661。连续第三轮 posted: 0, fresh: 0

唯一的延后项确实是我上一轮编辑留下的:把用例包进 try/finally 时,那段 opt-out 说明被挪到了 stub 上方,而原来那份还留在 try 里面,同样三行隔几行出现了两次。删掉留下的那份。

R1-3

判断和状态与上一轮相同:机制我不争,等的是维护者对范围的决定,不是等实现。


review 套件 99 文件 4437 通过 / 2 跳过。tsc / lint / prettier 干净。

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite was not run on Windows locally — the changed test file's platform arms (skipIf gating, win32-only mount pin) run there.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite was not run on macOS locally — the changed test file's realpath/mount fixtures run there.

Test Plan (not a blocker): 324 passed — this review observed 23018 passed.

Deferred under the convergence posture (round 15, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:58 — [probe] sandboxPolicy's file-sourced guard and tighten-only property are unpinned (two surviving mutants)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:258 — [probe] refuseUnsandboxedPhase's pass path (container + required + mountable → final return null) is never executed by any test
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:234 — [probe] sandboxVerdict's auto-with-no-runtime → direct fallback return is unreached by any assertion
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:25 — [probe] containerPathFor (feeds --workdir at both spawn sites) has zero tests; realpath and parent-fallback mutants ship green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:459 — [probe] the --tmpfs assertion pins only the path prefix; the load-bearing mode=1777 is unpinned
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:231 — [probe] the auto-with-runtime → container verdict is never observed directly (null under auto cannot tell container from direct)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:639 — [probe] the symlinked-ANCESTOR half of redirectedAncestor is exercised by no test (only a link AT .qwen/tmp is planted)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:394 — [probe] the '.git stays outside' property inspects only the FIRST --volume; a second mount ships green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:76 — [probe] QWEN_CODE_CUSTOM_SANDBOX_IMAGE has no provenance test; unguard and delete-fallback mutants ship green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:129 — [probe] the provenance-scrub fixture is satisfiable by a 16-key name-blocklist mutant
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:204 — [probe] the SANDBOX-set + runtime-ANSWERING verdict cell is unpinned (runtime-conditioned shortcut mutant ships green)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:458 — [probe] nothing asserts npm_config_cache lines up with the mount; an off-mount cache mutant ships green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:550 — [probe] killContainer (the exported reaper) has zero tests; rm-without--f and no-op mutants ship green, failed reap silent
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:219 — [probe] containerRuntime (sandboxVerdict's default probe at every real call site) is referenced by no test
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:519 — [probe] --init is deliberately emitted by containerCommand but mentioned by no assertion
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:376 — [probe] the capitalised "Rootless" negative twin is never exercised; a value-blind matcher mutant ships green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:686 — [probe] image-override precedence is unpinned when both keys are set; an operand-swap mutant ships green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:60 — [probe] the garbled-value fall-through contract is unpinned for the SETTINGS layer (a typo behaves like auto)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:248 — [probe] no test reads the container verdict's runtime payload; hardcoded/swapped-runtime mutants ship green

[Critical] R1-3: still stands at this head — deliberately deferred by the author since round 1 (reaffirmed in the round-10 and round-14 replies; the round-15 delta is test-comment-only and does not touch the mechanism). The single RW bind mount covers the whole review temp dir (containerCommand mounts tmpDir:tmpDir RW at sandboxed-exec.ts:616-617, read directly at the reviewed commit), so it covers every pipeline tree's .git gitfile: the gitfile sits inside its tree, i.e. inside this mount, and containerized PR code can rewrite it to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge; the host-side git invocations that follow the redirect (probe-tree restore, base-tree rerun, discard-worktree sweeps) then execute the planted filter — host code execution out of the container. Round 13's live probe executed the planted smudge filter on the host; no filter/fsmonitor/replace neutralisation has landed on the restore/creation paths since (only scratch-tree carries the identity gate). Until the gitfiles sit outside the RW mount (or the filter surface is otherwise neutralised), required cannot claim to contain host execution.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite was not run on Windows locally — the changed test file's platform arms (skipIf gating, win32-only mount pin) run there。

未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite was not run on macOS locally — the changed test file's realpath/mount fixtures run there。

Test Plan(非阻断):324 passed — this review observed 23018 passed

收敛姿态下延后(第 15 轮,非阻断)——已记录,本轮不要求修改:共 19 条(原文未翻译,列表见上方英文部分)。

[Critical] R1-3: still stands at this head — deliberately deferred by the author since round 1 (reaffirmed in the round-10 and round-14 replies; the round-15 delta is test-comment-only and does not touch the mechanism). The single RW bind mount covers the whole review temp dir (containerCommand mounts tmpDir:tmpDir RW at sandboxed-exec.ts:616-617, read directly at the reviewed commit), so it covers every pipeline tree's .git gitfile: the gitfile sits inside its tree, i.e. inside this mount, and containerized PR code can rewrite it to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge; the host-side git invocations that follow the redirect (probe-tree restore, base-tree rerun, discard-worktree sweeps) then execute the planted filter — host code execution out of the container. Round 13's live probe executed the planted smudge filter on the host; no filter/fsmonitor/replace neutralisation has landed on the restore/creation paths since (only scratch-tree carries the identity gate). Until the gitfiles sit outside the RW mount (or the filter surface is otherwise neutralised), required cannot claim to contain host execution.

— qwen3.8-max via Qwen Code /review (v0.22.0)

Four cells the suite reached only by accident of the machine it ran on, each
carrying a live mutant on a property the PR description states as a
guarantee. All four functions already took their ambient dependency as an
injectable parameter, so this is assertions, not seams.

- sandboxPolicy: strictest-wins in BOTH directions, and a file-sourced value
  counting for nothing. "A repository cannot switch off the containment that
  exists to contain it" was described in a comment and asserted nowhere;
  env-overrides-settings, a looser strictest, and a dropped file-sourced guard
  all shipped green.
- sandboxVerdict: `auto` with nothing answering runs DIRECTLY. Every other
  case here either has a runtime or is `required`, so a mutant refusing
  instead — turning `auto` into `required` on every machine without docker —
  survived.
- refuseUnsandboxedPhase: the PASS path. Every other assertion about this gate
  is a refusal, so unconditional refusal under `required` — every review on a
  perfectly good host — survived.
- containerPathFor: it feeds `--workdir` at both spawn sites, and had no test
  at all. A lexical spelling names a directory the container does not have;
  the parent fallback is what lets a probe tree be named before it is built.

Mutation-checked, one at a time: seven mutants, seven reds.
@wenshao

ghost commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

第 15 轮

b11492e2cd。连续第四轮 posted: 0, fresh: 0

这四条延后项虽然被你标成建议级,但我认为它们比我前几轮修的某些东西更要紧,所以全部处理了——它们是本 PR 描述里当作保证写出来的性质上存活的变异。四个函数的环境依赖本来就是可注入参数,所以这次加的是断言,不是接缝。

  • sandboxPolicy — 「仓库无法关掉那个为了容纳它而存在的容器化」这句话,此前只写在注释里,一条断言都没有。三个变异都能全绿发车:env 覆盖 settings、strictest 取宽松者、丢掉 file-sourced 门。现在两个方向的「更严者胜」和「文件来源的值一文不值」都钉住了。
  • sandboxVerdictauto 且无运行时要直跑。其余用例要么有运行时、要么是 required,所以「改成拒绝」这个变异(等于在每台没有 docker 的机器上把 auto 变成 required)活着。
  • refuseUnsandboxedPhase放行路径。关于这道门的其他断言全是拒绝,所以「required 下无条件拒绝」(等于在每台健康宿主上拒掉每次评审)活着。
  • containerPathFor — 它在两个 spawn 点喂 --workdir,此前零测试。词法拼写指向容器里不存在的目录;父目录回退才让探针树可以「先命名后创建」。

逐条变异,七个变异,七次红。

R1-3

状态与前几轮相同:机制我不争,等的是维护者对范围的决定。


review 套件 99 文件 4441 通过 / 2 跳过。tsc / lint / prettier 干净。

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the new test file's platform-gated assertions (uid/symlink) bite exactly there.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform-gated assertions (win32/symlink privilege) bite exactly there.

Not explored to full depth (tool budget reached): "agent 2": run sandboxed-exec.test.ts under vitest to confirm it is green (no node_modules/dist in this worktree; full install+build exceeded remaining budget). Assertion-….

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 10.

Test Plan (not a blocker): 324 passed — this review observed 23018 passed.

Deferred under the convergence posture (round 16, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:58 — [review] sandboxPolicy's settings = operatorReviewSettings() default (every production decision shape) is never discriminated — mutant settings = {} ships green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:61 — [review] QWEN_REVIEW_SANDBOX .trim().toLowerCase() normalisation pinned by nothing — QWEN_REVIEW_SANDBOX=Required would silently fall through to off
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:171 — [review] SANDBOX_SET_UID_GID parse normalisation unpinned — SANDBOX_SET_UID_GID=False silently ignores the documented operator opt-out
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:178 — [review] containerCommand options literal pasted twice (uid A/B pair) plus a third near-identical base fixture — the pair can silently drift apart
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:203 — [review] SANDBOX-set AND runtime-answering verdict cell unpinned — mutant returning direct ships green, losing required's secret-stripping inside qwen --sandbox
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:220 — [review] refuseUnsandboxedPhase default-parameter wiring (the shape of all three production gates) never observed — default mutants turn refusals into passes
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:265 — [review] sandboxVerdict's env = process.env default (every production call shape) never discriminated — disclosure strings wrong on every production call
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:304 — [review] file-sourced-ignore gate's no-settings shape untested — a repo .env could flip an operator-less review to required
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:317 — [review] auto/off-with-runtime verdict cells unpinned — under auto on a daemon host the reviewed code would silently run with the full environment
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:318 — [review] auto-fallback disclosure ternary ('ran directly' vs 'does not strip its environment') has no assertion — branch swap ships green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:321 — [review] sandboxVerdict's probe = containerRuntime default (all three production call sites) unobserved — probe = () => null mutant ships green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:354 — [review] containerPathFor test leaks its mkdtemp tree in os.tmpdir() on every run — sibling mountRootFor block sweeps its fixtures, this one doesn't
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:367 — [review] containerPathFor's third answer — null from the inner catch — never reached by any test — lexical --workdir instead of the direct fallback
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:440 — [review] containerRuntime — the availability probe every sandboxVerdict turns on — has zero tests; inverting its success condition ships green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:440 — [review] runtimeIsRootless default reader wiring ( read = cachedInfoDocument , both production call sites) never observed — rootless hosts answer rootful
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:497 — [review] file: opts.runtime pass-through pinned only against 'docker' — a hardcoded-'docker' mutant ships green and ENOENTs every podman host
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:555 — [review] env canary inspects only the --env long spelling — -e / --env-file forwarding re-introduction escapes every assertion
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:555 — [review] valueless --env KEY pass-through crosses the canary (the value resolves from the client env, never appears in argv) — pin the whole token surface
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:562 — [review] npm_config_cache allowlist wiring (the one entry composed at the call site) is never asserted — cache-off-mount mutant ships green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:563 — [review] HOME tmpfs writability ( rw,mode=1777 ) unpinned — a :ro regression ships green and every contained npm ci fails before it starts
  • …and 4 more (see the run report)

[Critical] R1-3 (packages/cli/src/commands/review/lib/sandboxed-exec.ts:616 — file not in this round's diff, so this blocker cannot be anchored inline): still stands at this head — deliberately deferred by the author since round 1 (reaffirmed in the round-10 and round-14 replies); the mechanism is unchanged and the round-16 delta (test-only, sandboxed-exec.test.ts) does not touch it. The single RW bind mount covers the whole review temp dir (containerCommand mounts tmpDir:tmpDir RW at sandboxed-exec.ts:616-617, read directly at the reviewed commit), so it covers every pipeline tree's .git gitfile: the gitfile sits inside its tree, i.e. inside this mount, and containerized PR code can rewrite it to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge; the host-side git invocations that follow the redirect (probe-tree restore, base-tree rerun, discard-worktree sweeps) then execute the planted filter — host code execution out of the container. Round 13's live probe executed the planted smudge filter on the host (canary present; honest-gitfile control clean); no filter/fsmonitor/replace neutralisation has landed on the restore/creation paths since (only scratch-tree carries the identity gate). Until the gitfiles sit outside the RW mount (or the filter surface is otherwise neutralised), required cannot claim to contain host execution.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on macOS locally — the new test file's platform-gated assertions (uid/symlink) bite exactly there。

未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and the packages/cli suite did not run on Windows locally — the new test file's platform-gated assertions (win32/symlink privilege) bite exactly there。

未探索到全部深度(达到工具调用预算):"agent 2"run sandboxed-exec.test.ts under vitest to confirm it is green (no node_modules/dist in this worktree; full install+build exceeded remaining budget). Assertion-…

未审查:反向审计——在 10 轮的反审轮数上限内未收敛。

Test Plan(非阻断):324 passed — this review observed 23018 passed

收敛姿态下延后(第 16 轮,非阻断)——已记录,本轮不要求修改:共 24 条(原文未翻译,列表见上方英文部分)。

[Critical] R1-3 (packages/cli/src/commands/review/lib/sandboxed-exec.ts:616 — file not in this round's diff, so this blocker cannot be anchored inline): still stands at this head — deliberately deferred by the author since round 1 (reaffirmed in the round-10 and round-14 replies); the mechanism is unchanged and the round-16 delta (test-only, sandboxed-exec.test.ts) does not touch it. The single RW bind mount covers the whole review temp dir (containerCommand mounts tmpDir:tmpDir RW at sandboxed-exec.ts:616-617, read directly at the reviewed commit), so it covers every pipeline tree's .git gitfile: the gitfile sits inside its tree, i.e. inside this mount, and containerized PR code can rewrite it to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge; the host-side git invocations that follow the redirect (probe-tree restore, base-tree rerun, discard-worktree sweeps) then execute the planted filter — host code execution out of the container. Round 13's live probe executed the planted smudge filter on the host (canary present; honest-gitfile control clean); no filter/fsmonitor/replace neutralisation has landed on the restore/creation paths since (only scratch-tree carries the identity gate). Until the gitfiles sit outside the RW mount (or the filter surface is otherwise neutralised), required cannot claim to contain host execution.

— qwen3.8-max via Qwen Code /review (v0.22.0)

`"Required"` in settings.json — or a stray trailing space — matched no policy,
resolved to `off`, and disabled the containment the operator had just asked
for. Silently. The environment value was already normalised; the settings
value was not, and the asymmetry fell on the wrong side: settings is the
documented place to turn this ON, since the environment can only tighten. So
the unnormalised half was the half operators actually use, and a fail-open on
the one setting whose whole purpose is to fail closed.

Reported as a suggestion in rounds 8, 9 and 16 and deferred each time under
the critical-only posture. Measured this round: both `"Required"` and
`"required "` returned `off`.

Also pins two shapes the new tests left open. `sandboxPolicy`'s settings
default is the production path every real caller takes, and with it swapped
for `{}` the whole settings half stopped being consulted while every
assertion — each passing settings explicitly — stayed green; it is now driven
through an isolated settings file. And the `SANDBOX_SET_UID_GID` opt-out
parses case- and space-insensitively, which nothing asserted.

The remaining deferral, a fixture literal pasted twice, is duplication rather
than a defect and is left alone.
@wenshao

ghost commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

第 16 轮

9f5e5ad093。连续第五轮 posted: 0, fresh: 0

归一化那条:我不再放它了,而且实测比你写的更宽

它在第 8、9、16 轮都出现过,每次都因为「只处理 critical」被我延后。这轮我先量了:

sandboxPolicy({}, { sandbox: 'Required'  })  →  off
sandboxPolicy({}, { sandbox: 'required ' })  →  off
sandboxPolicy({ QWEN_REVIEW_SANDBOX: 'Required'  }, {})  →  required
sandboxPolicy({ QWEN_REVIEW_SANDBOX: ' required ' }, {})  →  required

不只是大小写,尾随空格一样中招。env 侧早就归一化了,settings 侧没有——而不对称落在了错的一边:settings.json 才是文档里推荐的开启方式(env 只能收紧)。也就是说,没做归一化的那一半,恰恰是操作者真正会用的那一半。操作者刚打开这个开关,就被静默关掉了,而这个开关存在的全部意义就是失败时要关向安全侧。

所以按建议级放了三轮之后,我把它当缺陷修了。变异:去掉归一化 → 红。

另外两条是我上一轮新测试自己留下的口子

  • sandboxPolicy 的 settings 默认值:那是每个真实调用者走的生产路径。把默认换成 {},整个 settings 半边就不再被查询,而我上一轮所有断言都显式传了 settings,所以全绿。现在用一个隔离的 settings 文件真正驱动它。变异 → 红。
  • SANDBOX_SET_UID_GID 的解析:大小写和空格都做了归一化,但没有任何断言。补上 False 一例。变异(改成严格相等)→ 红。

没修的那条

夹具字面量粘了两份——那是重复,不是缺陷,而这个 PR 已经太大。留着。


review 套件 100 文件 4446 通过 / 2 跳过。tsc / lint / prettier 干净。

R1-3

状态不变:机制我不争,等的是维护者对范围的决定。

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • stale sandboxVerdict JSDoc (SANDBOX-set session 'returns direct') — already reported (comment 3836852398)
  • SandboxVerdict.disclose produced but never surfaced in any phase output — already reported (comment 3836396804)
  • orphaned restoreProbeTreeTracked doc comment after probeContainer insertion — already reported (comment 3836852318)

Not reviewed: reverse audit — stopped before round 4 by the review time budget.

Test Plan (not a blocker): 324 passed — this review observed 23019 passed.

Deferred under the convergence posture (round 17, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:321 — [probe] normalization test pins only 'required' spellings — 'Auto'/'Off' operator spellings unpinned on both sides, mutant survives
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:63 — [probe] garbled-env assertion pins only the empty-settings cell — the garbled-env + valid-settings interaction mutant survives
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:345 — [probe] sandboxPolicy's env-half default (process.env) is exercised by no test — env={} default mutant survives both consuming suites
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

Test Plan(非阻断):324 passed — this review observed 23019 passed

收敛姿态下延后(第 17 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +622 to +623
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

ghost Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-3: still stands at this head — deliberately deferred by the author since round 1 (reaffirmed in the round-10, round-14 and round-16 replies); the round-17 delta (the sandboxPolicy settings normalization plus tests) does not touch the mount geometry. The single RW bind mount covers the whole review temp dir (containerCommand mounts tmpDir:tmpDir RW at lines 622-623, read directly at the reviewed commit), so it covers every pipeline tree's .git gitfile: the gitfile sits inside its tree, i.e. inside this mount, and containerized PR code can rewrite it to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge; the host-side git invocations that follow the redirect (probe-tree restore, base-tree rerun, discard-worktree sweeps) then execute the planted filter — host code execution out of the container. Round 13's live probe executed the planted smudge filter on the host (canary present; honest-gitfile control clean); no filter/fsmonitor/replace neutralisation has landed on the restore/creation paths since (only scratch-tree carries the identity gate). Until the gitfiles sit outside the RW mount (or the filter surface is otherwise neutralised), required cannot claim to contain host execution.

中文说明

R1-3:在本 head 上依旧成立——自第 1 轮起由作者有意延后(第 10、14、16 轮答复中再次确认);第 17 轮的增量(sandboxPolicy 的 settings 归一化及测试)没有触碰挂载几何。单一的 RW 绑定挂载覆盖整个 review 临时目录(containerCommand 在 622-623 行以 tmpDir:tmpDir RW 挂载,已在被审提交直接读取),因此也覆盖了每棵流水线树的 .git gitfile:gitfile 位于自己的树内、即位于本挂载之内,容器化的 PR 代码可以把它改写为携带 filter.<x>.smudgegitdir: <同一挂载下的植入目录>;随后跟随该重定向的宿主侧 git 调用(探针树恢复、基线树重跑、丢弃工作树的清扫)就会执行植入的 filter——容器之外的宿主代码执行。第 13 轮的活体探针已在宿主上执行了植入的 smudge filter(金丝雀出现;诚实 gitfile 对照干净);此后 restore/创建路径上没有落地任何 filter/fsmonitor/replace 中和(只有 scratch-tree 带身份门)。在 gitfile 移出 RW 挂载(或以其他方式中和 filter 面)之前,required 无法声称约束了宿主执行。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Last round's normalisation fix was asserted only against `required`
spellings, which is the shape a mutant keyed on that one value walks
straight through — leaving an operator's `"Auto"` resolving to `off`, the
same silent downgrade one rung lower. Assert every policy on both sides.

Two adjacent cells with it. A garbled ENVIRONMENT value must be dropped on
its own rather than taking the operator's setting down with it: the
environment is the half a repository can reach, so garbage there must never
answer for the half it cannot. And `sandboxPolicy`'s env default is the twin
of the settings default pinned last round — every assertion here hands it an
env literal, so `env = {}` as the default stops the environment half being
read at all and nothing notices.

Three mutants, three reds.
@wenshao

ghost commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

第 17 轮

fd97d082e7posted: 1, fresh: 0——那 1 条是 R1-3 的重复计入,连续第六轮没有新发现。

三条延后项都成立,都是我上一轮新测试的下一圈未钉单元,都修了:

  • 归一化只断言了 required 这一行。按那个值做条件的变异能直接走过去,结果是操作者写 "Auto" 解析成 off——同一个静默降级,只是低一档。现在两侧的每个策略值都断言了。
  • 无法解析的 env 值不能把 settings 一起带下水。这条是三条里最要紧的:env 是仓库能够得着的那一半,所以那里的垃圾绝不能替它够不着的那一半作答。变异(env 值解析失败时整体归零)→ 红。
  • sandboxPolicy 的 env 默认值,和我上一轮钉住的 settings 默认值是一对。这个 describe 里每条断言都显式传 env 字面量,所以把默认换成 {} 之后环境那一半根本不再被读,而没有一条会察觉。

三个变异,三次红。

说一句方法层面的观察

从第 15 轮起,每轮的延后项都是「我上一轮新加的测试没有钉住的下一圈单元」。这个方向本身没有尽头——测试的测试的测试。我这三轮之所以还在跟,是因为每一圈里都还有真性质(仓库无法关掉容器化、env 垃圾不能替 settings 作答、生产默认路径无人经过)。等哪一轮的延后项只剩形状而没有性质了,我会停在那里并说明理由,而不是继续往下钉。


review 套件 99 文件 4444 通过 / 2 跳过。tsc / lint / prettier 干净。

R1-3

状态不变。

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • reviewSandboxImage's second pick (QWEN_CODE_CUSTOM_SANDBOX_IMAGE) untested in both directions — already reported (comment 3837175067)
  • the allowlist canary inspects only the VALUES of the forwarded --env entries, never the SET — already reported (comment 3837175072)

Test Plan (not a blocker): 324 passed — this review observed 23019 passed.

Deferred under the convergence posture (round 18, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:422 — [probe] the containerPathFor test leaks its qwen-workdir-* temp fixture every run (the mountRootFor sibling cleans up in afterEach)
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:92 — [review] the paired SANDBOX_SET_UID_GID tests paste the containerCommand options literal twice instead of sharing one fixture
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:730 — [review] killContainer — the reap itself — is executed by no test anywhere; garbling the spawnSync ships the suite green
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:381 — [review] containerRuntime's real availability probe (docker-then-podman status check + cache) is exercised by no test
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:435 — [probe] containerPathFor's return-null branch is unpinned — probe-measured: dropping the try/catch ships green
中文说明

本轮确认的 2 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

Test Plan(非阻断):324 passed — this review observed 23019 passed

收敛姿态下延后(第 18 轮,非阻断)——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

// file one run rewrote, an ignored plant a sweep honoured. `--rm` is
// isolation by construction rather than by hygiene.
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

ghost Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-3: still stands at this head — deliberately deferred by the author since round 1; the round-18 delta is test-only and does not touch the mount geometry. The single RW bind mount covers the whole review temp dir (--volume ${tmpDir}:${tmpDir}, containerCommand lines 622-623), so it covers every pipeline tree's .git gitfile: the gitfile sits inside its tree, i.e. inside this mount. Containerized PR code can rewrite it to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge, and the host-side git invocations that follow the redirect (probe/base/scratch tree creation, post-run git operations) then execute the planted filter — host code execution. Witness: carried from round 1 (live probe — the gitfile redirect inside the mount reaches a planted repo and its filter executes on the host-side git call); re-verified at fd97d08 that the mount line is byte-identical. Fix direction: exclude each tree's .git gitfile from the RW mount (read-only bind or per-tree mounts), or have the host-side consumers refuse a gitdir redirect that stays under the mounted temp dir — the same fail-closed shape redirectedAncestor gives the mount walk.

中文说明

R1-3:在本 head 上依旧成立——自第 1 轮起由作者明确延后;第 18 轮的增量只动了测试文件,未触及挂载几何。单一 RW 绑定挂载覆盖整个 review 临时目录(--volume ${tmpDir}:${tmpDir}containerCommand 第 622-623 行),因此也覆盖了每条流水线树的 .git gitfile:gitfile 位于树内,即位于此挂载之内。容器化的 PR 代码可以把它改写为 gitdir: <同一挂载下的植入目录>,其中带 filter.<x>.smudge;随后跟随该重定向的宿主侧 git 调用(探针/基线/草稿树的创建、运行后的 git 操作)就会执行植入的 filter——宿主代码执行。证据:沿用第 1 轮的活体探针(挂载内的 gitfile 重定向到达植入仓库,其 filter 在宿主侧 git 调用时执行);并已在 fd97d08 复核挂载行逐字节未变。修复方向:把每棵树的 .git gitfile 排除在 RW 挂载之外(只读绑定或按树挂载),或让宿主侧消费者拒绝仍落在挂载临时目录之下的 gitdir 重定向——与 redirectedAncestor 为挂载游走提供的失败即关闭形态一致。

— qwen3.8-max via Qwen Code /review (v0.22.0)

…ng a fixture

Three of the four recorded this round; the fourth is duplication, not a
defect, and is left alone for the third time.

`killContainer` is the whole answer to a container that outlived the client's
deadline, and nothing anywhere ran it — a garbled argv, or a dropped `-f`,
shipped green, and what survives is a container holding the review tree open
past the end of the run. It now takes its spawn as a parameter, so the argv
and the swallow-and-continue contract are both asserted.

`containerRuntime`'s probe had the same hole with a memo and a real daemon
call in the way. The decision is now separated from both: `firstAnsweringRuntime`
is order and nothing else, which is the content — a client installed but not
running must never shadow one that is.

And the `containerPathFor` case left a temp tree behind on every run. In a
change about not leaving residue behind, that one is just embarrassing.
@wenshao

ghost commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

第 18 轮

62040e4f1aposted: 1, fresh: 0——那 1 条是 R1-3,连续第七轮没有新发现。

按我上一轮说的判据(钉的是性质还是形状)逐条筛,四条里做了三条:

  • killContainer 从来没有被任何测试执行过。它是「容器活过了客户端的 deadline」这件事的全部答案,而 argv 写坏、-f 丢掉,套件都是绿的——活下来的是一个把 review 树占住、活过整轮评审的容器。现在 spawn 是参数,argv 和「吞掉异常不制造第二次失败」两条都断言了。变异(丢 -f)→ 红。
  • containerRuntime 的探测同样有洞,只是隔着一层记忆化和一次真实 daemon 调用。把决策抽出来:firstAnsweringRuntime 只剩顺序,而顺序就是全部内容——装了但没跑起来的客户端,绝不能盖过真正在跑的那个。变异(返回第一个名字而不看是否应答)→ 红。
  • containerPathFor 那条每跑一次泄漏一个临时目录。在一个讲「不留残留」的改动里,这条纯属难看。已按 mountRootFor 那块的写法清扫;实测跑前 24、跑后 24(另外那 24 个是我之前几轮跑出来的,一并清了)。

第四条(夹具字面量粘两份)第三次出现,仍然不做:那是重复,不是缺陷。


review 套件 99 文件 4446 通过 / 2 跳过。tsc / lint / prettier 干净。

R1-3

状态不变。

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed.

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

Test Plan (not a blocker): 324 passed — this review observed 23027 passed.

Deferred under the convergence posture (round 19, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:570 — [review] orphaned docblock: containerCommand's argv contract attached to containerSeq
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:151 — [probe] daemonAnswers answer-criterion and containerRuntime memo exercised by no test
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:451 — [probe] reap test's fake spawn discards the options bag — env scrub and timeout unpinned
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:623 — [probe] containerCommand's runtime passthrough pinned only for docker, never podman
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:690 — [probe] --tmpfs assertion pins only the path prefix; rw,mode=1777 unpinned
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:210 — [probe] already-sandboxed anti-shortcut test misses the runtime-answering cell
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:911 — [probe] mountRootFor's catch→null branch untested; two gate chains have no catch
  • packages/cli/src/commands/review/lib/sandboxed-exec.ts:625 — [probe] --init flag pinned by no test
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:920 — [probe] default image pinned only by substring 'sandbox'
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:917 — [probe] pick().trim() normalization exercised by no fixture
  • packages/cli/src/commands/review/lib/sandboxed-exec.test.ts:566 — [probe] runtimeIsRootless default-reader wiring and info-document memo unpinned

[Critical] R19-1 (new this round, high confidence; dropped from inline only by the same-line overlap dedup against an unrelated round-1 comment at sandboxed-exec.ts:641, comment 3836852396 — the finding itself is verified, not a duplicate): The single RW bind mount (--volume ${tmpDir}:${tmpDir}, sandboxed-exec.ts:641-642) also covers the pipeline's worktree-LEASE files — leaseDirectory = join(repositoryRoot, REVIEW_TMP_DIR) in review-worktree-lease.ts, direct children of the mounted temp dir. The container runs as the host uid (--user uid:gid, or rootless root = host user), so reviewed code can read, plant, edit and delete leases. The session's own lease is written by fetch-pr before any containerized command runs and leaks sessionId/promptId/repositoryRoot. cleanupReviewWorktreeLeases matches leases by (sessionId, promptId) only — never validates provenance — then runs git worktree remove --force + git branch -D on whatever worktreePath/branch the lease names, invoked with the session's ids at prompt end/process exit (useGeminiStream.ts:4034, nonInteractiveCli.ts:546, Session.ts:5178). Failure scenario: during the networked npm ci (host uid, mount RW), a malicious postinstall reads sibling leases under <repo>/.qwen/tmp/ (every concurrent review of this checkout, enumerable from the mount), edits or plants a lease naming a victim session's ids with the worktreePath/branch of another session's tree — at the victim's prompt end, cleanup force-removes the wrong worktree and deletes the wrong branch: cross-session data loss from inside 'contained' execution. Cheaper wedge needing no secrets: plant a lease with any foreign sessionId → cleanup refuses the tree (cleanup.ts:746), fetch-pr throws on the EEXIST holder mismatch, and nothing ever sweeps a foreign-session lease — permanent denial of review for that PR on that machine until an operator deletes the file by hand. Witness (probe in a scratch checkout with two sessions Y/pr-1 and Z/pr-2, invoking the real cleanupReviewWorktreeLeases): edit-in-place of Y's lease (worktreePath → Z's tree): BEFORE review-pr-2 exists: trueAFTER review-pr-2 exists: false; planted sibling lease for pr-2 carrying Y's readable ids: AFTER review-pr-2 exists: false, AFTER branch qwen-review/pr-2 exists: false; foreign-sessionId wedge: blocks fetch-pr/cleanup of session "some-other-session": true, foreign lease survives the automatic sweep: true. Flip check: a lease-content guard flipped the edit-in-place arm but NOT the well-formed planted lease — content-indistinguishable, so the fix belongs at the mount boundary. Distinct from R1-3 (different asset, mechanism, harm). Fix direction: do not keep host-trusted lease state under the container's RW mount — move lease files out of REVIEW_TMP_DIR (e.g. a sibling dir like <repo>/.qwen/review-leases/), or validate lease provenance at read time before acting on it; whatever geometry fix lands for R1-3 should explicitly name this artifact class too.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

Test Plan(非阻断):324 passed — this review observed 23027 passed

收敛姿态下延后(第 19 轮,非阻断)——已记录,本轮不要求修改:共 11 条(原文未翻译,列表见上方英文部分)。

[Critical] R19-1 (new this round, high confidence; dropped from inline only by the same-line overlap dedup against an unrelated round-1 comment at sandboxed-exec.ts:641, comment 3836852396 — the finding itself is verified, not a duplicate): The single RW bind mount (--volume ${tmpDir}:${tmpDir}, sandboxed-exec.ts:641-642) also covers the pipeline's worktree-LEASE files — leaseDirectory = join(repositoryRoot, REVIEW_TMP_DIR) in review-worktree-lease.ts, direct children of the mounted temp dir. The container runs as the host uid (--user uid:gid, or rootless root = host user), so reviewed code can read, plant, edit and delete leases. The session's own lease is written by fetch-pr before any containerized command runs and leaks sessionId/promptId/repositoryRoot. cleanupReviewWorktreeLeases matches leases by (sessionId, promptId) only — never validates provenance — then runs git worktree remove --force + git branch -D on whatever worktreePath/branch the lease names, invoked with the session's ids at prompt end/process exit (useGeminiStream.ts:4034, nonInteractiveCli.ts:546, Session.ts:5178). Failure scenario: during the networked npm ci (host uid, mount RW), a malicious postinstall reads sibling leases under <repo>/.qwen/tmp/ (every concurrent review of this checkout, enumerable from the mount), edits or plants a lease naming a victim session's ids with the worktreePath/branch of another session's tree — at the victim's prompt end, cleanup force-removes the wrong worktree and deletes the wrong branch: cross-session data loss from inside 'contained' execution. Cheaper wedge needing no secrets: plant a lease with any foreign sessionId → cleanup refuses the tree (cleanup.ts:746), fetch-pr throws on the EEXIST holder mismatch, and nothing ever sweeps a foreign-session lease — permanent denial of review for that PR on that machine until an operator deletes the file by hand. Witness (probe in a scratch checkout with two sessions Y/pr-1 and Z/pr-2, invoking the real cleanupReviewWorktreeLeases): edit-in-place of Y's lease (worktreePath → Z's tree): BEFORE review-pr-2 exists: trueAFTER review-pr-2 exists: false; planted sibling lease for pr-2 carrying Y's readable ids: AFTER review-pr-2 exists: false, AFTER branch qwen-review/pr-2 exists: false; foreign-sessionId wedge: blocks fetch-pr/cleanup of session "some-other-session": true, foreign lease survives the automatic sweep: true. Flip check: a lease-content guard flipped the edit-in-place arm but NOT the well-formed planted lease — content-indistinguishable, so the fix belongs at the mount boundary. Distinct from R1-3 (different asset, mechanism, harm). Fix direction: do not keep host-trusted lease state under the container's RW mount — move lease files out of REVIEW_TMP_DIR (e.g. a sibling dir like <repo>/.qwen/review-leases/), or validate lease provenance at read time before acting on it; whatever geometry fix lands for R1-3 should explicitly name this artifact class too.

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +641 to +642
'--volume',
`${opts.tmpDir}:${opts.tmpDir}`,

ghost Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] R1-3: still stands at this head — deliberately deferred by the author since round 1 (reaffirmed in the round-10, round-14 and round-16 replies); the round-19 delta (the firstAnsweringRuntime extraction, killContainer's injectable spawn parameter, and their tests) does not touch the mount geometry. The single RW bind mount covers the whole review temp dir (--volume ${tmpDir}:${tmpDir}, containerCommand lines 641-642), so it covers every pipeline tree's .git gitfile: the gitfile sits inside its tree, i.e. inside this mount. Containerized PR code can rewrite it to gitdir: <planted dir under the same mount> carrying filter.<x>.smudge, and the host-side git invocations that follow the redirect (probe/base/scratch tree creation, post-run git operations) then execute the planted filter — host code execution. Witness: carried from round 1 (live probe — the gitfile redirect inside the mount reaches a planted repo and its filter executes on the host-side git call); re-verified at 62040e4 that the mount line is byte-identical through this round's delta. Fix direction: exclude each tree's .git gitfile from the RW mount (read-only bind or per-tree mounts), or have the host-side consumers refuse a gitdir redirect that stays under the mounted temp dir — the same fail-closed shape redirectedAncestor gives the mount walk. Note: round 19 surfaced a sibling artifact of the same mount width — worktree-lease files — reported in the review body.

中文说明

R1-3:在本 head 上依旧成立——自第 1 轮起由作者明确延后(第 10、14、16 轮的回复中再次确认);第 19 轮的增量(firstAnsweringRuntime 提取、killContainer 的可注入 spawn 参数及其测试)未触及挂载几何。单一 RW 绑定挂载覆盖整个 review 临时目录(--volume ${tmpDir}:${tmpDir}containerCommand 第 641-642 行),因此也覆盖了每条流水线树的 .git gitfile:gitfile 位于树内,即位于此挂载之内。容器化的 PR 代码可以把它改写为 gitdir: <同一挂载下的植入目录>,其中带 filter.<x>.smudge;随后跟随该重定向的宿主侧 git 调用(探针/基线/草稿树的创建、运行后的 git 操作)就会执行植入的 filter——宿主代码执行。证据:沿用第 1 轮的活体探针(挂载内的 gitfile 重定向到达植入仓库,其 filter 在宿主侧 git 调用时执行);并已在 62040e4 复核挂载行在本轮增量中逐字节未变。修复方向:把每棵树的 .git gitfile 排除在 RW 挂载之外(只读绑定或按树挂载),或让宿主侧消费者拒绝仍落在挂载临时目录之下的 gitdir 重定向——与 redirectedAncestor 为挂载游走提供的失败即关闭形态一致。注:第 19 轮发现了同一挂载宽度下的另一类产物——worktree 租约文件——已在评审正文中报告。

— qwen3.8-max via Qwen Code /review (v0.22.0)

@wenshao

ghost commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

第 19 轮

R19-1 我逐条核过,成立——而且有一处你的表述需要更正,方向对你不利也对我不利,先说清楚。

核实的部分

三条关键事实都对上了:

  • leaseDirectory(repositoryRoot) = join(repositoryRoot, REVIEW_TMP_DIR)services/review-worktree-lease.ts:62-63),就是挂载根本身。
  • cleanupReviewWorktreeLeases 只按 (sessionId, promptId, repositoryRoot) 匹配(:316-322),没有任何来源校验。
  • removeLeaseWorktree 的护栏我也看了:target 必须匹配 ^pr-(\d+)$branch 必须等于由 target 派生的 reviewBranch(N)worktreePath 必须在 REVIEW_TMP_DIR 之内。所以危害被限定在「另一个 review 会话的树 + 它的 qwen-review/pr-N 分支」——你没有夸大,描述的正是这个范围

它与 R1-3 确实是不同的资产、机制和危害,但同一个根因:宿主可信状态待在容器的可写面之内。

需要更正的部分

cross-session data loss from inside 'contained' execution

这个措辞会让人以为是容器化制造了这个暴露面。不是。我去 origin/main 上核了:

origin/main  build-test.ts:325
  spawnSync(command, { cwd, shell: true, ..., env: buildRunEnv() })
origin/main  review-worktree-lease.ts:62-63
  leaseDirectory = join(repositoryRoot, REVIEW_TMP_DIR)

也就是说,今天在 main 上,被审仓库的 npm ci 就是以宿主用户身份、在毫无文件系统限制的情况下跑的,<repo>/.qwen/tmp 下的每个 lease 本来就可读可写可删——而且不止 lease,整台机器上这个用户能碰的一切都可以。容器化把可达范围收窄到挂载之内,不是扩大。

所以 R19-1 是既有漏洞,不是本 PR 引入的回归。这不是推脱:它仍然是真问题,仍然该修;但归因错了会让人以为「不上容器更安全」,而事实恰好相反。

我做了什么,没做什么

做了:把这个资产类别写进 PR 描述的「本 PR 关不掉的」一节,和 gitfile 并列点名,并写明两者都不是本改动打开的、以及为什么。一个声称容器化的 required 应当把没关掉的东西说出来,而不是让人以为相反。

没做:没有在本 PR 里改 lease 的落盘位置。理由有三条,第三条是你自己给的:

  1. 它不是本 PR 引入的(上面已实证)。
  2. 它落在 services/review-worktree-lease.tscleanup.ts 的生命周期代码上,而本 PR 已经 +2100/-80、19 个提交;这类改动在本 PR 的历史里从来不是「一处小改」,它会自己长出五轮。
  3. 你自己写的:whatever geometry fix lands for R1-3 should explicitly name this artifact class too ——你也认为它属于 R1-3 那次几何修复。

所以它和 R1-3 一起,作为同一个待定范围决策的一部分交给维护者。本轮其余 11 条延后项按同一判据(钉的是性质还是形状)留待下一轮。

# Conflicts:
#	packages/cli/src/commands/review/lib/review-settings.ts
@wenshao

ghost commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

ghost commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 83 passed · 0 failed · 83 total

Flakiness gate: ✅ 4 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:83 通过 · 0 失败 · 83 总计

抖动门:✅ 4 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR 9723 deep verification — review sandbox (review.sandbox)

Verdict: merge-ready — 83/83 scripted assertions passed, 0 unexpected failures
(one non-blocking Suggestion below; the full-suite gate count is cited in
Targeted gates).
Verified head: 5d2a126de0109fcce82908c2cbf365e1f6f00c4b (git rev-parse HEAD^2),
base tip 22bb5e8b9ff815f2fbb5d6013bec27a67cb0b2ee, merge commit fcd8285b9c.

中文摘要
  • 判定:merge-ready 83/83 条脚本化断言通过,0 条意外失败;唯一发现是一条
    Suggestion 级覆盖缺口(见下),且作者已在提交信息中自认。
  • A/B 结论01-ab-host-cells.png):基线侧按原样复现泄漏——被审命令看得到
    OPENAI_API_KEY/GH_TOKEN 金丝雀;head 侧 offauto(无运行时)行为与基线
    完全一致(直接 spawn、整份环境);required 且无运行时答复时,真实
    runTestDelta 链路 0 次执行 并报 "NOTHING was attributed"——fail-closed 成立。
  • 容器 argv 与真实运行时02-argv-policy-oracle.png03-contained-test-run.png):
    本机 verify 容器挂着一个活的 docker daemon(引擎 29.1.3,客户端缺失、由本验证自行
    补齐静态二进制)。模块生成的 argv 在真实容器内逐条兑现:环境恰为五条白名单、
    金丝雀不外泄、id 为宿主 1000:1000、$HOME 是可写 tmpfs、test 类 --network none
    下回环可用而外网被阻(EAI_AGAIN)、install 类外网放行(registry 200);
    TERM 忽略型负载在截止期被按名字收割,无孤儿容器。挂载是 review 临时目录(最深
    一层 .qwen/tmp),符号链接祖先与冒号路径被拒。
  • 仓库自带 .qwen/.env 的攻击链经由真实 loadEnvironment 驱动:
    QWEN_REVIEW_SANDBOX=off 无法压低设置侧 required;镜像覆盖、
    SANDBOX_SET_UID_GID=falseDOCKER_HOST/DOCKER_CONFIG 以及未按名字枚举的
    文件来源键全部被拦;操作者自己的同名真实环境变量仍被尊重(双向对照)。
  • 变异矩阵04-mutation-matrix.png):四条关键 hunk(CI=1、env 白名单、
    只紧不松、最深挂载点)各自变异后均被对应测试以预期断言杀死;阳性对照成立。
    唯一存活者:把 runBuildTestUnguarded 里的相位门整块删掉,
    build-test.test.ts 109/109 仍绿——相位门函数本身有测试钉住,但
    runBuildTest 调用它这件事没有测试钉住
    ;作者在提交信息中已自认
    ("What stays unpinned is only that runBuildTest calls it")。分类为覆盖缺口
    (Suggestion),并给出会去红的夹具形状。
  • 未覆盖:逐提交归因(depth-2 浅克隆,23 个提交不可达,仅验证聚合 diff);
    宿主文件经挂载进入容器的端到端运行(本机 daemon 与 verify 容器不共享文件系统
    命名空间,绑定挂载不传播——已用实验证明,属环境限制而非 PR 属性);
    原生模块在镜像内的构建;rootless podman;Windows 分支;
    工作流接入(PR 本身声明另行提交)。

Central claim

When review.sandbox resolves to required, the reviewed repository's own
commands (build-test's install/build/test, test-efficacy's probe suites, and
test-delta's base-side rerun) either run inside a container — env allowlist,
review-temp-dir mount, network only for installs — or the phase refuses;
they never fall back to a direct spawn with the full environment.
Default off is today's behaviour exactly.

Secondary claims: (A) the reviewed repository cannot lower its own containment
(workspace settings skipped; file-sourced env ignored for policy/image/uid and
scrubbed from the runtime client; env can only tighten); (B) the argv encodes
the three measured decisions plus uid mapping, tmpfs HOME, --rm --init --name,
and reap-by-name on abnormal exit.

A/B table (host-side cells — 01-ab-host-cells.png)

Each cell is a fresh process driving the real code: base cell reproduces
base run() byte-for-byte (quoted from git show HEAD^1:…/build-test.ts:
spawnSync(command, {cwd, shell:true, …, env: buildRunEnv()}) with
buildRunEnv = {...process.env, CI, npm_config_yes, QWEN_SKIP_PREPARE});
head cells import the compiled dist/ at the merge head. Canary secrets
(OPENAI_API_KEY, GH_TOKEN) planted in the review process's env; a fake
<repo>/.git/secret-filter sits outside .qwen/tmp.

cell code under test env canary visible <repo>/.git readable executions
base-direct HEAD^1 spawn pattern yes (predicted) yes 1
head-off head run(), policy off yes (= base) yes 1
head-auto, no runtime head run(), auto yes (direct fallback, by design) yes 1
head-required, no runtime real refuseUnsandboxedPhase + full runTestDelta chain n/a n/a 0 — "NOTHING was attributed"
head, no policy (delta) real runTestDelta n/a n/a 1 (no blanket refusal)

17/17 assertions. The base cell's leak is the control assertion (base is
expected to leak — that is the defect the PR exists to close), encoded as a
passing expectation in the harness.

The author's "checkable rather than argued" base claim was checked by direct
reading of the parent commit and holds: build-test.ts passed
env: buildRunEnv() (full spread) and test-efficacy.ts's probe spawn passed
no env key at all (full inheritance).

Contained cells against the live daemon (03-contained-test-run.png)

This verify container ships no docker client but a live daemon on
/var/run/docker.sock (engine 29.1.3 — the same lineage the PR's measurements
cite). I supplied a static docker client 29.7.2 outside the repo tree. One
environmental limit, proven by its own cell: bind mounts of this container's
filesystem do not propagate to the daemon's containers (the daemon does not
share its fs namespace), so contained commands were self-contained
(env/id/HOME/network probes) rather than host-fixture-backed. Everything that
does not need shared files was verified against real containers created by the
module's own argv (run()containerised()containerCommand()), with
QWEN_REVIEW_SANDBOX_IMAGE=node:22-bookworm (cached on the daemon) via the
documented override:

property test kind install kind
exit 0, ran inside container
canary OPENAI_API_KEY/GH_TOKEN absent
env is exactly the allowlist (CI, npm_config_yes, QWEN_SKIP_PREPARE, HOME=tmpfs, cache under mount; image defaults PATH/HOSTNAME only)
id = uid 1000 gid 1000 (host uid mapping)
$HOME is tmpfs and writable by the mapped uid
loopback fixture server works
egress to registry.npmjs.org blocked (EAI_AGAIN) ok:200

runtimeIsRootless('docker') against the live info document: 8 991-byte
JSON, no rootless marker → rootful → --user correctly retained (23/23
assertions across the five runtime cells, including the reap cell below).

Reap-by-name on deadline (real daemon, 03-reap.log)

Workload trap "" TERM; sleep 60, 3 s deadline through the real run():
result {exitCode: null, timedOut: true} (client SIGKILLed at the deadline);
the container was polled Up mid-run (11 sightings across 3 state lines) and
docker ps -a --filter name=qwen-review- was empty after run() returned —
killContainer reaped it by name. 3/3.

Corrections

None — no prior review round; I also found no inaccurate statement in the PR
text that survived contact with the code (the three headline decisions, the
SANDBOX-is-not-a-shortcut rule, the hand-off conversion at the single exit,
and the base-side leak all reproduce as described).

Findings

S1 (Suggestion, non-blocking): the phase-gate invocations in build-test and test-efficacy are not pinned by any test — mutation M1 survives

Deleting the entire gate block from runBuildTestUnguarded
(const refusal = refuseUnsandboxedPhase(root); … if (refusal) return refusedReport(refusal);) leaves build-test.test.ts at 109 passed (109).
The twin one-liner in runTestEfficacy
(refuseUnsandboxedPhase(probeWorktreePath(worktree))) is in the same class:
the integration test's new lines only isolate the operator's settings so other
tests don't inherit a required, nothing drives the gate. Classification per
the survivor taxonomy: ordinary coverage gap — not dead code (harness 01
proves the call is load-bearing: run() itself does not enforce required,
so without the gate the same scenario executes the reviewed commands
directly), not redundant defence (no other hunk covers these routes). The gate
function is thoroughly pinned in sandboxed-exec.test.ts, and test-delta's
wiring is pinned by its own test ("refuses the base-side rerun under
required"); only the build-test and test-efficacy call sites are unpinned.
The author says exactly this for build-test in the round-3 commit message
("What stays unpinned is only that runBuildTest calls it, which is one
visible line"). The fixture that would go red: with
QWEN_REVIEW_SANDBOX=required and no runtime on PATH,
runBuildTest({root: &lt;npm fixture>, …}) must return toolchain: 'refused',
ok: false, with its injected exec seam invoked zero times — the mirror
of the test test-delta.test.ts already has for its own gate.

No other finding survived the round. Specifically probed and not findings:

  • Sibling sweep of execution sites: every place the pipeline executes the
    reviewed repo's commands routes through the gate — base-tree builds via
    runBuildTest; test-delta's rerun was re-pointed at build-test's single
    run() (the duplicate that could drift is gone); script-lint spawns only
    host-installed linters over text (a different hazard class with its own
    config isolation). No ungated route found.
  • Network per kind is structural: exactly one exec call passes
    'install'; every other call (build, suite, retries, base-side rerun) takes
    the default, which is the restrictive 'test' — a future adapter cannot
    silently grant egress.
  • The second-continuation-exit invariant (refusal on --resume throws
    instead of overwriting the in-flight report) is pinned by
    resumeWouldDestroyReport tests plus the gate's own throw.

Targeted gates

  • packages/cli review suite (npx vitest run src/commands/review):
    5 017 passed | 5 skipped | 0 failed (logs/review-suite-rerun.log),
    plus per-file runs of build-test.test.ts (109/109) and
    sandboxed-exec.test.ts (35 passed | 1 skipped) during the mutation round.
    The PR body cites 4 324 passed / 1 skipped — a count from an earlier stage
    of the branch; the final head's later test commits grew the suite. Zero
    failures on both sides is the comparison that matters.
  • Mutation matrix (vacuity): control green (35 passed | 1 skipped on the
    unmutated sandboxed-exec.test.ts); M0/M2/M3/M4 killed with the intended
    assertion messages quoted in logs/05-detail.log
    (e.g. M3: expected 'off' to be 'required'); M1 survived as classified
    above. 04-mutation-matrix.png.
  • Generated artifact: npm run generate:settings-schema reproduces the
    committed packages/vscode-ide-companion/schemas/settings.schema.json
    byte-for-byte (git diff empty after regeneration).

Not covered

  • Per-commit attribution — checkout is depth 2 (merge commit + parents
    only); git rev-list HEAD^1..HEAD^2 shows 1 commit locally vs 23 in the
    metadata snapshot, i.e. the shallow boundary. The aggregate
    HEAD^1..HEAD diff is what was verified; the 23-commit history (four
    fix rounds) was not individually exercised.
  • Host-file-backed contained runs — bind mounts do not propagate from this
    container to the daemon (proven by the mount-propagation cell: a marker
    file mounted with the module's exact -v spelling is ENOENT inside). So
    "the container resolves the dependency farm's links through the mount" and
    "a contained suite produces the same verdicts" remain untested end-to-end
    here — the same gap the author names as not done. The mount string, the
    deepest-root arithmetic, and the symlink/colon refusals are verified
    against the real filesystem in harness 02.
  • Native module builds inside the image (packages/audio-capture/node-gyp
    class), rootless podman (no podman here), Windows cells
    (win32-only tests skip on Linux), real npm ci registry load under the
    install-kind network.
  • CI wiring (QWEN_REVIEW_SANDBOX=required in workflows) — deliberately a
    separate change per the PR description.
  • Host-trusted state under the mount (gitfile redirects, worktree lease
    files): the PR discloses these itself as pre-existing and not widened
    (consistent with base: on main the same commands run with no filesystem
    restriction at all). Not re-litigated here.
  • The verify-capture.mjs captures for the reap cell and the policy attack
    cell were not separately imaged; their raw logs are in logs/03-reap.log
    and logs/02-argv-oracle.log.

Methodology

Environment: CI verify container (node:22-bookworm, uid 1000, no docker
client but a reachable daemon at /var/run/docker.sock, engine 29.1.3; static
docker client 29.7.2 fetched to /__w/_temp/docker-cli, outside the repo).
npm ci + npm run build had completed at the merge head before this round;
all head-side harnesses import the compiled packages/cli/dist output, so the
verified artifact is the shipped JS, not a re-translation. Harnesses
(harness/01-ab-leak.mjs, 02-argv-oracle.mjs, 03-real-runtime.mjs,
04-mutations.sh, m1-mutate.mjs) run each cell in a fresh child process
(sandboxed-exec memoises its runtime probe at module scope; loadEnvironment
mutates process.env), and every assertion is a scripted comparison that can
fail — counts above come from those runs only. Mutations were applied in place
and restored with git checkout, each restore verified by a clean
git status and the four hunk greps; the v1 mutation script had a broken
restore path which accumulated mutants, was caught by its own red control, and
redone (v2 logs are authoritative). Raw per-cell output lives in logs/
(01-ab-leak.log, 02-argv-oracle.log, 03-real-runtime.log, 03-reap.log,
m0m4, m-ctrl.txt, m1.txt, 05-detail.log).

Flakiness gate log

rounds=5 files=4 skipped=0
file packages/cli/src/commands/review/build-test.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/build-test.test.ts
file packages/cli/src/commands/review/lib/sandboxed-exec.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/lib/sandboxed-exec.test.ts
file packages/cli/src/commands/review/test-delta.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/test-delta.test.ts
file packages/cli/src/commands/review/test-efficacy.integration.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/test-efficacy.integration.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/commands/review/build-test.test.ts: PPPPP
  packages/cli/src/commands/review/lib/sandboxed-exec.test.ts: PPPPP
  packages/cli/src/commands/review/test-delta.test.ts: PPPPP
  packages/cli/src/commands/review/test-efficacy.integration.test.ts: PPPPP

verdict: pass
summary: 4 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/commands/review/build-test.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/review/lib/sandboxed-exec.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/review/test-delta.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/review/test-efficacy.integration.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/build-test.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/lib/sandboxed-exec.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/test-delta.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/test-efficacy.integration.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/review/build-test.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/review/lib/sandboxed-exec.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/review/test-delta.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/review/test-efficacy.integration.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/review/build-test.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/review/lib/sandboxed-exec.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/review/test-delta.test.ts: P (exit 0)
round 4 · packages/cli/src/commands/review/test-efficacy.integration.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/review/build-test.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/review/lib/sandboxed-exec.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/review/test-delta.test.ts: P (exit 0)
round 5 · packages/cli/src/commands/review/test-efficacy.integration.test.ts: P (exit 0)

Evidence images

01-ab-host-cells

02-argv-policy-oracle

03-contained-test-run

04-mutation-matrix

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@wenshao
wenshao enabled auto-merge August 25, 2026 01:39

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved per maintainer decision.

@qwen-code-ci-bot

ghost commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 5d2a126de0109fcce82908c2cbf365e1f6f00c4b — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 5d2a126de0109fcce82908c2cbf365e1f6f00c4b既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM✅

@qwen-code-ci-bot

ghost commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants