Skip to content

fix(web-shell): scope artifact actions to owning workspace - #8510

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
zjunothing:fix/issue-8494-workspace-artifact-actions
Aug 6, 2026
Merged

wenshao merged 5 commits into
QwenLM:mainfrom
zjunothing:fix/issue-8494-workspace-artifact-actions

Conversation

@zjunothing

@zjunothing zjunothing commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

This PR binds artifact previews, downloads, code-review reports, file reviews, and durable scheduled-task actions to the registered workspace that produced each turn output. It carries immutable workspace identity (workspaceCwd and, when advertised, workspaceId) through primary chat, split panes, nested subagents, and side tasks; secondary file access uses the workspace-qualified daemon client, and scheduled-task operations always use the owning workspace ID.

The owner is resolved against the daemon's current capabilities and fails closed after removal, trust loss, duplicate identity, replacement, or remove-and-re-add. A stable authority token preserves valid in-flight work across semantically equivalent capability refreshes and React StrictMode effect replay without allowing an old request to regain access after its owner was revoked.

Why it's needed

Turn outputs from a secondary workspace previously retained or reused primary-workspace action objects. When two repositories had the same relative artifact path, opening or downloading the secondary output could read the primary repository's file, and durable task controls could target the wrong workspace. Passing live actions through nested panes also allowed stale authority to survive workspace removal or runtime replacement.

The first ownership fix exposed a React StrictMode edge case in the repository's real visual scenario: effect cleanup briefly revoked a still-valid owner, so the code-review artifact panel rendered “Workspace artifact owner is no longer available.” The authority-token lifecycle fixes that regression while retaining the original fail-closed security boundary.

Reviewer Test Plan

How to verify

  1. Run a Web Shell daemon with two trusted registered workspaces. Put different contents at the same relative sentinel path in each workspace, create an artifact from the secondary workspace, then open and download it from its turn output. Confirm the secondary sentinel is returned and the request uses the secondary workspace-qualified route.
  2. Open a secondary-workspace code-review/file-review output and confirm preview and download operations stay scoped to the secondary workspace. Create, list, update, and delete a durable scheduled task from that output and confirm every operation carries the secondary workspace ID.
  3. Keep a secondary file read pending and refresh capabilities with equivalent workspace records; the read must complete. Remove or replace the owner, including removing and re-advertising the same ID/CWD, and confirm the old read rejects without retrying against the primary workspace.
  4. Exercise the code-review visual scenario under React StrictMode in both themes and confirm the right panel renders “Authoritative verdict” instead of the unavailable-owner alert.
  5. Run npm test, npm run build, npm run typecheck, npm run lint, and npm run test:e2e:visuals -- --grep "code review artifact" from packages/web-shell.

Evidence (Before & After)

Before the ownership change, the failure-first route probes produced 3 failures and 29 passes: secondary artifact/file reads and scheduled-task mutations reached primary actions. Before the StrictMode follow-up, the real Playwright visual scenario failed in both dark and light themes at screenshots.spec.ts:848; the panel showed “Unable to display code review — Workspace artifact owner is no longer available.” The same failure was reproduced locally and in visual workflow run 30879548937.

After the change, the focused ownership suites pass 413/413, the complete Web Shell suite passes 2,780/2,780, and the StrictMode visual scenario passes 2/2 with real Chromium screenshots in dark and light themes. The generated local screenshots were visually inspected and show the complete “Authoritative verdict” panel; this environment has no authenticated interactive browser available for uploading those local PNGs, so the repository's visual-preview workflow is the shareable image source for this head.

Production two-workspace validation used primary workspace ID 35f375e336aa5962 and secondary workspace ID 2162ccb7ea97c9ec. The same relative sentinel path returned distinct SHA-256 values (818d5f…b9127 primary, 0e9fac…4a12 secondary), and file, byte-range, and scheduled-task CRUD requests all used the expected owner route.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not locally tested
🐧 Linux ⚠️ not locally tested

Environment (optional)

macOS arm64; repository Node/npm toolchain; Playwright Chromium 149. Linux behavior is additionally covered by the repository's Ubuntu CI, but was not counted as a local test.

Risk & Scope

  • Main risk or tradeoff: Workspace operations now reject when ownership metadata is missing, ambiguous, untrusted, or changes during a request. This is intentional fail-closed behavior; equivalent capability refreshes keep the same authority token, while removal/replacement/re-add creates a new token.
  • Not validated / out of scope: Windows and Linux were not exercised locally. No daemon routes or persistence formats are changed. The package-wide format check still reports five unchanged baseline CSS/HTML files; every file changed by this PR passes Prettier.
  • Breaking changes / migration notes: Host integrations that manually construct the exported TurnOutputOpenRequest with a retained workspaceActions object should pass workspaceCwd and, for registered workspaces, workspaceId instead. Normal Web Shell consumers require no migration.

Linked Issues

Fixes #8494

中文说明

本 PR 做了什么

本 PR 将产物预览、下载、代码审查报告、文件审查和持久化定时任务操作绑定到实际生成对应 turn output 的已注册工作区。它在主聊天、分屏、嵌套子代理和 side task 之间传递不可变的工作区身份(workspaceCwd,以及 daemon 已公告时的 workspaceId);次工作区文件访问使用带工作区限定的 daemon 客户端,定时任务操作始终使用所有者工作区 ID。

所有者会依据 daemon 当前 capabilities 重新解析;工作区被移除、失去信任、出现重复身份、被替换,或被移除后重新加入时均会 fail closed。稳定的 authority token 允许语义等价的 capabilities 刷新和 React StrictMode effect 重放期间的合法进行中操作继续完成,同时禁止旧请求在其所有者被撤销后重新获得访问权。

为什么需要它

来自次工作区的 turn output 以前会保留或复用主工作区 action 对象。当两个仓库拥有相同的相对产物路径时,打开或下载次工作区输出可能读取主仓库文件,持久化任务控件也可能操作错误的工作区。在嵌套面板间传递实时 action 还会使过期权限在工作区移除或运行时替换后继续存活。

第一版所有权修复暴露了仓库真实视觉场景中的 React StrictMode 边界情况:effect cleanup 会短暂撤销仍然有效的所有者,使代码审查产物面板显示“Workspace artifact owner is no longer available”。authority-token 生命周期修复了该回归,同时保留原有的 fail-closed 安全边界。

审查者测试计划

如何验证

  1. 启动包含两个可信已注册工作区的 Web Shell daemon。在两个工作区的同一相对 sentinel 路径写入不同内容,从次工作区创建产物,再从 turn output 打开并下载它。确认返回的是次工作区 sentinel,且请求使用带次工作区限定的路由。
  2. 打开次工作区的代码审查/文件审查输出,确认预览和下载始终限定在次工作区。通过该输出创建、列出、更新和删除持久化定时任务,确认每个操作都携带次工作区 ID。
  3. 保持一次次工作区文件读取处于 pending 状态,用语义等价的工作区记录刷新 capabilities;读取必须成功完成。随后移除或替换所有者(包括移除后以相同 ID/CWD 再次公告),确认旧读取被拒绝且不会回退重试主工作区。
  4. 在 React StrictMode 下分别以深色和浅色主题运行代码审查视觉场景,确认右侧面板显示“Authoritative verdict”,而不是所有者不可用告警。
  5. 在 packages/web-shell 目录运行 npm test、npm run build、npm run typecheck、npm run lint 和 npm run test:e2e:visuals -- --grep "code review artifact"。

证据(修复前后)

所有权修复前,failure-first 路由探针结果为 3 个失败、29 个通过:次工作区产物/文件读取和定时任务修改会命中主工作区 action。StrictMode 跟进修复前,真实 Playwright 视觉场景的深色和浅色主题都在 screenshots.spec.ts:848 失败;面板显示“Unable to display code review — Workspace artifact owner is no longer available”。同一失败已在本地和视觉工作流运行 30879548937中复现。

修复后,所有权聚焦测试 413/413 通过,Web Shell 完整测试 2,780/2,780 通过,StrictMode 视觉场景使用真实 Chromium 在深色和浅色主题下 2/2 通过。本地生成的截图已目视检查,完整显示“Authoritative verdict”面板;当前环境没有可用于上传本地 PNG 的已认证交互式浏览器,因此此 head 的可共享图片来源是仓库视觉预览工作流。

生产双工作区验证使用主工作区 ID 35f375e336aa5962 和次工作区 ID 2162ccb7ea97c9ec。同一相对 sentinel 路径返回不同的 SHA-256(主工作区 818d5f…b9127,次工作区 0e9fac…4a12),文件、字节区间和定时任务 CRUD 请求均使用预期的所有者路由。

测试平台

系统 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未在本地测试
🐧 Linux ⚠️ 未在本地测试

环境(可选)

macOS arm64;仓库规定的 Node/npm 工具链;Playwright Chromium 149。仓库 Ubuntu CI 也覆盖 Linux 行为,但未计为本地测试。

风险与范围

  • 主要风险或权衡:工作区所有权元数据缺失、存在歧义、不可信,或在请求期间变化时,工作区操作现在会被拒绝。这是有意的 fail-closed 行为;语义等价的 capabilities 刷新会保留同一 authority token,而移除、替换或移除后重加会生成新 token。
  • 未验证/范围之外:未在本地运行 Windows 和 Linux。未修改任何 daemon 路由或持久化格式。包级完整格式检查仍会报告 5 个未修改的历史 CSS/HTML 文件;本 PR 修改的每个文件都通过 Prettier。
  • 破坏性变更/迁移说明:手动构造导出的 TurnOutputOpenRequest 且保留 workspaceActions 对象的宿主集成,应改为传递 workspaceCwd,并为已注册工作区传递 workspaceId。普通 Web Shell 使用方无需迁移。

关联 Issue

Fixes #8494

@zjunothing

zjunothing commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

Verification report

Verified head: 46a0b6cfb4d3ffdc2fb1a910d2405ae9785ae85f

Failure-first evidence

The original ownership probes failed 3 cases while 29 passed: an ownerless artifact tab read the primary sentinel, a secondary artifact download never called the qualified secondary client, and a secondary durable-task list omitted its workspaceId.

The follow-up visual regression was independently reproduced with real Playwright Chromium. Both dark and light scenarios failed at screenshots.spec.ts:848; the JSON file request returned successfully, but the post-read authority check rejected it and the panel displayed Unable to display code review — Workspace artifact owner is no longer available. The same failure is visible in visual workflow run 30879548937.

Automated checks at this head

  • StrictMode regression plus equivalent-refresh/remove-re-add authority coverage: 31/31 ArtifactPanel tests passed
  • Related ownership suites (resolver, turn outputs, artifact panel): 3 files, 47/47 tests passed
  • Complete Web Shell suite: 167 files, 2,780/2,780 tests passed
  • Real Chromium code-review visual scenario: 2/2 passed (dark and light)
  • npm run build, npm run typecheck, and npm run lint in packages/web-shell: passed
  • git diff --check and explicit Prettier checks for every changed file: passed
  • Pre-commit formatter/linter: passed

The package-wide format check still reports only five unchanged baseline files: BranchPickerPopover.module.css, GitModePopover.module.css, GitDialog.module.css, PlanExecutionView.module.css, and packages/web-shell/client/index.html.

One intermediate full-suite run produced 10 build-artifact.test.ts failures because I incorrectly ran the production build concurrently with the suite, causing dist to change while the artifact assertions were reading it. I then ran the build and suite sequentially: the build exited 0 and all 2,780 tests passed.

Production two-workspace validation

The built CLI/Web Shell was run with two registered local workspaces containing the same relative sentinel path but different contents. /capabilities advertised primary ID 35f375e336aa5962 and secondary ID 2162ccb7ea97c9ec.

  • Root GET /file?path=artifact-owner.txt returned PRIMARY_WORKSPACE_SENTINEL_8494 (SHA-256 818d5f4f1fb9c7e3f9bdfd9a3ad39361c93a23ca3f3d0ba5e4a7c889b33b9127).
  • Secondary GET /workspaces/2162ccb7ea97c9ec/file?path=artifact-owner.txt returned SECONDARY_WORKSPACE_SENTINEL_8494 (SHA-256 0e9fac7909b16ff8b17014ea103ed5018c8b4e9ad21d2f95e128fef3a3544a12).
  • The secondary bytes route returned only secondary bytes.
  • Secondary scheduled-task POST/GET/PATCH/DELETE requests all used /workspaces/2162ccb7ea97c9ec/scheduled-tasks...; update/delete affected only the secondary fixture, and both test fixtures were cleaned up.

Visual evidence

After the fix, Playwright generated genuine 1366×768 dark and light screenshots. I inspected both images: the right panel renders the full Authoritative verdict, metrics, caps, filters, and finding content. This runtime still has no authenticated interactive browser session with which to upload local PNG attachments to GitHub; I did not substitute synthetic images. The push triggers the repository's visual-preview workflow, which is the shareable screenshot source for reviewers.

中文验证报告

验证报告

验证 head:46a0b6cfb4d3ffdc2fb1a910d2405ae9785ae85f

失败优先证据

最初的所有权探针有 3 个失败、29 个通过:缺少所有者的产物标签错误读取主工作区 sentinel;次工作区产物下载没有调用带工作区限定的次工作区客户端;次工作区持久化任务列表遗漏 workspaceId。

后续视觉回归已使用真实 Playwright Chromium 独立复现。深色和浅色场景都在 screenshots.spec.ts:848 失败;JSON 文件请求已经成功返回,但读取后的 authority 二次校验拒绝了结果,面板显示 Unable to display code review — Workspace artifact owner is no longer available。同一失败也可见于视觉工作流运行 30879548937。

当前 head 的自动化检查

  • StrictMode 回归,以及等价刷新/移除后重加 authority 覆盖:ArtifactPanel 31/31 通过
  • 相关所有权套件(resolver、turn outputs、artifact panel):3 个文件,47/47 通过
  • Web Shell 完整套件:167 个文件,2,780/2,780 通过
  • 真实 Chromium 代码审查视觉场景:深色和浅色 2/2 通过
  • packages/web-shell 中的 npm run build、npm run typecheck、npm run lint:通过
  • git diff --check 和全部改动文件的独立 Prettier 检查:通过
  • pre-commit 格式化和 lint:通过

包级完整格式检查仍只报告 5 个未修改的历史文件:BranchPickerPopover.module.css、GitModePopover.module.css、GitDialog.module.css、PlanExecutionView.module.css 和 packages/web-shell/client/index.html。

有一次中间的完整测试出现 10 个 build-artifact.test.ts 失败,因为我错误地让生产构建与测试并发运行,导致断言读取 dist 时该目录正在变化。随后按顺序运行构建和测试:构建退出码为 0,2,780 个测试全部通过。

生产双工作区验证

构建后的 CLI/Web Shell 使用两个已注册本地工作区运行;两个工作区包含相同相对 sentinel 路径但内容不同。/capabilities 公告主工作区 ID 35f375e336aa5962 和次工作区 ID 2162ccb7ea97c9ec。

  • 根 GET /file?path=artifact-owner.txt 返回 PRIMARY_WORKSPACE_SENTINEL_8494(SHA-256 818d5f4f1fb9c7e3f9bdfd9a3ad39361c93a23ca3f3d0ba5e4a7c889b33b9127)。
  • 次工作区 GET /workspaces/2162ccb7ea97c9ec/file?path=artifact-owner.txt 返回 SECONDARY_WORKSPACE_SENTINEL_8494(SHA-256 0e9fac7909b16ff8b17014ea103ed5018c8b4e9ad21d2f95e128fef3a3544a12)。
  • 次工作区 bytes 路由只返回次工作区字节。
  • 次工作区定时任务 POST/GET/PATCH/DELETE 请求全部使用 /workspaces/2162ccb7ea97c9ec/scheduled-tasks...;更新和删除只影响次工作区 fixture,测试结束后已清理两个 fixture。

视觉证据

修复后,Playwright 生成了真实的 1366×768 深色和浅色截图。我已检查两张图片:右侧面板完整显示 Authoritative verdict、指标、caps、筛选器和 finding 内容。当前运行环境仍没有可用于向 GitHub 上传本地 PNG 附件的已认证交互式浏览器会话;我没有使用合成图片替代。此次推送会触发仓库视觉预览工作流,它将作为审查者可共享的截图来源。

@zjunothing

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

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

Reviewed. Suggestions are inline.

中文说明

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

— qwen3.7-max via Qwen Code /review (v0.21.4)

Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/components/artifacts/TurnOutputs.tsx
@zjunothing

Copy link
Copy Markdown
Collaborator Author

Review follow-up verification report

Verified head: ac1ad61b4b3663e6a424a37df9e92c071e185687

Failure-first evidence

The Critical loading bug was independently reproduced on the reviewed head for all three mutation paths: save, enable/disable, and delete. In each case, a mutation superseded an in-flight reload through the shared request counter and left the panel permanently rendering Loading…. A separate runtime probe confirmed that a non-durable task with an unavailable workspace owner was incorrectly hidden behind the workspace-unavailable alert.

Review fixes

  • Separated load lifecycle ownership from mutation/data invalidation, so an orphaned reload can settle its own loading/error state without overwriting newer task data.
  • Kept stale mutation results scoped to their original task/owner and verified the replacement task remains usable.
  • Allowed non-durable scheduled-task tabs to render their local snapshot without workspace access.
  • Added fail-closed coverage for durable scheduled-task, file, and artifact tabs with missing owners.
  • Added legacy single-workspace coverage for list/update/delete with workspaceId: undefined.
  • Added post-await authority-revocation coverage for text, bytes, and stat reads.
  • Strengthened split-pane dedup/session-switch assertions and added the unknown-workspace download guard regression.

Automated verification

ArtifactPanel DOM suite                         PASS (40/40)
TurnOutputs DOM suite                           PASS (8/8)
Complete App suite                              PASS (303/303)
Web Shell TypeScript typecheck                  PASS
Changed-file ESLint                             PASS
Changed-file Prettier                           PASS
Web Shell production build + library build      PASS
git diff --check                                PASS
Pre-commit formatter/linter                      PASS

The full App run emits existing test-harness act(...) warnings and intentional error-path logs; all 303 tests pass. The browser connector exposed no browser instance in this runtime, so no new trustworthy screenshot could be captured or uploaded. I did not substitute a synthetic image; the original PR report retains its real Chromium visual evidence, while this follow-up is pinned by the DOM interaction suites and production build above.

中文验证报告

审查跟进验证报告

验证 head:ac1ad61b4b3663e6a424a37df9e92c071e185687

失败优先证据

在被审查的 head 上,已分别复现 Critical Loading 问题的 3 条变更路径:保存、启用/禁用、删除。每种情况下,变更操作都会通过共享 request 计数器使进行中的 reload 失效,并让面板永久显示 Loading…。另一条运行时探针也确认:当 workspace owner 不可用时,非持久化任务会被错误地挡在 workspace 不可用告警之后。

审查修复

  • 将 load 生命周期所有权与 mutation/data 失效分离,使被变更操作顶掉的 reload 仍能结束自己的 loading/error 状态,同时不能覆盖更新的数据。
  • 将过期 mutation 结果限制在原任务/owner scope,并验证替换任务仍可继续操作。
  • 非持久化 scheduled-task 标签无需 workspace 即可显示本地快照。
  • 为 owner 缺失时的持久化 scheduled-task、file 和 artifact 标签补充 fail-closed 覆盖。
  • 补充 legacy 单工作区 workspaceId: undefined 的 list/update/delete 覆盖。
  • 补充 text、bytes、stat 读取完成后的 authority 撤销覆盖。
  • 加强 split-pane 去重、会话切换断言,并补充未知 workspace 时隐藏下载按钮的回归测试。

自动化验证

ArtifactPanel DOM 套件                         通过(40/40)
TurnOutputs DOM 套件                           通过(8/8)
完整 App 套件                                  通过(303/303)
Web Shell TypeScript 类型检查                  通过
变更文件 ESLint                                通过
变更文件 Prettier                              通过
Web Shell 生产构建与 library 构建              通过
git diff --check                               通过
Pre-commit 格式化与 lint                       通过

完整 App 测试会输出已有测试夹具的 act(...) 警告和刻意触发的错误路径日志,但 303 个测试全部通过。当前运行环境的浏览器连接器没有暴露可用浏览器实例,因此无法生成或上传新的可信截图;没有使用合成图片替代。原 PR 报告仍保留此前真实 Chromium 的视觉证据,本次跟进则由上述 DOM 交互套件和生产构建固定行为。

@wenshao

wenshao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 4, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

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

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Collaborator

⚠️ AutoFix round 4 ended without publishing a report — view run.

中文说明

⚠️ AutoFix 第 4 轮结束但未发布报告 —— 查看运行。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Address-review summary — PR #8510 (round 2)

Commit: dd489502e — fix(web-shell): keep open artifact tabs alive across pane and list gaps (on fix/issue-8494-workspace-artifact-actions, additive; no conflicts, --conflict false).

Triage gate: PR body template (stage 1a, CHANGES_REQUESTED)

The triage review asks to restructure the PR body around the repository template (What this PR does / Why it's needed / Reviewer Test Plan with How to verify / Evidence (Before & After) / Tested on / Risk & Scope / Linked Issues), keeping the paragraph-for-paragraph 中文说明 block. It implies no code changes. This environment has no GitHub credentials and cannot edit the PR body, so the restructured body was prepared per the prepare-pr skill and written to <workdir>/pr-title.txt and <workdir>/pr-body.md (template headings, long unwrapped lines, full Chinese translation retained). The workflow or a maintainer needs to apply those files to the PR; the content incorporates the original verification reports plus this round's changes.

Round-2 findings

  • [Critical] R2-1 — open artifact tabs orphaned when the producing pane closes (rc:3714673743): FIXED. The extra-artifact purge in the pane-report handler now keeps extras referenced by open artifact tabs, so a tab's open-time row survives the pane closing or its snapshot clearing; the pre-PR behavior let tabs survive pane close, and the design doc ties tab invalidation to workspace removal/trust loss/runtime replacement only. Because a retained extra would otherwise shadow newer pane reports in the merged artifact list, the merge now prefers pane snapshots over open-time extras, preserving the update-propagation behavior added earlier in this PR (the pane-report test asserts 10 B → 20 B → changed while the pane is alive, and continued rendering after the pane clears; it was renamed to updates an open artifact tab from pane snapshots and keeps it after the pane clears).
  • [Suggestion] R2-2 — actions={activeWorkspaceActions!} unsound for non-durable scheduled-task tabs (rc:3714673746): FIXED. ScheduledTaskDetail's actions prop is widened to ArtifactWorkspaceActions | undefined, the call site drops the assertion, and the four actions dereferences are guarded (loadTask treats missing actions like the non-durable snapshot path; the three mutation handlers return early). No behavior change for reachable states; the type now matches the fail-closed guard's exemption.
  • [Suggestion] R2-3 — no test drives scheduled_task/review opens through App's handleTurnOutputOpen (rc:3714673750): COVERED. The App SplitView mock gains split-open-scheduled-task and split-open-review buttons carrying stamped workspaceCwd/workspaceId/sourceSessionId. New tests assert a durable scheduled-task panel lists through listScheduledTasks('pane-ws') and renders the task (dropping or transposing the stamped identity fails closed and turns the test red), and a review-tab download routes fileStat/readWorkspaceFileBytes through client.workspaceByCwd('/tmp/pane').
  • [Suggestion] R2-4 — handleSave's workspaceId never asserted (rc:3714673751): COVERED. The save branch of settles a pending reload after a %s mutation now asserts updateScheduledTask is called with ('cron-secondary', objectContaining({ prompt }), 'secondary-id').
  • [Suggestion] R2-5 — ChatPane.handleRightPanelOpen had zero coverage (rc:3714673755): COVERED. The ChatPane MessageList mock exposes a turn-output open trigger; a new test asserts the forwarded request equals the emitted request plus sourceSessionId: 'sess-1' with no workspaceActions key (exact-equality assertion mirrors the SubagentDetail integration test).
  • [Suggestion] R2-6 — main-session opens no longer cached in extras (rc:3714673760): FIXED. The open-time row is cached unconditionally again (restoring the pre-PR !request.workspaceActions arm of the gate). The suggested fix alone was insufficient: the live-list reconcile purged a main-session extra as soon as the live list covered the artifact, so the reconcile's exemption was also extended from pane-owned tabs to all open artifact tabs; retained extras stay inert because live rows and pane snapshots merge first. New App test opens a main-session artifact, flips the connection to disconnected (live list empties), asserts the tab still renders, then reconnects and asserts the live list takes over.

Round-1 findings (re-verified, already fixed by earlier commits)

All eleven round-1 inline findings were re-verified against this head and remain resolved by fd1f53e4e / ac1ad61b4:

  • rc:3712736510 (R1-1 Critical, loading-flag race) — load-only request counter plus settles a pending reload after a %s mutation for save/toggle/delete.
  • rc:3712736518 (R1-3) — discards a pending mutation when the task scope changes.
  • rc:3712736525 (R1-4) — keeps legacy single-workspace scheduled-task routes unqualified.
  • rc:3712736533 + rc:3712736542 (R1-7 1/2 and 2/2) — fail-closed cases for durable scheduled-task and file tabs with missing owners, asserting zero workspace calls.
  • rc:3712736553 (R1-8) — revokes every pending file read when its owner is removed covers text, bytes, and stat wrappers.
  • rc:3712736559 (R1-9) — fail-closed guard exempts non-durable scheduled tasks; shows a session-scoped task snapshot without a workspace owner.
  • rc:3712736594 (R1-10) — session-switch test installs resolvable capabilities and asserts rendered content plus absence of the workspace alert.
  • rc:3712736598 (R1-11) — re-add recovery asserts the target is defined before comparing actions.
  • rc:3711796884 (doudouOUC, dedup) — same-ID extra vs pane snapshot is covered by the pane-snapshot test; note the semantics intentionally evolved with the R2-1 fix: while the tab is open the extra is retained but inert (the pane snapshot merges first), so no duplicate rows appear and updates still propagate.
  • rc:3711796892 (doudouOUC, download guard) — hides Download when the artifact workspace cannot be resolved in TurnOutputs.dom.

Verification

Commands actually run this round (all at the final working tree, before the commit; the repository's pre-commit hook also passed during the commit):

  • cd packages/web-shell && npx tsc --noEmit — passed (no output)
  • cd packages/web-shell && npx eslint client/App.tsx client/App.test.tsx client/components/ChatPane.test.tsx client/components/artifacts/ArtifactPanel.tsx client/components/artifacts/ArtifactPanel.test.tsx — passed (no findings)
  • npx prettier --check on the five changed files — passed
  • cd packages/web-shell && npx vitest run client/components/artifacts/ArtifactPanel.test.tsx client/components/ChatPane.test.tsx — 2 files, 106/106 passed
  • cd packages/web-shell && npx vitest run client/App.test.tsx — 306/306 passed
  • cd packages/web-shell && npx vitest run (complete Web Shell suite) — 167 files, 2794/2794 passed
  • npm run build (all packages) — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npm run generate:settings-schema — not needed (no settings source changed); integration tests not needed (changed behavior is exercised by the web-shell DOM suites, not the bundled CLI harness)
中文说明

审查处理总结 — PR #8510(第 2 轮)

提交:dd489502e — fix(web-shell): keep open artifact tabs alive across pane and list gaps(位于 fix/issue-8494-workspace-artifact-actions 分支,追加提交;无冲突,--conflict false)。

Triage 门禁:PR 正文模板(stage 1a,CHANGES_REQUESTED)

Triage 审查要求按仓库模板重组 PR 正文(What this PR does / Why it's needed / Reviewer Test Plan(含 How to verify / Evidence (Before & After) / Tested on)/ Risk & Scope / Linked Issues),并保留逐段对应的 中文说明 折叠块。该要求不涉及代码改动。当前环境没有 GitHub 凭证、无法直接编辑 PR 正文,因此已按 prepare-pr skill 准备好重组后的正文,写入 <workdir>/pr-title.txt 与 <workdir>/pr-body.md(使用模板标题、不做固定列宽换行、保留完整中文翻译),需要由 workflow 或维护者将其应用到 PR 上;内容已合并原始验证报告与本轮改动。

第 2 轮发现

  • [Critical] R2-1 — 产生产物的 pane 关闭时,已打开的产物标签页变成孤儿(rc:3714673743):已修复。 pane 上报处理器中的 extra-artifact 清除逻辑现在会保留被打开的产物标签页引用的 extra,使标签页在 pane 关闭或其快照被清除后仍能继续渲染;改动前的行为本就允许标签页在 pane 关闭后存活,且设计文档将标签页失效限定为工作区移除/失去信任/运行时替换。由于保留的 extra 会在合并后的产物列表中遮蔽更新的 pane 上报,合并顺序改为 pane 快照优先于打开时缓存的 extra,从而保留本 PR 早前加入的更新传播行为(pane 上报测试断言 pane 存活期间 10 B → 20 B → changed,pane 清除后继续渲染;测试更名为 updates an open artifact tab from pane snapshots and keeps it after the pane clears)。
  • [Suggestion] R2-2 — 非持久化定时任务标签页上 actions={activeWorkspaceActions!} 不健全(rc:3714673746):已修复。 ScheduledTaskDetail 的 actions prop 放宽为 ArtifactWorkspaceActions | undefined,调用点去掉断言,四处 actions 引用加上守卫(loadTask 在 actions 缺失时走非持久化快照路径;三个变更处理器提前返回)。可达状态下行为不变,类型现在与 fail-closed 守卫对非持久化任务的豁免一致。
  • [Suggestion] R2-3 — 没有测试通过 App 的 handleTurnOutputOpen 驱动 scheduled_task/review 打开(rc:3714673750):已覆盖。 App 的 SplitView mock 新增 split-open-scheduled-task 与 split-open-review 按钮,携带 stamp 的 workspaceCwd/workspaceId/sourceSessionId。新测试断言持久化任务面板通过 listScheduledTasks('pane-ws') 列表并渲染任务内容(丢弃或调换 stamp 身份会 fail-closed 并使测试变红),以及 review 标签页的下载经由 client.workspaceByCwd('/tmp/pane') 调用 fileStat/readWorkspaceFileBytes。
  • [Suggestion] R2-4 — handleSave 的 workspaceId 从未被断言(rc:3714673751):已覆盖。 settles a pending reload after a %s mutation 的 save 分支现在断言 updateScheduledTask 以 ('cron-secondary', objectContaining({ prompt }), 'secondary-id') 被调用。
  • [Suggestion] R2-5 — ChatPane.handleRightPanelOpen 零覆盖(rc:3714673755):已覆盖。 ChatPane 的 MessageList mock 暴露一个 turn-output 打开触发器;新测试断言转发的请求等于原始请求加 sourceSessionId: 'sess-1' 且不含 workspaceActions 键(精确相等断言,与 SubagentDetail 集成测试对齐)。
  • [Suggestion] R2-6 — 主会话打开不再缓存进 extras(rc:3714673760):已修复。 打开时的产物行重新无条件缓存(恢复改动前 !request.workspaceActions 分支的门控语义)。仅按建议修复并不够:live 列表一旦覆盖该产物,reconcile 会立刻清除主会话的 extra,因此 reconcile 的豁免也从 pane 所有标签页扩展到所有打开的产物标签页;保留的 extra 不会产生副作用,因为 live 行与 pane 快照在合并时优先。新增 App 测试:打开主会话产物,将连接切为 disconnected(live 列表清空),断言标签页仍渲染;再恢复连接,断言 live 列表接管。

第 1 轮发现(重新验证,已由早前提交修复)

全部 11 条第 1 轮行内发现已在当前 head 重新验证,确认仍由 fd1f53e4e / ac1ad61b4 解决:

  • rc:3712736510(R1-1 Critical,loading 竞态)— 仅 load 使用的请求计数器,外加 save/toggle/delete 的 settles a pending reload after a %s mutation。
  • rc:3712736518(R1-3)— discards a pending mutation when the task scope changes。
  • rc:3712736525(R1-4)— keeps legacy single-workspace scheduled-task routes unqualified。
  • rc:3712736533 + rc:3712736542(R1-7 两处)— 持久化定时任务与 file 标签页在 owner 缺失时的 fail-closed 用例,断言零工作区调用。
  • rc:3712736553(R1-8)— revokes every pending file read when its owner is removed 覆盖 text、bytes、stat 三个包装函数。
  • rc:3712736559(R1-9)— fail-closed 守卫豁免非持久化定时任务;shows a session-scoped task snapshot without a workspace owner。
  • rc:3712736594(R1-10)— 会话切换测试安装可解析的 capabilities,断言渲染内容且无工作区告警。
  • rc:3712736598(R1-11)— re-add 恢复先断言 target 已定义,再比较 actions。
  • rc:3711796884(doudouOUC,去重)— 同 ID 的 extra 与 pane 快照由 pane 快照测试覆盖;注意语义随 R2-1 修复有意演进:标签页打开期间 extra 被保留但不生效(pane 快照优先合并),不会出现重复行,更新仍会传播。
  • rc:3711796892(doudouOUC,下载守卫)— TurnOutputs.dom 中的 hides Download when the artifact workspace cannot be resolved。

验证

本轮实际运行的命令(均在最终工作树上、提交前执行;提交时仓库 pre-commit 钩子也已通过):

  • cd packages/web-shell && npx tsc --noEmit — 通过(无输出)
  • cd packages/web-shell && npx eslint client/App.tsx client/App.test.tsx client/components/ChatPane.test.tsx client/components/artifacts/ArtifactPanel.tsx client/components/artifacts/ArtifactPanel.test.tsx — 通过(无发现)
  • 对 5 个改动文件执行 npx prettier --check — 通过
  • cd packages/web-shell && npx vitest run client/components/artifacts/ArtifactPanel.test.tsx client/components/ChatPane.test.tsx — 2 个文件,106/106 通过
  • cd packages/web-shell && npx vitest run client/App.test.tsx — 306/306 通过
  • cd packages/web-shell && npx vitest run(完整 Web Shell 套件)— 167 个文件,2794/2794 通过
  • npm run build(全部包)— 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npm run generate:settings-schema — 无需执行(未改动 settings 源);无需集成测试(改动行为由 web-shell DOM 套件覆盖,而非打包 CLI harness)

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

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (1080000ms)) (attempt 2/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: timeout (1080000ms).

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

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


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (1080000ms)) (attempt 3/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: timeout (1080000ms).

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

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


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix stopped: this counting window now contains 3 time-budget exhaustions (pushed rounds in between included; this round itself may have failed differently). That is 3 full agent runs that pushed nothing. A human should split or reduce the PR (or raise the agent time budget AND its step backstop together), then comment @qwen-code /retry to re-arm. Until then future scans will skip this PR.

What I found before stopping:
Qwen failed during address-review: timeout (1080000ms).

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

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


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

⏸️ Takeover paused: this PR reached its round cap (100/100). Comment @qwen-code /takeover to re-arm a fresh window and continue management, or @qwen-code /takeover stop to release.

中文说明

⏸️ 托管已暂停:本 PR 达到轮次上限(100/100)。评论 @qwen-code /takeover 可重新武装、开启新窗口继续托管;或评论 @qwen-code /takeover stop 释放。

@wenshao

wenshao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge August 6, 2026 14:12
@wenshao
wenshao added this pull request to the merge queue Aug 6, 2026
Merged via the queue into QwenLM:main with commit 79027bb Aug 6, 2026
155 of 157 checks passed
@wenshao

wenshao commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Independent local verification (real two-workspace daemon + real browser)

I rebuilt this scenario locally instead of re-reading the tests: a real qwen serve daemon with two registered workspaces, a real model turn that records a real artifact, and the actual Web Shell driven in headless Chromium — once with the client from PR head dd489502, once with the client from merge-base 06cc41ee, once from PR commit 1 8aadba34. Same daemon, same flow, only the client code differs.

Harness (click to expand)
Piece Setup
Daemon qwen serve --port 4510 --workspace <primary> --workspace <secondary> (loopback + bearer token). /capabilities advertised 48177a5afd42bbd7 (primary, trusted) and b3d18abc3b1313f5 (secondary, trusted).
Sentinels Same relative path report.html in both workspaces, different contents: primary sha256 0a19eca8…2664 (32 B), secondary sha256 e38630d9…3a64 (34 B).
Artifact Not hand-built: a mock OpenAI provider made the agent call record_artifact inside a secondary-workspace session. The daemon persisted artifact 6ea52c9e5348ef6b with qwen.workspace.sha256 = e38630d9… (the secondary file).
Client packages/web-shell vite dev server per worktree (React StrictMode on, as in client/main.tsx), proxying to the same daemon. Playwright recorded every /file / /file/bytes request.
Flow Sidebar → secondary workspace → New task → prompt → artifact card → Open, then Download.

1. The vulnerability is real, and the fix closes it

Preview request Rendered content Download bytes
merge-base 06cc41ee GET /file?path=report.html PRIMARY_WORKSPACE_SENTINEL_8510 ❌ primary ❌
PR head dd489502 GET /workspaces/%2F…%2Fsecondary/file?path=report.html SECONDARY_WORKSPACE_SENTINEL_8510 ✅ secondary ✅

Note the card in both runs reads HTML · 34 B (secondary metadata) while the old client renders the 32-byte primary file — the mismatch users would never notice.

Control: on PR head, a primary-workspace session still uses the unqualified /file route and renders the primary file, so the qualified route is not applied indiscriminately.

secondary artifact before/after

2. The StrictMode follow-up is load-bearing (and effective)

  • PR commit 1 (8aadba34) in the same live flow: right panel stuck on "Loading preview… / Workspace artifact owner is no longer available".
  • The repository's own visual scenario reproduces it: screenshots.spec.ts:848 → 2 failed at commit 1, 2 passed at PR head, 2 passed at merge-base. So the regression was introduced and fixed inside this PR.
  • Bonus coverage: that harness advertises capabilities without a workspaces array, so the passing run also exercises the resolver's legacy single-workspace branch.

strictmode before/after

3. Finding — fail-closed also removes preview/download in untrusted workspaces

resolveArtifactWorkspaceOwner requires match.trusted === true. With security.folderTrust.enabled: true and the folder not yet trusted, the daemon reports trusted: false but still serves the file (GET /file → 200, verified by curl). Result:

Untrusted workspace, artifact "Open"
merge-base Preview renders the correct file; Download present and returns correct bytes
PR head Right panel: "This workspace may have been removed or the link is no longer valid."; Download button gone from the turn-output card

This is not limited to multi-workspace setups: a single-workspace daemon also advertises the array, so an ordinary untrusted single-workspace deployment loses artifact/review/file previews entirely (verified: workspaces: [{id: 48177a5afd42bbd7, primary: true, trusted: false}], /file → 200). The trust bit does not change which workspace a read targets, so it isn't part of the ownership ambiguity this PR is fixing.

One more wrinkle: trusted in /capabilities is entry.state === 'active' && entry.current?.runtime.trusted === true (packages/cli/src/serve/routes/capabilities.ts:92) — it also encodes runtime liveness, so any window where the runtime is not active flips an open panel to the "may have been removed" alert.

Suggestion (either is fine, but please pick one before merge):

  1. Accept an unambiguous single-cwd match regardless of trusted, leaving the daemon as the trust authority for its own routes; or
  2. Keep failing closed on untrusted, but use a distinct message (e.g. "workspace is untrusted") instead of "may have been removed", and call the behavior change out in the PR description / release notes.

Severity: medium — security.folderTrust.enabled defaults to false, so most deployments are unaffected.

untrusted before/after

4. Ownership loss

With the secondary artifact tab open on PR head, I restarted the daemon without the secondary workspace. No request was retried against the primary route (the recorded network log contains no unqualified /file after the restart); the session view reset and the tab closed with the reconnect. The finer-grained states (duplicate id, replaced runtime, remove-and-re-add) are covered by the PR's own unit tests, which I ran — I did not reproduce each one live.

5. Gates I ran on PR head

Gate Result
Focused ownership suites (5 files: resolver, ArtifactPanel, TurnOutputs, SubagentDetail, ChatPane) 124/124 passed
Full packages/web-shell vitest 2784 passed, 10 failed — all 10 in build-artifact.test.ts, which reads dist/ and needs npm run build first (environment, unrelated to this PR)
Typecheck of client/** (same file set as tsconfig.lib.json, sibling packages mapped to source) clean
ESLint on the 13 changed client files clean
Test-file typecheck (not gated by CI) Same pre-existing errors on merge-base and head; this PR adds none
Repository visual scenario code review artifact (dark + light) 2 passed

npm run build could not be completed in this environment (the shared install lacks vite-plugin-dts / comment-json, and the sibling packages/webui/dist is stale) — that is an environment issue, not the PR; CI covers it.

code review visual

6. Smaller notes

  1. ArtifactPanel resolves ownership from activeTab.workspaceCwd only (no fallback to the panel's own workspaceCwd prop), and then requires activeWorkspaceTarget?.workspaceId === activeTab.workspaceId. A tab opened before capabilities land carries a cwd but no id, so the equality never holds afterwards and that tab shows the unavailable alert permanently. Treating "tab has cwd, no id" as a match when the resolved owner's cwd is equal would remove that trap.
  2. useArtifactWorkspaceTarget writes authorityRef during render and revokes it from an effect cleanup via queueMicrotask. Ref mutation during render is not safe under concurrent rendering — an abandoned render can rewrite the authority the committed tree is using. It behaves correctly today (verified above), but given this PR already had one StrictMode incident here, an effect/state-driven authority would be sturdier.
  3. artifactPanelExtraArtifacts now caches every opened artifact and only drops entries the live list covers; entries for artifacts that vanish from every list are held until the panel/session resets. Bounded by user clicks — noting for completeness.

Verdict

The core security fix is real and verified end-to-end against a real daemon: opening or downloading a secondary-workspace artifact no longer reads the primary repository. Item 3 (untrusted workspaces losing preview/download, with a misleading message) is what I'd like resolved or explicitly accepted before merge; 6.1 is a cheap robustness win. Everything else looks good.

Not verified here: the durable scheduled-task workspaceId path (unit tests + code reading only, no live CRUD run), and code-review/file-review previews (same resolver and action surface as the artifact path verified above).

中文版

独立本地验证(真实双工作区 daemon + 真实浏览器)

我没有只复核测试,而是在本地重建了这个场景:一个注册了两个工作区的真实 qwen serve、一次真实的模型回合来记录真实产物,以及在无头 Chromium 中驱动真实的 Web Shell —— 分别使用 PR head dd489502、merge-base 06cc41ee、PR 第 1 个提交 8aadba34 的客户端代码。daemon 与流程完全相同,只有客户端代码不同。

验证环境(点击展开)
组成 配置
Daemon qwen serve --port 4510 --workspace <primary> --workspace <secondary>(loopback + bearer token)。/capabilities 公告 48177a5afd42bbd7(primary,trusted)与 b3d18abc3b1313f5(secondary,trusted)。
Sentinel 两个工作区放同一相对路径 report.html,内容不同:主工作区 sha256 0a19eca8…2664(32 B),次工作区 sha256 e38630d9…3a64(34 B)。
产物 不是手工构造:mock OpenAI provider 让 agent 在次工作区会话内调用 record_artifact。daemon 持久化了产物 6ea52c9e5348ef6b,其 qwen.workspace.sha256 = e38630d9…(即次工作区文件)。
客户端 每个 worktree 各自的 packages/web-shell vite dev(React StrictMode 开启,见 client/main.tsx),代理到同一个 daemon。Playwright 记录了所有 /file / /file/bytes 请求。
流程 侧边栏 → secondary 工作区 → New task → 提示词 → 产物卡片 → Open,再 Download。

1. 漏洞真实存在,修复确实闭合了它

预览请求 渲染内容 下载字节
merge-base 06cc41ee GET /file?path=report.html PRIMARY_WORKSPACE_SENTINEL_8510 ❌ 主工作区 ❌
PR head dd489502 GET /workspaces/%2F…%2Fsecondary/file?path=report.html SECONDARY_WORKSPACE_SENTINEL_8510 ✅ 次工作区 ✅

注意两次运行卡片都显示 HTML · 34 B(次工作区元数据),而旧客户端渲染的是 32 字节的主工作区文件 —— 这种不一致用户几乎不可能察觉。

对照:在 PR head 上,主工作区会话仍走非限定的 /file 路由并渲染主工作区文件,说明限定路由不是无差别套用的。

2. StrictMode 跟进提交是必要且有效的

  • PR 第 1 个提交(8aadba34)在同样的实时流程中:右侧面板卡在 "Loading preview… / Workspace artifact owner is no longer available"。
  • 仓库自带的视觉场景可复现:screenshots.spec.ts:848 在第 1 个提交 2 failed,在 PR head 2 passed,在 merge-base 2 passed。即该回归在本 PR 内被引入、也在本 PR 内被修复。
  • 额外收获:该 harness 公告的 capabilities 不含 workspaces 数组,因此这次通过也覆盖了 resolver 的 legacy 单工作区分支。

3. 发现 —— fail-closed 同时移除了未受信任工作区的预览/下载

resolveArtifactWorkspaceOwner 要求 match.trusted === true。当 security.folderTrust.enabled: true 且目录尚未被信任时,daemon 会报告 trusted: false,但依然正常提供文件(GET /file → 200,已用 curl 验证)。结果:

未受信任工作区,点击产物 "Open"
merge-base 正常渲染正确文件;Download 存在且返回正确字节
PR head 右侧面板:"This workspace may have been removed or the link is no longer valid.";turn output 卡片上的 Download 按钮消失

这不限于多工作区场景:单工作区 daemon 同样会公告该数组,因此普通的未受信任单工作区部署会完全失去产物/审查/文件预览(已验证:workspaces: [{id: 48177a5afd42bbd7, primary: true, trusted: false}],/file → 200)。信任位并不改变一次读取指向哪个工作区,因此它并不属于本 PR 要消除的归属歧义。

还有一点:/capabilities 中的 trusted 是 entry.state === 'active' && entry.current?.runtime.trusted === true(packages/cli/src/serve/routes/capabilities.ts:92)—— 它同时编码了运行时存活状态,所以运行时不处于 active 的任何窗口都会让已打开的面板翻转成 "may have been removed" 告警。

建议(二选一,但希望合并前明确其一):

  1. 只要 cwd 唯一匹配就接受,无论 trusted 如何,把信任判定继续交给 daemon 自己的路由;或
  2. 保留对未受信任工作区 fail closed,但改用独立文案(例如 "workspace is untrusted")而不是 "may have been removed",并在 PR 描述/发布说明中写明这一行为变更。

严重程度:中 —— security.folderTrust.enabled 默认为 false,多数部署不受影响。

4. 所有权丢失

在 PR head 上保持次工作区产物标签页打开,然后不带次工作区重启 daemon。没有任何请求回退重试主工作区路由(重启后的网络记录中没有非限定 /file);会话视图重置,标签页随重连关闭。更细粒度的状态(重复 id、运行时被替换、移除后重加)由本 PR 自带的单元测试覆盖,我运行了这些测试,但没有逐一做实时复现。

5. 我在 PR head 上跑过的检查

检查项 结果
聚焦的所有权测试(5 个文件:resolver、ArtifactPanel、TurnOutputs、SubagentDetail、ChatPane) 124/124 通过
packages/web-shell 完整 vitest 2784 通过,10 失败 —— 全部在 build-artifact.test.ts,它读取 dist/,需要先 npm run build(环境问题,与本 PR 无关)
client/** 类型检查(文件集合同 tsconfig.lib.json,兄弟包映射到源码) 通过
对 13 个改动客户端文件跑 ESLint 通过
测试文件的类型检查(CI 并未设卡) merge-base 与 head 上是同一批既有错误;本 PR 未新增
仓库视觉场景 code review artifact(深色 + 浅色) 2 通过

npm run build 在本环境无法完成(共享安装缺少 vite-plugin-dts / comment-json,且兄弟包 packages/webui/dist 已过期)—— 这是环境问题而非本 PR 的问题,CI 会覆盖。

6. 其他小问题

  1. ArtifactPanel 只从 activeTab.workspaceCwd 解析归属(不回退到面板自身的 workspaceCwd prop),随后又要求 activeWorkspaceTarget?.workspaceId === activeTab.workspaceId。在 capabilities 到达之前打开的标签页只带 cwd 而没有 id,之后该等式永远不成立,这个标签页会永久显示不可用告警。当解析出的所有者 cwd 相等时,把"有 cwd、无 id"视为匹配即可消除该陷阱。
  2. useArtifactWorkspaceTarget 在 render 期间写 authorityRef,并在 effect cleanup 中通过 queueMicrotask 撤销。在并发渲染下,render 期间写 ref 并不安全 —— 被放弃的 render 可能改写已提交树正在使用的 authority。目前行为正确(上文已验证),但考虑到本 PR 在这里已经出过一次 StrictMode 事故,改为由 effect/state 驱动的 authority 会更稳。
  3. artifactPanelExtraArtifacts 现在缓存每一个被打开的产物,且只丢弃实时列表已覆盖的条目;从所有列表中消失的产物条目会保留到面板/会话重置。数量受用户点击次数限制 —— 仅作完整性说明。

结论

核心安全修复真实有效,并已针对真实 daemon 端到端验证:打开或下载次工作区产物不再读取主仓库文件。第 3 点(未受信任工作区失去预览/下载,且文案有误导)希望在合并前解决或明确接受;6.1 是低成本的健壮性改进。其余部分看起来没问题。

本次未验证:持久化定时任务的 workspaceId 路径(仅单元测试 + 代码阅读,没有实时 CRUD 运行),以及代码审查/文件审查预览(与上面已验证的产物路径共用同一 resolver 和 action 接口)。

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

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(web-shell): secondary artifact actions can target primary workspace

4 participants