Skip to content

fix(serve): allow dotdot-prefixed cwd subdirectories - #9848

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
AaronZ345:fix-contained-cwd-dotdot
Aug 29, 2026
Merged

fix(serve): allow dotdot-prefixed cwd subdirectories#9848
wenshao merged 1 commit into
QwenLM:mainfrom
AaronZ345:fix-contained-cwd-dotdot

Conversation

@AaronZ345

@AaronZ345 AaronZ345 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Updates serve workspace cwd containment checks so an in-workspace directory whose basename starts with .. (for example <workspace>/..build) is treated as contained, while real parent traversal such as <workspace>/../outside remains rejected.

Why it's needed

The serve git routes accept a cwd under the workspace. The previous inline check used rel.startsWith('..'), so path.relative(workspace, path.join(workspace, '..build')) produced ..build and was incorrectly classified as escaping the workspace. Read routes could silently fall back to the workspace root and mutation routes could reject the cwd with invalid_cwd, even though the path is inside the workspace. This aligns the check with the repository's segment-aware containment rule: reject only .., ../..., and absolute relatives.

Reviewer Test Plan

How to verify

Run the targeted serve runtime tests and confirm that dotdot-prefixed in-workspace directories are accepted while real parent traversal is still rejected.

Evidence (Before & After)

Before: the added regression test for ..build fails because the old startsWith('..') check treats it as parent traversal. After: the targeted test suite passes, and existing parent traversal rejection remains covered.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally; covered by CI
🐧 Linux ⚠️ not tested locally; covered by CI

Environment (optional)

Local Node/npm workspace on macOS.

Risk & Scope

  • Main risk or tradeoff: Low; this only narrows the rejection condition to segment-aware parent traversal and keeps absolute-path rejection intact.
  • Not validated / out of scope: Manual end-to-end serve route testing in a browser client.
  • Breaking changes / migration notes: None.

Linked Issues

Refs #8835 (contained-cwd-dotdot-prefix).

中文说明

What this PR does

更新 serve workspace 的 cwd 包含性判断:如果工作区内目录名以 .. 开头(例如 <workspace>/..build),仍然视为工作区内路径;真正的父目录逃逸(例如 <workspace>/../outside)仍会被拒绝。

Why it's needed

serve 的 git 路由允许传入工作区内的 cwd。旧逻辑使用 rel.startsWith('..'),所以 path.relative(workspace, path.join(workspace, '..build')) 得到 ..build 后会被误判为逃逸工作区。读取类路由可能静默回退到工作区根目录,变更类路由可能返回 invalid_cwd,但该路径实际仍在工作区内。本 PR 将判断改为仓库已有的分段感知规则:只拒绝 ..../... 和绝对相对路径。

Reviewer Test Plan

How to verify

运行目标 serve runtime 测试,确认工作区内 ..build 这类目录可被接受,同时真正的父目录逃逸仍被拒绝。

Evidence (Before & After)

Before:新增的 ..build 回归测试会失败,因为旧的 startsWith('..') 检查会把它当作父目录穿越。After:目标测试套件通过,已有父目录逃逸拒绝逻辑仍被覆盖。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested locally; covered by CI
🐧 Linux ⚠️ not tested locally; covered by CI

Environment (optional)

macOS 本地 Node/npm 工作区。

Risk & Scope

  • Main risk or tradeoff: 低;只把拒绝条件收窄为分段感知的父目录穿越判断,并保留绝对路径拒绝。
  • Not validated / out of scope: 未手工跑浏览器客户端里的 serve route 端到端流程。
  • Breaking changes / migration notes: 无。

Linked Issues

Refs #8835 (contained-cwd-dotdot-prefix).

Use the shared path containment helper so workspace-local directories such as ..build are not treated as escapes while real parent traversal remains rejected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head d34c443, 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

@AaronZ345
AaronZ345 marked this pull request as ready for review August 24, 2026 05:48
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 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 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.

Thanks for the fix, @AaronZ345. The direction looks reasonable — the current inline check treats any relative path starting with .. as parent traversal, which wrongly rejects contained directories like ..build, and the shared containment helper already handles that distinction correctly. Before we can take this further, though, the PR description needs to follow the repository's pull request template.

