Skip to content

fix(core): keep no-follow reads protected where O_NOFOLLOW is missing - #10007

Open
yiliang114 wants to merge 20 commits into
QwenLM:mainfrom
yiliang114:fix/issue-8227-windows-nofollow
Open

fix(core): keep no-follow reads protected where O_NOFOLLOW is missing#10007
yiliang114 wants to merge 20 commits into
QwenLM:mainfrom
yiliang114:fix/issue-8227-windows-nofollow

Conversation

@yiliang114

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a cross-platform "open without following symlinks" helper (openNoFollow / openSyncNoFollow in packages/core/src/utils/no-follow-open.ts) and routes the confirmed O_NOFOLLOW read call sites through it. On platforms that expose O_NOFOLLOW the helper simply ORs the kernel flag into the open flags — behavior is byte-for-byte unchanged. Where the constant does not exist (Windows: fs.constants.O_NOFOLLOW is undefined), the previous (O_RDONLY | (O_NOFOLLOW ?? 0)) expressions silently collapsed into a plain open that follows symlinks; the helper instead compensates with an lstatopenfstat identity check that refuses symlinked final components, refuses opens whose dev/ino no longer matches the pre-open lstat (the swap race), and refuses filesystems that report ino: 0 because identity cannot be proven there. Every refusal carries code: 'ELOOP' so existing caller error handling applies unchanged.

Converged call sites: the validated @-file read path (readManyFiles.ts, both the text-handle read and the snapshot), session metadata reads (sessionStorageUtils.ts), background-shell output tails (backgroundShellRegistry.ts), untracked diff line counting and hunk synthesis (gitDiff.ts), the workspace registration store (cli/serve/workspace-registration-store.ts), and session-artifact workspace status (acp-bridge/sessionArtifacts.ts).

Why it's needed

PR #7206 hardened @-referenced file reads with symlink/TOCTOU protection, but on Windows that protection materially disappears because O_NOFOLLOW is undefined and the ?? 0 fallback drops the guarantee (issue #8227, claim 1). With the constant stubbed away (the Windows flag set), a symlink planted over a session file redirected the metadata read to the link target, and a symlink planted over a background-shell output file leaked its content into model context — both reproduced as red tests in this PR before the fix. Claim 2 of the issue (vacuous dev/ino checks) was already closed by #8290 and #9857; the fail-closed posture on ino: 0 established there is reused here via hasVerifiableInode.

Reviewer Test Plan

How to verify

Two new reproduction tests stub fs.constants.O_NOFOLLOW to undefined (the same seam session-start-profiler.test.ts already uses) and plant a symlink at the read path; they fail on main and pass with this PR:

cd packages/core
npx vitest run src/utils/sessionStorageUtils.test.ts src/services/backgroundShellRegistry.test.ts src/utils/no-follow-open.test.ts
  • Before: sessionStorageUtils ... expected undefined, received 'leaked-secret' and BackgroundShellRegistry ... expected '<task-notification>...' not to contain 'secret credentials'.
  • After: both pass; no-follow-open.test.ts additionally pins the native path (regular file opens, symlink → ELOOP, ENOENT passthrough) and the fallback path (symlink refusal, identity-mismatch refusal, ino: 0 refusal).

Broader targeted suites, all green: gitDiff.test.ts (124), readManyFiles.test.ts, sessionService.test.ts, session-transcript-reader.test.ts, gitDirect.test.ts, session-start-profiler.test.ts, fileReadCache.test.ts — 770 tests across 10 core files; cli workspace-registration-store.test.ts (26 passed / 1 skipped); acp-bridge sessionArtifacts.test.ts (126). tsc --noEmit clean for core, cli, and acp-bridge; ESLint clean on all changed files.

Evidence (Before & After)

N/A — no user-visible UI change (defense-in-depth on internal read paths).

Tested on

OS Status
🍏 macOS ⚠️ not tested (CI covers)
🪟 Windows ⚠️ not tested (CI covers; fallback path exercised via stubbed constants on Linux)
🐧 Linux ✅ tested

Environment (optional)

Unit tests only (vitest), Node v24.19.0.

Risk & Scope

  • Main risk or tradeoff: on platforms without O_NOFOLLOW the fallback adds one lstat + one fstat per open (low-frequency read paths only), and reads on ino: 0 volumes (FAT/exFAT, some SMB) now fail closed instead of opening without any no-follow guarantee — consistent with the posture decided in fix(core): fail closed on zero inode file cache #8290/fix(core): reject unverifiable validated read inodes #9857. POSIX behavior is unchanged (same kernel flag as before).
  • Not validated / out of scope: issue claim 3 (Windows CI runner coverage and un-skipping skipIf(process.platform === 'win32') tests) remains separate follow-up scope, as in fix(core): reject unverifiable validated read inodes #9857. Other O_NOFOLLOW call sites not confirmed in the issue thread (write paths such as skill-args-file.ts, gitUtils.ts, skill-curator.ts, session-writer-lease.ts; platform-guarded sites in session-start-profiler.ts / sessionService.ts; typeof-guarded voice-keyterms.ts / customBanner.ts) are intentionally left for a follow-up.
  • Breaking changes / migration notes: none.

Linked Issues

Refs #8227 (claim 1 compensating control; claim 3 remains follow-up). Claim 2 was closed by #8290 and #9857. Builds on the hardening introduced by #7206.

中文说明

本 PR 做了什么

新增跨平台"打开但不跟随符号链接"助手函数(packages/core/src/utils/no-follow-open.ts 中的 openNoFollow / openSyncNoFollow),并把已确认的 O_NOFOLLOW 读取调用点统一收敛过去。在提供 O_NOFOLLOW 的平台上,助手只是把内核标志按位或进打开标志——行为与之前完全一致。在该常量不存在的平台(Windows 上 fs.constants.O_NOFOLLOWundefined),之前 (O_RDONLY | (O_NOFOLLOW ?? 0)) 的写法会静默塌缩成跟随符号链接的普通 open;助手改用 lstatopenfstat 身份校验来补偿:拒绝末位成分是符号链接的路径,拒绝打开后 dev/ino 与打开前 lstat 不一致的情况(即替换竞态),并拒绝报告 ino: 0 的文件系统(因为无法证明文件身份)。所有拒绝都带 code: 'ELOOP',调用方现有的错误处理无需改动即可生效。

收敛的调用点:@ 文件校验读路径(readManyFiles.ts 的文本句柄读取与快照两处)、会话元数据读取(sessionStorageUtils.ts)、后台 shell 输出尾部(backgroundShellRegistry.ts)、untracked diff 行数统计与 hunk 合成(gitDiff.ts)、工作区注册存储(cli/serve/workspace-registration-store.ts)、会话产物工作区状态(acp-bridge/sessionArtifacts.ts)。

为什么需要

PR #7206@ 引用文件读取加了符号链接/TOCTOU 防护,但在 Windows 上这层防护实质失效:O_NOFOLLOWundefined?? 0 回退把保证丢掉了(issue #8227 的 claim 1)。把该常量 stub 掉(即 Windows 的标志集)后可以复现:在会话文件上植入符号链接,元数据读取会被重定向到链接目标;在后台 shell 输出文件上植入符号链接,其内容会泄漏进模型上下文——两者都作为红测试包含在本 PR 中,修复前失败、修复后通过。issue 的 claim 2(dev/ino 空检)已由 #8290#9857 关闭;本 PR 复用它们确立的 ino: 0 fail-closed 立场(hasVerifiableInode)。

审阅者测试计划

如何验证

两个新的复现测试把 fs.constants.O_NOFOLLOW stub 成 undefined(与 session-start-profiler.test.ts 已有的测试接缝相同),并在读取路径上植入符号链接;它们在 main 上失败,在本 PR 上通过:

cd packages/core
npx vitest run src/utils/sessionStorageUtils.test.ts src/services/backgroundShellRegistry.test.ts src/utils/no-follow-open.test.ts
  • 修复前:sessionStorageUtils ... expected undefined, received 'leaked-secret' 以及 BackgroundShellRegistry ... expected '<task-notification>...' not to contain 'secret credentials'
  • 修复后:两者通过;no-follow-open.test.ts 还固化了原生路径(正常文件可打开、符号链接 → ELOOP、ENOENT 透传)和回退路径(符号链接拒绝、身份不一致拒绝、ino: 0 拒绝)。

