fix(serve): Allow approved same-host text reads outside workspace - #8620
Conversation
E2E test reportBaseline
Post-change validation
Remaining scopeWindows and Linux were not tested locally, and the complete repository test suite was not run. The integration runner emitted its existing non-blocking tsconfig warning that |
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
f624bc9 to
63a6cc4
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
|
Re-run at
Moving on to code review. 🔍 中文说明应用 @wenshao 的要求在当前 head 上重跑 —— 门禁上次跑在
进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewMy independent proposal for #8618 — advertise No blocking findings. The review history did the heavy lifting already — @wenshao's real-daemon A/B on Linux verified all four reviewer-test-plan items (capability frames on the wire, allow/reject sentinel flow, zero Three non-blocking notes:
Test evidence (this PR's own CI at the reviewed commit)The one red check is infra noise:
The central behavioural claim — approved external reads reach the model, rejected ones never do — is substantiated beyond the CI table: the maintainer's tapped-stdio real-daemon A/B on Linux, an integration test that fails on the reverted build, and a green Serve A/B diff. Sandboxed verification ( Not verified here: Windows behaviour (author and maintainer both tested macOS/Linux only) and the full repo suite — the targeted suites cover the change, but a red merge-queue run later would be the first broad signal. 中文说明代码审查我对 #8618 的独立方案——让 daemon 自有的 bridge 宣告 无阻塞项。审查历史已完成大部分重活:@wenshao 在 Linux 上的真实 daemon A/B 验证了 reviewer test plan 全部四项,变异测试杀死了全部 4 个接线变异体;修复后的集成测试有牙齿(在 BEFORE 构建上失败)。 三条非阻塞意见:
测试证据(本 PR 自身 CI,审查提交点)唯一红色 check 为基础设施噪声: 核心行为主张(批准的外部读取可达模型、拒绝的永不可达)由 CI 表之外的证据支撑:维护者在 Linux 上抓取 stdio 的真实 daemon A/B、在回退构建上失败的集成测试、绿色的 Serve A/B diff。针对本 head 的沙箱验证( 此处未验证:Windows 行为(作者与维护者均只测了 macOS/Linux)、仓库全量测试套件——定向套件已覆盖改动,merge queue 的宽泛跑是首个更广信号。 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — a minimal fix for an observed bug, verified more thoroughly than almost any PR this gate sees; the deduction is two open follow-ups, not doubt about the code. Stepping back: this PR started as a 63-line production change and survived four review rounds, an adversarial author self-audit, and two maintainer real-daemon A/B runs — and what came out the other end is still a 63-line production change. The scope never ballooned: writes stay delegated, the HTTP routes stay workspace-scoped, generic ACP keeps delegated reads, and the tradeoffs this change does accept (WFS read-side caps, symlink rejection, audit on the read path) are owned in one design doc rather than scattered across comments. The self-audit's blocking items were either resolved by evidence (the symlink-escape concern — @wenshao forced it on a real daemon and the prompt fires, with #8636's canonicalization closing the layer the author worried about) or correctly ruled out of scope (the L4 lexical-allow semantics change is maintainer-gated core work, and the write/edit family reproducing #8618 later in the sequence is a documented, separate concession). My two reservations are the non-blocking ones from Stage 2: the design doc's inaccurate env-scrub sentence, and the L4 follow-up issue that was promised but never filed. Neither touches the shipped behaviour, and on a re-run neither would justify holding the PR — but the issue should be filed before this merges so the exposure this PR documents doesn't quietly lose its tracking. On the verdict itself: the problem was real and reproduced, the fix does what it says on both wire and behaviour, the tests pin the wiring in all four sites and fail without the change, and the maintainer who knows this code best approved this exact commit after verifying it on the platform the PR hadn't tested. Approving. 中文说明置信度:4/5 —— 对一个已观测 bug 的最小修复,验证深度超过本门禁见过的绝大多数 PR;扣分来自两个未闭环的跟进项,而非对代码的怀疑。 退一步看:这个 PR 以 63 行生产改动开始,经历四轮 review、一次作者对抗性自我审计、两次维护者真实 daemon A/B —— 走出来时仍然是 63 行生产改动。范围从未膨胀:写入保持委托、HTTP 路由保持 workspace 作用域、通用 ACP 保持委托读取,而本改动接受的取舍(WFS 读取侧上限、symlink 拒绝、读取审计)由一份设计文档统一持有。自我审计的阻塞项要么被证据解决(symlink 逃逸担忧 —— @wenshao 在真实 daemon 上强制触发,确认提示会出现,#8636 的规范化也闭合了作者担心的层面),要么被正确判定为超出范围(L4 词法 allow 语义变更属于维护者保留的核心工作;write/edit 家族在序列后段复现 #8618 是已记录的另一让步)。 我的两点保留是 Stage 2 的非阻塞项:设计文档中关于 env 清除的不准确表述,以及承诺创建但尚未创建的 L4 跟进 issue。两者都不影响已交付的行为,re-run 规则下也不构成扣住 PR 的理由 —— 但合并前应当补建该 issue,以免本 PR 文档化的暴露面悄悄失去跟踪。 结论本身:问题真实且有复现,修复在线缆与行为两个层面都做到了它声称的事,测试钉住了全部四处接线且去掉改动即失败,最了解这块代码的维护者在验证了他此前未测的平台后批准了同一提交。批准。 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewReviewed at Critical1. A symlink under an always-allow read root now returns arbitrary host files with no confirmation at all. Major2. The read/write asymmetry means #8618's actual failure mode still happens for 3. External file content reaches every SSE subscriber before anyone approves anything. Suggestions4. Unbounded pre-reads can now OOM the ACP child. 5. No opt-out. 6. Nits
|
doudouOUC
left a comment
There was a problem hiding this comment.
Review notes (at 63a6cc4)
Reviewed the approach and the full diff, and independently validated on a clean checkout of the PR head. Summary: the approach is correct, the implementation is small and focused (~10 production logic lines plus tests/docs), and I found no blocking issues. Two non-blocking suggestions below.
Approach verification
- The capability flip reuses the existing child-side branch:
AcpFileSystemService.readTextFilealready falls back to the regular CLI filesystem service whencapabilities.readTextFileis false, so no new protocol surface is introduced. The fallback path forwards the fullCoreReadTextFileRequest(stats,_meta,limit,maxOutputBytes,signal) verbatim, which the updated unit test now pins. - Permission semantics line up with the regular CLI:
getFileReadDefaultPermissionreturnsaskfor paths outside the workspace and the managed read roots, so approved external reads return content and rejected ones never do. - Downstream consumer sweep:
createAcpSessionBridgehas exactly 4 non-test production call sites (default embedded inserver.ts, primary / static-secondary / dynamic inrun-qwen-serve.ts), and all 4 setdelegateReadTextFileToClient: falsewith per-site test assertions. The option is declared, read at the singleinitializecall site (child restart/respawn shares it), and set by all daemon-owned constructors — not a dead switch. Caller-injecteddeps.bridgeand generic ACP/IDE consumers (e.g. vscode-ide-companion) keep delegated reads. - Defense in depth holds:
BridgeClient.readTextFilestill routes through the injected WFS adapter first, so a capability-violating delegated read from a misbehaving child still fails closed at the workspace boundary. - The stated tradeoff scope is accurate: the shared
FileSystemService.readTextFileconsumers in core are exactly the write/edit/notebook/shell(sed)/artifact/fileUtils pre-reads, and the WFS caps quoted in the docs (256 KiB / 8 MiB) matchserve/fs/policy.ts.
Independent validation
On a clean checkout of 63a6cc4 with a fresh npm ci: the 3 targeted test files pass (2 + 33 + 220 = 255 tests, matching the PR description), root npm run typecheck passes, and ESLint on all 8 changed source/test files passes. Note the full CI test matrix has not run yet on this PR — only label/precheck jobs so far.
Suggestions (non-blocking)
- Integration fixture lives in the real
$HOME.findExternalReadBaseprefershomedir()of the test process, so a hard-killed suite (signal/OOM skipsafterAll) can leave.qwen-serve-external-read-*directories in the developer's real home.mkdtempSyncis 0700 and the content is harmless, so the risk is low, but preferring/var/tmpfirst would keep stray fixtures out of$HOME. The exclusion of/tmpand the repo root is correct and worth keeping (POSIX ACP local-read roots include/tmp, and on Linuxtmpdir()is/tmp, which would overlap the fake-HOME allowed roots and break theaskdeterminism). reject_onceasserts exactly one model request. This pins the current cancel semantics where a rejected tool call ends the turn without feeding the rejection back to the model. If that behavior ever changes to a model-visible rejection round, this assertion fails even though the security property (sentinel never exposed) still holds — the sentinel assertions are the load-bearing ones. Fine as-is; just flagging the coupling.
yiliang114
left a comment
There was a problem hiding this comment.
LGTM, no blockers. The relaxation is deliberate and gated: production qwen serve sets delegateReadTextFileToClient=false so text reads use the child's regular CLI filesystem service and the normal permission flow (default ask — approval required, rejection blocks), while final writes stay delegated through WorkspaceFileSystem and HTTP routes stay workspace-scoped. The adapter keeps a fail-closed read path for unexpected/capability-violating delegated reads. The documented tradeoff (child-local reads give up WFS read-side symlink/TOCTOU/audit protections in exchange for approved same-host reads, under the one-UID/not-an-OS-sandbox daemon model) is honest and acceptable. Tests cover the capability wiring and fail-closed reads. Non-blocking: keep the design doc's 'accepts CLI read-side limits' caveat visible in user docs so operators know approved outside-workspace reads are less hardened than WFS reads.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not reviewed: CI verification — no "Qwen Code CI" run exists at this SHA (fork PR awaiting CI approval); the ci.yml test job carrying the required no-AK integration gate that collects the PR's new integration test (integration-tests/cli/qwen-serve-streaming.test.ts) never executed in CI. The suite was run locally during this review and passed (real daemon + real ACP child, 5.2s)..
中文说明
已审查。 建议见行内评论。 未审查:CI verification — no "Qwen Code CI" run exists at this SHA (fork PR awaiting CI approval); the ci.yml test job carrying the required no-AK integration gate that collects the PR's new integration test (integration-tests/cli/qwen-serve-streaming.test.ts) never executed in CI. The suite was run locally during this review and passed (real daemon + real ACP child, 5.2s).。
— qwen3.8-max via Qwen Code /review (v0.21.6)
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 1363 passed · 0 failed · 1363 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:1363 通过 · 0 失败 · 1363 总计 Verification reportPR 8620 Deep Verification — fix(serve): Allow approved same-host text reads outside workspaceVerdict: 中文摘要
Central claim + A/BCentral claim: under Mechanism traced end to end: parent advertises the capability in the per-channel initialize ( The A/B ran the PR's own deterministic end-to-end scenario (real daemon + real
Witnesses: Control hygiene (per methodology): the base worktree's Mutation matrix (scratch worktree at head; unmutated control green): see
The integration test's own non-vacuity is proven by the A/B itself: it flips from red on base to green on head. CorrectionsNone (first round; no prior review claims to correct). FindingsNone. Notes from boundary checks, so the absences are measured rather than assumed:
Not covered
MethodologyEnvironment: CI verify container ( Evidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
samuelhsin
left a comment
There was a problem hiding this comment.
Code review (at 63a6cc4, locally verified)
I checked the PR head out into a clean worktree and verified everything below at this exact commit on macOS.
Local verification evidence
- The 255 targeted tests claimed in the PR description, re-run one by one and passing: bridge-file-capabilities 2/2, AcpFileSystemService 31/31, server-default-bridge-wiring 2/2, run-qwen-serve 220/220.
- Full acp-bridge suite 1069/1069 passed (including bridge.test.ts's 479 tests — no regression on the default-delegation path); root
npm run typecheckclean across all packages; Prettier + ESLint clean on every changed file. - Built the bundle and ran the new integration test
same-host external text readsagainst a real daemon + fake model:allow_oncesurfaces the sentinel into the next model request and final response;reject_oncenever surfaces it. Both directions pass. - Wiring verified at this commit: the new option's only read site is the single initialize call in
bridge.ts; all four daemon-owned construction sites (server.ts embedded default bridge; primary, static-secondary, and dynamic workspace in run-qwen-serve.ts) set it tofalse; generic ACP / IDE companion / caller-injected bridges keep the defaulttrue; the WFS adapter retains its readText implementation; HTTP file routes untouched. - Branch hygiene: one commit directly on the latest main at creation time; main has not touched any of the affected files since the PR base, so no conflict.
The core mechanism is sound, the diff is small, and test coverage is good. However, I confirm the Critical in @wenshao's review is real (independently verified in code), plus several Majors — recommendation at the end.
Critical (independently verified)
A symlink under an always-allow read root yields a no-confirmation read of arbitrary host files. getFileReadDefaultPermission (packages/core/src/tools/file-read-permission.ts:18) resolves with path.resolve (lexical, no realpath), then tests isSubpaths against projectTempDir, <projectDir>/subagents, the global temp dir, user skills dirs, the extensions dir, and the plans dir. So <projectTempDir>/x → ~/.aws/credentials gets default allow with no prompt, and after this PR the child serves that read locally and follows the symlink. Before this PR the daemon path was shielded: the delegated read hit WFS (symlink_escape), and AcpFileSystemService's local fallback only accepted paths whose realpath was inside the local read roots. This PR removes that shield. The lexical gap already exists for the plain CLI today; this PR extends the exposure to daemon sessions. Suggested fix: realpath the requested path before isSubpaths inside getFileReadDefaultPermission — one change closes it for both CLI and daemon. (Note the fix lands in core tools, which is maintainer territory per AGENTS.md.)
Majors (verified)
- The write family still fails after approval. Reads become local while writes stay delegated, so
write_file/replace/notebook_editon an external path: pre-read succeeds locally → diff renders → user approves →writeTextFile→ WFSpath_outside_workspace→ the tool fails after approval and the model falls back to shell — the same pathology #8618 set out to remove, relocated from step 1 to step 3 with a wasted confirmation round trip. The docs should at least acknowledge this plainly, or the write side needs the same treatment. - Pre-read content is broadcast before anyone approves. For edits,
buildPermissionRequestContent(packages/cli/src/acp-integration/session/permissionUtils.ts:129-136) puts the fulloldTextinto the permission request as a diff content block, which the mediator fans out to every attached SSE client before any decision exists. Pre-PR, WFS refused the external pre-read, so this payload never materialized. The PR's own "reject ⇒ content never surfaces" guarantee holds forread_filebut not for edit-family pre-reads. - Windowless pre-reads lose WFS's caps.
write-file.ts:203/341/874,edit.ts:199/871/886, andnotebook-edit.ts:489/918callreadTextFile({ path })with nomaxOutputBytes;readFileWithLineAndLimit(packages/core/src/utils/fileUtils.ts:341-360) falls through to full-file buffering when no line/limit/maxOutputBytes is present. Previously WFS's 256 KiB full-snapshot cap failed these closed with a typed error; now a multi-GB target can OOM the child →session_diedfor every attached client. Passing a cap at these call sites would keep the PR's intent without the edge. - No opt-out, and a topology concern.
falseis hardcoded at all four sites with no setting/env override. For the explicitly documented--hostname 0.0.0.0 --tokentopology, authenticated clients gain host-read reach (subject to approval), andGET /filestill returns 403 for the very same path — the HTTP and ACP halves of one daemon now disagree about the boundary. A settings key to restore delegation would make this a policy choice; either way it deserves explicit maintainer sign-off.
One rebuttal on the existing review
@wenshao's point #6 ("adapter.readText has no test") does not hold at this commit: packages/cli/src/serve/bridge-file-system-adapter.test.ts has a full readText describe covering the happy path, windowing, size caps, symlink_escape, and external path_outside_workspace rejection (see the boundary enforcement block). The "no production caller anymore" half is accurate, but that is the documented, deliberate defensive posture.
Suggestions / nits
isPathAtOrWithinin the integration test duplicates the already-exportedisPathWithinRootfrom core (packages/core/src/utils/workspaceContext.ts, re-exported from the package index) — semantics identical today, argument order swapped. Endorse @qwen-code-ci-bot's suggestion to reuse it; this helper is the fixture-placement guard of a security-relevant test.findExternalReadBase()places the fixture dir under the real$HOME, and cleanup lives only inafterAll— Ctrl-C /--bail/ CI timeout leaves a.qwen-serve-external-read-*behind on the developer's machine.expect(serializedEvents).toContain('was canceled by the user.')pins a security assertion to a user-facing product string; the neighboringtool_call_updatestatus === 'failed'andnot.toContain(sentinel)assertions already carry the meaning.- In
filesystem.test.ts,expect(writeResult).toEqual({ _meta: undefined })is close to a no-op sincetoEqualignores undefined-valued properties; also consider splitting the combinedit(read fallback + write delegation) so deleting "the write test" later can't silently drop read coverage. docs/design/serve-large-text-range-consistency.mdstill lists "the injected ACPreadTextFileadapter" among the large-text-range boundary's consumers — stale after this PR. And thedocs/users/qwen-serve.mdrewrite drops "Environment isolation between runtimes is not an operating-system security boundary", which was load-bearing for the multi-workspace threat model.
Conclusion
Request changes: the Critical's realpath fix is cheap and should land with this PR (or the maintainer should explicitly accept and track the risk); Majors 1–3 need at minimum documentation or follow-up issues. Otherwise the implementation, wiring coverage, and the deterministic approve/reject integration test are solid — that part passed on my machine exactly as described.
Record what the read capability does not fix: QwenLM#8618 still reproduces for the write and edit family, whose delegated writes are refused after the user has already approved the diff. Give the daemon's pre-approval SSE fan-out its own bullet in the user-facing security section, restore the sentence stating that environment isolation is not an OS security boundary, and make the design doc the single owner of the tradeoff list so tuning a limit cannot leave stale copies behind. Test fixtures no longer land in the developer's real home directory, the assertion pinned to localized rejection copy is dropped, and the combined capability case is split so deleting the write half cannot silently remove read coverage.
|
Thanks @wenshao — this was a genuinely useful review. I verified every claim against Critical1. Symlink under an always-allow read root — confirmed, split out. This is the one item in the review that removes an existing defense rather than merely matching plain-CLI behavior. Before this PR a delegated read was canonicalized at the workspace boundary and rejected as Since the root cause is in the shared read-permission check and reproduces in the plain CLI too, I filed it as #8635 and fixed it in #8636 on current main rather than folding it in here. Two notes for that review, because the obvious patch is not quite right:
The new tests were run against the unfixed code as a negative control: nine escape cases report Major2. The write family still reproduces #8618 — accepted, documented; writes stay delegated. Confirmed. The adapter resolves writes with a write intent, so an out-of-workspace target is refused after the diff has already rendered and the user has already approved. I am not moving writes child-local to match. That would give up the trust gate, symlink rejection, TOCTOU protection, atomic temp-and-rename with mode preservation, and the write audit — a materially larger concession than the read change, and not one this PR should make by implication. What was missing is that the docs never said the reported bug therefore survives for the edit and write family. The design doc now has a "What this does not fix" section stating it plainly, and the developer doc points at it. 3. Pre-approval content reaches every SSE subscriber — accepted, documented. Confirmed: the confirmation payload is built by reading the file, so an out-of-workspace diff is fanned out before the approval decision exists, and in the daemon that means every subscriber rather than one person at a terminal. You were right that the existing framing buries this. The user-facing security section now carries it in its own bullet — previously this material was appended to a bullet whose heading is about the child's environment, which is exactly where a reader auditing filesystem scope would skip past it — and the design doc has a matching section. Suggestions4. Unbounded pre-reads — premise confirmed, but the proposed fix would corrupt files. Deferring. Your read of the branch condition is exactly right: But passing That is a real change with real regression surface, and it is a Suggestion on a PR whose production logic is about ten lines. I would rather do it deliberately in its own change than bolt it on here. Deferring to a follow-up issue; recording the deferral here so it is not silently dropped. 5. No opt-out — agreed in principle, deferring as a feature. The point stands: binding to a non-loopback host with a token is a documented topology, and for those operators "authenticated clients are the same security principal" widens the trust model rather than restating it. But a settings key is a new user-facing feature, and adding one to a fix PR is how a ten-line change becomes a fifty-line one with its own settings/schema/docs surface. Deferring together with #4. I did take the adjacent documentation point: the security section now says the HTTP filesystem route still refuses these paths, so the disagreement between the two halves of one daemon is at least written down. 6.
There is also a symlink-escape case that drives NitsApplied: the fixture base no longer prefers the real On the duplicated tradeoff enumeration: the design doc now declares itself the owner and the developer and user docs link to it instead of restating the limits. The bridge README turned out not to duplicate it — its addition is an API description of the new option — so I left it alone. Also folded in the CI bot's suggestion to drop the local ProcessStill outstanding from the triage bot: 中文说明感谢 @wenshao,这份 review 很有价值。所有论断我都对照 Critical1. 始终允许根目录下的 symlink —— 确认属实,已拆分处理。 这是本次 review 中唯一真正移除既有防线、而非仅仅对齐纯 CLI 行为的一项。改动前,被委托的读取会在 workspace 边界被规范化并以 由于根因位于共享的读取权限检查、且在纯 CLI 上同样复现,我将其提为 #8635,并在基于当前 main 的 #8636 中修复,而非并入本 PR。有两点提醒,因为最直观的补法并不正确:其一,始终允许的根目录必须与候选路径一起规范化,只解析一侧会导致根目录自身位于符号链接之后时所有临时目录读取都变成确认提示(macOS 上 新增测试在未修复代码上做了反向对照:9 条逃逸用例报 Major2. 写家族仍复现 #8618 —— 采纳,已写入文档;写入维持委托。 确认属实:adapter 以 write 意图解析路径,workspace 外目标在 diff 已渲染、用户已批准之后才被拒绝。 我不打算把写入也改为 child 本地。那会放弃 trust gate、symlink 拒绝、TOCTOU 防护、保留权限位的原子 temp+rename 以及写入审计——代价远大于读侧的改动,本 PR 不应以隐含方式做出这种让步。真正缺失的是:文档从未说明所报告的 bug 因此对 edit/write 家族仍然存在。设计文档现已新增"What this does not fix"章节明确写出,开发者文档指向它。 3. 批准前内容到达所有 SSE 订阅者 —— 采纳,已写入文档。 确认属实。你指出的"现有表述掩盖了这一点"是对的:用户文档的安全章节现已将其独立成 bullet——此前这段内容被追加在标题讲 child environment 的 bullet 里,正是审计文件系统作用域的读者会略过的位置——设计文档也有对应章节。 Suggestions4. 无上限预读 —— 前提确认属实,但按建议修会损坏文件,延后处理。 你对分支条件的判断完全正确:无窗口时会走整文件缓冲且无上限,失败模式确实从带类型的拒绝变成了进程死亡。 但在那些调用点传 这是一个有真实回归面的改动,而它只是一条 Suggestion,本 PR 的生产逻辑约十行。我更倾向于单独、审慎地做,而不是附加在这里。延后到 follow-up issue,并在此记录以免被静默丢弃。 5. 缺少 opt-out —— 原则上同意,作为功能延后。 你的论点成立:绑定非回环地址加 token 是文档化的部署形态,对这类运维者而言"认证客户端即同一安全主体"是信任模型的扩大而非重述。但 settings key 是新的用户可见功能,把它加进一个 fix PR,正是十行改动膨胀成五十行并附带 settings/schema/文档面的方式。与 #4 一并延后。 相邻的文档要点我采纳了:安全章节现已说明 HTTP 文件路由仍然拒绝这些路径,从而把同一 daemon 两半之间的判断分歧至少记录在案。 6.
Nits已采纳:fixture 根目录不再优先使用真实 关于重复的 tradeoff 枚举:设计文档现已声明为唯一来源,开发者文档与用户文档改为链接而不再复述。bridge README 经核实并未重复——它新增的是该选项的 API 说明——因此未作改动。 同时采纳了 CI bot 关于用导出的 流程triage bot 提出的两点仍未完成:该 SHA 上 |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 4 scenario(s). — Qwen Code · serve A/B |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and the unit suites were run locally on Linux only.
中文说明
未审查:build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and the unit suites were run locally on Linux only。
— qwen3.8-max via Qwen Code /review (v0.21.6)
|
On the CHANGES_REQUESTED grounds —
So the observation is accurate but the conclusion does not follow: no PR in this repo can show green macOS/Windows unit jobs, and holding this one to that bar would gate it on something its author cannot trigger. The genuine merge gate on this PR is the Critical you re-flagged, which I agree with — see the thread on 中文说明关于 CHANGES_REQUESTED 的依据 ——
因此该观察属实,但结论不成立:本仓库任何 PR 都不可能出现绿色的 macOS/Windows 单元任务,以此为标准会让本 PR 被卡在作者无法触发的条件上。 本 PR 真正的合并门禁是你重新标记的那条 Critical,我认同它 —— 见 |
Review —
|
Resolves a conflict in the streaming integration test where both sides added a module-level fixture directory: keep the external-read dir from this branch alongside main's workspace dir, and clean up both. Also folds in the review round on the same-host read capability: - The external-read fixture base takes an env override and now fails loudly on CI instead of skipping, so a silently disabled security regression test cannot pass as green on an image without /var/tmp. - Its comment no longer implies the interrupt-time cleanup leak was solved; /var/tmp relocates the leak rather than removing it. - The design doc bounds its own "fails closed" claim: the local read retry covers paths under the managed read roots, which include /tmp on POSIX, and that retry is unreachable once the capability is off. - The service carries the same note at the branch that makes it unreachable, so the retry is not mistaken for a live daemon backstop. - The bridge resolves the delegation default once beside its siblings rather than inline at the initialize site.
|
Thanks @wenshao — round 2 is sharper than round 1 and two items changed my mind. Pushed Fixed in
|
yiliang114
left a comment
There was a problem hiding this comment.
Re-reviewed the updated revision. The intent (per maintainer) is confirmed in code: production qwen serve sets delegateReadTextFileToClient=false so read_file goes through the child's own CLI filesystem service (normal permission flow) instead of ACP client delegation — avoiding the 'ACP permission denied then shell fallback' path. New delta is consistent and safe: bridge resolves the default once (prevents drift across initialize sites), filesystem.ts documents that the localReadRoots retry is unreachable under serve (generic-ACP-only guard), and added tests pin delegateReadTextFileToClient:false across serve wiring. Writes stay delegated through WorkspaceFileSystem; unexpected delegated reads fail closed. No new P0/P1.
Self-audit: must-fix list before this is mergeableI re-audited this PR adversarially — presuming the diff wrong rather than looking to confirm it — and it does not hold up. Four items must land before merge. Everything below was verified against this branch's HEAD, not inferred from the diff. The diff itself is small and defaults safe: one 1. Symlink reads outside the workspace, with no prompt — blockingCovered in detail in the reopened thread above. Summary: Fix, kept in scope: re-check the resolved real path against the workspace and 2. The shared text pre-read is now unbounded — blocking
Before this PR those reads hit WFS's 256 KiB The design doc is wrong here too. It says this path "accepts the CLI's read-side limits and behavior", then contrasts it with direct Fix: pass an explicit 3. "Same-host" is asserted nowhere in code — blockingThe title and the design doc both rest on "same-host". All four production sites are unconditional constants. There is no peer-address check, no header check, and no coupling to the bind address — while There is also no operator kill switch: nothing in settings or flags restores the previous boundary. Fix: gate the toggle on 4. The fixes for 1 and 2 need pinning tests — blockingThe tests added here are good in isolation — the capability defaults are asserted both ways, the integration
Without these, both fixes regress silently. Should fix, not blocking
Note on CI
|
Maintainer verification — real daemon on LinuxI built this branch and verified it against a real Verdict: the production change is correct and I reproduced #8618 before it and its absence after it. One blocking defect in the new E2E test — it cannot execute on any machine, and it takes the four pre-existing tests in the same file down on CI. Everything else below is confirmation. 1. Production behaviour — all four reviewer test-plan items hold
The BEFORE arm is a clean reproduction of #8618: the permission request fires, the user selects All four daemon construction sites carry 2. Blocking — the new E2E regression test cannot run
Every other daemon test file declares its own (
Minimal repair (what I ran locally): const TOKEN = 'streaming-integ-secret';
+const REPO_ROOT = path.resolve(__dirname, '../..');
@@
- workspaceCwd: REPO_ROOT,
+ workspaceCwd: workspaceDir,With those two lines the test is correct and has real teeth — it passes on the AFTER build and fails on the BEFORE build ( 3. Why CI would not have caught it
4. Unit-test teeth — every mutant killedBaseline: 4 suites / 267 tests pass (
Splitting the combined capability case was the right call — mutant 1 and mutant 2 are each caught by exactly one of the two cases. 5. Documented tradeoffs I re-checked (not defects)
6. What I did not coverStatic-secondary and dynamic-workspace runtimes were verified through their unit suites and mutation testing, not on the live ACP wire (my repeated Recommendation: fix the two lines in 中文版维护者验证 —— Linux 真实 daemon 环境我在本地构建了该分支,并在真实 结论:生产代码的改动是正确的,我在 BEFORE arm 复现了 #8618,在 AFTER arm 确认已修复。但新增的 E2E 测试存在一个阻塞性缺陷 —— 它在任何机器上都无法真正执行,并且在 CI 上会连带拖垮同文件中已有的 4 个测试。 其余内容都是确认性结论。 1. 生产行为 —— Reviewer 测试计划的 4 项全部成立
BEFORE arm 是对 #8618 的干净复现:权限请求正常弹出,用户选择 四个 daemon 构造点都带上了 2. 阻塞项 —— 新增的 E2E 回归测试无法运行
其他 daemon 测试文件都各自声明了这个常量(
最小修复(我在本地实际使用的): const TOKEN = 'streaming-integ-secret';
+const REPO_ROOT = path.resolve(__dirname, '../..');
@@
- workspaceCwd: REPO_ROOT,
+ workspaceCwd: workspaceDir,补上这两行之后,测试本身是正确且有牙齿的:在 AFTER 构建上通过,在 BEFORE 构建上失败( 3. 为什么 CI 抓不到
4. 单测牙齿 —— 所有变异体均被杀死基线:4 个 suite / 267 个测试全部通过(
把合并的 capability 用例拆开是正确的决定 —— 变异 1 与变异 2 各自只被其中一个用例捕获。 5. 已复核的既定取舍(非缺陷)
6. 未覆盖的部分static-secondary 与 dynamic-workspace 两类 runtime 是通过单测 + 变异测试验证的,未在真实 ACP 线缆上验证(我用重复 建议: 修掉 |
…e daemon's workspace The external-read regression test referenced REPO_ROOT twice without declaring it, which made it unrunnable everywhere: - On a developer box the ReferenceError was swallowed by the bare catch in findExternalReadBase(), every candidate was discarded, and the test reported a green skip -- exactly the silently-disabled security test the CI loud-fail added last round was meant to prevent. The guard was defeated three lines above itself. - On CI that loud-fail branch threw at module scope, so the file failed to collect and took the four pre-existing tests down with it. Declare REPO_ROOT the way every other daemon integration test does. The session also asked for `workspaceCwd: REPO_ROOT` while beforeAll binds the daemon with `--workspace workspaceDir`, so the create returned 400 Workspace mismatch even once the constant existed. The read under test is external because externalReadDir sits outside the bound workspace, not because the session claims a wider one. Finally, collect each candidate's rejection reason instead of dropping it, and fold it into both branches: the CI throw names why every candidate failed and the developer-box skip warns with the same text. A bare catch cannot tell "no /var/tmp on this image" from a bug in the function, and the second reads as a green skip. Reported by @wenshao, who reproduced all three consequences against a real qwen serve daemon on Linux and supplied the repair. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thank you for this @wenshao — an A/B against a real daemon with the ACP stdio tapped is far more than a review, and you found the one thing that mattered. You are right on all three counts and the fix is pushed in The defect
Taking your repair as written:
Locally it now executes rather than skips: I have not re-run your BEFORE/AFTER arm; your The
|
Re-verified at
|
| Item | Observed | Result |
|---|---|---|
REPO_ROOT declared (line 56) |
tsc clean — no TS2304, no swallowed ReferenceError |
✅ fixed |
workspaceCwd: workspaceDir (line 577) |
no 400 workspace_mismatch; session created against the bound workspace |
✅ fixed |
bare catch {} → per-candidate diagnostics |
rejection reason now surfaces on both the skip and the CI throw | ✅ fixed |
Whole qwen-serve-streaming.test.ts, developer box |
5 passed (5) — was 5 silently skipped | ✅ green |
Same file with CI=1 |
1 passed / 4 skipped — was Failed Suites 1 / no tests |
✅ green |
| Teeth: repaired test against the BEFORE build | fails with AssertionError: expected false to be true |
✅ has teeth |
| Unit suites re-run at this head | bridge-file-capabilities 2 · filesystem + server-default-bridge-wiring 34 |
✅ pass |
Two details worth recording:
- The refactored predicate is equivalent.
!inTmp && !inRepo → returnbecameinTmp || inRepo → continue; return— De Morgan, same behaviour, and thecontinueis now explicit rather than relying on loop fall-through. - The diagnostics fix is load-bearing, and I forced it. I made
/var/tmpunusable inside a mount namespace (unshare -m+ read-only tmpfs) to drive the branch that previously produced the silent green skip. Non-CI now prints[qwen-serve-streaming] skipping external read tests: no usable external-read fixture base (/var/tmp: Error: EROFS: read-only file system, access '/var/tmp'), andCI=1throws with that same reason inline instead of the generictried: /var/tmp. A genuine environment gap and a bug in the helper are no longer indistinguishable — which is exactly what let the original defect ship green.
Everything from my previous comment still stands: BEFORE reproduces #8618, AFTER fixes the read path, writes and HTTP reads remain fail-closed at WorkspaceFileSystem, and all four mutants on the production wiring are killed. The documented tradeoffs (post-approval symlink reads no longer hit WFS symlink rejection; the write/edit family still reproduces #8618) are unchanged and owned by the design doc.
The integration-tests/ typecheck blind spot from §3 of my earlier comment is a repo-level gap, not this PR's — worth a separate follow-up.
LGTM — good to merge.
中文版
已在 297dea9 重新验证 —— 阻塞项已解除,LGTM
沿用上一轮相同的 Linux 环境(真实 qwen serve daemon + 真实 qwen --acp 子进程、mock OpenAI、通过 QWEN_CLI_ENTRY 抓取 ACP stdio)。git diff <上一个 head>..297dea9 -- packages/ docs/ 为空,说明我此前验证过的生产改动逐字节未变;本轮重点复核测试修复,并重跑相关套件。
| 项目 | 观测结果 | 结论 |
|---|---|---|
第 56 行声明了 REPO_ROOT |
tsc 干净 —— 无 TS2304,也不再有被吞掉的 ReferenceError |
✅ 已修复 |
第 577 行 workspaceCwd: workspaceDir |
不再出现 400 workspace_mismatch,会话按绑定的 workspace 正常创建 |
✅ 已修复 |
空 catch {} → 逐候选项收集诊断信息 |
跳过时与 CI 抛错时都会带出真实原因 | ✅ 已修复 |
开发机上跑完整 qwen-serve-streaming.test.ts |
5 passed (5) —— 此前是 5 个静默跳过 | ✅ 全绿 |
同一文件加 CI=1 |
1 passed / 4 skipped —— 此前是 Failed Suites 1 / no tests |
✅ 全绿 |
| 牙齿:修复后的测试对 BEFORE 构建 | 以 AssertionError: expected false to be true 失败 |
✅ 有牙齿 |
| 在新 head 上重跑单测 | bridge-file-capabilities 2 · filesystem + server-default-bridge-wiring 34 |
✅ 通过 |
两点值得记录:
- 重构后的判断条件是等价的。
!inTmp && !inRepo → return改成了inTmp || inRepo → continue; return—— 德摩根律,行为一致,而且continue现在是显式的,不再依赖循环自然落空。 - 诊断信息这一处修复是实打实起作用的,我强制触发了它。 我在 mount namespace 里(
unshare -m+ 只读 tmpfs)把/var/tmp变成不可用,以驱动此前产生「静默绿色跳过」的那条分支。非 CI 场景现在会打印[qwen-serve-streaming] skipping external read tests: no usable external-read fixture base (/var/tmp: Error: EROFS: read-only file system, access '/var/tmp'),CI=1场景则把同样的原因内联到抛出的错误里,而不再是笼统的tried: /var/tmp。真实的环境缺失与该 helper 自身的 bug 从此可以区分 —— 而这正是原缺陷得以以绿色状态混过去的原因。
上一条评论中的其余结论依然成立:BEFORE 复现 #8618,AFTER 修复读取路径,写入与 HTTP 读取仍在 WorkspaceFileSystem 处失败关闭,生产代码接线的 4 个变异体全部被杀死。既定取舍(批准之后的 symlink 读取不再经过 WFS 的 symlink 拒绝;write/edit 家族仍复现 #8618)未变,且由设计文档统一持有。
上一条评论第 3 节提到的 integration-tests/ typecheck 盲区属于仓库级问题,与本 PR 无关,建议单独跟进。
LGTM —— 可以合并。
|
Thanks @wenshao. One thing in there I want to call out, because it is better than what I did: you forced the diagnostics branch with Agreed on the De Morgan rewrite being the load-bearing detail worth recording: the And agreed the Current state for anyone reading later: the only standing gate is 中文说明感谢 @wenshao。其中有一点我想特别指出,因为它比我做得更好:你用 同意德摩根改写是值得记录的关键细节: 也同意 供后续查阅的当前状态:唯一仍然生效的门禁是 |
|
@qwen-code /triage |
|
Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 802 passed · 0 failed · 802 total 中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:802 通过 · 0 失败 · 802 总计 Verification reportPR 8620 Deep Verification (round 2) — fix(serve): Allow approved same-host text reads outside workspaceVerdict: 中文摘要
Previous-round finding statusRound 1 (head
Central claim + A/B (re-measured at the new head)Central claim: under
Witnesses: One environmental note: the first attempt to run both arms concurrently killed both vitest processes with zero output (four vitest instances racing at once); each arm re-run in isolation completed normally. The concurrent-death is a harness artifact of this verification run, not a property of the PR. What the delta since round 1 adds, and whether it holdsCommit
At head, the identical scenario runs and passes (head arm above) — the fix is load-bearing, and the rejection-reason collector added on the way now surfaces the real cause in both branches instead of a bare catch. (First reconstruction attempt was unfaithful — a Split capability tests ( Refactored default resolver ( Mutation matrix at head
|
| Mutant | Suite | Result | Failure mode |
|---|---|---|---|
A: initialize hunk → readTextFile: true |
bridge-file-capabilities | killed | opt-out test red; default test stays green (correct attribution) |
A2: resolver ?? true → ?? false |
bridge-file-capabilities | killed | default test red; opt-out stays green (mirror) |
B: 3× delegateReadTextFileToClient: false deleted in run-qwen-serve.ts |
run-qwen-serve | killed (both wiring tests) | expected {…} to match object { delegateReadTextFileToClient: false } |
| C: wiring line deleted in server.ts | server-default-bridge-wiring | killed | same shape |
| D: write delegation condition keyed on read capability | filesystem | killed | only the write test red — split isolation holds |
| E: read-capability early return deleted (positive control) | filesystem | killed | only the read test red |
Survivors: none. Positive control (E) landed beside the others, so "suite can fail" is proven, not assumed.
Targeted gates + boundary checks (re-measured)
- Unit gates, head (witness
04-targeted-gates-and-boundary-checks.png):bridge-file-capabilities.test.ts2 ✓,bridge.test.ts479 ✓ (bridge.ts changed shape, so its own suite gates the refactor),filesystem.test.ts32 ✓,bridge-file-system-adapter.test.ts21 ✓,server-default-bridge-wiring.test.ts2 ✓,run-qwen-serve.test.ts231 ✓ — 767/767. Build = typecheck: fullnpm run buildcompleted at HEAD (CI) and at base (this round). - Reviewer Test Plan: step 1 (default
{true,true}vs daemon-owned{false,true}) — capabilities + wiring tests, mutants A/A2/B/C; step 2 (real daemon/ACP child, allow_once/reject_once) — the A/B itself; step 3 (asymmetric service: reads local, writes delegated) — split filesystem tests, mutants D/E; step 4 (HTTP reads outside workspace + external/untrusted/symlinked delegated writes still rejected) — adapter suite 21/21 + zero diff underpackages/cli/src/serve/fs/. - Construction sites: exactly 4 production
createAcpSessionBridgecall sites (grep); all 4 set the flag (server.ts:978,run-qwen-serve.ts:3911/4311/4836);runtime.createAcpSessionBridgeis a pass-through alias of the same function (run-qwen-serve.ts:1157), no option filtering. - No dead switch: one declaration, one functional read site (bridge.ts resolver + its single use), four set sites.
- Tradeoff single-owner claim: the design doc's numeric caps (256 KiB / 8 MiB) appear only in the design doc itself and in the WFS subsystem's own reference doc/tables (
07-workspace-filesystem.md,qwen-serve-protocol.md, etc. — pre-existing owners of the WFS constants, not copies of the tradeoff list); the three PR-touched sibling docs reference the design doc instead of restating numbers. Claim holds within its stated scope.
Corrections
None to round 1 (its descriptions of the code checked out again this round). One correction to this round's own first attempt, recorded for auditability: the initial broken-state reconstruction mutated the wrong test's workspaceCwd (non-anchored String.replace), which produced a spurious SIGKILL-flow failure; it was redone with a unique anchor before being used as evidence.
Findings
F1 (Low, docs): the design doc asserts a daemon neutralization that no code performs. docs/design/daemon-local-text-reads.md ("Compatibility"): "The daemon neutralizes the env-supplied half by setting QWEN_ACP_LOCAL_READ_ROOTS empty for the child." Measured facts: the string appears nowhere in packages/cli/src/serve/ or packages/acp-bridge/src/; it is not in SCRUBBED_CHILD_ENV_KEYS (spawnChannel.ts:262 — only QWEN_SERVER_TOKEN, QWEN_CODE_SIMPLE, the tool-guard token); the only place anything sets it empty is the integration test fixture (qwen-serve-streaming.test.ts:275). In production, an operator's QWEN_ACP_LOCAL_READ_ROOTS passes through to the child via the runtime env. Security consequence today: none — the local-read retry that consumes those roots sits below the capability check, which returns first when the capability is off (filesystem.ts early return), and all four daemon construction sites turn it off; the doc's next paragraph says exactly this and the A/B proves the behavior. But the sentence describes a defense that does not exist, in the security section of the doc that is now the single owner of this tradeoff — a future reader (or a future change re-enabling delegation) could rely on it. Suggested one-line fix, measured against the code facts above: replace the sentence with "The daemon's protection against the env-supplied half is the capability itself: with delegation off, the retry that consults those roots is unreachable (below); nothing scrubs the variable, so a generic ACP host that keeps delegation on inherits the operator's roots by design." Doc-only change; no suite moves either way, which is the unpinned-axis signal — if the maintainer wants it pinned, the fixture would need an assertion that the child env carries the variable through, and that is a test of absence-of-scrubbing, recommended only if the sentence is kept in any "the daemon manages this variable" form.
Not covered
- Windows (PR marks it untested; this round is Linux, same as round 1).
- Repository-wide test suite and standalone repo-wide typecheck — targeted suites + the full CI build at HEAD stand in.
- Sandbox modes (
QWEN_SANDBOX=docker/podman); the suite skips there by design (child cannot reach host loopback) and ran withQWEN_SANDBOX=false. - Byte-level capture of the
initializeJSON-RPC frame on real stdio — wire payload asserted via the in-memory channel in the capabilities tests, and the end-to-end A/B proves the child behaved asreadTextFile: falseover real stdio; no raw frame dump. - Base-side
reject_oncesub-scenario: unreachable becauseallow_oncefails first; rejection was already safe on base (reads always denied), so no behavior is unmeasured. - Per-commit attribution: the checkout is depth 2 (
git rev-parse --is-shallow-repository= true); only the merge commit, base tip, and PR head are reachable locally. The intermediate four commits were verified as the aggregateHEAD^1..HEADdiff; their individual boundaries (which review-round change landed in which commit) were not separable. The metadata snapshot's 5-commitcommitsarray matchesHEAD^2and the aggregate diff content. - The fix(serve): align same-host daemon text reads with CLI permissions #8618 write/edit-family residual failure is documented by the PR as out of scope ("What this does not fix"); the pinned write-side behavior (adapter rejects external writes) is consistent with that claim, but the end-to-end write repro was not driven.
Methodology
Environment: CI verify container (node:22-bookworm, Node v22.23.2), working tree = refs/pull/8620/merge at depth 2, npm ci + npm run build completed pre-verification; /var/tmp present and writable (the fixture base the test probes). A/B: base worktree at HEAD^1 rebuilt (npm run build + npm run bundle, exit 0) with root node_modules shared via symlinks and the @qwen-code/* workspace links recreated as relative links inside the base tree (realpath asserted into tmp/base-tree/packages/*); nested per-package node_modules mirrored from head (lockfile untouched by the PR, verified by diffstat). Dependency state is therefore a clean control. The PR's test file was copied verbatim (cmp clean) and run through the base bundle; the identical command ran against the head bundle. Mutation matrix ran in a second scratch worktree at HEAD with the same link recipe; its first pass had red controls because worktrees lack built dist/ and generated files — repaired by symlinking HEAD's per-package dist/ and src/generated dirs (mutated files are transformed from src by vitest and were never served from the shared dists), after which controls went green and only then were the B/C mutants trusted. Broken-state repro used a faithful reconstruction (unique-anchor replacement verified by grep: exactly 2 undeclared REPO_ROOT references) as a temporary test file, deleted afterwards (working tree confirmed clean). Assertion counting convention: one vitest test case = one scripted assertion unit (each fails if any contained expect fails), plus one unit per scripted shell/expectation check (realpath, purity greps, repro outcomes, control-cell predictions); raw per-cell logs in logs/, harness scripts in mutation-matrix.sh / make-broken-repro.mjs, evidence PNGs rasterised with scripts/verify-capture.mjs.
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
…hat found (QwenLM#8693) * fix(integration-tests): make the project typecheckable and fix what that found `tsc -p integration-tests/tsconfig.json` could not run at all. The config carried a `"//"` documentation key inside `compilerOptions.paths`, and every value there must be an array, so tsc aborted with TS5063 before checking a single file. Nothing in CI runs it either, so the directory has been unchecked for its whole life -- which is how PR QwenLM#8620 shipped an `integration-tests/cli/qwen-serve-streaming.test.ts` that referenced an undeclared `REPO_ROOT`, swallowed the ReferenceError in a bare catch, and reported a green skip for a security regression test. Moving that note out of `paths` exposed 404 errors. Three more config defects accounted for 353 of them: - `composite: true` is inherited from the root config for the packages that are actually referenced. Composite requires every file in the program to appear in `include`, and these tests import package sources by relative path, so it produced 324 TS6307. Nothing references this project and it emits nothing, so it is now `composite: false`. - The root `lib` is ES2023 only. The suite drives browser-side code in `terminal-capture/` and pulls SDK sources that name `WebSocket` and `HeadersInit`, so 21 identifiers resolved to nothing. Now DOM + DOM.Iterable + ES2023, matching packages/cli. - Workspace packages resolved through `packages/core/dist` via a project reference, so with core unbuilt the checker reported a dozen members as missing from `Storage` that are right there in the source. They now resolve from source through `paths`, mirroring packages/cli, and the reference is gone. node-pty declares `types` at the top level but its `exports` map is a bare string with no `types` condition, so nodenext never reached the declarations and every pty handle degraded to `any` -- which is what silently untyped the `data` and `exitCode` callbacks in test-helper.ts. It now resolves through `paths` as well. `@types/jsdom` is added for the one file that uses it; DefinitelyTyped has no release matching jsdom 26 (it jumps 21 -> 27), so this pins the current 28.x. Two real defects fell out of the remaining 51: - write_file.test.ts built a detailed tool-call failure message and passed it to `toBeTruthy()`, which takes no arguments. It was discarded on every failure, leaving only a bare literal. - Two terminal-capture scenarios set `gif: true` inside `streaming`, where the runner never reads it. It is a scenario-level switch. The rest was making an existing `undefined` visible. `readToolLogs()` promised `name: string` for fields copied straight out of telemetry attributes that nothing validates; the stdout fallback can promise them, the telemetry branch cannot, and claiming otherwise just moved the `undefined` past the type checker into the assertions. This is type resolution only. `integration-tests/vitest.config.ts` keeps its own hardcoded aliases onto the built SDK bundle, so the suite still exercises the published-bundle shape at runtime. Not wired into CI here, but not for cost reasons: a cold run of `tsc -p integration-tests/tsconfig.json` takes about 106s on an idle developer box. The program is 2679 files, of which 103 are integration tests and roughly 1100 are package sources their own projects already check, so there is duplicated work available to reclaim by resolving the packages from their built declarations -- but at ~106s it is already cheap enough to gate on as-is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(integration-tests): isolate jsdom types and complete source-resolution paths Address review round 1: - external-context: override `types` to ["node"]. The root @types/jsdom entered its program through vitest's optional jsdom types and injected lib dom, flipping @types/node's fetch globals to DOM variants whose ReadableStream is not async-iterable (TS2504 in http-client.ts), which failed every CI job during the npm ci prepare build. - integration-tests tsconfig: explicit nodenext paths entries for every workspace subpath the program imports (sdk/daemon, 19 acp-bridge subpaths, core goalWire/memoryScopes/userPromptSubmitContext, webui daemon-react-sdk, channel-base); drop the dead `*` wildcards; include **/*.tsx. Typechecks green with the source packages' dists removed. - Relax noPropertyAccessFromIndexSignature in integration-tests and revert the six bracket-access rewrites it forced in SDK sources. - channel-plugin: import channels/base from src and map @qwen-code/channel-base to source so both declarations agree. - qwen-serve-streaming: asAccepted delegates to the SDK's exported isNonBlockingAccepted type predicate instead of a drifted copy. - sleep-interception: tighten blocked predicates to success === false and fix the comment describing them. - Declare jsdom at the root next to @types/jsdom. * fix(integration-tests): complete source-resolution paths and restore single channel-base instance Address review round 2: - Map the eight builtin channel adapters and web-templates to source. channel-registry.ts and html.ts still resolved them through their exports maps to dist, so the typecheck's build-independence was incomplete: on a tree without built dists it failed with the exact 9 x TS2307 the maintainer verification measured. - channel-plugin.test.ts: import @qwen-code/channel-base by bare specifier instead of a relative src path. At runtime the test and plugin-example now resolve the same dist/index.js through the exports map, restoring the single ChannelBase / SessionRouter instance the relative src import silently split; type resolution still maps to source through paths, and vitest.config.ts keeps pointing e2e runs at the built bundles. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com> Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
|
Released in v0.21.8. |












What this PR does
This PR keeps generic ACP text-read delegation enabled by default, while same-host daemon-owned bridges advertise
readTextFile: falseandwriteTextFile: true. Their ACP children therefore use the regular CLI filesystem service for text reads, including directread_fileand shared text pre-reads, while final ACP text writes remain delegated through the workspace filesystem boundary.It wires the capability choice into the default embedded, primary, static-secondary, and dynamic workspace runtimes; leaves caller-injected bridges under caller control; preserves the defensive workspace-filesystem read adapter; and documents the exact permission, audit, resource, and security boundaries.
Why it's needed
Before this change,
qwen servedelegated text reads to its workspace-scoped client filesystem. A directread_filefor a host text file outside the registered workspace was therefore rejected by the workspace filesystem even after the normal CLI tool permission flow approved it, and the model could retry through shell instead. Same-host daemon reads should follow the same explicit permission rules as the regular CLI without weakening HTTP file routes or final ACP text writes.Reviewer Test Plan
How to verify
{ readTextFile: true, writeTextFile: true }, while each daemon-owned bridge initializes them as{ readTextFile: false, writeTextFile: true }.read_fileon a text file outside the workspace and all managed read roots. Selectingallow_onceshould place the sentinel content in the next model request and final response; selectingreject_onceshould never place the sentinel in subsequent model input or output.Validation on a clean checkout of the latest
mainpassed:npm cicompleted the full build and bundle, root typecheck passed, 255 targeted capability/filesystem/wiring tests passed, target-file ESLint and Prettier passed, pre-commit checks passed, andgit diff --checkpassed. The deterministic real daemon/ACP test also passed:allow_onceexposed the sentinel to the next model request and final response, whilereject_onceemitted a failed tool update, completed the turn without another model request, and never exposed the sentinel. The globalqwenbaseline reproduced the approved-read failure before the fix.Evidence (Before & After)
Before: a deterministic run against the globally installed
qwenfailed the approved-content assertion because the workspace filesystem rejected the external text read after approval; the observed product flow could then fall back to shell.After: the updated bundle's deterministic real-daemon regression passes for both approval and rejection, capability and asymmetric-routing tests pass, and all four daemon construction paths advertise local reads. The PR is based directly on the latest
mainwith one commit; none of the #7311 workspace-runtime stack commits are included.Tested on
Environment (optional)
macOS, Node.js 26.0.0, clean
mainworktree, local same-host daemon/ACP child, and a fake OpenAI-compatible server.Risk & Scope
FileSystemService.readTextFileconsumer, so write, edit, notebook, sed, and artifact pre-reads also move to the child-local CLI filesystem and no longer receive WFS's 256 KiB returned-output and full-snapshot cap, 8 MiB large-text scan cap, read audit, symlink rejection, or read-side TOCTOU protections. This is the explicit same-host, same-UID compatibility tradeoff; directread_filestill applies CLI permissions and core output limits.Linked Issues
Refs #8618
中文说明
此 PR 做了什么
此 PR 保持通用 ACP 文本读取默认委托,同时让 daemon 自己创建的同机 bridge 宣告
readTextFile: false、writeTextFile: true。因此 ACP child 使用普通 CLI 文件系统服务执行文本读取,包括直接read_file和共享文本预读;ACP 最终文本写入仍通过 workspace 文件系统边界委托执行。它将这个 capability 选择接入 default embedded、primary、static-secondary 和 dynamic workspace 四类 runtime;调用方注入的 bridge 仍由调用方控制;防御性的 workspace 文件系统读取 adapter 继续保留;同时文档明确记录权限、审计、资源和安全边界。
为什么需要
改动前,
qwen serve将文本读取委托给 workspace 范围内的 client 文件系统。因此,直接read_file读取注册 workspace 之外的主机文本文件时,即使普通 CLI 工具权限流程已经批准,workspace 文件系统仍会拒绝,模型随后可能改走 shell。同机 daemon 的读取应遵循普通 CLI 相同的显式权限规则,同时不能削弱 HTTP 文件路由或 ACP 最终文本写入的保护。Reviewer 测试计划
如何验证
{ readTextFile: true, writeTextFile: true },而每个 daemon 自有 bridge 初始化为{ readTextFile: false, writeTextFile: true }。read_file。选择allow_once后,下一次模型请求和最终响应中应出现 sentinel 内容;选择reject_once后,后续模型输入或输出中都不应出现 sentinel。在最新
main的干净 checkout 上验证通过:npm ci完成完整 build 和 bundle,根目录 typecheck 通过,255 个定向 capability/filesystem/wiring 测试通过,目标文件 ESLint 和 Prettier 通过,pre-commit 检查通过,git diff --check通过。确定性的真实 daemon/ACP 测试也已通过:allow_once将 sentinel 暴露给下一次模型请求和最终响应;reject_once发出失败的工具更新,不再请求模型便完成 turn,并且始终不暴露 sentinel。修复前,全局qwen基线复现了批准读取仍失败的问题。证据(改动前后)
改动前:针对全局安装
qwen的确定性运行在批准内容断言处失败,因为 workspace 文件系统在批准之后仍拒绝外部文本读取;观察到的产品流程随后可能回退到 shell。改动后:更新 bundle 的确定性真实 daemon 回归测试在批准和拒绝两条路径均通过,capability 与非对称路由测试通过,四个 daemon 构造路径都宣告本地读取。PR 直接基于最新
main且仅有一个提交,不包含 #7311 workspace-runtime 堆栈中的任何提交。已测试平台
环境(可选)
macOS、Node.js 26.0.0、干净的
mainworktree、本地同机 daemon/ACP child,以及假 OpenAI 兼容服务器。风险与范围
FileSystemService.readTextFile消费者,因此 write、edit、notebook、sed 和 artifact 的预读也迁移到 child 本地 CLI 文件系统,不再获得 WFS 的 256 KiB 返回输出与全量快照上限、8 MiB 大文本扫描上限、读取审计、symlink 拒绝或读侧 TOCTOU 保护。这是明确接受的同机、同 UID 兼容性取舍;直接read_file仍应用 CLI 权限和 core 输出限制。关联 Issue
Refs #8618