Skip to content

fix(autofix): pass CI=true through the gate's env -i launches - #9649

Merged
wenshao merged 10 commits into
QwenLM:mainfrom
wenshao:fix/autofix-gate-ci-allowlist
Aug 22, 2026
Merged

fix(autofix): pass CI=true through the gate's env -i launches#9649
wenshao merged 10 commits into
QwenLM:mainfrom
wenshao:fix/autofix-gate-ci-allowlist

Conversation

@wenshao

@wenshao wenshao commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Restores the runner-provided CI=true to both autofix verification-gate launches. The gate runs the branch's own build/typecheck/lint/test through an env -i clean child whose allowlist covered only 8 variables; CI=true was not among them, so the gate's checks ran with inverted CI semantics relative to the repository's regular CI.

Why it's needed

packages/cli/src/ui/auth/AuthDialog.test.tsx treats process.env['CI'] === 'true' as "TUI-input tests are unreliable here" and skips 18 such tests on CI. Without CI=true in the gate child they un-skip inside the gate, and one of them flakes (~5s vi.waitFor timeout) — reject_fix then fires (retryable) on a fix the PR's own CI passes green, burning the repair pass on an unreachable failure and potentially mislabeling the round's A/B baseline. Reproduced locally: with CI=true the AuthDialog file is green; with CI unset it fails 1/25 on exactly that TUI test. This was deferred from PR #9262 (R5-1) and tracked as issue #9648.

Reviewer Test Plan

How to verify

# The gate allowlists now carry CI in BOTH launches (first pass + repair pass):
grep -n 'CI="${CI:-true}"' .github/workflows/qwen-autofix.yml

# Contract suite (allowlist contents are pinned — a dropped variable now fails):
npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js

The new pin enumerates the full env -i allowlist of both gate launches as a sorted multiset, so a symmetric drop or duplicate of any entry — including CI — fails the suite. The old pin counted env -i occurrences only.

Evidence (Before & After)

N/A (CI machinery). Behavior probe: CI=true npx vitest run packages/cli/src/ui/auth/AuthDialog.test.tsx → file green (18 TUI tests skipped); env -u CI npx vitest run ...1 failed | 24 passed (25), the failing test being the flaky TUI-input one the gate would otherwise trip on.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Contract suite (211 passed / 4 environment-only bash-3.2 mapfile failures, unrelated) + behavior probe above.

Risk & Scope

Linked Issues

Closes #9648 (deferred from PR #9262, R5-1).

中文说明

本 PR 做了什么

把 runner 提供的 CI=true 恢复到 autofix 验证 gate 的两次启动中。gate 通过 env -i 干净子进程运行分支自身的 build/typecheck/lint/test,其允许列表只覆盖 8 个变量;CI=true 不在其中,导致 gate 内的检查以与仓库常规 CI 相反的 CI 语义运行。

为什么需要

packages/cli/src/ui/auth/AuthDialog.test.tsxprocess.env['CI'] === 'true' 视为「TUI 输入测试在此环境不可靠」并在 CI 上跳过 18 个此类测试。gate 子进程里没有 CI=true 时它们被解除跳过,其中一个会 flake(约 5 秒 vi.waitFor 超时)——reject_fix 就会对一个 PR 自身 CI 全绿的修复触发 retryable,为一个不可复现的失败烧掉 repair 通道,并可能误标该轮的 A/B 基线。本地复现:CI=true 时 AuthDialog 文件全绿;去掉 CI 时 1/25 失败,恰好是该 TUI 测试。从 PR #9262(R5-1)延期,跟踪于 issue #9648

审查者测试计划

验证方式:grep -n 'CI="${CI:-true}"' .github/workflows/qwen-autofix.yml(两处启动都有);契约套件 npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js。新 pin 以排序多重集合枚举两次 gate 启动的完整 env -i 允许列表——任何条目的对称删除或重复(包括 CI)都会使套件失败;旧 pin 只数 env -i 出现次数。

证据:N/A(CI 机制)。行为探针:CI=true 时 AuthDialog 全绿(18 个 TUI 测试跳过);env -u CI1 failed | 24 passed (25),失败者正是 gate 会误触发的 flaky TUI 测试。

已在 macOS 上验证契约套件(211 通过 / 4 个无关的 bash-3.2 mapfile 环境失败)与行为探针。

风险与范围

关联 Issue

Closes #9648(从 PR #9262 延期,R5-1)。

The verification gate launches (first pass + repair pass) run the branch's
build/typecheck/lint/test through an env -i clean child that allowlisted
only 8 variables and dropped the runner-provided CI=true. Without it the
gate's checks run with inverted CI semantics relative to the repo's
regular CI: packages/cli/src/ui/auth/AuthDialog.test.tsx skips 18
TUI-input tests on CI as unreliable, and without CI=true they un-skip
inside the gate and one flakes (~5s vi.waitFor) — reject_fix fires
retryable on a fix the PR's own CI passes green, burning the repair pass
and mislabeling the round's A/B baseline.

Add CI="${CI:-true}" to both env -i allowlists (probe: CI=true → file
green; env -u CI → the TUI test fails 1/25), and pin the full allowlist
contents in the contract tests — the old pin counted env -i occurrences
only, so a missing variable shipped green.

Follow-up from PR QwenLM#9262 (R5-1); issue QwenLM#9648.
@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 21, 2026
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run — this pass reviews the current head; the first pass saw 8db672e, before the takeover pushed the review-round hardenings.

Template looks good ✓

Problem: observed, with strong evidence. Issue #9648 tracks it (deferred from #9262 R5-1), and the field evidence in this thread is concrete: four autofix rounds on 2026-08-21 died in the identical shape — agent pass green, gate failing on AuthDialog.test.tsx alone, repair pass spent on a failure it can't reach — and #9340 was terminated with completed work discarded. The mechanism checks out against the tree: both gate launches passed an 8-variable env -i allowlist without CI, the gate script never sets CI itself, and AuthDialog.test.tsx:238 keys its TUI-input skips on process.env['CI'] === 'true'.

Direction: aligned — restores CI-semantic parity between the gate and the repo's regular CI, where CI=true is always present. Nothing speculative.

Size: not applicable — no core package paths (+165/-5 across the workflow file and its contract test; CI machinery).

Approach: the original two-line fix grew through review rounds 2–6: each addition (the structural launch pin, the step-level LD_*/RUNNER_TEMP/WORKDIR/BRANCH pins, the absolute-path digest check) answers a specific recorded [Critical] finding about the new pin being bypassable, and round 7 converged with zero new findings. That is the review loop working as designed, not scope creep — but the PR body still describes only the pre-takeover scope ("the fix only adds the runner-provided variable"), so it's worth syncing before merge.

Risk: no elevated risk signals — no high-risk paths touched.

Moving on to code review. 🔍

中文说明

Re-run——本轮审查当前 head;首轮看到的是 8db672e,早于 takeover 推入的各轮评审加固。

模板完整 ✓

问题:已观测到,证据充分。issue #9648 跟踪(自 #9262 R5-1 延期),本帖中的现场证据具体:2026-08-21 四轮 autofix 形态完全一致——agent 主轮绿、闸门仅挂 AuthDialog.test.tsx、repair 轮消耗在它够不着的失败上——#9340 被终结且已完成的工作被丢弃。机制链条已在代码树中核实:两次 gate 启动的 env -i 允许列表均为 8 个变量且不含 CI;gate 脚本自身从不设置 CIAuthDialog.test.tsx:238process.env['CI'] === 'true' 决定跳过 TUI 输入测试。

方向:对齐——恢复 gate 与仓库常规 CI(始终带 CI=true)的语义一致性,无推测性内容。

规模:不适用——未触及核心包路径(+165/-5,涉及 workflow 文件与其契约测试;CI 机制)。

方案:最初的两行修复在评审第 2–6 轮中扩展:每一处新增(结构化启动 pin、step 级 LD_*/RUNNER_TEMP/WORKDIR/BRANCH pin、绝对路径摘要检查)都对应一条关于新 pin 可被绕过的 [Critical] 记录,第 7 轮已收敛、无新发现。这是评审循环按设计运转,不是范围蔓延——但 PR 描述仍只描述 takeover 之前的范围("只补充 runner 提供的变量"),合并前值得同步。

风险:无升级风险信号——未触及高风险路径。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 001d1cc28e42832c8e5519fee55e4c53848e5742 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run — re-reviewed the full +165/-5 diff at the current head, not just the delta the first pass saw.

Code review

No blockers. My independent proposal for the stated problem — pass the runner's CI through both env -i gate allowlists with a default, and upgrade the contract pin so a dropped variable fails the suite — matches the core of this PR exactly. The rest of the diff is the review loop's own output from rounds 2–6, and I verified each piece at this head:

  • Both gate steps (Verification gate, Repair verification gate) are symmetric: CI="${CI:-true}" sits in both allowlists, in the neighboring KISS_AUDIT/FOOTPRINT_ENFORCE default style.
  • The new step-level env pins carry no semantic drift: WORKDIR and BRANCH match the job-level values byte-for-byte, and RUNNER_TEMP: '${{ runner.temp }}' restates the runner-provided value — they re-pin from trusted expression context rather than change anything.
  • The digest check's move to /usr/bin/echo … | /usr/bin/sha256sum is valid on the ubuntu runners (both coreutils) and closes the BASH_FUNC_echo%% shadowing shape.
  • The new structural pin is sound: the adjacency-chain regex matches the workflow text verbatim (separator is backslash-newline plus bash whitespace only), the statement-list comparison slices after run: |- with an ASCII-only strip so NBSP-smuggled lines stay visible (self-test included), and the one-bash --norc-per-step count closes the demoted-arm shape. I traced the expected statement list against both run bodies line by line — exact match.
  • The lowercase broadening of the upsert pin's assignment grammar re-syncs the two duplicated token sites (the drift round 2 flagged); behavior-neutral on the current all-uppercase text, but closes a lowercase-name bypass.
  • Causal chain end-to-end, verified in-tree: AuthDialog.test.tsx:238 skips TUI-input tests on CI === 'true' → without CI in the gate child they un-skip on Linux → one flakes → reject_fix burns the repair pass on a failure the branch can't fix.

Non-blocking notes, all already on the round-7 review ledger as deferred: (1) CI rides the allowlist without the step-level pin its siblings got — a $GITHUB_ENV plant of CI=false would re-invert the semantics for that round (worst case: the flake this PR fixes recurs; fail-closed direction, no forged verdict); (2) the step-level env pins are asserted with bare toContain in the round-3 block, so a commented-out entry survives; (3) the PR body's Risk & Scope still describes the pre-takeover diff — see Stage 1.

Test evidence

This pass carries the PR's own CI signal on the reviewed commit, fetched via API (PR code is never executed in triage). Everything completed, nothing red: Test (ubuntu-latest, Node 22.x) — which runs npm run test:ci including the contract suite this PR extends — is green, so the new pins pass on the commit they pin; both security checks are green; the macOS/Windows test matrix and CLI-integration entries were skipped by the classifier (workflow-only change).

What PR CI cannot exercise is the gate itself end-to-end — it only runs during real autofix rounds. The sandboxed verification lane triggered alongside this triage (run 32551085388, in progress) covers that gap and will post its own report here; my attestation rests on the statically verified mechanism chain plus the green CI above, and the failure shape itself is documented by four production rounds in the thread, not by author claim.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Dependency CVE audit ✅ success
Secret scan (TruffleHog) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
route ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Real-scenario testing (local run): N/A — CI machinery with no user-visible surface.

中文说明

代码审查

无阻塞项。我对该问题的独立方案——把 runner 的 CI 以默认值形式加入两个 env -i gate 允许列表,并升级契约 pin 使删除变量会失败——与本 PR 的核心完全一致。diff 的其余部分是第 2–6 轮评审循环的产物,均已在本 head 上逐项核实:

  • 两次 gate 启动(Verification gateRepair verification gate)对称:CI="${CI:-true}" 均在允许列表中,风格与相邻的 KISS_AUDIT/FOOTPRINT_ENFORCE 一致。
  • 新增 step 级 env pin 无语义漂移:WORKDIR/BRANCH 与 job 级值逐字节一致,RUNNER_TEMP: '${{ runner.temp }}' 复述 runner 提供的值——只是从可信表达式上下文重新钉住,未改变任何值。
  • 摘要检查改用 /usr/bin/echo … | /usr/bin/sha256sum 在 ubuntu runner 上有效(均为 coreutils),封堵 BASH_FUNC_echo%% 影子形态。
  • 新结构化 pin 可靠:邻接链正则与 workflow 文本逐字匹配(分隔符仅为反斜杠换行加 bash 空白),语句列表比较在 run: |- 之后切片且仅剥离 ASCII 空白,NBSP 走私行保持可见(含自测),每步恰好一个 bash --norc 的计数封堵降级到从不执行分支的形态。预期语句列表已与两个 run body 逐行比对——完全一致。
  • upsert pin 赋值语法放宽到小写,是重新同步两处重复的 token 站点(第 2 轮指出的漂移);对当前全大写文本行为中性,但封堵小写变量名绕过。
  • 因果链端到端在树中核实:AuthDialog.test.tsx:238CI === 'true' 跳过 TUI 输入测试 → gate 子进程缺 CI 时这些测试在 Linux 上解除跳过 → 其中一个 flake → reject_fix 为分支无法修复的失败烧掉 repair 轮。

非阻塞项(均已在第 7 轮评审台账中记录为延后):(1) CI 未获得兄弟变量那样的 step 级 pin——$GITHUB_ENV 植入 CI=false 会在该轮重新反转语义(最坏情况:本 PR 修复的 flake 复现;方向上失败关闭,不会伪造裁决);(2) round-3 块中 step 级 env pin 仅以裸 toContain 断言,注释掉的条目可存活;(3) PR 描述的 Risk & Scope 仍描述 takeover 之前的 diff——见 Stage 1。

测试证据

本轮携带被审 commit 的 PR 自身 CI 信号(经 API 获取——triage 从不执行 PR 代码)。全部完成、无红项:运行 npm run test:ci(含本 PR 扩展的契约套件)的 Test (ubuntu-latest, Node 22.x) 为绿,即新 pin 在其所钉的 commit 上通过;两项安全检查为绿;macOS/Windows 测试矩阵与 CLI 集成项被分类器跳过(仅 workflow 改动)。

PR CI 无法端到端运行 gate 本身——它只在真实 autofix 轮次中运行。随本次 triage 触发的沙箱验证通道(run 32551085388,进行中)覆盖该缺口,会在本帖发布自己的报告;我的背书建立在上述静态核实的机制链与绿色 CI 之上,失败形态本身由本帖中四个生产轮次记录,而非作者单方声明。

真实场景测试(本地运行):N/A——CI 机制,无用户可见面。

Qwen Code · qwen3.8-max

Reviewed at 001d1cc28e42832c8e5519fee55e4c53848e5742 · re-run with @qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — solid across every stage; the reservations are real but non-blocking, and two of them already sit on the review ledger's deferred list.

Stepping back: this started as a two-line fix for a reproduced, production-costing bug — four dead autofix rounds and one terminated PR with discarded work are documented in the thread — and at its core it still is exactly that fix. The surrounding growth happened to the PR, not because of it: the review loop spent five rounds attacking the new contract pin, and the diff absorbed every surviving finding (structural launch pin, step-level env pins, absolute-path digest check), converging at round 7 with zero new ones. Reviewing the result cold, the hardenings hold up — both gate steps symmetric, pinned values match the job-level ones, the structural pin matches the workflow text verbatim — CI is fully green on the reviewed commit including the contract suite, and the causal chain is verified end-to-end in the tree. If I had to maintain this in six months, the dense pin comments are exactly the documentation I'd want at 3 a.m.

Why 4 and not 5, named plainly: (1) the PR body still describes the pre-takeover scope — "only adds the runner-provided variable" no longer matches a +165/-5 diff; (2) CI rides the allowlist without the step-level pin its siblings got, so a planted CI=false could re-invert semantics for one round (fail-closed, worst case is the flake recurring); (3) the step-level env pins are asserted with bare toContain, so a commented-out entry survives. All three are tracked, none blocks this fix — but a follow-up to sync the description and close (2)/(3) is worth filing.

CI is fully settled on this head — no pending pull_request runs — so no deferred-approval marker; approving now, pinned to the reviewed commit. Process note for the maintainer: the standing CHANGES_REQUESTED state comes from the review loop's own intermediate rounds (3/4/6) against earlier commits; round 7 converged on this head with zero findings, and this approval supersedes the bot's earlier stance.

中文说明

置信度:4/5 —— 各阶段均扎实;保留意见真实存在但都不阻塞,其中两条已在评审台账的延后清单上。

退后一步看:这最初是一个两行修复,针对的是已复现、在生产上持续付出代价的 bug——帖中记录了四个死掉的 autofix 轮次和一个被终结、已完成工作遭丢弃的 PR——其核心至今仍是那个修复。外围的扩展是评审循环作用于 PR 的结果,而非作者主动扩权:五轮评审反复攻击新契约 pin,diff 吸收了全部幸存发现(结构化启动 pin、step 级 env pin、绝对路径摘要检查),第 7 轮以零新发现收敛。冷审最终结果:加固站得住——两次 gate 启动对称、钉住的值与 job 级一致、结构化 pin 与 workflow 文本逐字吻合——被审 commit 上 CI 全绿(含契约套件),因果链已在树中端到端核实。若六个月后由我维护,这些密集的 pin 注释正是凌晨三点想要的文档。

为什么是 4 而不是 5,直说:(1) PR 描述仍停留在 takeover 之前的范围——"只补充 runner 提供的变量"已不匹配 +165/-5 的 diff;(2) CI 未获得兄弟变量那样的 step 级 pin,植入 CI=false 可在单轮内重新反转语义(失败关闭,最坏情况是 flake 复现);(3) step 级 env pin 仅以裸 toContain 断言,注释掉的条目可存活。三者均已被跟踪,都不阻塞本修复——但值得开一个后续 issue 同步描述并收掉 (2)/(3)。

CI 已在本 head 上完全收敛——无进行中的 pull_request 运行——因此不需要延迟批准标记;现在批准,钉在被审 commit 上。给维护者的流程说明:当前 CHANGES_REQUESTED 状态来自评审循环自身的中间轮次(第 3/4/6 轮,针对更早的 commit);第 7 轮已在本 head 上以零发现收敛,本次批准取代机器人早前的立场。

Qwen Code · qwen3.8-max

Reviewed at 001d1cc28e42832c8e5519fee55e4c53848e5742 · re-run with @qwen-code /triage

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship — CI landed green after the review. ✅

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