更大范围的目标测试套件全部通过:gitDiff.test.ts(124)、readManyFiles.test.tssessionService.test.tssession-transcript-reader.test.tsgitDirect.test.tssession-start-profiler.test.tsfileReadCache.test.ts——core 共 10 个文件 770 个测试;cliworkspace-registration-store.test.ts(26 通过 / 1 跳过);acp-bridgesessionArtifacts.test.ts(126)。tsc --noEmitcorecliacp-bridge 三个包均通过;所有改动文件 ESLint 通过。

证据(前后对比)

N/A——无用户可见 UI 变化(内部读取路径的纵深防御)。

测试环境

系统 状态
🍏 macOS ⚠️ 未测试(CI 覆盖)
🪟 Windows ⚠️ 未测试(CI 覆盖;回退路径已在 Linux 上通过 stub 常量验证)
🐧 Linux ✅ 已测试

运行环境(可选)

仅单元测试(vitest),Node v24.19.0。

风险与范围

  • 主要风险或取舍:在没有 O_NOFOLLOW 的平台上,回退路径每次 open 多一次 lstat + 一次 fstat(仅低频读取路径);在 ino: 0 的卷(FAT/exFAT、部分 SMB)上,这些读取现在会 fail closed,而不是在毫无 no-follow 保证的情况下打开——与 fix(core): fail closed on zero inode file cache #8290/fix(core): reject unverifiable validated read inodes #9857 确定的立场一致。POSIX 行为不变(与之前相同的内核标志)。
  • 未验证 / 超出范围:issue 的 claim 3(Windows CI runner 覆盖、去掉 skipIf(process.platform === 'win32') 的测试)照 fix(core): reject unverifiable validated read inodes #9857 的做法留作后续单独跟进。未在 issue 线程中确认的其他 O_NOFOLLOW 调用点(写路径如 skill-args-file.tsgitUtils.tsskill-curator.tssession-writer-lease.ts;平台守卫的 session-start-profiler.ts / sessionService.ts;typeof 守卫的 voice-keyterms.ts / customBanner.ts)有意留给后续 PR。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Refs #8227(claim 1 的补偿控制;claim 3 留作后续)。claim 2 已由 #8290#9857 关闭。建立在 #7206 引入的加固之上。

O_NOFOLLOW does not exist on Windows: fs.constants.O_NOFOLLOW is undefined, so the `(O_RDONLY | (O_NOFOLLOW ?? 0))` flag expressions silently collapse into a plain open that follows symlinks, dropping the symlink/TOCTOU hardening added for @-referenced file reads (QwenLM#7206). Add a cross-platform open helper that uses the kernel flag where present and otherwise compensates with an lstat -> open -> fstat identity check, refusing symlinked paths, identity races, and zero-inode filesystems (fail-closed, matching QwenLM#8290/QwenLM#9857). Route the confirmed no-follow read call sites through it: validated @-file reads, session metadata reads, background-shell output tails, untracked diff line counts, the workspace registration store, and session-artifact workspace status.

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

qwen-code-ci-bot commented Aug 25, 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 25, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR — re-run after the merge of main (merge commit e2c80eda). Both external blockers from the last pass are now cleared: the conflict is resolved (the resolution itself is reviewed in the Stage 2 comment) and CI ran green on the reviewed head. Gate conclusions below are unchanged.

Template looks good ✓

Problem: this is an observed defect, not theoretical hardening. Issue #8227 documents that on Windows fs.constants.O_NOFOLLOW is undefined, so the house idiom (O_RDONLY | (O_NOFOLLOW ?? 0)) silently collapses into a plain open that follows symlinks. The PR ships two reproduction tests that fail on main (a planted symlink leaking session metadata, and one leaking background-shell output into model context).

Direction: aligned. This is the claim-1 compensating control for the hardening already accepted in #7206, and it reuses the fail-closed posture on ino: 0 that #8290/#9857 already decided. Claim 3 (Windows CI runner coverage) remains correctly out of scope.

Size: core paths touched (core + cli + acp-bridge) — 376 production lines vs. 938 test lines, plus 27 lines of subpath wiring (tsconfigs, vitest aliases, package.json exports) at the current head. Well under any advisory threshold. Author is a maintainer.

Approach: the scope still feels right after the iteration. Six confirmed read call sites converge on one shared helper instead of duplicating the lstat → open → fstat fallback; the redundant per-site flag helpers are deleted in the same pass; write paths and platform-guarded sites stay deferred per the issue thread. The merge of main added nothing to the PR's own surface — 18 of the 22 changed files are byte-identical to the previously reviewed tip, and the four files main also touched (sessionArtifacts and its test, the core barrel, core package.json) kept both sides' changes without dropping anything. POSIX behavior is byte-for-byte unchanged (same kernel flag).

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

Moving on to code review. 🔍

中文说明

感谢贡献!这是在合入 main(合并提交 e2c80eda)之后的 re-run。上一轮的两个外部阻塞都已解除:冲突已解决(解决方式在 Stage 2 评论中审查),且被审查的 head 上 CI 已全绿。以下门控结论不变。

模板完整 ✓

问题:这是已观测到的缺陷,不是理论性加固。Issue #8227 记录了 Windows 上 fs.constants.O_NOFOLLOWundefined,导致惯用写法 (O_RDONLY | (O_NOFOLLOW ?? 0)) 静默塌缩为跟随符号链接的普通 open。本 PR 附带两个在 main 上失败的复现测试(植入符号链接泄漏会话元数据、泄漏后台 shell 输出进入模型上下文)。

方向:对齐。这是 #7206 已接受加固的 claim 1 补偿控制,并复用了 #8290/#9857 已确立的 ino: 0 fail-closed 立场。claim 3(Windows CI runner 覆盖)继续正确地留在范围之外。

规模:触及核心路径(core + cli + acp-bridge)——当前 head 上 376 行生产代码、938 行测试代码,另有 27 行子路径接线(tsconfig、vitest 别名、package.json exports)。远低于任何提醒阈值。作者是维护者。

方案:迭代之后范围依然合理。六个已确认的读取调用点收敛到一个共享助手,而不是复制 lstat → open → fstat 回退逻辑;各调用点多余的标志位助手在同一改动中删除;写路径和平台守卫调用点按 issue 线程留给后续。合入 main 没有给本 PR 自身的改动面增加任何东西——22 个改动文件中 18 个与之前审查过的 tip 逐字节一致,main 同样改动过的 4 个文件(sessionArtifacts 及其测试、core 桶模块、core package.json)两侧改动都完整保留、没有丢失。POSIX 行为逐字节不变(同样的内核标志)。

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

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run on e2c80eda, the merge of main into the branch. The delta reviewed this pass is the merge itself — I verified blob-by-blob that 18 of the 22 changed files are byte-identical to the previously reviewed tip 5aec7c2e, and three-way diffed the four files main also touched. The code-review conclusions from the last pass still stand unchanged.

Merge of main — conflict resolution review

The only real conflict was getWorkspaceStatus in sessionArtifacts.ts, where two hardenings met: main (#10064) had switched the workspace identity check to bigint stats for NTFS 64-bit file-id precision, while this PR replaced the raw O_NOFOLLOW open with openNoFollow(). The resolution keeps both, and I checked it three ways (branch→merged, main→merged, and side-by-side against main's version of the region):

  • The merged region is structurally identical to main's, with exactly one substitution: fs.open(realPath, O_RDONLY | O_NOFOLLOW)openNoFollow(realPath). The { bigint: true } pre-open lstat, the bigint identity re-check, the isSameFile(BigIntStats, BigIntStats) typing, and the second non-bigint handle.stat() for the isFile/mtime comparisons (with its comment on why the bigint fstat can't be reused) are all preserved from main.
  • The isUnverifiableIdentityError degradation lands after the symlink check in the catch block, ordered correctly: inode-0 volumes degrade to { status: 'missing' } without the escape flag; real symlink refusals still flag escaped: true.
  • sessionArtifacts.test.ts unioned cleanly: main's bigint-aware lstat-spy refactor and this PR's UNVERIFIABLE_IDENTITY_CODE upsert/refresh tests are both present, no edits to either side.
  • core/src/index.ts is a purely additive union — zero deletions; the PR's no-follow-open exports sit alongside main's new ipc / scheduled-task / token-estimation exports. core/package.json differs from the branch tip only by main's version bump; the ./noFollowOpen subpath export is intact.
  • The helper's one internal dependency, hasVerifiableInode in file-identity.ts, is blob-identical across the merge, so no semantic drift under the helper.

Code review (carried over — code unchanged since the last pass)

The implementation matches my independent proposal for this problem — one shared helper, kernel flag where available, lstat → open → fstat identity fallback otherwise — and the iteration added exactly what the review rounds asked for:

  • The inode-0 refusal carries its own code. EUNVERIFIABLE instead of ELOOP, so callers' ELOOP-specific handling doesn't misfire on legitimate files that merely live on FAT/exFAT/SMB volumes. Each consumer picks its own degradation, and each is right for its context: session artifacts report missing without raising the symlink-escape flag, the workspace registration store raises an explicit "identity could not be verified" error, readManyFiles fails the validated read closed (identity is part of its contract), background-shell tails collapse to the unreadable marker, and only untracked diff display falls back to a plain open — display-only, gated by the immediately preceding lstat().isFile() check, restoring pre-Windows: validated @-file reads lose O_NOFOLLOW and may have vacuous dev/ino identity checks (follow-up to #7206) #8227 behavior on volumes where identity can never be proven. The tests pin both directions of that split: EUNVERIFIABLE falls back, ELOOP never does.
  • The fallback is fail-closed and fully pinned. Symlink refusal at the lstat gate; dev/ino identity re-check against the PRE-OPEN snapshot (the tests perturb only post-open stats, so an implementation re-basing the comparison on a fresh lstat would fail them); bigint-safe hasVerifiableInode (reused from file-identity.ts); best-effort fd/handle close on every rejection path, including the case where the close itself throws — which must not mask the pinned refusal. Each of these has a test that breaks if the behavior regresses.
  • The scope is exactly the confirmed set. The six converged read call sites (readManyFiles ×2, sessionStorageUtils ×2, backgroundShellRegistry, gitDiff ×2, workspace-registration-store, sessionArtifacts) are exactly the ones routed through the helper, and every deferred site named in the PR body (skill-curator, gitUtils, session-start-profiler, sessionService, voice-keyterms, customBanner, plus the write paths) is still on its old expression, awaiting the follow-up.
  • The noFollowOpen leaf subpath is wired everywhere the sibling transcriptRecords subpath is — cli / acp-bridge / integration-tests tsconfigs, cli / acp-bridge vitest aliases, core package.json exports — plus a serve-fast-path bundle check so the leaf import can't pull the core barrel into serve. The helper binds node:fs through a default import so spy-based suites intercept it; a namespace import would escape those spies.
  • Caller error semantics survive. sessionStorageUtils catches refusals to undefined exactly like a real ELOOP today; workspace-registration-store keeps its ENOENT → empty-snapshot branch; gitDiff and backgroundShellRegistry keep their fail-safe collapses; readManyFiles and sessionArtifacts keep their post-open validated-identity re-checks as the second layer.

No blockers found. Standing non-blocking nits: on Windows, sessionArtifacts.getWorkspaceStatus stats twice (the helper's identity check plus its own pre/post-open layer) — harmless on that low-frequency path; and the actual Windows runtime still has no coverage anywhere — that is issue claim 3, scoped as follow-up.

Files changed (22 of 22 shown)
File What changed
packages/core/src/utils/no-follow-open.ts New helper: kernel flag where available, lstat/open/fstat identity fallback otherwise, distinct EUNVERIFIABLE code for inode 0
packages/core/src/utils/no-follow-open.test.ts Pins native and fallback paths: symlink refusal, identity-mismatch refusal, pre-open snapshot comparison, inode 0, rejection-path close
packages/core/src/tools/readManyFiles.ts Both validated-read opens (text handle + snapshot) routed through the helper
packages/core/src/utils/sessionStorageUtils.ts Metadata reads use the sync helper; old flag helper deleted
packages/core/src/services/backgroundShellRegistry.ts Output-tail open uses the sync helper; old flag helper deleted
packages/core/src/utils/gitDiff.ts Untracked opens routed through the helper with an EUNVERIFIABLE-only plain-open fallback; memoized flag cache deleted
packages/core/src/index.ts Barrel re-export of the helper and its refusal helpers
packages/cli/src/serve/workspace-registration-store.ts Store read routed through the helper; explicit identity-unverified error
packages/acp-bridge/src/sessionArtifacts.ts Workspace-status open routed through the helper; inode-0 degrades to missing without the escape flag; merge kept main's bigint identity layer
packages/core/package.json Exports the ./noFollowOpen leaf subpath
packages/cli/tsconfig.json Paths entry for the leaf subpath
packages/acp-bridge/tsconfig.json Paths entry for the leaf subpath
integration-tests/tsconfig.json Paths entry for the leaf subpath
packages/cli/vitest.config.ts Alias resolving the leaf subpath to core source
packages/acp-bridge/vitest.config.ts Alias resolving the leaf subpath to core source
scripts/tests/serve-fast-path-bundle-check.test.js Guards that the leaf import does not pull the core barrel into serve
packages/core/src/utils/sessionStorageUtils.test.ts Reproduction: symlinked session file no longer leaks metadata under the Windows flag set (single and multi-field)
packages/core/src/services/backgroundShellRegistry.test.ts Reproduction: symlinked output file no longer leaks content into model context
packages/core/src/utils/gitDiff.test.ts Untracked reads on inode-0 volumes: line-count fallback, hunk fallback, and no fallback on ELOOP
packages/cli/src/serve/workspace-registration-store.test.ts Inode-0 refusal surfaces as a store error; barrel mock scoped to the write helper
packages/acp-bridge/src/sessionArtifacts.test.ts Inode-0 degrades to missing on upsert and refresh, never the escape flag
packages/core/src/services/sessionService.rename.test.ts lstat/fstat spies so the fallback accepts the fabricated session paths

Testing evidence (the PR's own CI via API — no PR code was executed in this review)

CI has now run on the reviewed commit and is fully green: all four pull_request-event workflows completed with success on e2c80eda — Qwen Code CI (#32499), Security Checks (#5469), Serve A/B (#3126), SDK Java (#4372). Zero failures, cancellations, or timeouts among the 27 check runs. The Test (macos-latest / windows-latest, Node 22.x) and Integration Tests (CLI, No Sandbox) lanes are skipped, as on every prior head — Windows/macOS runtime coverage is issue claim 3, follow-up scope. The only in-flight check is the automated-review orchestration run (pull_request_target event), which is bot machinery, not PR CI.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Secret scan (TruffleHog) ✅ success
Dependency CVE audit ✅ success
Real daemon E2E / Java 11 ✅ success
ubuntu-latest / Java 11 · 17 · 21 ✅ success
macos-latest / Java 21 ✅ success
windows-latest / Java 21 ✅ success
precheck-pr / precheck ✅ success
Classify PR ✅ success
label ✅ success
authorize ✅ success

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

Sandboxed verification is now unblocked — with the conflict resolved, refs/pull/10007/merge exists again. @qwen-code /verify remains the lane to prove the two planted-symlink reproduction tests fail on the base build and pass with this PR (i.e. the change is load-bearing) — a green suite proves the tests pass, not that main fails them. Nothing gates on it here: the fix's correctness is statically established (the ?? 0 collapse is visible in the base code) and CI is green on the exact reviewed commit.

Real-scenario testing: N/A — no user-visible behavior change (defense-in-depth on internal read paths), and this is an unattended run.

Not verified here: the author's local test-run numbers (770 core tests etc.) — attributed to the author, not re-run; Windows/macOS runtime behavior — those lanes are skipped in CI (issue claim 3, follow-up scope).

中文说明

合入 main —— 冲突解决审查

本轮在 e2c80eda(把 main 合入分支的合并提交)上 re-run。本轮审查的增量就是合并本身——逐 blob 核实了 22 个改动文件中 18 个与之前审查过的 tip 5aec7c2e 逐字节一致,并对 main 同样改动过的 4 个文件做了三方 diff。上一轮的代码审查结论原样成立。

唯一的真实冲突在 sessionArtifacts.tsgetWorkspaceStatus:两层加固在此相遇——main#10064)把工作区身份校验切到 bigint stat(NTFS 64 位 file-id 精度),本 PR 则把裸 O_NOFOLLOW open 换成 openNoFollow()。解决方式两者都保留,且我从三个方向核实过(branch→merged、main→merged、并与 main 版本逐段对照):

  • 合并后的区域在结构上与 main 完全一致,只有一处替换:fs.open(realPath, O_RDONLY | O_NOFOLLOW)openNoFollow(realPath){ bigint: true } 的打开前 lstat、bigint 身份复核、isSameFile(BigIntStats, BigIntStats) 类型、以及为 isFile/mtime 比较而做的第二次非 bigint handle.stat()(连同解释为何不能复用 bigint fstat 的注释)全部来自 main 且完整保留。
  • isUnverifiableIdentityError 降级落在 catch 块中符号链接检查之后,顺序正确:inode-0 卷降级为 { status: 'missing' } 且不打逃逸标记;真实的符号链接拒绝仍会打 escaped: true
  • sessionArtifacts.test.ts 干净并集:main 的 bigint 感知 lstat-spy 重构与本 PR 的 UNVERIFIABLE_IDENTITY_CODE upsert/refresh 测试都在,两侧均无改动。
  • core/src/index.ts 是纯增量并集——零删除;本 PR 的 no-follow-open 导出与 main 新增的 ipc/定时任务/token 估算导出并存。core/package.json 相对分支 tip 只有 main 的版本号变化;./noFollowOpen 子路径导出完好。
  • 助手唯一的内部依赖 file-identity.tshasVerifiableInode 在合并前后 blob 一致,助手底下没有语义漂移。

代码审查(承接上一轮——代码未变)

实现与我对这个问题的独立方案一致——一个共享助手,平台支持时走内核标志,否则用 lstat → open → fstat 身份回退——迭代恰好补齐了评审轮次要求的内容:

  • inode-0 拒绝使用独立错误码。 EUNVERIFIABLE 而非 ELOOP,避免调用方的 ELOOP 专属处理误伤只是恰好位于 FAT/exFAT/SMB 卷上的合法文件。每个消费方自选降级方式且都符合各自语境:会话产物报 missing 但不打符号链接逃逸标记、工作区注册存储抛出明确的"身份无法验证"错误、readManyFiles 对校验读直接失败关闭、后台 shell 输出尾部塌缩为不可读标记,只有 untracked diff 展示回退到普通 open——纯展示路径、紧邻前置 lstat().isFile() 守卫、恢复 Windows: validated @-file reads lose O_NOFOLLOW and may have vacuous dev/ino identity checks (follow-up to #7206) #8227 之前在这类卷上的行为。测试固化了该分流的两个方向:EUNVERIFIABLE 回退、ELOOP 绝不回退。
  • 回退路径 fail-closed 且已完全固化。 lstat 门口拒绝符号链接;对打开前快照做 dev/ino 身份复核(测试只扰动打开后的 stat,若实现改为基于新的 lstat 比较会直接失败);复用 file-identity.ts 的 bigint 安全 hasVerifiableInode;每条拒绝路径都尽力关闭 fd/句柄,包括关闭本身抛错时也不得遮蔽固定的拒绝错误。每一条都有回归即失败的测试。
  • 范围恰好是已确认的集合。 收敛的六个读取调用点与改走助手的集合完全一致;PR 正文点名的每个延后调用点仍保持旧表达式等待后续。
  • noFollowOpen 叶子子路径在所有兄弟子路径 transcriptRecords 的接线处都有对应条目——外加 serve 快速路径打包检查,确保叶子导入不会把 core 桶模块拖进 serve。助手通过默认导入绑定 node:fs,使基于 spy 的测试套件能拦截它。
  • 调用方错误语义保持不变。 sessionStorageUtils 对拒绝的捕获结果与今天真实 ELOOP 完全一致;workspace-registration-store 保留 ENOENT → 空快照分支;gitDiffbackgroundShellRegistry 保留故障安全塌缩;readManyFilessessionArtifacts 保留打开后的校验身份复核作为第二层。

未发现阻塞项。遗留的非阻塞小问题不变:Windows 上 sessionArtifacts.getWorkspaceStatus stat 两次(低频路径,无害);真实 Windows 运行时仍无覆盖——即 issue claim 3,后续范围。

测试证据(通过 API 读取本 PR 自己的 CI——本审查未执行任何 PR 代码)

被审查的提交上现在已跑过 CI 且全绿:e2c80eda 上四个 pull_request 事件的 workflow 全部成功——Qwen Code CI(#32499)、Security Checks(#5469)、Serve A/B(#3126)、SDK Java(#4372)。27 个 check run 中零失败、零取消、零超时。Test (macos-latest / windows-latest, Node 22.x)Integration Tests (CLI, No Sandbox) 通道照旧跳过——Windows/macOS 运行时覆盖是 issue claim 3,后续范围。唯一在途的 check 是自动评审编排(pull_request_target 事件),属于机器人机制而非 PR CI。

CI 表格见上方标记区域。

沙箱验证现在已解除阻塞——冲突解决后 refs/pull/10007/merge 重新可用。@qwen-code /verify 仍是证明两个植入符号链接的复现测试在 base 构建上失败、在本 PR 上通过(即改动承重)的通道——绿色套件只能证明测试通过,不能证明 main 会失败。此处不以它为前提:修复的正确性从静态上已经确立(?? 0 塌缩在 base 代码中可见),且 CI 已在被审查的确切提交上全绿。

真实场景测试:N/A——无用户可见行为变化(内部读取路径的纵深防御),且本次为无人值守运行。

此处未验证:作者本地测试数字(770 个 core 测试等)——转述作者说法,未重跑;Windows/macOS 运行时行为——这些通道在 CI 中被跳过(issue claim 3,后续范围)。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — approving. Both preconditions that held this PR at 4/5 last pass are now cleared: the conflict with main is resolved — carefully, as verified below — and CI is fully green on the exact commit I reviewed. What keeps this at 4 rather than 5 are the two standing, already-accepted nits: no real Windows runtime coverage anywhere yet (issue claim 3, scoped follow-up), and the load-bearing A/B (reproduction tests failing on the base build) rests on static evidence plus the PR's own CI rather than a /verify run.

Stepping back: this is what a good compensating-control PR looks like after iteration converges. The problem is documented in #8227, observable in the base code's own ?? 0 expressions, and reproduced with real planted-symlink tests. Six call sites converge on one helper; the per-site flag helpers it replaces are deleted in the same pass; POSIX stays byte-for-byte unchanged, so the entire risk surface is the new fallback — which is fail-closed, reuses the ino: 0 posture the repo already decided in #8290/#9857, and distinguishes "identity unverifiable" from "symlink refused" so downstream handling doesn't misfire. My independent proposal for this problem was this shape; I didn't find a simpler path the PR missed, and nothing in the diff is drive-by.

The merge of main was the only new material this pass, and it is clean: 18 of 22 files blob-identical to the reviewed tip, and the one genuine conflict (sessionArtifacts.getWorkspaceStatus, where main's bigint identity layer met this PR's openNoFollow swap) keeps both sides intact — the merged region reads exactly like main's with the single open-call substitution, the degradation ordering is right, and the test file, barrel, and package.json unions keep everything from both parents. If I had to maintain this in six months, the helper and its pinned tests are the kind of code you thank the author for.

Housekeeping: my approval below supersedes this bot's own two stale change-request reviews from the earlier automated rounds — same account, and GitHub counts the latest review per reviewer; every finding they carried was resolved as a thread and the code has been fully re-reviewed at this head. The remaining merge gate is the second human approval main requires.

Optional, not gating: @qwen-code /verify is unblocked now (refs/pull/10007/merge exists) if a maintainer wants the A/B proof that the two reproduction tests fail on the base build — the static case is already conclusive in my view.

中文说明

置信度:4/5 —— 批准合入。上一轮让本 PR 停在 4/5 的两个前置条件现已全部解除:与 main 的冲突已解决——且如下所验证,解决得很干净——被审查的确切提交上 CI 全绿。停在 4 而非 5 的,是两个既有的、已被接受的小问题:真实 Windows 运行时仍无任何覆盖(issue claim 3,已划为后续范围);承重性 A/B(复现测试在 base 构建上失败)目前依靠静态证据加本 PR 自己的 CI,而非 /verify 运行。

退一步看:这是一个优秀的补偿控制 PR 在迭代收敛之后该有的样子。问题记录在 #8227,在 base 代码自己的 ?? 0 表达式里可观察,并用真实植入符号链接的测试复现。六个调用点收敛到一个助手;被取代的各调用点标志位助手在同一改动中删除;POSIX 逐字节不变,整个风险面就是新的回退路径——它 fail-closed,复用仓库在 #8290/#9857 中已确立的 ino: 0 立场,并把"身份无法验证"与"符号链接被拒绝"区分开。我对这个问题的独立方案正是这个形状;没有找到 PR 遗漏的更简路径,diff 里也没有顺手夹带的改动。

合入 main 是本轮唯一的新材料,且是干净的:22 个文件中 18 个与审查过的 tip blob 一致;唯一的真实冲突(sessionArtifacts.getWorkspaceStatus——main 的 bigint 身份层与本 PR 的 openNoFollow 替换在此相遇)两侧都完整保留——合并后的区域与 main 的版本读起来完全一致,只有那一处 open 调用替换;降级顺序正确;测试文件、桶模块与 package.json 的并集把双亲的内容都留住了。如果六个月后由我来维护,这个助手和它固化的测试是那种你会感谢作者的代码。

事务性说明:下方的批准取代本机器人在更早自动评审轮次中的两个过期 request-changes 评审——同一账号,GitHub 只计每个评审者的最新评审;其中的每条发现都已作为线程解决,且代码已在当前 head 上被完整重新审查。剩余的合入门槛是 main 要求的第二个人类批准。

可选项(不作为门槛):@qwen-code /verify 现已解除阻塞(refs/pull/10007/merge 可用),若维护者想要"两个复现测试在 base 构建上失败"的 A/B 证明可以触发——在我看来静态证据已经足够定论。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

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

Qwen Code · serve A/B

@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 reverse-audit (round 1)": none — no check was cut short..

Test Plan (not a blocker): src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 1702, 23964, 21483, 1659, 601, 4226, 627 passed.

中文说明

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

Test Plan(非阻断):src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 1702, 23964, 21483, 1659, 601, 4226, 627 passed

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

Comment thread packages/core/src/utils/sessionStorageUtils.ts
Comment thread packages/core/src/utils/no-follow-open.ts
Comment thread packages/core/src/utils/no-follow-open.ts Outdated
Comment thread packages/core/src/utils/no-follow-open.test.ts Outdated
Comment thread packages/core/src/utils/sessionStorageUtils.ts
yiliang114 and others added 4 commits August 25, 2026 21:29
openSyncNoFollow bound node:fs through a namespace import, which vitest
resolves to its own copy of the externalized CJS module. Suites that spy
the fs object — sessionService.rename.test.ts stubs openSync/readSync for
fabricated session paths — never intercept that copy, so the open threw on
the mocked paths and the catch-all reported "no title" (10 of 19 tests red,
the CI Test failure). Take the fs binding through the default import the
way the callers' suites spy it, teach the doMock factories to carry the
stub on the default binding too, and stub lstatSync/fstatSync in the rename
suite so the Windows lstat -> open -> fstat fallback accepts the fabricated
paths as well.

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
The fallback's inode-0 fail-closed refusal carried code 'ELOOP' like a
genuine symlink refusal, so consumers with ELOOP-specific handling
misfired on LEGITIMATE files on inode-0 volumes (Windows FAT/exFAT/SMB,
where Node reports ino 0): session-artifact workspace status flagged a
contained file as an escape, the workspace registration store reported a
regular store as "must be a regular file", and untracked text files
rendered as binary with dropped hunks. Give the inode-unverifiable
refusal its own code (EUNVERIFIABLE) plus an isUnverifiableIdentityError
guard, keep ELOOP for genuine symlink refusals and identity races, and
adjust the three consumers: the artifact status degrades to plain
'missing', the store surfaces an identity-unverifiable error, and the
untracked diff read falls back to the pre-QwenLM#8227 plain read (its lstat
gate already rejected symlinks and non-regular files).

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
…opens

No call site and no test ever passes flags or mode — repo-wide, every
invocation opens by path alone — yet the fs.open-shaped signature was
exported through the core index and invited write/create flags through a
helper whose docs, lstat -> open -> fstat semantics, and tests cover only
the read-only case (O_WRONLY | O_CREAT would create on POSIX while the
Windows fallback's pre-open lstat throws ENOENT for the same input).
Hardcode the read-only base flags and delete resolveBaseFlags; a PR that
first needs more can re-add them with a caller and tests.

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
readLastJsonStringFieldsSync was rerouted through the no-follow helper in
the same pass as the single-field variant, but only the latter got a
symlink-refusal test — reverting the plural open to a plain fs.openSync
kept the whole suite green. Mirror the Windows-flag-set symlinked-session
test for the plural variant, asserting the all-undefined empty result;
the test is red on that mutant (leaked-secret surfaces) and green on the
restored code.

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout — cap-4 round on the 12:31Z review (single non-force push c067b924..47d0757fb to the fork branch):

  • C1 FIXED (bdecd4c): reproduced at head — sessionService.rename.test.ts 10 failed | 9 passed. Root cause: the no-follow helper bound node:fs via namespace import; vitest externalizes CJS namespace imports with their own module copy, so the suite's vi.spyOn(fs, ...) never intercepted it. Fix keeps the helper spy-mockable. Suite now 19/19 (= the red required CI Test).
  • C2 FIXED (84df828): inode-0 fail-closed refusal no longer carries code: 'ELOOP' — distinguishable from genuine symlink refusals, so consumer ELOOP handling (sessionArtifacts, workspace-registration-store, gitDiff) stops misfiring on legitimate files on inode-0 volumes (FAT/exFAT/SMB). The refusal posture itself (fix(core): fail closed on zero inode file cache #8290/fix(core): reject unverifiable validated read inodes #9857) unchanged. Note: spans 6 files (>3 ceiling) because the finding itself names 4 fix sites + test + index export; each hunk small and mechanical.
  • S1 FIXED (55ce9d7): dropped the never-used speculative flags?/mode? params from the no-follow opens (no caller repo-wide).
  • S3 FIXED (47d0757): plural readLastJsonStringFieldsSync symlink-refusal regression added; mutation red (plain fs.openSync revert leaks the secret).
  • S2 DEFERRED to next round (untouched, within cap): async TOCTOU re-check pinning test — valid, bot probe design ready.

Verification: rename suite 19/19, no-follow-open 10/10, sessionStorageUtils 55/55, 411 passed across 6 directly-hit files, cli workspace-registration-store 26 passed; typecheck clean core/cli/acp-bridge; eslint+prettier clean. Not run locally: gitDiff fixture tests (host git 2.19 lacks git init -b; fails identically without these changes). Push note: this host's git-over-https to github.com was blocked, so the push went through the GitHub Git Data API with every tree/commit SHA verified byte-identical before the fork ref fast-forward.

@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 of PR #10007 — fix(core): keep no-follow reads protected where O_NOFOLLOW is missing

Reviewed at: 47d0757 (HEAD), 13 files, +748/−81

Verdict: no blocking issues found.

Summary

This PR adds a cross-platform openNoFollow/openSyncNoFollow helper to replace the collapsing idiom (O_RDONLY ?? 0) | (O_NOFOLLOW ?? 0) used across six read call sites. On POSIX the helper ORs the same kernel flag — byte-for-byte unchanged. On Windows, where O_NOFOLLOW is undefined, the helper compensates with an lstat → open → fstat identity check that refuses to follow symlinks. Well-scoped, well-tested, and correct.

What I verified

  1. The fallback is correct and fail-closed. lstat refuses a symlinked final component; the post-open fstat must match dev/ino; ino: 0 volumes are refused through hasVerifiableInode. Each refusal closes the handle and carries either ELOOP (symlink/race) or EUNVERIFIABLE (inode-0) so callers can distinguish.

  2. Caller error semantics survive. workspace-registration-store keeps its ENOENT → empty path; gitDiff and backgroundShellRegistry keep their fail-safe catches; readManyFiles and sessionArtifacts keep their validated-identity re-checks.

  3. POSIX is untouched. Where O_NOFOLLOW exists the helper ORs the same flag — no behavioral change. The lazy fs.constants?.O_NOFOLLOW access also keeps strict vitest mocks loadable.

  4. Scope is disciplined. Six confirmed read sites converge; four redundant per-site flag helpers are deleted; write paths and platform-guarded sites are explicitly deferred per the issue thread.

  5. Test coverage is thorough. Two reproduction tests (session metadata leak, background-shell output leak) that fail on main and pass here. The new no-follow-open.test.ts covers the native path (open, ELOOP, ENOENT), the fallback path (symlink, identity mismatch, inode-0), and the sync/async variants.

Minor observations (none blocking)

  • The openUntrackedForDiffRead two-phase fallback (try openNoFollow → if EUNVERIFIABLE → plain open()) in gitDiff.ts has no dedicated test for the inode-0 degradation path. The behavior is correct and the existing gitDiff.test.ts covers the normal path, but the fallback is untested.
  • The readManyFiles.ts changes don't explicitly handle EUNVERIFIABLE from openNoFollow — on inode-0 volumes the error propagates through existing error handling with a technical error message. This is consistent with the ino: 0 posture from #8290/#9857.
  • The sessionStorageUtils.test.ts single-field and multi-field reproduction tests have nearly identical setup code. Minor style concern.

Conclusion

Clean, focused fix for a real Windows security gap. No regressions on POSIX. The two reproduction tests are real evidence that the fix works. Ready to merge once CI on the latest commit (47d0757) lands green.

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

Two-phase code review summary (round 1 only)

PR: #10007 — fix(core): keep no-follow reads protected where O_NOFOLLOW is missing
Head reviewed: 47d0757fb2e0ed2d9bec9b485d74e603142f2ec8
Round 1 model: deepseek-v4-flash
Round 2: skipped because round 1 reported findings

Verdict

No blocking issues, but 3 minor observations were raised:

  1. Untested inode-0 degradation path in gitDiff.ts — the EUNVERIFIABLE branch when hasVerifiableInode is false lacks direct test coverage.
  2. EUNVERIFIABLE propagation in readManyFiles.ts — confirm that callers downstream of the text-handle read correctly distinguish EUNVERIFIABLE from ELOOP so legitimate files on FAT/exFAT/SMB volumes are not misclassified as symlink attacks.
  3. Test setup duplication — the symlink-stubbing pattern is repeated across new tests; consider a shared helper.

Notes

  • The openNoFollow/openSyncNoFollow helper design looks correct: fail-closed on Windows, byte-for-byte unchanged on POSIX.
  • All six converged call sites route through the helper, and four redundant per-site flag helpers are removed.
  • Caller error semantics (ENOENT → empty, ELOOP → binary-row/<error>) are preserved.
  • The full review pipeline could not run due to a network failure (getaddrinfo() thread failed to start on git operations), so this pass was performed from the downloaded diff and relevant source files.

The async fallback's TOCTOU identity re-check in openNoFollow
(assertSameIdentity plus close-on-rejection) was pinned by no test:
deleting the whole try/catch block kept the suite green, because the
async symlink tests reject at the earlier isSymbolicLink() check and
only the sync variant's re-check was driven. A refactor dropping that
block would ship green while a path swapped for a symlink between
lstat and open gets read through on Windows and the rejection-path
handle leaks unclosed.

Add the async identity-change test using the same prototype trick as
the sync one, applied to fs.promises.lstat (the real opened
FileHandle's stat() cannot be intercepted through fs mocks): the
doctored before-stats carry ino + 1, so the real handle's stat
mismatches and the open must reject with code ELOOP after closing the
handle (asserted through a close spy). The test is red on the mutant
(block deleted: 1 failed | 10 passed) and green on restored code
(11 passed).

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout round: cleared the last unresolved finding (deferred S2).

  • 88d16cb50 pins the async TOCTOU identity re-check of openNoFollow: fs.promises.lstat is mocked with doctored stats (ino + 1, same prototype trick as the sync test) so the real opened handle's stat mismatches before; the test asserts the ELOOP rejection and exactly one handle.close().
  • Mutation check: deleting the try { assertSameIdentity(...) } catch { await handle.close() } block fails exactly the new test (1 failed | 10 passed); restored code green. Suite 11/11.
  • Pushed 47d0757fb..88d16cb50 (non-force); thread replied with SHA evidence + resolved. 0 unresolved now; review lane auto-triggered by the push.

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

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@yiliang114

Copy link
Copy Markdown
Collaborator Author

The Ubuntu failure was PR-caused, not an infra flake: check:serve-fast-path-bundle found the ACP pre-listen closure pulling in the core barrel, including shell.ts, fzf, @iarna/toml, chokidar, and glob.

Fixed in 7354a1f2b by exporting the no-follow helper through a leaf core subpath and importing that subpath from sessionArtifacts.ts, keeping the ACP path off the eager barrel. The directly hit bundle-check suite passes (35 passed). Full core typecheck/build could not be reproduced in this existing worktree because its shared dependencies are stale/incomplete (missing OTel/fdir/etc.); no install was run. Fresh CI is now running on the new head.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Test Plan (not a blocker): src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 1702, 20501, 21495, 1659, 601, 4226, 627 passed.

中文说明

Test Plan(非阻断):src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 1702, 20501, 21495, 1659, 601, 4226, 627 passed

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

Comment thread packages/acp-bridge/src/sessionArtifacts.ts
Comment thread packages/core/src/utils/no-follow-open.ts
Comment thread packages/acp-bridge/src/sessionArtifacts.ts
Comment thread packages/cli/src/serve/workspace-registration-store.ts
Comment thread packages/core/src/utils/gitDiff.ts
Comment thread packages/core/src/utils/no-follow-open.test.ts Outdated
Comment thread packages/core/package.json
Comment thread packages/acp-bridge/src/sessionArtifacts.ts
Comment thread packages/core/src/utils/no-follow-open.test.ts Outdated
Comment thread packages/core/src/utils/no-follow-open.test.ts Outdated
yiliang114 and others added 4 commits August 26, 2026 09:52
Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
wenshao and others added 4 commits August 26, 2026 12:32
Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
Both integration-tests and acp-bridge import the new
@qwen-code/qwen-code-core/noFollowOpen subpath but had no paths entry
for it, so typecheck resolved it through core's pre-built dist and
failed with TS2307 on dist-less trees. Add the source entries next to
the sibling subpaths, following the documented 'keep in sync with the
exports maps' invariant.

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
Three pins for the no-follow fallback's guarantee surface:
- spy fs.closeSync in both sync rejection tests (identity change,
  inode 0) so a dropped close in openSyncNoFollow's catch turns red,
  mirroring the async closeSpy pin
- add a dev-mismatch identity variant so deleting the dev comparison
  from assertSameIdentity turns red (ino alone is unique per device)
- add sync+async tests that perturb every lstat after the first call
  and expect the open to SUCCEED, pinning that the identity re-check
  compares against the pre-open lstat snapshot rather than a fresh
  post-open lstat

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

@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 of PR #10007 — fix(core): keep no-follow reads protected where O_NOFOLLOW is missing

Reviewed at: e2b59655 (HEAD), 19 files, +1029/−81
Previous rounds: 2 rounds (CHANGES_REQUESTED twice), 15 findings posted (3 Criticals, 12 Suggestions); this re-review at commit e2b59655

Previous findings — status

3 Criticals — all resolved:

  • R1-1 (sessionStorageUtils.ts rerouting breaks sessionService.rename.test.ts): fixed in bdecd4c8 — lstatSync/fstatSync spies added to the rename suite; default-import binding ensures vitest spies intercept the helper. ✓
  • R1-2 (inode-0 carries ELOOP, misclassifying legitimate files): fixed in 84df828cUNVERIFIABLE_IDENTITY_CODE (EUNVERIFIABLE) replaces ELOOP for inode-0 refusals; all three consumers (sessionArtifacts, workspace-registration-store, gitDiff) distinguish the two codes. ✓
  • R2-1 (cli vitest config missing subpath alias, breaking 63 tests): fixed in d6e4a63a — alias entries in both cli/vitest.config.ts and acp-bridge/vitest.config.ts. ✓

12 Suggestions — 10 resolved, 2 standing (see below):

  • R1-3 (flags/mode params): doc comment clarifies read-only-only. ✓
  • R1-4 (async TOCTOU re-check): tested in no-follow-open.test.ts. ✓
  • R1-5 (multi-field symlink refusal): tested in sessionStorageUtils.test.ts. ✓
  • R2-2 (default-import mockability): test factories set default: modified. ✓
  • R2-3 (sessionArtifacts EUNVERIFIABLE branch untested): still standing — no test exercises the isUnverifiableIdentityError branch in sessionArtifacts.ts.
  • R2-4 (cli workspace-registration-store EUNVERIFIABLE branch untested): now tested ("reports an unverifiable store identity as a store error"). ✓
  • R2-5 (gitDiff inode-0 degradation path untested): still standing — the openUntrackedForDiffRead fallback to plain open() on EUNVERIFIABLE has no test in gitDiff.test.ts.
  • R2-6 (sync rejection-path fd close unpinned): now tested (closeSpy). ✓
  • R2-7/R2-8 (subpath export config): package.json export entry + tsconfig/vitest aliases all present. ✓
  • R2-9 (dev identity check untested): now tested. ✓
  • R2-10 (pre-open snapshot comparison untested): now tested. ✓

My review — no new issues found

I reviewed the full diff (1421 lines across 19 files) and the prior review threads. The implementation is correct and well-tested. Key observations:

1. Helper design is correct. The openNoFollow/openSyncNoFollow functions are cleanly split between the POSIX fast path (kernel O_NOFOLLOW — byte-for-byte unchanged) and the fallback path (lstat → open → fstat identity check). The fallback correctly:

  • Refuses symlinked final components via pre-open lstat
  • Refuses dev/ino identity mismatches (TOCTOU swap race)
  • Refuses inode-0 volumes via hasVerifiableInode with a distinct error code (EUNVERIFIABLE)

2. Error code separation is consistent. EUNVERIFIABLE is used ONLY for the inode-0 case; all genuine symlink/race refusals carry ELOOP. All three consumers that distinguish them (sessionArtifacts.ts, workspace-registration-store.ts, gitDiff.ts) use the correct code checks. The remaining three consumers (sessionStorageUtils.ts, backgroundShellRegistry.ts, readManyFiles.ts) do not need to distinguish — their catch-all handling is correct for either code.

3. Cross-package integration is complete. The four subpath export entries (package.json, both vitest.config.ts files, acp-bridge tsconfig.json, integration-tests tsconfig.json) are all present. The serve-fast-path-bundle-check.test.js confirms the leaf import does not pull the core barrel.

4. Test coverage is thorough. The no-follow-open.test.ts (468 lines) covers both the native and fallback paths with all error variants. The reproduction tests in sessionStorageUtils.test.ts and backgroundShellRegistry.test.ts prove the fix works by failing on main and passing here. The sessionService.rename.test.ts compatibility test confirms the helper does not break existing mock patterns.

5. Two minor Suggestions remain standing (from previous rounds, not new):

  • R2-3 (Suggestion): sessionArtifacts.ts isUnverifiableIdentityError branch is untested. The sessionArtifacts.test.ts file is not in this PR's diff. Adding a test there would require setting up O_NOFOLLOW=undefined + ino: 0 in the acp-bridge test suite.
  • R2-5 (Suggestion): gitDiff.ts openUntrackedForDiffRead inode-0 degradation path is untested in gitDiff.test.ts. The fallback behavior (plain open() on EUNVERIFIABLE) is correct but has no dedicated test.

Verdict

No blocking issues. The 3 Criticals from previous rounds are all verified fixed at this HEAD. The 2 remaining Suggestions are minor and non-blocking. The implementation is correct, well-tested, and consistent with the established #8290/#9857 posture. Ready to merge (pending CI green).

— Independent review (no worktree: git fetch blocked on Windows; reviewed from full diff, 15 prior finding threads, and PR context)

…gitDiff

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

@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 reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Test Plan (not a blocker): src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 1704, 24732, 21627, 1659, 601, 4235, 630 passed.

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

  • packages/core/src/utils/gitDiff.ts:92 — [review] openUntrackedForDiffRead doc comment overclaims the symlink guarantee (code unchanged since the previous round)
  • packages/core/src/tools/readManyFiles.ts:303 — [probe] readManyFiles open-side guard has no paired test (code unchanged since the previous round)
  • packages/core/src/utils/no-follow-open.test.ts:285 — [probe] async identity-change test does not pin the fd-based re-check (code unchanged since the previous round)

Convergence: round 3 posted 3 inline comment(s), 3 of them reported for the first time; the previous round posted 10 (10 new). Findings keep coming back to the same files: packages/core/src/utils/no-follow-open.test.ts (findings in round 2; 2 more now); packages/cli/src/serve/workspace-registration-store.ts (findings in round 2; 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. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

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

Test Plan(非阻断):src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 1704, 24732, 21627, 1659, 601, 4235, 630 passed

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

收敛情况:第 3 轮发布了 3 条行内评论,其中 3 条是首次提出;上一轮发布了 10 条(其中 10 条首次提出)。发现反复回到同一批文件:packages/core/src/utils/no-follow-open.test.ts(第 2 轮已出过发现,本轮又有 2 条);packages/cli/src/serve/workspace-registration-store.ts(第 2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread packages/core/src/utils/no-follow-open.test.ts Outdated
Comment thread packages/core/src/utils/no-follow-open.test.ts Outdated
Comment thread packages/cli/src/serve/workspace-registration-store.ts
yiliang114 and others added 2 commits August 26, 2026 22:17
The ino-mismatch and dev-mismatch identity-change tests never plant a
symlink: they write a plain file and perturb fstatSync via vi.doMock.
The itNoSymlink guard therefore only skipped them on win32, the one
platform where the lstat/open/fstat fallback is the production path.
Switch both to plain it, matching the async twin.

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
packages/cli imports @qwen-code/qwen-code-core/noFollowOpen in
workspace-registration-store.ts but had no paths entry for the leaf,
so cli typecheck resolved it through core's compiled dist and breaks
in deep-cleaned worktrees. Mirror the entry already added for
acp-bridge and integration-tests.

Co-authored-by: Qwen-Coder <qwen-coder @alibabacloud.com>
@yiliang114

Copy link
Copy Markdown
Collaborator Author

CI note — Test (ubuntu-latest, Node 22.x) failure on run 32945995633 is an infra flake, not PR-caused:

  • Log shows Test Files 65 passed (65) / Tests 1743 passed | 11 skipped — zero assertion failures.
  • The only error is Error: [vitest-worker]: Timeout calling "onTaskUpdate" (vitest worker RPC hang in Unhandled Errors).
  • CI's own deflake machinery classified it: deflake issue creation failed for #42; the rerun stands and it retries on the next flaky occurrence: 502 from GitHub.
    Conclusion: known flake TypeError: Cannot read properties of undefined (reading 'value') #42; rerun needed.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

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

Partially reviewed — gaps disclosed. Suggestions are inline.

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

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI; the fallback tests un-skipped this round ran only on Linux.

Test Plan (not a blocker): src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 24739, 21627, 1704, 1659, 601, 4235, 630 passed.

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

  • packages/core/src/utils/no-follow-open.test.ts:67 — [probe] helper's read-only open flags unpinned by any test
  • packages/core/src/utils/no-follow-open.test.ts:445 — [probe] isUnverifiableIdentityError tested only on its true branch

Convergence: round 4 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: packages/core/src/utils/no-follow-open.test.ts (findings in round 3; 2 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. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

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

未审查:build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI; the fallback tests un-skipped this round ran only on Linux。

Test Plan(非阻断):src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 24739, 21627, 1704, 1659, 601, 4235, 630 passed

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

收敛情况:第 4 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/core/src/utils/no-follow-open.test.ts(第 3 轮已出过发现,本轮又有 2 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread packages/core/src/utils/no-follow-open.test.ts Outdated
Comment thread packages/core/src/utils/no-follow-open.test.ts Outdated
Extract the repeated O_NOFOLLOW-less node:fs mock skeleton into a shared
mockNoFollowFs factory (the load-bearing `default` member stays) plus a
perturbedStats helper, and drop the six per-test doUnmock/resetModules
blocks already covered by the describe-level afterEach. Add sync, async,
and inode-0 variants asserting a failing rejection-path close never masks
the pinned ELOOP / EUNVERIFIABLE refusal codes.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR has merge conflicts, so refs/pull/10007/merge is unavailable — resolve conflicts and re-run.

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR has merge conflicts, so refs/pull/10007/merge is unavailable — resolve conflicts and re-run。

Qwen Code · sandboxed verification

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR has merge conflicts, so refs/pull/10007/merge is unavailable — resolve conflicts and re-run.

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR has merge conflicts, so refs/pull/10007/merge is unavailable — resolve conflicts and re-run。

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 5aec7c2e1e65f458fb99d4fcb5b532cd8b991fc5 — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 5aec7c2e1e65f458fb99d4fcb5b532cd8b991fc5既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline.

Not explored to full depth (tool budget reached): "agent 6c": could not execute src/utils/no-follow-open.test.ts — neither the review worktree nor the parent checkout has node_modules (vitest fails at config load with …; "agent 3b": executing packages/core/src/utils/no-follow-open.test.ts — the review worktree has no node_modules , and npm ci + npm run build to enable it exceeds the …; "agent 6a": executed npx vitest run src/utils/no-follow-open.test.ts at HEAD — worktree has no node_modules and a full monorepo install in the shared review tree was not ….

Test Plan (not a blocker): src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 21630, 1704, 24740, 1659, 601, 4235, 630 passed.

Convergence: round 5 posted 2 inline comment(s), 2 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/utils/no-follow-open.test.ts (findings in round 4; 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, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

未探索到全部深度(达到工具调用预算):"agent 6c"could not execute src/utils/no-follow-open.test.ts — neither the review worktree nor the parent checkout has node_modules (vitest fails at config load with …"agent 3b"executing packages/core/src/utils/no-follow-open.test.ts — the review worktree has no node_modules , and npm ci + npm run build to enable it exceeds the …"agent 6a"executed npx vitest run src/utils/no-follow-open.test.ts at HEAD — worktree has no node_modules and a full monorepo install in the shared review tree was not …

Test Plan(非阻断):src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory; 26 passed — this review observed 21630, 1704, 24740, 1659, 601, 4235, 630 passed

收敛情况:第 5 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/core/src/utils/no-follow-open.test.ts(第 4 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment thread packages/core/src/utils/no-follow-open.test.ts
Comment thread packages/core/src/utils/no-follow-open.test.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR has merge conflicts, so refs/pull/10007/merge is unavailable — resolve conflicts and re-run.

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR has merge conflicts, so refs/pull/10007/merge is unavailable — resolve conflicts and re-run。

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot's only review on 5aec7c2e1e65f458fb99d4fcb5b532cd8b991fc5 is a COMMENTED one, which carries no vote — so it has no verdict of its own on this commit, and main needs two approving reviews: an approval left by another account is a separate vote and does not count as the bot's own. Two different things look like this, and the stage-3 comment above says which: the triage skill deferring on purpose at 3/5 — a fork refactor hitting the approval guardrail, or a core change escalated for maintainer awareness, both normal outcomes — or an earlier approval that a push dismissed, leaving only the comment behind, which needs a fresh review.

⚠️ 机器人在 5aec7c2e1e65f458fb99d4fcb5b532cd8b991fc5 上唯一的评审是 COMMENTED不带票 —— 因此它在该 commit 上没有自己的裁决,而 main 需要两个批准(其他账号的批准是另一张票)。有两种情况长这样,上方的 stage-3 评论会说明是哪一种:triage skill 在 3/5 时有意 defer(fork refactor 命中审批护栏,或核心改动被升级交由维护者把关,两者都是正常结果);或者更早的批准被一次推送作废、只剩下这条评论,此时需要重新评审。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Merge conflict with main resolved by merging main into the branch (merge commit e2c80ed, plain non-force push — branch history preserved).

Conflict: packages/acp-bridge/src/sessionArtifacts.ts only. Main switched the workspace identity check to bigint stats (NTFS 64-bit file-id precision); this PR replaced the raw O_NOFOLLOW open with openNoFollow(). Resolution keeps both: bigint lstat for the pre-open identity, and the openNoFollow() helper for the open, so no-follow protection stays intact on platforms without O_NOFOLLOW (#8227).

Verification at the merge commit:

  • core: no-follow-open.test.ts, sessionStorageUtils.test.ts, gitDiff.test.ts (127/127), backgroundShellRegistry.test.ts, sessionService.rename.test.ts — all pass
  • acp-bridge: sessionArtifacts.test.ts — 131/131 pass
  • cli: workspace-registration-store.test.ts — 27 pass / 1 skipped
  • typecheck clean for core, acp-bridge, cli

Mergeable: now MERGEABLE (was CONFLICTING).

Note: reviewDecision shows CHANGES_REQUESTED, but the only CR reviews on this PR are two stale ones from qwen-code-ci-bot; the latest human review (@doudouOUC, 2026-08-26) found no blocking issues.

@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-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI; the fallback path is exercised on Linux only via stubbed constants (the PR's documented simulation).

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

Test Plan (not a blocker): src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory.

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

  • packages/core/src/utils/gitDiff.ts:14 — [review] Deferral inventory mislabels gitUtils.ts readFirstLineNoFollow — a read site with the collapsing idiom — as a write path
  • packages/core/src/tools/readManyFiles.ts:303 — [probe] readManyFiles open reroute has no test pinning the openNoFollow open (plain-open regression ships green)
  • integration-tests/tsconfig.json:48 — [test] noFollowOpen paths mapping imported by no integration test — dead config
  • packages/core/src/utils/no-follow-open.test.ts:352 — [probe] async inode-0 fail-closed refusal unpinned (async openNoFollow has no EUNVERIFIABLE witness)
  • packages/core/src/index.ts:154 — [review] barrel re-exports of the four no-follow symbols have zero read sites — dead API surface
  • packages/core/src/utils/no-follow-open.test.ts:188 — [probe] fallback's lstat-before-open ordering unpinned (open-first mutant passes all 12 tests)
  • packages/core/src/utils/no-follow-open.test.ts:73 — [review] R5-1 still stands — O_NOFOLLOW-stripped fs mock copied five times across two packages (deferred; main-repo follow-up acknowledged)
  • packages/core/src/utils/no-follow-open.test.ts:475 — [review] R4-2 still stands — best-effort-close tests re-paste the deduplicated perturbation blocks (deferred; main-repo follow-up acknowledged)
中文说明

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

未审查:build-and-test — Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) were skipped in CI; the fallback path is exercised on Linux only via stubbed constants (the PR's documented simulation)。

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

Test Plan(非阻断):src/utils/sessionStorageUtils.test.tsno such file or directory; src/services/backgroundShellRegistry.test.tsno such file or directory; src/utils/no-follow-open.test.tsno such file or directory

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

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants