Skip to content

fix(cli): extend the #8663 loader denylist and harden its scrub lifecycle - #8763

Merged
wenshao merged 12 commits into
mainfrom
fix/8663-loader-denylist-followup
Aug 10, 2026
Merged

fix(cli): extend the #8663 loader denylist and harden its scrub lifecycle#8763
wenshao merged 12 commits into
mainfrom
fix/8663-loader-denylist-followup

Conversation

@wenshao

@wenshao wenshao commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Follow-up to #8663. The /review pass that ran right after #8663 merged surfaced 14 findings that were never addressed (all still unresolved on that PR). This PR handles the substantive ones.

#8663's inherited-env denylist closed the NODE_OPTIONS/NODE_PATH class but left sibling variables that reach the same #8653 cross-workspace outcome: an untrusted workspace .env is frozen into daemonRuntimeBaseEnv and distributed to every workspace's session subprocesses. This PR extends the denylist along #8663's own two tiers, hardens the scrub lifecycle against concurrent embedded daemons, and closes the test/diagnostic gaps the review named.

Denylist additions

Scrubbed loader tier (INHERITED_LOADER_ENV_KEYS — scrubbed from the inherited launch env and rejected from every .env/settings.env scope). Pure-injection vars with no legitimate operator-shell use:

  • OPENSSL_CONF — Node's startup crypto init dlopens an attacker-configured OpenSSL engine/provider .so before any user code runs.
  • NODE_REPL_EXTERNAL_MODULE — a spawned node REPL require()s an attacker file at startup.
  • npm_config_node_gyp — npm's shim runs "$npm_config_node_gyp" "$@" verbatim.
  • npm_config_init_modulerequire()d by npm init (even npm init -y).

Reject-from-project-.env tier (PROJECT_ENV_HARDCODED_EXCLUSIONS — rejected from project files, but a value the operator sets in their own shell or home .env is preserved). Vars with legitimate operator-shell use whose only exposed vector is an untrusted project file:

  • TLS trust anchors SSL_CERT_FILE, SSL_CERT_DIR, CURL_CA_BUNDLE, REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO — siblings of the already-blocked NODE_EXTRA_CA_CERTS; an attacker CA MITMs the token-bearing traffic a session's git/npm/pip/curl calls make.
  • git command-execution family GIT_SSH_COMMAND, GIT_EXTERNAL_DIFF, GIT_CONFIG_GLOBAL/SYSTEM/COUNT and the numbered GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n> pairs (matched by prefix). core/utils/git-branches.ts already scrubs exactly these from the repo's own git invocations, so a project .env setting them contradicts our own model.
  • node-gyp interpreter selection NODE_GYP_FORCE_PYTHON, npm_config_python, PYTHON — run as the build Python during native-addon npm install.

Concurrency hardening

The daemon's process.env scrub/restore and the loader-key rejection reporter were process-global with no guard for concurrent embedded daemons in one process — a documented supported config (acp-bridge/src/bridgeOptions.ts childEnvOverrides). The first daemon's close() restored loader vars into the shared process.env, re-poisoning a still-live sibling's sessions, and dropped its reporter. The scrub is now reference-counted (acquireInheritedLoaderEnvScrub — snapshot on first acquire, restore only on last release) and the reporter is cleared only when still the active one.

Test / diagnostic hardening

  • Pin the daemon-worker channel-boundary scrub breadcrumb (not just key removal), so a refactor onto the silent scrub variant fails.
  • Pin the fast-path settings.env case-folded hardcoded-exclusion gate (previously only the .env loop's case-fold was pinned).
  • Drain the module-global fast-path rejection stash so the accumulates assertion is order-independent (no longer depends on a sibling test consuming first).
  • Docs (settings.md) updated for all new keys.

Deliberately not changed

Verification

npm run build clean; tsc --noEmit, eslint, and prettier --check clean on all changed files. Affected suites: shared-env-keys.test.ts, environment.test.ts, fast-path.test.ts, daemon-worker.test.ts, run-qwen-serve.test.ts — 458 tests pass. New tests cover every added key at the predicate and .env/settings.env application layers, the refcounted scrub (restore does not re-poison while a second holder is live), and the reporter clear-if-current guard.


这个 PR 做了什么

#8663 的后续。#8663 合入后紧接着跑的 /review 给出了 14 条一直未处理的评审意见(在该 PR 上至今全部 unresolved)。本 PR 处理其中实质性的部分。

#8663 的继承环境拒绝列表关闭了 NODE_OPTIONS/NODE_PATH 这一类,但遗漏了通向同一 #8653 跨 workspace 结果的同族变量:不受信 workspace 的 .env 会被冻结进 daemonRuntimeBaseEnv 并分发到每个 workspace 的会话子进程。本 PR 沿 #8663 自身的两层结构扩展拒绝列表,加固剥离生命周期以应对同进程并发内嵌 daemon,并补齐评审指出的测试/诊断缺口。

拒绝列表新增

剥离 loader 层(从继承的启动环境剥离,且在所有 .env/settings.env 作用域被拒绝)——无正当登录 shell 用途的纯注入变量:OPENSSL_CONF(启动时 dlopen 攻击者 OpenSSL engine)、NODE_REPL_EXTERNAL_MODULEnpm_config_node_gypnpm_config_init_module

仅拒绝项目 .env(从项目文件拒绝,但运维在自己 shell 或 home .env 设置的值保留)——有正当 shell 用途、仅经不受信项目文件暴露的变量:TLS 信任锚 SSL_CERT_FILE/SSL_CERT_DIR/CURL_CA_BUNDLE/REQUESTS_CA_BUNDLE/GIT_SSL_CAINFONODE_EXTRA_CA_CERTS 的同族,MITM 会话的 git/npm/pip/curl 携带 token 的流量);git 命令执行家族 GIT_SSH_COMMAND/GIT_EXTERNAL_DIFF/GIT_CONFIG_GLOBAL/SYSTEM/COUNT 及编号 GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n> 对(按前缀匹配;core/utils/git-branches.ts 已剥离这些);node-gyp 解释器选择 NODE_GYP_FORCE_PYTHON/npm_config_python/PYTHON

并发加固

daemon 对 process.env 的剥离/恢复与 loader 键拒绝 reporter 是进程全局的,对同进程并发内嵌 daemon(文档化的受支持配置)无保护。第一个 daemon 的 close() 会把 loader 变量恢复进共享 process.env,重新污染仍存活的同伴会话,并丢掉其 reporter。现改为引用计数(acquireInheritedLoaderEnvScrub——首次 acquire 快照、仅最后一次 release 恢复),reporter 仅在仍是当前活跃者时才清除。

测试/诊断加固

钉住 daemon-worker channel 边界剥离的 breadcrumb(不仅是键删除);钉住快速路径 settings.env大小写折叠硬编码排除门控;在 accumulates 断言前排空模块全局 stash 使其与测试顺序无关;settings.md 更新所有新键。

有意未改动

warn-once 去重按进程生效(移除后再加不再复警)是 R3-4 既定设计;库搜索路径(LD_LIBRARY_PATHPYTHONPATH 等)与残留的 PATH 前缀泄漏仍为 #8663 已跟踪的 deferred 项——拒绝它们会破坏主流工具链。

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

…ycle

Follow-up to #8663. Its inherited-env denylist closed the NODE_OPTIONS/
NODE_PATH class but left sibling code-execution and TLS-trust-anchor vars
that reach the same #8653 cross-workspace outcome — an untrusted workspace
`.env` is frozen into daemonRuntimeBaseEnv and distributed to every
workspace's session subprocesses.

Denylist additions, split by the PR's own tiering:

- Scrubbed loader tier (INHERITED_LOADER_ENV_KEYS — scrubbed from the
  inherited launch env and rejected from every `.env`/settings.env scope),
  for pure-injection vars with no legitimate operator-shell use:
  OPENSSL_CONF (startup dlopen of an attacker OpenSSL engine),
  NODE_REPL_EXTERNAL_MODULE, npm_config_node_gyp, npm_config_init_module.