Right now the body only has ## Summary and ## Test plan; the sections the template requires are missing:

  • What this PR does — partly covered by your Summary, needs to move into the template section.
  • Why it's needed — missing, and this is the important one. This is a fix: PR, but there is no linked issue or before/after reproduction showing that a contained ..-prefixed directory is actually rejected in real usage of the serve routes. Please describe the scenario that hits this, or link an issue.
  • Reviewer Test Plan (How to verify / Evidence (Before & After) / Tested on) — missing; your three checked commands are a good start for "How to verify".
  • Risk & Scope — missing.
  • Linked Issues — missing.

Please edit the PR description to fill in the template — above all Why it's needed with a reproduction — then re-run triage with @qwen-code /triage.

中文说明

感谢提交,@AaronZ345。修复方向是合理的——目前的内联检查会把所有以 .. 开头的相对路径都当作上级目录穿越,从而错误地拒绝像 ..build 这样位于工作区内的目录,而仓库里共享的路径包含判断辅助函数已经能正确区分这两种情况。不过在继续审查之前,PR 描述需要先符合仓库的 PR 模板

目前 PR 正文只有 ## Summary## Test plan,模板要求的部分均缺失:

  • What this PR does —— Summary 已部分覆盖,需要迁移到模板对应章节。
  • Why it's needed —— 缺失,也是最重要的一项。这是一个 fix: PR,但没有关联 issue,也没有 before/after 复现来证明 serve 路由的真实使用中确实会出现「工作区内以 .. 开头的目录被拒绝」的情况。请补充触发此问题的场景,或关联相应 issue。
  • Reviewer Test PlanHow to verify / Evidence (Before & After) / Tested on)—— 缺失;你已勾选的三条命令很适合作为 "How to verify" 的内容。
  • Risk & Scope —— 缺失。
  • Linked Issues —— 缺失。

请编辑 PR 描述补全模板——尤其是 Why it's needed 和复现——然后使用 @qwen-code /triage 重新触发审查。

Qwen Code · qwen3.8-max

@AaronZ345

Copy link
Copy Markdown
Contributor 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.

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

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

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

@AaronZ345

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

@AaronZ345

Copy link
Copy Markdown
Contributor Author

@qwen-code /review --severity-floor critical

@AaronZ345

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

1 similar comment
@AaronZ345

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

@AaronZ345

Copy link
Copy Markdown
Contributor Author

All review threads are resolved and CI is green. The CHANGES_REQUESTED decision is from an earlier bot review that predates the current head. @qwen-code /review

@AaronZ345

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

1 similar comment
@AaronZ345

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

@AaronZ345

Copy link
Copy Markdown
Contributor Author

@qwen-code /review\n\nThe latest review reports no findings. Please refresh the review decision for this PR.

@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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

Skipped because the pre-execution risk screen refused this sponsored run: the model risk screen failed or returned an unparseable answer. A maintainer who has reviewed the diff can run the verification manually in a disposable environment.

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

跳过原因:the pre-execution risk screen refused this sponsored run: the model risk screen failed or returned an unparseable answer. A maintainer who has reviewed the diff can run the verification manually in a disposable environment。

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR, and for filling in the template — the earlier block was only the missing template sections, and that's resolved now. ✓

Problem: Real but rare. The old rel.startsWith('..') check deterministically misclassifies any in-workspace directory whose name starts with .. (e.g. <workspace>/..build) as a parent-traversal escape. It was identified by the repo-hygiene scan (the bug class referenced from #8835) rather than a field report, but it's a deterministic classification error demonstrated by the added regression tests — not a theoretical concern. Impact fails closed: read routes silently fall back to the workspace root, mutation routes return invalid_cwd for a legitimate path. No security exposure either way.

Direction: Aligned. This brings the serve cwd check in line with the repo's existing segment-aware containment helper instead of carrying a divergent inline check. The sibling instances of the same bug class named in #8835 (acpAgent.ts, session.ts, run-qwen-serve.ts, fs/policy.ts) remain report-only there awaiting maintainer judgment — correctly out of this PR's scope. Claude Code's CHANGELOG has no direct counterpart (the serve daemon is qwen-code-specific), but it carries several recent path-containment correctness fixes, so the area is squarely in scope.

Size: Core-module gate not applicable — packages/cli/src/serve/** is not a core path. 7 production lines (3 added / 4 removed) + 16 test lines.

Approach: Minimal and the right shape — both resolveContainedCwd and resolveContainedCwdOrFail swap the inline check for the shared isWithinRoot helper from config/path-comparison.ts, already imported by neighboring serve modules. Two regression tests pin the new acceptance behavior; existing rejection tests stay untouched.

Risk: No elevated risk signals — none of the revert-correlated high-risk paths are touched.

Moving on to code review. 🔍

中文说明

感谢提交,也感谢补全模板——此前的阻塞仅因描述缺少模板章节,现已解决。✓

问题: 真实但罕见。旧的 rel.startsWith('..') 检查会把工作区内任何以 .. 开头的目录名(例如 <workspace>/..build)确定性地误判为父目录逃逸。该问题由 repo-hygiene 扫描发现(#8835 中提到的同类缺陷),并非来自线上报告,但它是确定性的分类错误,新增的回归测试即可证明——不是理论性担忧。影响是失败即关闭(fail closed):读取类路由静默回退到工作区根目录,变更类路由对合法路径返回 invalid_cwd。两种方向都没有安全风险。

方向: 对齐。此改动让 serve 的 cwd 检查与仓库已有的分段感知包含判断辅助函数保持一致,而不是各自维护一套内联检查。#8835 中点名的同类缺陷的其余位置(acpAgent.ts、session.ts、run-qwen-serve.ts、fs/policy.ts)在该 issue 中仍为 report-only、等待 maintainer 判断——不在本 PR 范围内是正确的。Claude Code 的 CHANGELOG 没有直接对应项(serve 守护进程是 qwen-code 特有的),但其中有多条近期的路径包含正确性修复,说明该领域在核心方向之内。

规模: 核心模块门槛不适用——packages/cli/src/serve/** 不属于核心路径。生产代码 7 行(3 增 / 4 删)+ 测试 16 行。

方案: 最小且形态正确——resolveContainedCwdresolveContainedCwdOrFail 都把内联检查替换为 config/path-comparison.ts 中共享的 isWithinRoot,邻近的 serve 模块已在使用它。两个回归测试钉住新的接受行为;已有的拒绝测试保持不变。

风险: 无升级风险信号——未触及任何与 revert 相关的高风险路径。

进入代码审查。🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal first, before reading the diff: for this defect I'd swap the inline check for the repo's existing segment-aware isWithinRoot helper rather than patching the condition in place, and add a regression test for a ..-prefixed contained directory. The diff does exactly that, so nothing simpler was missed.

Semantics check: both call sites pass already-realpath'd paths, where the helper rejects exactly .., ..+separator prefixes, and absolute relatives — so it differs from the old check only for in-root names starting with .., which is precisely the intended relaxation. Escape, cross-drive, and symlink-outside cases still fail closed and remain pinned by the pre-existing tests; the new tests pin the ..build acceptance in both the lenient and the strict variant. Argument order (child, parent) is correct, and the import matches the helper neighboring serve modules already use. The repo carries a second isWithinRoot in core fileUtils (equivalent for realpath'd inputs); consolidating those duplicates is a pre-existing wart, not this PR's job.

No blocking findings.

Test evidence — the PR's own CI on the reviewed commit

Nothing was built or run locally (review is static; fork code is never executed here). The table below is the PR's own CI on d34c44339f11b685aae011a00cbf43d4195f59fd:

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
precheck-pr / precheck ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Real daemon E2E / Java 11 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Dependency CVE audit ✅ success
Secret scan (TruffleHog) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped

Reading the signal: no red checks anywhere. The ubuntu unit suite ran the two new regression tests green, and they genuinely pin the change — statically, the old startsWith('..') check rejects '..build', so both tests fail without the fix. The Serve A/B job hit 12 endpoint scenarios against base vs head with zero response differences — expected, since none of its scenarios use a ..-prefixed cwd; it proves no regression rather than exercising the new path. The macOS/Windows unit jobs and the CLI integration job were skipped — that's the pre-existing fork gating for this PR, not something the diff caused. Not verified: end-to-end route behavior with a real ?cwd=<workspace>/..build (no job here drives it).

Sandboxed verification would settle that last gap: @qwen-code /verify — that the serve routes actually accept a ..-prefixed contained cwd end-to-end is pinned only at the resolver level above. The author has read access only, so this would be a sponsored run: a maintainer's @qwen-code /verify approves the head it's written against, and the run carries a pre-execution risk screen plus a full workspace wipe — read the resulting report with the same skepticism as the fork's own CI logs.

中文说明

代码审查

先说独立方案:看到这个缺陷,我会把内联检查替换为仓库已有的分段感知 isWithinRoot 辅助函数,并为以 .. 开头的工作区内目录补一个回归测试,而不是就地修补条件。diff 正是这么做的,没有更简单的路径被遗漏。

语义核对:两个调用点传入的都是已经过 realpath 的路径,此时该辅助函数恰好拒绝 ....+分隔符前缀和绝对相对路径——因此与旧检查相比,唯一的行为差异就是接受工作区内以 .. 开头的目录名,正是本次修复的意图。逃逸、跨盘符、指向外部的符号链接等情况仍然失败即关闭,且仍由原有测试钉住;新增测试在宽松版和严格版两个函数上都钉住了 ..build 的接受行为。参数顺序(子路径、根路径)正确,导入的正是邻近 serve 模块已在使用的辅助函数。仓库里 core fileUtils 中还有第二个 isWithinRoot(对 realpath 输入等价);合并这些重复实现是历史遗留问题,不属于本 PR 的职责。

无阻塞性发现。

测试证据 —— PR 自身的 CI(被审查提交)

本地未构建、未运行任何代码(审查为静态;fork 代码不在此执行)。上表为 d34c44339f11b685aae011a00cbf43d4195f59fd 上 PR 自身 CI 的结果:无任何红色检查。ubuntu 单元测试套件已运行两个新回归测试并通过,且它们确实钉住了改动——静态分析即可确认旧的 startsWith('..') 检查会拒绝 '..build',因此没有此修复两个测试都会失败。Serve A/B 任务以 12 个端点场景对比 base 与 head,响应零差异——符合预期,因为其场景均未使用以 .. 开头的 cwd;它证明无回归,但未走到新路径。macOS/Windows 单元测试与 CLI 集成任务被跳过——这是 fork PR 的既有限流机制,与本 diff 无关。未验证:使用真实 ?cwd=<workspace>/..build 的端到端路由行为(此处没有任务驱动该场景)。

沙盒验证可以补上最后这个缺口:@qwen-code /verify —— serve 路由端到端是否真的接受以 .. 开头的工作区内 cwd,目前只在 resolver 层面被钉住。作者仅有只读权限,因此这将是赞助运行:由 maintainer 发起 @qwen-code /verify,运行前有风险筛查与完整工作区清理——请对报告的怀疑程度与对待 fork 自身 CI 日志一致。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal correctness fix pinned by green regression tests; the only loose ends are pre-existing (fork CI gating skips the macOS/Windows legs) and tracked elsewhere (#8835).

Reflection: this one earns its merge. The problem is real if unfashionable — nobody names a directory ..build on purpose, but when one exists, the serve routes misclassify it deterministically, and the fix is exactly what I would have written: stop carrying a private containment check and reuse the segment-aware helper the rest of the serve module already trusts. Seven production lines, two tests, zero drive-by edits. The relaxation is provably narrow — with realpath'd inputs on both sides, the only paths newly accepted are ones physically inside the workspace, and every escape shape still fails closed with its test intact. The Serve A/B run adds a quiet no-regression signal on top. My one reservation is not about this diff: the same bug class still sits in four sibling files per #8835, and that broader relaxation is deliberately waiting on maintainer judgment — this PR doesn't prejudice it. CI is fully settled (no pending runs) and green on the reviewed commit.

Approving, pinned to the reviewed commit. ✅

中文说明

置信度:4/5 —— 干净、最小的正确性修复,回归测试全绿且确实钉住改动;遗留的小尾巴都是既有问题(fork 的 CI 限流跳过了 macOS/Windows 腿),且已在别处跟踪(#8835)。

复盘:这个 PR 值得合入。问题真实存在,只是不常见——没人会故意把目录命名为 ..build,但一旦存在,serve 路由会确定性地误判它。修复正是我会写的样子:不再各自维护私有的包含性检查,改用语义感知、serve 模块其余部分已经信任的共享辅助函数。生产代码 7 行、测试 2 个、零顺手改动。放宽的范围可证明是窄的——两侧均为 realpath 后的输入时,新接受的唯一可能是物理上位于工作区内的路径,所有逃逸形态仍然失败即关闭,且原有测试完好。Serve A/B 运行额外提供了无回归信号。唯一的保留意见与这个 diff 无关:按 #8835,同类缺陷仍存在于另外四个文件,更大范围的放宽刻意留待 maintainer 判断——本 PR 不影响该决定。CI 已完全结束(无进行中任务),在被审查的提交上全绿。

批准合入,审批固定于被审查的提交。✅

Qwen Code · qwen3.8-max

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM, looks ready to ship. ✅

@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real local environment, base vs head

I built this PR into a full bundle and drove it against two live qwen serve daemons, because the triage review explicitly left one thing open:

Not verified: end-to-end route behavior with a real ?cwd=<workspace>/..build (no job here drives it).

That gap is now closed. The PR does exactly what its description claims, changes nothing else, and I found no blocking issue.

Setup — how the A/B was made airtight
Base origin/main 8104071260
Head that main + PR head d34c4433 (clean merge, 6c06f3be; diff vs main is exactly the PR's 2 files)
Build npm run build && npm run bundle twice — once per side — producing dist-base/ and dist-head/
Bundle delta diff of the two bundles is only the PR hunk: isWithinRoot(resolved, root) vs const rel = path.relative(root, resolved); if (!rel.startsWith('..') && ...), twice. Nothing else differs.
Fixture <ws> = git repo on main-ws; <ws>/..build = nested git worktree on dotdot-branch with its own commit (DOTDOT-ONLY) and its own dirty file, so a wrong-repo answer is visible in the payload, not just in workspaceCwd
Daemons qwen serve --port 4871 --workspace <ws> (head) and --port 4872 (base), same fixture, bearer token set (the strict mutation routes require one)
Env macOS 26.6.2 / Darwin 25.6.0, Node v24.18.1, npm 11.16.0

1. Live route behaviour

live serve A/B

Request with ?cwd=<ws>/..build BASE HEAD
GET /workspaces/:ws/git <ws> · main-ws <ws>/..build · dotdot-branch
GET …/git/branches <ws> · head main-ws <ws>/..build · head dotdot-branch
GET …/git/log <ws> · ROOT-ONLY: workspace root commit <ws>/..build · DOTDOT-ONLY: build worktree commit
GET …/git/diff <ws> · only-in-root.txt <ws>/..build · only-in-dotdot.txt
POST …/git/branch 400 invalid_cwd 200, branch really created in the worktree

So on main the silent fallback is not just a wrong workspaceCwd field — four read routes return another repository's branch, history and diff for a cwd the caller explicitly scoped, and the mutation route refuses a legitimate path. Both symptoms in the PR description reproduce, and both are fixed. Negative controls (<ws>/../outside, <ws>/..) are byte-identical on both sides, and the outside repo is untouched.

2. Nothing new gets in

16 cwd shapes against the live daemon, both builds:

containment sweep

The only rows that flip base→head are ..build, ...three and their ./ / trailing-slash / re-entrant spellings — all physically inside the workspace. Everything else still fails closed, including the two I most wanted to see: <ws>/..build/../../outside (traversal through the dotdot directory) and <ws>/..link-outside (a symlink out whose own name starts with ..) — realpathSync runs before the predicate, so the relaxation opens no symlink hole. <base>/ws-evil (prefix sibling) also stays rejected.

3. Do the new tests actually pin it?

tests and mutants

  • Reverting only the production hunk and keeping the tests: 2 failed | 26 passed — precisely the two new cases, no collateral.
  • 5-mutant matrix on the merged tree: isWithinRoot(...)→true killed, argument swap killed, regressing the helper to the old '..' prefix killed. relativePath !== '..' survives this file but is killed one level out by workspace-inputs.test.ts > drops nested non-primary roots. !path.isAbsolute(...) survives everywhere on POSIX — path.relative never returns an absolute path there, so it is an equivalent mutant on this platform, not a coverage hole.

4. Windows, since CI skipped it here

Test (windows-latest, Node 22.x) and Test (macos-latest, Node 22.x) are skipped on this PR, so "covered by CI" doesn't hold for the Node suites. I ran both predicates under path.win32 instead:

win32 predicate

12/12 as intended: C:\work\ws\..build newly accepted; exact parent, sibling, prefix sibling, drive root, cross-drive D:\evil and UNC \\server\share\evil all still rejected. The !path.isAbsolute arm is what rejects the cross-drive and UNC cases, which is why keeping it matters even though POSIX can't exercise it. (Predicate-level simulation — not a Windows host run.)

5. Repo health on the merged tree

npm run typecheck → 0 · eslint on both changed files → 0 · the 8 serve/git suites (workspace-git, -branches, -diff, -log, -github-prs, workspace-route-runtime, workspace-inputs, fs/policy) → 137 passed.

6. Two #8835 sibling claims, re-tested (out of scope — informational)

adjacent findings

  • run-qwen-serve.ts nested---workspace false negative — does not reproduce. qwen serve --workspace <ws> --workspace <ws>/..build is refused, same as .../sub. The front-line guard is workspace-inputs.ts, which already uses the shared isWithinRoot; the startsWith('..') block further down run-qwen-serve.ts is a second, now-unreachable layer. Worth correcting in [repo-hygiene] 2026-W33 report-only findings (8 items) #8835 before someone "fixes" dead code.
  • fs/policy.ts shouldIgnore — reproduces, on both base and head. With ..build/, ...three/, sub/ all in .gitignore, GET …/list filters sub out but returns ..build and ...three with ignored: false: the rel.startsWith('..') early return skips the matcher entirely. Pre-existing and untouched by this PR — but note it means the file routes already accepted ..-prefixed paths while the git routes rejected them, so this PR also removes an inconsistency inside serve.

Verdict

No blocking findings. The relaxation is provably narrow, the escape surface is unchanged in a live daemon, and the behaviour the PR describes is reproduced end-to-end on the real HTTP routes rather than only at the resolver. Good to merge from my side; the only thing standing in the way is the review decision, and the earlier CHANGES_REQUESTED was about the (now-fixed) description template.

中文说明

维护者验证 —— 本地真实环境,base 对 head 双跑

我把这个 PR 打成完整 bundle,挂到两个真实运行的 qwen serve 守护进程上对跑,因为 triage 评审明确留了一个口子:

Not verified: end-to-end route behavior with a real ?cwd=<workspace>/..build(没有任何 job 会驱动它)。

这个口子现在补上了。PR 的行为与描述完全一致,没有任何额外影响,未发现阻塞问题。

环境搭建 —— 如何让 A/B 无懈可击
Base origin/main 8104071260
Head 该 main + PR head d34c4433(干净合并,6c06f3be;对 main 的 diff 恰好就是 PR 的两个文件)
构建 两侧各跑一次 npm run build && npm run bundle,产出 dist-base/dist-head/
bundle 差异 两个 bundle 的 diff 只有 PR 那个 hunk:isWithinRoot(resolved, root)const rel = path.relative(root, resolved); if (!rel.startsWith('..') && ...),出现两处。其余逐字节相同。
夹具 <ws> 是分支 main-ws 的 git 仓库;<ws>/..build嵌套 git worktree,分支 dotdot-branch,有自己的提交(DOTDOT-ONLY)和自己的脏文件,因此「答错仓库」会直接体现在响应载荷里,而不只是 workspaceCwd 字段
守护进程 qwen serve --port 4871 --workspace <ws>(head)与 --port 4872(base),同一夹具,配 bearer token(严格变更类路由必须有)
环境 macOS 26.6.2 / Darwin 25.6.0、Node v24.18.1、npm 11.16.0

1. 真实路由行为

携带 ?cwd=<ws>/..build 的请求 BASE HEAD
GET /workspaces/:ws/git <ws> · main-ws <ws>/..build · dotdot-branch
GET …/git/branches <ws> · head main-ws <ws>/..build · head dotdot-branch
GET …/git/log <ws> · ROOT-ONLY: workspace root commit <ws>/..build · DOTDOT-ONLY: build worktree commit
GET …/git/diff <ws> · only-in-root.txt <ws>/..build · only-in-dotdot.txt
POST …/git/branch 400 invalid_cwd 200,分支真的在该 worktree 中被创建

也就是说,main 上的静默回退不只是 workspaceCwd 字段填错:四条读取路由会为调用方明确指定的 cwd 返回另一个仓库的分支、历史和 diff;变更类路由则拒绝一个合法路径。PR 描述里的两种症状都复现了,也都被修好了。负向对照(<ws>/../outside<ws>/..)两侧逐字节一致,工作区外的仓库未被触碰。

2. 没有放进任何新东西

对活着的守护进程跑了 16 种 cwd 形态,两侧都跑:

base→head 唯一翻转的行是 ..build...three 以及它们的 ./、结尾斜杠、绕回写法——全部物理上位于工作区内。其余仍然失败即关闭,其中我最想看到的两条也如此:<ws>/..build/../../outside穿过 dotdot 目录的穿越)与 <ws>/..link-outside(指向工作区外、且自身名字以 .. 开头的符号链接)——realpathSync 在判定之前先执行,所以这次放宽没有打开符号链接的口子。<base>/ws-evil(前缀兄弟目录)同样仍被拒绝。

3. 新测试是否真的钉住了行为?

  • 只回退生产代码 hunk、保留测试:2 failed | 26 passed —— 恰好是新增的两条,无误伤。
  • 合并树上的 5 个变异体:isWithinRoot(...)→true 被杀、参数互换 被杀、把 helper 退回旧的 '..' 前缀判断 被杀relativePath !== '..' 在本文件内存活,但被上一层的 workspace-inputs.test.ts > drops nested non-primary roots 杀死。!path.isAbsolute(...) 在 POSIX 上到处存活——因为 path.relative 在 POSIX 永远不会返回绝对路径,所以它在本平台是等价变异体,不是覆盖缺口。

4. Windows:因为本 PR 的 CI 跳过了它

本 PR 上 Test (windows-latest, Node 22.x)Test (macos-latest, Node 22.x) 都是 skipped,所以「由 CI 覆盖」对 Node 测试套件并不成立。我改用 path.win32 跑了两侧的判定式:

12/12 符合预期:C:\work\ws\..build 新被接受;父目录、兄弟目录、前缀兄弟、盘符根、跨盘 D:\evil、UNC \\server\share\evil 全部仍被拒绝。跨盘与 UNC 正是靠 !path.isAbsolute 这一支拒绝的——这也是为什么即便 POSIX 无法触发它,也必须保留。(判定式级别的模拟,不是真的 Windows 主机运行。)

5. 合并树的仓库健康度

npm run typecheck → 0 · 对两个改动文件跑 eslint → 0 · 8 个 serve/git 套件(workspace-git-branches-diff-log-github-prsworkspace-route-runtimeworkspace-inputsfs/policy)→ 137 全过

6. 顺带复测 #8835 的两条兄弟结论(不在本 PR 范围,仅供参考)

  • run-qwen-serve.ts 嵌套 --workspace 漏判 —— 不复现。 qwen serve --workspace <ws> --workspace <ws>/..build 确实被拒绝,与 .../sub 一致。前置关卡是 workspace-inputs.ts,它已经在用共享的 isWithinRootrun-qwen-serve.ts 靠后那段 startsWith('..') 是第二层、如今不可达。建议先在 [repo-hygiene] 2026-W33 report-only findings (8 items) #8835 里更正,免得有人去「修」一段死代码。
  • fs/policy.tsshouldIgnore —— 复现,且 base 与 head 一致。..build/...three/sub/ 都写进 .gitignore 后,GET …/list 会把 sub 过滤掉,却把 ..build...threeignored: false 返回:rel.startsWith('..') 的提前返回把匹配器整个跳过了。这是既有问题,本 PR 未触及——但它同时说明:在此之前文件类路由已经接受 .. 前缀路径,而 git 类路由却拒绝,本 PR 顺带消除了 serve 内部的这处不一致。

结论

无阻塞问题。放宽范围可证明是窄的,活体守护进程上的逃逸面没有变化,PR 所描述的行为在真实 HTTP 路由上端到端复现,而不只是停留在 resolver 层。从我这边看可以合入;目前唯一挡着的是评审决策,而此前的 CHANGES_REQUESTED 针对的是已经修好的描述模板问题。

@wenshao
wenshao added this pull request to the merge queue Aug 29, 2026
Merged via the queue into QwenLM:main with commit 70c5520 Aug 29, 2026
122 checks passed
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.

3 participants