Not explored to full depth (tool budget reached): "agent 1a": none (no check was cut short; the only substituted check — a true vitest execution — was replaced by the equivalent standalone replication above, which exercise…; "agent 3a": none — but note the vitest run of the actual test ( npm run test:scripts ) could not be executed here because the review worktree has no installed dependencies;….

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

中文说明

已审查。 建议见行内评论。

未探索到全部深度(达到工具调用预算):"agent 1a"none (no check was cut short; the only substituted check — a true vitest execution — was replaced by the equivalent standalone replication above, which exercise…"agent 3a"none — but note the vitest run of the actual test ( npm run test:scripts ) could not be executed here because the review worktree has no installed dependencies;…

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

Comment on lines +11199 to +11201
const passed = (
argList.match(/[A-Z_][A-Z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? []
)

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 allowlist pin's regex matches uppercase-only variable names ([A-Z_][A-Z0-9_]*), but the comment above claims it enumerates "the full allowlist of BOTH gate launches" — a lowercase or mixed-case entry is invisible to the sorted-multiset check.

This matters because lowercase npm_config_* is npm's own configuration convention, and the gate child runs npm build/test: if a future edit (or an autofix repair round touching this workflow) adds e.g. npm_config_registry="https://evil.example" \ to one gate launch, this pin stays green. A later change that drops that entry from one launch only then produces exactly the asymmetric-allowlist regression this test was written to catch — first-pass and repair-pass gates running with different environments — with CI green the whole time.

Verified by probe at this commit: injecting npm_config_registry="https://evil.example" \ into the FIRST gate launch only leaves the whole file green (215/215); widening the name class to [A-Za-z_][A-Za-z0-9_]* makes the same mutant fail with + "npm_config_registry" in the assertion diff (and stays green on the pristine workflow).

Suggested change
const passed = (
argList.match(/[A-Z_][A-Z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? []
)
const passed = (
argList.match(/[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? []
)
中文说明

新增允许列表 pin 的正则只匹配大写变量名([A-Z_][A-Z0-9_]*),但上方注释声称它枚举了「两次 gate 启动的完整允许列表」——小写或混合大小写的条目对排序多重集合检查不可见。

这之所以重要:小写 npm_config_* 是 npm 自身的配置约定,而 gate 子进程会运行 npm build/test。若未来某次编辑(或触及此 workflow 的 autofix repair 轮次)向其中一个 gate 启动添加例如 npm_config_registry="https://evil.example" \,该 pin 仍为绿色;之后若某次变更只从其中一个启动删除该条目,就会产生本测试要捕获的允许列表不对称回归——首过与修复过 gate 以不同环境运行——而 CI 全程全绿。

已在本提交上通过探针验证:仅向第一个 gate 启动注入 npm_config_registry="https://evil.example" \,整个文件仍全绿(215/215);将名称类放宽为 [A-Za-z_][A-Za-z0-9_]* 后,同一突变体在断言 diff 中以 + "npm_config_registry" 失败(在原始 workflow 上保持全绿)。

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

@wenshao

wenshao commented Aug 21, 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 21, 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. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

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

Copy link
Copy Markdown
Collaborator

AutoFix round 7 finishedview run. See this round's report below.

中文说明

AutoFix 第 7 轮已完成 —— 查看运行。本轮报告见下方。

The allowlist pins extract passed variables with [A-Z_][A-Z0-9_]*, so a
lowercase or mixed-case entry — e.g. npm's own npm_config_* convention
— is invisible to the sorted-multiset check: adding one to a single
launch ships green, and only a later asymmetric drop then fails,
producing exactly the divergent-environment regression the pins exist
to catch while CI stayed green the whole way.

Widen the name class to [A-Za-z_][A-Za-z0-9_]* in both pins — the gate
launches pin added in 8db672e and the sibling run_deferred_upsert
pin that shares the identical regex and blind spot (probe: inject
npm_config_registry="..." into one launch → old regex 215/215 green,
widened regex fails with + "npm_config_registry" at each pin; pristine
workflow stays green).

Review round 1 finding R1-1.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Address-review round summary — PR #9649

Feedback points

[rc:3828417467] [Suggestion] Allowlist pin regex matches uppercase-only variable names

Classification: Optional (Suggestion) — a checkable defect claim about the pin this PR added, verified by reproduction before implementing.

Reproduction (before fix): injected npm_config_registry="https://evil.example" \ into the FIRST gate launch of .github/workflows/qwen-autofix.yml and ran the test file: 215/215 green — the lowercase entry was invisible to the sorted-multiset check because the extraction regex [A-Z_][A-Z0-9_]* only matches uppercase names. Exactly the asymmetric-allowlist regression class the pin exists to catch, shipping green.

Fix: widened the variable-name class to [A-Za-z_][A-Za-z0-9_]* — a one-token change, exactly the reviewer's suggested edit — applied in two places in scripts/tests/qwen-autofix-workflow.test.js:

  1. the gate-launch allowlist pin this PR added (line ~11200), and
  2. the sibling run_deferred_upsert clean-child pin (line ~12809), which uses the identical regex with the identical verified blind spot. It lives in the same file (inside this PR's footprint), and leaving it would re-raise the same finding class in a later round.

No comment or expectation-list changes were needed: the expected allowlists are all-uppercase and unchanged; the widened regex only stops missing non-uppercase entries.

Mutation probes (witnesses for the widened guards):

  • Probe B1: re-injected the same mutant into the first gate launch → the pin now fails with + "npm_config_registry" in the assertion diff at line 11204. Mutant reverted → 215/215 green (no false positive on the pristine workflow).
  • Probe B2: injected the same mutant into the first run_deferred_upsert launch → the sibling pin fails with + "npm_config_registry" at line 12814. Mutant reverted → green.

Disposition: resolved in code (commit 0fcdd5afbe).

Other items

  • No failed checks, no still-red checks, no conflict (--conflict false), no deferred non-Critical section, no growth-audit section. Net diff growth this round: 0 source / 0 test lines (2 test lines changed in place).
  • The review body's two "not explored to full depth" notes and the actionlint limitation are tool-budget/tool-capability disclosures, not findings — no action.

Environment repair (affects verification runs)

The first npm run build / npm run typecheck runs failed for a reason unrelated to this change: node_modules had drifted from the committed lockfile — @opentelemetry/api-logs/sdk-logs were installed at 0.221.0 while package-lock.json pins 0.203.0 (and packages/core declares ^0.203.0, which 0.221.0 does not satisfy). In 0.221.0 LogRecordExporter requires forceFlush, so FileLogExporter in packages/core failed to compile, and the aborted build left packages/acp-bridge/dist stale, cascading into the cli typecheck errors. Repaired with npm ci --cache /tmp/npm-cache-9649 (the default ~/.npm cache is root-owned on this runner → EACCES; the --cache flag bypasses it without touching system state). No tracked files were modified by the repair. All checks below were run on the repaired tree.

Verification

  • npm ci --cache /tmp/npm-cache-9649 — exit 0 (environment repair only; restores node_modules to the committed lockfile, runs the prepare build)
  • npm run build — passed (exit 0; failed only before the environment repair above)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js — 215/215 tests passed (vitest exits 1 solely due to a pre-existing internal [vitest-worker]: Timeout calling "onTaskUpdate" unhandled error, present identically in the pre-edit baseline run on this runner; unrelated to the change)
  • Mutation probe A (pre-fix reproduction): lowercase mutant + old regex → 215/215 green (defect confirmed)
  • Mutation probe B1 (post-fix): same mutant + widened pin → fails at line 11204 with + "npm_config_registry"; mutant reverted → 215/215 green
  • Mutation probe B2 (post-fix): mutant in a run_deferred_upsert launch + widened sibling pin → fails at line 12814 with + "npm_config_registry"; mutant reverted → 215/215 green
中文说明

处理评审轮次总结 — PR #9649

反馈点

[rc:3828417467] [Suggestion] 允许列表 pin 的正则只匹配大写变量名

分类: Optional(建议)——针对本 PR 新增 pin 的可检验缺陷声明,在实施前先复现验证。

复现(修复前):.github/workflows/qwen-autofix.yml 的第一个 gate 启动注入 npm_config_registry="https://evil.example" \,然后运行测试文件:215/215 全绿——小写条目对排序多重集合检查不可见,因为提取正则 [A-Z_][A-Z0-9_]* 只匹配大写名称。这正是该 pin 要捕获的允许列表不对称回归类别,却全程绿灯通过。

修复: 将变量名字符类放宽为 [A-Za-z_][A-Za-z0-9_]*——单一 token 的改动,与评审者建议的编辑完全一致——应用于 scripts/tests/qwen-autofix-workflow.test.js 的两处:

  1. 本 PR 新增的 gate 启动允许列表 pin(约第 11200 行),以及
  2. 同类的 run_deferred_upsert 清洁子进程 pin(约第 12809 行),它使用完全相同的正则、存在完全相同的已验证盲点。它位于同一文件内(在本 PR 的足迹范围内),若不修复,后续轮次会再次出现同类发现。

无需修改注释或期望列表:期望的允许列表全为大写且保持不变;放宽后的正则只是不再漏掉非大写条目。

突变探针(放宽后守卫的见证):

  • 探针 B1:将同一突变体重新注入第一个 gate 启动 → 该 pin 现在失败,断言 diff 中在第 11204 行出现 + "npm_config_registry"。还原突变体 → 215/215 全绿(对原始 workflow 无误报)。
  • 探针 B2:将同一突变体注入第一个 run_deferred_upsert 启动 → 同类 pin 失败,在第 12814 行出现 + "npm_config_registry"。还原突变体 → 全绿。

处置: 已在代码中解决(提交 0fcdd5afbe)。

其他事项

  • 无失败检查、无持续红色检查、无冲突(--conflict false)、无「已延期的非 Critical 反馈」部分、无 growth-audit 部分。本轮净 diff 增长:源码 0 行 / 测试 0 行(原地修改了 2 行测试代码)。
  • 评审正文中两条「未探索到全部深度」的说明和 actionlint 的限制属于工具预算/工具能力的披露,不是发现——无需处理。

环境修复(影响验证运行)

首次 npm run build / npm run typecheck 失败,原因与本改动无关:node_modules 与已提交的 lockfile 发生了漂移——@opentelemetry/api-logs/sdk-logs 安装成了 0.221.0,而 package-lock.json 锁定的是 0.203.0(且 packages/core 声明的是 ^0.203.0,0.221.0 并不满足该范围)。在 0.221.0 中 LogRecordExporter 要求 forceFlush,因此 packages/core 中的 FileLogExporter 编译失败;中断的构建又使 packages/acp-bridge/dist 过期,级联导致 cli 的 typecheck 报错。已用 npm ci --cache /tmp/npm-cache-9649 修复(本 runner 上默认的 ~/.npm 缓存目录为 root 所有 → EACCES;--cache 参数绕开它且不触碰系统状态)。修复未改动任何被跟踪文件。以下所有检查均在修复后的环境上运行。

验证

  • npm ci --cache /tmp/npm-cache-9649 — exit 0(仅环境修复;将 node_modules 恢复到已提交 lockfile 的状态,并执行 prepare 构建)
  • npm run build — 通过(exit 0;仅在上述环境修复前失败过)
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js — 215/215 测试通过(vitest 以 exit 1 结束仅因一个预先存在的内部 [vitest-worker]: Timeout calling "onTaskUpdate" 未处理错误,在改动前的基线运行中同样存在于本 runner;与本改动无关)
  • 突变探针 A(修复前复现):小写突变体 + 旧正则 → 215/215 全绿(确认缺陷存在)
  • 突变探针 B1(修复后):同一突变体 + 放宽后的 pin → 在第 11204 行以 + "npm_config_registry" 失败;还原突变体 → 215/215 全绿
  • 突变探针 B2(修复后):run_deferred_upsert 启动中的突变体 + 放宽后的同类 pin → 在第 12814 行以 + "npm_config_registry" 失败;还原突变体 → 215/215 全绿

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.

Partially reviewed — gaps disclosed.

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

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

  • scripts/tests/qwen-autofix-workflow.test.js:11196 — [probe] No pin covers the gate launches' LD_PRELOAD/LD_AUDIT/LD_LIBRARY_PATH command-prefix strip
中文说明

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

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

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

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

Comment on lines +11198 to +11199
const argList = step.slice(argStart, step.indexOf('bash --norc'));
const passed = (

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 new gateAllowlist pin enumerates text tokens, not the shell command structure the runner actually executes. Five distinct shell-level mutations — each probe-verified to keep all 215 contract pins green — silently defeat the gate's clean-child isolation:

  1. Window truncation / smuggling — the slice ends at the FIRST textual bash --norc. Moving an entry whose value is nowhere pinned by value (e.g. BRANCH) last with the value "bash --norc" truncates the enumeration window, so an entry appended after it passes undetected while env still parses it. Observed at runtime: CHILD_SAW_TOKEN=smuggled-v, and a smuggled BASH_ENV sourced attacker code inside the child before the digest-verified runner started (BASH_ENV_SCRIPT_EXECUTED=yes).
  2. Commented-out entry# CI="${CI:-true}" \ still satisfies both the multiset and the containment pin, but at runtime the # breaks the continuation chain and the launch splits: the gate script runs as a second command inheriting the parent step's FULL environment — the $GITHUB_ENV plant class the launch's own comment says env -i exists to drop. Observed: PLANT_VERDICT=[evil_verdict] in the child (baseline <unset>), with the runner even inheriting CI=true from the parent, masking the very symptom this pin was written for.
  3. Dropped continuation — removing one trailing \ relocates every later assignment out of the clean child the same way; this pin and all other launch pins stay green while the child inherits the parent environment (observed as above).
  4. =-less operand hijack — inserting one =-less operand (e.g. "${PLANT_CMD}") before bash --norc is invisible to the NAME=value grammar, yet env(1) execs exactly that operand as the command word, demoting the digest-verified gate script to argv. Observed: /usr/bin/env -i A=1 /bin/echo HIJACKED-CMD bash --norc script prints HIJACKED-CMD bash --norc script. The hijacked word still receives the allowlisted GITHUB_OUTPUT, so it writes the verdict that decides the PAT push.
  5. Suffix after the closing quoteCI="${CI:-true}"x passes both checks while bash concatenates the suffix into the value, so the child sees CI=truex; process.env['CI'] === 'true' then becomes false in the gate, the 18 deliberately-skipped TUI tests un-skip and one flakes — the exact inversion this PR exists to prevent.

The entry space — shell edits that preserve the matched token text — is unbounded: a new bypass shape was found in each of four successive audit rounds, so patching shape by shape will not close it. Pin the launch structurally instead: one verbatim adjacency regex over the whole launch block (launch head, every assignment in order with exact values and trailing \ continuations, through bash --norc), mirroring the sibling upsert pin's LD_* adjacency regex — probe-verified red on shapes 2 and 3; a residue-exclusivity check (strip the matched NAME=value tokens and expect the remainder to equal /usr/bin/env -i) — probe-verified red on shape 4 and also catches in-window comments; the CI token pinned WITH its trailing delimiter (CI="${CI:-true}" \) — probe-verified red on shape 5, where a bare exact-token check is not enough because the suffix sits outside the regex match; and terminator uniqueness (expect((step.match(/bash --norc/g) ?? []).length).toBe(1)) or slicing to the full launch token — closes shape 1.

Witness:

Shape 1: mutant suite Tests 215 passed (215); runtime CHILD_SAW_TOKEN=smuggled-v, BASH_ENV_SCRIPT_EXECUTED=yes
Shapes 2/3: mutant suites green; BASE PLANT_VERDICT=[<unset>] -> MUTANT GATE RUNNER PLANT_VERDICT=[evil_verdict], PATH=<parent>, exit 0
Shape 4: Tests 215 passed (215) on "${PLANT_CMD}" mutant; env -i A=1 /bin/echo HIJACKED-CMD bash --norc s -> HIJACKED-CMD bash --norc s
Shape 5: mutant lane green; MUTANT child CI=[truex] vs BASELINE child CI=[true]
中文说明

新的 gateAllowlist pin 枚举的是文本 token,而不是 runner 实际执行的 shell 命令结构。五种不同的 shell 层面改动——每一种都经探针验证可让全部 215 个契约 pin 保持绿色——会悄无声息地击穿 gate 的干净子进程隔离:

  1. 窗口截断/走私——切片在第一个文本 bash --norc 处结束。把一个值未被任何 pin 锁定的条目(如 BRANCH)移到最后并赋值为 "bash --norc",枚举窗口即被截断,其后追加的条目不会被 pin 看到,但 env 仍会解析它。运行时观测到:CHILD_SAW_TOKEN=smuggled-v;走私的 BASH_ENV 在 digest 校验过的 runner 启动之前就在子进程内 source 了攻击者代码(BASH_ENV_SCRIPT_EXECUTED=yes)。
  2. 注释掉的条目——# CI="${CI:-true}" \ 同时满足多重集与包含检查,但运行时 # 会打断续行链、把启动拆成两条命令:gate 脚本作为第二条命令运行,继承父步骤的完整环境——正是启动自身注释所说 env -i 要丢弃的 $GITHUB_ENV 植入类。观测到子进程中 PLANT_VERDICT=[evil_verdict](基线为 <unset>),runner 甚至从父环境继承 CI=true,把这个 pin 要防的症状也掩盖了。
  3. 丢失续行符——删掉一个行尾 \ 会以同样方式把其后所有赋值移出干净子进程;该 pin 与其他所有启动 pin 保持绿色,子进程继承父环境(观测同上)。
  4. = 操作数劫持——在 bash --norc 之前插入一个无 = 操作数(如 "${PLANT_CMD}")对 NAME=value 语法不可见,但 env(1) 恰恰会把该操作数作为命令 exec,digest 校验过的 gate 脚本被降级为 argv。观测:/usr/bin/env -i A=1 /bin/echo HIJACKED-CMD bash --norc script 输出 HIJACKED-CMD bash --norc script。被劫持的命令仍能拿到允许列表中的 GITHUB_OUTPUT,从而写入决定 PAT push 的 verdict。
  5. 闭引号后的后缀——CI="${CI:-true}"x 能同时通过两项检查,而 bash 会把后缀拼进值里,子进程得到 CI=truex;gate 内 process.env['CI'] === 'true' 变为 false,18 个被刻意跳过的 TUI 测试解除跳过并出现 flake——正是本 PR 要防止的语义反转。

入口空间——保持被匹配 token 文本不变的 shell 编辑——是无界的:连续四轮审计每轮都发现新的绕过形态,逐个形态打补丁无法收敛。请改为结构性地钉住整个启动块:对整个启动块使用一条逐字邻接正则(启动头、按序的每条赋值及其精确值与行尾 \ 续行,直到 bash --norc),镜像 sibling upsert pin 的 LD_* 邻接正则——探针验证对形态 2、3 变红;残差排他检查(剥掉匹配到的 NAME=value token 后,剩余部分必须恰好等于 /usr/bin/env -i)——探针验证对形态 4 变红,且能捕获窗口内注释;CI token 连同其后分隔符一起钉住(CI="${CI:-true}" \)——探针验证对形态 5 变红(仅精确 token 检查不够:后缀位于正则匹配之外);钉住终止符唯一性(expect((step.match(/bash --norc/g) ?? []).length).toBe(1))或切片到完整启动 token——关闭形态 1。

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

Comment on lines +11202 to +11203
.map((m) => m.split('=')[0])
.sort();

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 multiset compares variable NAMES only, so a symmetric value swap on any of the six non-CI entries is invisible to this pin, while the sibling upsert pin pins every entry by exact token. Probe-verified: swapping PATH="${TRUSTED_PATH}" to PATH="${PATH}" in both gate launches keeps all 215 pins green. The bare swap is masked at runtime by the gate body's preceding export PATH="${TRUSTED_PATH}" — but that export line is itself unpinned for the gate steps: the compound mutant (swap + export-line deletion in both gates) is also green, and the host PATH then serves both the pre-launch digest check (sha256sum -c) and the gate child, so host-planted binaries win both silently.

After the multiset check, mirror the sibling's exact-token loop for the six non-CI entries — e.g. for (const entry of ['PATH="${TRUSTED_PATH}"', 'HOME="${HOME}"', 'RUNNER_TEMP="${RUNNER_TEMP}"', 'WORKDIR="${WORKDIR}"', 'BRANCH="${BRANCH}"', 'GITHUB_OUTPUT="${GITHUB_OUTPUT}"']) expect(argList).toContain(entry); — and pin the gate body's export PATH="${TRUSTED_PATH}" line to close the compound shape.

Witness: PATH-swap mutant and swap+export-deletion compound both Tests 215 passed (215); with the exact-token loop the compound fails: expected '/usr/bin/env -i …' to contain 'PATH="${TRUSTED_PATH}"'.

中文说明

多重集只比较变量,因此对六个非 CI 条目中任意一个做对称的替换,该 pin 都不可见;而 sibling upsert pin 对每个条目都按精确 token 锁定。探针验证:在两次 gate 启动中把 PATH="${TRUSTED_PATH}" 换成 PATH="${PATH}",全部 215 个 pin 保持绿色。裸替换在运行时会被 gate 主体中先行的 export PATH="${TRUSTED_PATH}" 掩盖——但该 export 行在 gate 步骤中同样没有被 pin:复合改动(值替换 + 删除两个 gate 的 export 行)同样全绿,随后宿主 PATH 会同时服务于启动前的 digest 检查(sha256sum -c)与 gate 子进程,宿主植入的二进制可以悄无声息地同时赢下两者。

建议:在多重集检查之后,仿照 sibling 的精确 token 循环锁定六个非 CI 条目——例如 for (const entry of ['PATH="${TRUSTED_PATH}"', 'HOME="${HOME}"', 'RUNNER_TEMP="${RUNNER_TEMP}"', 'WORKDIR="${WORKDIR}"', 'BRANCH="${BRANCH}"', 'GITHUB_OUTPUT="${GITHUB_OUTPUT}"']) expect(argList).toContain(entry);——并把 gate 主体的 export PATH="${TRUSTED_PATH}" 行也 pin 上,以关闭复合形态。

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

expect(argStart, 'gate step lacks the env -i launch').toBeGreaterThan(-1);
const argList = step.slice(argStart, step.indexOf('bash --norc'));
const passed = (
argList.match(/[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? []

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 env-assignment token grammar is now duplicated byte-for-byte at two sites — here and in the upsert clean-child pin (~line 12809) — and this diff's second hunk exists solely to re-sync the two copies after they drifted apart (the older copy stayed uppercase-only until this delta aligned it; that stale form failed OPEN, hiding lowercase-named entries). If the grammar ever needs a new alternative (e.g. single-quoted values) and only one site is updated, the stale regex silently under-matches, the extracted multiset still equals the sanctioned list, and an unsanctioned environment widening ships green — exactly the mutation class these pins exist to catch.

Hoist a shared module-scope helper (matching this file's existing helper convention) and call it from both pins; anchors, expected lists, and per-site token pins stay local:

const envTokenNames = (argList) =>
  (argList.match(/[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? []).map(
    (m) => m.split('=')[0],
  );
中文说明

环境赋值 token 语法现在在两个位置逐字节重复——此处与 upsert 干净子进程 pin(约第 12809 行)——而本 diff 第二个 hunk 存在的唯一原因,就是在两份拷贝漂移之后重新同步它们(较旧的一份在本 delta 对齐之前一直只匹配大写;那个陈旧形态是失效开放的,会隐藏小写命名的条目)。如果该语法未来需要新的分支(例如单引号值)而只更新了一处,陈旧的正则会静默漏匹配,提取出的多重集仍等于许可列表,未经认可的环境放宽就会绿灯通过——恰恰是这些 pin 要捕获的改动类别。

建议提升为模块级共享辅助函数(与本文件既有的辅助函数约定一致),两个 pin 都调用它;锚点、期望列表与逐位置的 token pin 保持局部:

const envTokenNames = (argList) =>
  (argList.match(/[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? []).map(
    (m) => m.split('=')[0],
  );

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

@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:
Autofix agent finished without required output file(s): address-summary.md, no-action.md.

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

中文说明

🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 main,因此已通过 update-branch 合入当前 main,并将在下次扫描时重试。过期的 base(main 已改动的依赖或符号)可能让构建失败而并非修复本身的错;若 base 更新后仍然失败,将移交人工处理。

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


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

@qqqys

qqqys commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Field evidence for this fix, from four autofix rounds today (2026-08-21). The flake described here is not hypothetical — it terminated one takeover PR and has started on a second.

Every one of the four runs has the identical shape: the agent's own pass succeeds, the verification gate then fails on AuthDialog.test.tsx alone, the repair pass is spent on a failure it cannot reach, and the round dies at the 18-minute budget.

#9340 — three consecutive rounds, then terminated

run agent pass gate rejection outcome
32469529030 10:44:11Z Autofix agent completed address-review successfully. 10:52:16Z ❯ vi.waitFor.timeout src/ui/auth/AuthDialog.test.tsx:141:7
Test Files 1 failed | 224 passed (225)
11:10:23Z timeout, attempt 6/100
32470852671 11:48:59Z Autofix agent completed address-review successfully. 11:57:31Z ❯ vi.waitFor.timeout src/ui/auth/AuthDialog.test.tsx:155:27
Test Files 1 failed | 224 passed (225)
12:15:38Z timeout, attempt 7/100
32479699617 12:48:56Z Autofix agent completed address-review successfully. 12:57:47Z ❯ vi.waitFor.timeout src/ui/auth/AuthDialog.test.tsx:141:7 13:15:57Z circuit breaker

Terminal state at 13:15:57Z:

🤖 AutoFix stopped: this counting window now contains 3 time-budget exhaustions⚠️ This change was NOT pushed — any commit referenced below was made only in the runner workspace and has been discarded

Then at 13:30:58Z: ⏸️ Takeover paused: this PR reached its round cap (100/100), plus the autofix/needs-human label. #9340 is now out of the loop entirely, and the discarded work was real — the round summary documents a completed origin/main conflict resolution across three files, two mutation probes, and npx vitest run src/commands/review/ green at 97 files / 4335 tests.

Note the line number moves between runs — :141, :155, :141 — different vi.waitFor assertions in the same file timing out on different runs, which is the signature of a flake rather than a stable failure.

#9394 — same failure, one hour later

32484864137:

13:54:57Z  Autofix agent completed address-review successfully.
14:02:34Z  FAIL src/ui/auth/AuthDialog.test.tsx > AuthDialog
             > drives API key provider steps from endpoint options metadata
           Test Files  1 failed | 866 passed (867)
           Tests       1 failed | 22846 passed | 4 skipped (22851)
14:08:39Z  repair pass starts
14:20:42Z  Test Files  1 failed | 866 passed (867)      ← unchanged; not the repair's to fix
14:20:48Z  🤖 AutoFix ran out of time (timeout (1080000ms)) (attempt 6/100)

One down, two to go before #9394 hits the same circuit breaker.

Why this is worth landing

Both PRs are unrelated to auth: #9340 is review-approach-signal, #9394 adds a DingTalk Workspace channel. Neither can ever make that test pass, and each wasted round feeds CONSEC_FAIL toward a terminal state — as the workflow's own comment anticipates:

# resulting wasted failure rounds feed CONSEC_FAIL toward a terminal

Cost so far: 4 rounds × ~18 min of agent budget plus 60–137 min of runner wall-clock each, one PR terminated with completed work thrown away, and a second PR one-third of the way to the same end. Any takeover PR whose gate happens to enter a repair pass is exposed.

Happy to re-run this check on further rounds if that helps close out the CHANGES_REQUESTED on this PR.

中文说明

本 PR 修复的问题在今天(2026-08-21)四次 autofix 轮次中都有实测证据,并非假设——它已经终结了一个 takeover PR,第二个已经开始。

四次形态完全一致: agent 主轮成功 → 验证闸门仅因 AuthDialog.test.tsx 失败 → Repair 轮被消耗在它够不着的失败上 → 18 分钟预算耗尽。

#9340 连续三轮(run 32469529030 / 32470852671 / 32479699617),主轮均报 Autofix agent completed address-review successfully.,闸门均挂在 vi.waitFor.timeout src/ui/auth/AuthDialog.test.tsx,行号分别为 :141 / :155 / :141——同一文件不同断言轮流超时,是 flake 而非稳定缺陷的特征。13:15:57Z 触发熔断(本窗口 3 次时间预算耗尽),13:30:58Z 托管暂停(100/100)并打上 autofix/needs-human。被丢弃的成果是真实的:该轮摘要记录了三个文件的 origin/main 冲突解决、2 个变异探测,以及 npx vitest run src/commands/review/ 97 文件 4335 通过。

#9394(run 32484864137)一小时后重演:13:54:57Z 主轮成功,14:02:34Z 闸门挂在同一文件的 drives API key provider steps from endpoint options metadata(1 failed / 866 passed),Repair 轮跑完后仍是 1 failed | 866 passed14:20:48Z 超时(attempt 6/100)。再有两次就会走 #9340 的老路。

两个 PR 都与 auth 无关(#9340 是 review-approach-signal,#9394 是钉钉 Workspace channel),永远不可能让那个测试变绿。目前代价:4 轮 × 约 18 分钟 agent 预算,外加每轮 60~137 分钟 runner 墙钟;一个 PR 被终结且已完成的工作被丢弃,另一个已走完三分之一。任何进入 Repair 轮的 takeover PR 都暴露在这个风险下。

@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 explored to full depth (tool budget reached): "agent 6c": none — I could not run the actual vitest suite (no node_modules in the review worktree), but the probe above replicates the changed assertions byte-for-byte aga….

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

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

  • .github/workflows/qwen-autofix.yml:5222 — [probe] CI is passed through the gate allowlist without the step/job-level pin its siblings get — a $GITHUB_ENV plant of CI=false flows through "${CI:-true}" and re-inverts the CI semantics this PR …
中文说明

未探索到全部深度(达到工具调用预算):"agent 6c"none — I could not run the actual vitest suite (no node_modules in the review worktree), but the probe above replicates the changed assertions byte-for-byte aga…

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

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

Comment on lines +11198 to +11199
const argList = step.slice(argStart, step.indexOf('bash --norc'));
const passed = (

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] R2-1: Still stands at HEAD — this pin region is byte-identical to round 2 (only a merge of main landed since; git diff 0fcdd5af..4d87d79c does not touch it). A fresh round-3 probe re-ran the REAL pin against every bypass shape at this commit: all six keep the suite green while the runtime isolation is broken.

The gateAllowlist pin enumerates text tokens, not the shell command structure the runner actually executes:

  1. Window truncation / smuggling — the slice ends at the FIRST textual bash --norc. An entry whose value is nowhere pinned (e.g. BRANCH="bash --norc") placed last truncates the enumeration window, so an entry appended after it is invisible to the pin but parsed by env. Round-2 runtime: a smuggled BASH_ENV sourced attacker code inside the child before the digest-verified runner started.
  2. Commented-out entry# CI="${CI:-true}" \ satisfies both the multiset and the containment check, but the # breaks the continuation chain and the gate script runs as a second command inheriting the parent step's FULL environment. Round-2 runtime: PLANT_VERDICT=[evil_verdict] in the child (baseline <unset>).
  3. Dropped continuation — removing one trailing \ relocates every later assignment out of the clean child the same way; the pin stays green while the child inherits the parent environment.
  4. =-less operand hijack — an operand without = before bash --norc is invisible to the NAME=value grammar, yet env(1) execs it as the command word, demoting the digest-verified gate script to argv; the hijacked word still receives the allowlisted GITHUB_OUTPUT, so it writes the verdict that decides the PAT push.
  5. Suffix after the closing quoteCI="${CI:-true}"x passes toContain('CI="${CI:-true}"') while the child sees CI=truex; process.env['CI'] === 'true' becomes false and the 18 deliberately-skipped TUI tests un-skip — the exact inversion this PR exists to prevent.
  6. NEW this round (reverse audit, probe-confirmed): the window is anchored on the FIRST textual mention of /usr/bin/env -i \ — delete the real launch head and let a preceding comment carry the anchor string, and the nine assignments become a bare VAR=… bash --norc prefix (bash runs it with the CURRENT environment, isolation gone) while every pin stays green (215/215), including the workflow-wide env -i count pin.

Witness (round 3, at HEAD, real vitest pin + bash runtime oracle):

m1 truncation/smuggle: Tests 1 passed; child env carries SMUGGLED_VERDICT=approved
m2 commented entry:    Tests 1 passed; child inherits full parent env (PLANT_FROM_PARENT, GH_TOKEN)
m3 dropped `\`:        Tests 1 passed; same full-parent-env inheritance
m4 =-less operand:     Tests 1 passed; evil binary executed with the allowlisted env, gate script never ran
m5 `…x` suffix:        Tests 1 passed; child CI=truex
shape-6 comment head:  Tests 215 passed (215); no env -i left in either gate step

The entry space — shell edits that preserve the matched token text — is unbounded (a new shape found in each of five audit rounds now); patching shape by shape will not close it. Pin the launch structurally instead: one verbatim adjacency regex over the whole launch block anchored on the launch head (as the sibling upsert pin anchors on LD_PRELOAD= LD_AUDIT=), every assignment in order with exact values and trailing \ continuations through bash --norc; a residue-exclusivity check (strip the matched NAME=value tokens, expect the remainder to equal /usr/bin/env -i); the CI token pinned with its trailing delimiter (CI="${CI:-true}" \); and terminator uniqueness (expect((step.match(/bash --norc/g) ?? []).length).toBe(1)).

中文说明

R2-1 在当前 HEAD 仍然成立——该 pin 区域与第 2 轮逐字节相同(此后仅合入了 main;git diff 0fcdd5af..4d87d79c 未触及此区域)。第 3 轮的新探针在该提交上用真实 pin 重跑了每一种绕过形态:六种形态全部让套件保持绿色,而运行时隔离均被击穿。

gateAllowlist pin 枚举的是文本 token,而不是 runner 实际执行的 shell 命令结构:

  1. 窗口截断/走私——切片在第一个文本 bash --norc 处结束。把一个值未被任何 pin 锁定的条目(如 BRANCH="bash --norc")移到最后会截断枚举窗口,其后追加的条目对 pin 不可见,但 env 仍会解析。第 2 轮运行时:走私的 BASH_ENV 在 digest 校验过的 runner 启动之前就在子进程内 source 了攻击者代码。
  2. 注释掉的条目——# CI="${CI:-true}" \ 同时满足多重集与包含检查,但 # 会打断续行链,gate 脚本作为第二条命令运行并继承父步骤的完整环境。第 2 轮运行时:子进程出现 PLANT_VERDICT=[evil_verdict](基线为 <unset>)。
  3. 丢失续行符——删掉一个行尾 \ 会以同样方式把其后所有赋值移出干净子进程;pin 保持绿色而子进程继承父环境。
  4. = 操作数劫持——在 bash --norc 之前插入无 = 操作数对 NAME=value 语法不可见,但 env(1) 恰恰把它作为命令 exec,digest 校验过的 gate 脚本被降级为 argv;被劫持的命令仍拿到允许列表中的 GITHUB_OUTPUT,从而写入决定 PAT push 的 verdict。
  5. 闭引号后的后缀——CI="${CI:-true}"x 能通过 toContain 检查,而子进程得到 CI=truexprocess.env['CI'] === 'true' 变为 false,18 个被刻意跳过的 TUI 测试解除跳过——正是本 PR 要防止的语义反转。
  6. 本轮新发现(反向审计,探针确认):窗口锚定在第一个文本 /usr/bin/env -i \ 上——删掉真实启动头、让前面的注释携带锚定字符串,九条赋值就变成裸的 VAR=… bash --norc 前缀(bash 以当前环境运行它,隔离消失),而所有 pin 保持绿色(215/215),包括全 workflow 的 env -i 计数 pin。

见证(第 3 轮,HEAD,真实 vitest pin + bash 运行时 oracle):m1 截断/走私:Tests 1 通过,子进程环境携带 SMUGGLED_VERDICT=approved;m2 注释条目:Tests 1 通过,子进程继承完整父环境(PLANT_FROM_PARENT、GH_TOKEN);m3 丢失 \:Tests 1 通过,同样继承完整父环境;m4 无 = 操作数:Tests 1 通过,恶意二进制以允许列表环境执行、gate 脚本从未运行;m5 …x 后缀:Tests 1 通过,子进程 CI=truex;形态 6 注释启动头:Tests 215 passed (215),两个 gate 步骤中均不再有 env -i。

入口空间——保持被匹配 token 文本不变的 shell 编辑——是无界的(至今五轮审计每轮都发现新形态),逐个形态打补丁无法收敛。请改为结构性钉住整个启动块:以启动头为锚(如同 sibling upsert pin 锚定 LD_PRELOAD= LD_AUDIT=)的一条逐字邻接正则,覆盖按序的每条赋值及其精确值与行尾 \ 续行直到 bash --norc;残差排他检查(剥掉匹配到的 NAME=value token 后,剩余部分恰好等于 /usr/bin/env -i);CI token 连同尾分隔符一起钉住(CI="${CI:-true}" \);终止符唯一性(expect((step.match(/bash --norc/g) ?? []).length).toBe(1))。

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

// allowlist of BOTH gate launches (first pass + repair pass) as a sorted
// multiset — a symmetric duplicate or a dropped entry both fail here.
const gateAllowlist = (step) => {
const argStart = step.indexOf('/usr/bin/env -i \\');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] No pin covers the gate launches' LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH= command-prefix strip or the body-side unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH. This pin's window starts at /usr/bin/env -i \ — AFTER the prefix — and every other LD_* pin in this file applies to other steps (the PAT steps and the upsert children). The sibling upsert pin ~40 lines away includes its prefix in its window and pins it by regex, documenting exactly why: LD_* is stripped by a command-prefix assignment BEFORE /usr/bin/env — the one channel env -i cannot block (ld.so preloads into the env binary itself at execve). The removal side of the gate's LD_* guard is unpinned, and no step/job/workflow-level LD_* pin exists on the gate steps.

Failure: a symmetric edit deleting the prefix + the body unset from both launches ships green — probe-verified at HEAD: baseline Tests 215 passed (215); mutant with both gates' prefix + unset removed: Tests 215 passed (215). An earlier shell-capable step — this step's own comment says the agent runs branch code on the host — plants LD_PRELOAD via $GITHUB_ENV; ld.so maps the attacker's DSO into /usr/bin/env itself at execve, and its constructor runs before env -i is processed, with the full step environment including GITHUB_OUTPUT — the branch writes its own verdict through the very launch this pin suite exists to isolate. Flip check: with pins for the unset + prefix adjacency added, the same mutant fails at this pin.

// start the window at the launch line, as the sibling does
const argStart = step.indexOf('LD_PRELOAD= LD_AUDIT=');
// + pin the prefix by regex (as the sibling upsert pin) and each gate body's
//   `unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH`

This fix also independently kills shape 6 of the R2-1 thread (the comment-anchor hijack) — land both repairs together.

中文说明

没有任何 pin 覆盖 gate 启动的 LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH= 命令前缀剥离,也没有 pin 覆盖步骤体内的 unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH。本 pin 的窗口从 /usr/bin/env -i \ 开始——位于该前缀之后——而文件中所有其他 LD_* pin 都作用于其他步骤(PAT 步骤与 upsert 子进程)。约 40 行外的 sibling upsert pin 把前缀纳入窗口并用正则钉住,其注释恰好说明了原因:LD_* 由 /usr/bin/env 之前的命令前缀赋值剥离——这是 env -i 唯一无法阻断的通道(ld.so 在 execve 时就把预加载注入 env 二进制本身)。gate 的 LD_* 守卫的「删除侧」无人钉守,且 gate 步骤上不存在任何 step/job/workflow 级别的 LD_* pin。

失败路径:对称地删除两处启动的前缀 + 体内 unset 可以全程绿灯——已在 HEAD 探针验证:基线 Tests 215 passed (215);删除两个 gate 前缀 + unset 的突变体:Tests 215 passed (215)。更早的 shell 级步骤——本步骤自己的注释说明 agent 会在宿主机上运行分支代码——可通过 $GITHUB_ENV 植入 LD_PRELOAD;ld.so 会在 execve 时把攻击者的 DSO 映射进 /usr/bin/env 本身,其构造函数先于 env -i 处理、在包含 GITHUB_OUTPUT 的完整步骤环境中运行——分支就此通过这套 pin 本要隔离的启动写入自己的 verdict。翻转验证:加入 unset + 前缀邻接 pin 后,同一突变体在该 pin 处失败。

修复(同时独立关闭 R2-1 线程的形态 6 注释锚定劫持——两处修复应一起落地):像 sibling 一样把窗口起点设为启动行 const argStart = step.indexOf('LD_PRELOAD= LD_AUDIT=');,并用正则钉住前缀、钉住每个 gate 体内的 unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH

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

Comment on lines +11202 to +11203
.map((m) => m.split('=')[0])
.sort();

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: Still stands at HEAD (code unchanged since round 2). The multiset compares variable NAMES only, so a symmetric value swap on any of the non-CI entries is invisible to this pin, while the sibling upsert clean-child pin pins every entry by exact token. Probe-verified at HEAD: swapping PATH="${TRUSTED_PATH}"PATH="${PATH}" keeps the real test green (215/215) while the runtime child receives the parent's PATH instead of the trusted value. Pin each entry by exact token as the sibling does (loop the expected NAME=value tokens with toContain), or fold into R2-1's structural adjacency regex, which pins every value.

中文说明

R2-2 在当前 HEAD 仍然成立(代码自第 2 轮以来未变)。该多重集只比较变量名,因此对任一非 CI 条目做对称的值替换对本 pin 不可见,而 sibling upsert 清洁子进程 pin 逐条以精确 token 钉值。已在 HEAD 探针验证:把 PATH="${TRUSTED_PATH}" 换成 PATH="${PATH}",真实测试仍全绿(215/215),而运行时子进程收到的是父进程的 PATH 而非受信路径。请像 sibling 一样逐条以精确 token 钉值(对期望的 NAME=value token 逐一 toContain),或并入 R2-1 的结构性邻接正则(每条值都被钉住)。

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

expect(argStart, 'gate step lacks the env -i launch').toBeGreaterThan(-1);
const argList = step.slice(argStart, step.indexOf('bash --norc'));
const passed = (
argList.match(/[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? []

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-3: Still stands at HEAD — the env-assignment token grammar is duplicated byte-for-byte at exactly two sites (here and the upsert clean-child pin, ~line 12813), and this diff's second hunk exists solely to re-sync the two copies after they drifted (the round-1 fix had to widen both sites in one commit). If the workflow's NAME=value quoting shape ever changes, the tokenizer must be fixed in two places; missing one makes that allowlist pin silently under-match — the quietly-weakened-pin failure mode the surrounding comments warn about. Extract the shared tokenizer into scripts/tests/workflow-helpers.js (already imported here):

export function extractEnvAssignments(argList) {
  return argList.match(/[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? [];
}

and call it from both sites, each keeping its own slice anchor, expected set, and extra pins.

中文说明

R2-3 在当前 HEAD 仍然成立——env 赋值 token 语法在两处逐字节重复(此处与 upsert 清洁子进程 pin,约 12813 行),而本 diff 的第二个 hunk 正是为了在两份拷贝漂移后重新同步(第 1 轮的修复不得不在一个提交里同时放宽两处)。如果 workflow 的 NAME=value 引号形态发生变化,tokenizer 必须在两处修改;漏掉一处就会让那个允许列表 pin 悄然欠匹配——正是周围注释反复警告的「pin 被悄悄削弱」失败模式。把共享 tokenizer 抽到 scripts/tests/workflow-helpers.js(此处已引入该模块):export function extractEnvAssignments(argList) { return argList.match(/[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? []; },两处调用它,各自保留自己的切片锚点、期望集合与附加 pin。

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

@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 轮)。改动内容与我反驳保留之处如下:

Autofix review round summary

Commit: 0fbc379511fix(autofix): pin the gate clean-child launches structurally (#9649)
Changed files: scripts/tests/qwen-autofix-workflow.test.js (+39/−29)

Feedback points and dispositions

[Critical] R2-1 — rc:3830343017, rc:3831502386 (the gateAllowlist pin enumerates text tokens, not shell structure): RESOLVED.
Reproduced first: all six bypass shapes (window truncation/smuggling, commented-out entry, dropped \, =-less operand hijack, quote suffix, comment-anchor head hijack) were applied as symmetric mutants to both gate steps and every one kept the real pin suite green (pre-fix probes below). The pin is now structural: one verbatim adjacency chain — built from the exact ordered token list and joined with \ + newline — running from the LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH= prefix through /usr/bin/env -i, every assignment in order with its exact value, through bash --norc "${RUNNER_TEMP}/run-autofix-review-verification.sh". Any shell edit that preserves token text but alters structure breaks the chain. Post-fix, all six mutants fail the pin.

Design notes vs the suggested recipe:

  • Residue-exclusivity check — not added as a separate guard: with a full adjacency chain there is no sliced window in which residue can exist; the shape-4 (=-less operand) and in-window-comment mutants are probe-verified red against the chain itself.
  • CI token with trailing delimiter — subsumed: the chain pins CI="${CI:-true}" followed by the mandatory \ continuation; shape-5 ("x suffix) is probe-verified red.
  • Terminator uniqueness — implemented: expect((step.match(/bash --norc/g) ?? []).length).toBe(1) per gate step, with its own witness mutant (a second unpinned launch appended after the pinned block → red).

[Critical] R3-1 — rc:3831502396 (LD_ command-prefix strip and body unset unpinned on the gate steps): RESOLVED.*
The adjacency chain now starts at the launch line (LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH=), pinning the prefix by adjacency exactly as the sibling upsert pin does — this also independently kills shape 6 of R2-1, as the finding predicted, and both repairs landed together. The removal side is pinned per gate body: expect(step).toContain('unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH'). The probe-verified compound mutant (prefix + unset deleted from both gates) now fails; an unset-only deletion mutant fails independently, so the unset pin is not masked by the chain.

[Suggestion] R2-2 — rc:3830343022, rc:3831502407 (multiset compares names only; symmetric value swap invisible): RESOLVED.
The adjacency chain pins every entry by its exact NAME=value token in order, so a value swap on any entry fails (folded into R2-1's structural pin, as the finding offered). The gate body's export PATH="${TRUSTED_PATH}" line is now pinned too, closing the compound shape: swap + export-deletion mutants (each applied symmetrically to both gates) are probe-verified red, as is the export-only deletion.

[Suggestion] R2-3 — rc:3830343032, rc:3831502409 (env-token grammar duplicated byte-for-byte at two sites): RESOLVED by subtraction.
The structural pin removed the gate side's tokenizer entirely, so the grammar now exists at exactly one site (the upsert clean-child pin, ~line 12823). With a single copy there is nothing left to drift, so no shared helper was extracted — dedup by deletion is the smaller fix and keeps the diff inside the PR's existing footprint (scripts/tests/workflow-helpers.js was never touched by this PR).

Review states rv:4993414577, rv:4994870562 (CHANGES_REQUESTED): their substance is the inline findings above; the disclosed gaps (reverse audit not started in round 2, tool budget in round 3, actionlint embedded-shell lint unsupported) are reviewer-side limitations, not requested changes.

Deferred by the reviewer, not requested this round: .github/workflows/qwen-autofix.yml:5222 — a $GITHUB_ENV plant of CI=false flows through "${CI:-true}" because CI is not pinned at step/job level like its siblings. Recorded here for the next round/maintainer; no action taken this round per the review's convergence posture.

Issue comment ic:5371388569 (@qqqys): field evidence from four autofix runs (#9340 terminated, #9394 progressing toward the same breaker) that the flake this PR fixes is real and costly. Informational — no change requested; noted with thanks.

Conflict

None (--conflict false); no merge performed.

Verification

All commands actually run this round, in order:

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js -t 'allows non-package fixes after deterministic verification' — passed (baseline before the change, and green again after the change)
  • Pre-fix mutation probes (each mutant applied symmetrically to both gate steps, focused pin run, workflow restored): s1_smuggle, s2_comment_entry, s3_dropped_continuation, s4_eqless_operand, s5_quote_suffix, s6_comment_head, r31_unset_removal (prefix + unset), r22_path_swap_compound (swap + export deletion) — all 8 GREEN pre-fix, reproducing every claimed bypass
  • Post-fix mutation probes (same 8 mutants): all RED — the new pins catch them
  • Post-fix guard witnesses: r31b_unset_removal (unset-only) — RED via the unset pin; r22b_export_removal (export-only) — RED via the export pin; second_launch (pinned block intact, second bash --norc appended) — RED via the uniqueness pin. Every guard added this round has its own failing witness
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js — 215 passed (215)
  • npm run lint — passed
  • npm run typecheck — passed
  • npm run build — passed
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js — passed
  • git status after all probes — only the test file modified; the workflow file was restored byte-exact after every probe

No settings source changed, so npm run generate:settings-schema was not applicable. No integration-test behavior was touched (test-only change to a contract pin), so no bundled-CLI integration run was needed.

中文说明

Autofix 审查轮次总结

提交: 0fbc379511fix(autofix): pin the gate clean-child launches structurally (#9649)
变更文件: scripts/tests/qwen-autofix-workflow.test.js(+39/−29)

反馈点与处置

[Critical] R2-1 — rc:3830343017、rc:3831502386(gateAllowlist pin 枚举的是文本 token 而非 shell 结构):已解决。
先复现:六种绕过形态(窗口截断/走私、注释掉的条目、丢失 \、无 = 操作数劫持、闭引号后缀、注释锚定启动头)全部以对称突变体施加到两个 gate 步骤,每一个都让真实 pin 套件保持绿色(见下方修复前探针)。现将 pin 改为结构性:一条逐字邻接链——由精确有序的 token 列表构造、以 \ + 换行连接——从 LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH= 前缀开始,经过 /usr/bin/env -i,按序钉住每条赋值及其精确值,直到 bash --norc "${RUNNER_TEMP}/run-autofix-review-verification.sh"。任何保留 token 文本但改变结构的 shell 编辑都会使链断裂。修复后六种突变体全部使 pin 变红。

相对建议方案的实现说明:

  • 残差排他检查——未作为独立守卫添加:完整邻接链下不存在可容纳残差的切片窗口;形态 4(无 = 操作数)与窗口内注释突变体已经探针验证对链本身变红。
  • CI token 连同尾分隔符——已并入:链中钉住 CI="${CI:-true}" 且其后必须跟随 \ 续行;形态 5("x 后缀)已探针验证变红。
  • 终止符唯一性——已实现:每个 gate 步骤 expect((step.match(/bash --norc/g) ?? []).length).toBe(1),并有独立见证突变体(在钉住的块之后追加第二条未钉住的启动 → 变红)。

[Critical] R3-1 — rc:3831502396(gate 步骤的 LD_ 命令前缀剥离与体内 unset 无 pin 覆盖):已解决。*
邻接链现在从启动行(LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH=)开始,像 sibling upsert pin 一样以邻接方式钉住前缀——正如该发现所预言,这也独立关闭了 R2-1 的形态 6,两处修复一起落地。删除侧按 gate 体钉住:expect(step).toContain('unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH')。探针验证的复合突变体(两个 gate 的前缀 + unset 同时删除)现在失败;仅删 unset 的突变体也独立失败,说明 unset pin 未被链掩盖。

[Suggestion] R2-2 — rc:3830343022、rc:3831502407(多重集只比较变量名,对称值替换不可见):已解决。
邻接链按序以精确 NAME=value token 钉住每个条目,因此任一条目的值替换都会失败(按该发现提供的选项并入 R2-1 的结构性 pin)。gate 体的 export PATH="${TRUSTED_PATH}" 行也被钉住,关闭复合形态:值替换 + 删除 export(各自对称施加于两个 gate)已探针验证变红,仅删 export 的突变体同样变红。

[Suggestion] R2-3 — rc:3830343032、rc:3831502409(env token 语法在两处逐字节重复):以减法解决。
结构性 pin 完全移除了 gate 侧的 tokenizer,该语法现在只存在于一处(upsert 清洁子进程 pin,约 12823 行)。只剩一份拷贝便无从漂移,因此没有提取共享 helper——按删除去重是更小的修复,且使 diff 保持在 PR 既有 footprint 内(scripts/tests/workflow-helpers.js 从未被本 PR 触及)。

审查状态 rv:4993414577、rv:4994870562(CHANGES_REQUESTED): 其实质内容即上述行内发现;披露的缺口(第 2 轮未开始反向审计、第 3 轮工具预算耗尽、actionlint 内嵌 shell lint 不支持)属于审查方限制,并非请求的变更。

审查方延后、本轮不要求: .github/workflows/qwen-autofix.yml:5222$GITHUB_ENV 植入的 CI=false 会经由 "${CI:-true}" 流入,因为 CI 没有像其 siblings 那样在 step/job 级别被钉住。记录于此供下一轮/维护者处理;按审查方的收敛姿态本轮不做处理。

Issue 评论 ic:5371388569(@qqqys): 四次 autofix 运行的现场证据(#9340 被终结、#9394 正走向同样的熔断)表明本 PR 修复的 flake 真实且代价高昂。信息性内容——未请求变更;致谢记录。

冲突

无(--conflict false);未执行合并。

验证

本轮实际执行的全部命令及结果(按顺序):

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js -t 'allows non-package fixes after deterministic verification' — 通过(变更前基线,变更后再次绿色)
  • 修复前变异探针(每个突变体对称施加于两个 gate 步骤,运行聚焦 pin,恢复 workflow):s1_smuggles2_comment_entrys3_dropped_continuations4_eqless_operands5_quote_suffixs6_comment_headr31_unset_removal(前缀 + unset)、r22_path_swap_compound(值替换 + 删 export)——8 个全部绿色,复现了每一个声称的绕过
  • 修复后变异探针(同 8 个突变体):全部红色——新 pin 将其捕获
  • 修复后守卫见证:r31b_unset_removal(仅删 unset)——经 unset pin 变红;r22b_export_removal(仅删 export)——经 export pin 变红;second_launch(钉住的块保持完整、追加第二条 bash --norc)——经唯一性 pin 变红。本轮新增的每个守卫都有自己的失败见证
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js — 215 通过(215)
  • npm run lint — 通过
  • npm run typecheck — 通过
  • npm run build — 通过
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js — 通过
  • 所有探针结束后 git status — 仅测试文件被修改;workflow 文件在每次探针后均逐字节恢复

未改动任何 settings 源,故不适用 npm run generate:settings-schema。未触及任何需要集成测试的行为(仅契约 pin 的测试变更),故无需 bundle 后的 CLI 集成运行。

🦷 Gate advisory — this round resolves a Critical/Request-changes finding with test-only changes (machine-measured): the bite check cannot verify a test-side fix, so the resolution rests on the round summary alone. · 本轮以纯测试改动解决 Critical/Request-changes 反馈(门自动测量):bite 检查无法验证测试侧修复,该解决仅以轮次摘要为凭。

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

qqqys
qqqys previously approved these changes Aug 21, 2026

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

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

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

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

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

  • .github/workflows/qwen-autofix.yml:5222 — [probe] CI passes through the gate allowlist without the step/job-level pin its siblings get — a $GITHUB_ENV plant of CI=false flows through "${CI:-true}" and re-inverts the CI semantics this PR res…
  • scripts/tests/qwen-autofix-workflow.test.js:12823 — [probe] upsert allowlist audit misses env operands whose names have no letter/underscore (1=x, =x) — the widened regex closed the lowercase corner but not the no-letter subclass (code unch…
中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

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

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

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

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

'bash --norc "${RUNNER_TEMP}/run-autofix-review-verification.sh"',
]
.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join(' \\\\\n\\s*'),

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 inter-token separator of gateLaunchPin treats every JS \s member as harmless indentation, but bash recognizes only space/tab/newline as whitespace — two edit shapes keep all four loop assertions green while breaking the isolation this pin exists to guarantee. A BLANK LINE between two continued allowlist entries splits the launch into two commands: the orphaned env -i prints and exits 0, and the second command (... bash --norc run-autofix-review-verification.sh) runs with the FULL inherited step environment — isolation gone, and NODE_OPTIONS is a live plant channel the gate script does not strip. A GLUED NBSP (U+00A0) or U+2028 before an entry name merges into the operand and silently renames it — probe-verified: the child then carries \xa0KISS_AUDIT=... with KISS_AUDIT itself unset (glued to CI= the exact inversion this PR fixes returns; to FOOTPRINT_ENFORCE= enforcement degrades to advisory). The full 215-test suite stays green under both mutants, and the previously suggested [^\S\n]* does NOT close the Unicode shape — it still matches NBSP/U+2028 — only [ \t]* closes both (fix-flip verified: mutants red, baseline 215/215 green). The sibling upsert pin (~line 12800) uses the same separator shape; the same tightening applies when it is next touched.

Witness:

blank_line: all4_green_under_HEAD_pin=true, all4_green_under_FIXED_pin=false
            harm run — child env had MUTANT_PLANT: YES (isolation broken), step exit 0
nbsp_glued: all4_green_under_HEAD_pin=true
            child env bytes 302 240 K I S S _ A U D I T = f a l s e, real KISS_AUDIT absent
full suite under both mutants: Tests 215 passed (215)
Suggested change
.join(' \\\\\n\\s*'),
.join(' \\\\\n[ \\t]*'),
中文说明

gateLaunchPin 的 token 间分隔符把 JS \s 类的每个成员都当作无害缩进,但 bash 只认空格/制表符/换行为空白——两种改动形态能让全部四条循环断言保持绿色,同时击穿这个 pin 本要保障的隔离。在两条续行条目之间插入空行会把启动拆成两条命令:落单的 env -i 打印后以 0 退出,第二条命令(... bash --norc run-autofix-review-verification.sh)带着完整的父步骤环境运行——隔离消失,且 NODE_OPTIONS 是 gate 脚本不会剥离的活植入通道。在条目前粘贴 NBSP(U+00A0)或 U+2028 会并入操作数并悄悄将其改名——探针验证:子进程随后携带 \xa0KISS_AUDIT=...KISS_AUDIT 本身未设置(粘到 CI= 上则本 PR 要修复的语义反转复现;粘到 FOOTPRINT_ENFORCE= 上则执行降级为 advisory)。两种突变体下完整 215 测试套件均保持绿色;此前提出的 [^\S\n]* 无法关闭 Unicode 形态(它仍匹配 NBSP/U+2028),只有 [ \t]* 能同时关闭两者(修复翻转验证:突变体变红,基线 215/215 绿)。sibling upsert pin(约 12800 行)使用同样的分隔符形态,下次触及该处时应做同样收紧。

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

// check are pinned with it (R3-1, R2-2).
const gateLaunchPin = new RegExp(
[
'LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH=',

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] gateLaunchPin is an unanchored substring match whose surroundings nothing bounds, and the literal bash --norc count is the only launch-shape guard — three probe-verified shapes keep the full 215-test suite green while defeating the gate. (a) A PREFIX command word before the LD_* head (e.g. printf 'outcome=pass\n' >> "$GITHUB_OUTPUT" \) turns the whole chain into argv — the gate never executes, the forged outcome is written, the step exits 0. (b) A command APPENDED after the launch line executes whenever the gate exits 0 and overwrites the gate's documented last-write-wins step outputs (verified_head / audit_verdict / preexisting), which drive the repair trigger and push routing. (c) A DEMOTED ARM — the pinned chain wrapped in if false; then ... fi plus an early if true; then bash -c '...'; exit 0; fi (a --n""orc spelling keeps the literal count at 1) — runs the runner with NO env -i isolation while all four assertions and all 11 other presence assertions stay green. The tripwire comment claims it catches a demoted pinned block; it catches only demotions that re-launch via the literal bash --norc. Fix (flip-verified: each mutant 1 failed | 214 passed, baseline 215/215 green) — anchor the chain head to a statement start and assert the launch ends the step's run body (the extraction lookahead cuts at the next - name:, and the launch line ends the slice modulo one trailing LF, so the tail check is cut-point-safe); this also defeats shape (c), whose if false; then prefix and trailing fi both break the anchored match. The same "anchor on the launch LINE" doctrine is already applied by the upsert audit at ~12813.

Witness:

prefix_forgery: {"step_exit":0,"gate_script_ran":false,"GITHUB_OUTPUT_content":"outcome=pass"}
demoted_arm:    {"step_exit":0,"gate_script_ran":true,"child_env_has_plant":true}
each mutant:    Tests 215 passed (215) on the unmodified PR
const gateLaunchPin = new RegExp(
  '(?:^|\\n)[ \\t]*' +
    [
      'LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH=',
      /* …remaining tokens unchanged… */
    ]
      .map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
      .join(' \\\\\n[ \\t]*'),
);
// …and in the loop, after toMatch, assert the launch ends the run body:
const launchMatch = step.match(gateLaunchPin);
expect(step.slice(launchMatch.index + launchMatch[0].length).trim()).toBe('');
中文说明

gateLaunchPin 是无锚定的子串匹配,其前后内容不受任何约束,而字面 bash --norc 计数是唯一的启动形态守卫——三种经探针验证的形态能让完整 215 测试套件保持绿色同时击穿 gate。(a) 在 LD_* 头部之前加前缀命令字(如 printf 'outcome=pass\n' >> "$GITHUB_OUTPUT" \)会把整条链变成该命令的 argv——gate 从不执行、伪造的 outcome 被写入、步骤以 0 退出。(b) 在启动行之后追加的命令会在 gate 以 0 退出时执行,并覆盖 gate 文档化的 last-write-wins 步骤输出(verified_head / audit_verdict / preexisting),这些输出驱动 repair 触发与 push 路由。(c) 降级臂——把被钉住的链包进 if false; then ... fi,并在前面加 if true; then bash -c '...'; exit 0; fi(用 --n""orc 拼写让字面计数保持为 1)——在没有 env -i 隔离的情况下运行 runner,而全部四条断言及其余 11 条存在性断言保持绿色。绊线注释声称能捕获被降级的被钉块,实际只能捕获经由字面 bash --norc 重新启动的降级。修复(翻转验证:每种突变体 1 failed | 214 passed,基线 215/215 绿)——把链头锚定到语句起始,并断言启动行结束步骤运行体(提取前瞻在下一个 - name: 处截断,且启动行就是切片末尾、仅差一个行尾 LF,尾部检查对截断点是安全的);这同时击败形态 (c),其 if false; then 前缀与行尾 fi 都会破坏锚定匹配。同样的「锚定到启动」纪律在 ~12813 的 upsert 审计中已有应用。

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

reviewVerificationGateStep,
repairVerificationGateStep,
]) {
expect(step).toMatch(gateLaunchPin);

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 block pins the chain "through the digest-verified script", but the pre-launch digest check itself is pinned only by a workflow-wide toHaveLength(2) count (line ~10529, regex not end-anchored) — never per gate step, never ordered before the launch, never guarded against weakening. Probe-verified mutants, baseline all-green and each mutant all-green against every pin touching the gate steps: relocating both digest lines out of the gate steps (or after the launch line) — the launch executes the staged script with no pre-execution verification, and RUNNER_TEMP is branch/agent-writable between staging and the gate per the stage step's own comment, with both gate steps continue-on-error so forged $GITHUB_OUTPUT verdicts survive; and appending || true to a digest line — the count regex still matches, and under the runner's bash -e the mismatch no longer aborts, so a tampered staged script launches. This is the exact mutant the resanitize sibling's comment (~10354) records as turning "the tamper gate into a decorative no-op while presence/order assertions stayed green" — that sibling pins all three properties (whole-line toContain + indexOf ordering before the call + no-|| true); this new block should mirror the doctrine in its own loop. The relocation and || true shapes also survive the head/tail anchoring proposed in the other thread (the count pin is workflow-global, the weakening intra-line).

Witness:

M1a (digest after launch):       exit=0 | Tests 215 passed
M1b (digest removed from gates): exit=0 | Tests 215 passed
M2  (|| true appended):          exit=0 | Tests 215 passed
with mirrored doctrine: M1a "expected 3407 to be less than 3333", M1b/M2 red, baseline green
Suggested change
expect(step).toMatch(gateLaunchPin);
expect(step).toMatch(gateLaunchPin);
expect(step).toContain('echo "${VERIFY_RUNNER_SHA256} ${RUNNER_TEMP}/run-autofix-review-verification.sh" | sha256sum -c - > /dev/null');
expect(
step.indexOf('sha256sum -c - > /dev/null'),
).toBeLessThan(step.search(gateLaunchPin));
expect(step).not.toMatch(/sha256sum -c[^\n]*\|\| true/);
中文说明

本块把链钉到「digest 校验过的脚本」,但启动前的 digest 检查本身只由一个全 workflow 的 toHaveLength(2) 计数 pin(~10529 行,正则无尾锚定)——既不按 gate 步骤钉,也不钉其在启动之前的顺序,更不防削弱。探针验证的突变体:基线全绿,且每个突变体对触及 gate 步骤的所有 pin 均保持绿色。把两条 digest 行移出 gate 步骤(或挪到启动行之后)——启动会在无执行前校验的情况下运行暂存脚本,而按 stage 步骤自己的注释,RUNNER_TEMP 在 staging 与 gate 之间可被分支/agent 代码写入,两个 gate 步骤又都是 continue-on-error,伪造的 $GITHUB_OUTPUT verdict 会留存;在 digest 行尾追加 || true——计数正则仍能匹配,而在 runner 的 bash -e 下不匹配不再中止步骤,被篡改的暂存脚本照样启动。这正是 resanitize sibling 注释(~10354)记录过的突变体——「防篡改 gate 变成装饰性空操作,而存在/顺序断言保持绿色」;该 sibling 钉住了全部三个属性(整行 toContain + 调用前 indexOf 排序 + 禁止 || true),本新块应在自己的循环里镜像该纪律。迁移与 || true 形态在另一线程提议的头/尾锚定下依然存活(计数 pin 是全 workflow 的,削弱发生在行内)。

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

// Exactly one launch per step: a second, unpinned `bash --norc` (the
// pinned block demoted into a never-run arm) must fail here (R2-1).
expect((step.match(/bash --norc/g) ?? []).length).toBe(1);
expect(step).toContain('unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH');

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] toContain('unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH') and toContain('export PATH="${TRUSTED_PATH}"') match inside a # comment or after the launch — nothing anchors them to an executed position, though the block's comment claims the body-side unset and PATH export "are pinned with it". Probe-verified mutants, each all-green against every pin touching the gate steps: commenting out the unset line — a $GITHUB_ENV plant of LD_PRELOAD=/tmp/evil.so (the stated threat model; the step env block pins BASH_ENV/SHELLOPTS but no LD_* variable) survives into the gate step's parent shell and loads into the first exec — the digest check's sha256sum — code execution before the env -i child launches, able to write forged verdict outputs; commenting out the PATH export — a planted PATH resolves sha256sum to an attacker binary (same forgery outcome); relocating both lines after the launch. The launch's own LD_* prefix + env -i keep the child-side isolation pins honest; the unprotected exec is the parent-shell digest check. The PAT-step preamble is held to a stricter bar (its adjacency regex fails if its unset line is commented). Fix (flip-verified: adjacency flips the comment-out mutants red, adding the ordering assertion also flips the relocation mutant, baseline green) — anchor the pair to statement position and order it before the digest check.

Witness:

M3 (unset commented):      exit=0 | Tests 215 passed
M4 (export commented):     exit=0 | Tests 215 passed
M5 (pair relocated later): exit=0 | Tests 215 passed
run-body order verified at 5205-5207: export -> unset -> digest check -> launch
anchored adjacency: M3/M4 red; + ordering: M5 "expected 3476 to be less than 2357"
Suggested change
expect(step).toContain('unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH');
expect(step).toMatch(/(?:^|\n)[ \t]*export PATH="${TRUSTED_PATH}"\n[ \t]*unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH/);
expect(
step.indexOf('unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH'),
).toBeLessThan(step.indexOf('echo "${VERIFY_RUNNER_SHA256}'));
中文说明

toContain('unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH')toContain('export PATH="${TRUSTED_PATH}"')# 注释内或启动之后同样匹配——没有任何东西把它们锚定到被执行的位置,尽管本块注释声称体内 unset 与 PATH 导出「一并被钉住」。探针验证的突变体,每一个对触及 gate 步骤的所有 pin 均保持绿色:注释掉 unset 行——按既有威胁模型由更早 shell 级步骤通过 $GITHUB_ENV 植入的 LD_PRELOAD=/tmp/evil.so(步骤 env 块钉住了 BASH_ENV/SHELLOPTS,但没有钉任何 LD_* 变量)会存活进 gate 步骤的父 shell,并加载进第一个 exec——digest 检查的 sha256sum——在 env -i 子进程启动之前实现代码执行,可写入伪造的 verdict 输出;注释掉 PATH 导出——植入的 PATH 会把 sha256sum 解析为攻击者二进制(同样的伪造结果);把两行挪到启动之后。启动自身的 LD_* 前缀 + env -i 使子进程侧隔离 pin 依然成立;失去保护的是父 shell 中的 digest 检查 exec。PAT 步骤前缀被要求更严格(其邻接正则在 unset 行被注释时会失败)。修复(翻转验证:邻接锚定使注释突变体变红,再加排序断言使迁移突变体也变红,基线绿)——把这对语句锚定到语句位置,并钉住其先于 digest 检查的顺序。

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — PR #9649 review feedback (round 4 findings)

All four inline findings from the automated review are implemented in one
commit. All four targeted the new gateLaunchPin block that pins the gates'
env -i clean-child launch in scripts/tests/qwen-autofix-workflow.test.js.

Findings and decisions

  • rc:3832783507 (R4-1, separator accepts non-bash whitespace)
    Implemented. The inter-token separator is now ' \\\\\n[ \\t]*' instead of
    ' \\\\\n\\s*'. Probe-verified: a blank line between two continued entries
    (splits the chain into two commands — the orphaned env -i prints and exits
    0 while the rest runs with the FULL step environment) and an NBSP glued
    before an entry name (silently renames the operand) each fail the pin; the
    baseline stays green.
    • The finding's note that the sibling upsert pin (~line 12800) uses the same
      separator shape is deliberately NOT touched this round: this batch does
      not modify that pin, and the review's own round-4 convergence record
      defers the upsert-pin items ("recorded, not requested in this round").
  • rc:3832783528 (R4-2, unanchored substring match) — Implemented with a
    stronger mechanism than the proposed head-anchor plus tail assertion, for an
    evidenced reason: a probe of that exact fix shape showed one variant of the
    finding's own shape-(a) mutant stays green under it — a PREFIX command on
    its own continued line (printf 'outcome=pass' >> "${GITHUB_OUTPUT}" \
    before the LD_* head) demotes the whole chain to that command's argv (the
    gate never executes, the forged outcome survives, the step exits 0), yet a
    newline still precedes the chain head, so the anchored regex still matches
    and the launch still ends the body. The loop instead asserts that the run
    body's whole statement list (comments stripped, lines trimmed) equals
    exactly [export PATH, unset LD_*, digest check, the 12 launch lines] in
    order. This defeats the prefix command word (same-line and own-line), an
    appended or inserted command around the launch, a demoted
    if false; then/fi arm, commented-out statements, and relocations — all
    probe-verified red.
  • rc:3832783530 (R4-3, digest check pinned only by a workflow-wide
    count)
    — Implemented. The whole digest line is a member of the per-step
    pinned statement list, which fixes presence in EACH gate step, its order
    before the launch, and a || true weakening in one assertion: probes
    removed the digest line from a gate step, appended || true to it, and
    relocated it after the launch — all red. This mirrors the resanitize
    sibling's doctrine, expressed as statement-list membership instead of three
    separate assertions.
  • rc:3832783534 (R4-4, export/unset match inside a comment or after the
    launch)
    — Implemented. The export and the unset are pinned as statements 1
    and 2 of the body list, ahead of the digest check: commenting either out
    removes it from the list, relocating the pair reorders the list — probes
    red. The two bare toContains they replace were deleted (strictly
    subsumed).

Changes

  • scripts/tests/qwen-autofix-workflow.test.js — the gate-launch pin block
    now derives both the adjacency regex (separators tightened to bash-only
    whitespace) and the expected run-body statement list from one shared token
    array; the loop asserts the regex, the statement-list equality, and the
    bash --norc count. Net +57/−19.
  • No workflow changes: the mutation probes edited
    .github/workflows/qwen-autofix.yml transiently and restored it
    byte-identical (git status clean after each probe).

Conflict notes

--conflict false — no merge performed.

Not touched this round

  • The two items the review deferred under the convergence posture (round 4,
    "recorded, not requested in this round"): the CI-allowlist step-level pin
    gap (.github/workflows/qwen-autofix.yml:5222) and the no-letter upsert
    env-operand subclass (scripts/tests/qwen-autofix-workflow.test.js:12823).
  • The sibling upsert pin's separator shape (see rc:3832783507 above).

Verification

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js
    — 215/215 passed (baseline at HEAD, after the change, and after the commit)
  • Mutation probes — 13 YAML mutants against the focused test
    allows non-package fixes after deterministic verification, each red, YAML
    restored byte-identical after each probe:
    • M1 blank line between continued entries — red (tightened separator)
    • M2 NBSP glued before an entry name — red (tightened separator)
    • M3 prefix command word, same line as the LD_* head — red (statement list)
    • M3b prefix command, own continued line — red (statement list; this mutant
      was GREEN under the proposed head-anchor + tail assertion)
    • M4 command appended after the launch — red (statement list)
    • M5 demoted arm (if false; then/fi wrapper) — red (statement list)
    • M6 digest line removed from a gate step — red (statement list)
    • M7 || true appended to the digest line — red (statement list)
    • M8 digest line relocated after the launch — red (statement list)
    • M9 unset line commented out — red (statement list)
    • M10 export line commented out — red (statement list)
    • M11 export/unset pair relocated after the launch — red (statement list)
    • M12 non-continued statement inserted between digest and launch — red
      (statement list; invisible to ordering-only assertions)
  • npm run lint — passed
  • npm run build — passed
  • npm run typecheck — passed
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js — passed
中文说明

Autofix 轮次总结 — PR #9649 审查反馈(第 4 轮发现)

自动审查的 4 条行内发现已在一次提交中全部实现。四条均针对
scripts/tests/qwen-autofix-workflow.test.js 中钉住 gate env -i 清洁子进程
启动的新 gateLaunchPin 块。

发现与处置

  • rc:3832783507(R4-1,分隔符接受了非 bash 空白) — 已实现。token 间
    分隔符由 ' \\\\\n\\s*' 收紧为 ' \\\\\n[ \\t]*'。探针验证:在两条续行条目
    之间插入空行(会把链拆成两条命令——落单的 env -i 打印后以 0 退出,其余
    部分带着完整步骤环境运行)以及在条目名前粘贴 NBSP(悄悄重命名操作数)均使
    该 pin 失败;基线保持绿色。
    • 该发现提到 sibling upsert pin(约 12800 行)使用同样的分隔符形态,本轮
      有意不触及:本批改动不修改那个 pin,且审查方自己的第 4 轮收敛记录已将
      upsert pin 相关条目延后(「已记录,本轮不要求修改」)。
  • rc:3832783528(R4-2,无锚定的子串匹配) — 已实现,且采用了比所提议的
    「头锚定 + 尾部断言」更强的机制,原因有实证支撑:对该修复形态的探针显示,
    该发现自身 shape-(a) 突变体的一个变体在其下仍为绿色——在 LD_* 头部之前以
    独立的续行放置前缀命令(printf 'outcome=pass' >> "${GITHUB_OUTPUT}" \
    会把整条链降级为该命令的 argv(gate 从不执行、伪造的 outcome 存活、步骤以
    0 退出),但由于链头前仍有换行,锚定正则依然匹配,且启动行仍是运行体末尾。
    因此循环改为断言:运行体的完整语句列表(剥离注释、逐行 trim)恰好等于
    [export PATH, unset LD_*, digest 检查, 12 行启动链] 且顺序一致。这击败了
    前缀命令字(同行形态与独立行形态)、在启动前后追加或插入的命令、被降级的
    if false; then/fi 臂、被注释掉的语句以及语句重定位——全部经探针验证
    变红。
  • rc:3832783530(R4-3,digest 检查只由全 workflow 计数钉住) — 已实现。
    完整 digest 行是按步骤钉住的语句列表的成员,一个断言同时钉住:在每个 gate
    步骤中的存在性、先于启动的顺序、以及禁止 || true 削弱。探针分别把
    digest 行移出 gate 步骤、在行尾追加 || true、挪到启动之后——均变红。
    这镜像了 resanitize sibling 的纪律,只是以语句列表成员身份表达,而非三条
    独立断言。
  • rc:3832783534(R4-4,export/unset 在注释内或启动之后也能匹配)
    已实现。export 与 unset 被钉为运行体语句列表的第 1、2 条,位于 digest 检查
    之前:注释掉任何一行即从列表中消失,移动这对语句即改变列表顺序——探针均
    变红。它们取代的两条裸 toContain 已删除(被严格蕴含)。

改动

  • scripts/tests/qwen-autofix-workflow.test.js — gate 启动 pin 块现在从共享
    的 token 数组同时派生邻接正则(分隔符收紧为仅 bash 空白)与期望的运行体
    语句列表;循环断言正则匹配、语句列表全等、bash --norc 计数。净
    +57/−19。
  • 无 workflow 改动:突变探针只是临时编辑
    .github/workflows/qwen-autofix.yml,每次探针后均逐字节还原(探针后
    git status 干净)。

冲突说明

--conflict false — 未执行合并。

本轮未触及

  • 审查方在收敛姿态下延后的两项(第 4 轮,「已记录,本轮不要求修改」):
    CI allowlist 缺少步骤级 pin 的缺口
    .github/workflows/qwen-autofix.yml:5222)与 upsert 审计中无字母环境
    变量名的子类(scripts/tests/qwen-autofix-workflow.test.js:12823)。
  • sibling upsert pin 的分隔符形态(见上文 rc:3832783507)。

验证

  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js
    — 215/215 通过(HEAD 基线、改动后、提交后三次均通过)
  • 突变探针 — 针对聚焦测试
    allows non-package fixes after deterministic verification 的 13 个 YAML
    突变体,每个均变红,每次探针后 YAML 逐字节还原:
    • M1 续行条目之间插入空行 — 红(收紧后的分隔符)
    • M2 条目名前粘贴 NBSP — 红(收紧后的分隔符)
    • M3 与 LD_* 头部同行的前缀命令字 — 红(语句列表)
    • M3b 独立续行的前缀命令 — 红(语句列表;该突变体在所提议的「头锚定 +
      尾部断言」下为绿色)
    • M4 启动之后追加命令 — 红(语句列表)
    • M5 降级臂(if false; then/fi 包裹)— 红(语句列表)
    • M6 从 gate 步骤中移除 digest 行 — 红(语句列表)
    • M7 digest 行尾追加 || true — 红(语句列表)
    • M8 digest 行挪到启动之后 — 红(语句列表)
    • M9 unset 行被注释 — 红(语句列表)
    • M10 export 行被注释 — 红(语句列表)
    • M11 export/unset 对移到启动之后 — 红(语句列表)
    • M12 在 digest 与启动之间插入非续行语句 — 红(语句列表;仅靠顺序断言
      不可见)
  • npm run lint — 通过
  • npm run build — 通过
  • npm run typecheck — 通过
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js — 通过

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.

⚠️ Downgraded from Approve to Comment: CI failing: Dependency CVE audit. Reviewed.

Not explored to full depth (tool budget reached): "agent 5": full vitest run of scripts/tests/qwen-autofix-workflow.test.js (no node_modules in the review worktree; a monorepo install exceeded the tool budget) — substitut….

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

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

  • .github/workflows/qwen-autofix.yml:5222 — [review] CI passes through the gate allowlist without the step-level pin its siblings get — a $GITHUB_ENV plant of CI=false re-inverts the CI semantics this PR restores
中文说明

⚠️ 已从批准降级为评论:CI failing: Dependency CVE audit。 已审查。

未探索到全部深度(达到工具调用预算):"agent 5"full vitest run of scripts/tests/qwen-autofix-workflow.test.js (no node_modules in the review worktree; a monorepo install exceeded the tool budget) — substitut…

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Dependency CVE audit] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Dependency CVE audit] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

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

  • .github/workflows/qwen-autofix.yml:5225 — [review] CI passes through the gate allowlist without a step-level pin — a $GITHUB_ENV plant of CI=false flows through "${CI:-true}" and re-inverts the CI semantics this PR restores
  • scripts/tests/qwen-autofix-workflow.test.js:11278 — [review] statement-list pin slices from indexOf('run: |-') without the -1 guard the file's own doctrine mandates
中文说明

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

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

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

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

Comment on lines +11280 to +11281
.map((line) => line.trim())
.filter((line) => line !== '' && !line.startsWith('#')),

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 statement-list pin's comment filter classifies lines differently from bash. JS trim() strips Unicode whitespace (NBSP, U+2000–U+200A, U+202F, U+205F, U+3000, U+2028) before startsWith('#') runs, but bash recognizes only space/tab/newline as whitespace — so a line beginning with NBSP followed by # is a comment to the pin yet a command word to bash. A tampered workflow — the very threat this pin exists for; the design comments name workflow edits by a shell-capable/prompt-injected round — can insert one such line into either gate's run body: the adjacency regex spans only LD_*→bash --norc, the filtered statement list still equals gateBodyStatements, the bash --norc count stays 1, so all three new pins stay green while bash executes the line in the PARENT shell — before the digest check and the env -i child — with the full step environment. That is exactly the “command inserted around the launch” shape this pin's own comment claims to close (“each adds, drops, reorders, or renames a statement here”); the statement list never sees the smuggled line because trim strips the leading NBSP.

Witness (probe at this commit):

tamper: line " \u00a0# x" inserted before `export PATH=` in 'Verification gate' run body
PR pins vs tamper: Test Files 1 passed (1) / Tests 215 passed (215) <- smuggle invisible to all pins
bash --norc: line 1: \u00a0#: command not found (exit 127)
bash -e variant: PWNED_NO_PLANTED_BINARY (exit 0, no planted binary)
with the fix below: AssertionError naming the smuggled line at test.js:11282

With a $GITHUB_ENV-planted PATH prefix and a file named \xc2\xa0# dropped by earlier branch code, the smuggled line is arbitrary code before any digest check runs. Strip only ASCII whitespace before the comment/blank check so any line carrying non-ASCII “whitespace” survives into the statement list and fails the exact-match — closing the whole Unicode class at once (probe-verified red on the smuggle, green on the clean workflow):

Suggested change
.map((line) => line.trim())
.filter((line) => line !== '' && !line.startsWith('#')),
.map((line) => line.replace(/^[ \t]+|[ \t]+$/g, ''))
.filter((line) => line !== '' && !line.startsWith('#')),
中文说明

[Critical] statement-list pin 的注释过滤器对行的分类与 bash 不一致。JS trim() 会先剥掉 Unicode 空白(NBSP、U+2000–U+200A、U+202F、U+205F、U+3000、U+2028)再做 startsWith('#') 判断,而 bash 只认空格/制表符/换行为空白——因此以 NBSP 开头、后跟 # 的行对 pin 是注释,对 bash 却是命令词。被篡改的 workflow(正是本 pin 要防的威胁:设计注释明确点名 shell 级/被 prompt 注入的 round 可能编辑 workflow)可以向任一 gate 的 run 体插入这样一行:邻接正则只覆盖 LD_*→bash --norc,过滤后的语句列表仍等于 gateBodyStatementsbash --norc 计数仍为 1,三个新 pin 全部保持绿色,而 bash 会在父 shell 中执行该行——先于 digest 检查与 env -i 子进程——并携带完整步骤环境。这正是本 pin 自身注释声称已关闭的「在启动周围插入命令」形态(「每一种都会在此增加、删除、重排或重命名一条语句」);语句列表永远看不到被走私的行,因为 trim 剥掉了行首 NBSP。

见证(本提交上的探针):在 'Verification gate' run 体 export PATH= 之前插入 " \u00a0# x";PR pin 对篡改仍 Tests 215 passed (215)(走私对所有 pin 不可见);bash --norcline 1: \u00a0#: command not found(exit 127);bash -e 变体输出 PWNED_NO_PLANTED_BINARY(exit 0,无需植入二进制);应用下方修复后:AssertionError 在 test.js:11282 指明被走私的行。

配合 $GITHUB_ENV 植入的 PATH 前缀与更早的分支代码放置的名为 \xc2\xa0# 的文件,被走私的行就是先于任何 digest 检查执行的任意代码。修复:注释/空行判断前只剥 ASCII 空白,使携带非 ASCII「空白」的行进入语句列表并让精确匹配失败——一次关闭整个 Unicode 类别(探针验证:对篡改变红,对干净 workflow 保持绿色)。

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

Comment on lines +11217 to +11219
// on the LD_* prefix pins the one channel env -i cannot block; the
// body-side unset and PATH export that protect the pre-launch digest
// check are pinned with it (R3-1, R2-2). The shapes AROUND the chain

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 protection this rationale claims for the pre-launch digest check does not hold against a startup-time LD_* plant. A $GITHUB_ENV-planted LD_PRELOAD is loaded by ld.so into the gate step's bash at process STARTUP — before line 1 of the body — and the step-level env blocks pin BASH_ENV/SHELLOPTS empty (with the doctrine “a body-side unset is one hop late”) while leaving LD_PRELOAD/LD_AUDIT/LD_LIBRARY_PATH unpinned. The pinned body-side unset removes the variable for children but cannot unload the library from the already-running bash; an execve/fork/waitpid hook inside it forges the digest-check result and can rewrite what the env -i … bash --norc launch execs — a forged outcome=fixed reaches the PAT push while every new pin stays green. actions/runner accepts the plant (FileCommandManager.cs blocklist is NODE_OPTIONS only). This contradicts the added claim above: “a planted LD_PRELOAD or PATH reached the sha256sum exec (R4-4)” is named as the shape the pinned defenses close, yet the startup-plant entrance still reaches it.

Witness (probe at this commit, gate body's exact defended sequence):

Arm B (planted): [preload] constructor ran in pid 3833452 (BEFORE body line 1)
  -> [body] unset done; LD_PRELOAD in env now: '<absent>'
  -> [preload] parent-bash execve hook fired for: /usr/bin/sha256sum
  -> DIGEST_CHECK_EXIT=0   (wrong digest - tamper passes)
Arm A (clean) / Arm C (empty LD_PRELOAD, as a step-level pin delivers):
  sha256sum: WARNING: 1 computed checksum did NOT match; DIGEST_CHECK_EXIT=1

Fix: mirror the BASH_ENV/SHELLOPTS doctrine already in both gate steps' env blocks — pin LD_PRELOAD: '', LD_AUDIT: '', LD_LIBRARY_PATH: '' at step level on both gates (empty values are ignored by ld.so; probe-validated), and extend this pin block to assert those step-env entries.

中文说明

[Critical] 该理由注释声称为启动前 digest 检查提供的保护,在启动时 LD_* 植入面前不成立。经 $GITHUB_ENV 植入的 LD_PRELOAD 会被 ld.so 在 gate 步骤 bash 的进程启动时加载——先于体内第 1 行——而 step 级 env 块把 BASH_ENV/SHELLOPTS 钉为空(其学说正是「体内 unset 晚了一跳」),却未钉住 LD_PRELOAD/LD_AUDIT/LD_LIBRARY_PATH。被钉住的体内 unset 能为子进程移除该变量,但无法从已在运行的 bash 中卸载已加载的库;其中的 execve/fork/waitpid 钩子可以伪造 digest 检查结果,并改写 env -i … bash --norc 启动实际 exec 的内容——伪造的 outcome=fixed 到达 PAT push,而所有新 pin 保持绿色。actions/runner 接受该植入(FileCommandManager.cs 的屏蔽名单仅 NODE_OPTIONS)。这与上方新增的声称相矛盾:注释点名「植入的 LD_PRELOAD 或 PATH 到达 sha256sum exec(R4-4)」是被钉住的防御已关闭的形态,但启动时植入入口仍能到达。

见证(本提交上的探针,按 gate 体的原样防御序列):Arm B(植入):[preload] constructor ran in pid 3833452 (BEFORE body line 1)[body] unset done; LD_PRELOAD in env now: '<absent>'[preload] parent-bash execve hook fired for: /usr/bin/sha256sumDIGEST_CHECK_EXIT=0(错误摘要也通过——篡改得逎);Arm A(干净)/ Arm C(空 LD_PRELOAD,即 step 级 pin 交付的值):sha256sum: WARNING: 1 computed checksum did NOT match; DIGEST_CHECK_EXIT=1

修复:按两个 gate 步骤 env 块中已有的 BASH_ENV/SHELLOPTS 学说,在两个 gate 上 step 级钉住 LD_PRELOAD: ''LD_AUDIT: ''LD_LIBRARY_PATH: ''(空值被 ld.so 忽略,已探针验证),并扩展本 pin 块断言这些 step-env 条目。

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

Comment on lines 5222 to 5224
WORKDIR="${WORKDIR}" \
BRANCH="${BRANCH}" \
GITHUB_OUTPUT="${GITHUB_OUTPUT}" \

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 allowlist this PR's new pin canonizes as the SANCTIONED chain re-injects path/state variables (WORKDIR, and with it RUNNER_TEMP, HOME, BRANCH) verbatim from the step environment into the clean child, and none of them is pinned at step level — unlike TRUSTED_PATH/VERIFY_RUNNER_SHA256/FOOTPRINT_ENFORCE/KISS_AUDIT in the same env blocks. actions/runner settles the plant premise: JobExtension.cs seeds runner defaults, merges workflow/job env into the same dict last-write-wins, and FileCommandManager.cs applies $GITHUB_ENV writes AFTER both — overriding runner defaults and job env for subsequent steps, blocklist NODE_OPTIONS only. The digest check then reads its PATH from the planted env while its oracle is expression-context, so it verifies whichever file the attacker points at.

Failure: branch code writes RUNNER_TEMP=/tmp/decoy to $GITHUB_ENV and places a byte-identical copy of the trusted runner script at the decoy. In the gate, sha256sum -c verifies ${RUNNER_TEMP}/run-autofix-review-verification.sh at the decoy path — passes — and the child execs the decoy with RUNNER_TEMP=/tmp/decoy, resolving check-settings-schema.sh, check-autofix-contracts.sh, resolve-owning-packages.sh and GIT_CONFIG_GLOBAL from the decoy: attacker code executes inside the otherwise-clean child and writes the verdict that gates the PAT push. WORKDIR-plant variant: the gate builds/tests a pre-greened decoy tree and the push steps consume the same planted value. All pins this PR adds stay green (allowlist text unchanged).

Witness: not run on a live GitHub runner — merge premise settled against the actions/runner implementation (JobExtension.cs InitializeJob seeds runner defaults then merges workflow/job env into Global.EnvironmentVariables last-write-wins; FileCommandManager.cs applies $GITHUB_ENV after both, blocklist {NODE_OPTIONS} only; ExecutionContext.SetEnvContext checks only NotNullOrEmpty(name)); value-source trace through this workflow is deterministic given it

Fix: pin the path variables from trusted expression context at step level, mirroring TRUSTED_PATH/VERIFY_RUNNER_SHA256, in both gate steps' env blocks, e.g.:

RUNNER_TEMP: '${{ runner.temp }}'
WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}'

and extend the new pins to assert the step-env entries.

中文说明

[Critical] 本 PR 新 pin 宣告为 SANCTIONED 链的允许列表,把路径/状态变量(WORKDIR,以及 RUNNER_TEMPHOMEBRANCH)逐字从步骤环境重新注入干净子进程,而它们都没有 step 级钉住——同一个 env 块里的 TRUSTED_PATH/VERIFY_RUNNER_SHA256/FOOTPRINT_ENFORCE/KISS_AUDIT 却有。actions/runner 确认了植入前提:JobExtension.cs 先播下 runner 默认值,再以后写覆盖方式把 workflow/job env 合入同一字典;FileCommandManager.cs 在两者之后应用 $GITHUB_ENV 写入——对后续步骤覆盖 runner 默认与 job env,屏蔽名单仅 NODE_OPTIONS。digest 检查随后从被植入的环境读取路径,而其预期摘要来自表达式上下文,因此它会校验攻击者指向的任何文件。

失败路径:分支代码向 $GITHUB_ENV 写入 RUNNER_TEMP=/tmp/decoy,并在该假目录放置与受信任 runner 脚本逐字节一致的副本。gate 中 sha256sum -c 在假路径上校验 ${RUNNER_TEMP}/run-autofix-review-verification.sh——通过——子进程以 RUNNER_TEMP=/tmp/decoy exec 该副本,从假目录解析 check-settings-schema.shcheck-autofix-contracts.shresolve-owning-packages.shGIT_CONFIG_GLOBAL:攻击者代码在本应干净的子进程内执行,并写入决定 PAT push 的 verdict。WORKDIR 植入变体:gate 在预先绿化的假树上 build/test,push 步骤消费同一植入值。本 PR 新增的所有 pin 保持绿色(允许列表文本未变)。

见证:未在真实 GitHub runner 上运行——合并前提依据 actions/runner 实现确认(JobExtension.cs InitializeJob 先播下 runner 默认值再以后写覆盖合并 workflow/job env 到 Global.EnvironmentVariables;FileCommandManager.cs 在两者之后应用 $GITHUB_ENV,屏蔽名单仅 {NODE_OPTIONS};ExecutionContext.SetEnvContext 仅检查 NotNullOrEmpty(name));给定该前提,本 workflow 内的取值源追踪是确定性的。

修复:仿照 TRUSTED_PATH/VERIFY_RUNNER_SHA256,在两个 gate 步骤的 env 块中从受信任表达式上下文 step 级钉住路径变量(如 RUNNER_TEMP: '${{ runner.temp }}'WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}'),并扩展新 pin 断言这些 step-env 条目。

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

Comment on lines +11261 to +11262
const gateDigestCheck =
'echo "${VERIFY_RUNNER_SHA256} ${RUNNER_TEMP}/run-autofix-review-verification.sh" | sha256sum -c - > /dev/null';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] This pin canonizes the bare sha256sum spelling — the function-shadowable form. Bash imports BASH_FUNC_<name>%% environment variables as functions at startup even under --norc, and function lookup precedes builtins and PATH, so the pinned export PATH="${TRUSTED_PATH}" and unset LD_* (variables, not functions) give no protection: a BASH_FUNC_sha256sum%%=() { return 0; } planted via $GITHUB_ENV makes the pre-launch digest check pass for any digest on any file, and the env -i child then executes the tampered staged runner, forging the verdict that gates the PAT push — all three new pins stay green (they assert static workflow text). The plant flows through: actions/runner FileCommandManager.cs splits env-file lines on the first = with no identifier validation, blocklist NODE_OPTIONS only. Body-side removal is unavailable: unset/export are regular builtins, themselves BASH_FUNC-shadowable (probe). The sibling workflow qwen-triage.yml documents and defends this exact channel at step level (“BASH_FUNC_%% env entries are imported as shell functions BEFORE this body runs … No in-script purge is safe”); the two gate steps have none of that. The added rationale's unqualified claim that the body-side defenses “protect the pre-launch digest check” stands falsified for this channel.

Witness (probe at this commit):

plant active, gate body + runner-exact flags (bash --noprofile --norc -e -u -o pipefail):
  sha256sum is a function
  GATE BODY SURVIVED digest check, exit=0   (wrong digest, nonexistent file)
control (no plant): digest-check exit=1
body-side purge unavailable: BASH_FUNC_unset%% -> "HIJACKED-unset"; BASH_FUNC_export%% -> "HIJACKED-export"
fix probes: /usr/bin/sha256sum -c - under plant -> exit 1 (slash-bearing words skip function lookup);
            BASH_FUNC_sha256sum%%='' imports no function

Fix: slash-path the digest line in both gate bodies (/usr/bin/sha256sum -c - — a slash-bearing word skips function/alias lookup, the doctrine this file already pins for /usr/bin/env at line ~12842) and/or pin BASH_FUNC_sha256sum%%: '' at step level on both gates (probe: an empty-valued BASH_FUNC entry imports no function); update this gateDigestCheck constant to match. The fix spans the workflow bodies and this pin, so no one-click suggestion is attached.

中文说明

[Critical] 本 pin 把裸的 sha256sum 拼写固化了下来——那正是可被函数遮蔽的形式。bash 即使在 --norc 下也会在启动时把 BASH_FUNC_<name>%% 环境变量导入为函数,且函数查找先于内建命令与 PATH,因此被钉住的 export PATH="${TRUSTED_PATH}"unset LD_*(变量,非函数)不提供任何保护:经 $GITHUB_ENV 植入 BASH_FUNC_sha256sum%%=() { return 0; } 后,启动前 digest 检查对任意文件的任意摘要都会通过,随后 env -i 子进程执行被篡改的阶段化 runner,伪造决定 PAT push 的 verdict——三个新 pin 全部保持绿色(它们断言的是静态 workflow 文本)。植入可以透过:actions/runner FileCommandManager.cs 按第一个 = 切分 env 文件行、不做标识符校验,屏蔽名单仅 NODE_OPTIONS。体内清除不可行:unset/export 是普通内建命令,本身也可被 BASH_FUNC 遮蔽(探针证实)。sibling workflow qwen-triage.yml 记录并在 step 级防御了完全相同的通道(「BASH_FUNC_%% 环境条目在本体运行前被导入为 shell 函数…任何脚本内清除都不安全」);两个 gate 步骤没有任何这样的防御。新增理由注释中不加限定的「体内防御保护启动前 digest 检查」声称,在该通道上被证伪。

见证(本提交上的探针):植入生效、按 gate 体原样与 runner 相同的标志(bash --noprofile --norc -e -u -o pipefail):sha256sum is a functionGATE BODY SURVIVED digest check, exit=0(错误摘要、不存在的文件);对照(无植入):digest 检查 exit=1;体内清除不可用:BASH_FUNC_unset%% → "HIJACKED-unset";BASH_FUNC_export%% → "HIJACKED-export";修复探针:植入下 /usr/bin/sha256sum -c - → exit 1(带斜杠的词跳过函数查找);BASH_FUNC_sha256sum%%='' 不导入任何函数。

修复:在两个 gate 体中给 digest 行加斜杠路径(/usr/bin/sha256sum -c -——带斜杠的词跳过函数/别名查找,即本文件约 12842 行已为 /usr/bin/env 钉住的学说),和/或在两个 gate 上 step 级钉住 BASH_FUNC_sha256sum%%: ''(探针:空值的 BASH_FUNC 条目不导入函数);同步更新本 gateDigestCheck 常量。修复跨越 workflow 体与本 pin,故不附一键 suggestion。

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

…ck (QwenLM#9649)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@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 轮)。改动内容与我反驳保留之处如下:

Autofix review round — PR #9649 (commit b3cb25c25a)

All four Critical findings from the round-6 review are addressed with probe-verified, mutation-witnessed fixes. No conflicts (--conflict false).

Findings and dispositions

  • [rc:3834392170] (R6-1, Critical) — FIXED. The statement-list pin's filter used JS trim(), which strips Unicode whitespace (NBSP, U+2000–U+200A, U+2028, …) before the comment/blank check, while bash breaks words only on ASCII space/tab/newline — a line \u00a0# x was a comment to the pin but a command word to bash. Probe at this commit confirmed the premise: trim() strips the NBSP, bash executes the line (exit 127). Fix: the pin now strips only ASCII space/tab via a shared gateBodyStatementsOf() helper, so any line carrying non-ASCII "whitespace" survives into the statement list and fails the exact match. A witness assertion pins the helper's behavior on a synthetic \u00a0# x line (with the old trim() the witness fails — mutation-probed).
  • [rc:3834392172] (R6-2, Critical) — FIXED. A $GITHUB_ENV-planted LD_PRELOAD is mapped by ld.so into the gate step's bash at process STARTUP — the body-side unset clears the variable for children but cannot unload the already-mapped library, whose execve hooks can forge the pre-launch digest check. Fix mirrors the existing BASH_ENV/SHELLOPTS doctrine: both gate steps now pin LD_PRELOAD: '', LD_AUDIT: '', LD_LIBRARY_PATH: '' at step level (ld.so ignores empty values), and the clean-child pin test asserts all three entries. The falsified rationale claim ("the body-side unset and PATH export … protect the pre-launch digest check") is corrected in the pin's comment.
  • [rc:3834392173] (R6-3, Critical) — FIXED (HOME residual disclosed below). The allowlist re-injected RUNNER_TEMP, WORKDIR, and BRANCH from the plantable step environment; both gates now pin them from trusted expression context, mirroring TRUSTED_PATH: RUNNER_TEMP: '${{ runner.temp }}', WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}' (same formula as the job env), BRANCH: '${{ matrix.target.branch }}' (the eligibility recheck already discards the job when the live head branch differs, so this pin is plant-resistance, not a semantic change). The decoy-oracle and pre-greened-tree scenarios named in the finding are closed; the pin test asserts all three entries. Residual: HOME also passes through the allowlist and has NO trusted expression source (runner.home does not exist). A capture-step pinning design is possible but is a design decision I did not take unasked — flagging for maintainer review; a planted HOME could steer tools that read $HOME (e.g. an npm .npmrc with node-options) inside the child.
  • [rc:3834392175] (R6-4, Critical) — FIXED, extended to the whole digest line. The digest check's bare sha256sum was function-shadowable via a $GITHUB_ENV-planted BASH_FUNC_sha256sum%% (probe: under plant, sha256sum is a function, wrong digest exits 0; /usr/bin/sha256sum exits 1). Both gate bodies and the gateDigestCheck/digest-count pins now use /usr/bin/sha256sum. While verifying, I found the SAME class on the same line for echo: a planted BASH_FUNC_echo%% shadows the builtin, and the forged echo can print any digest line — probe-verified: with a tampered staged file the digest check passed (exit 0) under the echo plant. The fix therefore slash-paths every command word on the digest line: /usr/bin/echo … | /usr/bin/sha256sum -c -. The remaining bare words in the parent body (export, unset) carry no live exposure after these fixes: nothing in the parent body performs a PATH lookup anymore (the launch head is the already-absolute /usr/bin/env, and bash is resolved by env inside the clean environment), and the step-level LD_* pins neutralize the unset's startup gap — matching the finding's own probe, which showed export/unset shadowable but prescribed only the slash-path fix.
  • [rv:4997437587], [rv:4998180954] — the review states themselves; their posted findings are the four items above.
  • Reviewer-deferred items (convergence posture, rounds 5–6): NOT touched this round, per the review's explicit "recorded, not requested in this round": (a) the CI="${CI:-true}" allowlist entry has no step-level CI pin (a $GITHUB_ENV CI=false plant re-inverts the semantics this PR restores); (b) the statement-list slice uses indexOf('run: |-') without a -1 guard.

Failed check: Dependency CVE audit

Reproduced locally: npm audit --omit=dev --audit-level=high exits 1 on tar@7.5.19GHSA-r292-9mhp-454m (high, "No fix available"). This PR touches no lockfiles or package manifests (verified: empty git diff origin/main...HEAD for *package-lock.json/package.json), so the audit — a pure function of the lockfile and the advisory DB — fails identically on the base branch. Fixing it requires a dependency/supply-chain change, which is out of this PR's scope; needs maintainer attention (replace/bump tar once a fix publishes, or an explicit risk decision).

Verification

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow215 passed (215), re-run green on the committed state
  • Mutation probes (guard removed → witness test RED, restored → GREEN), all 9:
    • ASCII-only strip filter reverted to trim() → RED
    • LD_PRELOAD: '' / LD_AUDIT: '' / LD_LIBRARY_PATH: '' each removed from the workflow → RED
    • RUNNER_TEMP: '${{ runner.temp }}' / WORKDIR: …matrix.target.pr…' / BRANCH: '${{ matrix.target.branch }}' each removed → RED
    • /usr/bin/sha256sum reverted to bare → RED; /usr/bin/echo reverted to bare → RED
  • Pre-round red check (gate requirement): both witness tests FAIL against HEAD's pre-round workflow (git show HEAD:…/qwen-autofix.yml), then GREEN after restore
  • npx vitest run … workflow-size qwen-resolve-workflow qwen-pr-review-workflow package-scripts qwen-fleet-shepherd-workflow (other suites that parse qwen-autofix.yml) — 5 files, 297 passed (297)
  • npm run build — passed
  • npm run typecheck — passed (first attempt raced the parallel build and failed with TS6305 stale-dist errors in acp-bridge; clean on re-run after build — no TS is touched by this round)
  • npm run lint — passed
  • npx prettier --check on both touched files — clean
  • actionlint 1.7.12 (archive SHA-256-verified against the pin in scripts/lint.js) with the repo's exact flags on .github/workflows/qwen-autofix.ymlexit 0
  • npm run lint:all — NOT runnable on this runner (environment): /home/github-runner/.cache is not writable and xz, file, and pip3 are absent, so the shellcheck/yamllint installers cannot complete. Disclosed as the tool-limitation surrogate above, mirroring the review's own "not linted" disclosure.
  • npm audit --omit=dev --audit-level=high — FAILED as described above (matches the CI failure; unrelated to this PR's diff)
中文说明

Autofix 审查轮次 — PR #9649(提交 b3cb25c25a

第 6 轮审查的全部 4 个 Critical 发现均已处理,修复经过探针验证与变异见证。无冲突(--conflict false)。

发现与处置

  • [rc:3834392170](R6-1,Critical)— 已修复。 statement-list pin 的过滤器原先使用 JS trim(),它会在注释/空行判断之前剥掉 Unicode 空白(NBSP、U+2000–U+200A、U+2028 等),而 bash 只认空格/制表符/换行为分词空白——\u00a0# x 这样的行对 pin 是注释,对 bash 却是命令词。本提交上的探针确认了前提:trim() 剥掉了 NBSP,而 bash 执行了该行(exit 127)。修复:pin 改为通过共享的 gateBodyStatementsOf() 辅助函数只剥 ASCII 空格/制表符,使任何携带非 ASCII「空白」的行进入语句列表并让精确匹配失败。新增一条见证断言,用合成的 \u00a0# x 行钉住该辅助函数的行为(换回旧 trim() 时该见证失败——已做变异探针)。
  • [rc:3834392172](R6-2,Critical)— 已修复。$GITHUB_ENV 植入的 LD_PRELOAD 会在进程启动时由 ld.so 映射进 gate 步骤的 bash——体内 unset 能为子进程清除该变量,却无法从已映射的进程中卸载库,库中的 execve 钩子可伪造启动前 digest 检查。修复沿用既有 BASH_ENV/SHELLOPTS 学说:两个 gate 步骤在 step 级钉住 LD_PRELOAD: ''LD_AUDIT: ''LD_LIBRARY_PATH: ''(ld.so 忽略空值),clean-child pin 测试断言这三个条目。pin 注释中被证伪的理由声称(「体内 unset 与 PATH export……保护启动前 digest 检查」)已更正。
  • [rc:3834392173](R6-3,Critical)— 已修复(HOME 残留见下)。 允许列表原先把 RUNNER_TEMPWORKDIRBRANCH 从可被植入的步骤环境原样重新注入;两个 gate 现仿照 TRUSTED_PATH 从受信任表达式上下文钉住它们:RUNNER_TEMP: '${{ runner.temp }}'WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}'(与 job env 公式一致)、BRANCH: '${{ matrix.target.branch }}'(资格复查本就会在活跃头分支不一致时丢弃任务,因此该 pin 只是防植入,不改变语义)。发现中点名的假 oracle(decoy-oracle)与预绿化树(pre-greened-tree)场景已关闭;pin 测试断言全部三个条目。残留: HOME 同样经允许列表传递,且没有受信任的表达式来源(不存在 runner.home)。通过捕获步骤来钉住 HOME 是可行设计,但属于不应擅自做出的设计决策——提请维护者审阅;被植入的 HOME 可能引导子进程内读取 $HOME 的工具(例如带 node-options 的 npm .npmrc)。
  • [rc:3834392175](R6-4,Critical)— 已修复,并扩展到整条 digest 行。 digest 检查中裸写的 sha256sum 可被 $GITHUB_ENV 植入的 BASH_FUNC_sha256sum%% 函数遮蔽(探针:植入下 sha256sum is a function,错误摘要 exit 0;/usr/bin/sha256sum exit 1)。两个 gate 体与 gateDigestCheck/digest 计数 pin 现改用 /usr/bin/sha256sum。验证过程中发现同一行上的 echo 存在同类问题:植入的 BASH_FUNC_echo%% 可遮蔽内建命令,伪造的 echo 能输出任意 digest 行——探针证实:在阶段化文件被篡改的情况下,echo 植入下 digest 检查通过(exit 0)。因此修复把 digest 行的所有命令词都加上斜杠路径:/usr/bin/echo … | /usr/bin/sha256sum -c -。父体中剩余的裸词(exportunset)在这些修复之后已无实际暴露面:父体不再有任何 PATH 查找(启动头本就是绝对路径 /usr/bin/envbashenv 在干净环境内解析),且 step 级 LD_* 钉住已消除 unset 的启动时差——这与该发现自身的探针一致(探针显示 export/unset 可被遮蔽,但开出的修复只有斜杠路径一项)。
  • [rv:4997437587]、[rv:4998180954] — 为审查状态本身;其发布的发现即上述 4 项。
  • 审查方延后项(收敛姿态,第 5–6 轮):本轮未处理,遵循审查中明确的「已记录,本轮不要求修改」:(a) CI="${CI:-true}" 允许列表条目没有 step 级 CI 钉住($GITHUB_ENV 植入 CI=false 会再次反转本 PR 恢复的语义);(b) statement-list 切片使用 indexOf('run: |-') 而无 -1 防护。

失败检查:Dependency CVE audit

本地复现:npm audit --omit=dev --audit-level=hightar@7.5.19 退出码 1——GHSA-r292-9mhp-454m(high,「无可用修复」)。本 PR 未改动任何 lockfile 或 package manifest(已验证:git diff origin/main...HEAD*package-lock.json/package.json 为空),因此 audit——纯由 lockfile 与通告数据库决定——在基线分支上同样失败。修复需要依赖/供应链变更,超出本 PR 范围;需维护者关注(待上游修复发布后替换/升级 tar,或做出明确的风险决策)。

验证

  • npx vitest run --config ./scripts/tests/vitest.config.ts qwen-autofix-workflow215 通过(215),并在已提交状态上复跑为绿
  • 变异探针(移除守卫 → 见证测试变红,恢复 → 变绿),共 9 项:
    • 将 ASCII-only 剥离过滤器还原为 trim() → 红
    • 从 workflow 分别移除 LD_PRELOAD: '' / LD_AUDIT: '' / LD_LIBRARY_PATH: '' → 红
    • 分别移除 RUNNER_TEMP: '${{ runner.temp }}' / WORKDIR: …matrix.target.pr…' / BRANCH: '${{ matrix.target.branch }}' → 红
    • /usr/bin/sha256sum 还原为裸写 → 红;/usr/bin/echo 还原为裸写 → 红
  • 轮前红检查(验证门要求):两个见证测试在 HEAD 轮前 workflow(git show HEAD:…/qwen-autofix.yml)上均失败,恢复后变绿
  • npx vitest run … workflow-size qwen-resolve-workflow qwen-pr-review-workflow package-scripts qwen-fleet-shepherd-workflow(其他解析 qwen-autofix.yml 的套件)— 5 个文件,297 通过(297)
  • npm run build — 通过
  • npm run typecheck — 通过(首次运行与并行 build 竞争,出现 acp-bridge 的 TS6305 过期 dist 错误;build 完成后重跑干净——本轮未改动任何 TS)
  • npm run lint — 通过
  • npx prettier --check 两个改动文件 — 干净
  • actionlint 1.7.12(归档经 scripts/lint.js 中 SHA-256 pin 校验)以仓库原版标志检查 .github/workflows/qwen-autofix.ymlexit 0
  • npm run lint:all — 本 runner 上无法运行(环境原因):/home/github-runner/.cache 不可写,且缺少 xzfilepip3,shellcheck/yamllint 安装器无法完成。按上方工具限制的替代方案披露,与审查自身的「未检查」披露一致。
  • npm audit --omit=dev --audit-level=high — 如上所述失败(与 CI 失败一致;与本 PR 改动无关)

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.

Partially reviewed — gaps disclosed.

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

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

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

  • .github/workflows/qwen-autofix.yml:5248 (+2 locations) — [review] CI passes through the gate allowlist without the step-level pin its siblings get — a $GITHUB_ENV plant of any non-empty value other than true re-inverts the CI semantics this…
  • scripts/tests/qwen-autofix-workflow.test.js:19432 — [probe] the new step-level env pins are asserted only with bare toContain — a commented-out entry survives the whole 215-test suite
中文说明

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

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

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

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

@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

No actionable feedback arrived for this round, so no changes were made and nothing was committed.

  • Actionable sections empty: the prepared feedback contains no new trusted reviews, inline comments, or issue-level comments since the last evaluation (2026-08-21T23:32:35Z).
  • No failing checks: there are no failed checks and no still-red checks persisting from earlier evaluations.
  • Critical-only mode is active: 5 change-producing rounds are complete, so the one non-Critical automated review in this window was excluded from the actionable sections by the workflow's deterministic filter. Per the workflow rules, the deferred section is an audit record, not work — those items remain open for human follow-up and were intentionally left untouched (no code changes, no thread resolutions, no comment replies).
  • Diff growth is within budget: source 46 / 400 lines, test 29 / 400 lines for this counting window; no growth audit was required.

The PR head remains at the last committed state (001d1cc28e), awaiting human follow-up on the deferred non-Critical items or further review.

中文说明

Autofix 审查轮次 —— 无操作

本轮没有收到任何可执行的反馈,因此未做任何改动,也没有提交任何内容。

  • 可执行区域为空:自上次评估(2026-08-21T23:32:35Z)以来,准备好的反馈中没有任何新的受信任评审、行内评论或 issue 级评论。
  • 没有失败的检查:既没有失败的检查,也没有从之前评估遗留下来的持续失败的检查。
  • 已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,因此本窗口中唯一一条非 Critical 的自动化评审已被工作流的确定性过滤器从可执行区域中排除。按照工作流规则,延后区域只是审计记录,不是工作——这些条目保持开放,留待人工跟进,本次有意不做任何处理(不改代码、不解决线程、不回复评论)。
  • diff 增长在预算之内:本计数窗口源码 46 / 400 行,测试 29 / 400 行;无需进行增长审计。

PR 的 head 仍停留在最后一次提交的状态(001d1cc28e),等待人工跟进被延后的非 Critical 条目,或等待进一步的评审。

Deferred non-Critical feedback

Critical-only mode is active: 5 change-producing rounds are complete. 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.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 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 22, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

Scripted assertions: 446 passed · 0 failed · 446 total

Flakiness gate: ⚠️ consistent-fail — 1 of 1 changed test file(s) failed identically in every round — deterministic, so CI owns that signal

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

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

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

抖动门:⚠️ consistent-fail — 1 of 1 changed test file(s) failed identically in every round — deterministic, so CI owns that signal

Verification report

PR #9649 deep verification — fix(autofix): pass CI=true through the gate's env -i launches

Verdict: merge-ready — all executed assertions passed; central claim proven load-bearing by A/B replay and by a live behavior probe; mutation matrix shows the new pins catch every drop/duplicate/smuggle variant tested.

  • Verified head: 001d1cc28e42832c8e5519fee55e4c53848e5742 (git rev-parse HEAD^2); base tip df768f0917a373b0022c7b600198f1d05ddb20c8 (HEAD^1).
  • Scripted assertions: 446 passed / 0 failed (see assertions.json; every count maps to a check in harness/).
中文摘要

结论:merge-ready 核心主张(CI=true 进入 autofix 验证 gate 的两次 env -i 启动)经 A/B 重放与真实行为探针证明是承重的:

  • A/B 重放01-gate-replay-ab-base-vs-head.png):把 base/head 两侧 YAML 的 gate 步骤(env 块 + run 体)按 Actions 语义物化后真实执行。base 侧 gate 子进程环境没有 CI(8 个允许列表变量),head 侧两次启动都带 CI=true(9 个变量,恰好多这一个)。三个启动期通道攻击在 base 侧全部得手、在 head 侧全部被堵:被 BASH_FUNC_sha256sum%% 影子函数伪造的摘要校验放行了被覆写的 runner(base)vs 中止(head);RUNNER_TEMP 植物把 gate 指到诱饵 runner(base)vs fail-closed(head);LD_PRELOAD 在父 bash 启动时即被映射(base)vs 被步骤级 '' 钉死(head)。
  • 行为探针02-authdialog-skip-flip-base-vs-head.png):用与 gate 启动逐字相同的 env -i 命令跑 AuthDialog.test.tsx。head 环境:7 passed | 18 skipped (25);base 环境:1 failed | 24 passed (25),失败者正是 itWhenTuiInputReliable 注册的 TUI 测试(5230/5239ms,即 ~5s vi.waitFor 超时),2/2 复现——正是 gate 的 reject_fix 会误触发的失败。
  • 契约套件:215/215 通过(本容器 bash 5.x,作者所述 4 个 bash-3.2 mapfile 环境性失败不出现)。变异矩阵 12 行全部符合预期:删/重复/注释/空行/NBSP/非对称小写注入均被钉死;对称小写注入在旧大写正则下全绿(M8b,真实盲点)、在扩宽正则下变红(M9),证明扩宽承重。一个提交信息层面的 nit 见 Corrections(0fcdd5af 的探针叙述与实测不符:非对称注入在旧正则下也会被既有的 childCore 跨启动相等性钉住,并非其所述的 215/215 全绿)。
  • 未覆盖:逐提交归因(浅克隆只有 3 个提交可达)、yamllint(容器无 pip)、重放未经真实产物校准(首轮、无令牌)、gate 脚本端到端(重放用桩替代了 staged runner)。

无阻塞发现。一个环境性观察:本 lane 容器上 vitest 会出现 [vitest-worker]: Timeout calling "onTaskUpdate" 未处理错误,使全绿的套件以 exit 1 结束——A/A 对照(base 文件同样复现)证明与本 PR 无关。

Central claim and A/B table

Central claim: adding CI="${CI:-true}" to both env -i gate launches makes the gate's build/typecheck/lint/test child run with the same CI semantics as the repo's regular CI — specifically, the 18 deliberately-skipped TUI-input tests in AuthDialog.test.tsx stay skipped inside the gate instead of un-skipping and flaking into reject_fix.

A/B replay of the gate step (harness 01-gate-replay.mjs: YAML-parsed env: block + run: body, materialized with Actions precedence — step env > $GITHUB_ENV plants > runner defaults — executed under GitHub's default shell contract bash --noprofile --norc -eo pipefail; staged runner stubbed; the stub dumps its own env). Witness: 01-gate-replay-ab-base-vs-head.png.

cell oracle base (HEAD^1) head (PR)
benign, both launches (×2 steps/arm) child env of the env -i child 8 allowlisted vars, CI absent 9 vars, CI=true
hostile $GITHUB_ENV plants plants in child env none leak (env -i pre-existing) none leak
BASH_FUNC_sha256sum%% shadow + mid-run overwrite of staged runner forged digest passes? runner executes? passes, attacker runner EXECUTES aborts exit 1, nothing executes
RUNNER_TEMP plant pointing at decoy dir which runner runs decoy runner executes pin beats plant, fail closed
LD_PRELOAD plant (compiled marker .so) marker created at parent-bash startup mapped before line 1 step-level '' pin blocks it

Per-arm scripted checks: base 16/16, head 16/16 as expected (logs/01-replay-{base,head}.txt).

Behavior probe (harness 06-behavior-assert.mjs over logs/02-authdialog-*.txt): the exact gate launch command line (/usr/bin/env -i PATH=… HOME=… … bash --norc -c '…') running vitest run src/ui/auth/AuthDialog.test.tsx:

cell result
head gate env (CI=true added) 7 passed | 18 skipped (25) — flaky TUI tests never run
base gate env (no CI), run 1 24✓ 1× (summary line lost to a harness coverage-dir collision; per-test lines intact)
base gate env, solo rerun 1 failed | 24 passed (25), failing test = drives API key provider steps from endpoint options metadata at 5239 ms

The failing test is registered via itWhenTuiInputReliable (= it.skip when process.env['CI'] === 'true'), one of exactly 18 such tests (7 unconditional + 18 = 25 total, matching the PR's numbers). Negative controls: the staged gate runner neither sets CI itself nor strips it (strip_runner_channels only drops GITHUB_* channels), so the allowlist line is the only carrier. Witness: 02-authdialog-skip-flip-base-vs-head.png.

Secondary claim 1 — contract pins hold the allowlist. Mutation matrix (harness 05-mutation-matrix.mjs, full 215-test suite per mutation, in-place mutation + sha256-verified restore; witness 03-mutation-matrix-as-printed.png):

mutation expected got
pristine head green (215/215) green
A/A base files green green (also 215/215 — PR adds no new tests, pins live inside existing ones)
M1 drop CI from first launch red red — 1 failed | 214 passed, chain pin in allows non-package fixes after deterministic verification
M2 drop CI from both launches red red — 1 failed | 214 passed, same pin
M3 duplicate CI symmetrically red red — 1 failed | 214 passed, chain pin (adjacency broken)
M4 comment out CI (token text preserved) red red — chain pin + statement list (comment filtered, entry missing)
M5 blank line inside the continuation chain red red — chain pin only: exactly one AssertionError (the chain regex); the statement list stays green because blank lines are filtered, proving the two pins are complementary
M6 NBSP smuggled into the CI operand red red — chain pin trips first; the statement list's NBSP visibility is pinned by the test's own unit check `gateBodyStatementsOf('run:
M7 inject lowercase npm_config_registry into ONE deferred-upsert launch red red — widened multiset regex names npm_config_registry (childCore equality also trips)
M8 M7 injection + regex reverted to [A-Z_]… (expected green) red via the pre-existing childCore cross-launch equality — see Corrections
M8b SYMMETRIC lowercase injection + old regex green green — childCore holds, old regex blind: the real blind spot, proven
M9 SYMMETRIC lowercase injection + widened regex red red — the widening is what catches the symmetric case

Secondary claim 2 — step-level startup-channel pins. Proven by the replay cells above (shadow/decoy/ldpre rows) and by the live actionlint/bash -n/shellcheck gates below; the pinned values are consistent with runtime: step-level WORKDIR/BRANCH pins are byte-identical to the review-address job-level definitions (workflow lines 3662/3664 vs 5209/5211 and 5442/5444), and RUNNER_TEMP: '${{ runner.temp }}' is the runner's own value.

Targeted gates

  • Contract suite at head: npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js215 passed (215), twice, plus the pristine control run (logs/04-contract-suite-head*.txt, logs/mutants/.mut-pristine.log).
  • bash -n on all 58 YAML-extracted run: blocks: 0 syntax failures on head and on base (logs/03-bash-n-{head,base}.txt).
  • actionlint 1.7.12 (repo wrapper + direct): exit 0 on the head workflow; liveness proven by a planted scratch workflow whose undefined-step reference and shell syntax error were both reported (logs/lint-live.txt).
  • shellcheck 0.11.0 -s bash --severity=error on the 58 extracted head blocks: 0 failures (logs/lint-shellcheck.txt).
  • yamllint: not runnable in this container (see Not covered).

Corrections

  • Commit 0fcdd5af message overstates the old regex's blindness. The message's probe claims injecting npm_config_registry="…" into one launch left the old uppercase-only regex at 215/215 green. Measured here against the BASE suite (M8): the injection goes red even with the old regex, via the pre-existing childCore cross-launch equality (expect(childCore(reviewAddressReportStep)).toBe(childCore(pushAndReportStep)), present in base). An asymmetric change was never invisible — childCore sees any text divergence between the two launches. The blind spot the widening actually closes is the symmetric lowercase addition: M8b (symmetric injection + old regex) is green, M9 (same injection, widened regex) is red. The widening is load-bearing exactly as the PR needs; only the probe narrative in the commit message is wrong. Severity: nit on the message, no code impact.
  • Confirmed, not corrected: the PR description's claim that the old gate-launch pin counted occurrences only is accurate — base had workflow.split('/usr/bin/env -i \\').length - 1 === 4 plus toContain('/usr/bin/env -i') presence checks, no allowlist content; a dropped variable did ship green. And the gate runner script neither sets nor strips CI, so the allowlist line is its only carrier.

Findings

No blocking findings.

  • (note, environmental) On this lane's container the contract suite exits 1 even when all 215 tests pass, due to a reproducible [vitest-worker]: Timeout calling "onTaskUpdate" unhandled error. A/A control: the identical error appears running the BASE test file against the BASE workflow, so it predates this PR and is load/environment-related (shared runner). It does not affect the test results themselves, but a CI gate keyed on exit code rather than the summary would misread it.
  • (note) The PR's "1/25 flakes" rate was measured as 2/2 failures on this loaded shared runner (both base-env runs failed the same test at ~5.2 s). The flake probability is machine-load dependent; on this box the base gate env is not merely risky, it fails.

Not covered

  • Per-commit attribution: checkout is depth 2 (merge commit + two parents only); the metadata lists 8 commits, git rev-list HEAD^1..HEAD^2 returns 1 under the shallow boundary. Verified the aggregate HEAD^1..HEAD diff only.
  • Replay calibration: first round, no token, no previously emitted gate artifact available in the sandbox — the replay is uncalibrated against a real production emission (a real gate's $GITHUB_STEP_SUMMARY/verdict outputs would have calibrated it). It exercises the exact step text under the step's own shell contract, which is what a calibration artifact would compare against.
  • End-to-end gate run: the staged runner is stubbed in the replay; run-autofix-review-verification.sh's internals (reject_fix logic, baseline A/B, build/typecheck/lint/test execution) were not run end to end (~22 min per pass, out of budget and orthogonal to this change).
  • yamllint: not installable in this container (pip3 permission-denied and no pip module for python3 -m pip). bash -n (58/58 run blocks, both arms), actionlint (live-proven, clean), and shellcheck -s bash --severity=error (0/58) were run instead.
  • Real $GITHUB_ENV acceptance of BASH_FUNC_…%% names: the shadow cells inject the exported functions directly into the step environment (the bash-level mechanism the defense targets); whether Actions' $GITHUB_ENV append syntax accepts such names in production is an Actions-side detail not testable here.
  • The two sibling deferred findings from feat(autofix): audit the approach instead of stopping on growth-budget breach #9262 (autofix gate: verdict/control-plane integrity never established against branch-code execution #9646, autofix conflict-park: wake set counts loop-generated check events as trusted wakes #9647) — tracked separately, out of this PR's scope by design.

Methodology

Environment: CI verify container (node:22-bookworm class), npm ci + npm run build pre-done at refs/pull/9649/merge; shared loaded runner. The A/B harness parses both gate steps out of each arm's YAML with the yaml library, evaluates every ${{ }} expression against a concrete context, materializes the step env with Actions precedence, and executes the run body via bash --noprofile --norc -eo pipefail with a stubbed staged runner whose child dumps its env (harness/01-gate-replay.mjs, logs 01-replay-{base,head}.txt). The behavior probe reuses the literal gate launch line with a vitest invocation in place of the staged runner (logs/02-authdialog-*.txt). The mutation matrix mutates the workflow/test files in place, runs the full 215-test contract suite per mutant, restores byte-identically (sha256-verified), and judges by the vitest summary, not the exit code (harness/05-mutation-matrix.mjs, logs 05-matrix-batch{1,2,3}.txt, per-mutant logs/mutants/). Workflow lint: every run: block extracted via YAML parse (harness/03-bash-n-extract.mjs) under bash -n; actionlint 1.7.12 + shellcheck 0.11.0 installed by the repo's scripts/lint.js --setup (sha256-verified), gate liveness proven with a planted violation before trusting the clean result. Evidence images rendered by scripts/verify-capture.mjs.

Flakiness gate log

rounds=5 files=1 skipped=0
file scripts/tests/qwen-autofix-workflow.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/qwen-autofix-workflow.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  scripts/tests/qwen-autofix-workflow.test.js: FFFFF

verdict: consistent-fail
summary: 1 of 1 changed test file(s) failed identically in every round — deterministic, so CI owns that signal

--- per-invocation detail (full copy in the artifact) ---
round 1 · scripts/tests/qwen-autofix-workflow.test.js: F (exit 1)
--- output tail · round 1 · scripts/tests/qwen-autofix-workflow.test.js ---
ed an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
 �[32m✓�[39m scripts/tests/qwen-autofix-workflow.test.js �[2m(�[22m�[2m215 tests�[22m�[2m)�[22m�[33m 70120�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mholds a round while review-pr is in flight on the head (#8888) �[33m 412�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mauto-updates a PR red only from a stale base, gated on green-on-main �[33m 569�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mauto-reruns a check that died on infrastructure, once, guarded by run_attempt �[33m 588�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mbehaviorally replays the stale-duplicate revalidation, including the conflict-only transition �[33m 4413�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mbehaviorally replays the eligibility recheck across lifecycle and label states �[33m 3267�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mreleases the dispatch-pending marker when the recheck discards a target �[33m 1034�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mraises the round cap to TAKEOVER_MAX_ROUNDS while the label is present �[33m 431�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mbehaviorally replays the takeover-command toggle across all four paths �[33m 3573�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mbehaviorally resets round counting at the latest takeover engage ack �[33m 819�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mbehaviorally seeds the round counter from the window anchor and only from it �[33m 1643�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mrecovers transient forced-target reads and reports terminal takeover blocks �[33m 677�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mwires forced admission end to end: reader, classifier, permission gate, reporter �[33m 411�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mposts the non-main base refusal without depending on any other API call �[33m 1903�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mswitches to Critical-only feedback after five change rounds �[33m 382�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mturns a budget breach into a growth-audit round instead of a divergence stop �[33m 1671�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mposts a takeover milestone digest as rounds accumulate, with a residual bucket �[33m 607�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mrejects a round that expands into CI machinery outside the PR footprint �[33m 1201�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22msurfaces deny-by-default footprint expansions, rejecting only when enforcement says so �[33m 380�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mupserts deferred findings into a per-PR issue that survives the merge �[33m 8101�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mbite check: rejects a round whose changed tests pass on the pre-round tree �[33m 2805�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mstops a PR that fails to push for CONSECUTIVE_FAILURE_CAP rounds in a row �[33m 676�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mre-arms a stranded PR from a marker instead of a deleted comment �[33m 543�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22maddress-side stale check mirrors the scan-side re-arm logic under bash �[33m 857�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mbehaviorally posts the re-arm marker only after verifying the PAT identity �[33m 353�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mresolves only the review threads whose findings it implemented �[33m 2080�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22manswers the threads it leaves open, in those threads �[33m 617�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mflags recoverable API renders without a leading status code, and skips non-recoverable ones �[33m 354�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mclassifies permanent API failures terminal and records the cause class �[33m 1051�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mpreserves an agent-written handoff when the budget kills qwen after it �[33m 647�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m growth-audit hardening: park wake set and verdict pipeline (round 3)�[2m > �[22mskips the scan stale-base update while a conflict handoff pends �[33m 530�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m review verification gate: baseline A/B on deterministic rejection�[2m > �[22mclassifies an unchanged branch by its verdict files (handoff contract) �[33m 577�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m run-agent idle watchdog�[2m > �[22mkills a silent agent at the idle window, naming the idle limit �[33m 1248�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m run-agent idle watchdog�[2m > �[22mnever fires while the agent emits protocol events, however slowly �[33m 3254�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m run-agent idle watchdog�[2m > �[22mnever fires while the agent talks on stderr only �[33m 3255�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m run-agent idle watchdog�[2m > �[22mdoes not treat an unterminated stdout byte stream as progress �[33m 797�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m run-agent idle watchdog�[2m > �[22mrequests streamed partial progress so active headless work refreshes the watchdog �[33m 3254�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m stale sandbox container cleanup�[2m > �[22man idle kill removes only the running sandbox its own agent launched �[33m 1545�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m stale sandbox container cleanup�[2m > �[22ma budget kill removes only the running sandbox its own agent launched �[33m 1249�[2mms�[22m�[39m
�[31m⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Unhandled Errors �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯�[39m
�[31m�[1m
Vitest caught 1 unhandled error during the test run.
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.�[22m�[39m

�[31m⎯⎯⎯⎯⎯⎯�[39m�[1m�[41m Unhandled Error �[49m�[22m�[31m⎯⎯⎯⎯⎯⎯⎯�[39m
�[31m�[1mError�[22m: [vitest-worker]: Timeout calling "onTaskUpdate"�[39m
�[90m �[2m❯�[22m Object.onTimeoutError node_modules/vitest/dist/chunks/rpc.-pEldfrD.js:�[2m53:10�[22m�[39m
�[90m �[2m❯�[22m Timeout._onTimeout node_modules/vitest/dist/chunks/index.B521nVV-.js:�[2m59:62�[22m�[39m
�[90m �[2m❯�[22m listOnTimeout node:internal/timers:�[2m585:17�[22m�[39m
�[90m �[2m❯�[22m processTimers node:internal/timers:�[2m521:7�[22m�[39m

�[31m⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯�[39m


�[2m Test Files �[22m �[1m�[32m1 passed�[39m�[22m�[90m (1)�[39m
�[2m      Tests �[22m �[1m�[32m215 passed�[39m�[22m�[90m (215)�[39m
�[2m     Errors �[22m �[1m�[31m1 error�[39m�[22m
�[2m   Start at �[22m 04:20:01
�[2m   Duration �[22m 70.70s�[2m (transform 224ms, setup 18ms, collect 292ms, tests 70.12s, environment 0ms, prepare 70ms)�[22m


round 2 · scripts/tests/qwen-autofix-workflow.test.js: F (exit 1)
--- output tail · round 2 · scripts/tests/qwen-autofix-workflow.test.js ---
ed an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
warning: You appear to have cloned an empty repository.
 �[32m✓�[39m scripts/tests/qwen-autofix-workflow.test.js �[2m(�[22m�[2m215 tests�[22m�[2m)�[22m�[33m 69317�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mholds a round while review-pr is in flight on the head (#8888) �[33m 413�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mauto-updates a PR red only from a stale base, gated on green-on-main �[33m 573�[2mms�[22m�[39m
   �[33m�[2m✓�[22m�[39m qwen-autofix workflow�[2m > �[22mauto-reruns a check that died on infrastructure, onc

...truncated -- full content in the run artifacts.

Evidence images

01-gate-replay-ab-base-vs-head

02-authdialog-skip-flip-base-vs-head

03-mutation-matrix-as-printed

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: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round: no action taken

PR: #9649 (fix/autofix-gate-ci-allowlist) · Round outcome: no actionable feedback; no code changes and no commit were made.

Triage result

Critical-only mode is active in this counting window (5 change-producing rounds are complete), so non-Critical feedback is excluded from this round's actionable sections and remains open for human follow-up. After that filtering, every actionable section for this round is empty:

  • New reviews: none newer than the last evaluation (2026-08-22T03:32:52Z).
  • Inline comments: none.
  • Issue-level comments: none actionable.
  • Failed checks / still-red checks: none — all CI checks on the current head report SUCCESS or SKIPPED.

Deferred (not worked, by design)

  • One non-Critical PR comment by @qwen-code-ci-bot (issuecomment-5377808836) remains deferred for human follow-up under the Critical-only brake. No code was changed, no thread was resolved, and no reply was posted for it, as that section is an audit record rather than work.

Diff growth

Net diff for this window is source 46 / test 29 lines against budgets of 400/400, with no prior over-budget rounds, so no growth audit was required.

中文说明

Autofix 评审轮次:未执行任何操作

PR: #9649fix/autofix-gate-ci-allowlist)· 本轮结果: 无可执行的反馈;未修改代码,也未产生任何提交。

分诊结果

本计数窗口已启用仅处理 Critical 的模式(已完成 5 个产生改动的轮次),因此非 Critical 反馈被排除在本轮可执行区域之外,保持开放留待人工跟进。经过该过滤后,本轮所有可执行区域均为空:

  • 新评审: 上次评估(2026-08-22T03:32:52Z)之后没有新的评审。
  • 行内评论: 无。
  • Issue 级评论: 无可执行项。
  • 失败检查 / 持续失败检查: 无 —— 当前分支头上的所有 CI 检查均为 SUCCESS 或 SKIPPED。

延后项(按设计未处理)

  • 按照 Critical-only 刹车机制,@qwen-code-ci-bot 的一条非 Critical PR 评论(issuecomment-5377808836)保持延后,留待人工跟进。该区域是审计记录而非工作任务,因此未针对它修改代码、未解决任何话题、也未发布任何回复。

Diff 增长

本窗口净 diff 为源码 46 行 / 测试 29 行,预算为 400/400,且此前没有超预算轮次,因此无需进行增长审计。

Deferred non-Critical feedback

Critical-only mode is active: 5 change-producing rounds are complete. 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.)

中文说明

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

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


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

⚠️ This run could not certify that any of this diff was reviewed.

Not reviewed: coverage — no plan was given, so this run cannot show that any of the diff was read.

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

Comment thread .github/workflows/qwen-autofix.yml

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

Review Summary (deepseek-v4-flash, round 1)

This PR restores CI=true to the autofix verification gate's env -i launches.

Findings

  • Critical (R1-1)CI is not pinned at the step level. The gate passes CI="${CI:-true}" into the env -i child, but the step-level env: block does not include CI. The :-true default only protects against an unset CI; if an earlier step injects CI=false via $GITHUB_ENV, the default is bypassed and the TUI-test flake can still fire with inverted semantics.
  • Suggestion (R1-2)scripts/tests/qwen-autofix-workflow.test.js lives outside all npm workspaces, so it is not exercised by workspace-scoped test suites and cannot be probed automatically.

Result

COMMENT — 1 inline Critical finding + 1 deferred Suggestion.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix round summary — PR #9649

Critical-only mode is active (5 change-producing rounds complete). This round addresses the single Critical finding in the actionable sections.

Feedback triage

[rc:3835335216] Critical — CI is not pinned at step level in .github/workflows/qwen-autofix.yml → RESOLVED IN CODE.

Claim: CI reaches the gate's env -i child only via the expansion CI="${CI:-true}", and :-true protects only against an UNSET CI. A $GITHUB_ENV plant of CI=false from an earlier shell-capable step (the agent runs branch code on the host) survives the expansion and inverts the CI semantics the gate relies on.

Reproduced before implementing anything:

  1. Expansion-semantics probe: CI=false bash -c 'echo "${CI:-true}"' prints false (while env -u CI and CI= both print true) — an explicit CI=false indeed bypasses the default.
  2. Failing witness: added expect(step).toContain("CI: 'true'") to the existing structural test launches both gates through pinned, allowlisted clean children and ran it against the unfixed workflow — it FAILED (neither gate's step-level env: block pins CI).

Fix: pin CI: 'true' at step level in BOTH gates (Verification gate and Repair verification gate), placed with the other step-environment pins (RUNNER_TEMP/WORKDIR/BRANCH), with a rationale comment in the file's established style. Step-level env: outranks any $GITHUB_ENV plant — the same doctrine this block already relies on for BASH_ENV/SHELLOPTS/LD_*/FOOTPRINT_ENFORCE. The child's CI="${CI:-true}" entry is kept as-is (defense-in-depth, consistent with the sibling KISS_AUDIT/FOOTPRINT_ENFORCE entries, and exactly the option the finding's suggested fix offered). The other two env -i launches in the workflow (deferred-findings upserts) run no tests, so CI is irrelevant to them; the defect class was confined to the two gates.

Mutation probe (before commit): removed the CI: 'true' lines from the workflow → the witness test FAILED; restored them → green. Repeated on the final file state with the same result, so the committed witness genuinely pins the new guard.

[rv:4999279178] COMMENTED review (round 1) — its Critical R1-1 is rc:3835335216 (resolved above). Its Suggestion R1-2 (scripts/tests/qwen-autofix-workflow.test.js lives outside all npm workspaces and is not exercised by workspace-scoped suites) was deferred as non-Critical under Critical-only mode; it is not in this round's actionable sections, so no action here.

[rv:4999255695] COMMENTED review — informational, no action. This is the /review runner's own coverage disclaimer ("no plan was given, so this run cannot show that any of the diff was read"). It makes no checkable claim that code behavior is wrong, so there is nothing to reproduce, implement, or decline in the diff.

Changes

  • .github/workflows/qwen-autofix.yml: added CI: 'true' to the step-level env: block of both verification gates (6 lines each, comment included). No other workflow changes; the env -i allowlist is untouched.
  • scripts/tests/qwen-autofix-workflow.test.js:
    • New witness assertion expect(step).toContain("CI: 'true'") in the existing gate-pin test (fails on the pre-round tree, passes after the fix — satisfying the bite-check requirement for a Critical resolved in code).
    • Extended that test's rationale comment with the CI-pin reasoning (R1-1).
    • One-word accuracy fix in the R5-1 allowlist comment: the CI value is now step-pinned, not "runner-provided".

No conflicts (--conflict false); no merge performed. Diff growth this round: +12 workflow / +7 test lines — far inside the window budgets (400/400).

Verification

Commands actually run and their results:

  • npx vitest run scripts/tests/qwen-autofix-workflow.test.js --config ./scripts/tests/vitest.config.ts215 passed (focused suite covering the touched file; includes the new witness).
  • Focused witness run pre-fix (reproduction) — FAILED as expected on the unfixed workflow; post-fix — passed.
  • Mutation probe — guard removed: witness FAILED; guard restored: passed (run twice, including on the final state).
  • Shell probe of ${CI:-true} under CI=false / unset / empty — confirms the hole and the fix semantics.
  • node -e "require('js-yaml').load(...)" on the workflow — parses OK, 8 jobs.
  • npm run buildpassed (also produced packages/audio-capture/dist, which an initial test:scripts run needed).
  • npm run typecheckpassed.
  • npm run lintpassed.
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.jsclean.
  • npm run test:scripts (full scripts suite) — 1514 passed | 16 skipped | 1 FAILED: verify-capture helper > renders 256-colour and truecolor via the default-grey fallback. This failure is environmental, pre-existing, and unrelated to this round — evidence:
    • The helper's stderr prints Fontconfig error: Cannot load default config file: File not found; this self-hosted runner has no /etc/fonts, no fc-list, and no /usr/share/fonts, so librsvg renders no glyph bodies and the exact #d4d4d4 pixel scan finds nothing (pixel dump: background #1e1e1e plus faint edge greys only).
    • Reproduced with this round's diff stashed (diff sha256 verified identical before/after): the test fails identically on the pre-round tree.
    • This branch touches only the two files above; neither is an input of verify-capture.test.js or scripts/verify-capture.mjs.
    • The gate never executes scripts/tests/** ("ordinary test code the gate never executes", run-autofix-review-verification.sh), so this cannot affect deterministic verification; hosted CI runners (which ship fonts) are the final gate for this suite.
  • git show --stat HEAD — 2 files, +19/−2; committed diff hash matches the audited round diff exactly (pre-commit/lint-staged changed nothing).
中文说明

Autofix 轮次总结 — PR #9649

当前处于仅处理 Critical 的模式(已完成 5 个产生改动的轮次)。本轮处理可执行区域中唯一的 Critical 反馈。

反馈分类

[rc:3835335216] Critical — .github/workflows/qwen-autofix.yml 中 CI 未在 step 级别固定 → 已在代码中解决。

论断:CI 仅通过展开式 CI="${CI:-true}" 进入 gate 的 env -i 子进程,而 :-true 只在 CI 未设置时生效。更早的、可执行 shell 的步骤(agent 在宿主机上运行分支代码)通过 $GITHUB_ENV 植入 CI=false 时,该值会穿过展开式,反转 gate 所依赖的 CI 语义。

实施前已完成复现:

  1. 展开语义探针:CI=false bash -c 'echo "${CI:-true}"' 输出 false(而 env -u CICI= 均输出 true)——显式的 CI=false 确实绕过了默认值。
  2. 失败见证测试:在既有结构性测试 launches both gates through pinned, allowlisted clean children 中加入 expect(step).toContain("CI: 'true'"),并在未修复的 workflow 上运行——失败(两个 gate 的 step 级 env: 块均未固定 CI)。

修复:在两个 gate(Verification gateRepair verification gate)的 step 级 env: 块中固定 CI: 'true',与其他 step 环境固定项(RUNNER_TEMP/WORKDIR/BRANCH)并列,并按该文件既有风格附上理由注释。step 级 env: 优先于任何 $GITHUB_ENV 植入——这正是该代码块在 BASH_ENV/SHELLOPTS/LD_*/FOOTPRINT_ENFORCE 上已经依赖的原则。子进程中的 CI="${CI:-true}" 保持不变(纵深防御,与相邻的 KISS_AUDIT/FOOTPRINT_ENFORCE 条目一致,也正是该反馈建议修复方案中给出的可选项之一)。workflow 中另外两处 env -i 启动(延迟发现项 upsert)不运行任何测试,CI 与之无关;该缺陷类别仅存在于这两个 gate。

变异探针(提交前):从 workflow 中移除 CI: 'true' 行 → 见证测试失败;恢复 → 通过。在最终文件状态上重复验证,结果相同,证明已提交的见证测试确实固定住了新增的守卫。

[rv:4999279178] COMMENTED 评审(第 1 轮) — 其中的 Critical R1-1 即 rc:3835335216(已在上方解决)。其中的 Suggestion R1-2(scripts/tests/qwen-autofix-workflow.test.js 位于所有 npm workspace 之外,不被 workspace 级测试套件执行)在仅处理 Critical 的模式下作为非 Critical 项被延后;它不在本轮可执行区域,因此本轮不做处理。

[rv:4999255695] COMMENTED 评审 — 信息性,不处理。 这是 /review 运行器自身的覆盖率声明("未提供计划,因此本次运行无法证明已阅读任何 diff")。它没有提出任何可核查的"代码行为错误"的论断,因此 diff 中没有可复现、可实现、可拒绝的内容。

改动

  • .github/workflows/qwen-autofix.yml:在两个验证 gate 的 step 级 env: 块中各新增 CI: 'true'(各 6 行,含注释)。无其他 workflow 改动;env -i 允许列表未动。
  • scripts/tests/qwen-autofix-workflow.test.js
    • 在既有 gate 固定测试中新增见证断言 expect(step).toContain("CI: 'true'")(在轮次前的树上失败、修复后通过——满足"以代码解决 Critical"所需的 bite-check 要求)。
    • 扩展该测试的理由注释,加入 CI 固定的推理(R1-1)。
    • R5-1 允许列表注释中的一词准确性修正:CI 的值现在来自 step 级固定,而非"runner 提供"。

无冲突(--conflict false);未执行合并。本轮 diff 增长:workflow +12 行 / 测试 +7 行——远低于窗口预算(400/400)。

验证

实际执行的命令及结果:

  • npx vitest run scripts/tests/qwen-autofix-workflow.test.js --config ./scripts/tests/vitest.config.ts215 通过(覆盖被改动文件的聚焦套件;含新见证测试)。
  • 修复前的聚焦见证运行(复现)— 在未修复的 workflow 上按预期失败;修复后 — 通过
  • 变异探针 — 移除守卫:见证测试失败;恢复守卫:通过(共运行两次,含最终文件状态)。
  • ${CI:-true}CI=false / 未设置 / 空值下的 shell 探针 — 确认漏洞及修复语义。
  • 对 workflow 执行 node -e "require('js-yaml').load(...)"解析正常,8 个 job
  • npm run build通过(同时生成了 packages/audio-capture/dist,首次 test:scripts 运行需要它)。
  • npm run typecheck通过
  • npm run lint通过
  • npx prettier --check scripts/tests/qwen-autofix-workflow.test.js干净
  • npm run test:scripts(完整 scripts 套件)— 1514 通过 | 16 跳过 | 1 失败verify-capture helper > renders 256-colour and truecolor via the default-grey fallback。该失败为环境问题、预先存在、与本轮无关——证据如下:
    • 辅助脚本 stderr 输出 Fontconfig error: Cannot load default config file: File not found;该自托管 runner 没有 /etc/fonts、没有 fc-list、没有 /usr/share/fonts,因此 librsvg 渲染不出任何字形主体,精确的 #d4d4d4 像素扫描找不到匹配(像素统计:仅背景 #1e1e1e 与少量淡灰边缘)。
    • 在本轮 diff 被 stash(前后 diff 的 sha256 已校验一致)的情况下复现:该测试在轮次前的树上以完全相同的方式失败。
    • 本分支仅改动上述两个文件;二者均非 verify-capture.test.jsscripts/verify-capture.mjs 的输入。
    • gate 从不执行 scripts/tests/**run-autofix-review-verification.sh:"ordinary test code the gate never executes"),因此该失败不影响确定性验证;携带字体的托管 CI runner 是该套件的最终验证关卡。
  • git show --stat HEAD — 2 个文件,+19/−2;已提交 diff 的哈希与审计过的轮次 diff 完全一致(pre-commit/lint-staged 未改动任何内容)。

Deferred non-Critical feedback

Critical-only mode is active: 5 change-producing rounds are complete. 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.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 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

yiliang114
yiliang114 previously approved these changes Aug 22, 2026

@yiliang114 yiliang114 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. Verified against the head:

  • The #9648 failure mode is real: AuthDialog.test.tsx:237-239 skips the 18 TUI-input tests on process.env['CI'] === 'true', so a gate child launched without CI un-skips them and inherits the flaky one. Both launches now pass CI="${CI:-true}", and the step-level CI: 'true' pin closes the gap the default cannot — :-true only covers an UNSET CI, so a $GITHUB_ENV plant of CI=false would otherwise survive the expansion.
  • The companion hardening is sound against the actual semantics: LD_PRELOAD/LD_AUDIT/LD_LIBRARY_PATH are mapped by ld.so at process startup, so the body-side unset cannot unload a library already mapped into the digest-check step's own bash (step-level empty pins fix that); RUNNER_TEMP/WORKDIR/BRANCH steer the digest oracle and the child's tree, so pinning them from expression context kills the decoy-path plant; and /usr/bin/echo + /usr/bin/sha256sum bypass BASH_FUNC_*%% function imports, which shadow builtins at bash startup even under --norc.
  • The test upgrade is the right shape: a verbatim adjacency chain plus an ASCII-only-stripped exact statement list (with the NBSP self-check), one bash --norc per step, and a sorted-multiset allowlist pin that now fails on any dropped variable — including the broadened assignment regex that catches lowercase names.

One observation, not blocking: the title/body lead with the CI fix, while the diff also lands the R6-2/R6-3/R6-4 startup-channel pins from the same deferred #9262 backlog — cohesive (one gate step, one attack surface), but worth naming in the body for future readers.

CI: 12 checks passing on head; the review-pr automated check is still pending at approval time.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

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

  • scripts/tests/qwen-autofix-workflow.test.js:19444 — [probe] step-level env pins asserted only with bare toContain — a commented-out entry survives the whole 215-test suite
中文说明

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

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

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

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

Comment thread .github/workflows/qwen-autofix.yml Outdated
Comment on lines 5237 to 5238
export PATH="${TRUSTED_PATH}"
unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH

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 gate body's first two statements resolve bare command words in the parent shell, and bash imports $GITHUB_ENV-planted BASH_FUNC_export%% / BASH_FUNC_unset%% entries as functions at startup even under --norc — function lookup precedes builtins. An earlier shell-capable step (the agent runs branch code on the host) can therefore execute attacker code inside this verdict-gating step: in the probe below, a shadowed export installs a DEBUG trap that overwrites ${RUNNER_TEMP}/run-autofix-review-verification.sh AFTER the digest check passes and BEFORE the env -i launch executes it — the attacker's copy runs and writes the forged verdict that gates the PAT-bearing push. A simpler variant writes outcome=fixed straight to $GITHUB_OUTPUT and exits 0. The R6-4 comment above claims the BASH_FUNC import channel is closed by the step-level pins and the absolute digest path, but those cover only the digest line's words; these two adjacent words remain shadowable.

Witness (probe, bash 5.2.21, gate body extracted verbatim at HEAD, hostile step environment):

ARM A (current body):      MARKER-export FIRED / MARKER-unset FIRED
                           digest check passed; staged runner swapped at launch
                           GITHUB_OUTPUT: outcome=fixed  (forged)
ARM B (both stmts removed): no markers; child env byte-identical to ARM A
ARM C (builtin prefix):    MARKER-builtin FIRED

Fix: delete both statements from both gate bodies — they are redundant. PATH reaches the child through the env -i allowlist (PATH="${TRUSTED_PATH}"), and LD_* is already neutralized three ways: the step-level LD_*: '' pins above (this step's bash startup), the LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH= command prefix on the /usr/bin/env execve, and env -i for the grandchild. ARM B verifies the child environment is byte-identical without them. Do NOT prefix with builtin/command — any spelling that keeps a command word stays a BASH_FUNC target (ARM C). Update the gateBodyStatements entries in the contract test in lockstep.

中文说明

[Critical] gate 体的前两条语句在父 shell 中以裸命令词解析,而 bash 即使在 --norc 下也会在启动时把 $GITHUB_ENV 植入的 BASH_FUNC_export%% / BASH_FUNC_unset%% 导入为函数——函数查找先于内建命令。因此更早的 shell 级步骤(agent 会在宿主机上运行分支代码)可以在这个决定 verdict 的步骤内执行攻击者代码:下方探针中,被遮蔽的 export 安装了一个 DEBUG trap,在 digest 检查通过之后、env -i 启动执行之前覆写 ${RUNNER_TEMP}/run-autofix-review-verification.sh——攻击者副本得以运行,并写入决定 PAT push 的伪造 verdict。更简单的变体直接向 $GITHUB_OUTPUT 写入 outcome=fixed 并以 0 退出。上方 R6-4 注释声称 BASH_FUNC 导入通道已被 step 级 pin 与绝对路径 digest 关闭,但那只覆盖 digest 行的命令词;这两条相邻语句的裸词仍然可被遮蔽。

修复:从两个 gate 体中删除这两条语句——它们是冗余的。PATH 通过 env -i 允许列表(PATH="${TRUSTED_PATH}")到达子进程;LD_* 已被三重中和:上方 step 级 LD_*: '' pin(本步骤 bash 启动时)、/usr/bin/env execve 上的 LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH= 命令前缀、以及孙子进程的 env -i。ARM B 验证删除后子环境变量逐字节不变。不要加 builtin/command 前缀——任何保留命令词的拼写仍是 BASH_FUNC 目标(ARM C)。同步更新契约测试中的 gateBodyStatements 条目。

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

Comment on lines +11274 to +11275
'export PATH="${TRUSTED_PATH}"',
'unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH',

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] Lockstep with the workflow-side finding: gateBodyStatements canonizes the bare export PATH="${TRUSTED_PATH}" / unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH spellings by exact equality, so the pin suite now mandates the function-shadowable form the R6-4 rationale claims to have closed. When the two statements are removed from both gate bodies (the fix for the workflow-side finding — removal, not builtin-prefixing, which a probe shows stays shadowable), drop these two entries here: the exact-list match expect(gateBodyStatementsOf(step)).toEqual(gateBodyStatements) below fails otherwise. Note the PAT-step toContain('export PATH=…') pins near line 10403 target different steps and need no change.

中文说明

[Critical] 与 workflow 侧发现联动:gateBodyStatements 以精确相等钉住了裸的 export PATH="${TRUSTED_PATH}" / unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH 拼写,pin 套件因此把 R6-4 理由声称已关闭的函数可遮蔽形态固化了下来。当两条语句从两个 gate 体中删除时(workflow 侧发现的修复——是删除而非 builtin 前缀,探针表明后者仍可被遮蔽),请同步删除此处这两个条目:否则下方的精确列表匹配 expect(gateBodyStatementsOf(step)).toEqual(gateBodyStatements) 会失败。注意约 10403 行处 PAT 步骤的 toContain('export PATH=…') pin 作用于不同步骤,无需改动。

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

Comment on lines +5208 to +5210
RUNNER_TEMP: '${{ runner.temp }}'
WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}'
BRANCH: '${{ matrix.target.branch }}'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] This pin block closes every $GITHUB_ENV→gate channel except HOME — the only allowlisted child variable (HOME="${HOME}" in the launch below, both gates) whose value still flows from the plantable step environment. Git is defended in the gate child, but npm resolves its userconfig from $HOME and has no defense: a planted HOME whose .npmrc sets script-shell wraps every npm run build|typecheck|lint|test invocation in an attacker-controlled shell, so a genuinely failing check exits 0 — every verdict-determining check reports green for a red branch, and the PAT-bearing push runs. actions/runner accepts the plant (FileCommandManager blocklist is NODE_OPTIONS only; ScriptHandler re-injects only RUNNER_*/allowlisted GITHUB_*, so HOME carries the plant through), which is exactly the doctrine this block applies to RUNNER_TEMP/WORKDIR/BRANCH/CI.

Witness (probe, node 22.23.2 / npm 10.9.8):

CONTROL (real HOME):                        npm run fail -> exit=7
PLANTED (HOME with .npmrc script-shell):    npm run fail -> exit=0
wrap.log: WRAPPED argv=-c exit 7

Fix: capture HOME at stage time with the existing trusted-value doctrine — add echo "trusted_home=${HOME}" >> "${GITHUB_OUTPUT}" to 'Stage trusted schema gate and agent runner' (it runs before any branch code), then pin HOME: '${{ steps.stage.outputs.trusted_home }}' in both gates' env blocks here and extend the pin assertions to it.

中文说明

[Critical] 本 pin 块关闭了所有 $GITHUB_ENV→gate 通道,唯独剩 HOME——它是允许列表中唯一一个(下方启动的 HOME="${HOME}",两个 gate 皆然)取值仍来自可植入步骤环境的变量。gate 子进程中 git 已有防御,但 npm 从 $HOME 解析 userconfig 且无防御:植入的 HOME 目录里 .npmrc 设置 script-shell 后,每一次 npm run build|typecheck|lint|test 都会被攻击者控制的 shell 包裹,真正失败的检查也会以 0 退出——红分支的所有 verdict 判定检查全绿,PAT push 随之执行。actions/runner 接受该植入(FileCommandManager 屏蔽名单仅 NODE_OPTIONSScriptHandler 只重新注入 RUNNER_*/允许列表内的 GITHUB_*,HOME 带着植入值透传)——这正是本块对 RUNNER_TEMP/WORKDIR/BRANCH/CI 所应用的学说。

修复:按既有的受信任值学说在 stage 阶段捕获 HOME——在 'Stage trusted schema gate and agent runner'(先于任何分支代码运行)中加 echo "trusted_home=${HOME}" >> "${GITHUB_OUTPUT}",然后在此处两个 gate 的 env 块中钉住 HOME: '${{ steps.stage.outputs.trusted_home }}',并扩展 pin 断言覆盖它。

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

…ing (QwenLM#9649)

The two gate bodies' leading statements were bare command words: bash
imports $GITHUB_ENV-planted BASH_FUNC_export%%/BASH_FUNC_unset%% (or
BASH_FUNC_builtin%% for a builtin-prefixed spelling) as functions at
startup even under --norc, and a shadowed pin can arm a DEBUG trap that
swaps the staged runner after the digest check passes and before the
env -i launch executes it — forging the verdict that gates the PAT push.
Both statements are redundant: PATH reaches the child through the env -i
allowlist, and LD_* is closed by the step-level pins, the env execve
prefix, and env -i. Probed: hostile plants fire on the pre-fix body and
are inert on the fixed body; child env is byte-identical without them.

HOME was the remaining $GITHUB_ENV channel into the gate child: npm
resolves its userconfig from HOME, and a planted HOME's .npmrc
script-shell wraps every verdict-determining npm run, so a red branch
reports green (probed: exit 7 becomes exit 0). Capture HOME at stage
time, before any branch code runs, and pin it at step level in both
gates — the trusted_path doctrine. Contract test updated in lockstep:
the pinned statement list drops the two entries, and the pin assertions
cover the HOME pin and its stage-time capture (mutation-probed).
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round summary — PR #9649

Feedback points and dispositions

[rc:3835686578] Gate bodies' leading statements are bare command words — Fixed

Reproduced before changing anything: the gate body was extracted verbatim from the workflow and run under runner-like bash (--noprofile --norc -e -o pipefail) in a hostile step environment carrying $GITHUB_ENV-style BASH_FUNC_export%% / BASH_FUNC_unset%% plants (bash 5.2 imports them as functions at startup even under --norc; function lookup precedes builtins). The shadowed export armed a DEBUG trap that swapped the staged runner AFTER the digest check passed and BEFORE the env -i launch executed it — the attacker copy ran and wrote the forged verdict (MARKER-export FIRED / MARKER-unset FIRED / ATTACKER RUNNER EXECUTED, exit 0). A third arm confirmed the finding's ARM C: a builtin export / builtin unset prefix is shadowed through BASH_FUNC_builtin%% the same way, so removal — not prefixing — is the fix.

Both statements are redundant: PATH reaches the child through the env -i allowlist (PATH="${TRUSTED_PATH}"), and LD_* is neutralized three ways (step-level LD_*: '' pins, the LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH= prefix on the /usr/bin/env execve, and env -i for the grandchild). The probe confirmed the child environment is byte-identical with the statements removed (diff shows only the per-run scratch paths).

Change: deleted export PATH="${TRUSTED_PATH}" and unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH from both gate bodies ('Verification gate' and 'Repair verification gate') and updated the body comment to record why the body carries no in-shell pin. Post-fix probe: the same hostile plants are inert on both gate bodies — no markers, digest check passes, trusted runner executes.

[rc:3835686580] Contract test canonizes the shadowable spellings — Fixed

Lockstep with the workflow fix: dropped the export PATH="${TRUSTED_PATH}" and unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH entries from gateBodyStatements, so the exact-list match now pins the digest line + launch as the complete body (a reintroduction of any bare-word statement adds a statement and fails the exact match). Updated the pinned-list rationale comment accordingly. The PAT-step export PATH pins near line 10403 target different steps and were left unchanged, as the finding noted.

[rc:3835686581] HOME is the remaining $GITHUB_ENV channel into the gate child — Fixed

Reproduced first (node v22.23.2 / npm 10.9.8, same as the finding's witness): control run of a genuinely failing script exits 7; with HOME planted to a directory whose .npmrc sets script-shell, npm run fail exits 0 and the wrapper log shows WRAPPED argv=-c exit 7 — every verdict-determining npm run build|typecheck|lint|test in the gate child is wrappable, so a red branch reports green and the PAT-bearing push runs. The gate child receives HOME from the step environment via the allowlist, and strip_runner_channels only strips GITHUB_* variables.

Change, following the existing trusted-value doctrine:

  • 'Stage trusted schema gate and agent runner' now records trusted_home=${HOME} in GITHUB_OUTPUT — expression context, captured before any branch code runs, next to the existing trusted_path capture.
  • Both gates' env blocks pin HOME: '${{ steps.stage.outputs.trusted_home }}', so the HOME="${HOME}" in the env -i launch resolves the pinned value and a $GITHUB_ENV plant never reaches npm's userconfig. (An empty output fails closed: the gate child's npm/node break loudly and the gate produces no passing verdict.)
  • Contract test extended: the gate pin loop asserts the HOME pin in both gates, and a workflow-wide pin asserts exactly one trusted_home=${HOME} recorder (the review stage step; the issue job's stage has no gate child re-injecting HOME). A semantics probe confirmed the pin: planted HOME in the step env + pinned value → the failing check exits 7 (honest red); unpinned → exit 0 (forged green).

[rv:4999821790] "Partially reviewed — gaps disclosed" — Addressed via its findings

The review's actionable content is the three Critical inline findings above, all fixed and probed. Its disclosures are informational: the reverse audit stopped on the review time budget (not a code item), and the embedded-shell actionlint source mapping remains a tool limitation (not linted — no action available). The deferred probe note (step-level env pins asserted with bare toContain, round 8 deferral) is explicitly "recorded, not requested in this round" — left untouched under Critical-only mode.

Critical-only mode / deferred feedback

feedback.md contains a Deferred non-Critical feedback section (the deterministic brake engaged after 5 change-producing rounds). Per the rules it is an audit record: no code changes, no thread resolutions, and no comment replies were made for those items.

Conflict

--conflict false — no base merge performed.

Mutation probes (witness check for this round's guards)

Each new guard was temporarily removed/negated and the focused tests confirmed to FAIL, then restored to green:

  • M1 — re-add the two statements to the first gate body → allows non-package fixes after deterministic verification FAILED (exact statement-list mismatch) ✓
  • M2 — remove the HOME pin from both gate env blocks → launches both gates through pinned, allowlisted clean children FAILED ✓
  • M3 — remove the trusted_home capture from the stage step → same test FAILED (recorder-count assertion) ✓
  • M4 — re-add the two entries to the test's gateBodyStatements → exact-list test FAILED ✓

All four restored byte-exact and green afterward.

Verification

Commands actually run and their results:

  • Probe 1 (BASH_FUNC shadowing, gate body extracted verbatim): ARM A pre-fix — markers fired, staged runner swapped post-digest, forged verdict, exit 0; ARM B (statements removed) — clean, trusted runner, child env byte-identical; ARM C (builtin prefix) — still shadowed, attack fires; ARM A post-fix on both gates — plants inert, digest passes, trusted runner executes ✓
  • Probe 2 (HOME → npm script-shell, node v22.23.2 / npm 10.9.8): control exit=7, planted exit=0, wrap.log WRAPPED argv=-c exit 7
  • Probe 3 (pin semantics): unpinned child exit=0 (forged), pinned child exit=7 (honest) ✓
  • Mutation probes M1–M4: each failed its focused vitest target when the guard was removed, green after restore ✓
  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js215/215 tests passed. Note: vitest exits non-zero on this runner due to an unhandled worker RPC timeout (Timeout calling "onTaskUpdate") with zero test failures; reproduced identically on the pre-round HEAD versions of both files (also 215/215 with the same unhandled error), so it is pre-existing infrastructure noise on this machine, not caused by this change.
  • npx prettier --check on both touched files — passed (test file reformatted during the round; two multi-line assertions collapsed to one line)
  • Workflow YAML parse (js-yaml) — OK
  • Settings sources unchanged → npm run generate:settings-schema not applicable; no bundled-CLI behavior touched → integration tests not applicable
中文说明

Autofix 审查轮次总结 — PR #9649

反馈点及处置

[rc:3835686578] gate 体的前两条语句是裸命令词 — 已修复

修改前先复现:从 workflow 中逐字提取 gate 体,在 runner 风格 bash(--noprofile --norc -e -o pipefail)下、携带 $GITHUB_ENV 风格的 BASH_FUNC_export%% / BASH_FUNC_unset%% 植入的 hostile 步骤环境中运行(bash 5.2 即使 --norc 也会在启动时将其导入为函数;函数查找先于内建命令)。被遮蔽的 export 安装了 DEBUG trap,在 digest 检查通过之后、env -i 启动执行之前 替换了 staged runner —— 攻击者副本得以运行并写入伪造 verdict(MARKER-export FIRED / MARKER-unset FIRED / ATTACKER RUNNER EXECUTED,exit 0)。第三个分支证实了发现的 ARM C:builtin export / builtin unset 前缀同样会被 BASH_FUNC_builtin%% 遮蔽,因此修复方式是删除而非加前缀。

两条语句都是冗余的:PATH 通过 env -i 允许列表(PATH="${TRUSTED_PATH}")到达子进程;LD_* 已被三重中和(step 级 LD_*: '' pin、/usr/bin/env execve 上的 LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH= 前缀、孙子进程的 env -i)。探针确认删除后子环境逐字节一致(diff 仅差每次运行的临时路径)。

改动:从两个 gate 体('Verification gate' 与 'Repair verification gate')中删除 export PATH="${TRUSTED_PATH}"unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH,并更新 body 注释,记录 body 不携带任何 in-shell pin 的原因。修复后探针:同样的 hostile 植入在两个 gate 体上均失效 —— 无 marker、digest 检查通过、trusted runner 执行。

[rc:3835686580] 契约测试将可遮蔽拼写固化 — 已修复

与 workflow 侧修复联动:从 gateBodyStatements 中删除 export PATH="${TRUSTED_PATH}"unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH 两个条目,使精确列表匹配现在把「digest 行 + 启动行」钉为完整 body(重新引入任何裸词语句都会增加语句并使精确匹配失败)。同步更新了 pinned-list 理由注释。约 10403 行处 PAT 步骤的 export PATH pin 作用于不同步骤,按发现所述保持不变。

[rc:3835686581] HOME 是进入 gate 子进程的最后一条 $GITHUB_ENV 通道 — 已修复

先复现(node v22.23.2 / npm 10.9.8,与发现的 witness 一致):真实失败脚本的对照运行退出码为 7;将 HOME 植入为含 script-shell.npmrc 目录后,npm run fail 退出码为 0,包装日志显示 WRAPPED argv=-c exit 7 —— gate 子进程中每一个决定 verdict 的 npm run build|typecheck|lint|test 都可被包装,红分支因此全绿,PAT push 随之执行。gate 子进程通过允许列表从步骤环境获得 HOME,而 strip_runner_channels 只剥离 GITHUB_* 变量。

按既有受信任值学说修复:

  • 'Stage trusted schema gate and agent runner' 现在向 GITHUB_OUTPUT 记录 trusted_home=${HOME} —— 表达式上下文,在任何分支代码运行之前捕获,紧邻既有的 trusted_path 捕获。
  • 两个 gate 的 env 块都钉住 HOME: '${{ steps.stage.outputs.trusted_home }}',使 env -i 启动中的 HOME="${HOME}" 解析到钉住的值,$GITHUB_ENV 植入永远到不了 npm 的 userconfig。(空输出为失败关闭:gate 子进程的 npm/node 会响亮地坏掉,gate 不会产生通过的 verdict。)
  • 契约测试扩展:gate pin 循环断言两个 gate 中的 HOME pin;workflow 级断言钉住恰好一处 trusted_home=${HOME} 记录点(review stage 步骤;issue job 的 stage 没有重新注入 HOME 的 gate 子进程)。语义探针确认 pin 有效:步骤环境中存在植入的 HOME + 钉住值 → 失败检查退出 7(诚实的红);不钉住 → 退出 0(伪造的绿)。

[rv:4999821790] "Partially reviewed — gaps disclosed" — 经其发现项处理

该审查的可执行内容即上方三条 Critical 内联发现,均已修复并经探针验证。其披露项为信息性:反向审计因审查时间预算停止(非代码项);actionlint 内嵌 shell 源码映射仍是工具限制(未 lint —— 无可执行动作)。延后的探针注记(step 级 env pin 仅以裸 toContain 断言,第 8 轮延后项)明确为「已记录、本轮不要求修改」—— 在仅处理 Critical 模式下保持不动。

仅处理 Critical 模式 / 延后反馈

feedback.mdDeferred non-Critical feedback 区段(确定性刹车在 5 个产生改动的轮次后启动)。按规则其为审计记录:未对这些条目做任何代码改动、线程解决或评论回复。

冲突

--conflict false —— 未执行 base 合并。

变异探针(本轮守卫的见证检查)

每个新守卫都被临时删除/取反,确认聚焦测试失败,随后恢复为绿:

  • M1 —— 向第一个 gate 体重新加回两条语句 → allows non-package fixes after deterministic verification 失败(精确语句列表不匹配)✓
  • M2 —— 从两个 gate env 块中移除 HOME pin → launches both gates through pinned, allowlisted clean children 失败 ✓
  • M3 —— 从 stage 步骤移除 trusted_home 捕获 → 同一测试失败(记录点计数断言)✓
  • M4 —— 向测试的 gateBodyStatements 重新加回两个条目 → 精确列表测试失败 ✓

四者均逐字节恢复且随后全绿。

验证

实际执行的命令及结果:

  • 探针 1(BASH_FUNC 遮蔽,gate 体逐字提取):修复前 ARM A —— marker 触发、digest 后 staged runner 被替换、伪造 verdict、exit 0;ARM B(删除语句)—— 干净、trusted runner 执行、子环境逐字节一致;ARM C(builtin 前缀)—— 仍可被遮蔽、攻击触发;修复后对两个 gate 的 ARM A —— 植入失效、digest 通过、trusted runner 执行 ✓
  • 探针 2(HOME → npm script-shell,node v22.23.2 / npm 10.9.8):对照 exit=7,植入 exit=0,wrap.log WRAPPED argv=-c exit 7
  • 探针 3(pin 语义):未钉住子进程 exit=0(伪造),钉住后 exit=7(诚实)✓
  • 变异探针 M1–M4:守卫移除时各自聚焦 vitest 目标失败,恢复后全绿 ✓
  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0)
  • npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/qwen-autofix-workflow.test.js215/215 测试通过。注:本机 vitest 因未处理的 worker RPC 超时(Timeout calling "onTaskUpdate")以非零码退出,但测试零失败;在本轮改动前的 HEAD 版本文件上同样复现(同为 215/215 且同样的未处理错误),属本机既有基础设施噪音,非本次改动引入。
  • 对两个改动文件的 npx prettier --check — 通过(轮次中对测试文件做了格式化;两个多行断言被折为单行)
  • workflow YAML 解析(js-yaml)— OK
  • 未改动 settings 源 → 不适用 npm run generate:settings-schema;未触及 bundled-CLI 行为 → 不适用集成测试

Deferred non-Critical feedback

Critical-only mode is active: 5 change-producing rounds are complete. 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.)

中文说明

已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 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.

No blocking issues. LGTM! ✅

Not linted (tool limitation, not a blocker): the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted.

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

  • .github/workflows/qwen-autofix.yml:5215 (+2 locations) — [probe] WORKDIR/BRANCH gate step pins hand-copy the job-level formula — three literal copies no test ties together
  • scripts/tests/qwen-autofix-workflow.test.js:11218 — [review] Stale contract-test comment claims the removed body-side unset/PATH export are pinned (R3-1, R2-2)
  • scripts/tests/qwen-autofix-workflow.test.js:10544 — [review] Stale 'with PATH pinned first' clause above the digest-count pin describes the pre-PR gate body
中文说明

无阻断问题。LGTM!✅

未检查(工具限制,非阻断):the executable-script lint — .github/workflows/qwen-autofix.yml: actionlint embedded-shell source mapping is not yet supported — not linted。

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

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

@wenshao
wenshao enabled auto-merge August 22, 2026 12:17

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

Review — APPROVE (C=0, no new findings)

Reviewed at 6aaf14ff against merge-base df768f09. All 20 open threads re-checked against the code as it stands — every one is fixed at this commit. Most read isOutdated: true because the hardening moved the anchored lines, not because the finding was dropped.

Disposition of the open threads

Threads Finding At 6aaf14ff
R1-1 (mine) CI not pinned at step level — a $GITHUB_ENV plant of CI=false survives ${CI:-true} fixedCI: 'true' in both gates' env: (5217, 5464)
R8-3 HOME was the last plantable channel; a planted .npmrc script-shell makes a red branch report green fixedHOME: '${{ steps.stage.outputs.trusted_home }}' (5229, 5476)
R6-3 RUNNER_TEMP/WORKDIR/BRANCH steer the digest oracle and the built tree fixed — pinned from expression context (5214-5216, 5461-5463)
R6-2, R8-1 LD_* map at ld.so startup, before a body-side unset can run fixedLD_PRELOAD/LD_AUDIT/LD_LIBRARY_PATH: '' at step level
R6-4 bare sha256sum/echo/export/unset are BASH_FUNC_ shadow targets fixed/usr/bin/echo, /usr/bin/sha256sum; the body now carries no bare command word
R2-1 the pin enumerated text tokens, not shell structure — 5 shell-level mutations stayed green fixedgateLaunchPin joins tokens with a literal \\\n[ \t]* adjacency chain, gateBodyStatements pins the whole comment-stripped statement list by toEqual, and bash --norc count is 1 per step
R2-2, R4-1, R6-1 value swaps invisible; JS \s ≠ bash whitespace; trim() misclassified NBSP-prefixed lines as comments fixed — tokens pin exact values; separator is [ \t] only; ASCII-only strip, plus a meta-assertion that '\u00a0# x' survives the filter
R2-3, R1-x uppercase-only assignment regex; grammar duplicated across two pin sites fixed — widened to [A-Za-z_][A-Za-z0-9_]*; the two copies re-synced
R3-1, R4-2/3/4, R5-1 no per-step pin on the digest check; statements could be added/reordered/demoted into a never-run arm fixed — the digest line is the first entry of gateBodyStatements, asserted per step in the loop over both gates

What I verified independently, beyond re-reading the pins

  • trusted_home has a real producer, in the right job, at the right time. It is emitted once (echo "trusted_home=${HOME}", line 3868) inside the review-address job's stage step (id: stage, 3827) — the same job whose gates consume steps.stage.outputs.trusted_home, so the step-output scoping is correct. Step order in that job is Checkout trusted baseStage … → … → Prepare branch and feedback (PR branch checkout) → Triage and address (agent runs branch code) → Verification gate. So HOME is captured before the branch is checked out and before any branch code executes, and Install dependencies runs against the trusted base. The capture is genuinely pre-plant.
  • The WORKDIR/BRANCH step pins match the job-level values byte-for-byte. Job env at 3662/3664 is WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}' and BRANCH: '${{ matrix.target.branch }}'; the four step-level copies (5215/5216, 5462/5463) are identical. A drifted pin would have silently pointed the gate at a tree the agent never wrote to, so this was worth confirming rather than assuming.
  • The CI=true blast radius is exactly what the PR claims — and no wider. Repo-wide, exactly one test file gates on process.env['CI'] === 'true': AuthDialog.test.tsx:237-239, where itWhenTuiInputReliable guards 18 of the file's 25 tests. That matches the PR's 1 failed | 24 passed (25) probe precisely. This mattered because the fix works by making tests skip, so the inverse risk is a gate that verifies less — with a single file affected, the gate's coverage is materially unchanged, and it now agrees with the CI semantics the repo's own green runs are measured under.
  • The third gate is correctly out of scope. issue-autofix's 'Verification gate' (1370) runs its checks directly in the step shell with no env -i child, so it inherits the runner's own CI=true and needs no re-declaration. The suite pins this reasoning explicitly (trusted_home=${HOME} must appear exactly once).
  • The failure mode is closed, not open. If stage never ran, TRUSTED_PATH/HOME arrive empty, env -i cannot resolve bash, the step fails, and 'Finalize verification' falls through to exit 1. An absent pin degrades to rejection, never to a forged pass.
  • The contract suite really runs in CI. test:ci chains npm run test:scripts (root package.json), which ci.yml invokes at line 528 — so the new structural pins are green on ubuntu/bash 5.x, where the local macOS bash-3.2 mapfile noise does not apply. All checks on this PR pass.

One non-blocking note, explicitly deferred

The WORKDIR and BRANCH literals now exist in three places per job (job env: + two gate step env: blocks), and the suite pins the step spelling without tying it to the job value. A future edit to the job-level WORKDIR would leave both step pins stale and green, and the gate would then build a tree the agent never wrote to. It fails closed and would break loudly on the first run, so it is a maintenance trap rather than a hole — a expect(reviewAddressJobEnv).toContain(WORKDIR_LITERAL) style cross-assertion would close it.

I am not opening a thread for this. This PR is at 8 review rounds; per AGENTS.md ("land only Critical fixes … defer remaining Suggestions to a follow-up"), it belongs in a follow-up, and I am recording it here so it is not silently dropped.

Assessment

The PR outgrew its title — it started as "add CI to the allowlist" and ended as a full hardening of the gate's parent-shell surface (startup channels, path-variable redirection, function shadowing, and a structural rather than textual pin). Each addition traces to a concrete, named attack path, both gates stayed symmetric throughout, and the pins now fail on shell-structure edits that previously passed. The original one-line fix is still in there and still correct.

中文说明

6aaf14ff 上复核,同意合并:20 个未解决线程逐条按 HEAD 核对,全部已修复——多数显示 isOutdated: true 是因为加固改动移动了锚定行,而非结论被丢弃。我自己的 R1-1(CI 未在 step 级钉住)已通过两个 gate 的 CI: 'true' 修复。

除复读 pin 之外我独立核验了六点:

  1. trusted_home 确有产出方(3868 行,review-address job 的 stage 步骤内),与消费它的两个 gate 同 job,step-output 作用域正确;且该步骤在 PR 分支 checkout 与 agent 执行分支代码之前运行,捕获时机可信。
  2. WORKDIR/BRANCH 的 step 级钉值与 job 级定义(3662/3664)逐字节一致——若漂移,gate 会去校验 agent 从未写入的目录树,因此必须实测而非假设。
  3. CI=true 的影响面与 PR 声明完全一致且不更宽:全仓库仅 AuthDialog.test.tsx:237-239 一个文件依赖 CI === 'true',其中 itWhenTuiInputReliable 覆盖 25 个用例中的 18 个,与 PR 的 1 failed | 24 passed (25) 探针吻合。这点很关键——修复方式是让测试跳过,反向风险是 gate 校验变少;仅一个文件受影响说明覆盖面实质未变。
  4. 第三个 gate(issue-autofix 的 1370 行)不在范围内是正确的:它直接在步骤 shell 内跑检查、无 env -i 子进程,因此自然继承 runner 的 CI=true
  5. 失败形态是 fail-closed:stage 未运行时 PATH/HOME 为空,env -i 无法解析 bash,步骤失败并由 'Finalize verification' 落到 exit 1,绝不会伪造通过。
  6. 契约套件确实在 CI 中运行(test:citest:scripts,ci.yml:528),在 ubuntu/bash 5.x 上为绿,本机 macOS bash 3.2 的 mapfile 环境噪音不适用;本 PR 全部检查通过。

一个非阻塞、明确延期的观察WORKDIR/BRANCH 字面量现在每个 job 出现三处(job env: + 两个 gate step env:),而测试只钉 step 侧拼写、未与 job 侧取值做交叉断言。未来若改动 job 级 WORKDIR,两处 step pin 会保持陈旧且测试仍绿,gate 将去构建 agent 从未写入的树。它 fail-closed 且首次运行即会显性失败,属维护陷阱而非漏洞。鉴于本 PR 已 8 轮,按 AGENTS.md「5 轮后只落 Critical,其余延期」的规则不新开线程,在此记录以免静默丢弃。

@wenshao
wenshao added this pull request to the merge queue Aug 22, 2026
Merged via the queue into QwenLM:main with commit 5efb424 Aug 22, 2026
40 checks passed
@wenshao

wenshao commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Verification report — real-environment evidence for #9649

I rebuilt this change's threat model in a real Linux environment and on a real GitHub runner, rather than reading the diff. Everything below is measured. Note on timing: the PR merged (5efb424, 12:30:45Z) while this was running, so this is a post-merge confirmation. I re-extracted the gate step from origin/main afterwards — its env: map and run: body are byte-identical to the PR head, so every result below applies to what actually landed.

Verdict: the change is sound, and does more than its description claims. I got a working attack against the pre-PR body for every hardening rationale in it. Five stand fully; the sixth (R6-3) stands for WORKDIR/BRANCH but its RUNNER_TEMP half describes a channel the runner already blocks — the pin is harmless defence-in-depth, but the comment overstates what is reachable. Details in §3.

Method

The gate step's run: body and env: map were extracted verbatim from the workflow YAML (parsed, never transcribed) for three trees — pre-PR cef54b8, this PR's first commit 8db672e, and the PR head 6aaf14f — then replayed under a simulated Actions step: process env = runner env + $GITHUB_ENV plants, with step-level env: layered on top, under bash --noprofile --norc -eo pipefail (the workflow's defaults.run.shell: bash). The staged runner was a probe script reporting what it saw.

Container node:22-bookworm — Debian 12, bash 5.2.15, aarch64
Real CI GitHub-hosted runner 2.336.0, ubuntu-24.04, bash 5.2.21 (a throwaway private repo — no repository code involved)
Local macOS 26.6.2 arm64, Node 24.18.1, vitest 3.2.4

1. The stated motivation is real — and understated

AuthDialog.test.tsx (unchanged by this PR) gates 18 TUI-input tests on process.env['CI'] === 'true'. The gate reaches them whenever the PR under repair touches packages/cli/** — I confirmed resolve-owning-packages.sh maps packages/cli/src/ui/auth/AuthDialog.tsxpackages/cli, which then runs under npm run test --workspace <p> -- --changed origin/main. (This PR's own files map to nothing, so its own gate run never executed these tests — the exposure was to every other autofix round touching the CLI package.) Nine runs of that one file:

Fig 5

  • CI=true7 passed | 18 skipped, 3/3 rounds green.
  • CI unset → 6/6 rounds red, 6 → 14 failures per round, 14 distinct tests flaking at least once, each a ~5s vi.waitFor timeout.

The PR body reports "1 failed | 24 passed". On this machine it is far worse — this is not a single flaky test but a whole class un-skipping, so the gate's exposure was larger than the description suggests.

One more thing the description doesn't mention, and which sharpens the argument: the issue-autofix job's own verification gate does not use env -i — it runs npm run build/typecheck/lint/test directly in the step, inheriting the runner's CI=true. Before this PR the two autofix paths therefore ran the same checks under opposite CI semantics. This change makes them agree.

2. Every attack the comments describe actually works on the pre-PR body

Both bodies, same harness, same tampered-runner scenario:

Fig 1

Fig 2

Read case F in Fig 1 with §3 in hand: the RUNNER_TEMP redirect works in this container but not on a real runner, which protects that name. Every other case in both figures is reachable on a real runner.

Rationale pre-PR body (cef54b8) PR body (6aaf14f)
R6-4 BASH_FUNC-shadowed echo blinds the digest check exit 0 — tampered runner executed exit 1, digest mismatch
R8-1 shadowed unset arms a DEBUG trap that swaps the runner after the digest passes exit 0 — tampered runner executed TRUSTED runner (no in-shell pin left to shadow)
R6-2 planted LD_PRELOAD; execve hook forges the digest check exit 0 — tampered runner executed library never mapped, exit 1
R8-3 planted HOME.npmrc script-shell wraps npm run red branch reported GREEN red branch correctly reported RED
R6-3 planted RUNNER_TEMP redirects the gate's helper scripts (container only — see §3) helper ran from the decoy tree, digest still passed helper trusted
R1-1 CI in the clean child child saw CI unset child saw CI=true

Two controls worth stating, because they show the attacks land specifically through the channels this PR closes, not through a broken harness:

  • B0 — tampered runner, no shadow: the pre-PR digest check does catch it (exit 1). The check works; the shadow is what blinds it.
  • A5 — replaying this PR's first commit (8db672e, allowlist entry only, no step-level pin): a $GITHUB_ENV plant of CI=false survives ${CI:-true} and reaches the child as false. That is exactly why the later CI: 'true' step pin is needed — :- only covers an unset value. The two-commit sequence is self-justifying.

3. On a real runner the threat model holds — with one exception

Fig 3

Runner provides CI=true — confirming the premise. Of the $GITHUB_ENV plants written by an earlier shell-capable step:

  • Landed (so the corresponding pins are load-bearing): CI, HOME, WORKDIR, BRANCH, KISS_AUDIT, FOOTPRINT_ENFORCE, LD_PRELOAD, and — notably — BASH_FUNC_echo%% and BASH_FUNC_unset%%. The runner applies no name validation whatsoever: in the next step a bare echo ran the attacker's function and declare -F echo reported a function. R6-4 and R8-1 are real, reachable threats, and slash-pathing the digest check is the right answer.
  • Ignored (runner-protected — the runner overwrites them with real values): RUNNER_TEMP, RUNNER_OS, RUNNER_TOOL_CACHE, GITHUB_WORKSPACE, GITHUB_OUTPUT. NODE_OPTIONS was stripped entirely.
  • Step-level env: outranked the plant for all five names tested.

The one inaccuracy. The R6-3 comment says a $GITHUB_ENV plant "points the digest oracle at a decoy runner" via RUNNER_TEMP. On runner 2.336.0 a RUNNER_TEMP plant does not land at all. My container demo (Fig 1, F1) reproduces it only because the harness has no such protection. WORKDIR and BRANCH, named in the same comment, do land — so the pin itself is correct and worth keeping (relying on undocumented runner behaviour would be worse); only the stated RUNNER_TEMP mechanism is unreachable. Worth a one-line amendment next time the file is touched, not a fix on its own.

Applicability caveat: the gate normally runs on the self-hosted ecs-qwen pool, with ubuntu-latest only as fallback. The probe ran on GitHub-hosted. Self-hosted uses the same actions/runner implementation so the behaviour should be identical, but I did not measure it there.

4. Both gates got the change, symmetrically

Parsed back out of the merged YAML: two launches carry the gate (verify and verify_repair, both in review-address). Their run: bodies are identical, and their env: maps differ in exactly one key — KISS_AUDIT, deliberately (steps.verify.outputs.kiss_audit || steps.prepare.outputs.kiss_audit on the repair leg). Every pin this PR adds is present on both.

5. The new contract pins are load-bearing, and close a real gap on main

Fig 4

Keyed on assertion failures, not on suite red/green — one of the three pinning tests spawns real shells and times out under load, which would otherwise read as "the pin caught it". --testTimeout=120000 and a classifier separate the two.

  • 18/18 mutations of the merged workflow produce a real assertion failure, including every token-preserving shape the comments call out: commented-out entry, dropped \, blank-line split, NBSP-smuggled statement, prefix command word, symmetric duplicate, value swap. A symmetric drop of both CI entries is caught — the pre-PR env -i-occurrence count would not have been.
  • 3/3 harmless edits stay green (adding a comment, rewording one, re-indenting one). The pins are precise, not just sensitive: gateBodyStatementsOf filters comment lines, so prose changes do not trip them.
  • 6/6 of the same shapes pass against pre-PR main's pins — main is blind to all of them.

Pre-PR the launch was pinned with a series of expect(step).toContain(...) substring checks, which is why every structure-preserving edit slipped through. drop-continuation-slash is the sharpest case: removing one \ splits the launch into two commands, so everything after the split runs with the full step environment instead of inside env -i — a total loss of the child's isolation — and main's pins report green. That gap is worth more than the CI fix that motivated the PR.

6. Suite health

Full qwen-autofix-workflow.test.js, three rounds each, on the true before/after trees (5efb4245^ vs 5efb4245 — both 217 tests, so the PR added assertions, not tests):

tree result failures
before run#1 Tests 2 failed | 215 passed (217) preserves an agent-written handoff when the budget kills qwen after it; upserts deferred findings into a per-PR issue that survives the merge
before run#2 Tests 217 passed (217)
before run#3 Tests 217 passed (217)
after run#1 Tests 217 passed (217)
after run#2 Tests 217 passed (217)
after run#3 Tests 217 passed (217)

No failure appears on after that does not also appear on before. The failures that do appear are wall-clock timeouts in shell-spawning tests, reproducible on the pre-PR tree on this machine — an established local flake, not a regression. Upstream CI on the PR was green (Test (ubuntu-latest, Node 22.x) 22m9s pass).

Scope notes / follow-ups

  • Not a gap, but worth knowing: on a persistent self-hosted pool, pinning HOME stops an attacker from redirecting npm's userconfig, but not from having polluted the real $HOME/.npmrc on an earlier run. Out of scope here; flagging it since R8-3 makes the .npmrc channel explicit.
  • The issue-autofix gate has no digest check or LD_* pins, but it also never launches a staged runner — different threat model, not a gap this PR left.
  • RUNNER_TEMP behaviour on ecs-qwen unmeasured (see §3).

Reproduce

The core loop is: parse the step out of the workflow YAML, replay it under env -i <plants> <step-level env> bash --noprofile --norc -eo pipefail, and diff what the staged runner sees. The two GitHub probes ran in a throwaway private repo containing nothing but the probe workflow. Harness and raw transcripts are kept locally and available on request.

中文版

验证报告 — #9649 的真实环境证据

我没有只读 diff,而是在真实 Linux 环境和真实 GitHub runner 上重建了这个改动的威胁模型。下面全部是实测。配图见上方英文版 Fig 1–5(Fig 1/2 容器实证、Fig 3 真实 runner、Fig 4 变异矩阵、Fig 5 AuthDialog)。时间说明: 验证进行期间这个 PR 已经合并(5efb424,12:30:45Z),所以这是合并后的确认。我事后重新从 origin/main 抽取了 gate step —— 它的 env: 映射和 run: 正文与 PR head 逐字节一致,因此下面每条结论都适用于真正落地的代码。

结论:改动是可靠的,而且做的比描述里说的更多。 对它列出的每一条加固理由,我都在合并前的正文上把对应攻击真正打通了。其中五条完全成立;第六条(R6-3)对 WORKDIR/BRANCH 成立,但它关于 RUNNER_TEMP 的那半描述的是 runner 本身已经挡住的通道 —— 这个 pin 属于无害的纵深防御,但注释把可达性说过头了。详见 §3。

方法

从 workflow YAML 里逐字抽取(解析得到,绝不手抄)三棵树的 gate step run: 正文与 env: 映射 —— 合并前的 cef54b8、本 PR 的首个 commit 8db672e、以及 PR head 6aaf14f —— 然后在模拟的 Actions step 下回放:进程环境 = runner 环境 + $GITHUB_ENV 植入,再叠加 step 级 env:,shell 用 bash --noprofile --norc -eo pipefail(即 workflow 的 defaults.run.shell: bash)。staged runner 换成探针脚本,报告它实际看到了什么。

容器 node:22-bookworm — Debian 12,bash 5.2.15,aarch64
真实 CI GitHub 托管 runner 2.336.0,ubuntu-24.04,bash 5.2.21(一次性私有仓库,不含任何仓库代码)
本机 macOS 26.6.2 arm64,Node 24.18.1,vitest 3.2.4

1. PR 声称的动机成立,而且被低估了

AuthDialog.test.tsx(本 PR 未改动)用 process.env['CI'] === 'true' 门控 18 个 TUI 输入测试。只要被修复的 PR 碰了 packages/cli/**,gate 就会跑到它 —— 我实测 resolve-owning-packages.shpackages/cli/src/ui/auth/AuthDialog.tsx 映射到 packages/cli,随后由 npm run test --workspace <p> -- --changed origin/main 执行。(本 PR 自己的文件映射为空,所以它自己那次 gate 从没跑过这些测试 —— 受影响的是其它每一轮碰到 CLI 包的 autofix。)对这一个文件跑九轮:

  • CI=true7 passed | 18 skipped3/3 轮全绿
  • CI 未设置 → 6/6 轮全红,每轮 6 → 14 个失败,14 个不同的测试至少 flake 过一次,每个都是约 5 秒的 vi.waitFor 超时。

PR 正文写的是「1 failed | 24 passed」。在我这台机器上要严重得多 —— 这不是单个 flaky 测试,而是一整类测试被解除跳过,所以 gate 此前的暴露面比描述中更大。

还有一点描述里没提、但能让论证更锋利:issue-autofix job 自己的验证 gate 不走 env -i —— 它直接在 step 里跑 npm run build/typecheck/lint/test,继承 runner 的 CI=true。也就是说,本 PR 之前,两条 autofix 路径在用相反的 CI 语义同一批检查。这个改动让它们一致了。

2. 注释里描述的每种攻击,在合并前的正文上都真的打得通

两份正文,同一套 harness,同一个「runner 被中途篡改」的场景:

理由 合并前正文(cef54b8 PR 正文(6aaf14f
R6-4 BASH_FUNC 影子化 echo 使 digest 检查失明 exit 0 —— 被篡改的 runner 被执行 exit 1,digest 不匹配
R8-1 影子化 unset 装上 DEBUG trap,在 digest 通过之后换掉 runner exit 0 —— 被篡改的 runner 被执行 TRUSTED runner(正文里已无可被影子化的 in-shell pin)
R6-2 植入 LD_PRELOAD,用 execve hook 伪造 digest 检查 exit 0 —— 被篡改的 runner 被执行 库根本未被映射,exit 1
R8-3 植入 HOME.npmrcscript-shell 包住 npm run 红分支报告为绿 红分支被正确判红
R6-3 植入 RUNNER_TEMP 改道 gate 的辅助脚本 (仅容器内成立 — 见 §3) helper 来自诱饵目录,digest 仍然通过 helper 可信
R1-1 clean child 里的 CI 子进程看到 CI 未设置 子进程看到 CI=true

有两个对照值得单独说,因为它们证明攻击是恰好从这个 PR 关掉的通道进来的,而不是 harness 本身有问题:

  • B0 —— runner 被篡改、但不加影子:合并前的 digest 检查确实抓住了(exit 1)。检查本身是有效的,是影子让它失明。
  • A5 —— 回放本 PR 的首个 commit8db672e,只加了 allowlist 条目、没有 step 级 pin):$GITHUB_ENV 植入的 CI=false 穿透${CI:-true},以 false 抵达子进程。这正是后来那个 CI: 'true' step pin 必要的原因 —— :- 只覆盖未设置的情况。这两个 commit 的先后顺序是自洽的。

3. 在真实 runner 上威胁模型成立 —— 但有一个例外

runner 确实提供 CI=true,前提成立。在由「先前那个能跑 shell 的 step」写入的 $GITHUB_ENV 植入中:

  • 生效的(对应的 pin 因此是承重的):CIHOMEWORKDIRBRANCHKISS_AUDITFOOTPRINT_ENFORCELD_PRELOAD,以及尤其值得注意的 BASH_FUNC_echo%%BASH_FUNC_unset%%。runner 对变量名完全没有校验:下一个 step 里一个裸 echo 执行了攻击者的函数,declare -F echo 报告存在同名函数。R6-4 和 R8-1 是真实可达的威胁,把 digest 检查改成绝对路径是正确解法。
  • 被忽略的(runner 保护,会用真实值覆盖):RUNNER_TEMPRUNNER_OSRUNNER_TOOL_CACHEGITHUB_WORKSPACEGITHUB_OUTPUTNODE_OPTIONS 被整个剥掉。
  • 对测试的全部五个名字,step 级 env: 都压过了植入。

唯一那处不准确。 R6-3 的注释说 $GITHUB_ENV 植入会通过 RUNNER_TEMP「把 digest oracle 指向诱饵 runner」。在 runner 2.336.0 上,RUNNER_TEMP 的植入根本不生效。我在容器里之所以能复现(Fig 1 的 F1),只是因为 harness 没有这层保护。同一条注释里点名的 WORKDIRBRANCH确实生效 —— 所以这个 pin 本身是对的、值得保留(去依赖一个未写入文档的 runner 行为反而更糟);只是注释陈述的 RUNNER_TEMP 机制不可达。下次动这个文件时顺手改一行注释即可,不值得单独发一个修复。

适用范围提醒: 这个 gate 平时跑在自建的 ecs-qwen 池上,ubuntu-latest 只是回退。我的探针跑在 GitHub 托管 runner 上。自建 runner 用的是同一份 actions/runner 实现,行为应当一致,但我没有在那上面实测。

4. 两处 gate 都改到了,而且是对称的

从合并后的 YAML 反解出来:承载 gate 的有两处 launch(verifyverify_repair,都在 review-address 里)。它们的 run: 正文完全一致env: 映射只差一个 key —— KISS_AUDIT,而且是刻意的(repair 那条腿是 steps.verify.outputs.kiss_audit || steps.prepare.outputs.kiss_audit)。本 PR 新增的每一个 pin,两处都有。

5. 新增的契约 pin 是承重的,并且堵上了 main 上一个真实缺口

判据是断言失败,而不是套件红/绿 —— 三个 pin 测试里有一个会 spawn 真实 shell,在负载下超时,否则会被误读成「pin 抓住了」。用 --testTimeout=120000 加一个分类器把两者分开。

  • 对合并后 workflow 做的 18 个变异中,18 个产生了真正的断言失败,涵盖注释点名的每一种「保留 token 文本」形态:注释掉条目、丢掉 \、空行切断、NBSP 夹带语句、前缀命令词、对称重复、值替换。对称删除两处 CI 条目也被抓住 —— 合并前那种「数 env -i 出现次数」的写法抓不到。
  • 3 个无害改动中 3 个保持绿(加一行注释、改写注释、改注释缩进)。这些 pin 不只是灵敏,而且精确:gateBodyStatementsOf 会过滤注释行,所以改文字不会误报。
  • 同样的形态打在合并前 main 的 pin 上,6 个中 6 个通过 —— main 对它们全盲。

合并前那处 launch 是用一串 expect(step).toContain(...) 子串断言 pin 住的,这正是每一种保结构的改动都能溜过去的原因。drop-continuation-slash 最尖锐:去掉一个 \ 会把 launch 切成两条命令,切口之后的一切都带着完整的 step 环境运行,而不是在 env -i 里 —— 子进程隔离彻底失效 —— 而 main 的 pin 报绿。这个缺口的价值高于最初触发本 PR 的那个 CI 修复。

6. 套件健康度

真正的 before/after 两棵树(5efb4245^5efb4245 —— 都是 217 个测试,说明本 PR 加的是断言而非测试)上各跑三轮完整的 qwen-autofix-workflow.test.js

结果 失败项
before run#1 Tests 2 failed | 215 passed (217) preserves an agent-written handoff when the budget kills qwen after it; upserts deferred findings into a per-PR issue that survives the merge
before run#2 Tests 217 passed (217)
before run#3 Tests 217 passed (217)
after run#1 Tests 217 passed (217)
after run#2 Tests 217 passed (217)
after run#3 Tests 217 passed (217)

after 上没有出现任何 before 上不存在的失败。 出现的失败都是会 spawn shell 的测试的挂钟超时,在合并前的树上于本机同样可复现 —— 是本机既有的抖动,不是回归。PR 上游 CI 全绿(Test (ubuntu-latest, Node 22.x) 22 分 9 秒通过)。

范围说明 / 后续

  • 不是缺口,但值得知道:持久化的自建 runner 池上,pin 住 HOME 能阻止攻击者改变 npm userconfig 的指向,但阻止不了它在更早一轮里污染了真实的 $HOME/.npmrc。这超出本 PR 范围;因为 R8-3 把 .npmrc 这条通道显式化了,所以在这里提一句。
  • issue-autofix 的 gate 没有 digest 检查、也没有 LD_* pin,但它同样从不启动 staged runner —— 威胁模型不同,不是本 PR 留下的缺口。
  • RUNNER_TEMPecs-qwen 上的行为未实测(见 §3)。

复现

核心做法是:把 step 从 workflow YAML 里解析出来,在 env -i <植入> <step 级 env> bash --noprofile --norc -eo pipefail 下回放,再比对 staged runner 实际看到了什么。两次 GitHub 探针跑在一个一次性私有仓库里,里面除了探针 workflow 没有任何东西。harness 与原始转录保存在本地,需要可提供。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.0.

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+) review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

autofix gate: env -i launches drop CI=true, un-skipping 18 TUI tests inside the gate

6 participants