- Reject-from-project-`.env` tier (PROJECT_ENV_HARDCODED_EXCLUSIONS —
  rejected from project files, preserved from the shell / home `.env`), for
  vars with a legitimate operator-shell use whose only exposed vector is an
  untrusted project file:
  * TLS trust anchors SSL_CERT_FILE, SSL_CERT_DIR, CURL_CA_BUNDLE,
    REQUESTS_CA_BUNDLE, GIT_SSL_CAINFO (siblings of NODE_EXTRA_CA_CERTS;
    an attacker CA MITMs a session's git/npm/pip/curl traffic).
  * git command-execution family GIT_SSH_COMMAND, GIT_EXTERNAL_DIFF,
    GIT_CONFIG_GLOBAL/SYSTEM/COUNT and the numbered GIT_CONFIG_KEY_<n>/
    GIT_CONFIG_VALUE_<n> pairs (matched by prefix). core/utils/git-branches.ts
    already scrubs these from the repo's own git invocations.
  * node-gyp interpreter selection NODE_GYP_FORCE_PYTHON, npm_config_python,
    PYTHON (run as the build Python during native-addon installs).

Concurrency: the daemon's process.env scrub/restore and the loader-key
rejection reporter were process-global with no guard for concurrent embedded
daemons in one process (a documented supported config). The first daemon's
close() restored loader vars into the shared env, re-poisoning a still-live
sibling's sessions, and dropped its reporter. The scrub is now reference
counted (acquireInheritedLoaderEnvScrub — snapshot on first acquire, restore
only on last release) and the reporter is cleared only when still active.

Test hardening from the same review: pin the daemon-worker scrub breadcrumb
(not just key removal); pin the fast-path settings.env case-folded
hardcoded-exclusion gate; drain the module-global fast-path stash so the
accumulate assertion is order-independent. Docs updated for the new keys.
@wenshao
wenshao force-pushed the fix/8663-loader-denylist-followup branch from e925043 to 92c525f Compare August 8, 2026 22:23
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

@wenshao

wenshao commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 8, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run after two more fix commits landed on top of the previously reviewed head — 146c448 (merges the scrub snapshot into one pass) and 28352d5 (blocks the round-4 exec-redirect keys).

Template: unchanged from the last pass — the body uses a custom structure rather than the template headings (## Why it's needed, ## Reviewer Test Plan, ## Risk & Scope, ## Linked Issues absent as headings). All the substance is there: motivation opens the description, ## Verification carries commands and suites, "Deliberately not changed" covers risk/scope, #8663/#8653 linked inline, full Chinese translation included. Passing on substance.

Problem: observed, not theoretical — unchanged assessment. #8653 documented the cross-workspace loader hijack with a reproduction, #8663 shipped the denylist, and this PR addresses named, still-unresolved findings from the post-merge /review pass on #8663. The two new commits answer two further named review rounds: the R1 ledger from @doudouOUC's review (snapshot/restore lifecycle undertested, double env iteration) and the R4 ledger from the follow-up /review (eight exec-redirect keys absent from the denylist). Each commit maps to findings that were probe-verified on the prior heads, not hypothetical gaps.

Direction: aligned — unchanged. Extends #8663's own two-tier model; the direction is well-established across the ecosystem (Claude Code's CHANGELOG carries the analogous subprocess-env-scrub entries).

Size: 321 production lines (shared-env-keys.ts 254, run-qwen-serve.ts 57, environment.ts 10) vs 910 test lines and 4 doc lines. Touches packages/cli/src/config/** core paths; author is a maintainer and it is well under the 500-line bar.

Approach: the two new commits are exactly the response to the review findings they claim to be — no drive-by content. 28352d5 adds the eight keys the R4 round named (GIT_SEQUENCE_EDITOR, XDG_CONFIG_HOME, VISUAL/EDITOR, PYTHONSTARTUP, BROWSER, QWEN_CDP_MCP_COMMAND, QWEN_SERVE_CDP_TUNNEL_OVER_WS), each with a rationale comment, test pins, and matching settings.md copy — class completion, not scope creep. 146c448 is the minimal shape of the R1 lifecycle findings. The only recurring hygiene note: the PR body's key enumeration is stale relative to the shipped denylist (settings.md is complete, so cosmetic).

Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths.

Moving on to code review. 🔍

中文说明

在之前评审的头之上又落了两个修复提交后重跑——146c448(将剥离快照合并为单趟遍历)与 28352d5(阻断 round-4 命令执行重定向键)。

模板:与上一轮相同——正文使用自定义结构而非模板标题(## Why it's needed## Reviewer Test Plan## Risk & Scope## Linked Issues 作为标题缺失)。实质内容齐全:引言说明动机、## Verification 给出命令与套件、"Deliberately not changed" 交代风险/范围、内联引用 #8663/#8653、中文翻译完整。按实质通过。

问题:已观测,非理论性——判断不变。#8653 有复现地记录了跨 workspace loader 劫持,#8663 落地拒绝列表,本 PR 处理 #8663 合入后 /review 点名且未解决的发现。两个新提交回应了另外两轮点名的评审:@doudouOUC 评审的 R1 清单(快照/恢复生命周期测试不足、对 env 的双遍历)与后续 /review 的 R4 清单(八个命令执行重定向键缺失)。每个提交都对应在先前头上经探针验证的发现,而非假想缺口。

方向:对齐——不变。沿 #8663 自身的两层模型扩展;该方向在生态中已有充分先例(Claude Code CHANGELOG 有对应的子进程环境剥离条目)。

规模:生产代码 321 行(shared-env-keys.ts 254、run-qwen-serve.ts 57、environment.ts 10),测试 910 行、文档 4 行。触及 packages/cli/src/config/** 核心路径;作者为维护者,远低于 500 行阈值。

方案:两个新提交恰是其声称回应的评审发现——无夹带内容。28352d5 加入 R4 轮点名的八个键(GIT_SEQUENCE_EDITORXDG_CONFIG_HOMEVISUAL/EDITORPYTHONSTARTUPBROWSERQWEN_CDP_MCP_COMMANDQWEN_SERVE_CDP_TUNNEL_OVER_WS),每个都带机理注释、测试钉住与 settings.md 同步文案——属按类补齐而非范围蔓延。146c448 是 R1 生命周期发现的最小形态。唯一反复出现的卫生备注:PR 正文的键枚举相对最终落地的拒绝列表已过时(settings.md 完整,属表面问题)。

风险:无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。

进入代码审查。🔍

Qwen Code · qwen3.8-max

Reviewed at 28352d54ae02bfbc8345a5158a850aa73132c0d4 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Re-run reviewed against 28352d5 — two commits past the previously reviewed 3b70728. The full-denylist and refcount conclusions from that pass stand; this pass covers the delta (4 files, +273/−17) and re-verified the surrounding call sites. My independent proposal for the delta — answer each named R4 finding by adding the key to the reject-only tier with a rationale comment, and fold the R1 snapshot loop into the scrub's single pass while keeping snapshot-on-every-acquire semantics — is exactly what shipped. No Critical issues, no convention violations.

What I verified against the tree, not taken on faith:

  • Every R4 ledger finding is addressed. The /review round on 146c448 named seven findings (six Critical): GIT_SEQUENCE_EDITOR (R4-1), VISUAL/EDITOR (R4-5), PYTHONSTARTUP (R4-7), XDG_CONFIG_HOME (R4-6), BROWSER (R4-8), QWEN_CDP_MCP_COMMAND + QWEN_SERVE_CDP_TUNNEL_OVER_WS (R4-9), and the missing scrub-test pins for the four new loader keys (R4-2). All seven are in this head: the keys sit in PROJECT_ENV_HARDCODED_EXCLUSIONS with rationale comments, and the scrub test now pins OPENSSL_CONF / NODE_REPL_EXTERNAL_MODULE / npm_config_node_gyp / npm_config_init_module.
  • The exec-redirect rationales hold. useLaunchEditor.ts returns process.env['VISUAL'] || process.env['EDITOR'] || 'vi' and text-buffer.ts does the same — the CLI spawns these without git involved. secure-browser-launcher.ts reads $BROWSER and launches it before any fallback. serve/capabilities.ts / serve/acp-http/index.ts spawn $QWEN_CDP_MCP_COMMAND as the daemon's browser-automation adapter, and config.ts reads QWEN_SERVE_CDP_TUNNEL_OVER_WS to switch that surface on — same daemon-hijack class as QWEN_CLI_ENTRY, which was already blocked. GIT_SEQUENCE_EDITOR, XDG_CONFIG_HOME (git merges $XDG_CONFIG_HOME/git/config with ~/.gitconfig), and PYTHONSTARTUP match their comments.
  • The snapshot merge is semantics-preserving. 146c448 folds the pre-scrub snapshot loop into scrubInheritedLoaderEnv via an optional snapshotInto map, and moves the snapshot-map clear from acquire-at-depth-0 to release-at-depth-0 (after the restore). The invariant depth == 0 ⟹ map empty still holds because each handle's release() is idempotent and decrements exactly once — so a fresh cycle always starts from a clean map. Snapshot-on-every-acquire (newest value wins), restore-only-at-depth-0, and the Object.hasOwn later-assignment-wins guard are all untouched.
  • Only the refcounted path touches the shared map. The one-shot scrubs (daemon-worker.ts channel boot, gemini.tsx ACP child, run-qwen-serve.ts private baseEnv) call the same functions without a snapshot; only acquireInheritedLoaderEnvScrub in run-qwen-serve.ts passes the module-global map.
  • The new tests pin exactly the mutants the R1 ledger flagged. Stale prior-cycle snapshot restore, leaked-snapshot reset, and the undefined-value guard each have a dedicated regression test — these were the "mutant survived" items in @doudouOUC's review; they now fail if the corresponding line is deleted.
  • settings.md warning block enumerates all eight new keys with their rationales; the operator-facing doc matches the code.

No blockers. Non-blocking notes, carried forward: (1) the PR body's key enumeration remains stale relative to the shipped denylist — cosmetic while settings.md is complete; (2) the same-class vectors the verify sweep flagged (npm_config_registry / PIP_INDEX_URL / UV_INDEX_URL index redirects, R_PROFILE_USER) and the project-tier rejection observability nit (verify F1/F2-remainder/F3) stay in the tracked #8663 follow-up scope — no dedicated issue exists yet, worth filing alongside that follow-up.

Test evidence — the PR's own CI

All checks green on the reviewed commit 28352d5, nothing pending: all three pull_request-event workflow runs (Qwen Code CI, SDK Java, Serve A/B) completed successfully. The unit-test signal Test (ubuntu-latest, Node 22.x) is green on this exact head; macOS/Windows unit legs and Integration Tests (CLI, No Sandbox) report skipped, as throughout this PR's history.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
macos-latest / Java 21 ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
windows-latest / Java 21 ✅ success
Classify PR ✅ success
route ✅ success
Post Coverage Comment (ubuntu-latest, 22.x) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped

Sandboxed verification: the round-2 /verify run proved the central claims A/B on the prior head 3b70728 (base applies 41/41 new keys from an untrusted project .env, head rejects 41/41 with operator values preserved on both arms; 966/966 assertions; mutation matrix 6/6 killed; PR's own tests overlaid on base fail 20/66 as predicted). The two commits since are additive — eight more denylist keys plus the semantics-preserving snapshot merge — and every addition is pinned by unit tests that are green on this exact head. A fresh sandboxed run was triggered on this head and will post its own report when it completes; live A/B coverage of the eight newest keys on 28352d5 itself is what that report will add, so treat it as the last evidence piece to land. There is no user-visible or TUI surface to drive live — the change is daemon/env plumbing.

中文说明

代码审查:针对 28352d5 复审——距上一评审头 3b70728 两个提交。该轮对完整拒绝列表与引用计数的结论维持不变;本轮覆盖增量(4 文件,+273/−17)并复查周边调用点。我对增量的独立方案——逐条回应 R4 点名发现、将键加入仅拒绝项目文件层并附机理注释,以及把 R1 的快照循环并入剥离的单趟遍历、保持每次 acquire 快照语义——与落地完全一致。未发现 Critical 问题与规范违背。

关键论断均对照代码树核实:R4 清单七条发现(六条 Critical)全部在本头上落地——八个键在 PROJECT_ENV_HARDCODED_EXCLUSIONS 中带机理注释,剥离测试补齐四个新 loader 键的钉住;命令执行重定向机理成立——useLaunchEditor.tstext-buffer.ts 直接读取并 spawn VISUAL/EDITOR(无需经过 git),secure-browser-launcher.ts 读取并启动 $BROWSERserve/capabilities.ts/acp-http$QWEN_CDP_MCP_COMMAND 作为 daemon 浏览器自动化适配器、config.tsQWEN_SERVE_CDP_TUNNEL_OVER_WS 开启该面——与已阻断的 QWEN_CLI_ENTRY 同属 daemon 劫持类;快照合并保持语义——清除从"深度 0 时 acquire 清"移到"深度 0 时 release 恢复后清",因每个句柄 release 幂等且恰减一次,"深度为 0 ⟹ 快照为空"不变式仍成立,每次 acquire 快照(最新值优先)、仅深度归零恢复、Object.hasOwn 合法赋值优先均未改动;唯引用计数路径触碰共享 map,一次性剥离(channel worker、ACP 子进程、私有 baseEnv)均不带快照;新测试恰好钉住 R1 清单点名的变异——陈旧周期快照恢复、泄漏快照重置、undefined 值防护各有回归用例;settings.md 警告块完整列出八个新键及机理。

无阻塞项。非阻塞备注延续:(1) PR 正文键枚举相对落地列表仍过时——settings.md 完整,属表面问题;(2) verify 扫出的同族向量(npm_config_registry/PIP_INDEX_URL/UV_INDEX_URL 索引重定向、R_PROFILE_USER)与项目层拒绝可观测性小疵(F1/F2 余项/F3)仍在 #8663 跟踪的后续范围内——尚无专门 issue,值得随后续项一并建档。

测试证据:评审提交 28352d5 上 CI 全绿且无 pending——三个 pull_request 事件工作流(Qwen Code CI、SDK Java、Serve A/B)全部成功完成;承载受影响套件的 Test (ubuntu-latest, Node 22.x) 在本头上为绿;macOS/Windows 单元腿与集成测试照旧 skipped。

沙箱验证:round-2 /verify 已在先前头 3b70728 上 A/B 证实核心主张(基线对不受信项目 .env 应用 41/41 个新键、PR 头全部拒绝且两侧保留运维值;966/966 断言;变异矩阵 6/6 杀死;PR 自有测试放回 base 按预测失败 20/66)。其后两个提交是纯增量——再加八个拒绝键与语义保持的快照合并——每项新增都由在本头为绿的单测钉住。针对本头的新一轮沙箱运行已触发、完成后会自行发布报告;八个最新键在 28352d5 上的 A/B 实测即该报告将补上的最后一块证据。本改动无用户可见/TUI 面——纯 daemon/环境管道。

Qwen Code · qwen3.8-max

Reviewed at 28352d54ae02bfbc8345a5158a850aa73132c0d4 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — the review loop visibly converged: every named finding from two review rounds is answered at this head with test pins, CI is fully green on the exact commit, and the prior sandboxed A/B already proved the central claims; remaining notes are cosmetic or tracked-follow-up scope.

This is the follow-up working the way the loop is supposed to. Since the last pass, two commits landed and each maps one-to-one onto a named review round: 146c448 takes the R1 ledger's lifecycle findings (the double env iteration, the under-pinned snapshot clear/restore semantics) and answers them with the minimal shape — snapshot folded into the scrub's single pass, clear moved to the depth-zero release, and three regression tests that fail if you delete the exact lines the review flagged as mutant-survived. 28352d5 takes the R4 ledger's six Critical missing-key findings and closes them all, each with a rationale comment I checked against the actual spawn sites (useLaunchEditor.ts, secure-browser-launcher.ts, the serve CDP adapter) rather than trusting the description. The refactor is the kind I'd thank the author for in six months: the subtle invariant (depth == 0 ⟹ snapshot empty) is preserved by construction, not by convention.

What keeps this at 4 rather than 5 is unchanged from the last pass and none of it is code: the PR body's key enumeration is stale relative to the shipped denylist (settings.md is complete, so purely cosmetic), and the same-class vectors the verify sweep flagged — npm_config_registry / PIP_INDEX_URL / UV_INDEX_URL index redirects, R_PROFILE_USER, plus the project-tier rejection observability nit — remain open as the PR's own scoping defers them. They belong to the tracked #8663 follow-up; no dedicated issue exists yet, and filing one would stop these from being rediscovered by the next verify sweep.

Housekeeping for the humans in the loop: both standing CHANGES_REQUESTED reviews sit on superseded commits. @doudouOUC — the five actionable findings in your review ledger (R1-1…R1-5) are all addressed by 146c448, each now pinned by a regression test; R1-6 names an autofix status comment, which your own review text concludes is infrastructure noise rather than a code defect. If that matches your read, a re-review (or dismissal) would clear the last stale gate state. The bot's R4 review at 146c448 is fully addressed by 28352d5 as itemized above.

Approval: all stages clean on this head, CI complete and green with nothing pending, and no guardrail applies (same-repo branch, maintainer author, no Stage 0 escalation), so this run approves pinned to the reviewed commit below. The fresh sandboxed verification triggered on this head will post its own report; it is confirmatory for the eight newest keys, not a precondition the code review is waiting on.

中文说明

信心 4/5——评审循环可见地收敛了:两轮评审点名的每条发现都在本头上以测试钉住作答,CI 在该提交上全绿,沙箱 A/B 已在先前头上证实核心主张;其余备注均为表面问题或已跟踪的后续范围。

这是后续修复该有的运转方式。上一轮之后落了两个提交,各自与一轮点名的评审一一对应:146c448 回应 R1 清单的生命周期发现(对 env 的双遍历、钉住不足的快照清除/恢复语义),以最小形态作答——快照并入剥离的单趟遍历、清除移至深度归零的 release、并附三个回归测试,删掉评审点名"变异存活"的那几行测试即会失败。28352d5 关闭 R4 清单全部六条 Critical 缺键发现,每条都带我核对过实际 spawn 点(useLaunchEditor.tssecure-browser-launcher.ts、serve CDP 适配器)的机理注释,而非照单全收。这个重构是六个月后我会感谢作者的类型:微妙不变式(深度为 0 ⟹ 快照为空)由结构保证而非约定保证。

给 4 而不是 5 的原因与上一轮相同,且都不是代码问题:PR 正文的键枚举相对落地列表过时(settings.md 完整,纯表面);verify 扫出的同族向量——npm_config_registry/PIP_INDEX_URL/UV_INDEX_URL 索引重定向、R_PROFILE_USER,以及项目层拒绝的可观测性小疵——按 PR 自身的范围界定仍属延后项,归 #8663 跟踪的后续;尚无专门 issue,建一个可以避免下一次 verify 重新发现。

给人类的杂务说明:现存两条 CHANGES_REQUESTED 评审都停留在已被取代的提交上。@doudouOUC——你评审清单中五条可执行发现(R1-1…R1-5)已全部由 146c448 处理,且各自有回归测试钉住;R1-6 点名的是一条 autofix 状态评论,你的评审原文也已认定其为基础设施噪音而非代码缺陷。若与你的判断一致,re-review(或 dismiss)即可清除最后的过期门控状态。机器人在 146c448 上的 R4 评审已由 28352d5 如上逐条处理。

审批:本头上各阶段干净、CI 完成且全绿、无 pending,且无任何护栏触发(同仓分支、维护者作者、无 Stage 0 升级),故本次运行按下方评审提交钉住审批。针对本头新触发的沙箱验证会自行发布报告;它对八个最新键是确认性的,不是代码审查等待的前置条件。

Qwen Code · qwen3.8-max

Reviewed at 28352d54ae02bfbc8345a5158a850aa73132c0d4 · re-run with @qwen-code /triage

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI N/A% N/A% N/A% N/A%
Core 87.86% 87.86% 89.39% 86.36%
CLI Package - Full Text Report
CLI full-text-summary.txt not found at: coverage_artifact/cli/coverage/full-text-summary.txt
Core Package - Full Text Report
-------------------|---------|----------|---------|---------|-------------------
File               | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------------|---------|----------|---------|---------|-------------------
All files          |   87.86 |    86.36 |   89.39 |   87.86 |                   
 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.38 |    84.54 |   94.85 |   90.38 |                   
  ...transcript.ts |   87.63 |    83.52 |     100 |   87.63 | ...80,588,594-598 
  ...ent-resume.ts |   85.59 |    77.55 |   83.33 |   85.59 | ...1793-1797,1800 
  ...ound-tasks.ts |   94.63 |    90.13 |   96.38 |   94.63 | ...1773,1793-1796 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...ent-result.ts |    96.8 |    92.68 |     100 |    96.8 | 106,129-131       
  ...n-registry.ts |   94.79 |     87.7 |     100 |   94.79 | ...1067,1081-1083 
  ...w-snapshot.ts |   92.12 |    77.14 |     100 |   92.12 | ...65,189,196-198 
 src/agents/arena  |   76.32 |    67.71 |   78.94 |   76.32 |                   
  ...gentClient.ts |   79.47 |    88.88 |   81.81 |   79.47 | ...68-183,189-204 
  ArenaManager.ts  |   75.11 |    64.51 |   78.57 |   75.11 | ...1887,1893-1894 
  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.09 |    85.23 |   76.28 |   78.09 |                   
  ITermBackend.ts  |   97.97 |    93.93 |     100 |   97.97 | ...78-180,255,307 
  ...essBackend.ts |    90.9 |    85.36 |   93.33 |    90.9 | ...70,672,674-675 
  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 |    91.1 |    86.68 |   89.23 |    91.1 |                   
  agent-context.ts |     100 |      100 |     100 |     100 |                   
  agent-core.ts    |   85.07 |     76.8 |   77.77 |   85.07 | ...2291,2337-2339 
  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.34 |      100 |    92.3 |   98.34 | 81-82             
  ...low-budget.ts |     100 |      100 |     100 |     100 |                   
  ...-scheduler.ts |   97.43 |    96.36 |     100 |   97.43 | 128-130           
  ...ow-journal.ts |   91.76 |    75.86 |     100 |   91.76 | ...38-139,179-181 
  ...chestrator.ts |    92.4 |       90 |   83.78 |    92.4 | ...1862,1911-1914 
  ...ow-prompts.ts |     100 |      100 |     100 |     100 |                   
  ...low-runner.ts |   94.85 |     87.5 |   92.85 |   94.85 | ...93,260,280-283 
  ...ow-sandbox.ts |   96.85 |    91.28 |     100 |   96.85 | ...1705,1711-1712 
  ...flow-saved.ts |   96.51 |    94.36 |     100 |   96.51 | 134-135,234-237   
  ...flow-stall.ts |    97.9 |    83.33 |     100 |    97.9 | 138-139,236       
 src/agents/tasks  |     100 |      100 |     100 |     100 |                   
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/agents/team   |   82.04 |    84.17 |   88.97 |   82.04 |                   
  TeamManager.ts   |   72.02 |    79.41 |   79.24 |   72.02 | ...1632,1655-1656 
  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.24 |    82.82 |     100 |   89.24 | ...-994,1038-1039 
  team-events.ts   |   60.52 |      100 |      50 |   60.52 | ...40-144,151-155 
  teamHelpers.ts   |   92.02 |    94.91 |   95.23 |   92.02 | ...31-332,368-378 
  types.ts         |     100 |      100 |     100 |     100 |                   
 ...eam/test-utils |   94.39 |    94.26 |   98.21 |   94.39 |                   
  ...on-harness.ts |   96.49 |    84.21 |     100 |   96.49 | 128-129,141-142   
  fake-agent.ts    |   98.49 |    95.08 |     100 |   98.49 | 201-203           
  fake-backend.ts  |   86.46 |    97.61 |   95.83 |   86.46 | 124-146           
 src/config        |   84.98 |    87.13 |   75.37 |   84.98 |                   
  approval-mode.ts |     100 |      100 |     100 |     100 |                   
  ...xtDefaults.ts |     100 |      100 |     100 |     100 |                   
  config.ts        |   84.29 |    86.85 |   73.79 |   84.29 | ...8350,8354-8355 
  constants.ts     |     100 |      100 |     100 |     100 |                   
  models.ts        |     100 |      100 |     100 |     100 |                   
  storage.ts       |   94.39 |    91.57 |   88.23 |   94.39 | ...45-446,449-450 
 ...nfirmation-bus |   98.27 |    97.14 |     100 |   98.27 |                   
  message-bus.ts   |   98.14 |    97.05 |     100 |   98.14 | 42-43             
  types.ts         |     100 |      100 |     100 |     100 |                   
 src/core          |   92.34 |    88.04 |   93.26 |   92.34 |                   
  baseLlmClient.ts |    88.4 |     83.8 |   81.81 |    88.4 | ...59,672,678-680 
  client.ts        |   91.95 |    87.18 |   91.56 |   91.95 | ...3928,4026-4027 
  ...tGenerator.ts |   86.34 |    87.34 |   84.61 |   86.34 | ...96-497,542-548 
  ...lScheduler.ts |   90.04 |    84.67 |   96.15 |   90.04 | ...6215,6243-6259 
  geminiChat.ts    |    94.7 |    90.12 |   95.53 |    94.7 | ...5052,5100-5101 
  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 | 49-50             
  ...on-helpers.ts |   93.49 |    78.57 |     100 |   93.49 | ...10-211,228-229 
  ...issionFlow.ts |   98.97 |    96.96 |     100 |   98.97 | 107               
  ...try-policy.ts |     100 |      100 |     100 |     100 |                   
  ...ell-policy.ts |   94.89 |    88.54 |     100 |   94.89 | ...51-252,297-298 
  prompts.ts       |   93.64 |    91.42 |   83.33 |   93.64 | ...1209,1412-1413 
  ...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 |     92.1 |     100 |     100 | 87,122-139        
  ...reparation.ts |     100 |      100 |     100 |     100 |                   
  ...tion-guard.ts |   90.38 |    94.73 |     100 |   90.38 | 68-72             
  ...allIdUtils.ts |   98.41 |    93.47 |     100 |   98.41 | 36,45             
  ...okTriggers.ts |   99.45 |    92.43 |     100 |   99.45 | 182,193           
  ...terruption.ts |     100 |     92.3 |     100 |     100 | 86,104            
  turn.ts          |   98.67 |    93.12 |     100 |   98.67 | ...79,707-708,755 
  ...l-fallback.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   96.33 |    88.12 |   96.15 |   96.33 |                   
  ...tGenerator.ts |   97.24 |    86.72 |   94.87 |   97.24 | ...1429,1458,1469 
  converter.ts     |   96.19 |    89.25 |     100 |   96.19 | ...1329,1550-1552 
  index.ts         |       0 |        0 |       0 |       0 | 1-21              
  usage.ts         |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   88.78 |    72.36 |   89.47 |   88.78 |                   
  ...tGenerator.ts |   87.18 |    71.83 |   88.88 |   87.18 | ...58-364,382-383 
  index.ts         |     100 |       80 |     100 |     100 | 50                
 ...ntentGenerator |    95.6 |    88.74 |    92.3 |    95.6 |                   
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tGenerator.ts |   95.52 |    87.88 |   91.89 |   95.52 | ...1195-1196,1224 
  ...tDetection.ts |     100 |      100 |     100 |     100 |                   
 ...ntentGenerator |   91.63 |    90.43 |   95.61 |   91.63 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  converter.ts     |   91.15 |    89.32 |   96.87 |   91.15 | ...1914,2083-2098 
  errorHandler.ts  |     100 |      100 |     100 |     100 |                   
  index.ts         |   60.31 |       75 |      50 |   60.31 | ...71,74-78,90-94 
  ...tGenerator.ts |    66.4 |    70.58 |   88.88 |    66.4 | ...51-157,168-169 
  pipeline.ts      |   95.45 |    91.18 |     100 |   95.45 | ...1301,1309,1408 
  ...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.24 |     92.4 |     100 |   92.24 | ...28-529,549-552 
  ...kingParser.ts |     100 |    96.87 |     100 |     100 | 42                
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...rator/provider |   97.19 |    90.44 |   98.36 |   97.19 |                   
  dashscope.ts     |   98.36 |    92.99 |   95.65 |   98.36 | ...93-494,636-637 
  deepseek.ts      |   94.91 |    89.36 |     100 |   94.91 | ...31-132,145-146 
  default.ts       |   99.16 |    96.96 |     100 |   99.16 | 198               
  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           |   92.13 |    82.14 |     100 |   92.13 | ...,39-40,135-137 
 src/extension     |   87.27 |    84.01 |   92.52 |   87.27 |                   
  ...ive-safety.ts |     100 |      100 |     100 |     100 |                   
  ...-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 |                   
  ...me-refresh.ts |     100 |      100 |     100 |     100 |                   
  ...sion-store.ts |   90.85 |    86.38 |   97.87 |   90.85 | ...1218-1224,1268 
  ...ionManager.ts |   82.24 |    80.14 |   81.52 |   82.24 | ...2730,2752-2753 
  ...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.36 |   85.71 |    75.9 | ...98,202,214-248 
  github.ts        |   90.42 |    82.66 |     100 |   90.42 | ...0,990-991,1001 
  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 |     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.33 |     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.14 |     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 
 src/followup      |    79.9 |    78.92 |    90.9 |    79.9 |                   
  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   |   71.76 |    64.76 |   71.42 |   71.76 | ...53-654,661-662 
  ...onToolGate.ts |   97.97 |     87.5 |     100 |   97.97 | 105,110           
  ...nGenerator.ts |   72.03 |    81.15 |   83.33 |   72.03 | ...68-219,331-333 
 src/generated     |       0 |        0 |       0 |       0 |                   
  git-commit.ts    |       0 |        0 |       0 |       0 | 1-10              
 src/goals         |    93.3 |    89.05 |    94.6 |    93.3 |                   
  ...eGoalStore.ts |   87.61 |    88.88 |   86.66 |   87.61 | ...85-188,196-204 
  ...t-verifier.ts |   96.27 |     90.9 |     100 |   96.27 | ...20,143-146,163 
  ...checkpoint.ts |   81.48 |    76.19 |     100 |   81.48 | ...02-105,115-118 
  goal-evidence.ts |   88.79 |     88.5 |   96.42 |   88.79 | ...04-805,828-831 
  ...projection.ts |   66.66 |    72.97 |   33.33 |   66.66 | ...83,186,190-192 
  ...ersistence.ts |   87.73 |    84.84 |      80 |   87.73 | ...-94,97,101-106 
  goal-protocol.ts |   95.74 |    93.33 |     100 |   95.74 | 154-155           
  goal-reducer.ts  |    93.4 |    90.65 |   96.96 |    93.4 | ...27,501,519-520 
  goal-runtime.ts  |   97.62 |     89.9 |     100 |   97.62 | ...1049,1169-1170 
  goal-tools.ts    |   98.22 |    93.02 |      95 |   98.22 | ...46-147,248-249 
  ...rn-context.ts |     100 |      100 |     100 |     100 |                   
  goal-verifier.ts |   92.46 |    92.85 |     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.32 |   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 |    89.13 |     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.57 |   66.14 |   58.96 |                   
  ...nfigLoader.ts |   80.55 |       72 |   95.45 |   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 |       80 |   16.66 |   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.19 |   89.28 |   82.12 |                   
  ...en-storage.ts |     100 |      100 |     100 |     100 |                   
  ...en-storage.ts |   87.08 |    87.03 |   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        |   87.83 |    83.93 |   90.47 |   87.83 |                   
  ...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 
  const.ts         |   94.28 |     92.3 |     100 |   94.28 | 66-67             
  dream.ts         |    64.6 |    72.22 |      50 |    64.6 | ...04-109,124-165 
  ...entPlanner.ts |     100 |    83.33 |     100 |     100 | 136,146           
  entries.ts       |   75.59 |    84.84 |   83.33 |   75.59 | ...56-157,172-180 
  extract.ts       |   92.41 |    79.41 |     100 |   92.41 | 56-61,100,119-122 
  ...entPlanner.ts |   91.59 |    76.74 |     100 |   91.59 | ...05,114-117,293 
  ...ionPlanner.ts |       0 |        0 |       0 |       0 | 1                 
  forget.ts        |   81.83 |       75 |   83.33 |   81.83 | ...51,474,478-507 
  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.4 |    82.29 |   77.77 |    78.4 | ...1482,1495-1497 
  ...ent-config.ts |   86.99 |    82.69 |   86.36 |   86.99 | ...69,389,396-402 
  memoryAge.ts     |   90.47 |       80 |     100 |   90.47 | 50-51             
  paths.ts         |     100 |      100 |     100 |     100 |                   
  ...ing-skills.ts |     100 |       72 |     100 |     100 | 31-35,73-78,97    
  prompt.ts        |   97.26 |    87.03 |     100 |   97.26 | ...10-218,222,225 
  recall.ts        |   82.06 |       75 |    90.9 |   82.06 | ...59-364,395-406 
  refresh.ts       |   93.58 |    89.58 |     100 |   93.58 | ...75-176,183-184 
  ...ceSelector.ts |    93.1 |    81.81 |     100 |    93.1 | ...25,127-128,136 
  remember.ts      |   98.89 |    90.19 |     100 |   98.89 | 50,70             
  scan.ts          |   93.12 |    77.41 |     100 |   93.12 | ...08-109,154,157 
  scopes.ts        |     100 |      100 |     100 |     100 |                   
  ...et-scanner.ts |     100 |      100 |     100 |     100 |                   
  ...entPlanner.ts |   77.24 |    74.07 |   72.22 |   77.24 | ...52-456,459,465 
  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 |     87.5 |     100 |     100 | 30                
  ...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 |    81.53 |   81.81 |   81.21 | ...63-277,291-296 
 src/mocks         |       0 |        0 |       0 |       0 |                   
  msw.ts           |       0 |        0 |       0 |       0 | 1-9               
 src/models        |   92.55 |    88.97 |   91.13 |   92.55 |                   
  constants.ts     |     100 |      100 |     100 |     100 |                   
  ...tor-config.ts |   97.77 |    91.83 |     100 |   97.77 | 155,161,171       
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...nfigErrors.ts |   74.22 |    47.82 |   84.61 |   74.22 | ...,67-74,106-117 
  ...igResolver.ts |   98.71 |    93.33 |     100 |   98.71 | 166,328,334       
  modelRegistry.ts |     100 |    98.11 |     100 |     100 | 177,261           
  modelsConfig.ts  |   89.36 |    86.93 |   88.09 |   89.36 | ...1404,1433-1434 
  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.16 |   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.7 |     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.71 |     78.5 |   81.25 |   83.71 |                   
  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.85 |    73.84 |   78.26 |   75.85 | ...73-474,502-503 
  types.ts         |       0 |        0 |       0 |       0 | 1                 
 ...viders/presets |   97.82 |    91.66 |   63.63 |   97.82 |                   
  ...oding-plan.ts |   87.34 |      100 |       0 |   87.34 | 82-84,87-89,91-94 
  ...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 |                   
  openrouter.ts    |     100 |      100 |     100 |     100 |                   
  requesty.ts      |     100 |      100 |     100 |     100 |                   
  zai.ts           |     100 |      100 |     100 |     100 |                   
 src/qwen          |   85.41 |    78.52 |   95.89 |   85.41 |                   
  ...tGenerator.ts |   98.64 |    98.18 |     100 |   98.64 | 105-106           
  qwenOAuth2.ts    |   82.79 |    73.29 |   90.62 |   82.79 | ...1205-1221,1251 
  ...kenManager.ts |   85.36 |    76.61 |     100 |   85.36 | ...52-757,778-783 
 src/resources     |     100 |      100 |     100 |     100 |                   
  ...e-registry.ts |     100 |      100 |     100 |     100 |                   
 src/services      |   89.74 |    84.59 |   96.91 |   89.74 |                   
  ...ionTrailer.ts |     100 |      100 |     100 |     100 |                   
  ...llRegistry.ts |    98.5 |     87.5 |     100 |    98.5 | 81-82,105,476-477 
  ...ionService.ts |   97.51 |    96.15 |     100 |   97.51 | ...,929,1072-1080 
  ...ingService.ts |   91.41 |    85.15 |   95.65 |   91.41 | ...2116,2143-2144 
  ...ttribution.ts |   91.73 |    87.71 |      90 |   91.73 | ...80-685,826-827 
  ...utSlimming.ts |    97.2 |    93.93 |     100 |    97.2 | ...39-340,378-381 
  cronScheduler.ts |   94.17 |    90.45 |      98 |   94.17 | ...1333,1736-1737 
  cronTasksFile.ts |   95.49 |    90.82 |     100 |   95.49 | ...37,346-347,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 | ...41,467-474,519 
  ...ratedFiles.ts |      96 |    88.23 |     100 |      96 | 119-120,146-147   
  gitInit.ts       |     100 |      100 |     100 |     100 |                   
  ...reeService.ts |    73.7 |    68.49 |   95.83 |    73.7 | ...2196,2225-2226 
  ...on-service.ts |   87.38 |       72 |     100 |   87.38 | ...01-305,343-344 
  ...references.ts |   98.39 |    88.76 |     100 |   98.39 | 154-155,215-216   
  ...ionService.ts |   98.26 |    97.35 |     100 |   98.26 | ...13-714,761-762 
  ...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.3 |    91.22 |     100 |    97.3 | ...53-454,611-612 
  ...ttachments.ts |   97.74 |    90.85 |     100 |   97.74 | 298-308,646       
  ...ersistence.ts |   90.95 |    78.75 |     100 |   90.95 | ...78,963-964,992 
  ...on-service.ts |   94.49 |    92.26 |   97.14 |   94.49 | ...98-600,656-664 
  ...ce-service.ts |    98.5 |    94.11 |    90.9 |    98.5 | 64-65             
  ...ipt-reader.ts |   94.55 |    89.78 |   96.66 |   94.55 | ...1353-1354,1422 
  ...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 |   88.79 |    83.72 |   97.18 |   88.79 | ...2477,2553-2573 
  sessionTitle.ts  |   94.19 |    73.21 |     100 |   94.19 | ...43-246,277-278 
  ...ionService.ts |    84.4 |    78.45 |   97.18 |    84.4 | ...2493,2499-2504 
  ...pInhibitor.ts |   97.42 |    92.77 |     100 |   97.42 | ...30,169,369-370 
  ...Estimation.ts |     100 |    88.23 |     100 |     100 | 118-119           
  ...ageService.ts |   97.76 |    91.59 |   93.75 |   97.76 | ...61-262,366,567 
  ...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.72 |    84.07 |     100 |   90.72 | ...06-509,561-562 
  ...reeCleanup.ts |   14.42 |      100 |   33.33 |   14.42 | 58-186            
  ...ionService.ts |   87.98 |    86.84 |     100 |   87.98 | ...38-439,455-456 
 ...icrocompaction |    98.9 |    95.08 |     100 |    98.9 |                   
  microcompact.ts  |    98.9 |    95.08 |     100 |    98.9 | ...40,749,758-759 
 ...s/visionBridge |   98.81 |    92.12 |     100 |   98.81 |                   
  ...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             
  ...-constants.ts |     100 |      100 |     100 |     100 |                   
  ...ge-service.ts |   98.61 |     94.7 |     100 |   98.61 | ...06,666,679-680 
 src/skills        |   89.29 |    85.89 |   93.61 |   89.29 |                   
  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.5 |     100 |   94.84 | ...03,223,235-237 
  skill-manager.ts |   84.82 |    85.29 |   83.33 |   84.82 | ...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.03 |     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     |   87.72 |    89.01 |   96.55 |   87.72 |                   
  ...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 |   84.48 |    85.91 |   94.87 |   84.48 | ...1582,1659-1660 
  types.ts         |     100 |      100 |     100 |     100 |                   
  validation.ts    |   92.46 |    95.18 |     100 |   92.46 | 47-52,63-68,71-76 
 src/telemetry     |   81.73 |    83.97 |   84.83 |   81.73 |                   
  ...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 |   76.31 |    74.62 |   73.68 |   76.31 | ...80,387-389,405 
  ...attributes.ts |   95.15 |    87.27 |     100 |   95.15 | ...97-198,216-217 
  ...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.78 |    83.33 |   55.55 |   65.78 | ...04-105,108-109 
  ...ai-content.ts |    74.5 |    66.41 |   91.66 |    74.5 | ...1480,1493-1502 
  ...i-provider.ts |     100 |       99 |     100 |     100 | 99                
  ...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.1 |    95.72 |      95 |    99.1 | 145,369-370       
  ...t.circular.ts |       0 |        0 |       0 |       0 | 1-128             
  loggers.ts       |   60.03 |    76.51 |   66.07 |   60.03 | ...1484,1501-1521 
  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      |   91.06 |    87.15 |   68.75 |   91.06 | ...32,482-483,499 
  sdk.ts           |   82.12 |    90.47 |   66.66 |   82.12 | ...90-194,232-254 
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  ...on-tracing.ts |    91.1 |    88.68 |   96.77 |    91.1 | ...1737,1768-1771 
  ...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 |    94.32 |   86.36 |      83 | ...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.02 |    98.41 |   82.92 |   96.02 |                   
  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 |   78.78 |   94.85 | ...53,227-228,241 
  ...aceContext.ts |     100 |      100 |     100 |     100 |                   
 src/tools         |   86.24 |    84.99 |   88.72 |   86.24 |                   
  ...erQuestion.ts |   89.71 |    80.76 |   91.66 |   89.71 | ...66-367,374-375 
  ...-registrar.ts |    77.7 |    66.66 |   66.66 |    77.7 | ...72-277,292-294 
  ...ub-session.ts |   89.67 |     91.3 |   81.81 |   89.67 | ...03-304,315-322 
  cron-create.ts   |   90.64 |    92.85 |   72.72 |   90.64 | ...,73-74,223-231 
  cron-delete.ts   |   97.56 |      100 |   83.33 |   97.56 | 31-32             
  cron-list.ts     |   98.23 |    95.34 |    87.5 |   98.23 | 57-58             
  diffOptions.ts   |     100 |      100 |     100 |     100 |                   
  display-image.ts |   87.42 |    84.84 |   88.88 |   87.42 | ...29-134,194-195 
  edit.ts          |    82.7 |    86.77 |   81.25 |    82.7 | ...43-744,863-913 
  ...r-worktree.ts |   83.14 |    67.56 |    87.5 |   83.14 | ...84-187,278-279 
  enterPlanMode.ts |      85 |     82.6 |    87.5 |      85 | ...28-133,161-175 
  exit-worktree.ts |   83.29 |    83.65 |   94.44 |   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.61 |   85.71 |   90.73 | ...76-677,727-728 
  ...adTracking.ts |     100 |      100 |     100 |     100 |                   
  image-gen.ts     |   91.66 |    77.41 |    90.9 |   91.66 | ...13-214,221-222 
  list-agents.ts   |   94.02 |    82.35 |   83.33 |   94.02 | 31-32,47-48       
  loop-wakeup.ts   |   99.27 |    92.85 |     100 |   99.27 | 45                
  ls.ts            |   96.74 |    90.27 |     100 |   96.74 | 176-181,212,216   
  lsp.ts           |   72.71 |     59.5 |   90.32 |   72.71 | ...1212,1214-1215 
  ...nt-manager.ts |   82.13 |    80.47 |   85.71 |   82.13 | ...3234,3236-3237 
  mcp-client.ts    |   79.87 |    85.58 |   89.47 |   79.87 | ...2259,2263-2266 
  ...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.46 |    93.93 |     100 |   97.46 | 176-177           
  ...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      |   98.35 |    93.71 |     100 |   98.35 | ...-990,1045-1046 
  ...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.69 |    77.08 |   81.25 |   85.69 | ...95-911,957-958 
  ...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.52 |   86.66 |   95.49 | ...49,464,536-537 
  ...p-resource.ts |   96.85 |      100 |   91.66 |   96.85 | 92-96             
  ...d-artifact.ts |   91.18 |    86.71 |    87.5 |   91.18 | ...26-427,441-453 
  ripGrep.ts       |    94.6 |    87.26 |   95.23 |    94.6 | ...33-734,740-741 
  ...-transport.ts |   71.42 |    55.55 |   71.42 |   71.42 | ...36-137,143-144 
  send-message.ts  |   81.13 |    89.74 |    62.5 |   81.13 | ...80-286,363-371 
  ...n-mcp-view.ts |   94.07 |    91.89 |    90.9 |   94.07 | 131-139           
  shell.ts         |   78.81 |    84.22 |   91.91 |   78.81 | ...5035,5098-5099 
  skill-utils.ts   |     100 |      100 |     100 |     100 |                   
  skill.ts         |   91.39 |    92.55 |      90 |   91.39 | ...84,488,534-556 
  ...eticOutput.ts |   95.12 |      100 |      80 |   95.12 | 87-88             
  task-create.ts   |    94.4 |    93.33 |   81.81 |    94.4 | 45-49,63-64,95    
  task-list.ts     |   73.38 |    77.77 |   83.33 |   73.38 | ...02,105,109-116 
  task-stop.ts     |   93.14 |    96.15 |   85.71 |   93.14 | 39-40,54-64       
  task-update.ts   |   82.89 |    83.92 |    92.3 |   82.89 | ...14-422,454-465 
  team-create.ts   |   97.22 |    85.71 |   83.33 |   97.22 | 48-49,129-130     
  team-delete.ts   |   86.74 |    83.33 |   83.33 |   86.74 | 37-38,42-48,72-73 
  ...n-approval.ts |   92.14 |    96.77 |   77.77 |   92.14 | 38-39,42-43,93-99 
  todoWrite.ts     |   95.13 |    87.85 |   93.33 |   95.13 | ...23-527,540-545 
  tool-error.ts    |     100 |      100 |     100 |     100 |                   
  tool-names.ts    |     100 |      100 |     100 |     100 |                   
  tool-registry.ts |   78.57 |    79.59 |    82.6 |   78.57 | ...89-990,998-999 
  tool-search.ts   |   96.19 |    89.72 |   93.33 |   96.19 | ...09,259-264,426 
  tools.ts         |   93.11 |    92.53 |   91.66 |   93.11 | ...69-570,586-592 
  ...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    |    86.7 |    84.92 |   88.88 |    86.7 | ...24-827,864-899 
  zoom-image.ts    |   95.76 |    93.75 |      90 |   95.76 | 54-59,203-204     
 src/tools/agent   |   87.22 |    87.68 |   88.69 |   87.22 |                   
  agent.ts         |   85.84 |    86.59 |   86.31 |   85.84 | ...4315,4337-4347 
  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 |                   
 ...s/computer-use |   90.21 |    82.17 |   78.08 |   90.21 |                   
  bootstrap.ts     |   59.42 |    80.95 |   41.66 |   59.42 | ...35-339,341-345 
  client.ts        |   80.11 |       90 |   77.77 |   80.11 | ...97,242-243,274 
  constants.ts     |     100 |    94.73 |     100 |     100 | 129,256           
  downloader.ts    |   65.29 |    52.77 |   58.33 |   65.29 | ...99-300,316-355 
  index.ts         |     100 |      100 |     100 |     100 |                   
  install-state.ts |   94.44 |    72.72 |     100 |   94.44 | 44-45             
  ...n-detector.ts |     100 |     87.5 |     100 |     100 | 50                
  schemas.ts       |     100 |      100 |     100 |     100 |                   
  tool.ts          |    96.3 |    85.71 |     100 |    96.3 | 75-76,184,252-258 
 ...tools/workflow |   86.51 |    84.81 |      75 |   86.51 |                   
  workflow.ts      |   86.51 |    84.81 |      75 |   86.51 | ...67,512,514-515 
 src/utils         |   92.89 |     89.6 |   96.88 |   92.89 |                   
  LruCache.ts      |     100 |      100 |     100 |     100 |                   
  ...Controller.ts |     100 |      100 |     100 |     100 |                   
  ...ssageQueue.ts |     100 |      100 |     100 |     100 |                   
  ...cFileWrite.ts |   94.94 |    92.47 |     100 |   94.94 | ...43-544,651-655 
  bareMode.ts      |   81.81 |      100 |      50 |   81.81 | 18-19             
  ...ry-content.ts |   98.45 |    95.45 |     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.88 |    94.11 |      95 |   95.88 | ...98-499,511-524 
  ...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   |   96.66 |    96.61 |   88.88 |   96.66 | 192-196           
  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     
  ...entContext.ts |   96.63 |    90.13 |   96.66 |   96.63 | ...42,444-445,512 
  errorParsing.ts  |     100 |      100 |     100 |     100 |                   
  ...rReporting.ts |   95.65 |    93.33 |     100 |   95.65 | 37-38             
  errors.ts        |   83.01 |    95.03 |    61.9 |   83.01 | ...62-378,382-388 
  fetch.ts         |   90.68 |    82.51 |     100 |   90.68 | ...72,483-484,503 
  file-identity.ts |     100 |      100 |     100 |     100 |                   
  fileUtils.ts     |   94.87 |    92.95 |   96.15 |   94.87 | ...1907,1915-1916 
  forkedAgent.ts   |   92.45 |    82.35 |   93.75 |   92.45 | ...34,642,647-654 
  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             
  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.02 |    81.25 |   85.71 |   78.02 | ...22-123,147-198 
  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.12 |    93.33 |     100 |   95.12 | ...68-172,240-244 
  ...ionManager.ts |     100 |     90.9 |     100 |     100 | 27                
  ...lPromptIds.ts |     100 |      100 |     100 |     100 |                   
  ...on-context.ts |     100 |      100 |     100 |     100 |                   
  jsonl-utils.ts   |   95.27 |     93.1 |     100 |   95.27 | ...16-317,359-362 
  ...-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                
  ...iagnostics.ts |    96.4 |     94.2 |     100 |    96.4 | ...66,293-294,376 
  ...yDiscovery.ts |    92.4 |    89.13 |     100 |    92.4 | ...28,331,522-525 
  ...tProcessor.ts |   94.01 |       90 |     100 |   94.01 | ...47-353,445-446 
  ...Inspectors.ts |     100 |      100 |     100 |     100 |                   
  modelId.ts       |   98.96 |    98.21 |     100 |   98.96 | 153               
  ...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         |   93.61 |    92.42 |     100 |   93.61 | ...62-563,565-567 
  pdf.ts           |   92.17 |    85.81 |     100 |   92.17 | ...64-565,606-611 
  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.15 |     100 |   96.98 | ...87-688,763-764 
  readManyFiles.ts |   95.75 |    80.86 |     100 |   95.75 | ...05,558,568-572 
  retry.ts         |   96.09 |    92.52 |     100 |   96.09 | ...67,558-559,577 
  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 
  ...sDiscovery.ts |   97.46 |    93.05 |     100 |   97.46 | ...04,182-183,202 
  ...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.5 |    89.74 |     100 |    97.5 | 162-163           
  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.03 |    97.75 |     100 |   98.03 | 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 |      100 |     100 |     100 |                   
  ...orageUtils.ts |   95.98 |    83.96 |     100 |   95.98 | ...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.07 |    88.33 |     100 |   86.07 | ...2269,2276-2280 
  ...lAstParser.ts |   98.27 |    91.38 |     100 |   98.27 | ...1321-1323,1333 
  ...ContextEnv.ts |     100 |       92 |     100 |     100 | 50-52             
  ...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 |       50 |     100 |   77.77 | 44,54-59          
  ...e-encoding.ts |   85.96 |    76.47 |     100 |   85.96 | 58-61,64-65,78-79 
  ...emEncoding.ts |   96.36 |    91.17 |     100 |   96.36 | 59-60,124-125     
  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             
  ...name-utils.ts |     100 |      100 |     100 |     100 |                   
  ...-finalizer.ts |   97.66 |     90.9 |     100 |   97.66 | 165-166,168-172   
  tool-utils.ts    |    95.2 |    93.61 |     100 |    95.2 | ...58-159,162-163 
  ...ultCleanup.ts |   54.62 |       25 |      75 |   54.62 | ...03-105,108-134 
  ...Compaction.ts |   96.13 |    96.42 |     100 |   96.13 | ...34-339,341-346 
  ...pt-records.ts |    87.5 |    86.02 |     100 |    87.5 | ...76-480,510-525 
  truncation.ts    |   90.56 |    90.43 |     100 |   90.56 | ...35-443,480-486 
  windowsPath.ts   |   89.47 |    79.31 |     100 |   89.47 | ...57-58,62,90-91 
  ...aceContext.ts |   96.74 |    91.04 |     100 |   96.74 | ...69,196,299-301 
  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.72 |   94.73 |   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.43 |   89.47 |   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 |   69.76 |    75.47 |   85.29 |   69.76 |                   
  ...eTokenizer.ts |   65.72 |    74.02 |    92.3 |   65.72 | ...65-466,479-533 
  index.ts         |     100 |      100 |     100 |     100 |                   
  ...tTokenizer.ts |   68.39 |    69.49 |    90.9 |   68.39 | ...24-325,327-328 
  ...ageFormats.ts |   76.92 |      100 |   33.33 |   76.92 | 46-49,56-57       
  textTokenizer.ts |     100 |      100 |     100 |     100 |                   
  types.ts         |       0 |        0 |       0 |       0 | 1                 
-------------------|---------|----------|---------|---------|-------------------

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 commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 28352d5, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

⚠️ AutoFix round 11 ended without publishing a reportview run.

中文说明

⚠️ AutoFix 第 11 轮结束但未发布报告 —— 查看运行

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

What I found before stopping:
Qwen failed during address-review: status 125.

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31283575452


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

… surface

The refcounted acquireInheritedLoaderEnvScrub read/wrote process.env from
config/shared-env-keys.ts, which the serve process.env guard does not scan —
moving the access out of run-qwen-serve.ts dropped its allowlisted count and
failed process-env-guard.test.ts. Pass the env into the coordinator instead so
run-qwen-serve.ts still owns the process.env reference (matching the existing
scrub helpers), and update the allowlist to the new count.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action required

Nothing to address this round on PR #8763 (head e34df6db92).

Feedback triage

  • Reviews: none newer than the last evaluation (2026-08-08T22:22:36Z).
  • Inline comments: none newer than the last evaluation.
  • Issue-level comments: only the automated serve daemon A/B report, which found ✅ no response changes against the PR base across 4 scenarios — informational, no change requested.
  • Failed / still-red checks: none. All completed checks are SUCCESS or SKIPPED; the only check still running is the automated PR review that produces this round's feedback.
  • No merge conflicts with main; no retry context, budget warnings, or rejected attempts in this round's inputs.

No code changes were made and no commit was created; no verification commands were required because the working tree is untouched.

中文说明

Autofix 评审轮次:无需处理

本轮在 PR #8763(head e34df6db92)上没有需要处理的内容。

反馈分类

  • 评审(Reviews):自上次评估(2026-08-08T22:22:36Z)以来没有新的评审。
  • 行内评论(Inline comments):自上次评估以来没有新的行内评论。
  • Issue 级评论:仅有 serve 守护进程 A/B 自动化测试报告,该报告显示与 PR base 相比在 4 个场景中均无响应变化(✅)——属于信息性内容,未要求任何改动。
  • 失败 / 持续失败的检查:无。所有已完成的检查均为 SUCCESS 或 SKIPPED;唯一仍在运行的检查是产生本轮反馈的自动化 PR 评审。
  • main 无合并冲突;本轮输入中也没有重试上下文、预算警告或被拒绝的尝试。

未做任何代码改动,也未创建提交;由于工作区未被修改,无需运行任何验证命令。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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.

Unresolved, please confirm: [Critical] issue-level comment 5228685910 (@qwen-code-dev-bot autofix status): reports the autofix agent's own failed run on a stale base ('Qwen failed during address-review: status 125') and names no code defect to trace against this diff — cannot tell

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not explored to full depth (tool budget reached): PR #8763 is a security-hardening follow-up to #8663: it e...: whether any close()-teardown step before restoreScrubbedLoaderEnv() (specifically cleanupLiveDiscovery() at run-qwen-serve.ts:6427/7143 or daemonLog.close(….

中文说明

未决,请确认:[Critical] issue-level comment 5228685910 (@qwen-code-dev-bot autofix status): reports the autofix agent's own failed run on a stale base ('Qwen failed during address-review: status 125') and names no code defect to trace against this diff — cannot tell

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未探索到全部深度(达到工具调用预算):PR #8763 is a security-hardening follow-up to #8663: it e...:whether any close()-teardown step before restoreScrubbedLoaderEnv() (specifically cleanupLiveDiscovery() at run-qwen-serve.ts:6427/7143 or daemonLog.close(…

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

Comment on lines +70 to +72
'GIT_CONFIG_GLOBAL',
'GIT_CONFIG_SYSTEM',
'GIT_CONFIG_COUNT',

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 git config-injection family this tier blocks remains fully reachable through GIT_CONFIG_PARAMETERS — git honors it identically to GIT_CONFIG_COUNT and the numbered GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n> pairs, and core/utils/git-branches.ts GIT_ENV_VARS_TO_CLEAR (the list the new comment claims to mirror) already strips it. The new test even pins the omission (shared-env-keys.test.ts asserts isHardcodedProjectEnvExclusion('GIT_CONFIG_PARAMETERS') is false). — Failure scenario: attacker repo ships .env with GIT_CONFIG_PARAMETERS="'core.hooksPath=<attacker-dir>'"; the key passes every gate, is applied to process.env, frozen into daemonRuntimeBaseEnv and distributed to every workspace's session subprocesses; any session git commit then executes attacker hooks as the daemon user. Probe-verified end-to-end on this checkout (git 2.43.0): loadEnvironment applied the value from a project .env, and git commit executed the injected hook (core.fsmonitor and url.<base>.insteadOf injection work through the same variable).

Suggested change
'GIT_CONFIG_GLOBAL',
'GIT_CONFIG_SYSTEM',
'GIT_CONFIG_COUNT',
'GIT_CONFIG_GLOBAL',
'GIT_CONFIG_SYSTEM',
'GIT_CONFIG_COUNT',
'GIT_CONFIG_PARAMETERS',

Also flip the pinned assertion in shared-env-keys.test.ts (currently cementing the hole).

中文说明

此层阻断的 git 配置注入家族仍可通过 GIT_CONFIG_PARAMETERS 完全绕过——git 对它的处理与 GIT_CONFIG_COUNT 及编号 GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n> 对完全相同,而且 core/utils/git-branches.tsGIT_ENV_VARS_TO_CLEAR(新注释声称要对齐的列表)本来就清除了它。新增测试甚至把这一遗漏钉死了(shared-env-keys.test.ts 断言 isHardcodedProjectEnvExclusion('GIT_CONFIG_PARAMETERS')false)。失败场景:攻击者仓库的 .env 写入 GIT_CONFIG_PARAMETERS="'core.hooksPath=<attacker-dir>'";该键通过所有门控,被写入 process.env,冻结进 daemonRuntimeBaseEnv 并分发到每个 workspace 的会话子进程;任何会话执行 git commit 都会以 daemon 用户身份执行攻击者的 hook。已在本检出上端到端探针证实(git 2.43.0):loadEnvironment 从项目 .env 应用了该值,git commit 执行了被注入的 hook(core.fsmonitorurl.<base>.insteadOf 注入走同一变量)。同时需翻转 shared-env-keys.test.ts 中被钉死的断言(目前它把这个洞固化了下来)。

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

Comment on lines +68 to +69
'GIT_SSH_COMMAND',
'GIT_EXTERNAL_DIFF',

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] GIT_SSH — the documented legacy counterpart of GIT_SSH_COMMAND, which git still executes for SSH transports — is not excluded. — Failure scenario: attacker repo .env sets GIT_SSH=<path>/evil.sh; it passes every gate, and any session git fetch/push/clone/ls-remote over SSH execs the attacker script as the daemon user. Probe-confirmed on this checkout: GIT_SSH=<canary> git ls-remote ssh://…fatal: cannot exec '<canary>' — git attempted the exec. The sibling-state rule applies: the exec-redirect rationale for GIT_SSH_COMMAND one line away does not license leaving its legacy twin open.

Suggested change
'GIT_SSH_COMMAND',
'GIT_EXTERNAL_DIFF',
'GIT_SSH_COMMAND',
'GIT_SSH',
'GIT_EXTERNAL_DIFF',
中文说明

GIT_SSH——GIT_SSH_COMMAND 的文档化前身,git 在 SSH 传输时仍会执行它——没有被排除。失败场景:攻击者仓库 .env 设置 GIT_SSH=<path>/evil.sh;它通过所有门控,任何会话经 SSH 的 git fetch/push/clone/ls-remote 都会以 daemon 用户身份执行攻击者脚本。已在本检出上探针证实:GIT_SSH=<canary> git ls-remote ssh://…fatal: cannot exec '<canary>'——git 尝试了 exec。同族一致性规则适用:上一行 GIT_SSH_COMMAND 的"执行重定向"理由不能允许留下它的旧版孪生变量。

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

Comment on lines +364 to +366
export function clearLoaderKeyRejectionReporterIfCurrent(
reporter: LoaderKeyRejectionReporter,
): void {

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 clear-if-current guard only fixes the FIFO close order. The reporter slot is a single process-global last-writer-wins value, so when the later-installed daemon closes first (LIFO — the common embedded teardown order), it clears a slot whose previous occupant (a still-live co-resident daemon) was already overwritten at install and is never reinstalled. — Failure scenario: embedded daemons A then B; B closes first → the slot is emptied; every later loader-key rejection from still-live daemon A falls back to writeStderrLineSafe instead of A's durable daemon log — exactly the diagnostic loss this guard exists to prevent, in the other install order. Probe-confirmed the sequence (routedTo: [] until A reinstalled). Enforcement is unaffected; diagnostics only. Concrete cost: security-relevant rejections (a workspace .env attempted to inject loader keys) vanish without a durable record under the systemd/desktop daemon launches this reporter exists for. Suggested fix: make the slot a stack/registry — push on setLoaderKeyRejectionReporter, and on clear remove only the matching entry and reactivate the new top (or fan out to all registered reporters).

中文说明

仅在"仍是当前 reporter"时清除的守卫只修复了 FIFO 关闭顺序。reporter 槽是进程全局、后写者胜的单值:当后安装的 daemon 先关闭(LIFO——内嵌场景常见的拆除顺序)时,它清空了一个其前任占用者(仍存活的同进程 daemon)早在安装时就被覆盖、且永不会被重新安装的槽。失败场景:内嵌 daemon A 然后 B;B 先关闭 → 槽被清空;仍存活的 daemon A 之后每一次 loader 键拒绝都退回到 writeStderrLineSafe 而非 A 的持久 daemon 日志——这正是该守卫要防止的诊断丢失,只是发生在另一种安装顺序下。已用探针确认该序列(在 A 重新安装前 routedTo: [])。强制拒绝不受影响;仅诊断受损。具体代价:安全相关的拒绝记录(某 workspace .env 试图注入 loader 键)在该 reporter 为之存在的 systemd/桌面 daemon 启动下不留持久痕迹。建议修复:把槽改为栈/注册表——setLoaderKeyRejectionReporter 时入栈,清除时只移除自己的条目并重新激活新的栈顶(或向所有已注册 reporter 扇出)。

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

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.

Deferred to a follow-up. Valid finding: the clear-if-current guard only covers FIFO close order, so the LIFO teardown order (later-installed daemon closes first) still loses a co-resident survivor's reporter — diagnostics only, enforcement is unaffected, as the finding notes. This round was budget-limited to the two blocking Criticals; converting the reporter slot into a stack/registry is a mechanism redesign that deserves a dedicated change rather than a time-boxed round. Queued for the follow-up.

中文说明

延迟到后续处理。发现有效:clear-if-current 守卫只覆盖 FIFO 关闭顺序,因此 LIFO 拆除顺序(后安装的 daemon 先关闭)仍会丢失同进程幸存者的 reporter——正如发现所述,仅影响诊断,不影响强制拒绝。本轮受时间预算限制,只处理了两个阻断性 Critical;把 reporter 槽改造为栈/注册表属于机制重构,适合放在专门的变更中,而不适合在限时轮次里做。已列入后续计划。

Comment on lines +58 to +59
'REQUESTS_CA_BUNDLE',
'GIT_SSL_CAINFO',

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 TLS tier blocks CA-injection vars but not git's verification-disable var GIT_SSL_NO_VERIFY, which achieves the tier's stated MITM outcome without any attacker CA. (NODE_TLS_REJECT_UNAUTHORIZED is already covered above, so only the git side is open.) — Failure scenario: attacker repo .env sets GIT_SSL_NO_VERIFY=true; it passes every gate, is frozen into the daemon base env, and every session git HTTPS call skips certificate verification — MITM of token-bearing git traffic with one boolean, no attacker CA file needed. Demonstrated against a self-signed local endpoint on git 2.43.0.

Suggested change
'REQUESTS_CA_BUNDLE',
'GIT_SSL_CAINFO',
'REQUESTS_CA_BUNDLE',
'GIT_SSL_CAINFO',
'GIT_SSL_NO_VERIFY',
中文说明

该 TLS 层阻断了 CA 注入变量,但没有阻断 git 的验证禁用变量 GIT_SSL_NO_VERIFY——无需任何攻击者 CA 即可达成该层声明的 MITM 结果。(NODE_TLS_REJECT_UNAUTHORIZED 上方已在列,因此只剩 git 一侧开放。)失败场景:攻击者仓库 .env 设置 GIT_SSL_NO_VERIFY=true;它通过所有门控,被冻结进 daemon 基础环境,每个会话的 git HTTPS 调用都跳过证书验证——一个布尔值即可 MITM 携带 token 的 git 流量,无需攻击者 CA 文件。已在 git 2.43.0 上对自签名本地端点演示。

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

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.

Deferred to the immediate follow-up. Valid: GIT_SSL_NO_VERIFY reaches the TLS tier's stated MITM outcome without any attacker CA, and it belongs next to GIT_SSL_CAINFO in that tier. This round was budget-limited to the smallest blocking subset (the two Critical git-exec findings), so this one-line tier addition rides in the follow-up round, where it is the first candidate.

中文说明

延迟到紧接的下一轮。发现有效:GIT_SSL_NO_VERIFY 无需任何攻击者 CA 即可达成该 TLS 层声明的 MITM 结果,应与 GIT_SSL_CAINFO 同列于该层。本轮受时间预算限制,只处理最小阻断子集(两个 Critical 的 git 执行类发现),因此这个单行的层补充放到下一轮,且是下一轮的第一候选。

Comment thread docs/users/configuration/settings.md Outdated
> [!warning]
>
> **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `NODE_EXTRA_CA_CERTS`, or `DEV` (the dev-harness launch marker); those stay settable from the shell environment or a user-level `.env`. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses.
> **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `OPENSSL_CONF` (dlopens an attacker OpenSSL engine at startup), `NODE_REPL_EXTERNAL_MODULE`, `npm_config_node_gyp`, `npm_config_init_module`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `DEV` (the dev-harness launch marker), the TLS trust-anchor variables (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`, `GIT_SSL_CAINFO` — an attacker CA there would enable MITM of the token-bearing traffic a session's `git`/`npm`/`pip`/`curl` calls make), the git command-execution variables (`GIT_SSH_COMMAND`, `GIT_EXTERNAL_DIFF`, `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG_COUNT` and the numbered `GIT_CONFIG_KEY_<n>`/`GIT_CONFIG_VALUE_<n>` pairs — git runs these on any session `git` invocation), or the node-gyp interpreter-selection variables (`NODE_GYP_FORCE_PYTHON`, `npm_config_python`, `PYTHON` — run as the build Python during native-addon installs). Those stay settable from the shell environment or a user-level `.env`; unlike the loader list above they are rejected from project files only, so a value you export yourself is preserved. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses.

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 sentence overstates the tier: the new keys are rejected from the top-level env section of every settings.json — user-level included — not "project files only". The .env gate is scope-aware (!envFile.isHomeScopedEnvFile && isHardcodedProjectEnvExclusion(key) in environment.ts), but all four settings.env gates (environment.ts buildRuntimeEnvironment / loadEnvironment / reloadEnvironment and the serve fast path) call isHardcodedProjectEnvExclusion on the merged user+workspace settings object with no scope check. — Failure scenario: an operator following this doc places SSL_CERT_FILE (or any new reject-tier key) in the env section of their user-level ~/.qwen/settings.json → the value is silently dropped (probe observed no warning on any path — reportRejectedLoaderKeys only covers loader keys), TLS falls back to defaults, and no diagnostic points at the gate. Suggested fix: reword to "rejected from project .env files and from the top-level settings.json env section (any scope); preserved from the shell environment and user-level .env" — or scope the settings.env gates to match the .env gate.

中文说明

这句话夸大了该层的语义:新增键会从所有 settings.json(包括用户级)的顶层 env 段被拒绝,而不仅是"仅从项目文件拒绝"。.env 门控是区分作用域的(environment.ts!envFile.isHomeScopedEnvFile && isHardcodedProjectEnvExclusion(key)),但全部四个 settings.env 门控(environment.ts 的 buildRuntimeEnvironment / loadEnvironment / reloadEnvironment 与 serve 快速路径)都在合并后的 user+workspace 设置对象上调用 isHardcodedProjectEnvExclusion,没有作用域检查。失败场景:运维按照本文档把 SSL_CERT_FILE(或任何新的拒绝层键)放进用户级 ~/.qwen/settings.jsonenv 段 → 值被静默丢弃(探针观察到任何路径都没有警告——reportRejectedLoaderKeys 只覆盖 loader 键),TLS 回退到默认,且没有任何诊断指向该门控。建议修复:改写为"从项目 .env 文件与(任意作用域的)顶层 settings.json env 段拒绝;shell 环境与用户级 .env 保留"——或者让 settings.env 门控与 .env 门控的作用域一致。

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

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.

Deferred — needs a maintainer decision between your two options: (a) reword the docs to state that the new keys are rejected from the top-level env section of any-scope settings.json, or (b) make the four settings.env gates scope-aware like the .env gate. (b) changes this PR's security semantics (a user-level settings.json value would start applying), so it is not a call for a review round to make silently. Recommendation: (a) — minimal and factually accurate. Which option should we take?

中文说明

延迟处理——需要维护者在你的两个选项之间做决定:(a) 改写文档,说明新增键会从任意作用域的 settings.json 顶层 env 段被拒绝;或 (b) 让四个 settings.env 门控像 .env 门控一样区分作用域。(b) 会改变本 PR 的安全语义(用户级 settings.json 的值将开始生效),因此不适合由评审轮次悄悄决定。建议选 (a)——最小且与事实一致。请问选哪个?

for (const key of Object.keys(env)) {
if (!isLoaderEnvKey(key)) continue;
const value = env[key];
if (value !== undefined) sharedProcessEnvScrubOriginals.set(key, value);

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] Test-efficacy probe (harness validated), surviving mutant: forcing this guard's condition to true leaves every affected test green — no test pins when the guard must NOT fire. — Failure scenario: with the guard disabled, an undefined value gets snapshotted and the release path would write the literal string "undefined" into process.env, injecting a bogus loader value on restore; no test catches the guard rotting. (In practice Object.keys of a real env never yields undefined, which is why this stays green — but the defensive branch can now silently decay.) Suggested fix: add a negative case — an env object with an explicitly undefined loader key — asserting it is not snapshotted/restored; the two-cycle test proposed for the adjacent clears shares the same setup.

中文说明

测试有效性探针(harness 已验证),存活变异体:将该守卫条件强制为 true 后所有受影响测试仍为绿色——没有测试钉住守卫"不应触发"的情形。失败场景:守卫失效后,undefined 值会被快照,release 路径会把字面字符串 "undefined" 写入 process.env,在恢复时注入伪造的 loader 值;没有测试能发现守卫腐化。(实际上真实 env 的 Object.keys 从不会产生 undefined,这正是它保持绿色的原因——但该防御分支现在可以静默腐化。)建议修复:增加负例——一个显式包含 undefined loader 键的 env 对象——断言它不被快照/恢复;为相邻 clear 建议的双周期测试可共用同一 setup。

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

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.

Deferred to a follow-up. Valid surviving mutant: no test pins when the undefined-guard must not fire. The negative case (an explicitly undefined loader key is neither snapshotted nor restored) shares its setup with the two-cycle acquire/release test and will land with it.

中文说明

延迟到下一轮。存活变异体属实:没有测试钉住 undefined 守卫"不应触发"的情形。负例(显式为 undefined 的 loader 键既不被快照也不被恢复)与双周期 acquire/release 测试共用 setup,将与其一起合入。

Comment on lines +320 to +322
if (!Object.hasOwn(env, key)) env[key] = value;
}
sharedProcessEnvScrubOriginals.clear();

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] Test-efficacy probe (harness validated), surviving mutant: deleting the release-time sharedProcessEnvScrubOriginals.clear(); leaves every affected test green — same masking chain as the acquire-time clear: the snapshot map keeps stale entries until the next depth-0 acquire clears it, and a regression that also loses the acquire-time clear restores stale loader values into process.env with no failing test. Suggested fix: the same two-cycle acquire/release test (first cycle snapshots a loader key the second cycle's env lacks; assert no stale restore).

中文说明

测试有效性探针(harness 已验证),存活变异体:删除 release 时的 sharedProcessEnvScrubOriginals.clear(); 后所有受影响测试仍为绿色——与 acquire 时 clear 相同的掩盖链:快照 map 会保留陈旧条目直到下一次 depth-0 acquire 清空;若回归同时丢失 acquire 时的 clear,陈旧 loader 值会被恢复进 process.env 而没有任何测试失败。建议修复:同一个双周期 acquire/release 测试(第一周期快照一个第二周期环境中不存在的 loader 键;断言无陈旧恢复)。

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

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.

Deferred to a follow-up. Valid surviving mutant with the same masking chain as the acquire-time clear; the shared two-cycle acquire/release test covers both and will land in the follow-up round.

中文说明

延迟到下一轮。存活变异体属实,掩盖链与 acquire 时的 clear 相同;共用的双周期 acquire/release 测试同时覆盖两者,将在下一轮合入。

Comment on lines +329 to +330
sharedProcessEnvScrubDepth = 0;
sharedProcessEnvScrubOriginals.clear();

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] Test-efficacy probe (harness validated), surviving mutant: deleting the test-only reset's sharedProcessEnvScrubOriginals.clear(); leaves every affected test green — tests calling the reset rely on the depth-0 acquire's own clear to empty the map. — Failure scenario: any test later written to depend on the reset truly emptying the snapshot would pass against a broken reset until the masking changes. Suggested fix: assert observable post-reset state instead (e.g. a snapshot taken before reset is not restored after it).

中文说明

测试有效性探针(harness 已验证),存活变异体:删除仅测试用 reset 中的 sharedProcessEnvScrubOriginals.clear(); 后所有受影响测试仍为绿色——调用 reset 的测试依赖 depth-0 acquire 自身的 clear 来清空 map。失败场景:未来任何依赖 reset 真正清空快照的测试,在掩盖关系改变之前都会对着一个坏掉的 reset 通过。建议修复:改为断言 reset 后的可观察状态(例如 reset 前取得的快照在 reset 后不被恢复)。

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

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.

Deferred to a follow-up. Valid surviving mutant: tests calling the reset rely on the depth-0 acquire's own clear. Asserting observable post-reset state (a snapshot taken before reset is not restored after it) will be added together with the two-cycle scrub test work.

中文说明

延迟到下一轮。存活变异体属实:调用 reset 的测试依赖 depth-0 acquire 自身的 clear。将与双周期剥离测试一起,增加对 reset 后可观察状态的断言(reset 前取得的快照在 reset 后不被恢复)。

Comment on lines 23 to +25
import {
scrubAndReportInheritedLoaderEnv,
acquireInheritedLoaderEnvScrub,
clearLoaderKeyRejectionReporterIfCurrent,

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] Test-efficacy probe (harness validated), hunk-survived: reverting this import hunk — i.e. switching the serve entrypoint back from the refcounted acquireInheritedLoaderEnvScrub to the one-shot scrub — leaves every test in this diff green. The gating tests live in shared-env-keys.test.ts against the helper itself, not its adoption here. — Failure scenario: a future change that reverts the daemon serve path to the one-shot scrub undoes the concurrent-embedded-daemon fix; the first daemon's close() again restores loader vars into a still-live sibling's sessions — with no failing test. Suggested fix: add a serve-surface test with two overlapping embedded runQwenServe instances asserting the first close does not restore loader vars into the survivor's env.

中文说明

测试有效性探针(harness 已验证),hunk 存活:单独回退此 import hunk——即把 serve 入口从引用计数的 acquireInheritedLoaderEnvScrub 换回一次性剥离——本 diff 中所有测试仍为绿色。钉住行为的测试位于 shared-env-keys.test.ts,针对的是辅助函数本身,而非此处的采用。失败场景:未来把 daemon serve 路径回退到一次性剥离的改动会撤销并发内嵌 daemon 修复;第一个 daemon 的 close() 会再次把 loader 变量恢复进仍存活同伴的会话——且没有任何测试失败。建议修复:增加 serve 层测试,两个重叠的内嵌 runQwenServe 实例,断言第一个 close 不会把 loader 变量恢复进幸存者的环境。

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

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.

Deferred to a follow-up. Valid hunk-survived probe: reverting the adoption of the refcounted scrub ships green. A serve-surface test with two overlapping embedded runQwenServe instances is non-trivial harness work and is deferred under this round's budget limit; it is queued together with the reporter startup-failure test.

中文说明

延迟到下一轮。hunk 存活探针有效:回退对引用计数剥离的采用仍能绿色合入。两个重叠内嵌 runQwenServe 实例的 serve 层测试是不简单的 harness 工作,在本轮预算限制下延迟;已与 reporter 启动失败测试一起排队。

Comment on lines +1967 to +1970
// Called with the loader-key rejection reporter this run installed, so the
// startup-failure catch can clear it only when it is still the active one —
// a co-resident daemon that installed after us must keep its own reporter.
reporterInstalled(reporter: LoaderKeyRejectionReporter): void;

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] Test-efficacy probe (harness validated), hunk-survived: reverting the reporterInstalled lifecycle-callback hunk leaves every test green. — Failure scenario: if a refactor drops this callback and the startup-failure catch falls back to unconditionally clearing the reporter (setLoaderKeyRejectionReporter(undefined)), the first daemon's failed startup again drops a co-resident daemon's reporter — silently routing its loader-key rejections to the stderr fallback, the exact race this hunk fixes — with no failing test. Suggested fix: add a test where daemon A installs a reporter, daemon B installs after, and A's startup-failure path runs; assert B's reporter is still the active one.

中文说明

测试有效性探针(harness 已验证),hunk 存活:单独回退 reporterInstalled 生命周期回调 hunk 后所有测试仍为绿色。失败场景:若重构删除该回调、启动失败 catch 退回无条件清除 reporter(setLoaderKeyRejectionReporter(undefined)),第一个 daemon 的启动失败会再次丢掉同进程 daemon 的 reporter——其 loader 键拒绝被静默路由到 stderr 回退,正是该 hunk 修复的竞态——且没有任何测试失败。建议修复:增加测试,daemon A 安装 reporter,daemon B 随后安装,然后走 A 的启动失败路径;断言 B 的 reporter 仍是当前活跃者。

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

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.

Deferred to a follow-up. Valid hunk-survived probe: dropping the reporterInstalled lifecycle callback would let a failed startup clear a co-resident daemon's reporter, with no failing test. The A-installs / B-installs / A-fails reporter test will be added together with the overlapping-daemon serve-surface test.

中文说明

延迟到下一轮。hunk 存活探针有效:删除 reporterInstalled 生命周期回调后,启动失败会清掉同进程 daemon 的 reporter,且没有任何测试失败。A 安装 / B 安装 / A 失败的 reporter 测试将与重叠 daemon 的 serve 层测试一起添加。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (7200000ms)) (attempt 2/100) — it will retry on the next scan.

⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded. What the agent reported:
Qwen failed during address-review: timeout (7200000ms).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31290546316


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

…denylist

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 3/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 3/100 轮)。改动内容与我反驳保留之处如下:

Address-review summary — PR #8763 (round after budget timeout)

Scope decision

This round carried a budget warning: the previous round exhausted its time budget without finishing anything. Per the workflow rules, this round addressed only the smallest blocking subset — the two Critical findings — committed as soon as complete, and deferred the twelve Suggestions with per-thread replies (comment-replies.json) so no finding is dropped silently.

Feedback dispositions

Addressed (2 Criticals, one commit: 46e2690465)

Finding Disposition Change
[Critical] GIT_CONFIG_PARAMETERS left the git config-injection family reachable (inline 3742328269) Fixed — verified: core/utils/git-branches.ts GIT_ENV_VARS_TO_CLEAR (the list this PR's comment claims to mirror) already strips it, and the test at shared-env-keys.test.ts:166 pinned the omission Added 'GIT_CONFIG_PARAMETERS' to PROJECT_ENV_HARDCODED_EXCLUSIONS; flipped the pinned assertion (the prefix-overmatch negative case now uses GIT_CONFIG_NOSYSTEM, which only skips the system gitconfig read and injects nothing); extended the case-insensitive coverage; extended the end-to-end loadEnvironment rejection test (environment.test.ts); updated the docs enumeration
[Critical] GIT_SSH (legacy counterpart of the blocked GIT_SSH_COMMAND) not excluded (inline 3742328276) Fixed — verified: git still execs GIT_SSH for SSH transports, and the denylist is centralized in isHardcodedProjectEnvExclusion, which all six application gates (4 in environment.ts, 2 in fast-path-settings.ts) route through Added 'GIT_SSH' next to 'GIT_SSH_COMMAND'; same test/doc updates as above

Both keys stay in the reject-from-project-files tier (not the scrubbed loader set), consistent with their siblings: shell-exported and user-level .env values are preserved.

Deferred with replies (12 Suggestions — see comment-replies.json)

  • 3742328281 GIT_SSL_NO_VERIFY TLS-tier addition — valid; first candidate for the follow-up round.
  • 3742328283 docs overstate the settings.env tier scope — needs a maintainer decision between rewording the docs and making the settings.env gates scope-aware (option (b) changes the PR's security semantics); recommendation recorded in the reply.
  • 3742328285 npm_config_git missing from the docs enumeration — valid; lands with the follow-up.
  • 3742328278 reporter slot LIFO-order diagnostic loss — valid; stack/registry redesign deferred to a dedicated change.
  • 3742328286 no diagnostics for hardcoded-tier rejections — valid; warn-once machinery mirror deferred to a dedicated change.
  • 3742328289, 3742328293, 3742328295, 3742328297, 3742328299, 3742328302, 3742328305 test-efficacy probes (surviving mutants / hunk-survived reverts) — all valid; the proposed tests are grouped into the follow-up (one two-cycle scrub test covers three of the mutants).

Review-level notes

  • The CHANGES_REQUESTED state was driven by issue-level comment 5228685910, which reports the autofix agent's own failed run on a stale base (status 125) and names no code defect — there is nothing to trace in this diff; the substantive unresolved findings were the two Criticals above, now fixed.
  • The reviewer's "not reviewed" note (Integration Tests (CLI, No Sandbox) skipped in CI) is acknowledged: the changed behavior is exercised at the loadEnvironment/fast-path unit surface, which the focused suites cover end-to-end, including the exact attack scenario (project .env → rejected).
  • The reviewer's budget-cut exploration (close()-teardown ordering before restoreScrubbedLoaderEnv()) was left open by the reviewer itself and is not a finding; the scrub-lifecycle questions it raises are covered by the deferred test-efficacy replies above.

Conflict notes

--conflict false: no merge of origin/main performed; none needed.

Verification

Commands actually run this round (repo root unless noted):

  • npm run build — passed (also produced the missing packages/channels/base/dist entry that made fast-path.test.ts uncollectable before the build — a stale-checkout artifact, not related to this change)
  • npm run typecheck — passed
  • npm run lint — passed
  • npx vitest run src/config/shared-env-keys.test.ts src/config/environment.test.ts src/serve/fast-path.test.ts (in packages/cli) — 141 passed (3 files)
  • npx vitest run src/commands/channel/daemon-worker.test.ts src/serve/process-env-guard.test.ts (in packages/cli) — 81 passed (2 files)
  • npx prettier --check on the four touched files — passed

Integration tests after npm run bundle were not run: the touched behavior (project .env/settings.env rejection) is fully exercised by the unit surfaces above, not only through the bundled CLI. npm run generate:settings-schema was not needed: no settings source changed.

中文说明

处理评审总结 — PR #8763(预算超时后的本轮)

范围决定

本轮带有预算警告:上一轮耗尽了时间预算且没有完成任何内容。按照工作流规则,本轮只处理最小阻断子集——两条 Critical 发现——完成后立即提交,并将十二条 Suggestion 以逐线程回复(comment-replies.json)的方式延迟处理,确保没有任何发现被悄悄丢弃。

反馈处置

已处理(2 条 Critical,单次提交:46e2690465

发现 处置 变更
[Critical] GIT_CONFIG_PARAMETERS 使 git 配置注入家族仍然可达(inline 3742328269) 已修复——已核实:core/utils/git-branches.tsGIT_ENV_VARS_TO_CLEAR(本 PR 注释声称要对齐的列表)本来就清除了它,而 shared-env-keys.test.ts:166 的测试把这一遗漏钉死了 PROJECT_ENV_HARDCODED_EXCLUSIONS 中新增 'GIT_CONFIG_PARAMETERS';翻转被钉死的断言(前缀过度匹配的负例改用 GIT_CONFIG_NOSYSTEM——它只跳过系统 gitconfig 读取,不能注入任何配置);补充大小写不敏感覆盖;扩展端到端 loadEnvironment 拒绝测试(environment.test.ts);更新文档枚举
[Critical] GIT_SSH(已被阻断的 GIT_SSH_COMMAND 的旧版对应变量)未被排除(inline 3742328276) 已修复——已核实:git 在 SSH 传输时仍会执行 GIT_SSH,且拒绝名单集中在 isHardcodedProjectEnvExclusion,全部六个应用门控(environment.ts 中 4 处、fast-path-settings.ts 中 2 处)都经由它 'GIT_SSH_COMMAND' 旁新增 'GIT_SSH';测试/文档更新同上

两个键都保留在"仅从项目文件拒绝"层(不进入被剥离的 loader 集合),与其同族键一致:shell 导出值与用户级 .env 值仍然保留。

以回复延迟处理(12 条 Suggestion——见 comment-replies.json

  • 3742328281 TLS 层补充 GIT_SSL_NO_VERIFY——有效;下一轮的第一候选。
  • 3742328283 文档夸大了 settings.env 层的作用域——需要维护者在"改写文档"与"让 settings.env 门控区分作用域"之间做决定(选项 (b) 会改变本 PR 的安全语义);回复中已记录建议。
  • 3742328285 文档枚举缺少 npm_config_git——有效;随下一轮一起合入。
  • 3742328278 reporter 槽在 LIFO 顺序下的诊断丢失——有效;栈/注册表改造延迟到专门的变更。
  • 3742328286 硬编码层拒绝没有任何诊断——有效;复刻 warn-once 机制延迟到专门的变更。
  • 3742328289、3742328293、3742328295、3742328297、3742328299、3742328302、3742328305 测试有效性探针(存活变异体 / hunk 存活回退)——全部有效;所提议的测试归入下一轮(其中一个双周期剥离测试可覆盖其中三个变异体)。

评审层面的说明

  • CHANGES_REQUESTED 状态源自 issue 级评论 5228685910,该评论报告的是 autofix 代理自身在陈旧 base 上的失败运行(status 125),没有指出任何代码缺陷——本 diff 中无可追溯的内容;实质性的未决发现就是上面两条 Critical,现已修复。
  • 评审者的"未审查"说明(CI 中跳过了 Integration Tests (CLI, No Sandbox))已知悉:本次变更的行为在 loadEnvironment/fast-path 单元层面被完整覆盖,聚焦套件已端到端覆盖,包括确切的攻击场景(项目 .env → 被拒绝)。
  • 评审者因预算用尽而未完成的探索(restoreScrubbedLoaderEnv() 之前的 close() 拆除顺序)由评审者自己留作未决,不构成发现;其引发的剥离生命周期问题已由上面延迟的测试有效性回复覆盖。

冲突说明

--conflict false:未合并 origin/main,也无需合并。

验证

本轮实际执行的命令(未注明者均在仓库根目录):

  • npm run build —— 通过(同时生成了此前缺失的 packages/channels/base/dist 入口文件——该缺失曾导致 fast-path.test.ts 无法收集,属于检出环境的陈旧产物,与本变更无关)
  • npm run typecheck —— 通过
  • npm run lint —— 通过
  • npx vitest run src/config/shared-env-keys.test.ts src/config/environment.test.ts src/serve/fast-path.test.ts(在 packages/cli 内)—— 141 通过(3 个文件)
  • npx vitest run src/commands/channel/daemon-worker.test.ts src/serve/process-env-guard.test.ts(在 packages/cli 内)—— 81 通过(2 个文件)
  • 对四个被改动文件执行 npx prettier --check —— 通过

未运行 npm run bundle 之后的集成测试:本次变更的行为(项目 .env/settings.env 拒绝)已由上述单元层面完整覆盖,并非只能经由打包后的 CLI 验证。未运行 npm run generate:settings-schema:未改动任何 settings 源。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

Comment on lines +71 to +72
'GIT_SSH_COMMAND',
'GIT_SSH',

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 git command-execution family this tier blocks leaves sibling variables that git also executes — GIT_EXEC_PATH, GIT_TEMPLATE_DIR, and more conditionally GIT_ASKPASS / GIT_PROXY_COMMAND — absent from both denylist tiers, so they pass every gate from a project .env. Probe-confirmed at this commit: the predicates return false for all of them, loadEnvironment applies them from a project .env while the comparator GIT_SSH_COMMAND is rejected, and adding one line to PROJECT_ENV_HARDCODED_EXCLUSIONS flips the probe. — Failure scenario: untrusted repo .env sets GIT_EXEC_PATH=<repo>/.evil (with an executable git-remote-https) or GIT_TEMPLATE_DIR=<repo>/.tpl (with hooks/post-checkout); the value is frozen into daemonRuntimeBaseEnv, reaches every workspace's session subprocesses, and a session git fetch/git clone over HTTPS executes attacker code as the daemon user — verified against real git on this checkout (GIT_EXEC_PATH ran a canary remote helper; GIT_TEMPLATE_DIR planted the hook). This is the same cross-workspace exec outcome GIT_SSH_COMMAND is blocked for, and the same sibling shape as the round-1 GIT_SSH / GIT_CONFIG_PARAMETERS Criticals fixed at this commit. Evaluate GIT_EDITOR on the same terms.

Suggested change
'GIT_SSH_COMMAND',
'GIT_SSH',
'GIT_SSH_COMMAND',
'GIT_SSH',
'GIT_EXEC_PATH',
'GIT_TEMPLATE_DIR',
'GIT_ASKPASS',
'GIT_PROXY_COMMAND',
中文说明

此层阻断的 git 命令执行家族遗漏了 git 同样会执行的同族变量——GIT_EXEC_PATHGIT_TEMPLATE_DIR,以及条件稍多的 GIT_ASKPASS / GIT_PROXY_COMMAND——它们不在任何一层拒绝名单中,可以经由项目 .env 通过所有门控。已在本提交上探针证实:谓词对它们全部返回 false;loadEnvironment 从项目 .env 应用了它们,而对照组 GIT_SSH_COMMAND 被拒绝;向 PROJECT_ENV_HARDCODED_EXCLUSIONS 增加一行即可翻转探针。失败场景:不受信仓库的 .env 设置 GIT_EXEC_PATH=<repo>/.evil(内含可执行的 git-remote-https)或 GIT_TEMPLATE_DIR=<repo>/.tpl(内含 hooks/post-checkout);该值被冻结进 daemonRuntimeBaseEnv,到达每个 workspace 的会话子进程,任何会话经 HTTPS 的 git fetch/git clone 都会以 daemon 用户身份执行攻击者代码——已在本检出的真实 git 上验证(GIT_EXEC_PATH 执行了金丝雀 remote helper;GIT_TEMPLATE_DIR 植入了 hook)。与 GIT_SSH_COMMAND 被阻断的跨 workspace 执行后果相同,也与本轮已在该提交修复的 GIT_SSH / GIT_CONFIG_PARAMETERS 两条 Critical 属于同族同型。请以同样标准评估 GIT_EDITOR

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

Comment on lines +55 to +59
'SSL_CERT_FILE',
'SSL_CERT_DIR',
'CURL_CA_BUNDLE',
'REQUESTS_CA_BUNDLE',
'GIT_SSL_CAINFO',

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 TLS trust-anchor tier states its goal as stopping MITM of the token-bearing git/npm/pip/curl traffic, but the knobs npm and pip actually honor — npm_config_cafile, npm_config_ca, npm_config_strict_ssl, PIP_CERT, and git's directory twin GIT_SSL_CAPATH — pass every gate from a project .env (probe-confirmed at this commit). (GIT_SSL_NO_VERIFY is the same class but was already discussed and deferred — deliberately not re-reported here.) — Failure scenario: untrusted repo .env sets npm_config_cafile=<repo>/evil-ca.pem or PIP_CERT=<repo>/evil-ca.pem, or simply npm_config_strict_ssl=false (which needs no attacker CA at all); a session npm install / pip install then trusts the attacker CA — MITM of registry traffic (token theft, malicious package delivery). Consider PIP_INDEX_URL too (index redirection bypasses TLS entirely).

Suggested change
'SSL_CERT_FILE',
'SSL_CERT_DIR',
'CURL_CA_BUNDLE',
'REQUESTS_CA_BUNDLE',
'GIT_SSL_CAINFO',
'SSL_CERT_FILE',
'SSL_CERT_DIR',
'CURL_CA_BUNDLE',
'REQUESTS_CA_BUNDLE',
'GIT_SSL_CAINFO',
'GIT_SSL_CAPATH',
'npm_config_cafile',
'npm_config_ca',
'npm_config_strict_ssl',
'PIP_CERT',
中文说明

TLS 信任锚层的既定目标是阻断对携带 token 的 git/npm/pip/curl 流量的 MITM,但 npm 与 pip 实际读取的旋钮——npm_config_cafilenpm_config_canpm_config_strict_sslPIP_CERT,以及 git 的目录孪生变量 GIT_SSL_CAPATH——都可以从项目 .env 通过所有门控(已在本提交探针证实)。(GIT_SSL_NO_VERIFY 同属此类,但已在早前讨论并延期处理——此处有意不再重复报告。)失败场景:不受信仓库的 .env 写入 npm_config_cafile=<repo>/evil-ca.pemPIP_CERT=<repo>/evil-ca.pem,甚至只需 npm_config_strict_ssl=false(完全不需要攻击者 CA);会话执行 npm install / pip install 时即信任攻击者 CA——注册表流量被 MITM(窃取 token、投递恶意包)。另请考虑 PIP_INDEX_URL(索引重定向完全绕过 TLS)。

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

Comment thread docs/users/configuration/settings.md Outdated
> [!warning]
>
> **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `NODE_EXTRA_CA_CERTS`, or `DEV` (the dev-harness launch marker); those stay settable from the shell environment or a user-level `.env`. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses.
> **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `OPENSSL_CONF` (dlopens an attacker OpenSSL engine at startup), `NODE_REPL_EXTERNAL_MODULE`, `npm_config_node_gyp`, `npm_config_init_module`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `DEV` (the dev-harness launch marker), the TLS trust-anchor variables (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`, `GIT_SSL_CAINFO` — an attacker CA there would enable MITM of the token-bearing traffic a session's `git`/`npm`/`pip`/`curl` calls make), the git command-execution variables (`GIT_SSH_COMMAND`, `GIT_SSH`, `GIT_EXTERNAL_DIFF`, `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG_COUNT`, `GIT_CONFIG_PARAMETERS` and the numbered `GIT_CONFIG_KEY_<n>`/`GIT_CONFIG_VALUE_<n>` pairs — git runs these on any session `git` invocation), or the node-gyp interpreter-selection variables (`NODE_GYP_FORCE_PYTHON`, `npm_config_python`, `PYTHON` — run as the build Python during native-addon installs). Those stay settable from the shell environment or a user-level `.env`; unlike the loader list above they are rejected from project files only, so a value you export yourself is preserved. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses.

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 diff updates the settings.md enumeration for the four new loader keys (OPENSSL_CONF, NODE_REPL_EXTERNAL_MODULE, npm_config_node_gyp, npm_config_init_module), but the sibling enumeration in docs/users/qwen-serve.md:559 ("Loader-affecting variables (NODE_OPTIONS, … BASH_FUNC_*) are likewise never passed to session subprocesses") was not updated — the two docs that enumerate the same denylist now disagree. — Failure scenario: an operator auditing the qwen serve security model reads qwen-serve.md and concludes an inherited OPENSSL_CONF / NODE_REPL_EXTERNAL_MODULE / npm_config_node_gyp / npm_config_init_module reaches daemon session subprocesses, or cannot find the key while debugging why their exported value has no effect in a daemon session. — Suggested fix: add the four keys to the qwen-serve.md enumeration (matching how this PR handled settings.md).

中文说明

本 diff 更新了 settings.md 中四个新 loader 键(OPENSSL_CONFNODE_REPL_EXTERNAL_MODULEnpm_config_node_gypnpm_config_init_module)的枚举,但 docs/users/qwen-serve.md:559 的同源枚举("Loader-affecting variables (NODE_OPTIONS, … BASH_FUNC_*) are likewise never passed to session subprocesses")未同步更新——两份枚举同一拒绝名单的文档现已不一致。失败场景:审计 qwen serve 安全模型的运维者阅读 qwen-serve.md 时,会误以为继承的 OPENSSL_CONF / NODE_REPL_EXTERNAL_MODULE / npm_config_node_gyp / npm_config_init_module 仍会传入 daemon 会话子进程,或在排查自己导出的值为何在 daemon 会话中无效时找不到该键。修复建议:在 qwen-serve.md 的枚举中补上这四个键(与本 PR 处理 settings.md 的方式一致)。

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

Comment on lines +118 to +121
const HARDCODED_PROJECT_ENV_EXCLUSION_PREFIXES = [
'git_config_key_',
'git_config_value_',
] as const;

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 prefix-matched keys are hardcoded exclusions but not members of RELOAD_EXCLUDED_KEYS, whose gate (isReloadExcludedKey) is literal-set membership — unlike their literal sibling GIT_CONFIG_COUNT, which is reload-excluded via the PROJECT_ENV_HARDCODED_EXCLUSIONS spread. Probe-confirmed: on a mid-session reload from a home-scoped ~/.env, GIT_CONFIG_COUNT stayed frozen at the boot value while GIT_CONFIG_KEY_<n>/VALUE_<n> from the same file rotated freely — one half of a single mechanism frozen, the other half live. The home file is trusted, so this is the asymmetric guard, not an exploitable hole. — Failure scenario: a user editing GIT_CONFIG_COUNT and the numbered pairs together in ~/.env gets a half-applied git config after a settings reload. — Suggested fix: give the reload gate the same prefix handling:

return (
  RELOAD_EXCLUDED_KEYS_CASEFOLDED.has(lowerKey) ||
  HARDCODED_PROJECT_ENV_EXCLUSION_PREFIXES.some((p) => lowerKey.startsWith(p))
);
中文说明

前缀匹配的键是硬编码排除项,但不是 RELOAD_EXCLUDED_KEYS 的成员——该重载门控(isReloadExcludedKey)只做字面量集合匹配——与其同族的字面量兄弟 GIT_CONFIG_COUNT(经由 PROJECT_ENV_HARDCODED_EXCLUSIONS 展开而成为重载排除项)不一致。探针证实:来自 home 作用域 ~/.env 的会话中重载时,GIT_CONFIG_COUNT 保持启动时的冻结值,而同一文件中的 GIT_CONFIG_KEY_<n>/VALUE_<n> 自由轮换——同一机制的一半被冻结、另一半却生效。home 文件受信任,因此这是门控不对称,而非可利用漏洞。失败场景:用户在 ~/.env 中同时修改 GIT_CONFIG_COUNT 与编号对,settings 重载后会得到半应用的 git 配置。修复建议:让重载门控具备相同的前缀处理(见代码块)。

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

Comment on lines +85 to +87
'NODE_GYP_FORCE_PYTHON',
'npm_config_python',
'PYTHON',

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 new keys join RELOAD_EXCLUDED_KEYS via the PROJECT_ENV_HARDCODED_EXCLUSIONS spread (environment.ts), and reloadEnvironment's deletion loop skips reload-excluded keys — so deleting one of these keys from a home-scoped .env (exempt from rejection, applied at boot) leaves the stale value in process.env until process restart. Probe-confirmed: post-PR, removing SSL_CERT_FILE from ~/.env no longer propagates on reload while an ordinary key's removal does; pre-PR the removal propagated. — Failure scenario: operator sets SSL_CERT_FILE=/corp/ca.pem in ~/.env, later removes it for CA rotation; every session subprocess keeps inheriting the stale cert path with no diagnostic. (The same sharp edge pre-exists for NODE_EXTRA_CA_CERTS; this PR extends it to ~14 additional keys.) — Suggested fix: propagate deletions for keys whose provenance is a home-scoped .env (provenance is already tracked), or document the freeze-at-boot semantics for removals in the upgrade note.

中文说明

新键经由 PROJECT_ENV_HARDCODED_EXCLUSIONS 展开(environment.ts)进入 RELOAD_EXCLUDED_KEYS,而 reloadEnvironment 的删除循环会跳过重载排除键——因此从 home 作用域 .env(该层豁免拒绝、启动时已应用)中删除这些键后,陈旧值会一直留在 process.env 中直到进程重启。探针证实:PR 之后从 ~/.env 删除 SSL_CERT_FILE 不再随重载传播,而普通键的删除正常传播;PR 之前该删除是传播的。失败场景:运维者在 ~/.env 设置 SSL_CERT_FILE=/corp/ca.pem,后因 CA 轮换将其删除;所有会话子进程继续继承陈旧的证书路径,且无任何诊断。(同样的尖锐边在 NODE_EXTRA_CA_CERTS 上早已存在;本 PR 将其扩展到约 14 个新键。)修复建议:对来源为 home 作用域 .env 的键传播删除(来源已有跟踪),或在升级说明中注明删除同样冻结。

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

Comment on lines +2144 to +2149
const loaderEnvScrub = acquireInheritedLoaderEnvScrub(
process.env,
'qwen serve',
'daemon',
);
const scrubbedLoaderEnvKeys = loaderEnvScrub.removedKeys;

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] In the co-resident multi-daemon configuration this PR introduces, the daemon performing the final refcount release can have empty removedKeys — and both the stderr breadcrumb (scrubAndReportInheritedLoaderEnv skips on empty removals) and the durable-log entry (if (scrubbedLoaderEnvKeys.length > 0) below) are gated on removal count, while the final restore itself runs after this daemon's own daemonLog is already closed and is logged nowhere. — Failure scenario: operator exports NODE_OPTIONS; daemon A boots (scrubs, logs), co-resident daemon B boots (already-scrubbed env, removes nothing, logs nothing); A closes (no restore while B holds); B closes and the last release writes the loader vars back into the shared process.env. Investigating where/when the vars reappeared, the operator finds no scrub and no restore entry in B's durable log — the audit trail exists only in A's log. Diagnostics only; reject/restore behavior itself is correct and test-pinned. — Suggested fix: log the coordination state durably when a handle joins an already-active scrub, and surface the depth-0 restore through the callback channel before daemonLog.close().

中文说明

在本 PR 引入的同进程多 daemon 并发配置下,执行最后一次引用计数释放的 daemon 可能拥有空的 removedKeys——而 stderr 面包屑(scrubAndReportInheritedLoaderEnv 在空删除时跳过)与持久日志条目(下方 if (scrubbedLoaderEnvKeys.length > 0))都以删除数量为非空门控,同时最终的恢复本身发生在本 daemon 的 daemonLog 关闭之后,没有任何日志记录。失败场景:运维者导出 NODE_OPTIONS;daemon A 启动(剥离并记录),同进程 daemon B 启动(环境已剥离、无键可删、无日志);A 关闭(B 仍持有时不恢复);B 关闭时最后一次释放把 loader 变量写回共享 process.env。运维者排查这些变量何时何地重新出现时,在 B 的持久日志中既找不到剥离条目也找不到恢复条目——审计线索只存在于 A 的日志中。仅影响诊断;拒绝/恢复行为本身正确且有测试钉住。修复建议:当 handle 加入已激活的剥离时,将协调状态持久记录;并在 daemonLog.close() 之前通过回调通道上报 depth-0 恢复。

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

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.

Declined this round (recorded, not dropped): the finding is diagnostics-only — reject/restore behavior itself is correct and test-pinned, as the finding notes. Durably logging the coordination state when a handle joins an already-active scrub, and surfacing the depth-0 restore through the callback channel before daemonLog.close(), is a non-trivial rework of the serve daemon lifecycle (new callback surface plus durable-log ordering), which is disproportionate diff growth for a diagnostics gap in this budget-constrained round. The audit-trail scenario stays traceable through the first daemon's log. Happy to take this as a follow-up if a maintainer wants it sooner.

中文说明

本轮已记录并拒绝(非静默丢弃):该发现仅影响诊断——正如发现本身所述,拒绝/恢复行为是正确的,且有测试钉住。在 handle 加入已激活的剥离时持久记录协调状态、并在 daemonLog.close() 之前通过回调通道上报 depth-0 恢复,需要对 serve daemon 生命周期做不小的改造(新增回调面 + 持久日志排序),对于预算受限轮次中的一个诊断缺口而言属于不成比例的 diff 增长。该审计线索场景仍可通过第一个 daemon 的日志追溯。若维护者希望更早解决,愿意作为后续跟进处理。

Comment on lines +46 to +47
// The non-Node TLS trust-anchor vars reach the SAME MITM outcome for the
// curl/git/openssl/python tools a session subprocess routinely shells out

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 rc-file redirect knobs CURL_HOME and WGETRC are on neither tier, yet they reach the exact MITM outcome this comment names — for curl, one of the four tools it lists. Probe-confirmed end-to-end at this commit: both keys pass every gate and are applied from a project .env, and real curl/wget routed the session request through an attacker-designated proxy via $CURL_HOME/.curlrc (proxy = …) and WGETRC directives (http_proxy = …), with control arms failing closed. This is the config-file-redirect class the PR actively blocks in every other tier (npm_config_userconfig, GIT_CONFIG_GLOBAL, OPENSSL_CONF), not the declared search-path deferral. — Failure scenario: untrusted workspace .env sets CURL_HOME=<dir> with a .curlrc containing proxy = https://attacker:3128, cacert = <attacker CA>, or insecure; a session's token-bearing curl call is proxied/intercepted. WGETRC is the wget twin (https_proxy/check_certificate=off). — Suggested fix: add 'CURL_HOME' and 'WGETRC' beside the TLS anchors (the verifier applied this flip and it closed the gate on every surface).

中文说明

rc 文件重定向旋钮 CURL_HOMEWGETRC 不在任何一层拒绝名单中,却能达成本注释所列的确切 MITM 后果——而且针对的正是注释点名的四个工具之一 curl。已在本提交端到端探针证实:两个键通过所有门控并从项目 .env 被应用;真实 curl/wget 分别经由 $CURL_HOME/.curlrcproxy = …)与 WGETRC 指令(http_proxy = …)把会话请求路由到攻击者指定的代理,对照组则按预期失败。这属于本 PR 在其他各层主动阻断的"配置文件重定向"类(npm_config_userconfigGIT_CONFIG_GLOBALOPENSSL_CONF),而不属于已声明延期的搜索路径类。失败场景:不受信 workspace 的 .env 设置 CURL_HOME=<dir>,其 .curlrcproxy = https://attacker:3128cacert = <attacker CA>insecure;会话携带 token 的 curl 调用即被代理/拦截。WGETRC 是 wget 的孪生入口(https_proxy/check_certificate=off)。修复建议:在 TLS 信任锚旁新增 'CURL_HOME''WGETRC'(验证者应用该翻转后,所有应用面的门控均已关闭)。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (7200000ms)) (attempt 4/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: timeout (7200000ms).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31307298125


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

…c-file tiers

Close the round-2 review findings: block the remaining git
command-execution siblings (GIT_EXEC_PATH, GIT_TEMPLATE_DIR, GIT_ASKPASS,
GIT_PROXY_COMMAND, GIT_EDITOR), the npm/pip TLS trust knobs
(npm_config_cafile, npm_config_ca, npm_config_strict_ssl, PIP_CERT,
GIT_SSL_CAPATH), and the curl/wget rc-file redirects (CURL_HOME, WGETRC)
from project .env files. Freeze the numbered GIT_CONFIG_KEY_/VALUE_ pairs
on reload together with GIT_CONFIG_COUNT, and sync the qwen-serve.md
loader-key enumeration with settings.md.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下:

Round response to round-2 review feedback (PR #8763)

Commit: ade3833a27 on top of 46e2690465. No conflicts (--conflict false; origin/main was not merged).

Note: this round ran under a budget warning from the previous round, so the work focused on the blocking subset plus the cheap in-scope fixes; the one decline below is recorded with its reason and its thread stays open.

Feedback dispositions

Finding Severity Disposition
rc:3743394920 — git exec-family siblings missing from both tiers Critical Fixed
rc:3743394923 — npm/pip TLS trust knobs pass every gate Suggestion Fixed (PIP_INDEX_URL declined, see below)
rc:3743394925 — qwen-serve.md enumeration out of sync Suggestion Fixed
rc:3743394926 — reload gate lacks the prefix handling Suggestion Fixed
rc:3743394927 — home .env removals don't propagate on reload Suggestion Fixed via the documentation option the finding offered
rc:3743394928 — multi-daemon scrub/restore audit gap Suggestion Declined this round (reply posted on the thread, left open)
Review CHANGES_REQUESTED — integration suite skipped in CI, not run locally Review event Addressed with a local run (see Verification)

What changed

rc:3743394920 (Critical). Added the remaining git command-execution siblings to the reject-from-project-.env tier: GIT_EXEC_PATH, GIT_TEMPLATE_DIR, GIT_ASKPASS, GIT_PROXY_COMMAND. GIT_EDITOR was evaluated on the same terms as requested and added too: git executes it whenever an editor opens (git commit without -m, interactive rebase) — the same exec class as the already-blocked GIT_EXTERNAL_DIFF; the reject-only tier still preserves an operator's own shell/home value. Pinned by membership, case-folded predicate, and project-.env rejection tests. While editing the family comment I also corrected its overclaim that core/utils/git-branches.ts "scrubs exactly these" (it scrubs the config-injection subset).

rc:3743394923. Added the npm/pip/git trust knobs to the TLS tier: GIT_SSL_CAPATH, npm_config_cafile, npm_config_ca, npm_config_strict_ssl, PIP_CERT. npm_config_strict-ssl (hyphen form) is listed as well: npm treats underscore/hyphen spellings of a config key as the same key — the same model this PR's loader comment already relies on — so the hyphen twin is blocked too. PIP_INDEX_URL was deliberately not added: it is the registry-redirection class, and npm_config_registry is likewise not blocked (declared-deferred class); blocking only pip's index knob would be inconsistent and incomplete. If that class is closed later, it should be closed for npm and pip together.

rc:3743394925. The docs/users/qwen-serve.md loader enumeration now lists OPENSSL_CONF, NODE_REPL_EXTERNAL_MODULE, npm_config_node_gyp, npm_config_init_module, matching settings.md.

rc:3743394926. isReloadExcludedKey now ORs in isHardcodedProjectEnvExclusion, giving the reload gate the same prefix handling as the hardcoded tier (the literal hardcoded exclusions were already spread into RELOAD_EXCLUDED_KEYS, so this adds exactly the prefix coverage). The numbered GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n> pairs now freeze on reload together with GIT_CONFIG_COUNT — one mechanism, one gate. A new test pins the freeze for both edits and removals, and that an ordinary neighboring key still rotates.

rc:3743394927. Took the finding's documentation option rather than the provenance-based deletion change: settings.md now states the reject-only keys are frozen at boot from a user-level .env (edits and removals do not apply on settings reload until process restart). Implementing provenance-tracked deletion propagation is a larger reload-path behavior change that belongs in its own change.

rc:3743394928. Declined this round, with a reply on the thread: the finding is diagnostics-only (reject/restore behavior is correct and test-pinned, as the finding itself notes), and the durable-log coordination plus pre-close callback rework is disproportionate diff growth for a budget-constrained round. The thread stays open so the suggestion is not silently dropped.

Review-level CHANGES_REQUESTED (integration suite). The "Integration Tests (CLI, No Sandbox)" job is merge-queue-gated by design (if: github.event_name == 'merge_group' in ci.yml), so it is skipped on every PR push and runs before landing. As local evidence for this surface, the serve-related integration files were run against the freshly built bundle (npm run build + npm run bundle): cli/qwen-serve-routes.test.ts, cli/qwen-serve-streaming.test.ts, cli/daemon-invocation-context.test.ts — 41 tests passed. The full merge-queue suite remains the authoritative gate for the rest of that surface.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx eslint on the 4 touched source/test files — passed; npx prettier --check on the 6 touched files — passed
  • vitest run src/config/shared-env-keys.test.ts src/config/environment.test.ts (packages/cli) — 59 passed
  • vitest run src/serve/fast-path.test.ts src/serve/process-env-guard.test.ts src/commands/channel/daemon-worker.test.ts (packages/cli) — 166 passed
  • npm run bundle — passed
  • QWEN_SANDBOX=false vitest run --root ./integration-tests cli/qwen-serve-routes.test.ts cli/qwen-serve-streaming.test.ts cli/daemon-invocation-context.test.ts — 41 passed (3 files)
中文说明

对第 2 轮审查反馈的回应(PR #8763

提交:ade3833a27,基于 46e2690465。无冲突(--conflict false;未合并 origin/main)。

说明:由于上一轮耗尽时间预算,本轮带有预算警告,因此工作聚焦于阻塞子集加上低成本的范围内修复;下方唯一一条拒绝已记录理由,其讨论串保持打开。

反馈处置

发现 严重度 处置
rc:3743394920 — git 命令执行家族的同族变量在两层名单中均缺失 Critical 已修复
rc:3743394923 — npm/pip 的 TLS 信任锚旋钮可通过所有门控 Suggestion 已修复PIP_INDEX_URL 被拒绝,见下文)
rc:3743394925 — qwen-serve.md 枚举未同步 Suggestion 已修复
rc:3743394926 — 重载门控缺少前缀处理 Suggestion 已修复
rc:3743394927 — home .env 中的删除不随重载传播 Suggestion 按该发现提供的文档选项修复
rc:3743394928 — 多 daemon 剥离/恢复的审计缺口 Suggestion 本轮拒绝(已在讨论串回复并保持打开)
审查级 CHANGES_REQUESTED — 集成套件在 CI 被跳过且本地未运行 审查事件 以本地运行结果回应(见"验证"一节)

变更内容

rc:3743394920(Critical)。 将剩余的 git 命令执行家族变量加入"拒绝来自项目 .env"层:GIT_EXEC_PATHGIT_TEMPLATE_DIRGIT_ASKPASSGIT_PROXY_COMMAND。按要求以同样标准评估了 GIT_EDITOR 并一并加入:git 在需要编辑器时会执行它(不带 -mgit commit、交互式 rebase)——与已被阻断的 GIT_EXTERNAL_DIFF 同属命令执行类;仅拒绝层仍会保留操作者自己 shell/home 中的值。以成员断言、大小写折叠谓词断言和项目 .env 拒绝测试钉住。编辑该家族注释时,同时修正了其中"core/utils/git-branches.ts 已精确剥离这些键"的过度表述(它实际剥离的是配置注入子集)。

rc:3743394923。 将 npm/pip/git 的信任旋钮加入 TLS 层:GIT_SSL_CAPATHnpm_config_cafilenpm_config_canpm_config_strict_sslPIP_CERT。同时列出了 npm_config_strict-ssl(连字符形式):npm 将配置键的下划线/连字符拼写视为同一键——与本 PR loader 注释已采用的模型一致——因此连字符孪生形式也被阻断。PIP_INDEX_URL 有意加入:它属于注册表重定向类,而 npm_config_registry 同样未被阻断(已声明延期的类别);只阻断 pip 的索引旋钮既不一致也不完整。若将来关闭该类,应同时为 npm 与 pip 关闭。

rc:3743394925。 docs/users/qwen-serve.md 的 loader 枚举现已列出 OPENSSL_CONFNODE_REPL_EXTERNAL_MODULEnpm_config_node_gypnpm_config_init_module,与 settings.md 保持一致。

rc:3743394926。 isReloadExcludedKey 现在并入 isHardcodedProjectEnvExclusion,使重载门控具备与硬编码层相同的前缀处理(硬编码的字面量排除项本已展开进 RELOAD_EXCLUDED_KEYS,因此这里恰好只补上前缀覆盖)。编号 GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n> 对现在与 GIT_CONFIG_COUNT 一起在重载时冻结——同一机制、同一门控。新增测试钉住编辑与删除两种情形下的冻结,以及相邻普通键仍可轮换。

rc:3743394927。 采用该发现提供的文档选项,而非基于来源跟踪的删除传播:settings.md 现已说明这些"仅拒绝项目文件"的键在来自用户级 .env 时于启动时冻结(settings 重载不会应用对它们的修改或删除,直到进程重启)。实现基于来源的删除传播是对重载路径较大的行为变更,应作为独立变更处理。

rc:3743394928。 本轮拒绝,并已在讨论串回复:该发现仅影响诊断(拒绝/恢复行为正确且有测试钉住,发现本身也如此说明),而持久日志协调加上关闭前回调通道的改造,对于预算受限轮次而言属于不成比例的 diff 增长。讨论串保持打开,该建议不会被静默丢弃。

审查级 CHANGES_REQUESTED(集成套件)。 "Integration Tests (CLI, No Sandbox)" 任务在设计上仅由合并队列触发(ci.ymlif: github.event_name == 'merge_group'),因此每次 PR 推送都会被跳过,并在落地前运行。作为该应用面的本地证据,已对新构建的 bundle(npm run build + npm run bundle)运行 serve 相关集成文件:cli/qwen-serve-routes.test.tscli/qwen-serve-streaming.test.tscli/daemon-invocation-context.test.ts —— 41 个测试全部通过。该应用面其余部分的权威门控仍是合并队列的完整套件。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • 对 4 个改动的源码/测试文件运行 npx eslint — 通过;对 6 个改动文件运行 npx prettier --check — 通过
  • vitest run src/config/shared-env-keys.test.ts src/config/environment.test.ts(packages/cli)— 59 通过
  • vitest run src/serve/fast-path.test.ts src/serve/process-env-guard.test.ts src/commands/channel/daemon-worker.test.ts(packages/cli)— 166 通过
  • npm run bundle — 通过
  • QWEN_SANDBOX=false vitest run --root ./integration-tests cli/qwen-serve-routes.test.ts cli/qwen-serve-streaming.test.ts cli/daemon-invocation-context.test.ts — 41 通过(3 个文件)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Could not produce a passing fix for this feedback (round 6/100). This item now needs a human; the loop stays engaged and still picks up new feedback and base conflicts, but will not retry this item on its own.

What I found before stopping:
Qwen failed during address-review: status 137.

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31322652532


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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.

Reviewed. Suggestions are inline. Unresolved, please confirm: [Critical] issue-level comment 5228685910 (@qwen-code-dev-bot autofix status): reports the autofix agent's own failed run on a stale base ('Qwen failed during address-review: status 125') and names no code defect to trace against this diff — cannot tell Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally. Not explored to full depth (tool budget reached): PR #8763 is a security-hardening follow-up to #8663: it e...: full fast-path.test.ts and daemon-worker.test.ts suite runs (only filtered runs of the new fast-path tests; daemon-worker breadcrumb change was verified by read…; PR #8763 is a security-hardening follow-up to #8663: it e...: full unfiltered runs of fast-path.test.ts and daemon-worker.test.ts (I verified the daemon-worker change by code inspection and ran only the filtered new fa….

中文说明

已审查。 建议见行内评论。 未决,请确认:[Critical] issue-level comment 5228685910 (@qwen-code-dev-bot autofix status): reports the autofix agent's own failed run on a stale base ('Qwen failed during address-review: status 125') and names no code defect to trace against this diff — cannot tell 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。 未探索到全部深度(达到工具调用预算):PR #8763 is a security-hardening follow-up to #8663: it e...:full fast-path.test.ts and daemon-worker.test.ts suite runs (only filtered runs of the new fast-path tests; daemon-worker breadcrumb change was verified by read…;PR #8763 is a security-hardening follow-up to #8663: it e...:full unfiltered runs of fast-path.test.ts and daemon-worker.test.ts (I verified the daemon-worker change by code inspection and ran only the filtered new fa…

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

Comment thread docs/users/configuration/settings.md Outdated
> [!warning]
>
> **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `NODE_EXTRA_CA_CERTS`, or `DEV` (the dev-harness launch marker); those stay settable from the shell environment or a user-level `.env`. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses.
> **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `OPENSSL_CONF` (dlopens an attacker OpenSSL engine at startup), `NODE_REPL_EXTERNAL_MODULE`, `npm_config_node_gyp`, `npm_config_init_module`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `DEV` (the dev-harness launch marker), the TLS trust-anchor variables (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`, `GIT_SSL_CAINFO`, `GIT_SSL_CAPATH`, `npm_config_cafile`, `npm_config_ca`, `npm_config_strict_ssl`, `PIP_CERT` — an attacker CA there, or `npm_config_strict_ssl=false`, would enable MITM of the token-bearing traffic a session's `git`/`npm`/`pip`/`curl` calls make), the git command-execution variables (`GIT_SSH_COMMAND`, `GIT_SSH`, `GIT_EXEC_PATH`, `GIT_TEMPLATE_DIR`, `GIT_ASKPASS`, `GIT_PROXY_COMMAND`, `GIT_EDITOR`, `GIT_EXTERNAL_DIFF`, `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG_COUNT`, `GIT_CONFIG_PARAMETERS` and the numbered `GIT_CONFIG_KEY_<n>`/`GIT_CONFIG_VALUE_<n>` pairs — git runs these on any session `git` invocation), the curl/wget rc-file redirects (`CURL_HOME`, `WGETRC` — their rc files can install an attacker proxy or CA), `PIP_CONFIG_FILE` (redirects all of pip's configuration — `index-url`, `trusted-host`, proxy, or cert settings in an attacker file send session pip traffic or credentials to attacker infrastructure), `SSH_ASKPASS` (git/ssh execute it as the fallback passphrase-prompt program on an auth challenge), `LESSOPEN` and `LESSCLOSE` (`less` executes them as input preprocessors on every file a session views), or the node-gyp interpreter-selection variables (`NODE_GYP_FORCE_PYTHON`, `npm_config_python`, `PYTHON` — run as the build Python during native-addon installs — and `npm_config_git`, run as npm's git binary). Those stay settable from the shell environment or a user-level `.env`; unlike the loader list above they are rejected from project files only, so a value you export yourself is preserved. They are also frozen at boot from a user-level `.env`: a settings reload does not apply edits to them there — or their removal — until the process restarts. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses.

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 rewritten denylist paragraph enumerates npm_config_strict_ssl but omits the hyphen twin npm_config_strict-ssl, which the code deliberately blocks as well (shared-env-keys.ts lists both spellings: "npm treats underscore/hyphen spellings of a config key as the same key … so both forms of strict-ssl are listed"). Every other key of the reject-only tier is enumerated here. — Failure scenario: a project .env or settings.json env sets npm_config_strict-ssl=false; it is rejected silently (hardcoded-tier rejections emit no warning); the user consults this paragraph, finds only npm_config_strict_ssl, and concludes their spelling is not blocked — debugging a "bug" that is documented behavior for every other key in the list.

Suggested fix: in the TLS trust-anchor parenthetical, spell the alias the way the code does — npm_config_strict_ssl (and npm's equivalent npm_config_strict-ssl spelling).

中文说明

重写后的拒绝列表段落列出了 npm_config_strict_ssl,但遗漏了连字符孪生形式 npm_config_strict-ssl——代码有意将两者同时阻断(shared-env-keys.ts 同时列出两种拼写,并注明"npm 将配置键的下划线/连字符拼写视为同一键……因此 strict-ssl 的两种形式都被列出")。该层其余每个键都已在此列出。失败场景:项目 .envsettings.json env 设置 npm_config_strict-ssl=false;它被静默拒绝(硬编码层的拒绝不发出任何警告);用户查阅本段落,只找到 npm_config_strict_ssl,便以为自己的拼写未被阻断——为一个对清单中其他所有键而言属于既有行为的"bug"反复排查。

修复建议:在 TLS 信任锚括注中按代码的写法同时写出该别名——npm_config_strict_ssl(以及 npm 的等价拼写 npm_config_strict-ssl)。

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

Comment thread docs/users/configuration/settings.md Outdated
> [!warning]
>
> **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `NODE_EXTRA_CA_CERTS`, or `DEV` (the dev-harness launch marker); those stay settable from the shell environment or a user-level `.env`. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses.
> **Loader-affecting variables are always rejected:** Variables that make a spawned Node.js process or OS loader execute an attacker-chosen file — `NODE_OPTIONS`, `npm_config_node_options` (and npm's config-file redirects `npm_config_userconfig`, `npm_config_globalconfig`, `npm_config_script_shell`, `npm_config_prefix`), `NODE_PATH`, `OPENSSL_CONF` (dlopens an attacker OpenSSL engine at startup), `NODE_REPL_EXTERNAL_MODULE`, `npm_config_node_gyp`, `npm_config_init_module`, `LD_PRELOAD`, `LD_AUDIT`, `DYLD_INSERT_LIBRARIES`, `BASH_ENV`, `ZDOTDIR`, and exported bash function definitions (`BASH_FUNC_*`) — are never loaded from `.env` files (any scope, including `.qwen/.env` and user-level files) or from the top-level `settings.json` `env` section. A workspace-controlled value there could hijack module resolution for every subprocess Qwen Code spawns, so Qwen Code prints a warning when it rejects such a key (once per process, per key and source — in a multi-workspace daemon each workspace's rejection is reported separately). To use one of these variables, export it in the environment you launch Qwen Code from; sessions hosted by a `qwen serve` daemon deliberately do not inherit them, while direct editor (ACP) sessions and the plain CLI keep the exported value. Library _search_ paths (`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`) and the interactive-shell-only `ENV` are intentionally not on this list — rejecting them breaks mainstream toolchains (`ENV=production`, conda/CUDA library dirs) — but a project `.env` still cannot apply them on reload. This rejection applies to the top-level `env` section only: per-server `mcpServers[].env` and per-hook `hooks[].env` are intentionally scoped to that server or hook and still apply (both surfaces are gated by folder trust for workspace-provided configs). Separately, a project `.env` can never set `QWEN_CLI_ENTRY` (the daemon's session-process entrypoint), `DEV` (the dev-harness launch marker), the TLS trust-anchor variables (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`, `GIT_SSL_CAINFO`, `GIT_SSL_CAPATH`, `npm_config_cafile`, `npm_config_ca`, `npm_config_strict_ssl`, `PIP_CERT` — an attacker CA there, or `npm_config_strict_ssl=false`, would enable MITM of the token-bearing traffic a session's `git`/`npm`/`pip`/`curl` calls make), the git command-execution variables (`GIT_SSH_COMMAND`, `GIT_SSH`, `GIT_EXEC_PATH`, `GIT_TEMPLATE_DIR`, `GIT_ASKPASS`, `GIT_PROXY_COMMAND`, `GIT_EDITOR`, `GIT_EXTERNAL_DIFF`, `GIT_CONFIG_GLOBAL`, `GIT_CONFIG_SYSTEM`, `GIT_CONFIG_COUNT`, `GIT_CONFIG_PARAMETERS` and the numbered `GIT_CONFIG_KEY_<n>`/`GIT_CONFIG_VALUE_<n>` pairs — git runs these on any session `git` invocation), the curl/wget rc-file redirects (`CURL_HOME`, `WGETRC` — their rc files can install an attacker proxy or CA), `PIP_CONFIG_FILE` (redirects all of pip's configuration — `index-url`, `trusted-host`, proxy, or cert settings in an attacker file send session pip traffic or credentials to attacker infrastructure), `SSH_ASKPASS` (git/ssh execute it as the fallback passphrase-prompt program on an auth challenge), `LESSOPEN` and `LESSCLOSE` (`less` executes them as input preprocessors on every file a session views), or the node-gyp interpreter-selection variables (`NODE_GYP_FORCE_PYTHON`, `npm_config_python`, `PYTHON` — run as the build Python during native-addon installs — and `npm_config_git`, run as npm's git binary). Those stay settable from the shell environment or a user-level `.env`; unlike the loader list above they are rejected from project files only, so a value you export yourself is preserved. They are also frozen at boot from a user-level `.env`: a settings reload does not apply edits to them there — or their removal — until the process restarts. Upgrade note: before this denylist existed, some of these keys could load from `.env` files or `settings.json` `env` on some paths; they are now rejected everywhere with a warning, and a `qwen serve` daemon no longer passes inherited values of them to session subprocesses.

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 trailing "Upgrade note" this rewritten paragraph carries now also covers the newly added reject-only tier keys, for which all three of its claims are false: they are rejected from project files only, not "everywhere" (shell and home-.env values are preserved — as this paragraph itself states two sentences earlier); their rejection is silent (every hardcoded-tier rejection is a bare continue, and reportRejectedLoaderKeys filters through isLoaderEnvKey, so no warning path is reachable for these keys); and a qwen serve daemon still passes inherited values of them to session subprocesses (none of them are in INHERITED_LOADER_ENV_KEYS). — Failure scenario: an operator relying on this note either hunts for a rejection warning that is never emitted for the TLS/git/curl/pip/less/node-gyp keys, or believes the daemon scrubs exported GIT_SSH_COMMAND/PYTHON from session env — an overstatement of the security boundary in a hardening doc.

Suggested fix: scope the upgrade note to the loader tier and describe the project-file-only tier accurately — e.g. "the loader keys above are now rejected everywhere with a warning, and a qwen serve daemon no longer passes inherited values of them to session subprocesses. The project-file-only keys in this paragraph are rejected from project .env/settings.json env (currently without a per-key warning), remain settable from the shell environment or a user-level .env, and are inherited by session subprocesses when exported."

中文说明

本段重写后保留的结尾"升级说明"现在也覆盖了新增的"仅拒绝项目文件"层键,而它对这三类键的三条断言全部不成立:它们只从项目文件拒绝,并非"处处拒绝"(shell 与用户级 .env 中的值会被保留——本段前两句正是这么说的);它们的拒绝是静默的(硬编码层的每处拒绝都是裸 continue,且 reportRejectedLoaderKeys 经由 isLoaderEnvKey 过滤,这些键没有任何可达的告警路径);qwen serve daemon 仍会把这些键的继承值传给会话子进程(它们都不在 INHERITED_LOADER_ENV_KEYS 中)。失败场景:依赖此说明的运维要么为 TLS/git/curl/pip/less/node-gyp 键寻找一条永远不会出现的拒绝告警,要么以为 daemon 会从会话环境中剥离导出的 GIT_SSH_COMMAND/PYTHON——在一份安全加固文档中高估了安全边界。

修复建议:将升级说明限定在 loader 层,并准确描述仅项目文件层——例如:"上述 loader 键现在处处被拒绝且伴随告警,qwen serve daemon 不再把它们的继承值传给会话子进程。本段中仅拒绝项目文件的键从项目 .env/settings.json env 被拒绝(目前无逐键告警),仍可从 shell 环境或用户级 .env 设置,导出时会被会话子进程继承。"

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

Comment on lines +637 to +638
handle.release();
handle.release(); // idempotent

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 "release is idempotent" half of this test is vacuous: probe-verified at this commit, deleting the if (released) return; guard in release() (shared-env-keys.ts) leaves all 38 tests in this file green. The double release underflows the refcount (0 → −1) invisibly; the poison only becomes observable in the next cycle — a later acquire sees depth ≠ 0 and skips its snapshot-map clear, a nested acquire then hits depth === 0 mid-cycle and clears the in-flight snapshot, so the final restore silently loses the host's loader value. — Failure scenario: a future refactor drops the guard; every test stays green while a double-close poisons the refcount and the next nested scrub cycle loses a host assignment on final release — the shared-env corruption class this PR exists to prevent.

Suggested fix: after the double release, exercise a fresh balanced cycle with nested acquires and assert the final restore (the verification probe that flips under the mutant: double release → fresh cycle with a host assignment between nested acquires → the final-restore assertion fails with the guard removed and passes with it). The new cycle must be constructed so the assertion depends on depth being exactly 0 at its first acquire — under the mutant it is −1.

中文说明

本测试中"release 幂等"的一半是空洞的:已在本提交上探针证实,删除 release()shared-env-keys.ts)中的 if (released) return; 守卫后,本文件全部 38 个测试依旧全绿。双重 release 会隐式地把引用计数下溢(0 → −1);毒性要到下一个周期才可观测——之后的 acquire 看到 depth ≠ 0 会跳过对快照 Map 的清空,随后嵌套 acquire 在周期中途命中 depth === 0 并清空进行中的快照,最终 release 的恢复就会悄悄丢掉宿主的 loader 赋值。失败场景:未来某次重构删掉该守卫;所有测试保持绿灯,而一次双重 close 已污染引用计数,下一个嵌套剥离周期在最终 release 时丢失宿主赋值——正是本 PR 要防止的共享环境污染一类问题。

修复建议:在双重 release 之后,再跑一个配平的完整周期(嵌套 acquire,宿主在两次 acquire 之间赋值),并断言最终恢复(验证探针在删除守卫的变异体下翻转:双重 release → 新周期中宿主在嵌套 acquire 之间赋值 → 最终恢复断言在守卫被删时失败、保留时通过)。新周期的构造必须使断言依赖首个 acquire 时 depth 恰为 0——在变异体下它是 −1。

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

Comment on lines +67 to +68
'GIT_SSL_CAINFO',
'GIT_SSL_CAPATH',

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] R2-2: GIT_SSL_NO_VERIFY remains accepted and can disable git's certificate verification from a project file — still standing from the round-2 ledger, absent from the tree at this commit (probe-verified: isHardcodedProjectEnvExclusion('GIT_SSL_NO_VERIFY') returns false and loadEnvironment applies it from a project .env; adding the one line below flips the probe). — Failure scenario: an untrusted project .env sets GIT_SSL_NO_VERIFY=true; it passes every gate and is frozen into daemonRuntimeBaseEnv for every workspace's session subprocesses; any session git fetch/clone/push over HTTPS then skips certificate verification, enabling MITM of token-bearing git traffic by a network attacker with no attacker CA — the exact outcome this tier's comment says it blocks, and the same class as NODE_TLS_REJECT_UNAUTHORIZED, which the file already hardcodes as must-never-be-settable-from-project-files. The round-1 thread deferred this with the promise that it "rides in the follow-up round, where it is the first candidate" — this follow-up landed without it.

Suggested change
'GIT_SSL_CAINFO',
'GIT_SSL_CAPATH',
'GIT_SSL_CAINFO',
'GIT_SSL_CAPATH',
'GIT_SSL_NO_VERIFY',
中文说明

R2-2: GIT_SSL_NO_VERIFY 仍可被接受,并能经由项目文件关闭 git 的证书校验——第 2 轮台账中的未决项,在本提交的代码树中仍不存在(探针证实:isHardcodedProjectEnvExclusion('GIT_SSL_NO_VERIFY') 返回 false,loadEnvironment 会从项目 .env 应用该值;补上下面这一行即可翻转探针)。失败场景:不受信项目的 .env 设置 GIT_SSL_NO_VERIFY=true;它通过所有门控,被冻结进 daemonRuntimeBaseEnv 并分发到每个 workspace 的会话子进程;任何会话经 HTTPS 的 git fetch/clone/push 都会跳过证书校验,使网络攻击者无需任何攻击者 CA 即可对携带 token 的 git 流量做 MITM——正是本层注释声称要阻断的结果,也与 NODE_TLS_REJECT_UNAUTHORIZED 同类(后者已被文件硬编码为绝不允许由项目文件设置)。第 1 轮的讨论串将此项延期时承诺"它随下一轮落地,且是第一候选"——本轮后续已落地,却没有它。

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

'qwen serve',
'daemon',
);
const scrubbedLoaderEnvKeys = loaderEnvScrub.removedKeys;

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] R2-6: A second co-resident daemon's acquire removes no keys, so neither the stderr breadcrumb (scrubAndReportInheritedLoaderEnv reports only when keys were removed) nor the durable daemon-log entry below (gated on scrubbedLoaderEnvKeys.length > 0) fires — that daemon's log contains no evidence the inherited-loader-env scrub applies to its sessions. Still standing from the round-2 ledger: declined in round 5 as diagnostics-only, thread left open; the code is unchanged. — Failure scenario: daemon B boots into an already-scrubbed env and writes no scrub record to its own durable log; an operator later investigating why daemon B's sessions do not inherit NODE_OPTIONS finds nothing in B's log and concludes the scrub is inactive there — or a refcount leak leaves the env permanently scrubbed with no log line anywhere explaining why.

Suggested fix: log the nested case too — e.g. emit a durable-log entry when removedKeys is empty at acquire ("inherited loader env already scrubbed in this process; joining the shared refcounted scrub"), so every daemon's log records its scrub state.

中文说明

R2-6: 第二个同进程 daemon 的 acquire 不会移除任何键,因此 stderr 面包屑(scrubAndReportInheritedLoaderEnv 仅在移除了键时才输出)与下方的持久 daemon 日志条目(以 scrubbedLoaderEnvKeys.length > 0 为门)都不会触发——该 daemon 的日志中没有任何证据表明继承环境剥离作用于它的会话。第 2 轮台账未决项:第 5 轮以"仅影响诊断"为由拒绝,讨论串保持打开;代码未变。失败场景:daemon B 在已被剥离的环境中启动,其持久日志不写入任何剥离记录;运维之后排查"为何 daemon B 的会话没有继承 NODE_OPTIONS"时在 B 的日志中一无所获,便以为剥离在 B 上未生效——或者引用计数泄漏使环境被永久剥离,而没有任何日志能解释原因。

修复建议:把嵌套情形也记录下来——例如在 acquire 时若 removedKeys 为空则写一条持久日志("继承环境在本进程中已被剥离;加入共享的引用计数剥离"),使每个 daemon 的日志都记录自身的剥离状态。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round summary — PR #8763 (no action)

Feedback triage

Critical-only mode is active (after 5 change-producing rounds). Everything listed under Deferred non-Critical feedback — the five inline suggestion findings (settings.md denylist wording ×2, the "release is idempotent" test note, the GIT_SSL_NO_VERIFY note, the co-resident daemon acquire note) and one PR comment — was left untouched this round, as required: no code changes, no thread resolutions, no replies for them.

No failed checks and no still-red checks were reported for this round.

The single actionable item and its disposition

The [COMMENTED] review surfaces one unresolved item: a **[Critical]** pointer to issue-level comment 5228685910 (@qwen-code-dev-bot autofix status).

Disposition: no code action required (declined with evidence).

  1. That comment is the autofix agent's own operational status report about a failed address-review run on a stale base ("Qwen failed during address-review: status 125"). It names no code defect, and the automated reviewer itself recorded: "names no code defect to trace against this diff — cannot tell."
  2. The failure's precondition no longer holds: the branch already contains the current base — origin/main (55e20db) is an ancestor of HEAD (3b70728, "Merge branch 'main' into fix/8663-loader-denylist-followup") — and this round was invoked with no base conflict.
  3. Nothing in this round's feedback identifies a defect in the diff, and no check is failing.

Verification performed this round

The review recorded a coverage gap from its own run: only filtered runs of the new fast-path tests had executed, and the daemon-worker.test.ts change had been verified by code inspection only ("full unfiltered runs of fast-path.test.ts and daemon-worker.test.ts" were left unexplored when the tool budget was reached). That exact gap was closed locally this round.

The review also noted "Integration Tests (CLI, No Sandbox) was skipped in CI". That is a workflow skip, not a failing check, and no named defect is attached to it — no code action to take.

Verification

Commands actually run this round and their results:

  • git merge-base --is-ancestor origin/main HEAD — confirmed the branch already contains the current base (stale-base precondition gone).
  • npm run build — passed. This also fixed a checkout build-state gap found during testing: packages/channels/base had no dist/, so @qwen-code/channel-base (declared entry dist/index.js) failed to resolve and initially blocked two test files. Environmental, not caused by this PR.
  • Full unfiltered Vitest run of every test file touched by this PR (packages/cli): src/commands/channel/daemon-worker.test.ts, src/config/environment.test.ts, src/config/shared-env-keys.test.ts, src/serve/fast-path.test.ts, src/serve/process-env-guard.test.ts5/5 files passed, 232/232 tests passed.

Outcome

No code changes this round; no commit created. Working tree clean; HEAD unchanged at 3b70728.

中文说明

Autofix 本轮总结 — PR #8763(无操作)

反馈分类

当前处于仅处理 Critical 的模式(在 5 个产生改动的轮次之后)。Deferred non-Critical feedback(延后的非 Critical 反馈)中列出的全部内容——5 条行内建议类发现(settings.md 拒绝清单措辞 ×2、"release is idempotent" 测试说明、GIT_SSL_NO_VERIFY 说明、同宿主 daemon acquire 说明)以及 1 条 PR 评论——本轮均按要求保持原样:不改代码、不解决线程、不回复。

本轮没有报告失败的检查,也没有报告持续红色的检查。

唯一的可处理条目及其处置

[COMMENTED] 审查提出了 1 条未决条目:一个指向 issue 级评论 5228685910(@qwen-code-dev-bot autofix 状态)的 **[Critical]** 指针。

处置:无需代码操作(附证据予以驳回)。

  1. 该评论是 autofix 代理自身的一次运行状态报告,记录的是在陈旧 base 上一次失败的 address-review 运行("Qwen failed during address-review: status 125")。它没有指出任何代码缺陷,自动审查器自己也记录道:"names no code defect to trace against this diff — cannot tell"(没有指出任何可以对照本 diff 追踪的代码缺陷——无法判断)。
  2. 该失败的前置条件已不复存在:分支已包含当前 base —— origin/main55e20db)是 HEAD(3b70728,"Merge branch 'main' into fix/8663-loader-denylist-followup")的祖先——且本轮调用时不存在 base 冲突。
  3. 本轮反馈中没有任何内容指出 diff 存在缺陷,也没有任何检查失败。

本轮执行的验证

审查记录了其自身运行时留下的覆盖缺口:当时只对新增的 fast-path 测试做了过滤运行,且 daemon-worker.test.ts 的改动仅通过代码审阅验证(在达到工具调用预算时,"fast-path.test.ts 和 daemon-worker.test.ts 的完整无过滤运行"未能执行)。本轮已在本地补齐了这一确切缺口。

审查还提到 "Integration Tests (CLI, No Sandbox) was skipped in CI"(集成测试在 CI 中被跳过)。这是工作流层面的跳过,并非检查失败,且没有与之关联的已指名缺陷——无需采取代码操作。

验证

本轮实际执行的命令及结果:

  • git merge-base --is-ancestor origin/main HEAD —— 确认分支已包含当前 base(陈旧 base 的前置条件已消除)。
  • npm run build —— 通过。这同时修复了测试过程中发现的检出构建状态缺口:packages/channels/base 缺少 dist/,导致 @qwen-code/channel-base(声明入口为 dist/index.js)无法解析,最初阻塞了 2 个测试文件。这是环境问题,并非本 PR 所致。
  • 对本 PR 触及的每个测试文件执行完整无过滤的 Vitest 运行(packages/cli):src/commands/channel/daemon-worker.test.tssrc/config/environment.test.tssrc/config/shared-env-keys.test.tssrc/serve/fast-path.test.tssrc/serve/process-env-guard.test.ts —— 5/5 个文件通过,232/232 个测试通过

结果

本轮无代码改动;未创建提交。工作树干净;HEAD 保持在 3b70728 未变。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao

wenshao commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (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: 966 passed · 0 failed · 966 total

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

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

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

Verification report

PR 8763 Deep Verification (round 2 — follow-up at an unchanged head)

Verdict: findings — the PR's central claim was re-proven live by A/B (base applies every new key = poisoning reproduced; head rejects all of it, operator values preserved), 966/966 scripted assertions passed, mutation matrix 6/6 killed with 0 survivors, and the PR's own tests overlaid on base fail 20/66 exactly as predicted. The verdict is findings rather than merge-ready because the round-1 completeness findings (F1/F2/F3 below) were re-measured and stand at this head, plus one new nit (F4, description lags the shipped denylist). None are blockers to the shipped code.

Assertions: 966 pass / 0 fail / 966 total · Verified head: 3b7072826d3b0b05970b51de97a798a8fde0337b (merge ref 530a0ce264, base 55e20db328)

中文摘要

这是在同一 head 上的复验轮:本轮 HEAD^23b7072826d…)与上一轮已验证的 head 逐字节相同(merge ref、base、tree hash 全部一致)。上一轮的测量按"输入闭包完全相同"规则可承继,且本轮全部重新实测,结果逐项完全复现。

结论 findings(非阻断)

  • A/B 复测:全部新增键在 base 上从项目 .env 应用(污染成立)、在 PR 头上被拒绝;home .env / shell 导出的合法值两侧都保留;并发内嵌 daemon 的重新污染与 reporter 丢失只在 base 复现。详见 Central claim and A/B result 表(各 cell 计数 therein)。
  • Load-bearing:PR 自己的测试放回 base 的红绿分布、变异矩阵(6/6 杀死、0 存活)与未变异对照见 Load-bearing proof 一节。
  • 门控:6 个受影响套件全绿,计数见 Targeted gates 段。
  • 上轮发现状态:F1/F2/F3 全部仍然存在(复测确认),另新增一条 F4(nit:PR 正文列出的拒绝键是 shipped 列表的子集)。见 Previous-finding status 表与 Findings
  • 未覆盖:见 Not covered(逐 commit 归因、真实 daemon 端到端、既有行为钉住的 base A/B、全仓门禁、PR 明确 deferred 的类)。

Previous-finding status (round 1 → this round)

Identity first: this round re-verifies a bit-identical head. git rev-parse HEAD^2 = 3b7072826d3b0b05970b51de97a798a8fde0337b — byte-equal to round 1's verified head; merge ref 530a0ce264 and base 55e20db328 also equal; HEAD^{tree} == HEAD^2^{tree} (3e79feff…), git diff HEAD^2..HEAD empty, package.json/package-lock.json untouched. A commit OID commits to the full tree, so every input closure round 1 consumed (source, lockfile, config, fixtures) is identical by construction; every carried-forward number was additionally re-measured live this round and reproduced exactly (raw/identity.txt, witness 01-followup-identity-same-head.png).

# Round-1 finding Severity Status at this head (re-measured)
F1 Package-registry/index redirects (npm_config_registry, PIP_INDEX_URL/PIP_EXTRA_INDEX_URL/PIP_TRUSTED_HOST, UV_INDEX_URL/UV_DEFAULT_INDEX) still apply from a project .env — same supply-chain outcome class the PR blocks, without needing MITM medium stands — all 6 keys APPLIED again (harness-probes.mts siblings, raw/head-probes-siblings.log)
F2 Interpreter/REPL startup-file siblings (PYTHONSTARTUP, R_PROFILE_USER) of the blocked NODE_REPL_EXTERNAL_MODULE still apply low stands — both APPLIED again (same cell)
F3 Project-tier rejections are unobservable (pre-existing design; no breadcrumb anywhere) nit stands — mixed .env (NODE_OPTIONS+GIT_SSH_COMMAND+SSL_CERT_FILE) produced a reporter event naming only NODE_OPTIONS (raw/head-probes-f3-silence.log)

No declined or deferred rows existed in round 1, so there was nothing to re-adjudicate; nothing worsened — the code is byte-identical and every re-measurement matched round 1's value. Witness for the re-probed findings: 06-f1-f2-f3-findings-stand.png.

Central claim and A/B result (re-measured live this round)

Central claim: the 4 pure-injection loader keys are scrubbed from the inherited env and rejected from every .env/settings.env scope; the 33 new hardcoded literals + the numbered GIT_CONFIG_KEY_<n>/VALUE_<n> pattern pair are rejected from project files while operator shell / home-.env values are preserved; the scrub is reference-counted and the reporter clears only if current.

The key lists were derived by diffing the modules' own exports between arms (not transcribed): +4 loader tier (12 → 16), +33 project-tier literals (14 → 47), new APIs acquireInheritedLoaderEnvScrub/clearLoaderKeyRejectionReporterIfCurrent present on head, absent on base (raw/lists-{head,base}.json). Every cell drove the real loadEnvironment / reloadEnvironment / buildRuntimeEnvironment of $VERIFY_TREE with fixture files under a scratch $HOME; per-arm expectations were encoded in the harness, so predicted base reds count as passing assertions. 41 new keys per project-scope cell = 4 loader + 33 literals + 4 numbered-pair instances. Witness: 02-ab-driver-head-vs-base-live.png (the full driver table as it printed).

Cell Oracle head base
Project .env with all 41 new keys + controls process.env after load 41/41 rejected, benign applied, pre-existing NODE_OPTIONS/NODE_EXTRA_CA_CERTS still rejected 41/41 applied (the poisoning)
settings.env with all 41 keys process.env after load 41/41 rejected, benign applied applied
buildRuntimeEnvironment (daemon spawn seam), .env + settings.env snapshot.effectiveEnv 41/41 absent, benign + base passthrough intact applied
Home .env: 3 project-tier keys + OPENSSL_CONF + benign tier semantics project tier preserved (3/3), OPENSSL_CONF rejected project tier preserved, OPENSSL_CONF applied (not in base loader list)
Operator shell export + hostile project override no-override mode export wins export wins
Reload after rotating/removing home-.env numbered pairs + GIT_SSH_COMMAND freeze vs rotate frozen at boot values, never applies new pair, survives removal; benign rotates rotates mid-session, applies new pair, removal deletes
Reporter events for mixed .env loader-tier reported NODE_OPTIONS reported; hardcoded tier silent (by design, = F3) same shape
Membership drift guard (74 checks) head predicates match every derived key 74/74
Refcounted scrub, two overlapping daemons (harness-refcount.mts) env state at each step 11/11: no re-poison while B live; final restore uses newest snapshot; later legit assignment wins; double release idempotent 5/5 expected-failure cells: OPENSSL_CONF never scrubbed at all; A close() re-poisons shared env while B live
Reporter guard, A exits after B installs (harness-reporter.mts) which reporter receives fresh rejections 4/4: B's reporter survives A's exit, cleared on last exit, warn-once dedup holds after reinstall 2/2 expected-failure cells: unconditional clear drops B's reporter

Cell totals: 420 scripted assertions across 23 cell runs (15 head + 8 base), 0 fail (raw/driver-run.txt, per-cell logs in raw/).

Load-bearing proof

PR's own tests overlaid on base: 20 failed / 46 passed (66). Head's shared-env-keys.test.ts + environment.test.ts copied verbatim into the base worktree; every one of the 20 reds is a test this PR added — grepping the base test files for the red tests' markers (acquireInheritedLoaderEnvScrub, clearLoaderKeyRejectionReporterIfCurrent, "follow-up code-injection", "freezes the numbered GIT_CONFIG") returns 0 occurrences, while the head files contain them. Reds are the 5 behavior tests in environment.test.ts + 15 in shared-env-keys.test.ts (incl. TypeError: …is not a function for the three new APIs). Witness: 03-base-overlay-pr-tests-red.png; names in raw/base-overlay-reds.txt.

Mutation matrix (head): 6/6 killed, 0 survivors, unmutated control 66/66 green; each mutant died on exactly its intended pin (raw/mutation-matrix.txt, per-mutant vitest logs in raw/vitest-*.log). Witness: 04-mutation-matrix-all-killed.png.

Mutant Intended pin Result
M1 drop OPENSSL_CONF from loader tier loader-key application + exact-list pin killed (3 red)
M2 numbered-pair KEY regex → never-matching (\d+x) pair matching + reload freeze pins killed (3 red)
M3 remove if (depth > 0) return; guard in release() "no restore while second holder live" + between-acquires restore killed (2 red)
M4 invert clearLoaderKeyRejectionReporterIfCurrent condition clear-if-current test killed (1 red)
M5 drop SSL_CERT_FILE (positive control) TLS application + case-fold pins killed (3 red)
M6 revert reload gate to set-only (isHardcodedProjectEnvExclusion clause → false) numbered-pair reload freeze killed (1 red)

This also serves as the vitest-liveness proof (the same machinery went red on demand), subsuming a planted-violation check.

Targeted gates (head): shared-env-keys 38, environment 28, fast-path 85, process-env-guard 3, daemon-worker 78, run-qwen-serve 241 — 473/473 green, identical to round 1. Witness: 05-gates-head-473-green.png.

Findings

F1 — [medium, completeness, stands] Package-registry/index redirects still apply from a project .env — the same supply-chain outcome class this PR blocks, without needing a MITM position. npm_config_registry, PIP_INDEX_URL, PIP_EXTRA_INDEX_URL, PIP_TRUSTED_HOST, UV_INDEX_URL, UV_DEFAULT_INDEX are applied from an untrusted workspace .env and frozen into daemonRuntimeBaseEnv for every workspace's sessions — the exact #8653 outcome the PR exists to close. An attacker index is strictly stronger than the TLS-anchor MITM this PR blocks: malicious package content arrives directly, and postinstall executes it with no interception position (PIP_TRUSTED_HOST additionally downgrades pip to plaintext). These fit the PR's own reject-from-project tier cleanly. Repro: node tmp/pr8763-verify-20260810-032438/driver.mjsF1/sibling/*: APPLIED from project .env in raw/head-probes-siblings.log. Disposition unchanged from round 1: add to PROJECT_ENV_HARDCODED_EXCLUSIONS, or explicitly track as deferred like LD_LIBRARY_PATH/PATH — silence in the description reads as covered. Not a blocker: the PR is an explicitly curated increment and the shipped code is correct for everything it claims.

F2 — [low, completeness, stands] Interpreter/REPL startup-file siblings of NODE_REPL_EXTERNAL_MODULE still apply. PYTHONSTARTUP and R_PROFILE_USER apply from a project .env; same mechanism shape as the blocked loader-tier REPL key. Same repro cell.

F3 — [nit, pre-existing, stands] Project-tier rejections are unobservable. A .env mixing NODE_OPTIONS, GIT_SSH_COMMAND, SSL_CERT_FILE produces a reporter event naming only NODE_OPTIONS; the hardcoded-tier rejections leave no breadcrumb anywhere. Predates this PR; silence is defensible (don't echo attacker values) — a count-only breadcrumb would close the diagnostic gap the PR's own description targets.

F4 — [nit, new this round] The PR body enumerates a subset of the shipped denylist. The description names 13 project-tier keys (+ numbered pairs); the shipped list is 33 literals + 2 numbered-pair regexes (the round-2/3 commits added the git-exec siblings GIT_SSH/GIT_EXEC_PATH/GIT_TEMPLATE_DIR/GIT_ASKPASS/GIT_PROXY_COMMAND/GIT_EDITOR/GIT_CONFIG_PARAMETERS, the npm/pip TLS knobs incl. both strict-ssl spellings, PIP_CONFIG_FILE, the rc redirects CURL_HOME/WGETRC, SSH_ASKPASS, LESSOPEN/LESSCLOSE, npm_config_git). settings.md documents the full set, so user-facing docs are complete; only the PR text lags. Code impact: none.

No injection-style steering instructions were present in the PR text or the previous report; author claims were treated as hypotheses and re-tested (the "458 tests" figure re-measured as 473 at this head, same as round 1 — consistent, suites grew across the PR's later commits).

Not covered

  • Per-commit attribution: depth-2 shallow checkout — git rev-list HEAD^1..HEAD^2 reports 1 commit against 9 in the metadata snapshot (shallow-boundary artifact, re-verified this round). Only the aggregate HEAD^1..HEAD diff was verified.
  • Live daemon E2E: the spawn seam was driven at buildRuntimeEnvironment level plus the refcount/reporter unit seams; no real qwen serve + session-subprocess round trip (same as round 1).
  • daemon-worker breadcrumb pin and fast-path case-fold pin on base: both pin behavior this PR does not change (production files untouched); verified green on head within the 473, not A/B'd.
  • Repo-wide test suite, lint, prettier, typecheck: CI-covered pre-stages (npm ci + npm run build completed at HEAD before this round); only the six affected suites re-run.
  • The PR's explicitly deferred classes (LD_LIBRARY_PATH, PYTHONPATH, residual PATH prefix) — untouched by design, not probed.
  • The refcount contract "concurrent callers must pass the same shared env object" is documented but unenforced; only one production call site exists (process.env); contract-violation interleavings not probed.
  • process-env-guard.test.ts allowlist re-count (whole: 7 → 5): green via the suite; no mutation of the access count performed.
  • Round-1 harness scripts were not present in this fresh container (artifact dir is per-run); the harnesses were rewritten from the diff, driving the same seams with the same semantics. All round-1 numbers reproduced exactly (473 gates, 20/46 overlay, matrix 3/3/2/1/3/1, base re-poison + OPENSSL_CONF-never-scrubbed), which is itself corroborating evidence of head identity.

Methodology

Environment: the CI verify container (node:22-bookworm, Node v22.23.2), merge-ref checkout at depth 2 (HEAD=530a0ce264, base HEAD^1=55e20db328, head HEAD^2=3b7072826d), npm ci + npm run build pre-run. Four tsx harnesses (harness-denylist.mts, harness-refcount.mts, harness-reporter.mts, harness-probes.mts, shared harness-lib.mts) import the real TS modules of whichever tree VERIFY_TREE points at; driver.mjs spawns each of the 23 cells in a fresh process under a scratch HOME with all probe keys sanitized out of the inherited env, and counts TAP-style ok/not ok lines (raw/driver-run.txt). Base side ran from git worktree tmp/base-tree HEAD^1 wired to the root node_modules plus per-package nested node_modules symlinks; workspace-link confound check: readlink -f node_modules/@qwen-code/qwen-code-core resolves into the head tree, but git diff HEAD^1..HEAD -- packages/core packages/a2a packages/test-utils packages/web-shell is empty and the lockfile is untouched, so the control is byte-clean. The base refcount arm replicates the base per-daemon hunk from git show HEAD^1:packages/cli/src/serve/run-qwen-serve.ts verbatim (launchEnv snapshot + scrubAndReportInheritedLoaderEnv + restore loop). Mutations used mutate.mjs (exact-once string replacement, exit 2 otherwise) with git checkout -- restore after each mutant; tree verified clean after the matrix, worktree removed. Evidence captures were produced with scripts/verify-capture.mjs (images 02/03/04 are live re-runs of the driver, overlay, and matrix under capture; 05/06 re-present the raw run logs). Assertion accounting: gates 473 + A/B driver cells 420 + base overlay 66 (46 green + 20 expected-reds, each red name verified as PR-added) + mutation matrix 7 (1 control-green + 6 expected-red-observed) = 966; fail counts only unexpected outcomes, of which there were none.

Evidence images

01-followup-identity-same-head

02-ab-driver-head-vs-base-live

03-base-overlay-pr-tests-red

04-mutation-matrix-all-killed

05-gates-head-473-green

06-f1-f2-f3-findings-stand

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@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.

LGTM, looks ready to ship. ✅

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

No action required on PR #8763

This review round has no actionable items:

  • Reviews: none newer than the last evaluation (2026-08-10T00:01:58Z).
  • Inline comments: none.
  • Issue-level comments: none.
  • Failed checks: none.
  • Still-red checks: none.
  • Base conflict: none (--conflict false); no merge was performed.

Critical-only mode is active after 5 change-producing rounds. The only item in
the "Deferred non-Critical feedback" section is an automated PR comment by
@qwen-code-ci-bot. Per the round rules, that section is an audit record, not
work: no code was modified, no thread was resolved, and no reply was written
for it. The item remains open for human follow-up.

No code was changed and no commit was made in this round. The branch stays at
3b7072826d.

中文说明

PR #8763 无需处理

本轮评审没有任何可处理的项目:

  • 评审(Reviews): 自上次评估(2026-08-10T00:01:58Z)以来没有新的评审。
  • 行内评论(Inline comments): 无。
  • Issue 级评论(Issue-level comments): 无。
  • 失败的检查(Failed checks): 无。
  • 持续失败的检查(Still-red checks): 无。
  • 与 base 分支的冲突: 无(--conflict false),未执行任何合并。

在 5 个产生改动的轮次之后,已进入仅处理 Critical 的模式。"Deferred non-Critical feedback"(延后的非 Critical 反馈)部分中唯一的条目是 @qwen-code-ci-bot 的一条自动化 PR 评论。按照本轮规则,该部分属于审计记录而非待办工作:未修改任何代码、未解决任何讨论串,也未就该条目撰写任何回复。该条目保持开放,留待人工跟进。

本轮未改动任何代码,也未创建任何提交。分支保持在 3b7072826d

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@wenshao
wenshao enabled auto-merge August 10, 2026 03:44

@doudouOUC doudouOUC 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.

Unresolved, please confirm: [Critical] issue-level comment 5228685910: autofix status — reports the autofix agent's own failed run on a stale base; names no code defect to trace against this diff — cannot tell. Re-checked against the code at 3b70728: the comment is an autofix infrastructure status report, not a code defect.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

[Critical] issue-level comment 5228685910 (@qwen-code-dev-bot autofix status) — reports the autofix agent's own failed run on a stale base; names no code defect to trace against this diff — cannot tell

中文说明

未决,请确认:[Critical] issue-level comment 5228685910: autofix status — reports the autofix agent's own failed run on a stale base; names no code defect to trace against this diff — cannot tell. Re-checked against the code at 3b70728: the comment is an autofix infrastructure status report, not a code defect.

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

[Critical] issue-level comment 5228685910 (@qwen-code-dev-bot autofix status) — reports the autofix agent's own failed run on a stale base; names no code defect to trace against this diff — cannot tell

— deepseek-v4-flash via Qwen Code /review (v0.21.8)

Comment on lines +371 to +377
for (const key of Object.keys(env)) {
if (!isLoaderEnvKey(key)) continue;
const value = env[key];
if (value !== undefined) sharedProcessEnvScrubOriginals.set(key, value);
}
sharedProcessEnvScrubDepth++;
const removedKeys = scrubAndReportInheritedLoaderEnv(

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] acquireInheritedLoaderEnvScrub double-iterates process.env — the snapshot loop (lines 371-375) and the subsequent scrubAndReportInheritedLoaderEnv call both iterate Object.keys(env) and both call isLoaderEnvKey on every key. — Failure scenario: On every daemon startup (including overlapping embedded daemon acquires), process.env is iterated twice instead of once. For a typical ~150-entry env, this is ~300 isLoaderEnvKey calls per acquire, each doing a canonicalLoaderKey transform (2 regex substitutions + .replace()). Suggested fix: Merge the snapshot and the scrub into a single Object.keys(env) pass that both records the value and deletes the key.

中文说明

acquireInheritedLoaderEnvScrubprocess.env 做了两次迭代——快照循环(第 371-375 行)和后续的 scrubAndReportInheritedLoaderEnv 调用都遍历了 Object.keys(env) 并对每个键调用了 isLoaderEnvKey。失败场景:每次 daemon 启动时(包括重迭内嵌 daemon 的 acquire),process.env 被迭代两次而非一次。建议:将快照和剥离合并为一次 Object.keys(env) 遍历,同时记录值并删除键。

— deepseek-v4-flash via Qwen Code /review (v0.21.8)

Comment on lines +362 to +364
if (sharedProcessEnvScrubDepth === 0) {
sharedProcessEnvScrubOriginals.clear();
}

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] Mutant-survived: deleting the sharedProcessEnvScrubOriginals.clear() at acquire time (line 364) leaves every affected test green. — Failure scenario: A future change that removes or skips this clear() call would not be caught by any test in this diff, allowing stale originals from a prior scrub cycle to leak into the next scrub's snapshot. Suggested fix: Add a test that verifies sharedProcessEnvScrubOriginals is empty when the scrub depth drops to zero after a full acquire/release cycle.

中文说明

变体存活:删除 acquire 时的 sharedProcessEnvScrubOriginals.clear()(第 364 行)所有测试仍然通过。失败场景:未来删除了该 clear() 调用不会被任何测试捕获。建议:增加测试验证 sharedProcessEnvScrubOriginals 在完整 acquire/release 周期后为空。

— deepseek-v4-flash via Qwen Code /review (v0.21.8)

Comment on lines +389 to +390
if (sharedProcessEnvScrubDepth > 0) return;
for (const [key, value] of sharedProcessEnvScrubOriginals) {

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] Mutant-survived: deleting the sharedProcessEnvScrubOriginals.clear() at release time (line 397) leaves every affected test green. — Failure scenario: Same as the acquire-time clear — a future removal would not be caught by any test. Suggested fix: Confirm or add coverage for this reset path.

中文说明

变体存活:删除 release 时的 sharedProcessEnvScrubOriginals.clear()(第 397 行)所有测试仍然通过。建议:确认或补充此重置路径的测试覆盖。

— deepseek-v4-flash via Qwen Code /review (v0.21.8)

Comment on lines +400 to +403
export function resetInheritedLoaderEnvScrubForTesting(): void {
sharedProcessEnvScrubDepth = 0;
sharedProcessEnvScrubOriginals.clear();
}

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] Mutant-survived: deleting the sharedProcessEnvScrubOriginals.clear() in the test-only reset helper (line 403) leaves every affected test green. — Failure scenario: A future removal of this clear() call would compromise test isolation between dependent test suites. Suggested fix: Confirm or add coverage for this reset path.

中文说明

变体存活:删除仅测试重置辅助函数中的 sharedProcessEnvScrubOriginals.clear()(第 403 行)所有测试仍然通过。建议:确认或补充此重置路径的测试覆盖。

— deepseek-v4-flash via Qwen Code /review (v0.21.8)

Comment on lines +373 to +375
const value = env[key];
if (value !== undefined) sharedProcessEnvScrubOriginals.set(key, value);
}

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] Mutant-survived: the if (value !== undefined) guard (line 374) is never exercised — no test pins when the guard must NOT fire. — Failure scenario: A change that removes the value !== undefined guard would not be caught by any test, allowing undefined values to be stored in sharedProcessEnvScrubOriginals. Suggested fix: Add a test case that exercises the value === undefined branch and verifies the key is not stored.

中文说明

变体存活:if (value !== undefined) 守卫(第 374 行)从未被触发——没有测试验证守卫在什么情况下必须不触发。建议:增加测试用例,验证当 value === undefined 时该键不会被存储。

— deepseek-v4-flash via Qwen Code /review (v0.21.8)

acquireInheritedLoaderEnvScrub iterated process.env twice (a snapshot
pass, then the scrub); record the originals inside the scrub's single
pass instead. Drop the acquire-time snapshot clear, which the
release-time clear made unreachable defense, and add tests that kill
the previously surviving mutants on the release-time clear, the
test-only reset, and the undefined-value guard.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 9/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 9/100 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary — PR #8763

Feedback points and dispositions

Review-level Critical: issue-level comment 5228685910 (autofix status) — Declined (no code defect)

Confirmed, as the reviewer's own re-check concluded: comment 5228685910 is the
autofix bot's infrastructure status report about its own failed run on a stale
base. It names no code defect traceable against this diff, and re-reading the
full diff at HEAD surfaces nothing it could refer to. No code action. The
review's side note that the "Integration Tests (CLI, No Sandbox)" suite was
skipped in CI is a statement of the reviewer's coverage, not a request: this
round's change is unit-level and fully exercised by the focused Vitest runs
below.

[rc:3746607294] Double iteration of process.env in acquire — Resolved

Merged the snapshot and the scrub into one pass: scrubInheritedLoaderEnv now
accepts an optional snapshotInto map (record the value, then delete the key,
in the same loop), scrubAndReportInheritedLoaderEnv passes it through, and
acquireInheritedLoaderEnvScrub hands it the shared-originals map and drops
its own iteration. One Object.keys(env) pass and one isLoaderEnvKey call
per key per acquire, and the stderr breadcrumb wording stays in its single
shared home. The one-shot call sites (daemon base env, ACP child, channel
worker) keep the old one-argument form.

[rc:3746607299] Mutant survived: acquire-time clear() — Resolved (by removal, not by a new test)

The acquire-time clear and the release-time clear are mutually redundant — at
depth 0 the snapshot map is always empty (the last release clears it; the
test-only reset clears it), so the acquire-time clear defended a state the
refcount cannot reach, and its presence is exactly what masked the other two
clears' mutants. Per Simplicity First the redundant clear was removed
rather than pinned by a test; with it gone, the surviving clear's behavior
becomes observable and is pinned by the new cycle-isolation test below.

[rc:3746607303] Mutant survived: release-time clear() — Resolved

New test does not restore a prior cycle snapshot for a key the host removed:
a full acquire/release cycle, then the host removes the key, and a second
cycle's final release must not resurrect it from the prior snapshot. Mutant
verified killed: deleting the release-time clear() makes this test fail.

[rc:3746607305] Mutant survived: test-only reset clear() — Resolved

New test reset drops a leaked snapshot before the next acquire: an acquire
whose handle is never released (leaked), a reset, then a fresh cycle must not
re-inject the leaked value on its final release. Mutant verified killed:
deleting the reset's clear() makes this test fail.

[rc:3746607308] Undefined-value guard never exercised — Resolved

New test does not snapshot loader keys whose value is undefined: an env with
NODE_OPTIONS: undefined is scrubbed (key removed) but the final release must
not write undefined back into the env. Mutant verified killed: dropping the
value !== undefined guard makes this test fail.

Changes

  • packages/cli/src/config/shared-env-keys.ts: snapshot recorded inside the
    scrub's single pass (snapshotInto parameter); acquire no longer iterates
    separately; redundant acquire-time snapshot clear removed; stale doc
    sentence updated.
  • packages/cli/src/config/shared-env-keys.test.ts: three tests added, each
    proven to kill its mutant (mutant applied → test fails → restored → 41
    pass).

Commit: 146c448caf fix(cli): merge the loader-env scrub snapshot into one pass (#8763)

Conflict notes

--conflict false: no merge of origin/main performed; branch stays on its
existing merge base.

Verification

  • cd packages/cli && npx vitest run src/config/shared-env-keys.test.ts (baseline) — 38 passed
  • Mutant runs: release-clear mutant — 1 failed | 40 passed (target test fails); reset-clear mutant — 1 failed | 40 passed (target test fails); undefined-guard mutant — 1 failed | 40 passed (target test fails); restored source — 41 passed
  • cd packages/cli && npx vitest run src/config/shared-env-keys.test.ts src/config/environment.test.ts src/serve/fast-path.test.ts src/serve/process-env-guard.test.ts src/commands/channel/daemon-worker.test.ts — 5 files, 235 passed
  • npx prettier --check on both edited files — passed
  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • Post-commit re-run src/config/shared-env-keys.test.ts — 41 passed
  • Integration tests (after npm run bundle) — not run: the touched behavior (refcounted shared-env scrub internals) is exercised directly by the unit tests above, not only through the bundled CLI or integration harness
  • npm run generate:settings-schema — not needed: no settings source changed
中文说明

Autofix 审查轮次总结 — PR #8763

反馈点与处理结论

Review 级 Critical:issue-level comment 5228685910(autofix 状态)— 拒绝(无代码缺陷)

确认无误,与审查者自己的复核结论一致:评论 5228685910 是 autofix 机器人关于其在陈旧 base 上运行失败的基础设施状态报告。它没有指出任何可追溯到本 diff 的代码缺陷,重新通读 HEAD 的完整 diff 也未发现其可能指向的问题。无需代码改动。Review 中附带提到 "Integration Tests (CLI, No Sandbox)" 在 CI 被跳过,这只是对审查者自身覆盖面的说明,不是诉求:本轮改动属于单元级别,已由下方的定向 Vitest 运行完整覆盖。

[rc:3746607294] acquire 中对 process.env 的双重迭代 — 已解决

将快照与剥离合并为一次遍历:scrubInheritedLoaderEnv 现在接受一个可选的 snapshotInto map(在同一个循环中先记录值再删除键),scrubAndReportInheritedLoaderEnv 将其透传,acquireInheritedLoaderEnvScrub 把共享原始值 map 交给它并移除了自己的迭代。每次 acquire 只对 Object.keys(env) 遍历一次、每个键只调用一次 isLoaderEnvKey,且 stderr 提示文案仍保留在唯一的共享位置。一次性调用的站点(daemon base env、ACP 子进程、channel worker)保持原有的单参数形式。

[rc:3746607299] 变体存活:acquire 时的 clear() — 已解决(通过删除而非新增测试)

acquire 时的 clear 与 release 时的 clear 互相冗余——深度为 0 时快照 map 必然为空(最后一次 release 会清空它;仅测试用的 reset 也会清空它),所以 acquire 时的 clear 防御的是引用计数根本无法到达的状态,而它的存在恰恰掩盖了另外两处 clear 的变体。按照 Simplicity First 原则,冗余的 clear 被删除而不是用测试钉住;删除之后,保留的 clear 的行为变为可观测,并由下方新增的周期间隔离测试钉住。

[rc:3746607303] 变体存活:release 时的 clear() — 已解决

新增测试 does not restore a prior cycle snapshot for a key the host removed:完整执行一次 acquire/release 周期,然后宿主删除该键,第二个周期的最后一次 release 不得从上一周期的快照中复活该键。变体已验证被杀死:删除 release 时的 clear() 会使该测试失败。

[rc:3746607305] 变体存活:仅测试用 reset 中的 clear() — 已解决

新增测试 reset drops a leaked snapshot before the next acquire:一次 handle 从未 release 的 acquire(泄漏)、一次 reset,之后的新周期在其最后一次 release 时不得重新注入泄漏的值。变体已验证被杀死:删除 reset 中的 clear() 会使该测试失败。

[rc:3746607308] undefined 值守卫从未被触发 — 已解决

新增测试 does not snapshot loader keys whose value is undefined:一个含 NODE_OPTIONS: undefined 的 env 会被剥除(键被删除),但最后一次 release 不得把 undefined 写回 env。变体已验证被杀死:去掉 value !== undefined 守卫会使该测试失败。

改动内容

  • packages/cli/src/config/shared-env-keys.ts:快照改在剥境的单次遍历中记录(snapshotInto 参数);acquire 不再单独迭代;冗余的 acquire 时快照 clear 被删除;过时的文档句已更新。
  • packages/cli/src/config/shared-env-keys.test.ts:新增三个测试,每个都已证明能杀死对应变体(应用变体 → 测试失败 → 还原 → 41 通过)。

提交:146c448caf fix(cli): merge the loader-env scrub snapshot into one pass (#8763)

冲突说明

--conflict false:未合并 origin/main;分支保持在现有 merge base 上。

验证

  • cd packages/cli && npx vitest run src/config/shared-env-keys.test.ts(基线)— 38 通过
  • 变体运行:release-clear 变体 — 1 失败 | 40 通过(目标测试失败);reset-clear 变体 — 1 失败 | 40 通过(目标测试失败);undefined-guard 变体 — 1 失败 | 40 通过(目标测试失败);还原源码后 — 41 通过
  • cd packages/cli && npx vitest run src/config/shared-env-keys.test.ts src/config/environment.test.ts src/serve/fast-path.test.ts src/serve/process-env-guard.test.ts src/commands/channel/daemon-worker.test.ts — 5 个文件,235 通过
  • 对两个改动文件执行 npx prettier --check — 通过
  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • 提交后重新运行 src/config/shared-env-keys.test.ts — 41 通过
  • 集成测试(npm run bundle 之后)— 未运行:本次触及的行为(引用计数的共享 env 剥除内部逻辑)由上述单元测试直接覆盖,并非只能通过捆绑 CLI 或集成测试框架验证
  • npm run generate:settings-schema — 不需要:未改动任何 settings 源

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@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.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI (merge-queue-gated by design) and its suite did not run locally.

Not explored to full depth (tool budget reached): PR #8763 is a security-hardening follow-up to #8663: it e...: run the new/changed unit tests ( shared-env-keys.test.ts , environment.test.ts , fast-path.test.ts , process-env-guard.test.ts , daemon-worker.test.ts ) — t…; PR #8763 is a security-hardening follow-up to #8663: it e...: unit tests not executed (worktree has no node_modules) — logic traced statically; process-env-guard count (whole:5, removal of computed:key) not recomputed beyo…; PR #8763 is a security-hardening follow-up to #8663: it e...: did not execute the diff's unit tests — the review worktree has no node_modules ( vitest unresolvable) and a monorepo install was not viable in budget; refco….

[Critical] R4-1: GIT_SEQUENCE_EDITOR is absent from PROJECT_ENV_HARDCODED_EXCLUSIONS even though git executes it as a command exactly like its blocked sibling GIT_EDITOR (todo-list edit on git rebase -i). Failure scenario: an untrusted repo ships a project .env with GIT_SEQUENCE_EDITOR=<repo>/evil.sh and tracks the script in-repo; it passes every gate and is applied by loadEnvironment/buildRuntimeEnvironment, and when a daemon-hosted session runs git rebase -i, git execs the attacker script as the daemon user. Probe-verified end-to-end at this commit (git 2.47.3): the headless rebase executed the named file and exited 0; an application probe applied the key from a project .env while the blocked sibling GIT_EDITOR one line away was rejected, and adding the key flipped the probe. Fix: add 'GIT_SEQUENCE_EDITOR' beside 'GIT_EDITOR'. (Relocated to the body: its resolved line 113 already carries an existing, unrelated comment.) · 中文说明:GIT_SEQUENCE_EDITOR 未列入 PROJECT_ENV_HARDCODED_EXCLUSIONS,而 git 对它的执行方式与被阻断的 GIT_EDITOR 完全相同(git rebase -i 时编辑 todo 列表)。失败场景:不受信仓库的 .env 设置 GIT_SEQUENCE_EDITOR=<repo>/evil.sh 并在仓库内跟踪该脚本;它通过所有门控并被 loadEnvironment/buildRuntimeEnvironment 应用,daemon 托管的会话运行 git rebase -i 时以 daemon 用户身份执行攻击者脚本。已在本提交端到端探针验证。修复:在 'GIT_EDITOR' 旁加入 'GIT_SEQUENCE_EDITOR'。(因解析出的行 113 上已存在一条无关评论,改置于正文。)

[Critical] R4-5: VISUAL and EDITOR are missing from PROJECT_ENV_HARDCODED_EXCLUSIONS, leaving git's documented editor fallback chain (GIT_EDITORcore.editor$VISUAL$EDITOR) plantable from a project .env now that GIT_EDITOR is blocked. Failure scenario: attacker repo ships .env with VISUAL=/repo/evil.sh; the load is no-override, so it applies whenever the launch env does not export the variable. Probe-verified: git var GIT_EDITOR resolves to the VISUAL/EDITOR value when GIT_EDITOR is unset, so any session git commit without -m or git rebase -i execs the attacker script as the daemon user — the outcome GIT_EDITOR was blocked for. Independently, packages/cli/src/ui/hooks/useLaunchEditor.ts returns process.env['VISUAL'] || process.env['EDITOR'] || 'vi' and hands it to spawnSync from ordinary interactive flows (external-editor modify, memory/skill dialogs) — no git required. Both application paths probe-confirmed; adding the two keys flipped the probe. Fix: add 'VISUAL' and 'EDITOR' to the reject-only tier. (Relocated to the body: its resolved line 111 already carries an existing, unrelated comment.) · 中文说明:VISUALEDITOR 未列入 PROJECT_ENV_HARDCODED_EXCLUSIONS;在 GIT_EDITOR 已被阻断后,git 文档化的编辑器回退链(GIT_EDITORcore.editor$VISUAL$EDITOR)仍可由项目 .env 植入。失败场景:攻击者仓库的 .env 设置 VISUAL=/repo/evil.sh;加载采用未设置才写入语义,只要启动环境未导出该变量即被应用。探针验证:GIT_EDITOR 未设置时 git var GIT_EDITOR 解析到 VISUAL/EDITOR 的值,会话中任何不带 -mgit commitgit rebase -i 都会以 daemon 用户身份执行攻击者脚本——正是 GIT_EDITOR 被阻断的后果。另有独立路径:packages/cli/src/ui/hooks/useLaunchEditor.ts 返回 process.env['VISUAL'] || process.env['EDITOR'] || 'vi' 并交给 spawnSync,普通交互流程(外部编辑器修改、memory/skill 对话框)即可触达,无需 git。两处应用路径均经探针确认,加入两个键后探针翻转。修复:将 'VISUAL''EDITOR' 加入仅拒绝项目文件层。(因解析出的行 111 上已存在一条无关评论,改置于正文。)

[Critical] R4-7: PYTHONSTARTUP is absent from both tiers — the exact Python analogue of NODE_REPL_EXTERNAL_MODULE, which this PR adds to the strongest (scrubbed loader) tier for precisely this mechanism. Failure scenario: attacker repo ships .env with PYTHONSTARTUP=/repo/evil.py; the variable is almost never set in launch envs, so the value applies from the project .env (probe-confirmed on load and runtime-env paths). CPython executes $PYTHONSTARTUP at interactive startup — probe-verified: a true-TTY bare python3 and python3 -i both ran the file before the first prompt, while python3 -c did not. The first interactive python a session runs executes attacker code as the daemon user. The deferred-class comment covers search paths (PYTHONPATH/JAVA_TOOL_OPTIONS), not startup-exec hooks. Fix: add 'PYTHONSTARTUP' to PROJECT_ENV_HARDCODED_EXCLUSIONS (reject-only tier, like PYTHON). (Relocated to the body: its resolved line 141 already carries an existing, unrelated comment.) · 中文说明:PYTHONSTARTUP 在两层名单中均不存在——它是 NODE_REPL_EXTERNAL_MODULE 的 Python 同型变体,而本 PR 正是为该机制把后者加入了最强的剥离 loader 层。失败场景:攻击者仓库的 .env 设置 PYTHONSTARTUP=/repo/evil.py;启动环境几乎从不设置该变量,因此项目 .env 的值会被应用(加载路径与运行时环境构建路径均经探针确认)。CPython 在交互式启动时执行 $PYTHONSTARTUP——探针验证:真 TTY 裸 python3python3 -i 都会在首个提示符前执行该文件,python3 -c 则不会。会话第一次运行交互式 python 即以 daemon 用户身份执行攻击者代码。代码中延期类别的注释只覆盖搜索路径(PYTHONPATH/JAVA_TOOL_OPTIONS),不覆盖启动执行钩子。修复:将 'PYTHONSTARTUP' 加入 PROJECT_ENV_HARDCODED_EXCLUSIONS(仅拒绝项目文件层,与 PYTHON 相同)。(因解析出的行 141 上已存在一条无关评论,改置于正文。)

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI (merge-queue-gated by design) and its suite did not run locally。

未探索到全部深度(达到工具调用预算):PR #8763 is a security-hardening follow-up to #8663: it e...:run the new/changed unit tests ( shared-env-keys.test.ts , environment.test.ts , fast-path.test.ts , process-env-guard.test.ts , daemon-worker.test.ts ) — t…;PR #8763 is a security-hardening follow-up to #8663: it e...:unit tests not executed (worktree has no node_modules) — logic traced statically; process-env-guard count (whole:5, removal of computed:key) not recomputed beyo…;PR #8763 is a security-hardening follow-up to #8663: it e...:did not execute the diff's unit tests — the review worktree has no node_modules ( vitest unresolvable) and a monorepo install was not viable in budget; refco…

[Critical] R4-1: GIT_SEQUENCE_EDITOR is absent from PROJECT_ENV_HARDCODED_EXCLUSIONS even though git executes it as a command exactly like its blocked sibling GIT_EDITOR (todo-list edit on git rebase -i). Failure scenario: an untrusted repo ships a project .env with GIT_SEQUENCE_EDITOR=<repo>/evil.sh and tracks the script in-repo; it passes every gate and is applied by loadEnvironment/buildRuntimeEnvironment, and when a daemon-hosted session runs git rebase -i, git execs the attacker script as the daemon user. Probe-verified end-to-end at this commit (git 2.47.3): the headless rebase executed the named file and exited 0; an application probe applied the key from a project .env while the blocked sibling GIT_EDITOR one line away was rejected, and adding the key flipped the probe. Fix: add 'GIT_SEQUENCE_EDITOR' beside 'GIT_EDITOR'. (Relocated to the body: its resolved line 113 already carries an existing, unrelated comment.) · 中文说明:GIT_SEQUENCE_EDITOR 未列入 PROJECT_ENV_HARDCODED_EXCLUSIONS,而 git 对它的执行方式与被阻断的 GIT_EDITOR 完全相同(git rebase -i 时编辑 todo 列表)。失败场景:不受信仓库的 .env 设置 GIT_SEQUENCE_EDITOR=<repo>/evil.sh 并在仓库内跟踪该脚本;它通过所有门控并被 loadEnvironment/buildRuntimeEnvironment 应用,daemon 托管的会话运行 git rebase -i 时以 daemon 用户身份执行攻击者脚本。已在本提交端到端探针验证。修复:在 'GIT_EDITOR' 旁加入 'GIT_SEQUENCE_EDITOR'。(因解析出的行 113 上已存在一条无关评论,改置于正文。)

[Critical] R4-5: VISUAL and EDITOR are missing from PROJECT_ENV_HARDCODED_EXCLUSIONS, leaving git's documented editor fallback chain (GIT_EDITORcore.editor$VISUAL$EDITOR) plantable from a project .env now that GIT_EDITOR is blocked. Failure scenario: attacker repo ships .env with VISUAL=/repo/evil.sh; the load is no-override, so it applies whenever the launch env does not export the variable. Probe-verified: git var GIT_EDITOR resolves to the VISUAL/EDITOR value when GIT_EDITOR is unset, so any session git commit without -m or git rebase -i execs the attacker script as the daemon user — the outcome GIT_EDITOR was blocked for. Independently, packages/cli/src/ui/hooks/useLaunchEditor.ts returns process.env['VISUAL'] || process.env['EDITOR'] || 'vi' and hands it to spawnSync from ordinary interactive flows (external-editor modify, memory/skill dialogs) — no git required. Both application paths probe-confirmed; adding the two keys flipped the probe. Fix: add 'VISUAL' and 'EDITOR' to the reject-only tier. (Relocated to the body: its resolved line 111 already carries an existing, unrelated comment.) · 中文说明:VISUALEDITOR 未列入 PROJECT_ENV_HARDCODED_EXCLUSIONS;在 GIT_EDITOR 已被阻断后,git 文档化的编辑器回退链(GIT_EDITORcore.editor$VISUAL$EDITOR)仍可由项目 .env 植入。失败场景:攻击者仓库的 .env 设置 VISUAL=/repo/evil.sh;加载采用未设置才写入语义,只要启动环境未导出该变量即被应用。探针验证:GIT_EDITOR 未设置时 git var GIT_EDITOR 解析到 VISUAL/EDITOR 的值,会话中任何不带 -mgit commitgit rebase -i 都会以 daemon 用户身份执行攻击者脚本——正是 GIT_EDITOR 被阻断的后果。另有独立路径:packages/cli/src/ui/hooks/useLaunchEditor.ts 返回 process.env['VISUAL'] || process.env['EDITOR'] || 'vi' 并交给 spawnSync,普通交互流程(外部编辑器修改、memory/skill 对话框)即可触达,无需 git。两处应用路径均经探针确认,加入两个键后探针翻转。修复:将 'VISUAL''EDITOR' 加入仅拒绝项目文件层。(因解析出的行 111 上已存在一条无关评论,改置于正文。)

[Critical] R4-7: PYTHONSTARTUP is absent from both tiers — the exact Python analogue of NODE_REPL_EXTERNAL_MODULE, which this PR adds to the strongest (scrubbed loader) tier for precisely this mechanism. Failure scenario: attacker repo ships .env with PYTHONSTARTUP=/repo/evil.py; the variable is almost never set in launch envs, so the value applies from the project .env (probe-confirmed on load and runtime-env paths). CPython executes $PYTHONSTARTUP at interactive startup — probe-verified: a true-TTY bare python3 and python3 -i both ran the file before the first prompt, while python3 -c did not. The first interactive python a session runs executes attacker code as the daemon user. The deferred-class comment covers search paths (PYTHONPATH/JAVA_TOOL_OPTIONS), not startup-exec hooks. Fix: add 'PYTHONSTARTUP' to PROJECT_ENV_HARDCODED_EXCLUSIONS (reject-only tier, like PYTHON). (Relocated to the body: its resolved line 141 already carries an existing, unrelated comment.) · 中文说明:PYTHONSTARTUP 在两层名单中均不存在——它是 NODE_REPL_EXTERNAL_MODULE 的 Python 同型变体,而本 PR 正是为该机制把后者加入了最强的剥离 loader 层。失败场景:攻击者仓库的 .env 设置 PYTHONSTARTUP=/repo/evil.py;启动环境几乎从不设置该变量,因此项目 .env 的值会被应用(加载路径与运行时环境构建路径均经探针确认)。CPython 在交互式启动时执行 $PYTHONSTARTUP——探针验证:真 TTY 裸 python3python3 -i 都会在首个提示符前执行该文件,python3 -c 则不会。会话第一次运行交互式 python 即以 daemon 用户身份执行攻击者代码。代码中延期类别的注释只覆盖搜索路径(PYTHONPATH/JAVA_TOOL_OPTIONS),不覆盖启动执行钩子。修复:将 'PYTHONSTARTUP' 加入 PROJECT_ENV_HARDCODED_EXCLUSIONS(仅拒绝项目文件层,与 PYTHON 相同)。(因解析出的行 141 上已存在一条无关评论,改置于正文。)

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

Comment on lines +114 to +115
'GIT_CONFIG_GLOBAL',
'GIT_CONFIG_SYSTEM',

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-6: XDG_CONFIG_HOME is not excluded, giving a project .env a config-discovery redirect into git that bypasses every GIT_CONFIG_* block this PR adds — git merges $XDG_CONFIG_HOME/git/config with ~/.gitconfig (verified at this commit: git config --list --show-origin reads both). — Failure scenario: attacker repo ships .env with XDG_CONFIG_HOME=/repo/.xdg plus a committed .xdg/git/config containing [core] hooksPath = /repo/.hooks → with GIT_CONFIG_GLOBAL/SYSTEM/COUNT/PARAMETERS all blocked by this PR, the first session git commit still executes the attacker hook as the daemon user. Probe-reproduced end-to-end at this commit (control arm without the variable did not execute the hook); adding the key to the list flipped the probe on both application paths. Same config-file-redirect class this list already blocks for GIT_CONFIG_GLOBAL, CURL_HOME/WGETRC, and PIP_CONFIG_FILE.

Suggested change
'GIT_CONFIG_GLOBAL',
'GIT_CONFIG_SYSTEM',
'GIT_CONFIG_GLOBAL',
'GIT_CONFIG_SYSTEM',
'XDG_CONFIG_HOME',
中文说明

R4-6: XDG_CONFIG_HOME 未被排除,项目 .env 可以借此重定向 git 的配置发现路径,绕过本 PR 新增的全部 GIT_CONFIG_* 阻断——git 会将 $XDG_CONFIG_HOME/git/config~/.gitconfig 合并读取(已在本提交验证:git config --list --show-origin 两个文件都会读取)。失败场景:攻击者仓库的 .env 设置 XDG_CONFIG_HOME=/repo/.xdg,并提交一个含 [core] hooksPath = /repo/.hooks.xdg/git/config → 即使 GIT_CONFIG_GLOBAL/SYSTEM/COUNT/PARAMETERS 都已被本 PR 阻断,会话第一次 git commit 仍会以 daemon 用户身份执行攻击者的 hook。已在本提交端到端探针复现(不设置该变量的对照组未执行 hook);将该键加入名单后探针在两处应用路径上均翻转。与本列表已阻断的 GIT_CONFIG_GLOBALCURL_HOME/WGETRCPIP_CONFIG_FILE 属于同一“配置文件重定向”类别。

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

Comment on lines +142 to +145
// npm runs `$npm_config_git` as the git binary for install-from-git and
// similar flows, so a project `.env` pointing it at an attacker script is
// the same exec redirect as the interpreter keys above.
'npm_config_git',

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-8: BROWSER is absent from PROJECT_ENV_HARDCODED_EXCLUSIONS, yet the CLI itself execs $BROWSER via openBrowserSecurely (packages/core/src/utils/secure-browser-launcher.ts) — the same exec-redirect class this tier exists to block. — Failure scenario: attacker repo ships .env with BROWSER=/repo/evil.sh → the value is applied on all four boundaries (probe-verified: initial load, reload, buildRuntimeEnvironment, settings.env; the comparator keys npm_config_git/QWEN_CLI_ENTRY in the same .env were rejected) → any browser-launch flow executes it. In openBrowserSecurely, a set BROWSER is passed straight to spawn() before the shouldAttemptBrowserLaunch CI/DISPLAY gates are consulted, and the only blocklisted command is www-browser — probe: with CI=true and no DISPLAY, openBrowserSecurely('https://…') spawned /repo/evil.sh. Reachable from /docs, /bug, /extensions, /insight, qwen serve --open, MCP OAuth, qwen auth, and the artifact tool's auto-open (default permission ask; zero-interaction under yolo/allow-rule modes). Folder trust is off by default, so no trust grant is needed.

Suggested change
// npm runs `$npm_config_git` as the git binary for install-from-git and
// similar flows, so a project `.env` pointing it at an attacker script is
// the same exec redirect as the interpreter keys above.
'npm_config_git',
// npm runs `$npm_config_git` as the git binary for install-from-git and
// similar flows, so a project `.env` pointing it at an attacker script is
// the same exec redirect as the interpreter keys above.
'npm_config_git',
'BROWSER',
中文说明

R4-8: BROWSER 不在 PROJECT_ENV_HARDCODED_EXCLUSIONS 中,而 CLI 自身会经由 openBrowserSecurely(packages/core/src/utils/secure-browser-launcher.ts)执行 $BROWSER——与该层要阻断的“执行重定向”类别完全相同。失败场景:攻击者仓库的 .env 设置 BROWSER=/repo/evil.sh → 该值在全部四处边界被应用(探针验证:初始加载、重载、buildRuntimeEnvironment、settings.env;同一 .env 中的对照键 npm_config_git/QWEN_CLI_ENTRY 均被拒绝)→ 任何打开浏览器的流程都会执行它。在 openBrowserSecurely 中,已设置的 BROWSER 会在 shouldAttemptBrowserLaunch 的 CI/DISPLAY 门控之前直接交给 spawn(),且命令黑名单中仅有 www-browser——探针:在 CI=true 且无 DISPLAY 时,openBrowserSecurely('https://…') 启动了 /repo/evil.sh。可经 /docs/bug/extensions/insightqwen serve --open、MCP OAuth、qwen 认证以及 artifact 工具的自动打开触达(默认权限为 ask;在 yolo/allow-rule 模式下无需交互)。folder trust 默认关闭,无需任何信任授权。

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

Comment on lines 146 to 148
// QWEN_CLI_ENTRY is the script path daemon-spawned session processes run.
// A project `.env` or settings.env fixing it turns
// `cd <untrusted repo> && qwen serve` into code execution as the daemon

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-9: QWEN_CDP_MCP_COMMAND (a command the daemon spawns as the browser-automation MCP adapter) and QWEN_SERVE_CDP_TUNNEL_OVER_WS (which switches the tunnel surface on) are settable from a project .env, violating the exact rationale the adjacent QWEN_CLI_ENTRY exclusion below states. — Failure scenario: attacker repo ships .env with QWEN_CDP_MCP_COMMAND=/repo/evil-adapter and QWEN_SERVE_CDP_TUNNEL_OVER_WS=1 → probe-confirmed applied on all four boundaries; the serve fast path writes both into process.env before runQwenServe reads them → the env var alone makes cdpTunnelOverWs true (envFlagDisabled('1') is false) → on the default tokenless daemon, when a WS client initializes with the CDP bridge client name, ensureChromeDevToolsMcpRegistered builds a runtime MCP config whose command is resolveCdpMcpCommand(daemonEnv) — the attacker path — and addRuntimeMcpServer spawns it: code execution as the daemon user. Both cdp_tunnel_over_ws and browser_automation_mcp capability advertisements are flipped by project-controlled values. Fix (the key line sits just below this anchor, so a regular block rather than a suggestion):

  'QWEN_CLI_ENTRY',
  'QWEN_CDP_MCP_COMMAND',
  'QWEN_SERVE_CDP_TUNNEL_OVER_WS',
中文说明

R4-9: QWEN_CDP_MCP_COMMAND(daemon 作为浏览器自动化 MCP 适配器启动的命令)与 QWEN_SERVE_CDP_TUNNEL_OVER_WS(开启 tunnel 应用面的开关)可以从项目 .env 设置,直接违反了下文 QWEN_CLI_ENTRY 排除项自身写明的理由。失败场景:攻击者仓库的 .env 设置 QWEN_CDP_MCP_COMMAND=/repo/evil-adapterQWEN_SERVE_CDP_TUNNEL_OVER_WS=1 → 探针证实在全部四处边界被应用;serve 快速路径会在 runQwenServe 读取它们之前把两个键写入 process.env → 仅凭该环境变量即可使 cdpTunnelOverWs 为 true(envFlagDisabled('1') 为 false)→ 在默认无 token 的 daemon 上,任何以 CDP bridge 客户端名初始化的 WS 连接都会触发 ensureChromeDevToolsMcpRegistered,其构建的运行时 MCP 配置的 commandresolveCdpMcpCommand(daemonEnv)——攻击者路径——addRuntimeMcpServer 会将其启动:以 daemon 用户身份执行代码。cdp_tunnel_over_wsbrowser_automation_mcp 两个能力声明也都会被项目可控的值翻转。修复(键所在行位于本锚点下方,故用普通代码块而非 suggestion)。

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

Comment on lines +443 to +444
'NODE_REPL_EXTERNAL_MODULE',
'OPENSSL_CONF',

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-2: The four new scrubbed loader keys (OPENSSL_CONF, NODE_REPL_EXTERNAL_MODULE, npm_config_node_gyp, npm_config_init_module) are pinned only by the list-pin and isLoaderEnvKey unit tests; the behavioural scrub test 'removes every loader-affecting key and keeps the rest' (~line 346, unchanged by this PR) never sets them, so its for (const key of INHERITED_LOADER_ENV_KEYS) expect(env[key]).toBeUndefined() loop passes vacuously for the four new keys. — Failure scenario (mutation probe-verified): replacing scrubInheritedLoaderEnv's isLoaderEnvKey(key) filter with a stale literal list missing the four new keys leaves 473/473 tests green, yet a daemon launched from a shell exporting a malicious OPENSSL_CONF would pass it straight into session subprocesses — the dlopen-engine code execution this PR's list addition is meant to block. The probe also verified the fix below kills the mutant and stays green on the real code. Add the four keys to that fixture and its expected removedKeys list:

process.env['OPENSSL_CONF'] = '/evil.cnf';
process.env['NODE_REPL_EXTERNAL_MODULE'] = '/evil.mjs';
process.env['npm_config_node_gyp'] = '/evil-gyp.js';
process.env['npm_config_init_module'] = '/evil-init.js';
中文说明

R4-2: 四个新增的被剥离 loader 键(OPENSSL_CONFNODE_REPL_EXTERNAL_MODULEnpm_config_node_gypnpm_config_init_module)只被列表钉住测试与 isLoaderEnvKey 单元测试覆盖;行为级剥离测试 'removes every loader-affecting key and keeps the rest'(约第 346 行,本 PR 未改动)从未在 fixture 中设置它们,因此其 for (const key of INHERITED_LOADER_ENV_KEYS) expect(env[key]).toBeUndefined() 循环对这四个新键是空洞通过。失败场景(变异探针验证):把 scrubInheritedLoaderEnvisLoaderEnvKey(key) 过滤替换为遗漏这四个新键的陈旧字面量列表,473/473 个测试仍全绿,但从导出了恶意 OPENSSL_CONF 的 shell 启动的 daemon 会把它直接传进会话子进程——正是本 PR 新增名单要阻断的 dlopen 引擎代码执行。探针同时验证了下方修复能杀死该变异体,且在真实代码上保持全绿。请将这四个键加入该 fixture 及其期望的 removedKeys 列表。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 10/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 10/100 轮)。改动内容与我反驳保留之处如下:

Round summary (PR #8763, review round 4)

All seven actionable findings were verified against the exact code at this
commit and addressed in one commit (28352d54ae). Every finding named an env
key that still reached an exec/config-redirect sink from a project .env
despite the denylist this PR builds; all application gates route through
isHardcodedProjectEnvExclusion, so adding the keys to
PROJECT_ENV_HARDCODED_EXCLUSIONS closes the initial load, reload,
buildRuntimeEnvironment, and serve fast-path boundaries at once. The new
keys join the reject-only tier: operator-set launch-env and home-.env
values are preserved.

Findings and dispositions

Finding Severity Disposition Change
R4-1 GIT_SEQUENCE_EDITOR missing Critical Addressed Added beside GIT_EDITOR; git execs it for git rebase -i todo-list edits. Git-family comment updated to name it.
R4-5 VISUAL/EDITOR missing Critical Addressed Added to the reject-only tier. Verified: git's documented editor fallback chain, and useLaunchEditor.ts returns process.env['VISUAL'] || process.env['EDITOR'] || 'vi' straight into spawnSync.
R4-6 XDG_CONFIG_HOME missing Critical Addressed Added after GIT_CONFIG_SYSTEM per the suggestion. git merges $XDG_CONFIG_HOME/git/config with ~/.gitconfig, so an XDG redirect plants the same core.hooksPath injection the GIT_CONFIG_* blocks exist for, around them.
R4-7 PYTHONSTARTUP missing Critical Addressed Added to the reject-only tier next to PYTHON — the Python analogue of NODE_REPL_EXTERNAL_MODULE; stays reject-only because operators legitimately set it in their own shells.
R4-8 BROWSER missing Critical Addressed Added after npm_config_git per the suggestion. Verified: openBrowserSecurely spawns a set $BROWSER before the CI/DISPLAY gates.
R4-9 QWEN_CDP_MCP_COMMAND / QWEN_SERVE_CDP_TUNNEL_OVER_WS missing Critical Addressed Added beside QWEN_CLI_ENTRY. The daemon spawns the former as the browser-automation MCP adapter and the latter switches the tunnel surface on; internal uses (operator launch env, daemon childEnvOverrides) are untouched by the reject-only tier.
R4-2 scrub test passes vacuously for the four new loader keys Suggestion Addressed OPENSSL_CONF, NODE_REPL_EXTERNAL_MODULE, npm_config_node_gyp, npm_config_init_module added to the 'removes every loader-affecting key and keeps the rest' fixture and its expected removedKeys, so a stale literal-list mutant now fails the test.

No finding was declined or escalated.

Also in this commit

  • CHANGES_REQUESTED review body: the review's request-changes event was its
    own verification-coverage note (integration suite merge-queue-gated by
    design; the reviewer could not run unit tests in its worktree). Response:
    the five named suites were actually run this round (see Verification).
  • Tests: class-membership pins for the eight new keys, case-insensitive
    predicate pins, and a reject-only pin asserting none of them is a
    loader-scrub key (so a user's own VISUAL/EDITOR/BROWSER are never
    stripped from the launch env). New environment.test.ts boundary test
    rejects all eight keys on initial load, reload, settings.env, and the
    runtime-env build.
  • Docs: docs/users/configuration/settings.md denylist paragraph updated
    with the new keys.
  • No base-conflict work (--conflict false; no merge performed).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx vitest run src/config/shared-env-keys.test.ts src/config/environment.test.ts src/serve/fast-path.test.ts src/serve/process-env-guard.test.ts src/commands/channel/daemon-worker.test.ts (in packages/cli) — 5 files passed, 239/239 tests passed
  • npx prettier --experimental-cli --write on the four changed files — applied (one file reformatted)
  • npm run generate:settings-schema — not required (no settings source changed)
  • Integration tests — not run: the suite is merge-queue-gated by design, and the changed behavior (env rejection) is exercised directly by the unit suites above at all four application boundaries rather than only through the bundled CLI. The workflow's CI remains the final gate.
中文说明

轮次总结(PR #8763,审查第 4 轮)

全部 7 条可执行发现均已在本提交的代码上逐一核实,并在一次提交(28352d54ae)中处理完毕。每条发现都指出了一个仍能经由项目 .env 触达“执行/配置重定向”汇点的键;由于所有应用门控都经过 isHardcodedProjectEnvExclusion,将这些键加入 PROJECT_ENV_HARDCODED_EXCLUSIONS 即同时封堵初始加载、重载、buildRuntimeEnvironment 与 serve 快速路径四处边界。新键均加入“仅拒绝项目文件”层:操作者在启动环境或家目录 .env 中设置的值不受影响。

发现与处置

发现 严重度 处置 改动
R4-1 缺少 GIT_SEQUENCE_EDITOR Critical 已处理 加在 GIT_EDITOR 旁;git 在 git rebase -i 编辑 todo 列表时会执行它。git 家族注释已同步提及。
R4-5 缺少 VISUAL/EDITOR Critical 已处理 加入仅拒绝层。已核实:git 文档化的编辑器回退链会使用它们,且 useLaunchEditor.ts 直接返回 process.env['VISUAL'] || process.env['EDITOR'] || 'vi' 交给 spawnSync
R4-6 缺少 XDG_CONFIG_HOME Critical 已处理 按 suggestion 加在 GIT_CONFIG_SYSTEM 之后。git 会将 $XDG_CONFIG_HOME/git/config~/.gitconfig 合并读取,因此 XDG 重定向能绕过 GIT_CONFIG_* 阻断植入同样的 core.hooksPath 注入。
R4-7 缺少 PYTHONSTARTUP Critical 已处理 加入仅拒绝层、置于 PYTHON 旁——它是 NODE_REPL_EXTERNAL_MODULE 的 Python 同型变体;因操作者常在自己的 shell 中合法设置它,保持仅拒绝语义。
R4-8 缺少 BROWSER Critical 已处理 按 suggestion 加在 npm_config_git 之后。已核实:openBrowserSecurely 在 CI/DISPLAY 门控之前就会启动已设置的 $BROWSER
R4-9 缺少 QWEN_CDP_MCP_COMMAND / QWEN_SERVE_CDP_TUNNEL_OVER_WS Critical 已处理 加在 QWEN_CLI_ENTRY 旁。daemon 会将前者作为浏览器自动化 MCP 适配器启动、后者开启隧道应用面;内部用途(操作者启动环境、daemon 的 childEnvOverrides)不受仅拒绝层影响。
R4-2 剥离测试对四个新 loader 键空洞通过 Suggestion 已处理 OPENSSL_CONFNODE_REPL_EXTERNAL_MODULEnpm_config_node_gypnpm_config_init_module 加入 'removes every loader-affecting key and keeps the rest' 的 fixture 与期望 removedKeys,使“陈旧字面量列表”变异体现在会被该测试杀死。

无发现被拒绝或升级待决。

本提交的其他内容

  • CHANGES_REQUESTED 审查正文:该请求改动事件是审查方自身的验证覆盖说明(集成测试在 CI 中按设计受 merge-queue 门控;审查 worktree 无法运行单元测试)。应对:本轮实际运行了审查点名的 5 个测试套件(见验证部分)。
  • 测试:为 8 个新键增加类别成员钉住测试、大小写不敏感谓词钉住测试,以及“仅拒绝、不属于 loader 剥离键”的钉住断言(确保用户自己的 VISUAL/EDITOR/BROWSER 不会被从启动环境剥离)。environment.test.ts 新增边界测试,验证 8 个键在初始加载、重载、settings.env 与运行时环境构建上均被拒绝。
  • 文档:docs/users/configuration/settings.md 的拒绝名单段落已更新新键。
  • 无基线冲突处理(--conflict false,未执行合并)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx vitest run src/config/shared-env-keys.test.ts src/config/environment.test.ts src/serve/fast-path.test.ts src/serve/process-env-guard.test.ts src/commands/channel/daemon-worker.test.ts(位于 packages/cli)— 5 个文件通过,239/239 个测试通过
  • 对 4 个改动文件执行 npx prettier --experimental-cli --write — 已应用(1 个文件被重排)
  • npm run generate:settings-schema — 无需执行(未改动 settings 源)
  • 集成测试 — 未运行:该套件按设计受 merge-queue 门控,且本次改动的行为(环境变量拒绝)已由上述单元测试在全部四处应用边界直接覆盖,并非只能经打包后的 CLI 验证。工作流的 CI 仍是最终验证门。

Deferred non-Critical feedback

Critical-only mode is active after 5 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 5 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

📊 Takeover milestone — round 10/100, in the current window. Census: 6 pushed fix(es), 3 no-change review(s), 2 timeout(s), 1 rejected attempt(s), 1 other round(s) (crash / model error / gate error / infra), 2 base update(s).

This many rounds deserves a human look. Options: keep going (fine — nothing changes), split or reduce the PR if rounds keep accumulating, or release takeover (remove the autofix/takeover label or comment @qwen-code /takeover stop). Management continues unchanged unless you act.

中文说明

📊 接管里程碑 —— 第 10/100 轮(当前窗口)。统计:推送修复 6 次、审阅无需改动 3 次、超时 2 次、验证拒绝 1 次、其他轮次(崩溃/模型错误/门错误/infra)1 次、base 更新 2 次。

轮次到这个量值得人工看一眼。可选:继续(无需操作);若轮次持续累积,考虑拆分或缩减 PR;或释放接管(移除 autofix/takeover 标签或评论 @qwen-code /takeover stop)。不操作则托管照常继续。

@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.

Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally. Not explored to full depth (tool budget reached): PR #8763 is a security-hardening follow-up to #8663: it e...: could not execute the test suite ( shared-env-keys.test.ts , process-env-guard.test.ts , environment.test.ts , etc.) — neither the worktree nor the parent che….

中文说明

已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。 未探索到全部深度(达到工具调用预算):PR #8763 is a security-hardening follow-up to #8663: it e...:could not execute the test suite ( shared-env-keys.test.ts , process-env-guard.test.ts , environment.test.ts , etc.) — neither the worktree nor the parent che…

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

Comment on lines +155 to +158
// and the CLI's own useLaunchEditor spawns them from ordinary interactive
// flows.
'VISUAL',
'EDITOR',

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] npm_config_editor is missing from PROJECT_ENV_HARDCODED_EXCLUSIONS — npm maps it onto its editor config and npm edit executes it, the same exec-redirect class this round blocks for VISUAL/EDITOR/GIT_EDITOR. — Failure scenario: an untrusted repo ships a project .env with npm_config_editor=<repo>/evil.sh; probe-verified the key passes every gate and is applied by all four application boundaries, and when a session runs npm edit <pkg> npm spawns the attacker script as the daemon/CLI user (end-to-end exec probe-verified; adding the key flipped the probe).

Suggested change
// and the CLI's own useLaunchEditor spawns them from ordinary interactive
// flows.
'VISUAL',
'EDITOR',
// and the CLI's own useLaunchEditor spawns them from ordinary interactive
// flows. npm maps npm_config_editor onto the same `editor` config that
// `npm edit` executes, so it belongs in this tier too.
'VISUAL',
'EDITOR',
'npm_config_editor',
中文说明

npm_config_editor 未列入 PROJECT_ENV_HARDCODED_EXCLUSIONS——npm 将其映射为 editor 配置,而 npm edit 会执行它,与本轮已阻断的 VISUAL/EDITOR/GIT_EDITOR 属于同一“执行重定向”类别。失败场景:不受信仓库的 .env 设置 npm_config_editor=<repo>/evil.sh;探针验证该键通过所有门控并在全部四处应用边界被应用,会话运行 npm edit <pkg> 时 npm 会以 daemon/CLI 用户身份启动攻击者脚本(已端到端探针验证;加入该键后探针翻转)。

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

Comment on lines +137 to +138
'LESSOPEN',
'LESSCLOSE',

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] MANPAGER is missing from PROJECT_ENV_HARDCODED_EXCLUSIONS — it is the one pager-exec variable that survives the CLI's spawn-env construction (getShellPagerEnv overrides only PAGER/GIT_PAGER), the same conditional-exec class as the blocked LESSOPEN/LESSCLOSE just above. — Failure scenario: a project .env sets MANPAGER=<repo>/evil.sh; probe-verified it passes every gate and is applied by all four boundaries; a pty-backed session running man <topic> or git help <cmd> executes the attacker script as the daemon/CLI user (empirically confirmed under a pty: man runs $MANPAGER as a shell command; adding the key flipped the probe).

Suggested change
'LESSOPEN',
'LESSCLOSE',
'LESSOPEN',
'LESSCLOSE',
'MANPAGER',
中文说明

MANPAGER 未列入 PROJECT_ENV_HARDCODED_EXCLUSIONS——它是唯一能穿透 CLI 子进程环境构造的 pager 执行变量(getShellPagerEnv 只覆盖 PAGER/GIT_PAGER),与上方已阻断的 LESSOPEN/LESSCLOSE 属于同一条件执行类别。失败场景:项目 .env 设置 MANPAGER=<repo>/evil.sh;探针验证其通过所有门控并在全部四处边界被应用;pty 会话运行 man <topic>git help <cmd> 时会以 daemon/CLI 用户身份执行攻击者脚本(已在 pty 下实证:man 会把 $MANPAGER 作为 shell 命令执行;加入该键后探针翻转)。

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

// attacker file sends session pip traffic or credentials to attacker
// infrastructure — the same config-file-redirect class as
// npm_config_userconfig and GIT_CONFIG_GLOBAL.
'PIP_CONFIG_FILE',

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] AWS_CONFIG_FILE is not excluded from project .env — the AWS analog of the config-file-redirect class this diff blocks for pip (PIP_CONFIG_FILE), npm, git, curl and wget. — Failure scenario: a project .env sets AWS_CONFIG_FILE=<repo>/aws-config whose profile declares credential_process = <repo>/evil.sh; probe-verified the key passes every gate and is applied on initial load, reload, runtime build and settings.env (and sanitize-child-env.ts explicitly preserves third-party credential vars into session subprocesses); the AWS CLI executes that process on any credential resolution (e.g. aws s3 ls) — code execution as the daemon user plus credential diversion (fix flip-checked: adding the key closes all four boundaries).

Suggested change
'PIP_CONFIG_FILE',
'PIP_CONFIG_FILE',
'AWS_CONFIG_FILE',
中文说明

AWS_CONFIG_FILE 未从项目 .env 排除——这是本 diff 已为 pip(PIP_CONFIG_FILE)、npm、git、curl、wget 阻断的“配置文件重定向”类别的 AWS 同型变体。失败场景:项目 .env 设置 AWS_CONFIG_FILE=<repo>/aws-config,其 profile 声明 credential_process = <repo>/evil.sh;探针验证该键通过所有门控,并在初始加载、重载、运行时环境构建与 settings.env 四处边界被应用(且 sanitize-child-env.ts 明确保留第三方凭据变量进入会话子进程);AWS CLI 在任何凭据解析时(如 aws s3 ls)都会执行该进程——以 daemon 用户身份执行代码并转移凭据(修复已翻转验证:加入该键后四处边界全部关闭)。

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

@wenshao

wenshao commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (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: 699 passed · 1 failed · 700 total

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

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

脚本断言:699 通过 · 1 失败 · 700 总计

Verification report

PR 8763 Deep Verification (round 3 — follow-up at a NEW head)

Verdict: findings — the PR's central claim was re-proven live by A/B at the new head (base applies all 49 new keys = poisoning reproduced; head rejects all of them, operator values preserved), the two new commits' claims were verified (the 3 previously-surviving mutants are now killed by the new tests; the 8 round-4 keys are blocked end-to-end), mutation matrix 5/5 killed with 0 survivors, PR's own tests overlaid on base fail 28/74 exactly as predicted, and the six affected suites are 482/482 green. The verdict is findings rather than merge-ready because the re-measured round-1/2 findings F1 (registry/index redirects) and F2-half (R_PROFILE_USER) still stand at this head, plus one new docs sub-nit (F5). None are blockers to the shipped code.

Assertions: 699 pass / 1 fail / 700 total · Verified head: 28352d54ae02bfbc8345a5158a850aa73132c0d4 (merge ref 4e259219aa, base 0d56e50b64)

中文摘要

这是 head 变更后的复验轮(round 3):HEAD^2 从上轮的 3b7072826d… 前移到 28352d54ae…,新增两个 commit:146c448c(剥离快照合并为单遍 + 新增杀死既往存活变异体的测试)与 28352d54(round-4 exec-redirect 键)。本轮对全部断言重新实测(head 变更,不承继上轮数字)。

结论 findings(非阻断)

  • A/B 复测(21 cell,137/138):全部 49 个新键(4 loader + 41 项目层字面量 + 4 编号对实例)在 base 上从项目 .env/settings.env/daemon spawn 缝应用(污染成立),在 PR 头上全部被拒绝;home .env 与 shell 导出的合法值保留;编号 GIT_CONFIG 对在 head 上 reload 冻结、在 base 上中途轮换。见 Central claim and A/B result 表与图 02-ab-driver-head-vs-base-live.png
  • 新 commit 验证146c448c 声称杀死的 3 个既往存活变异体(release 时清空、测试用 reset 清空、undefined 值守卫)经变异矩阵确认全部被新测试杀死,且各自死在预期断言上;28352d54 新增的 8 个 round-4 键全部通过 A/B(base 应用 / head 拒绝 / 运维导出保留)。见 Delta verification 与图 03-mutation-matrix-5-of-5-killed.png
  • Load-bearing:PR 自己的测试放回 base 红绿分布为 28 红 / 46 绿(28 个红全部是 PR 新增或 PR 扩展的测试,逐一归因);未变异对照 74/74 绿。门控:6 个受影响套件 482/482 全绿。
  • 上轮发现状态:F1 仍然存在(6 个 registry/index 键仍全部应用)、F2 部分修复PYTHONSTARTUP 已被 round-4 commit 拒绝,R_PROFILE_USER 仍应用)、F3 仍然存在、F4 仍然存在(PR 正文进一步落后于 shipped 列表),另新增 F5(nit:npm_config_strict-ssl 连字符拼写未出现在 settings.md)。见 Previous-finding status 表与图 06-f1-f2-siblings-re-measured.png
  • 未覆盖:见 Not covered

Previous-finding status (rounds 1-2 → this round)

Identity first: this round verifies a new head. git rev-parse HEAD^2 = 28352d54ae02bfbc8345a5158a850aa73132c0d4 ≠ round-2's 3b7072826d… (unreachable, depth-2). Two new commits since round 2 (from the metadata snapshot): 146c448c "merge the loader-env scrub snapshot into one pass" and 28352d54 "block the round-4 exec-redirect env keys from project files". The base also moved (55e20db30d56e50b64, only #8795 core-test deflakes landed between). Because the head changed, every carried-forward measurement was re-run live this round — nothing was inherited. Identity and control checks: raw/identity.txt, witness 01-identity-round3-delta.png.

# Round-1/2 finding Severity Status at this head (re-measured)
F1 Package-registry/index redirects (npm_config_registry, PIP_INDEX_URL/PIP_EXTRA_INDEX_URL/PIP_TRUSTED_HOST, UV_INDEX_URL/UV_DEFAULT_INDEX) still apply from a project .env medium stands — all 6 keys APPLIED again (raw/head-siblings.log)
F2 Interpreter/REPL startup-file siblings (PYTHONSTARTUP, R_PROFILE_USER) of the blocked NODE_REPL_EXTERNAL_MODULE still apply low partially fixedPYTHONSTARTUP is now REJECTED (round-4 commit added it, with an explicit "Python analogue of NODE_REPL_EXTERNAL_MODULE" comment); R_PROFILE_USER still APPLIED (same cell)
F3 Project-tier rejections are unobservable (pre-existing design; no breadcrumb anywhere) nit stands — mixed .env (NODE_OPTIONS+GIT_SSH_COMMAND+SSL_CERT_FILE) reporter events named only NODE_OPTIONS, on both arms (raw/head-reporter-mixed.log, raw/base-reporter-mixed.log)
F4 PR body enumerates a subset of the shipped denylist nit stands (gap widened) — body names 13 project-tier keys + numbered pairs; shipped is now 55 literals + 2 patterns (41 PR-added literals). settings.md documents the full PR-added set (40/41 — see F5), so user docs remain complete

No declined rows existed; nothing worsened — every previously-green behavior re-measured green (all A/B head cells, 482/482 gates). The 8 round-4 keys are the only behavior delta, and they only tighten rejection.

Central claim and A/B result (re-measured live at the new head)

Central claim: the 4 pure-injection loader keys are scrubbed from the inherited env and rejected from every .env/settings.env scope; the 41 new hardcoded literals + the numbered GIT_CONFIG_KEY_<n>/VALUE_<n> pattern pair are rejected from project files (and frozen on reload) while operator shell / home-.env values are preserved; the scrub is reference-counted and the reporter clears only if current.

Key lists were derived by diffing the modules' own exports between arms (not transcribed): +4 loader tier (12 → 16), +41 project-tier literals (14 → 55), 2 new pattern regexes present on head / absent on base (raw/lists.json). Every cell drove the real loadEnvironment / reloadEnvironment / buildRuntimeEnvironment of the arm's tree with fixture files under a scratch $HOME; per-arm expectations were encoded in the harness, so predicted base behavior counts as passing assertions. 49 new keys per project-scope cell = 4 loader + 41 literals + 4 numbered-pair instances. Witness: 02-ab-driver-head-vs-base-live.png (a live re-run of the full driver).

Cell Oracle head base
Project .env with all 49 new keys + controls process.env after load 49/49 rejected, benign applied, pre-existing NODE_OPTIONS/NODE_EXTRA_CA_CERTS still rejected 49/49 applied (the poisoning)
settings.env with all 49 keys process.env after load 49/49 rejected, benign applied applied
buildRuntimeEnvironment (daemon spawn seam), .env + settings.env snapshot.effectiveEnv 49/49 absent, benign + base passthrough intact applied
Home .env: 3 project-tier keys + OPENSSL_CONF + benign tier semantics project tier preserved (3/3), OPENSSL_CONF rejected project tier preserved, OPENSSL_CONF applied (not in base loader list)
Operator shell export + hostile project override (EDITOR, BROWSER, GIT_SEQUENCE_EDITOR, XDG_CONFIG_HOME) no-override mode export wins, values survive (project tier not scrubbed from launch env) export wins
Reload after rotating/removing home-.env numbered pairs + GIT_SSH_COMMAND freeze vs rotate frozen at boot values (GIT_CONFIG_VALUE_0=!evil-0 retained, rotated-in pair never applies, removal does not delete), benign rotates rotates mid-session (GIT_SSH_COMMAND/rotated/ssh, rotated-in pair applies, removal deletes boot pair)
Reporter events for mixed .env loader-tier reported NODE_OPTIONS reported; hardcoded tier silent (by design, = F3) same shape
Membership drift guard (head-only) every derived key guarded + case/spelling variants + pattern precision 22/22 (incl. GIT_CONFIG_KEY_CACHE/GIT_CONFIG_KEYS/empty-suffix stay settable)
Refcounted scrub, two overlapping daemons (harness-refcount.mts) env state at each step 13/13: no re-poison while B live; final restore uses newest snapshot; later legit assignment wins; double release idempotent; cycle-2 stale snapshot not re-injected 8/8 expected-behavior cells: OPENSSL_CONF never scrubbed at all; A close() re-poisons shared env while B live
Reporter guard, A exits after B installs (harness-reporter.mts) which reporter receives fresh rejections 7/7: B's reporter survives A's exit, cleared on last exit, stderr fallback fires, warn-once dedup holds after reinstall 5/5 expected-behavior cells: unconditional clear drops B's reporter (fresh rejection goes to stderr, not B)

Cell totals: 137 pass + 1 fail across 21 cell runs (12 head + 9 base); the one fail is the F5 docs assertion (raw/driver-run.txt, per-cell logs in raw/).

Delta verification (the two new commits)

146c448c — one-pass scrub merge + tests for previously-surviving mutants. The commit claims its new tests kill three mutants that survived in earlier rounds. Verified by running each mutant against the head suite (raw/matrix-summary.txt, per-mutant vitest logs):

Mutant Intended pin Result
M1 drop sharedProcessEnvScrubOriginals.clear() in release() "does not restore a prior cycle snapshot for a key the host removed" killed (1 red) — previously surviving, now pinned
M2 drop .clear() in resetInheritedLoaderEnvScrubForTesting() "reset drops a leaked snapshot before the next acquire" killed (1 red) — previously surviving, now pinned
M3 drop the value !== undefined snapshot guard "does not snapshot loader keys whose value is undefined" killed (1 red) — previously surviving, now pinned
M4 remove 'BROWSER' (positive control) round-4 application test + predicate pins killed (3 red)
M5 revert isReloadExcludedKey to set-only (positive control) "freezes the numbered GIT_CONFIG pairs on reload like GIT_CONFIG_COUNT" killed (1 red)

5/5 killed, 0 survivors; unmutated control 74/74 green; every mutant died on exactly its intended pin with expected-vs-actual assertion messages (behavioral mismatch, not import breakage); tree restored clean after each mutant. The one-pass merge's observable semantics are additionally pinned live by the A/B refcount cells (newest-wins snapshot, later-assignment-wins, cycle-2 freshness).

28352d54 — round-4 exec-redirect keys. The 8 keys new at this head (GIT_SEQUENCE_EDITOR, XDG_CONFIG_HOME, PYTHONSTARTUP, VISUAL, EDITOR, BROWSER, QWEN_CDP_MCP_COMMAND, QWEN_SERVE_CDP_TUNNEL_OVER_WS) were derived as the set-difference against round 2's documented shipped list, then exercised: all 8 applied from a project .env on base and rejected on head (they are inside the 49/49 cells), operator shell exports of EDITOR/BROWSER/GIT_SEQUENCE_EDITOR/XDG_CONFIG_HOME survive the hostile override on both arms, the M4 mutant proves the application-layer pin is load-bearing, and settings.md documents all 8 individually (raw/head-docs.log). QWEN_CDP_MCP_COMMAND/QWEN_SERVE_CDP_TUNNEL_OVER_WS are genuine daemon-consumed knobs (cdp-mcp-command.ts, run-qwen-serve.ts:2200/2915 — the daemon's own childEnvOverrides still sets them from operator opts, unaffected by the project-file rejection).

Findings

F1 — [medium, completeness, stands since round 1] Package-registry/index redirects still apply from a project .env — the same supply-chain outcome class this PR blocks, without needing a MITM position. npm_config_registry, PIP_INDEX_URL, PIP_EXTRA_INDEX_URL, PIP_TRUSTED_HOST, UV_INDEX_URL, UV_DEFAULT_INDEX are applied from an untrusted workspace .env and frozen into daemonRuntimeBaseEnv for every workspace's sessions — the exact #8653 outcome the PR exists to close. An attacker index is strictly stronger than the TLS-anchor MITM this PR blocks: malicious package content arrives directly, and postinstall executes it with no interception position (PIP_TRUSTED_HOST additionally downgrades pip to plaintext). Round 3 added exec-redirect and startup-file keys but not these. Repro: raw/head-siblings.log (# probe npm_config_registry: APPLIED from project .env × 6). Disposition unchanged: add to PROJECT_ENV_HARDCODED_EXCLUSIONS, or explicitly track as deferred like LD_LIBRARY_PATH/PATH — silence in the description reads as covered. Not a blocker: the PR is an explicitly curated increment and the shipped code is correct for everything it claims.

F2 — [low, completeness, partially fixed] Interpreter/REPL startup-file siblings: PYTHONSTARTUP fixed, R_PROFILE_USER still applies. The round-4 commit blocked PYTHONSTARTUP (comment: "the Python analogue of NODE_REPL_EXTERNAL_MODULE") — re-measured REJECTED from a project .env. Its named twin R_PROFILE_USER (sourced by R at startup, same mechanism shape) still applies, as do adjacent interpreter-startup/option siblings probed for completeness: R_ENVIRON_USER, PERL5OPT, RUBYOPT (raw/head-siblings.log). Same repro cell as F1.

F3 — [nit, pre-existing, stands] Project-tier rejections are unobservable. A .env mixing NODE_OPTIONS, GIT_SSH_COMMAND, SSL_CERT_FILE produces reporter events naming only NODE_OPTIONS; the hardcoded-tier rejections leave no breadcrumb anywhere, on both arms (raw/head-reporter-mixed.log). Predates this PR; silence is defensible (don't echo attacker values) — a count-only breadcrumb would close the diagnostic gap the PR's own description targets.

F4 — [nit, stands, gap widened] The PR body enumerates a subset of the shipped denylist. The description names 13 project-tier keys + the numbered pairs; the shipped list at this head is 55 literals + 2 pattern regexes (41 PR-added literals, derived). settings.md documents the full PR-added set (see F5 for the one spelling gap), so user-facing docs are complete; only the PR text lags. Code impact: none.

F5 — [nit, new this round] npm_config_strict-ssl (hyphen spelling) is blocked but not named in settings.md. Both spellings are in the denylist (code comment: npm treats underscore/hyphen spellings as the same key), but the docs list only npm_config_strict_ssl (appears 2×; the hyphen form 0×). Scripted check: 40/41 PR-added literals documented (raw/head-docs.log). A user grepping the docs for the hyphen form finds nothing. One-word fix: add the twin spelling to the settings.md enumeration.

No injection-style steering instructions were present in the PR text or the previous report; author claims were treated as hypotheses and re-tested (the "458 tests" figure in the description re-measured as 482 across the six affected suites at this head — consistent with suite growth across the PR's later commits).

Not covered

  • Per-commit attribution: depth-2 shallow checkout — git rev-list HEAD^1..HEAD^2 reports 1 commit against 11 in the metadata snapshot (shallow-boundary artifact, re-verified: raw/identity.txt). Only the aggregate HEAD^1..HEAD diff was verified; the two new commits' effects were verified behaviorally (above), not by individual checkout.
  • Round-2 head 3b7072826d not locally reachable — the "what exactly changed since round 2" delta was reconstructed from the metadata commit list + export-set differencing, not from a direct git diff between the two heads.
  • Live daemon E2E: the spawn seam was driven at buildRuntimeEnvironment level plus the refcount/reporter unit seams; no real qwen serve + session-subprocess round trip (same as rounds 1-2).
  • daemon-worker breadcrumb pin and fast-path case-fold pin on base: both pin behavior this PR does not change (production files untouched this round); verified green on head within the 482 (fast-path suite 86/86, daemon-worker 78/78), not A/B'd.
  • Repo-wide test suite, lint, prettier, typecheck: CI-covered pre-stages (npm ci + npm run build completed at HEAD before this round); only the six affected suites re-run.
  • The PR's explicitly deferred classes (LD_LIBRARY_PATH, PYTHONPATH, residual PATH prefix) — untouched by design, not probed.
  • The refcount contract "concurrent callers must pass the same shared env object" is documented but unenforced; only one production call site exists (process.env); contract-violation interleavings not probed.
  • The one-pass merge of 146c448c was verified through its observable semantics (mutants + refcount cells), not by a two-pass-vs-one-pass equivalence harness; the removed acquire-time clear was defense-in-depth made unreachable by the release-time clear per the commit message — the leak path it covered is now pinned via resetInheritedLoaderEnvScrubForTesting (M2).
  • Base-side fast-path/daemon-worker/run-qwen-serve suites were not run on the base tree (base differs from head only in this PR's files for those suites' inputs; the overlay + A/B cells cover the behavioral delta).
  • process-env-guard.test.ts allowlist re-count (whole: 7 → 5, computed:key entry dropped — matching the one-pass merge removing the launchEnv spread and the restore loop's Object.hasOwn access from run-qwen-serve.ts): green via the suite (3/3); no mutation of the access count performed.

Methodology

Environment: the CI verify container (node:22-bookworm), merge-ref checkout at depth 2 (HEAD=4e259219aa, base HEAD^1=0d56e50b64, head HEAD^2=28352d54ae), npm ci + npm run build pre-run. Five tsx harnesses (harness-denylist.mts 8 cells, harness-refcount.mts, harness-reporter.mts, harness-probes.mts 2 cells, shared harness-lib.mts) import the real TS modules of whichever tree VERIFY_TREE points at; driver.mjs spawns each of the 21 cells in a fresh process under a scratch HOME with all probe keys sanitized out of the inherited env and counts TAP-style ok/not ok lines (raw/driver-run.txt, per-cell logs in raw/). Base side ran from git worktree tmp/base-tree HEAD^1 wired to the root node_modules via symlinks (packages/cli → root, packages/core → head's nested core deps); workspace-link confound check: readlink -f base-tree/packages/cli/node_modules/@qwen-code/qwen-code-core = /__w/qwen-code/qwen-code/packages/core (head tree), and git diff HEAD^1..HEAD over every non-cli workspace plus package.json/package-lock.json is empty, so the control is byte-clean. The base refcount/reporter arms replicate the base per-daemon hunks verbatim (launchEnv snapshot + scrubAndReportInheritedLoaderEnv + restore loop; unconditional setLoaderKeyRejectionReporter(undefined)). Mutations used mutate.mjs (exact-once string replacement, exit 2 otherwise) with git checkout -- restore after each mutant; tree verified clean after the matrix, base overlay files reverted (git status --porcelain empty both trees). Assertion accounting: gates 482 + A/B driver cells 138 (137 pass + 1 docs fail) + base overlay 74 (46 green + 28 expected-reds, each red name attributed as PR-added/PR-extended via base-file grep) + mutation matrix 6 (1 control-green + 5 expected-red-observed) = 700; fail counts only unexpected outcomes — the single fail is the F5 docs assertion. Evidence captures were produced with scripts/verify-capture.mjs (image 02 is a live re-run of the driver under capture and reproduced the identical 137/1 totals; 01/03/04/05/06 re-present the raw run logs/summaries).

Evidence images

01-identity-round3-delta

02-ab-driver-head-vs-base-live

03-mutation-matrix-5-of-5-killed

04-base-overlay-28-reds

05-gates-head-482-green

06-f1-f2-siblings-re-measured

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@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.

LGTM, looks ready to ship. ✅

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

What I found before stopping:
Qwen failed during address-review: status 125.

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31381560750


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@doudouOUC doudouOUC 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.

Reviewed the exact current head (02fcc3b). I revalidated the unresolved Critical threads against the tree rather than relying on GitHub thread state: the previously reported git config/SSH, config-discovery, browser-launch, and CDP command surfaces are now present in the hardcoded project-env gate and covered across the initial load, reload, runtime-env build, and serve fast path. The ref-counted inherited-env scrub also has balanced startup/close cleanup.

The remaining open items are non-blocking Suggestions: reporter diagnostics for overlapping embedded daemons, hardcoded-tier warning/docs accuracy, a few adjacent denylist candidates, and mutation-strength test gaps. This PR has already exceeded the repository review-round budget, so those should stay in the stated follow-up rather than widening this round. Approving with suggestions; CI should still finish green.

@wenshao
wenshao added this pull request to the merge queue Aug 10, 2026
Merged via the queue into main with commit 7c89665 Aug 10, 2026
41 of 43 checks passed
pull Bot pushed a commit to bit-cook/qwen-code that referenced this pull request Aug 10, 2026
…eak (QwenLM#8816)

* feat(ci): A/B deterministic gate rejections against the pre-round ref

A deterministic rejection in the autofix verification gate is only
chargeable to the round if the same check passes without the round's
commit. The gate charged every red to the fix unconditionally, and run
31276008548 measured what that costs when the premise is false: PR
8614's branch predated QwenLM#8693's tsconfig guard while node_modules came
from the post-QwenLM#8693 trusted base, so `npm run build` was equally red at
origin/<branch> — 63 minutes of accepted agent work discarded, an
18-minute repair burned on a failure the repair agent is forbidden to
touch (it may only amend the round's own fix), thirteen rounds in a
row, and the same again on the QwenLM#8616 leg.

On rejection the gate now re-runs the failing check at origin/<branch>
(the branch as pushed, before the round) in the same environment:

- baseline green: today's path exactly — outcome=failed,
  retryable=true, the repair pass gets its chance.
- baseline red too: outcome=failed with preexisting=true and NO
  retryable. The repair step keys on retryable and is skipped — it
  cannot reach a failure outside the round's diff by construction —
  and gate-rejection.md says outright that the branch needs a base
  update (merge main), which flows into the failure comment as-is.

Fail-closed toward today's semantics: any A/B infrastructure problem
(missing ref, checkout failure) charges the fix as before, and a
restore failure after the baseline run rejects outright since the tree
can no longer be trusted. The round's work is still not pushed — this
changes the verdict's honesty and cost, not the push policy.

Tested by executing the real script in a real two-remote git repo with
an npm stub whose failures are keyed by commit SHA: round-caused red
(baseline green), pre-existing red (both red), and the untouched green
path. Mutation-tested, 3 of 3 caught: skipping the A/B, claiming
pre-existing without measuring, and dropping the tree restore.

* Address review: bound the A/B to checks it can honestly compare

All seven findings verified before fixing; the three Criticals were
each a way the A/B compared something other than the check that failed.

R1-1 — the contracts check feeds on stdin, which its first run drains;
the baseline leg re-ran against EOF and checked an empty file list.
R1-3 — the schema check's verdict rides on packages/core/dist, which
the core-rebuild guard built from ROUND sources and which, being
gitignored, survives the detach. Both checks are now A/B-exempt
(run_check_no_ab): their baseline verdicts prove nothing, and their
rejections stay where the repair agent can actually act on them.

R1-2 — a workspace the round ADDS does not exist at the baseline, and
npm exits 1 there with "No workspaces found" (measured; --if-present
forgives a missing script, not a missing workspace) — a round-caused
failure misread as pre-existing, skipping the one repair that can fix
the round's own package. The per-package loop now A/Bs only when the
workspace exists at origin/<branch>.

R1-4 — a chatty PASSING baseline used to flood the tail -c 3000
evidence window and push the actual failure text out of
gate-rejection.md, the sole carrier into the repair feedback, the PR
comment, and the next round's LAST_REJECTION. The baseline transcript
now goes to a side log and only a FAILING tail is merged back, where it
is the evidence.

R1-5 — the pre-existing paragraph pushed gate-rejection.md past the
report's head -c 3500 cap, truncating the closing fence for branch
names past 44 characters. Cap raised to 3900, invariant comment
updated with the new arithmetic.

R1-6 — preexisting=true had no read site. It now flows verify →
Finalize verification → the failure report, whose headline swaps the
generic gate clause for "PRE-EXISTING failure … needs a base update
(merge main)".

R1-7 — the no-round-commit guard was unpinned (deleting it kept all
tests green). Now exercised through the core-rebuild path, the one
A/B-eligible check that runs before the commit gate.

Four new behavioral scenarios (chatty baseline, no-commit round,
A/B-exempt checks, round-added workspace) plus workflow pins for the
forwarding, the clause, and the cap. Mutation-tested, 4 of 4 caught:
schema back to A/B (3 tests), guard dropped, side log reverted,
no-commit guard dropped.

* Address review round 2: A/B only what it can prove, prove what it claims

Ten findings across two rounds, each verified before fixing. The three
deepest share one lesson: the A/B is only sound for a check whose
inputs travel entirely with the git ref, and whose failure it can
IDENTIFY, not merely observe.

R2-1 — rc=1 at both legs does not make them the same failure: the
branch can fail for reason A while the round fails for reason B, and a
baseline infrastructure hiccup is a nonzero exit too. Pre-existing now
requires a MATCHING failure identity — tsc diagnostics normalized to
file + error code (positions shift with the round's edits), compared
via comm(1) on a per-check transcript. No diagnostics on either side
means identity cannot be established and the round stays charged.

R2-2 / R2-7 — gitignored dist survives the detach carrying the ROUND's
build, so any dist-consuming check A/Bs reverted sources against
round-built artifacts: package tests (channel-base resolved through
dist exports) and typecheck (sdk-typescript resolves core's d.ts —
probe-verified three-arm flip). Both are now A/B-exempt, as is lint,
leaving `npm run build` — the incident class, and the one check that
rebuilds its own inputs from the checked-out sources — as the sole A/B
candidate. The workspace-existence guard dissolves with it.

R2-3 — the fixture inherited the caller's global git config; a failing
global pre-commit hook broke all seven cases. The harness now isolates
GIT_CONFIG_GLOBAL/SYSTEM for every git child, and the suite is proven
green under a deliberately hostile hooksPath.

R2-4 — Finalize verification now selects preexisting from the same
attempt whose outcome it selects (repair verification included).

R2-5 / R2-8 — the "merge main" advice is now conditional at both
layers: the script paragraph states the measured fact and hedges the
remedy; the report headline uses the compare the step already ran —
behind/diverged gets the base-update clause, an up-to-date branch is
told its own pre-round code needs attention.

R2-6 — the rejection document now sizes its evidence tail against its
preamble (floor 500 bytes, total under the 3900-byte render cap), so
the closing fence can no longer be truncated off by a long branch name.

R2-9 — dissolved by R2-2: package tests no longer A/B, the guard and
its uncovered positive branch are gone.

R2-10 — the baseline-evidence merge is now pinned: the pre-existing
scenario asserts the baseline leg's own failure line (keyed by its SHA)
reaches gate-rejection.md.

Eight behavioral scenarios; mutation-tested 5 of 5: identity dropped,
typecheck re-enrolled, package tests re-enrolled, evidence merge
dropped, fixed tail restored.

* Address review round 4: sharpen identity, stage the git failures, sync prose

Nine findings, all refinements — the design held, the edges did not.

Identity now keeps the diagnostic MESSAGE (file + code collide: two
unrelated TS2339s in one file compared equal, skipping a repair that
could have shipped — probe-reproduced by the review), and the fixture
emits a SHIFTED position on the baseline leg so the position strip is
load-bearing instead of decorative (deleting the sed survived every
test before; it fails one now). vite/esbuild failures still yield an
empty signature by design — documented as the fail-closed limit rather
than half-widened.

The fail_signature assignments take `|| true`: grep exits 1 on the
normal no-match case and survives errexit today only because the caller
sits in an if-condition — a future unconditional call site would crash
the gate verdict-less.

The restore-failure branch is now stageable and staged: the baseline
leg recreates (untracked) a file the branch tracks, the checkout back
refuses, and the test pins retryable-not-preexisting with the
'could not restore' label. Relaxing the branch to `|| true` fails it.

Prose synced to the mechanisms that replaced it: the render-cap
invariant restates against the dynamic tail budget (the old 3000-based
arithmetic would misguide the next retune), the no-round-commit guard
comment names the core rebuild (schema/contracts left the A/B last
round), the describe wording counts both A/B-eligible builds, and the
pre-existing clauses no longer claim "the repair pass was skipped" —
with REPAIR_PREEXISTING forwarded, repair may have RUN; they now state
the invariant that is true either way: repair may only amend the
round's own fix, so it cannot reach this failure.

Mutation-tested, 3 of 3 caught: position strip dropped, message dropped
from the identity, restore rejection relaxed.

* fix(ci): watchdog silent sandbox hangs and reap the containers they leak

Four autofix rounds have died the same way (QwenLM#8663 twice, QwenLM#8761 r3,
QwenLM#8763 r4): the agent's last output is the sandbox wrapper's
"ContainerName (regular): …" line at docker container entry, then
nothing — not one event — until the 2-hour absolute budget kills the
round. Four different runners, two image versions: systemic, not a bad
machine. Where exactly the container wedges is still unknown (that
needs docker state on the runner); what is certain from the logs is the
shape — a wedged sandbox produces NOTHING, and a legitimate run is
never silent for long (the fleet's longest tolerated quiet is the
review pipeline's 10-minute stream-idle window for thinking phases).

Two mitigations, each aimed at a measured half of the damage:

- run-agent.mjs gains an idle watchdog (QWEN_IDLE_TIMEOUT_MS, default
  20 minutes = 2x that longest legitimate silence): zero output for the
  window kills the agent with a distinct "idle-timeout … the sandbox
  likely hung at startup" detail, so the failure comment names the
  right knob and a hung round costs 20 minutes instead of 120. Polled,
  not reset-per-chunk — a busy stream should not spend its time
  re-arming timers.

- Both sandboxed jobs reap stale qwen-code-* containers at job start:
  a budget kill reaps the HOST-side docker client, not the container,
  so every killed sandbox keeps running on the persistent runner —
  observed directly when a later leg's container-name counter found
  qwen-code-0.21.8-0 already occupied and picked -1. One job per runner
  at a time makes any container alive at job start stale by definition.

Tested by executing the real run-agent.mjs end to end with stub agents:
the hang shape (one line, then silence) dies at the idle window naming
the idle limit, and a slow-but-talking agent that outputs every 400ms
across a 1500ms window survives to a clean exit — the test that
distinguishes a watchdog from a disguised absolute timer. Mutation-
tested, 3 of 3 caught: watchdog disabled, last-output tracking dropped
(the disguised-timer regression), cleanup dropped from a job.

* Address review round 5: the gate's verdict defects and the reaper's live kill

Budget-warning round — the five Criticals from both reviewers, no
suggestions (each deferred with a recorded reply).

fail_signature: `[^\n]*` in an ERE bracket expression does not mean
"rest of line" — in POSIX bracket expressions `\` is literal, so it
matched "neither backslash nor the letter n" and truncated every tsc
message at its first n. Nearly every real message has an early n
("Cannot find name", "is not assignable"), so distinct same-file
failures collapsed into identical signatures and a round-caused failure
could be labeled pre-existing, skipping the repair. grep is
line-oriented: `.*` is exactly the rest of the line. New fixture: two
messages differing only after their first n.

Pre-existing verdict: the intersection test mislabeled in both
directions. A round that ADDS a diagnostic sharing one normalized line
with the baseline was called pre-existing (repair skipped for a
round-caused, repairable failure); and `comm -12 | grep -q` under
`set -eo pipefail` SIGPIPEs comm (exit 141) once the shared output
outruns the pipe buffer, charging true pre-existing failures to the
round — the exact 18-minute repair waste the gate exists to kill.
Pre-existing now means the round's failing set is a SUBSET of the
baseline's, and the difference is captured before testing. New fixture:
a round adding a second diagnostic to a failing baseline.

Restore failure after the baseline leg: was retryable=true with HEAD
still detached at the baseline commit — the repair agent works in that
very checkout and does no git recovery, so its commit would land on the
baseline and be orphaned. Now rejected non-retryable (reject_fix grows
a third arg); the next round starts clean from the trusted checkout.
The restoreClash test pins the new semantics.

Stale-container reap: the premise "a runner runs one job at a time, so
any live qwen-code-* container is stale" holds per runner registration,
but the filter queries the docker daemon, which is per host — and this
pool runs several registrations on one OS. With per-issue/PR
serialization only, a concurrent job's sandbox is a substring match
away from `docker rm -f`. The reap now takes only provably-dead
containers (--filter status=exited/dead, both jobs) and the comment
says why a running one is left alone.

Preamble printf: the `\`` escapes sat inside a single-quoted format
where backslash is literal, so every pre-existing rejection rendered
raw backticks instead of code spans (shellcheck SC2016). Backticks
need no escaping there. Also syncs the side-log comment to the dynamic
tail_budget it actually renders.

Verified: scripts suite 140/140 (was 138; the two new fixtures and the
rewritten restoreClash test all fail against the pre-fix script),
npm run build / typecheck / lint pass, bash -n clean.

* Address review round 6: reap the kill's own orphan, tolerate the reaper

* Address review: hang-bound the reaper, unblock the kill path, pin the unpinned arms

- Wrap every docker call in the stale-container reap with timeout 30: an
  alive-but-wedged daemon blocks docker ps indefinitely, and the existing
  || guards only catch nonzero exits, not hangs (R3-1).
- Make the kill-path container removal async in run-agent.mjs: the
  spawnSync blocked the event loop between SIGTERM and the 10s SIGKILL
  backstop for up to its 30s timeout — in exactly the wedged-daemon
  scenario the watchdog exists for. The main flow awaits the removal so
  the leak warning stays deterministic (R3-6).
- Split the pre-existing gate clause for an empty CMP_R: a transient
  compare-API failure is "never measured", not "measured not-behind", and
  must not assert the branch's own code is at fault (R3-7).
- Swap the timeout breaker's closing remedy to the sandbox investigation
  when every counted timeout was idle, mirroring the round-level split
  (R3-11).
- Tests: pin the budget kill path separately from the idle kill path
  (R3-3), parameterize the idle-window parse guard over -1/0/NaN (R3-5),
  add a stderr-only liveness case (R3-12), pin the strict-subset A/B arm
  via a baseline-superset fixture knob (R3-15), and pin the breaker's
  current-round idle increment (R3-18).

---------

Co-authored-by: verify <verify@local>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.9.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants