feat(core): add a per-project outside-repo artifact landing - #9776
Conversation
`/audit` (landing in follow-ups) writes reports, sidecars, and intermediates that quote the audited module verbatim. When the audited repository's ignore state cannot keep those out of version control, they have to land somewhere the repository can never commit — this is that landing: per user and per project under QWEN_HOME, 0700, so quoted (possibly exploitable) module content stays private. Adoption is validated rather than assumed. The path is fully predictable — the project hash is a pure function of the root — and 0700 does not exclude the user's own other processes, so an existing leaf is not evidence this tool created it. A leaf planted as a symlink is refused, and a permissive mode is tightened, since mkdirSync's mode only applies to directories it creates. The contents are validated too, because artifacts land BELOW the leaf and an O_NOFOLLOW open only guards the final component: a symlink child is a complete escape (mkdirSync treats a symlink-to-directory as the directory, so everything written "inside" goes wherever it points) while the leaf keeps passing every check on itself. A hardlinked file is the same story through O_TRUNC. Both are refused. The landing is reused across runs — the report and its sidecar are the durable artifacts — so a merely non-empty landing stays adoptable.
|
Re-run at Template looks good ✓ — all sections present, including the reviewer test plan with per-guard mutation evidence. Problem: unchanged from the prior pass — not a bugfix; part 1 of 6 of the Direction: aligned, same anchor as before (merged design doc; no CHANGELOG analogue, expected for a novel workflow). One naming nit: the PR body still says Size: core paths touched ( Approach: scope still right. Every line added since the prior pass maps to a prior-round finding; no drive-by changes. The one shared-helper edit (case folding in Risk: no elevated risk signals — neither changed file matches the revert-correlated high-risk paths. Moving on to code review. 🔍 中文说明在 模板完整 ✓ —— 各节齐全,评审测试计划仍为每个防护提供变异验证说明。 问题:与上一轮相同——不是 bug 修复;是 方向:对齐,锚点不变(已合入的设计文档;CHANGELOG 无对应项,对新工作流属预期)。一个小命名问题:PR 描述仍写 规模:触及核心路径( 方案:范围仍然合理。上次通过之后新增的每一行都对应上一轮的发现,无顺手改动。唯一一处共享 helper 修改( 风险:无升级风险信号——两个改动文件均未命中与回滚相关的高风险路径。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewThe blocker from the last review is fixed, in exactly the shape suggested there. Adoption validation now walks every component the method creates, not just the leaf: The rounds since then added hardening that holds up, not churn:
Non-blocking, recorded on this PR already and not repeated as findings: the TestingUnattended CI run — nothing is built or executed from the PR in this review; the evidence below is the PR's own CI read through the API. All checks on
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 中文说明代码审查上一轮的阻塞项已修复,且正是当时建议的形态。采纳校验现在覆盖本方法创建的每一层组件,而不只是叶子: 其后的各轮补充的是站得住的加固,而非搅动:
非阻塞、已在本 PR 记录、不再作为发现重复: 测试无人值守 CI 运行——本审查不构建、不执行 PR 的任何代码;以下证据为经 API 读取的 PR 自身 CI。 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — the prior blocker is fixed with a discriminating regression test, the hardening added since is real and tested rather than churn, and every check on this commit is green; the two remaining items are recorded non-blocking nits. Stepping back: this is what a good review loop looks like. The last pass found a genuine containment bypass against the PR's own stated threat model — the Against my independent proposal from the last pass, this exceeds it — I would have validated the parent and stopped there; the containment guard, the case-folding for the win32/darwin spelling problem, and the fail-closed final check all answer attacks I did not enumerate. Every line of the growth since the gate pass maps to a prior-round finding; there is no drive-by scope. The two open items — the untested vanished-entry continue arm and the stale 中文说明置信度:4/5 —— 上一轮的阻塞项已以具有判别性的回归测试修复,其后的加固是真实且有测试支撑的而非搅动,本提交上所有检查为绿;剩余两项均为已记录的非阻塞小问题。 退后看:这是一次良性评审循环的样子。上一轮发现了一个针对 PR 自述威胁模型的真实容纳性绕过——把 对照我上一轮的独立方案,本实现超出了它——我只会校验父目录然后止步;容纳性防护、针对 win32/darwin 写法问题的大小写折叠、终点失败即关闭,都回应了我未列举的攻击。门禁通过以来新增的每一行都对应上一轮的发现,无顺手扩张。两个未决项——消失条目 continue 分支暂无测试、PR 描述中 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
One blocking finding: the static-plant arm is still open through the audits parent component — a planted symlink there relocates the whole landing while every check on the leaf passes. Full analysis, repro, and suggested fix in my review comments above. Everything else is mergeable quality. 🙏
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 12 scenario(s). — Qwen Code · serve A/B |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
The leaf was validated but the path walked to reach it was not, which left the whole landing relocatable by a single symlink. mkdirSync(recursive) follows symlinks in every component ABOVE the final one, and lstat refuses to follow only the final one — so a leaf-only check cannot see a redirected parent. Planting `audits` needs one `ln -s` and no race, because ~/.qwen exists long before `audits` does: the leaf is then created inside the planter's directory, reports as a perfectly real directory, passes the contents check, and every artifact written "into the landing" lands wherever the link points. Probed against the built module: before, the landing resolved inside the planted directory and a written report appeared there; after, the call refuses by name and the planter's directory stays empty. Each component this method creates is now created non-recursively and adopted only after its own lstat — recursive creation is precisely what walks a planted component unchecked. QWEN_HOME itself stays unvalidated: it is the user's own configured location, not a path this method invents. Also closes the unknown-dirent-type gap in the contents check: on a filesystem that does not report entry types, isSymbolicLink() and isFile() both answer false, so an entry slipped past every arm; those entries now get an explicit lstat.
|
The blocker is real and is fixed at Reproduced first, against the built module rather than a transcription. With Your analysis of why the leaf check cannot see it is exactly right: The fix walks every component this method creates. Each is now created non-recursively and adopted only after its own
A regression test plants the parent and asserts both the refusal and that nothing was created inside the planted directory. It is discriminating: reverting to the old recursive-mkdir-plus-leaf-lstat shape turns it red while the other 74 stay green. The non-blocking dirent observation is in too. On a filesystem that does not report entry types, On the CI note: the Ubuntu leg's earlier failure was infrastructure, not this branch — the self-hosted runner |
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/config/storage.test.ts — no such file or directory.
中文说明
Test Plan(非阻断):src/config/storage.test.ts — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.22.0)
| if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0) { | ||
| fs.chmodSync(dir, 0o700); |
There was a problem hiding this comment.
[Suggestion] On Windows, adoption applies zero privacy enforcement: this mode-repair branch is gated off (process.platform !== 'win32') and has no ACL counterpart, so a pre-existing audits or landing directory is adopted with whatever DACL it carries, while on POSIX the same pre-existing component is tightened to 0700. assertAuditLandingIsClean never inspects the directory's own permissions. This contradicts the unconditional docstring purpose ("0700 so the quoted … content stays private to the user"). The comment argues only that the POSIX check cannot run on Windows — not that win32 adoption is privacy-safe.
Modeled probe (win32 gate spoofed on Linux — real Windows not exercised): a planted 0777 component is adopted silently, mode intact (auditsMode=777), while the POSIX arm tightens it to 700. Concretely: on a shared Windows host/CI runner where another principal can write into QWEN_HOME, that principal pre-creates the fully predictable audits/<sha256(root)> with a permissive DACL (e.g. Everyone:Read); adoption passes on isDirectory() alone, and once the audit writer exists the quoted module content is written into a directory readable by the other principal.
Node exposes no portable ACL API, so the cheap safe option is to refuse adoption of pre-existing components on win32 — in the EEXIST branch, when process.platform === 'win32', throw the existing "remove it and re-run" error instead of adopting an unverified pre-existing directory (the method created nothing in that case, so refusal costs only the planted/legacy directory).
中文说明
[Suggestion] 在 Windows 上,采纳不施加任何隐私强制:本模式修复分支被门控关闭(process.platform !== 'win32')且没有 ACL 对应物,因此已存在的 audits 或落点目录会带着其既有 DACL 被采纳;而在 POSIX 上同样的已存在组件会被收紧为 0700。assertAuditLandingIsClean 也从不检查目录自身的权限。这与无条件的 docstring 目的(「0700 以使被引用的内容仅属主可见」)相悖。注释只论证了 POSIX 检查在 Windows 上无法运行,并未论证 win32 采纳在隐私上是安全的。
模型化探针(在 Linux 上伪造 win32 门控——未跑真实 Windows):植入的 0777 组件被静默采纳、模式原样保留(auditsMode=777),而 POSIX 分支将其收紧为 700。具体地:在另一个主体可写入 QWEN_HOME 的共享 Windows 主机/CI runner 上,该主体可预建完全可预测的 audits/<sha256(root)> 并赋宽松 DACL(如 Everyone:Read);采纳仅凭 isDirectory() 通过,一旦审计写入方落地,被引用的模块内容就会被写进其他主体可读的目录。
Node 没有可移植的 ACL API,因此廉价而安全的选项是在 win32 上拒绝采纳已存在组件——在 EEXIST 分支,当 process.platform === 'win32' 时抛出既有的「移除后重跑」错误,而不是采纳未经验证的已存在目录(此时方法什么都没创建,拒绝的代价只是那个植入/遗留目录)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
There was a problem hiding this comment.
Declined as proposed, with the limitation documented instead. Refusing adoption of pre-existing components on win32 would also refuse this tool's OWN landings from previous runs — the landing is deliberately reused across runs (the report and its sidecar are the durable artifacts), and on Windows nothing distinguishes self-created from planted. The effect would be every second /audit run per project failing forever with "remove it and re-run", breaking the reuse contract this same file argues for. Instead the mode-repair comment now states the win32 truth plainly: an adopted component keeps whatever DACL it had, since Node exposes no portable ACL enforcement. If a maintainer wants a stronger win32 guarantee anyway, a creation marker (written when this tool creates a component, required for adoption) could distinguish self-created landings — say the word and I will implement it.
中文说明
按提议的方案予以拒绝,改为如实记录该限制。在 win32 上拒绝采纳已存在组件,会同时拒绝本工具此前运行自建的落点——落点是刻意跨运行复用的(报告及其 sidecar 是持久产物),而 Windows 上无法区分自建与植入。其后果将是每个项目每隔一次 /audit 运行就永远失败并提示「移除后重跑」,打破同一文件为之辩护的复用契约。因此改为在模式修复注释中如实写明 win32 现状:已存在组件保留其既有 DACL,因为 Node 没有可移植的 ACL 强制。若维护者仍希望更强的 win32 保证,可以用创建标记(本工具创建组件时写入、采纳时要求存在)来区分自建落点——如有指示,乐意实现。
Adoption now normalizes the full mode — a 0300 plant kept owner write and blinded the content check while writes still succeeded — and an unlistable landing fails closed instead of skipping validation. The landing is refused before creation when QWEN_HOME resolves inside the audited repository, a missing QWEN_HOME base is created instead of throwing ENOENT, and the mutating getter is renamed to ensureAuditFallbackDir so the filesystem side effect is visible.
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 1/100 轮)。改动内容与我反驳保留之处如下: Feedback dispositions
Changes
Mutation probes (each new guard witnessed)
Verification
中文说明反馈处置
改动
变异探针(每个新防御均有见证)
验证
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/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/config/storage.test.ts — no such file or directory.
中文说明
Test Plan(非阻断):src/config/storage.test.ts — no such file or directory。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🤖 Addressed the latest review feedback (round 2/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 2/100 轮)。改动内容与我反驳保留之处如下: Round summary — PR #9776 (audit fallback landing)One commit: Feedback dispositions[rc:3838796976] Critical — check→mkdir race on the QWEN_HOME tail — RESOLVEDReproduced first: a deterministic test injects the attack at the exact seam (the Fix: the containment check is re-run immediately after the base creation, when [rc:3838796985] Critical —
|
| Probe | Mutation | Result |
|---|---|---|
| 0 | Pre-round source (HEAD) against the new tests |
6 failed / 81 passed — all new tests reproduce the filed defects |
| 1 | Remove the post-mkdir containment re-check | 1 failed — QWEN_HOME-tail race test |
| 2 | Remove the pre-return re-adoption of audits |
1 failed — audits-race test |
| 3 | Remove the pre-return re-adoption of the leaf | 1 failed — leaf-race test |
| 4 | Remove the resolution-failure fall-through wrapper | 1 failed — audits-as-file test |
| 5 | FatalConfigError reverted to bare Error |
2 failed — both error-class tests |
| — | Restored fixed source | 87/87 passed |
Verification
npm run build— passed (exit 0)npm run typecheck— passed (exit 0)npm run lint(repo-wideeslint . --ext .ts,.tsx+integration-tests) — passed (exit 0)npx eslint packages/core/src/config/storage.ts packages/core/src/config/storage.test.ts— passed (exit 0)cd packages/core && env -u QWEN_HOME npx vitest run src/config/storage.test.ts— 87 passed (87)cd packages/core && npx vitest run src/config/(with the sandbox's ambientQWEN_HOME) — 683 passed / 3 failed; the 3 failures are pre-existing environment-dependent storage tests that assumeQWEN_HOMEis unset (they fail identically on the pre-round commit and pass with the variable unset). Note on the sandbox: the harness exportsQWEN_HOMEto isolate its own state, and the real home directory is root-owned, so the two suites need opposite settings of that variable here; CI is unaffected (the feedback's failed-checks sections are empty).- Integration tests after
npm run bundle— not run:ensureAuditFallbackDirhas no production callers yet (it is staged for the coming /audit wiring), so nothing exercises this behavior through the bundled CLI or integration harness; unit coverage is exhaustive. npm run generate:settings-schema— not run: no settings source changed.
Diff growth this round: source net +45 / test +144 lines.
中文说明
本轮摘要 — PR #9776(审计落备落点)
单次提交:fix(core): close audit landing race seams with re-validation (#9776)(packages/core/src/config/storage.ts 及其测试共 +189/−11)。无需冲突解决(--conflict false)。
反馈处置
[rc:3838796976] Critical — QWEN_HOME 末端的「检查→mkdir」竞争 — 已解决
先复现:确定性测试在精确缝隙处注入攻击(在 mkdirSync(baseDir) 调用时,把尚不存在的 QWEN_HOME 末端植入为指向被审计仓库的符号链接,模拟同 UID 进程的行为)。在本轮之前的代码上,方法返回了一个 realpath 位于仓库内部的落点——没有抛出。
修复:在基础目录创建之后立即重跑封闭性检查——此时 baseDir 已存在,谓词会对其做 realpath 解析,与建议完全一致。测试还断言工作树内部没有创建任何东西。
[rc:3838796985] Critical — 两次采纳检查之间 audits 被交换 — 已解决
先复现:确定性测试在叶子创建缝隙处(第一次与第二次 adoptDirectory 之间的窗口)把 audits 换为指向被审计仓库的符号链接;第二个测试在内容检查缝隙处交换叶子本身(即发现中「其后一步」的变体)。在本轮之前的代码上,两者都在落点被搬迁的情况下成功返回。
修复:方法在返回前重新校验完整路径——对 audits 与叶子重跑 adoptDirectory(复用既有校验:lstat 拒绝被交换的组件、拒绝非目录),并在所有组件已存在时重跑封闭性检查,以捕获 lstat 看不到的、被交换的祖先组件。
范围说明:在最终复核之后发生的交换仍然可能——这是发现本身指明的不可约 TOCTOU 尾部,只有通过相对文件描述符的创建(O_NOFOLLOW | O_DIRECTORY + mkdirat)才能完全闭合,而 Node.js 不暴露该能力。实现的形态与发现建议的修复一致(其自身的测量也显示存在同样的残余尾部):窗口内的交换现在表现为显式拒绝而非静默搬迁,方法失败即关闭。
[rc:3838796987] Suggestion — 用户可处置的拒绝抛出裸 Error — 已解决
通过阅读 errors.ts 与 packages/cli/src/cli.ts 中的 handleCriticalError 核实:裸 Error 会打印「An unexpected critical error occurred:」加堆栈并以 1 退出。五处拒绝(封闭性、非目录、无法列出、符号链接子项、硬链接文件)现在都抛 FatalConfigError——本文件对同一失败类别已在使用的类(assertPathWithinDirectory、getPlansDir)——干净地打印可照做的消息,并以脚本可分支的退出码 52 退出。FatalConfigError 此前已被导入。
[rc:3838796990] Suggestion — 裸 ENOTDIR 抢占了可照做的诊断 — 已解决
先复现:当 audits 被植入为普通文件时,本轮之前的代码抛出裸 ENOTDIR: not a directory, lstat '…',而不是第一轮锚点对同一状态给出的可照做的采纳消息。
修复:封闭性检查被包装(提取为 assertAuditLandingIsOutsideRepo,三个调用点共享);解析失败不再以裸 errno 逃逸,而是穿透到组件检查——组件不是目录的路径不可能解析为可用落点,采纳检查会以可照做的消息呈现该状态。友好消息恢复,且封闭性拒绝仍然触发。
[rv:5002654243] CHANGES_REQUESTED — 「Test Plan(非阻断):src/config/storage.test.ts — no such file or directory」 — 已证伪,无代码动作
该文件存在于 packages/core/src/config/storage.test.ts(1108 行;已在当前检出中核实),聚焦 vitest 运行 87/87 通过。ENOENT 源于在未带 packages/core 前缀的情况下运行相对路径:正确命令是 cd packages/core && npx vitest run src/config/storage.test.ts。该评审将此条标注为非阻断;代码中无可改动之处,且本模式下无法编辑 PR 正文。
变异探针(每个新防线均有见证)
| 探针 | 变异 | 结果 |
|---|---|---|
| 0 | 本轮之前的源码(HEAD)运行新测试 |
6 失败 / 81 通过 —— 全部新测试复现了所申报的缺陷 |
| 1 | 移除 mkdir 之后的封闭性复核 | 1 失败 —— QWEN_HOME 末端竞争测试 |
| 2 | 移除返回前对 audits 的重新采纳 |
1 失败 —— audits 竞争测试 |
| 3 | 移除返回前对叶子的重新采纳 | 1 失败 —— 叶子竞争测试 |
| 4 | 移除解析失败穿透包装 | 1 失败 —— audits 为文件的测试 |
| 5 | FatalConfigError 还原为裸 Error |
2 失败 —— 两个错误类测试 |
| — | 恢复修复后的源码 | 87/87 通过 |
验证
npm run build— 通过(退出码 0)npm run typecheck— 通过(退出码 0)npm run lint(全仓库eslint . --ext .ts,.tsx+integration-tests)— 通过(退出码 0)npx eslint packages/core/src/config/storage.ts packages/core/src/config/storage.test.ts— 通过(退出码 0)cd packages/core && env -u QWEN_HOME npx vitest run src/config/storage.test.ts— 87 通过(87)cd packages/core && npx vitest run src/config/(带沙箱环境自带的QWEN_HOME)— 683 通过 / 3 失败;这 3 个失败是既有的、依赖环境的 storage 测试,它们假定QWEN_HOME未设置(在本轮之前的提交上同样失败,去掉该变量后通过)。沙箱说明:本运行环境导出QWEN_HOME以隔离自身状态,且真实家目录属主为 root,因此这两个测试套件在此需要该变量的相反设置;CI 不受影响(反馈中失败检查部分为空)。npm run bundle之后的集成测试 — 未运行:ensureAuditFallbackDir尚无生产调用方(为即将到来的 /audit 接线而预备),没有任何行为经由打包后的 CLI 或集成测试装置被执行;单元覆盖已完备。npm run generate:settings-schema— 未运行:未改动任何 settings 源。
本轮差异增长:源码净 +45 / 测试 +144 行。
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
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/config/storage.test.ts — no such file or directory.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/core/src/config/storage.ts:482 — [review] both vanished-entry continue branches untested — mutating each to throw keeps 87/87 green (deferred by the code-age rule: anchored on code unchanged since the round-2 head)
Convergence: round 3 posted 7 inline comment(s), 7 of them reported for the first time; the previous round posted 4 (4 new). Findings keep coming back to the same files: packages/core/src/config/storage.ts (findings in round 2; 5 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
Test Plan(非阻断):src/config/storage.test.ts — no such file or directory。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 3 轮发布了 7 条行内评论,其中 7 条是首次提出;上一轮发布了 4 条(其中 4 条首次提出)。发现反复回到同一批文件:packages/core/src/config/storage.ts(第 2 轮已出过发现,本轮又有 5 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run. 中文说明🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): src/config/storage.test.ts — no such file or directory.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/core/src/config/storage.ts:356 — [review] Design doc promises mkdtemp semantics; code delivers a predictable reused landingpackages/core/src/config/storage.ts:350 — [review] Hand-rolled realpath duplicates realpathNearestExistingpackages/core/src/config/storage.test.ts:737 — [probe] No test pins that the landing ignores runtime-base redirectionpackages/core/src/config/storage.ts:483 — [probe] Both vanished-entry catch-continue branches untestedpackages/core/src/config/storage.ts:383 — [probe] QWEN_HOME existing as/traversing a regular file -> raw errno escapepackages/core/src/config/storage.ts:356 — [review] Design doc mentions QWEN_RUNTIME_DIR; code honors only QWEN_HOMEpackages/core/src/config/storage.ts:402 — [probe] Final re-validation never re-runs the content checkpackages/core/src/config/storage.ts:446 — [probe] adoptDirectory's chmodSync unguarded — raw EPERM on unowned component
中文说明
Test Plan(非阻断):src/config/storage.test.ts — no such file or directory。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 8 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on macOS.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on Windows.
Not explored to full depth (tool budget reached): "agent 4": none — nothing was cut short..
Test Plan (not a blocker): src/config/storage.test.ts — no such file or directory.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/core/src/config/storage.test.ts:23 — [probe] Default mockReaddirSync forces { withFileTypes: true }, hiding a production-option mutationpackages/core/src/config/storage.test.ts:1056 — [probe] Untyped-dirent fallback test covers only the symlink shape; the stat?.isDirectory()/stat?.isFile() disjuncts survive mutationpackages/core/src/config/storage.ts:432 — [probe] adoptDirectory's non-EEXIST mkdir failure escapes as a raw errno instead of the classified FatalConfigErrorpackages/core/src/config/storage.ts:386 — [probe] Base-creation catch's remediation text misdirects for EACCES/EROFS/ENOSPC — tells the operator to remove something that does not existpackages/core/src/config/storage.ts:436 — [probe] adoptDirectory's post-mkdir lstatSync/chmodSync sit outside the try/catch — raw ENOENT escapes when a planter removes the component mid-window
Convergence: round 6 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 5 (0 new). Findings keep coming back to the same files: packages/core/src/config/storage.test.ts (findings in round 3; 1 more now); packages/core/src/config/storage.ts (findings in round 3; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on macOS。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on Windows。
未探索到全部深度(达到工具调用预算):"agent 4":none — nothing was cut short.。
Test Plan(非阻断):src/config/storage.test.ts — no such file or directory。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 6 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 5 条(其中 0 条首次提出)。发现反复回到同一批文件:packages/core/src/config/storage.test.ts(第 3 轮已出过发现,本轮又有 1 条);packages/core/src/config/storage.ts(第 3 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #9776Commit: Feedback points and dispositions[rv:5003994672] CHANGES_REQUESTED review body — Addressed via its inline findingsThe review body itself carries disclosures (unreviewed macOS/Windows test runs) and an observation about findings recurring in The five items listed as "Deferred under the convergence posture (round 6, not a blocker)" were explicitly recorded-but-not-requested this round and were left untouched. [rc:3840203224] Critical — pre-return re-validation never re-runs the content check — FIXEDClaim verified from source: the re-validation block re-ran both adoptions and the containment check but not [rc:3840203230] Critical — socket test path exceeds AF_UNIX sun_path on every platform — FIXEDClaim reproduced on this runner with the test's exact path shape ( No conflict resolution was needed ( VerificationCommands actually run this round, in order:
CI failure notes (no GitHub credentials on this runner, so logs were unavailable; diagnosed from the workflow definition and local evidence):
中文说明Autofix 审查轮次 — PR #9776提交: 反馈点与处置[rv:5003994672] CHANGES_REQUESTED 审查正文 — 通过其行内发现项处置审查正文本身只包含披露信息(未审查的 macOS/Windows 测试运行)以及关于发现反复出现在 列为「在收敛姿态下延后(第 6 轮,非阻断)」的五条项目本轮明确为「已记录、不要求处理」,未做任何改动。 [rc:3840203224] Critical — 返回前重新校验从未重跑内容检查 — 已修复从源码核实了该论断:重新校验块重跑了两次采纳与封闭性检查,却没有重跑 [rc:3840203230] Critical — socket 测试路径在所有平台都超过 AF_UNIX sun_path — 已修复在本运行环境按该测试的确切路径形态复现了论断( 无需解决冲突( 验证本轮实际运行的命令,按顺序:
CI 失败说明(本运行环境无 GitHub 凭据,无法获取日志;依据工作流定义与本地证据诊断):
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/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally on macOS.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally on Windows.
Test Plan (not a blocker): src/config/storage.test.ts — no such file or directory.
Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:
packages/core/src/config/storage.ts:455 — [probe] Mode normalization masks with 0o777 — planted setuid/setgid/sticky bits survive adoptionpackages/core/src/config/storage.ts:540 — [probe] Hardlink refusal false-positives on hardlinking backup tools of the durable landing
Convergence: round 7 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/core/src/config/storage.ts (findings in round 6; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally on macOS。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally on Windows。
Test Plan(非阻断):src/config/storage.test.ts — no such file or directory。
收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 7 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/config/storage.ts(第 6 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
) The content check decided typed dirents' entry type from the readdir snapshot while consulting a fresh lstat only for nlink, so a same-UID swap of a listed entry for a symlink between the snapshot and the loop passed every arm and returned a landing holding a symlink child. Lstat every entry once and drive the symlink, directory, special-file, and hardlink arms from that single fresh stat — the same single-stat shape the unknown-dirent-type path already used, so the two paths no longer disagree about what "validated" means.
|
🤖 Addressed the latest review feedback (round 5/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 5/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #9776 (round 8)One Critical inline finding this round; resolved in code with a witness test. All other feedback items were informational disclosures, reviewer-recorded deferrals explicitly not requested this round, or observations. No base-conflict merge was performed ( Feedback points and decisions[rc:3840755230] [Critical] — RESOLVED in codeFinding: the pre-return content check decides every entry type for typed dirents from the readdir snapshot taken at the top of the walk: the fresh Reproduction (before any fix): added a deterministic witness test, Fix (minimal, root-cause): lstat every entry unconditionally and drive all four arms — symlink refusal, directory recursion, special-file refusal, hardlink refusal — from that single fresh stat. This removes the Mutation probe: negated the new symlink arm ( [rv:5004599342] review body — "Partially reviewed — gaps disclosed"
Changes
Commit: Verification
中文说明Autofix 审查轮次 — PR #9776(第 8 轮)本轮有一条 Critical 行内发现,已在代码中解决并附见证测试。其余反馈项均为信息披露、审查者明确记录为「本轮不要求修改」的延后项,或仅为观察。未执行 base 冲突合并( 反馈点与处置[rc:3840755230] [Critical] — 已在代码中解决发现: 返回前的内容检查对已定型的 dirent 仍以走查开始时 readdir 快照里的条目类型为准:nlink 分支处新做的 复现(修复前): 新增确定性见证测试 修复(最小、根因): 对每个条目无条件执行 lstat,并让全部四个分支——符号链接拒绝、目录递归、特殊文件拒绝、硬链接拒绝——都依据这一个新鲜的 stat 判断。这同时彻底移除了 变异探针: 将新的符号链接分支取反( [rv:5004599342] 审查正文 — 「部分审查,缺口已披露」
变更内容
提交: 验证
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/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally on macOS.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally on Windows.
Test Plan (not a blocker): src/config/storage.test.ts — no such file or directory.
Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:
packages/core/src/config/storage.ts:357 — [review] 'audits' hardcoded as a bare literal twice, against the file's named-constant conventionpackages/core/src/config/storage.ts:507 — [probe] Vanished-entry continue branch exercised by no test — the continue→throw mutant survives the whole suitepackages/core/src/config/storage.ts:440 — [probe] adoptDirectory's post-mkdir lstatSync/chmodSync escape as raw errno in their own race windows instead of FatalConfigErrorpackages/core/src/config/storage.ts:438 — [probe] Non-EEXIST mkdirSync failure escapes raw — the same misconfiguration is classified or raw depending on whether the base dir existspackages/core/src/config/storage.ts:521 — [probe] A directory child that vanishes between lstat and the recursive readdir aborts with a hard refusal, while the identical file-entry race is tolerated by the continue arm
Convergence: round 8 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 1 (1 new). Findings keep coming back to the same files: packages/core/src/config/storage.ts (findings in round 7; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally on macOS。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally on Windows。
Test Plan(非阻断):src/config/storage.test.ts — no such file or directory。
收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 8 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 1 条(其中 1 条首次提出)。发现反复回到同一批文件:packages/core/src/config/storage.ts(第 7 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🤖 Addressed the latest review feedback (round 6/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 6/100 轮)。改动内容与我反驳保留之处如下: Autofix round summary — PR #9776 (issue #9776)Commit: Feedback points and decisions[rc:3841965743] R8-1 — Critical: directory arm decides from a fresh lstat, then follows the re-resolved name — RESOLVED IN CODEReproduced first, per the source-blind rule: a new race test injects a deterministic seam at the recursive readdir of the FINAL content check, swapping a listed directory child for a symlink to a clean (empty) directory. On the unmodified code Fix: the probe-verified narrowing suggested in the finding. After the recursion validates a directory child, the arm re-lstats the child and refuses when it is no longer a directory (a swap for a symlink-to-directory during the walk validated the link target); a child that vanished during the walk is skipped, mirroring the existing vanished-entry tolerance. The residual window shrinks to the documented post-return tail of a path-returning API. The O_NOFOLLOW/fd-relative class-closing alternative was not taken: it would rebuild the walk around a different fs surface than the suite's readdirSync seams and grow the diff far past the narrowing this round needs. Witnesses: [rc:3841965757] R8-2 — Critical: pre-return re-validation runs the only non-following checks BEFORE the two follow-based checks — RESOLVED IN CODEReproduced first with two new race tests injecting at the SECOND content check, as the finding requested (the existing Fix: the probe-verified suggestion — the two Witnesses: [rv:5006087989] Review body — CHANGES_REQUESTED ("Partially reviewed — gaps disclosed") — ADDRESSED VIA THE INLINE FINDINGSThe actionable substance of the review is the two Criticals above; both are resolved in code. On the disclosed gaps:
The five items listed under the convergence posture are recorded as deferred (not requested this round) and were left untouched, per critical-only mode. No code, threads, or replies were produced for them. Mutation probes (each new guard/branch has its own witness)
Conflict notesNone ( Verification
中文说明Autofix 轮次总结 — PR #9776(issue #9776)提交: 反馈点与处理决定[rc:3841965743] R8-1 — Critical:目录分支先以新鲜 lstat 判定、随后以跟随语义重新解析同名路径 — 已在代码中解决按来源盲审规则先行复现:新增竞争测试在最终内容检查的递归 readdir 处注入确定性缝隙,把已列出的目录子项换成指向干净(空)目录的符号链接。在未修改的代码上, 修复:采用发现中经探针验证的收窄建议。递归校验完一个目录子项后,该分支对子项重新 lstat,若其不再是目录则拒绝(走查期间被换成「指向目录的符号链接」时,校验的是链接目标);走查期间消失的子项则跳过,与既有的「消失条目」容忍保持一致。残余窗口收缩到路径返回型 API 已书面承认的返回后尾部。未采用 O_NOFOLLOW/文件描述符相对的类别闭合替代方案:那需要围绕与测试套件 readdirSync 缝隙不同的 fs 面重建整个走查,且 diff 增长远超本轮所需的收窄。 证据测试: [rc:3841965757] R8-2 — Critical:返回前重新校验把仅有的非跟随检查放在两次跟随型检查之前 — 已在代码中解决按发现的要求,用两个在第二次内容检查处注入的新竞争测试先行复现(既有的 修复:采用经探针验证的建议——两次 证据测试: [rv:5006087989] 评审正文 — CHANGES_REQUESTED(「部分评审——缺口已披露」)— 经由行内发现处理该评审的可执行实质即上述两条 Critical,均已在代码中解决。关于披露的缺口:
收敛姿态下列出的 5 条延后项已记录为「本轮不要求」,按仅 Critical 模式未做改动;未就其产生任何代码、线程操作或回复。 变异探针(每个新增守卫/分支都有自己的证据)
冲突说明无( 验证
Deferred non-Critical feedbackCritical-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. ( 中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 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/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
5 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- chmodSync/lstatSync raw errno escape in adoptDirectory (storage.ts:460) — already reported in the round-6 and round-8 deferred lists (storage.ts:446 / storage.ts:440)
- vanished-entry continue arm and adoptDirectory non-EEXIST re-throw untested (storage.ts:509, storage.ts:441) — already reported in the round-8 and round-6 deferred lists (storage.ts:507 / storage.ts:422)
- vanished directory child between lstat and recursive readdir aborts with a hard refusal (storage.ts:525) — already reported in the round-8 deferred list (storage.ts:521)
- base-creation catch's remediation text misdirects for permission/quota failures (storage.ts:389) — already reported in the round-6 deferred list (storage.ts:386)
- non-EEXIST mkdirSync failure escapes raw between the two consecutive adoptDirectory calls (storage.ts:441) — already reported in the round-8 deferred list (storage.ts:438)
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on macOS.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on Windows.
Test Plan (not a blocker): src/config/storage.test.ts — no such file or directory.
Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round:
packages/core/src/config/storage.ts:423 — [probe] Triple re-validation is a non-converging enumeration of race windows the path-returning API provably reopenspackages/core/src/config/storage.ts:534 — [probe] Post-recursion re-lstat refusal always claims 'contains a symlink' for any non-directory swappackages/core/src/config/storage.ts:391 — [review] (err as Error).message casts instead of the package's getErrorMessage() convention
Convergence: round 9 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/core/src/config/storage.ts (findings in round 8; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 5 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on macOS。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on Windows。
Test Plan(非阻断):src/config/storage.test.ts — no such file or directory。
收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 9 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/config/storage.ts(第 8 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🤖 Addressed the latest review feedback (round 7/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 7/100 轮)。改动内容与我反驳保留之处如下: Autofix review round — PR #9776Critical-only mode is active (5 change-producing rounds complete), so only the Feedback points and dispositions[rc:3846130622] [Critical] Containment guard bypassed on case-insensitive Reproduced first, on the pre-fix code, with the two new regression tests
Fix (all inside the PR's footprint,
The reviewer also suggested folding in Regression coverage: two new tests pin the case-mismatch refusal and the Mutation probes (each guard witnessed by its own test):
[rv:5011033098] [CHANGES_REQUESTED] "Partially reviewed — gaps disclosed" — Deferred non-Critical feedback — no action. Critical-only mode excludes it Conflict: none ( VerificationCommands actually run this round (from the repository root unless noted;
中文说明Autofix 审查轮次 — PR #9776当前处于仅处理 Critical 的模式(已完成 5 个产生改动的轮次),因此本轮只有 Critical 发现可以处理。 反馈点及处置[rc:3846130622] [Critical] 在大小写不敏感的文件系统上,当同一物理仓库在被比较的两个输入中以不同大小写写法出现时,封闭性防线被绕过 —— 已在代码中解决。 先在未修复的代码上复现,用的是两个新增的回归测试(通过
修复(全部位于本 PR 的足迹内,
评审还建议直接在 回归覆盖:两个新测试分别钉住「大小写不一致时拒绝」与「单叶子稳定性」。两者在轮次前的代码上都会失败(满足验证门的 bite 要求),修复后通过。 变异探针(每个守卫都有各自的测试作见证):
[rv:5011033098] [CHANGES_REQUESTED]「Partially reviewed — gaps disclosed(部分审查——缺口已披露)」—— 无代码改动。 该评审正文本身不含新发现:它披露在被审查的提交上 macOS/Windows 的 CI 腿被跳过、且未在本地 macOS/Windows 上运行(本 Linux 运行器上同样不可用;此处的平台行为通过钉住平台的回归测试来演练)。它确认的 5 条建议级发现均为先前轮次已报告内容的重复,不再重复处理。其中「Test Plan: 延后的非 Critical 反馈 —— 无改动。 仅 Critical 模式将其排除在本轮之外;这些条目保持开放,留待人工跟进。评审正文中点名的 3 条收敛姿态延后项属于「已记录、本轮不要求修改」,未做触碰。 冲突:无( 验证本轮实际执行的命令(除特别说明外在仓库根目录运行;Vitest 运行使用了
Deferred non-Critical feedbackCritical-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. ( 中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 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/模型 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- vanished-entry continue arm in assertAuditLandingIsClean untested — continue→throw mutant survives the whole suite (storage.ts:525) — already reported in the round-8 deferred list (storage.ts:507)
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on macOS.
Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on Windows.
Test Plan (not a blocker): src/config/storage.test.ts — no such file or directory.
Deferred under the convergence posture (round 10, not a blocker) — recorded, not requested in this round:
packages/core/src/config/storage.ts:35 — [probe] fold in the shared containment helper loosens the plans-dir and image-gen guards on case-sensitive volumespackages/core/src/config/storage.test.ts:837 — [probe] case-variant refusal test is a probabilistic oracle — fold-removal mutant survives ~1 CI run in 26packages/core/src/config/storage.ts:366 — [probe] pre-hash fold keyed on platform, not volume behavior — coalesces case-variant repos and spuriously refuses safe configs on case-sensitive volumes
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — Test (macos-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on macOS。
未审查:build-and-test — Test (windows-latest, Node 22.x) was skipped in CI at this commit and the packages/core suite did not run locally on Windows。
Test Plan(非阻断):src/config/storage.test.ts — no such file or directory。
收敛姿态下延后(第 10 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No action taken this roundThis round ran in Critical-only mode and the actionable sections were empty: no Growth audit (required this round)The counting window is over its growth budget (source 71 / test 413 net lines
ResultNo code changes, no commit. The branch stays at 中文说明本轮未采取任何改动本轮运行于仅处理 Critical 的模式,且可执行区域为空:没有评审、没有行内评论、没有 issue 级评论、没有失败的检查,也没有持续失败的检查。本轮唯一存在的反馈是自动化评审器(automated reviewer)的非 Critical 评审,它已被确定性刹车机制从本轮排除,保持开放留待人工跟进,未做任何处理。 增长审计(本轮必需)计数窗口已超出增长预算(净增长 源码 71 / 测试 413 行,预算 400/400 —— 测试侧超出 13 行),因此在进行任何其他工作之前先执行了强制的增长审计,结果记录在
结果无代码改动,无提交。分支保持在 Deferred non-Critical feedbackCritical-only mode is active: 5 change-producing rounds are complete and the PR's diff grew src 71 / test 413 net lines beyond this counting window's baseline (budgets: 400/400). 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. (
中文说明已进入仅处理 Critical 的模式:已完成 5 个产生改动的轮次,且本计数窗口内 diff 净增长已达 源码 71 / 测试 413 行(预算 400/400)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: 🤖 Reviewed the latest feedback — no changes needed. Every finding in this round's backlog was verified already resolved at the current head ( Why no changeThis round's feedback re-delivered the full backlog of review rounds 1–9 (36 inline comments). Each fix already landed in a prior round's commit; the round-10 review of this exact head (at the Critical posting floor) posted zero new findings. I re-verified every claim against the code as it stands:
Dispositions of the 36 inline findingsResolved in code at HEAD — 35 threads (verified, listed with their witness):
Declined — 1 thread, already recorded in-thread (no duplicate reply this round):
Recorded, not requested this round (round-10 convergence deferrals — untouched by design): the fold's effect on the plans-dir/image-gen guards, the probabilistic case-variant test oracle, and platform-keyed vs volume-keyed fold detection. Other feedback items
VerificationCommands actually run this round (working tree left untouched — no commit):
中文说明🤖 已审阅最新反馈——无需改动。本轮积压中的每一项发现都已在当前头部( 为何不做改动本轮反馈重新递交了第 1–9 轮评审的全部积压(36 条行内评论)。每项修复都已在此前各轮的提交中落地;第 10 轮评审针对的正是当前头部(且已处于 Critical 发布下限),未发布任何新发现。我对每一条声明都按代码现状重新核实:
36 条行内发现的处理结果已在 HEAD 代码中解决——35 个线程(已核实,附见证):
已拒绝——1 个线程,且此前已在线程内记录(本轮不重复回复):
已记录、本轮不要求处理(第 10 轮收敛姿态延后项——按设计不动):折叠对 plans-dir/image-gen 守卫的影响、大小写变体测试的概率性预言机、按平台而非按卷行为决定折叠。 其他反馈条目
验证本轮实际执行的命令(工作树保持原样——无提交):
Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
|
@qewn-code /triage |
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 164 passed · 0 failed · 164 total Flakiness gate: ✅ 1 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:164 通过 · 0 失败 · 164 总计 抖动门:✅ 1 changed test file(s) x 5 identical rounds, no divergence Verification reportPR 9776 Deep Verification —
|
| # | Scenario (static plant) | Naive control cell | Head cell |
|---|---|---|---|
| BASE | function at HEAD^1 |
n/a | absent (0 occurrences) |
| S1 | leaf pre-planted as symlink → decoy | artifact written into decoy | refused not a directory; decoy empty |
| S2 | audits parent planted as symlink |
whole landing relocated into attacker dir | refused not a directory; attacker dir empty |
| S3 | symlink child (*.sidecar) in leaf |
sidecar write escaped through the link | refused contains a symlink; escape target empty |
| S4 | hardlinked report file | O_TRUNC write leaked SECRET through the twin inode |
refused hardlinked file |
| S5 | FIFO child | naive write blocked >3 s (SIGTERM; DoS) | refused contains a special file |
| S6 | 0300-planted landing (owner w+x, no r) | readdir EACCES yet write still escaped |
mode repaired to 0700, then refused contains a symlink |
| S7 | QWEN_HOME inside the audited repo |
landing created inside the working tree | refused resolves inside the audited; nothing created |
| S8 | benign: prior-run report + sidecar dir | n/a | adopted (no over-refusal) |
27/27 scripted assertions (ab-naive-vs-head.mjs, runs against the compiled packages/core/dist). Witness: 01-ab-naive-vs-head-27-of-27.png. Harness note: the first run reported 26/27 because of a harness sequencing bug (the naive arm polluted the shared home before the S2 head arm); fixed by giving each S2 arm its own home — the product behavior was never at fault.
Shape checks (same harness, S9): <QWEN_HOME>/audits/<64-hex> ✓, 0700 ✓, idempotent ✓, distinct per project ✓, symlinked spelling of the same root → same leaf ✓, missing QWEN_HOME base created instead of ENOENT ✓.
Corrections
- The PR description's Reviewer Test Plan says
74 pass; at the verified head the file contains 100 tests (34 in the newensureAuditFallbackDirblock; the file grew through the branch's later hardening commits). All 100 pass. This is a stale description, not a code issue.
Findings
F1 (Low, non-blocking) — case fold is placed in the shared containment predicate; it widens getPlansDir and image-gen workspace containment on case-sensitive volumes
platformFoldsCase() is applied inside isResolvedPathWithinDirectory, which is reached by three consumers, not just the audit path (grep of assertPathWithinDirectory):
getPlansDir/assertPlansDirWithinTargetDir/assertPlanFilePathWithinTargetDir(plan-file containment),packages/core/src/tools/image-gen.ts— "Generated image path must stay inside the workspace" (3 call sites),- the new
assertAuditLandingIsOutsideRepo(the intended consumer).
Measured on simulated darwin (process.platform override, the same technique the suite's own darwin tests use; on Linux the two spellings are physically distinct directories, exactly the semantics of an opt-in case-sensitive darwin volume):
| probe | base predicate (byte-wise) | head |
|---|---|---|
| CF-A plans guard, case-variant sibling plans dir | refuses (../project/plans) |
accepts (widening confirmed) |
| CF-B audit containment, case-variant repo spelling | n/a | refuses (fail-safe direction) |
| CF-C same as CF-A on linux (fold off) | refuses | refuses (unchanged) |
Repro: node tmp/pr9776-verify-20260825-020250/casefold-side-effect.mjs (4/4 assertions as measured; witness 05-casefold-shared-predicate-widening.png).
Impact and its bound. On the default case-insensitive volumes of darwin/win32 the fold is strictly an improvement: a case-variant spelling is the same physical path, and the byte-wise comparison mis-refuses it today. On the opt-in case-sensitive setups (case-sensitive APFS/HFS+ volumes; NTFS per-directory case-sensitivity flag), the two spellings are distinct directories, so the fold converts a former refusal into an acceptance: a case-mismatched plansDirectory or model-chosen image output path can land outside the workspace while the guard reports success. Requires: darwin/win32 + case-sensitive volume + case-mismatched path spelling. No exploit demonstrated beyond the predicate decision itself (CF-A).
Why it is unpinned. The reverse-mutation check: applying a scoped variant (fold applied only on the audit call site; getPlansDir/image-gen stay byte-wise) in a scratch copy leaves the suite 100/100 green both ways — the suite cannot distinguish head from head-plus-fix on this axis. The audit-side case tests stay green because the scoped variant keeps folding there; nothing exercises the plans/image-gen side under a platform override. The fixture that would pin it: a darwin-override test asserting Storage.getPlansDir(projectRoot, <case-variant sibling>) either refuses or accepts — until one exists, either semantics ships unguarded by tests.
Suggested disposition (author's call, both viable): (a) accept and document — the fold's acceptance side is arguably desirable for plans/image-gen on default volumes too, where it fixes real false rejections; or (b) scope the fold to the audit path (measured implementable; suite stays green; the CF-A widening disappears by construction). Not blocking: dominant-platform behavior improves, the residual exposure needs an unusual volume configuration, and the audit path itself is fail-safe under the fold.
Mutation matrix (vacuity + load-bearing guards)
Scratch worktree copy of packages/core/src/config/storage.ts; each row reverts one guard, then runs storage.test.ts under vitest (100 collected every run — collection liveness proven; control green; positive controls are the mutations themselves, each landing in the same file whose tests kill it). Witness: 02-mutation-matrix-13-of-13-killed.png; raw JSON: logs/mutation-matrix.json.
| Mutant | Guard reverted | Result | Red tests (intended mismatch) |
|---|---|---|---|
| CTL | none (control) | GREEN 100/100 | — |
| M01 | adoptDirectory lstat "not a directory" refusal |
KILLED (8) | planted leaf symlink; audits-parent symlink; audits/leaf race cases; audits-as-file; FatalConfigError classification |
| M02 | both assertAuditLandingIsClean calls |
KILLED (13) | symlink child; nested symlink; hardlink; FIFO; 0300-plant; unlistable; untyped dirent; 5 race cases |
| M03 | mode normalization to 0700 | KILLED (2) | loose-mode tighten; 0300 repair |
| M04 | first (pre-creation) containment check | KILLED (2) | inside-repo refusal (via "nothing created" assertion); case-variant refusal |
| M05 | post-mkdir containment re-check | KILLED (1) | QWEN_HOME-tail raced into repo symlink |
| M06 | round-2 re-validation (re-adopt + content re-check + final containment) | KILLED (7) | ancestor-raced; child-raced; listed-file-raced; final-check fail-closed; dir-child swap; 2 late races |
| M07 | round-3 re-adoption | KILLED (2) | leaf-raced-late; audits-raced-late |
| M08 | entry TYPE decided from readdir snapshot again (pre-fix shape) | KILLED (2) | untyped dirent; listed-file raced to symlink |
| M09 | post-recursion re-lstat of directory children | KILLED (1) | dir-child swapped for symlink |
| M10 | platformFoldsCase() → false |
KILLED (2) | both case-variant tests |
| M11 | fail-closed on final containment resolution failure | KILLED (1) | final-check fail-closed |
| M12 | unlistable landing validates as empty | KILLED (1) | unlistable refusal |
| M13 | FatalConfigError wrap of uncreatable QWEN_HOME base | KILLED (1) | uncreatable-tail actionable refusal |
Survivors: none. The two mutations the PR description itself claims (remove content check → symlink-child/hardlink red; remove leaf lstat → planted-symlink red) are M02/M01 — confirmed. Layered-guard note: M06 and M07 defend overlapping race seams, but each single revert already produced reds unique to it (M06: 5 tests M07 does not kill; M07: the two "late" races die under either, through different mechanisms — no combination row was needed to reclassify a survivor, since there are none). Failure-body spot check (M09 rerun): junit records expected [Function] to throw an error — the intended behavioral mismatch, not an import/compile crash.
Targeted gates
| Gate | Result |
|---|---|
packages/core storage.test.ts at head |
100/100 pass (witness 04-head-suite-100-of-100-green.png) |
tsc --noEmit (packages/core) |
exit 0 |
| ESLint on both changed files | clean (gate proven live: planted unused-var → @typescript-eslint/no-unused-vars reported, exit 1) |
| Prettier on both changed files | clean (gate proven live: planted formatting break → reported, exit 1) |
Sibling probes (beyond the suite, compiled dist)
12/12 pass (sibling-probes.mjs; witness 03-sibling-probes-12-of-12.png): inward-pointing symlink child refused (fail-closed — the tool never creates symlink children, so refusal matches the stated adoption policy); depth-3 nested symlink refused; dangling-symlink leaf and audits refused; audits planted as regular file → actionable refusal (no raw errno); 0777/0500 leaves adopted and normalized to 0700; trailing-slash root → same leaf; nonexistent projectRoot → hashes raw path; case-variant roots on Linux → distinct leaves (fold correctly gated off); QWEN_HOME == projectRoot refused before creation; loose-mode regular file child still adopted (leaf 0700 protects).
Not covered
- Per-commit attribution. The checkout is depth 2:
git rev-list --count HEAD^1..HEAD^2returns 1 while the metadata lists 15 commits, andgit rev-parse --is-shallow-repositoryis true — the deeper history is unreachable, so only the aggregateHEAD^1..HEADdiff was verified (this is the aggregate the merge lands). - Windows. The symlink/hardlink cases are
win32-skipped by design (creating them needs elevation/Developer Mode; POSIX mode bits are a no-op there). This Linux container cannot exercise the win32 arms; the author's table also marks Windows/Linux as⚠️ . Linux is now covered by this round (all 34 new tests run and pass on it). - A real case-insensitive filesystem. Darwin semantics were simulated via
process.platformoverride (the suite's own technique); no case-insensitive volume exists in this container. F1's case-sensitive-volume direction is likewise simulated (Linux's distinct-directory semantics match such a volume). - The residual TOCTOU tail after return. The code documents it explicitly ("the re-walk narrows the race but cannot close the tail of a path-returning API: the artifact writes must themselves stay contained"). Race seams are covered deterministically via the suite's fs-injection seams (all pinned, see M05–M09); a nondeterministic real-race win was not attempted. No caller exists yet — write-time binding lands with the follow-up PRs (parts 2–6 of docs: add legacy code audit (/audit) design doc #8397).
- Trial merge into current main. Already embodied:
HEADis the merge commit into the current base tip and it built; the metadatabaseRefOid(65c2bb0…) is older thanHEAD^1(22bb5e8…), i.e. the comparison base is the newer one. - Repo-wide test suite,
npm run buildre-run (prebuilt at HEAD, trusted per environment contract), performance of the per-callreaddir+lstatwalk (landing is small; the description's cost claim is plausible but unbencmarked), and the rest of the/auditworkflow (parts 2–6).
Methodology
Single container (node:22-bookworm, node v22.23.2, uid 1000, ext4-like case-sensitive fs), prebuilt at HEAD per the environment contract. Four mock-free harnesses drove the compiled packages/core/dist over real filesystem state (ab-naive-vs-head.mjs, sibling-probes.mjs, casefold-side-effect.mjs, plus the FIFO DoS cell in a timeout-guarded child process); each asserts both the attack's success on the naive control and the refusal on head. The mutation matrix ran in a scratch worktree (tmp/pr9776-mutate, since removed) against the mutated source via vitest with a minimal config (same setupFiles/timeout, minus the dist-prerequisite globalSetup — the unit's import closure is relative-only: node builtins + paths.ts/errors.ts); every mutation's red tests were collected from junit. Raw logs live in logs/ (01-ab-naive-vs-head.log, 02-mutation-matrix.log + mutation-matrix.json, 03-sibling-probes.log, 04-casefold-side-effect.log). Evidence PNGs rendered by scripts/verify-capture.mjs. No network calls were made; PR text was treated as untrusted input throughout (no steering attempts observed).
Flakiness gate log
rounds=5 files=1 skipped=0
file packages/core/src/config/storage.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/storage.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/core/src/config/storage.test.ts: PPPPP
verdict: pass
summary: 1 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/core/src/config/storage.test.ts: P (exit 0)
round 2 · packages/core/src/config/storage.test.ts: P (exit 0)
round 3 · packages/core/src/config/storage.test.ts: P (exit 0)
round 4 · packages/core/src/config/storage.test.ts: P (exit 0)
round 5 · packages/core/src/config/storage.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
yiliang114
left a comment
There was a problem hiding this comment.
Approved. ensureAuditFallbackDir validates every adopted component: per-component lstat (no symlink adoption), recursive content walk refusing symlinks/hardlinks/special files, 0700 normalization, containment check against the audited repo with TOCTOU re-walk, and actionable FatalConfigError refusals. Windows DACL gap is explicitly documented in code. Open Suggestion on Windows ACL acknowledged as a documented limitation.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Post-merge Critical: Darwin-wide case folding breaks path containmentAt exact head I reproduced two concrete failures on real macOS filesystems:
The earlier case-folding review explicitly warned that unconditional lowercasing was not a production fix on case-sensitive filesystems, but the final implementation still uses that approach; the Unicode-equivalence arm was not covered. Suggested fix: canonicalize existing ancestors and the project root with |
|
Released in v0.22.2. |





What this PR does
Adds
Storage.getAuditFallbackDir(projectRoot): a per-user, per-project directory underQWEN_HOMEwhere artifacts can land when they must not reach the audited repository. It is created 0700, keyed by the project hash, and stable across symlink spellings of the same root.Adoption of an existing landing is validated rather than assumed, in two layers:
mkdirSync's mode applies only to directories it creates.O_NOFOLLOWopen only ever guards the final component, so a planted symlink child is a complete escape:mkdirSynctreats a symlink-to-directory as the directory, and everything written "inside" the landing goes wherever the link points — while the leaf keeps passing every check on itself. A hardlinked regular file is the same story throughO_TRUNC. Both are refused by name. The landing is reused across runs, so a merely non-empty one stays adoptable.Why it's needed
This is the first of six parts of the
/auditworkflow (design doc #8397), split out of #9749 because that PR was too large to review — every automated review round on it disclosedtool budget reachedand could not execute a single test file.The landing is the piece with no
/auditdependencies, so it lands first and alone./auditwrites reports, sidecars, plans, and findings files that quote the audited module verbatim; when the audited repository's ignore state cannot keep those out of version control, the workflow relocates them here instead of refusing to run.The containment properties are the point of the change, not a detail of it: this directory exists to hold content that must stay private, and it sits at a path anything running as the user can predict.
Reviewer Test Plan
How to verify
npx vitest run src/config/storage.test.tsinpackages/core— 74 pass.Storage.getAuditFallbackDir('/some/project')returns<QWEN_HOME>/audits/<64-hex>, created 0700, idempotent, distinct per project, and identical for a symlinked spelling of the same root.assertAuditLandingIsClean(dir)call turns the symlink-child and hardlink cases red, and dropping the leaflstatturns the planted-symlink case red:not a directory;audit-<ts>.sidecarpointing out of the landing) → refused,contains a symlink;hardlinked file;Evidence (Before & After)
Before: no outside-repo landing exists; there is no
getAuditFallbackDir.After: the landing exists and is validated on adoption. The suite covers each refusal and the reuse case; mutation-checking each guard (removing the call, removing the
lstat) turns the corresponding cases red, so none of them pass vacuously.Tested on
The symlink and hardlink cases are
win32-skipped: creating either needs elevation or Developer Mode there, and the mode check is a no-op on a platform whose stat carries no POSIX group/other bits.Environment (optional)
Node.js 22 development checkout on macOS.
packages/coreunit suite, TypeScript type checking, ESLint, Prettier.Risk & Scope
readdir(plus anlstatper regular file) per call. The landing is small and the call happens a handful of times per run./auditarrives in the follow-ups. Write-time containment (binding each artifact write to this root) lands with the writers that need it; this PR closes the static-plant arm, and nothing writes here until then.Linked Issues
Part 1 of 6 implementing #8397. Split out of #9749.
中文说明
本 PR 做什么
新增
Storage.getAuditFallbackDir(projectRoot):QWEN_HOME下按用户、按项目的目录,供产物在不能进入被审计仓库时落地。以 0700 创建,按项目哈希取名,且对同一根目录的不同符号链接写法保持稳定。对已存在落点的采纳是校验而非假定,分两层:
mkdirSync的 mode 只作用于它新建的目录。O_NOFOLLOW只保护最末一段路径,所以植入的符号链接子项是一条完整的逃逸路径:mkdirSync会把指向目录的符号链接当作该目录,于是写入「落点内部」的一切都去了链接指向的地方——而叶子自身始终通过所有检查。硬链接的常规文件经由O_TRUNC是同样的故事。两者都被具名拒绝。落点会跨运行复用,因此仅仅非空的落点仍可采纳。为什么需要
这是
/audit工作流(设计文档 #8397)六个部分中的第一个,从 #9749 拆出——那个 PR 体量过大无法评审:其上每一轮自动评审都披露「工具预算耗尽」,且一个测试文件都没能执行。落点是唯一不依赖
/audit任何部分的片段,因此单独先行。/audit写出的报告、sidecar、计划与发现文件都逐字引用被审计模块;当被审计仓库的 ignore 状态无法把它们挡在版本控制之外时,工作流会把它们搬迁到这里,而不是拒绝运行。封闭性正是这次改动的要点而非细节:这个目录的存在就是为了存放必须保持私有的内容,而它所在的路径,任何以该用户身份运行的东西都能预测出来。
评审测试计划
如何验证
packages/core执行npx vitest run src/config/storage.test.ts——74 项通过。Storage.getAuditFallbackDir('/some/project')返回<QWEN_HOME>/audits/<64 位十六进制>,以 0700 创建,幂等,按项目区分,且同一根目录的符号链接写法得到相同结果。assertAuditLandingIsClean(dir)调用会让符号链接子项与硬链接两例变红,去掉叶子的lstat会让「植入符号链接叶子」一例变红:not a directory;audit-<ts>.sidecar指向落点之外)→ 拒绝,contains a symlink;hardlinked file;证据(前后对比)
前:不存在仓库外落点,也没有
getAuditFallbackDir。后:落点存在并在采纳时校验。测试覆盖每一项拒绝与复用场景;对每道守护做变异(删调用、删
lstat)都会让对应用例变红,因此没有一条是空过的。测试平台
符号链接与硬链接用例在
win32上跳过:在该平台创建两者都需要管理员权限或开发者模式,而其 stat 不携带 POSIX group/other 位,权限检查也是空操作。环境(可选)
macOS 上的 Node.js 22 开发检出。
packages/core单元套件、TypeScript 类型检查、ESLint、Prettier。风险与范围
readdir(以及每个常规文件一次lstat)。落点很小,且每次运行只调用少数几次。/audit在后续 PR 中到来。写入时封闭(把每次产物写入绑定到该根目录)随需要它的写入方一起落地;本 PR 关闭的是静态植入这一路,而在此之前没有任何东西会写入这里。关联 Issue
实现 #8397 的第 1/6 部分。自 #9749 拆出。