Skip to content

feat(web-shell): add web previews with saved delivery history - #11276

Merged
wenshao merged 22 commits into
mainfrom
codex/web-shell-preview-history
Sep 11, 2026
Merged

feat(web-shell): add web previews with saved delivery history#11276
wenshao merged 22 commits into
mainfrom
codex/web-shell-preview-history

Conversation

@wenshao

@wenshao wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a Web preview panel for browser-reachable development URLs, with desktop/mobile widths, refresh, and external opening. Standalone Web Shell enables it for workspace sessions by default; embedded hosts can opt in. Live preview retains the existing workspace requirement; saved HTML versions can also open in standalone and Live sessions without a workspace. The selected URL and width are remembered per session and workspace. Published Artifact links can open in live preview; ordinary recorded links keep their metadata view and external action.

In managed ACP sessions with chat recording enabled, each new self-contained HTML Artifact delivery also saves an independent immutable version. Ordinary CLI publication and recording-disabled sessions do not create history files. Each saved-version card stays with the original message, so closing the panel, refreshing Web Shell, or restarting the daemon still opens the HTML delivered at that time. Later deliveries retain their own cards and timestamps while the existing latest publication URL keeps its current behavior.

Saved content is read through an authenticated, session-owner-scoped route that verifies the stored file and hash. Once a saved version has loaded, Refresh rebuilds it from in-memory HTML without another daemon request. Existing PDF attachments remain frameable in development and production. A failed history save keeps the successful publication available with a warning; transient preview reads offer retry instead of reporting data loss. Saved versions and existing HTML previews share an isolated viewer that allows inline interaction while blocking network access and self-navigation.

Why it's needed

A live development page is useful while iterating, but its URL changes content as the project changes. Keeping only a right-panel tab or the latest publication URL loses what was delivered in an earlier turn. This adds live iteration and a durable record of each HTML delivery in the conversation.

Reviewer Test Plan

How to verify

  1. Open Web preview in standalone Web Shell and enter a reachable HTTP/HTTPS development URL. Confirm desktop/mobile widths, refresh, external opening, and development-server updates work. Switch panel tabs and confirm the page remains mounted; reload the shell and confirm its entry URL and width return for the same session.
  2. Deliver two different self-contained HTML pages through Artifact using the same source path. Confirm both original messages have a Saved version card, and each opens its own content and saved timestamp. Inline controls should work; reopening starts from the delivered HTML rather than preserving transient interaction state.
  3. Close the panel, reload the shell, and restart the daemon with the same runtime. Confirm both cards still open their original versions, including after deleting the source and latest publication output.
  4. Remove or alter a saved file. Confirm its card reports the version unavailable without showing the current page. Requests from an unrelated session or a different runtime must not return the saved content.
  5. Try an HTML page that changes its own location in both the saved-version viewer and the existing HTML viewer. Confirm the browser blocks the navigation and the destination receives no request, while ordinary inline interaction still works. If a saved frame becomes blank, make further content reads unavailable and use Refresh; the same saved version should return without a new read.
  6. Open a PDF attachment in both development and the built Web Shell; it should remain visible.
  7. Fork a session with saved versions, then delete the parent before opening the child. The child must still open the original versions. Removing the final owner should reclaim the files. Failed persistence must preserve the content referenced by durable history; deletion in one workspace must not affect a different workspace with the same session ID. If snapshot storage is already missing, forking must still preserve the conversation and saved-page record, with a warning; other ownership errors must not commit a partial fork.

Evidence (Before & After)

Historical deliveries stay with their original messages. Both screenshots below were captured for this feature after restoring the same recorded session; each card opens its own saved HTML and supports inline interaction.

Original message cards remain visible while saved version 1 is open

Saved version 1 remains available after version 2 has been delivered.

The second historical card opens saved version 2 in the same session

Opening the second card shows version 2 without replacing version 1.

Live development preview. A real Vite page runs in the right panel alongside the conversation. Desktop and 390 px mobile views share the same interactive page.

Live development page in the desktop-width Web preview panel

The same development page at 390 px mobile width

Before: the installed qwen 0.23.0 right panel had no Web preview action Baseline right panel with Changes, Side task and Terminal actions
Scenario Before After
Publish v1 then v2 from the same source Global qwen 0.23.0 retained one stable descriptor; v2 replaced the published output. Two independent saved versions retain their original bytes, timestamps, and message associations.
Delete source/latest output and restart No saved-content route; HTTP 404. Both saved versions return their exact original 1,439-byte HTML through the actual daemon and open from their original cards.
Inline interaction and attempted navigation Broadening the host frame policy alone allowed the existing offline viewer to navigate. Actual daemon/browser checks for saved v1, saved v2, and generic HTML: working counters, blocked navigation, zero destination requests.

The current repair passes build, bundle, full typecheck, lint, 4,936 selected unit tests, 61 Chromium smoke scenarios and 6 focused preview browser scenarios. Real-daemon checks confirm deletion cleanup and retention for a never-loaded fork after parent deletion. Real-module checks cover persistence rejection, rewind, owner-runtime isolation, same-ID workspaces and managed tool registration. Two controlled mutation groups fail as expected when fork ownership or persistence protection is removed. Two consecutive full reverse audits and an independent review found no remaining Critical. The separate E2E report states the limits of each test method.

Tested on

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

Environment (optional)

macOS, Node.js 22.22.2, Chromium, and isolated local daemon runtimes. Real-daemon checks used deterministic recorded conversations and actual Artifact publication/storage without mocked HTTP responses or model requests. Temporary test servers were stopped after verification.

Risk & Scope

  • Main risk or tradeoff: live URLs require a broader host frame policy; trusted wrapper documents and opaque content frames preserve offline HTML isolation. Saved files stay local, and the existing default limit of 200 artifact records can evict older history entries. Reference ownership keeps shared fork content until the last session releases it. Legacy snapshots without ownership metadata, crash-before-recording orphans and failed-cleanup files are retained conservatively; there is no orphan collector.
  • Not validated / out of scope: capturing arbitrary running websites, restoring transient browser interaction state, automatic port forwarding, backfilling old deliveries, and Windows/Linux or WebKit verification. Embedded hosts must permit their intended frame origins and retain suitable ancestor restrictions. The existing heuristic HTML validator is unchanged. Some blocked navigation attempts can blank offline content; saved versions can be refreshed, while generic anchor/navigation handling remains a follow-up.
  • Breaking changes / migration notes: no migration required; existing latest publication URLs remain stable. Saved versions apply to new self-contained HTML Artifact deliveries. Moving a transcript without its runtime snapshot files makes those versions unavailable.

Linked Issues

None.

中文说明

本 PR 的改动

新增 Web 预览面板,支持浏览器可访问的开发页面 URL,提供桌面/手机宽度切换、刷新和外部打开。独立 Web Shell 的工作区会话默认启用,嵌入式宿主可以选择启用。实时预览沿用现有工作区要求;已保存的 HTML 版本也能在无工作区的 standalone 和 Live 会话中打开。所选 URL 和宽度按会话及工作区保存。Artifact 发布的链接可进入实时预览,普通记录链接保留详情查看和外部打开操作。

在开启会话记录的受管 ACP 会话中,每次通过 Artifact 新交付的自包含 HTML 还会保存一个独立、不可变的版本。普通 CLI 发布和关闭记录的会话不创建历史文件。卡片保留在原始消息中,因此关闭面板、刷新 Web Shell 或重启 daemon 后,仍能打开当时交付的 HTML。后续交付各自保留卡片和时间,现有最新发布 URL 的行为保持不变。

已保存内容通过经过身份验证、按会话所属运行时限定的接口读取,并校验存储文件和哈希。已加载历史版本的刷新直接使用内存中的 HTML,不再请求 daemon。开发与生产环境继续支持 PDF 附件的 iframe 展示。历史版本写入失败时,已成功发布的内容仍可用,并显示提示;临时读取失败会提供重试,不会误报数据丢失。已保存版本与现有 HTML 预览共用隔离查看器,允许内联交互,同时阻止网络访问和页面自身导航。

为什么需要

实时开发页面适合迭代,但随着项目变化,同一 URL 的内容也会改变。如果只保留右侧面板标签或最新发布 URL,就会丢失先前轮次交付的内容。本改动同时提供实时迭代能力,并在会话中持久记录每次 HTML 交付。

审阅者测试计划

如何验证

  1. 在独立 Web Shell 中打开 Web 预览,输入可访问的 HTTP/HTTPS 开发 URL。确认桌面/手机宽度切换、刷新、外部打开及开发服务器更新正常。切换面板标签,确认页面保持挂载;刷新 Shell,确认同一会话恢复入口 URL 和宽度。
  2. 使用同一源路径,通过 Artifact 交付两个不同的自包含 HTML 页面。确认两条原始消息都有“已保存版本”卡片,各自打开对应内容和保存时间。内联控件应正常工作;重新打开从交付的 HTML 开始,不保留临时交互状态。
  3. 关闭面板、刷新 Shell,并使用相同运行时重启 daemon。确认两张卡片仍能打开原始版本,删除源文件和最新发布输出后也一样。
  4. 删除或修改已保存文件。确认对应卡片提示版本不可用,不显示当前页面。来自无关会话或其他运行时的请求不得返回该版本内容。
  5. 分别在已保存版本查看器和现有 HTML 查看器中,尝试让 HTML 页面修改自身地址。确认浏览器阻止导航、目标地址没有收到请求,同时普通内联交互仍然正常。若历史页面变空白,让后续内容读取不可用后点击刷新,应恢复同一版本且不新增读取。
  6. 分别在开发环境和打包后的 Web Shell 打开 PDF 附件,确认可以显示。
  7. 对包含历史版本的会话创建分支,在首次打开子会话前删除父会话。子会话应仍能打开原始版本;删除最后一个持有者后才回收文件。持久化失败时,已有记录引用的内容应保留;删除一个工作区的会话不能影响其他工作区中 ID 相同的会话。 如果快照存储已经缺失,分叉仍应保留对话和历史网页记录并发出警告;其他引用错误不得提交不完整分支。

验证证据(改动前后)

历史交付保留在各自的原始消息中。 下方两张截图来自本功能构建恢复后的同一个会话;每张卡片打开各自保存的 HTML,并支持内联交互。

原始消息卡片保持可见,右侧打开已保存的版本 1

交付版本 2 后,已保存的版本 1 仍然可以打开。

同一会话中,第二张历史卡片打开已保存的版本 2

打开第二张卡片显示版本 2,不会覆盖版本 1。

实时开发预览。 真实 Vite 页面在会话旁边的右侧面板中运行,桌面与 390 px 手机宽度共用同一个可交互页面。

桌面宽度的 Web 预览面板显示实时开发页面 同一开发页面切换到 390 px 手机宽度
改动前:已安装的 qwen 0.23.0 右侧面板没有 Web 预览入口 基线右侧面板仅有 Changes、Side task 和 Terminal 入口
场景 改动前 改动后
从同一源文件先发布 v1,再发布 v2 全局 qwen 0.23.0 只保留一个稳定描述符,v2 覆盖发布输出。 两个独立的已保存版本保留原始字节、时间和消息关联。
删除源文件/最新输出后重启 没有已保存内容接口,返回 HTTP 404。 实际 daemon 返回两个版本各自原始的 1,439 字节 HTML,均可从原始卡片打开。
内联交互与导航尝试 仅放宽宿主 frame 策略会允许现有离线查看器发起导航。 实际 daemon/浏览器对已保存 v1、已保存 v2 和普通 HTML 的检查结果:计数器正常,导航被阻止,目标请求数为零。

当前修复已通过构建、打包、全量类型检查、lint、4,936 个相关单元测试、61 个 Chromium smoke 场景和 6 个预览浏览器场景。真实 daemon 验证了删除清理,以及父会话删除后尚未加载的分支仍保留内容。真实模块验证覆盖持久化失败、回退、所属运行目录隔离、同 ID 跨工作区和受管工具注册。两组受控变异在移除分支引用或持久化保护后按预期失败。连续两轮完整反向审计及独立审查未发现遗留 Critical;独立 E2E 报告明确各验证方式的范围。

已测试平台

操作系统 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未测试
🐧 Linux ⚠️ 未测试

环境(可选)

macOS、Node.js 22.22.2、Chromium 及隔离的本地 daemon 运行时。实际 daemon 检查使用确定性的会话记录和真实 Artifact 发布/存储,没有模拟 HTTP 响应,也没有发送模型请求。验证后已停止临时测试服务器。

风险与范围

  • 主要风险或取舍:实时 URL 需要放宽宿主 frame 策略;可信包装文档与独立不透明源内容 frame 保持离线 HTML 隔离。已保存文件保留在本地,现有默认 200 条产物记录上限可能淘汰较早的历史条目。分支共享内容会保留到最后一个会话释放引用。没有引用信息的旧快照、记录落盘前崩溃或清理失败留下的文件会保守保留,本次没有加入孤儿文件清理器。
  • 未验证或不在范围内:捕获任意运行中的网站、恢复临时浏览器交互状态、自动端口转发、补存旧交付,以及 Windows/Linux 或 WebKit 验证。嵌入式宿主必须允许预期的 frame 来源,并保留合适的祖先嵌入限制。现有启发式 HTML 校验器保持不变。部分被阻止的导航可能让离线内容变为空白;历史版本可以刷新恢复,通用锚点及导航处理另行跟进。
  • 破坏性改动或迁移说明:无需迁移,现有最新发布 URL 保持稳定。已保存版本适用于新交付的自包含 HTML Artifact。迁移会话记录时,如果没有同时保留运行时快照文件,对应版本将不可用。

关联 Issue

无。

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

E2E verification report

Verified on macOS with Node.js 22.22.2 and Chromium. These results apply to the submitted implementation; Windows, Linux, and WebKit were not verified locally.

Baseline

The globally installed qwen 0.23.0 was exercised with the real Artifact publisher. Publishing v1 and then v2 from the same source retained one stable descriptor and replaced the output. There was no independent saved version, and the proposed saved-content route returned HTTP 404.

Actual publication, persistence, daemon, and browser

The test-script fallback invoked the actual built Artifact publisher, trusted bridge ingestion, artifact store, conversation recorder, JSONL restore, daemon routes, and Web Shell. The conversation was a deterministic authored fixture, not model-generated output. No HTTP responses were mocked and no model requests were sent.

Check Result
Publish v1 and v2 from the same source Distinct saved files and records; stable latest publication URL preserved.
Delete the source and latest output Both saved responses retain their exact original 1,439-byte HTML and SHA-256.
Restart the daemon with the same runtime Both original timestamps, tool-call associations, and HTML versions survive restoration.
Open v1, v2, then v1 from their original messages Correct original headings and working inline counters in each version.
Close/reopen the panel and reload with v2 active Both cards remain; v2 is restored, with transient counter state reset.
Request an unregistered artifact or use a different real session HTTP 404; no content returned.
Remove, alter, or replace saved content with an outside symlink containing the same bytes HTTP 404 and unavailable UI; no latest/source fallback.
Read valid saved content HTTP 200, HTML attachment, nosniff, and private/no-store headers.

After the final viewer isolation correction, a fresh actual daemon run verified saved v1, saved v2, and a generic HTML artifact. Each rendered its expected heading and working counter. Each then attempted self-navigation to a separate real HTTP listener: all three were blocked by frame-src 'none', the listener received zero requests, and no frame reached the target. Reloading with v2 active retained v2 and reset its counter. This final run also deleted source/latest output before serving the saved versions.

Independent browser review reproduced the policy regression before the correction and confirmed the fix: the old narrow host policy blocked navigation, the broadened host policy with an unwrapped offline viewer allowed it, and the final isolated viewer blocked it. Parent-policy tampering and malicious title probes were also blocked. The review finding was closed with no new findings.

All temporary daemons and HTTP listeners were stopped; direct connection probes confirmed their ports were closed. Existing user daemons were left running.

Automated coverage

Validation Result
Full build, typecheck, and bundle Passed after the final source correction.
ESLint on changed/new TypeScript files; diff whitespace check Passed.
Core persistence, Artifact publication, HTML validation, snapshot storage 104 unit tests passed.
Bridge artifact store 150 unit tests passed.
Daemon multi-workspace routes and host framing policy 157 unit tests passed.
SDK daemon client 416 unit tests passed.
Web Shell App, artifact panel, live URL helper 825 unit tests passed before the final shared-wrapper correction; affected wrapper/component tests rerun below.
Final shared viewer helper, artifact panel, saved viewer 100 unit tests passed across focused runs after the correction.
Turn output cards 18 unit tests passed.
Chromium live-preview browser suite 5 tests passed.
Chromium saved-history and offline-navigation browser suite 2 tests passed after the final correction.

Counts are per focused run and overlap; they are not a unique aggregate. Package unit tests ran from their respective package directories. Browser history suites use daemon API fixtures and do not establish real persistence by themselves; the real-daemon checks above cover that separately. Cross-runtime ownership, matching-file primary-runtime fallback rejection, descriptor validation, and size limits are covered by focused tests, not claimed as additional real-daemon E2E observations.

Scope

Saved versions cover newly delivered self-contained HTML, not arbitrary website capture or transient browser state. The existing default 200-record artifact retention limit remains. Snapshot bytes remain local to the owning runtime.

中文摘要:真实发布、持久化、重启恢复、原消息版本打开、异常文件拒绝及最终浏览器隔离均已通过。两份已保存版本与普通 HTML 均可正常交互,导航攻击的目标请求数为零。上表明确区分真实 daemon 验证与模拟 API 的浏览器测试;未验证 Windows、Linux 和 WebKit。

@wenshao
wenshao marked this pull request as ready for review September 7, 2026 07:12
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Qwen Triage ended earlyview run. It stopped before finishing; check the run log.

⚠️ Qwen Triage 提前结束 —— 查看运行。未跑完,请查看运行日志。

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

health-deep-with-session

field PR base (before) this PR (after)
activeWorkStaleMs 6 7

Qwen Code · serve A/B

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head e180a24. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 8 render-shaping files:

  • packages/web-shell/client/App.tsx
  • packages/web-shell/client/components/artifacts/ArtifactPanel.tsx
  • packages/web-shell/client/components/artifacts/TurnOutputs.tsx
  • packages/web-shell/client/components/preview/SavedWebPreview.tsx
  • packages/web-shell/client/components/preview/WebPreviewPanel.tsx
  • packages/web-shell/client/customization.tsx
  • packages/web-shell/client/i18n.tsx
  • packages/web-shell/client/main.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR — this is a substantial one, so here is the gate pass before the code review.

Template ✓ — every required section is present, including the before/after evidence table and the Chinese translation.

Problem — this is a capability gap rather than a bug, and it is evidenced rather than theoretical: the right panel can render offline HTML but cannot open a running dev server, and the "latest publication URL" model genuinely loses what an earlier turn delivered. The before/after table names a concrete failure (publish v1 then v2 from one source, and v1 is gone). No reproduction is needed for a feature of this shape, so this passes.

Direction — aligned. The right panel already carries review / sideTask / terminal tabs and an artifact panel that sandboxes HTML, so a preview tab and per-delivery version history continue an existing trajectory rather than opening a new one. Upstream signal: Anthropic's CHANGELOG has an active Artifact surface (several recent artifact entries, including one about reading an artifact that isn't yours now requiring confirmation), so owner-scoped artifact reads are a live concern in the same area. No direct "web preview panel" reference, but the area is clearly relevant.

One thing that does need a human owner rather than a bot verdict: this touches a process-global security policy and public contract — the shell document's frame-src, a new authenticated daemon route, a new DaemonClient method, and a new member on the exported WebShellRightPanelItem union. Those are exactly the surfaces the gate escalates instead of auto-approving. Flagged for @yiliang114 (who owns packages/core/src/tools/ + services/ and is already assigned); more detail in the code review.

Size — core paths are touched (packages/core/src/tools/artifact/**, packages/core/src/services/**) and the change spans five packages, so the breakdown matters:

  • production logic: 922 lines across 21 files
  • tests: 1446 lines across 16 files
  • generated/schema: 0
  • docs (.md): 235 lines across 3 files

The author is a repo admin, so this is maintainer-authored and exempt from the two-tier core gate; 922 production lines is also below the 1000-line large-PR advisory. Reporting the numbers for awareness only — nothing here blocks on size.

Approach — the mechanics look right, but I would push on scope. This is two separable features plus a third ride-along:

  1. The live URL preview is nearly all client-side — a tab, an address field, viewport toggle, and one CSP line. Small.
  2. The saved-version history is what pulls in everything else: core Artifact tool changes, snapshot storage and hash verification, persistence/fork remapping, an acp-bridge trust relaxation, a new daemon route, and a new SDK method.
  3. The tightening of validateSelfContained in packages/core/src/tools/artifact/html.ts is a user-visible behaviour change to a core tool that affects every Artifact publication, not just previews. It is not mentioned under "Breaking changes / migration notes", which currently says "no migration required". I think this one deserves its own discussion — see the code review for the concrete cases I believe it now rejects wrongly.

So: if you cut 80% of the scope, the live preview alone would already deliver most of the "iterate on a running page" value at a fraction of the surface. The saved-version half is the more interesting feature but also the one carrying the security and contract weight. Would landing them as two PRs be feasible? Not a blocker — asking because it would make both much easier to review and to revert.

Risk — the revert-history path check found no matches, so no elevated-risk-path signal. The real risk is the one the PR names itself: frame-src goes from a pinned loopback allowlist to http: https: for every consumer of the served shell document, and the helper that implemented the pinning (plus its tests) is deleted. That is a genuine reduction in defense-in-depth and it is load-bearing for the feature. Covered in the code review.

Moving on to code review. 🔍

中文说明

感谢贡献!这个 PR 体量不小,先给出门禁结论,再看代码。

模板 ✓ —— 必填章节齐全,包含 before/after 证据表和中文说明。

问题 —— 这是能力缺口而非 bug,而且有实证、不是理论性加固:右侧面板能渲染离线 HTML,但打不开正在运行的开发服务器;「最新发布 URL」的模型确实会丢失早前轮次交付的内容。before/after 表格点名了一个具体失效场景(同一源发布 v1 再发布 v2,v1 就没了)。这类特性不需要复现步骤,此项通过。

方向 —— 对齐。右侧面板已经有 review / sideTask / terminal 标签页,产物面板也已对 HTML 做沙箱渲染,所以预览标签页与按次保存的历史版本是延续既有路线,而不是新开一条。上游信号:Anthropic 的 CHANGELOG 中 Artifact 是活跃面(近期多条 artifact 相关条目,其中一条正是关于读取不属于你的 artifact 现在需要确认),说明「按所有者限定 artifact 读取」在同一领域是当前的关注点。没有直接的「网页预览面板」条目,但该领域确实相关。

有一点需要人来定夺、而不是由机器人下结论:本 PR 触及进程级安全策略公共契约——shell 文档的 frame-src、一个新的需鉴权 daemon 路由、一个新的 DaemonClient 方法,以及导出的 WebShellRightPanelItem 联合类型新增成员。这些正是门禁选择升级、而非自动批准的面。已提请 @yiliang114 关注(他负责 packages/core/src/tools/services/,且已被指派);细节见代码审查。

规模 —— 触及核心路径(packages/core/src/tools/artifact/**packages/core/src/services/**),且跨五个 package,因此拆分数据有意义:

  • 生产逻辑:922 行,21 个文件
  • 测试:1446 行,16 个文件
  • 生成/schema:0
  • 文档(.md):235 行,3 个文件

作者是仓库 admin,属维护者自建 PR,豁免两级核心门禁;922 行生产代码也低于 1000 行的大 PR 提示线。此处仅为知会而列出数字——不因体量而阻塞。

方案 —— 机制看起来是对的,但我想就范围提个问题。这实际上是两个可拆分的特性,外加一个搭车改动:

  1. 实时 URL 预览几乎全在客户端——一个标签页、一个地址栏、视口切换,加一行 CSP。很小。
  2. 保存的历史版本才是把其余部分都拉进来的原因:核心 Artifact 工具改动、快照存储与哈希校验、持久化/fork 重映射、acp-bridge 的信任放宽、新 daemon 路由、新 SDK 方法。
  3. packages/core/src/tools/artifact/html.tsvalidateSelfContained 的收紧,是对一个核心工具的用户可见行为变更,影响所有 Artifact 发布,而不只是预览。它没有出现在「Breaking changes / migration notes」里,而那一节目前写的是「no migration required」。我认为这一条值得单独讨论——具体我认为会被误拒的用例见代码审查。

所以:如果砍掉 80% 的范围,单是实时预览就已经能以极小的面积交付「在运行中的页面上迭代」的大部分价值。保存历史版本这一半更有意思,但也是承载安全与契约权重的那一半。拆成两个 PR 是否可行?这不是阻塞项——之所以问,是因为拆开后两者都更容易审查、也更容易回滚。

风险 —— 回滚历史的路径检查无命中,因此没有高风险路径信号。真正的风险正是 PR 自己点出的那条:frame-src 从按 loopback 端口收窄的白名单变成对所有使用被服务 shell 文档的消费者都生效的 http: https:,并且实现该收窄的辅助函数(连同其测试)被删除。这是防御纵深的实际削弱,而且对该特性是必需的(load-bearing)。代码审查中会展开。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

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

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@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 Sep 7, 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. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

Not explored to full depth (tool budget reached): "agent reverse-audit (round 3)": did not execute the spec (dev server + browser launch) to observe the frame-ancestors violation's source frame at runtime; the attribution above is establishe…; "agent reverse-audit (round 1)": the README block's live-preview behavioural claims (hot reload, refresh returning to the entry URL, 390px/desktop toggle, cross-origin navigation not synced to …; "agent reverse-audit (round 2)": whether the daemon serves any attacker-influenced HTML at its own origin, and therefore whether framing a workspace.baseUrl origin-alias ( http://127.0.0.1:PO…; "agent reverse-audit (round 1)": did not re-execute the file under CI settings ( CI=1 → non-reused webServer, retries: 2 , forbidOnly ) or concurrently with the sibling new spec web-shell.s….

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

中文说明

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

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

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 3)"did not execute the spec (dev server + browser launch) to observe the frame-ancestors violation's source frame at runtime; the attribution above is establishe…"agent reverse-audit (round 1)"the README block's live-preview behavioural claims (hot reload, refresh returning to the entry URL, 390px/desktop toggle, cross-origin navigation not synced to …"agent reverse-audit (round 2)"whether the daemon serves any attacker-influenced HTML at its own origin, and therefore whether framing a workspace.baseUrl origin-alias ( http://127.0.0.1:PO…"agent reverse-audit (round 1)"did not re-execute the file under CI settings ( CI=1 → non-reused webServer, retries: 2 , forbidOnly ) or concurrently with the sibling new spec web-shell.s…

未审查:反向审计——评审时间预算不足,未能开始第 4 轮。

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

Comment thread packages/core/src/tools/artifact/html.ts Outdated
Comment thread packages/core/src/tools/artifact/artifact-tool.ts Outdated
Comment thread packages/web-shell/client/components/preview/SavedWebPreview.tsx
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/components/artifacts/artifactUtils.ts
Comment thread packages/web-shell/client/e2e/web-shell.live-preview.spec.ts
Comment thread packages/web-shell/client/components/preview/web-preview.test.ts
Comment thread packages/web-shell/client/e2e/web-shell.live-preview.spec.ts Outdated
Comment thread packages/web-shell/client/e2e/web-shell.live-preview.spec.ts
Comment thread packages/web-shell/client/e2e/web-shell.live-preview.spec.ts
@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 2 finishedview run. See this round's report below.

中文说明

AutoFix 第 2 轮已完成 —— 查看运行。本轮报告见下方。

@wenshao wenshao removed the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Sep 7, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix crashed before it could evaluate the feedback (attempt 1/100) — it will retry on the next scan.

AutoFix failed before producing a verified commit (the run crashed or timed out before it could explain why).

中文说明

🤖 AutoFix 在评估反馈之前崩溃(第 1/100 次尝试)—— 将在下次扫描时重试。

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


🧠 Handled by Qwen Code · model/模型 kimi-k3

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Review repair and validation for 18f87ca (merged main at 63578c7).

The three Critical reports were reproduced before repair and rechecked against actual source modules afterward: all ten HTML compatibility cases pass, a successful publication survives a failed history write through the real transcript → bridge → artifact store, and foreign-session preview reads omit the active session clientId while local reads retain it. The additional ArtifactPanel integration test checks source-session propagation into the SDK call.

Verification Result
Root build, typecheck, bundle, full ESLint, pre-commit checks Passed
Core Artifact/persistence tests 104 passed
ACP artifact store tests 150 passed
SDK client tests 416 passed
CLI owner routing, CSP and telemetry tests 225 passed
Web Shell App/preview/artifact tests 958 passed
Full preview browser specs, CI mode and non-reused Vite 8 passed
Actual CI smoke selection, full development response CSP 52 passed
Four isolated regression mutations with intact controls All four detected

The browser tests verify real Vite modules, storage, HMR, widths, frame retention, referrer suppression, blocked popups, direct redirect/script navigation, explicit descendant anti-framing, and immutable-card restore with inline controls. Offline interaction/data image/data WAV checks run under the full response CSP. Session/artifact HTTP responses in these Playwright scenarios are fixtures. The publication/bridge reproductions separately use real implementations; this is not claimed as a complete real-daemon side-task delivery E2E. Earlier real-daemon snapshot/restart evidence and PR screenshots remain available above.

The source tree was not committed until two consecutive complete reverse audits were clean after all fixes. The commit hook left the audited tree unchanged (tree a28b1df8ffc8ef7349634341ce5d73521a9b6ae2). An independent reviewer also found no remaining Critical. Each review thread is answered individually; R1-5 (tiny escaper extraction), R1-19 (generic anchor/navigation behavior) and R1-27 (runtime migration/classification contract) remain explicitly discussed rather than falsely marked implemented. The unrelated validator expansion was removed, keeping main’s existing validation contract.

Local platform: macOS, Node 22.22.2, Chromium. The post-fix source scripts and these browser scenarios do not rely on model-generated responses. The conflicting AutoFix takeover was paused before work to prevent competing unaudited pushes; the new GitHub CI run is separate from these local results.

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed the CI route drift guard failure in c575fdf. The new saved-page content endpoint increases the registered route inventory from 69 to 70. The previous update corrected the telemetry behavior test but missed the separate registration drift guard; this commit updates that remaining count and preserves the exact route-to-catalog equality assertion.

Local validation: reproduced the original failure on 18f87ca before changing the test, then verified both telemetry test files pass (69 tests). Root build, typecheck, bundle, focused ESLint and Prettier all pass. The rebuilt CLI starts successfully. Two consecutive reverse-audit passes found no further issue in this correction, and no production code changed.

Remote validation is now complete for c575fdf: Qwen Code CI passed, including Linux full unit tests, static checks, no-AK integration tests and the Web Shell browser smoke gate. The original route drift test passed in CI; the CLI suite finished with 28,989 passed and 90 skipped. Web-shell Visuals, Serve A/B, SDK Java and tui-parity also passed.

@wenshao

wenshao commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /review

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is running in workflow run. A command-triggered review is not listed under the checks of this PR; the result is posted here as a review when it finishes.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": confirming whether any shipped flow forks or replays a session journal under a runtime base dir that differs from the one that wrote the snapshot bytes (write s…; "agent 1b": none — the walk completed within budget.; chunk 4: I did not execute packages/core vitest for artifact-snapshots.test.ts / artifact-tool.test.ts / html.test.ts ; I verified the new html.test.ts cases by…; "agent reverse-audit (round 4)": none of my checks were cut short — but two verifications I chose not to run are owed scope rather than passes: isTrustedArtifactToolUpdate 's per-batch vs per-…; "agent reverse-audit (round 4)": whether the artifact *list* scope and SavedWebPreview 's clientId-narrowed content read can diverge (list-wide via App.tsx:5388 / 5515 calling listSessionAr…, and 2 more.

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

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

  • docs/design/web-shell/web-shell-preview-snapshots.md:49 — [review] The dedup this doc specifies removes the…
  • packages/acp-bridge/src/sessionArtifacts.test.ts:6217 — [review] The restore-trust gate this diff adds depends on the tool…
  • packages/cli/src/serve/routes/session.ts:6375 — [review] The header-absent branch (clientId === undefined) is the…
  • packages/cli/src/serve/web-shell-static.test.ts:24 — [review] The rewritten CSP test is the repo's only oracle for…
  • packages/core/src/services/session-artifact-persistence.ts:116 — [review] Two of the new whitelist's guards - kind !== 'html' and…
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:67 — [review] The 'oversized' test never reaches the size guard it…
  • packages/core/src/tools/artifact/artifact-snapshots.ts:25 — [review] The snapshot directories are created with no mode, so they…
  • packages/core/src/tools/artifact/artifact-snapshots.ts:30 — [review] The mode: 0o600 on the snapshot file is the only thing…
  • packages/core/src/tools/artifact/artifact-snapshots.ts:59 — [review] The descriptor persists an absolute file:// URL bound to…
  • packages/core/src/tools/artifact/artifact-tool.ts:262 — [review] Nothing in the suite pins that the saved snapshot is the…
  • packages/web-shell/client/App.tsx:4620 — [review] Manually opened preview tabs are all titled…
  • packages/web-shell/client/App.tsx:6343 — [review] Re-clicking an artifact card replaces an already-open…
  • packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:3362 — [review] artifactPreviewDocument - a full DOM parse, a serialize,…
  • packages/web-shell/client/components/artifacts/TurnOutputs.tsx:464 — [review] The saved-version card label in the transcript has no test…
  • packages/web-shell/client/components/artifacts/artifactUtils.test.ts:328 — [review] The new test for artifactPreviewDocument never exercises…
  • packages/web-shell/client/components/artifacts/turnOutputSelectors.ts:76 — [review] The saved/latest card dedup compares the raw publishedUrl…
  • packages/web-shell/client/components/artifacts/turnOutputSelectors.ts:84 — [review] The new de-duplication pass mutates the map that…
  • packages/web-shell/client/components/preview/web-preview.test.ts:24 — [review] The credential clause of the preview allow-list guard…
  • packages/web-shell/client/e2e/web-shell.saved-preview.spec.ts:66 — [review] The @smoke spec builds its sandbox fixture in the…
中文说明

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

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

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"confirming whether any shipped flow forks or replays a session journal under a runtime base dir that differs from the one that wrote the snapshot bytes (write s…"agent 1b"none — the walk completed within budget.;chunk 4:I did not execute packages/core vitest for artifact-snapshots.test.ts / artifact-tool.test.ts / html.test.ts ; I verified the new html.test.ts cases by…"agent reverse-audit (round 4)"none of my checks were cut short — but two verifications I chose not to run are owed scope rather than passes: isTrustedArtifactToolUpdate 's per-batch vs per-…"agent reverse-audit (round 4)"whether the artifact *list* scope and SavedWebPreview 's clientId-narrowed content read can diverge (list-wide via App.tsx:5388 / 5515 calling listSessionAr…,另有 2 条。

未审查:反向审计——评审时间预算不足,未能开始第 5 轮。

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

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

Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/components/preview/web-preview.ts
Comment thread packages/web-shell/client/e2e/web-shell.live-preview.spec.ts Outdated
Comment thread packages/web-shell/client/components/preview/web-preview.test.ts
Comment thread packages/web-shell/vite.config.ts
Comment thread packages/web-shell/client/e2e/web-shell.live-preview.spec.ts

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

7 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • D8-2 retain-on-upsert fault isolation, packages/acp-bridge/src/sessionArtifacts.ts:486 — already reported (round-7 deferral at the same line, review 5159878427)
  • D8-3 per-runtime snapshot base-dir wiring unpinned, packages/acp-bridge/src/bridge.ts:7132 — already reported (round-7 deferral at packages/cli/src/serve/run-qwen-serve.ts:6123, review 5159878427)
  • D8-8 O_NOFOLLOW flag collapse on Windows, packages/core/src/tools/artifact/artifact-snapshots.ts:92 — already reported (round-7 deferral at artifact-snapshots.ts:97, review 5159878427)
  • D8-14 auto-open suppression assertion vacuous on CI, packages/core/src/config/config.test.ts:6040 — already reported (round-7 deferral at the same line, review 5159878427)
  • D8-15 English-only design docs, docs/design/web-shell/web-shell-live-preview.md:1 — already reported (round-7 deferral at the same path, review 5159878427)
  • D8-16 transient I/O collapsed into the terminal 404 class, packages/cli/src/serve/routes/session.ts:6346 — already reported (round-7 deferral at packages/core/src/tools/artifact/artifact-snapshots.ts:82, review 5159878427)
  • D8-25 snapshot write ignores the abort signal, packages/core/src/tools/artifact/artifact-tool.ts:264 — already reported (round-7 deferral at the same line, review 5159878427)

Not reviewed: reverse audit — stopped after the round 1/2 convergence pair: both members reported findings, so the pair is one reporting round and the two-consecutive-dry-rounds criterion was never reached; rounds 3-5 of this plan's 5-round cap did not run. The pair's 26 findings WERE verified in the repair round (10 confirmed high, 5 confirmed low, 11 rejected).

Not reviewed: verifier-incidental findings inc1 (routes/session.ts:6341) and inc2 (local-publisher.ts:36-38) — reported by round-2 verifiers and never ruled on by a verification round, so they stay unverified and terminal-only.

Not reviewed: issue-fidelity — closing-issue discovery unavailable on this runner (gh 2.45.0; the subcommand requires >= 2.72.0 for closingIssuesReferences), so the linked-issue set is UNKNOWN rather than empty; the PR declares no linked issue and the motivating-incident replay did run.

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

Not reviewed: build-and-test — "Test (windows-latest, Node 22.x)" was skipped in CI, so no platform exercised the Windows-only paths; the O_NOFOLLOW finding was verified against a model of win32 constants, not win32.

Not reviewed: browser e2e — packages/web-shell/client/e2e/web-shell.live-preview.spec.ts, web-shell.pdf-preview.spec.ts and web-shell.saved-preview.spec.ts are excluded from the vitest run and no browser was available to this reviewer, so the PR's browser-level containment evidence was not re-executed as a suite (individual verifier probes did drive real Chromium for three claims).

Not reviewed: test-efficacy probe — all 25 probed files returned inconclusive (reason: no-output, from the vitest global-setup prerequisite guard), so no mutant or hunk survivor was measured and the harness control never ran; the mutation-coverage conclusions in this review come from hand-built verifier probes instead.

Not reviewed: did not verify whether create-sub-session.ts:1131's unpinned new SessionService(boundWorkspace).removeSession(...) runs inside a runWithWorkspaceRuntimeStorage scope at its call site — if it does not, a sub-session deletion resolves the primary runtime and silently no-ops the new snapshot release — stopped at the agent tool budget.

Not reviewed: verifying that config.getSessionId() (the snapshot reference key written at artifact-tool.ts:270) cannot diverge from the SessionArtifactStore's fixed this.sessionId used by every retain/release call — Config.startNewSession() reassigns it — stopped at the agent tool budget.

Not reviewed: whether toolName on a tool-recorded artifact can be caller-supplied through record_artifact params, which decides whether a model-driven record_artifact call can mint a full getWebPreviewSnapshotId descriptor and drive retain/delete against an arbitrary snapshot id — stopped at the agent tool budget.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 2)": determining the numeric limit passed to extractCappedArtifactInputs for tool_call_update artifacts (traced the parameter to bridgeClient.ts:443 / :3334 …; "agent reverse-audit (round 2)": verifying that config.getSessionId() (the snapshot reference key written at artifact-tool.ts:270 ) cannot diverge from the SessionArtifactStore 's fixed th…; "agent reverse-audit (round 2)": running packages/core/src/tools/artifact/html.test.ts to confirm the ten new characterization cases pass — I hand-evaluated each against the unchanged valida…; "agent reverse-audit (round 1)": Firefox behaviour of connect-src 'self' against same-origin ws:// — the installed firefox-1532 build would not launch under this tree's playwright-core …; "agent reverse-audit (round 2)": did not confirm referential stability of useWorkspace().client in daemon-react-sdk (it sits in the effect dep array; I inferred stability from the pre-exist…, and 6 more.

⚠️ 3 finding(s) still carried the — [unverified] tag when the loop ended — the verifier never ruled on them, and they are not confirmed.

Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/core/src/services/sessionService.ts:4130 — [probe] Critical [fails-closed] [new-surface] forkSession aborts with a raw ENOENT once a durable snapshot descriptor's bytes are missing, permanently unbranching the session
  • packages/acp-bridge/src/sessionArtifacts.test.ts:6594 — [probe] D8-4 the runtime-capture test passes runtimeBaseDir explicitly, so the construction-time fallback it is named after is never reached
  • packages/core/src/services/sessionService.ts:3232 — [probe] D8-5 the snapshot collection scan swallows stream/IO failures with no diagnostic, so the resulting permanent leak is invisible
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:263 — [probe] D8-6 the lease-loss test never asserts the reference directory survived, so the pre-unlink ownership recheck is unpinned
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:78 — [probe] D8-7 the oversize case cannot fail on the size guard it names, because truncate destroys the bytes so the sha256 mismatch rejects first
  • packages/acp-bridge/src/sessionArtifacts.ts:565 — [probe] D8-9 the droppedArtifacts operand of the new reclamation call has no test, so removing it ships green
  • packages/core/src/services/sessionService.test.ts:5961 — [probe] D8-10 the fork-rollback test cannot discriminate operation-scoped from session-scoped release despite its name
  • packages/web-shell/client/App.tsx:6485 — [probe] D8-11 no test pins sourceSessionId propagation onto the artifact tab; the App-level test is masked by an identical mock session id
  • packages/web-shell/client/components/preview/SavedWebPreview.tsx:68 — [probe] D8-12 the result?.key === key staleness guard is load-bearing but no test goes red when it is removed
  • packages/web-shell/client/e2e/web-shell.saved-preview.spec.ts:161 — [probe] D8-13 the route handler can throw before fulfilling, turning a clear assertion failure into an unexplained 60s timeout
  • packages/core/src/services/session-artifact-persistence.test.ts:115 — [probe] D8-17 three getWebPreviewSnapshotId rejection conditions are unpinned; all four mutations survive every suite
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:195 — [probe] D8-18 the session-scoped retain tolerance production depends on is never asserted anywhere in the suite
  • packages/web-shell/client/App.test.tsx:3343 — [probe] D8-19 the only open/persist/reload test asserts nothing about the URL that survived the round trip
  • packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:3390 — [probe] D8-20 the preview document is rebuilt inline on every render while the sibling call site added in the same PR memoizes it
  • packages/web-shell/vite.config.ts:93 — [probe] D8-21 the dev CSP duplicates ten shipped directives with nothing pinning the copy to the builder it names
  • packages/web-shell/client/App.tsx:5967 — [probe] D8-22 restored preview tabs mount eagerly while the terminal case two branches below deliberately defers
  • packages/web-shell/client/e2e/web-shell.live-preview.spec.ts:288 — [probe] D8-23 the 'preserves the working preview' assertion is load-invariant because the fixture rewrites the value on every load
  • packages/web-shell/client/e2e/web-shell.live-preview.spec.ts:377 — [probe] D8-24 the recorded-link fixture fails two independent predicate arms, so removing either leaves the suite green

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

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

本轮确认的 7 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):reverse audit — stopped after the round 1/2 convergence pair: both members reported findings, so the pair is one reporting round and the two-consecutive-dry-rounds criterion was never reached; rounds 3-5 of this plan's 5-round cap did not run. The pair's 26 findings WERE verified in the repair round (10 confirmed high, 5 confirmed low, 11 rejected).

未审查(原文为英文):verifier-incidental findings inc1 (routes/session.ts:6341) and inc2 (local-publisher.ts:36-38) — reported by round-2 verifiers and never ruled on by a verification round, so they stay unverified and terminal-only.

未审查(原文为英文):issue-fidelity — closing-issue discovery unavailable on this runner (gh 2.45.0; the subcommand requires >= 2.72.0 for closingIssuesReferences), so the linked-issue set is UNKNOWN rather than empty; the PR declares no linked issue and the motivating-incident replay did run.

未审查(原文为英文):build-and-test — "Integration Tests (CLI, No Sandbox)" was skipped in CI and its suite did not run locally.

未审查(原文为英文):build-and-test — "Test (windows-latest, Node 22.x)" was skipped in CI, so no platform exercised the Windows-only paths; the O_NOFOLLOW finding was verified against a model of win32 constants, not win32.

未审查(原文为英文):browser e2e — packages/web-shell/client/e2e/web-shell.live-preview.spec.ts, web-shell.pdf-preview.spec.ts and web-shell.saved-preview.spec.ts are excluded from the vitest run and no browser was available to this reviewer, so the PR's browser-level containment evidence was not re-executed as a suite (individual verifier probes did drive real Chromium for three claims).

未审查(原文为英文):test-efficacy probe — all 25 probed files returned inconclusive (reason: no-output, from the vitest global-setup prerequisite guard), so no mutant or hunk survivor was measured and the harness control never ran; the mutation-coverage conclusions in this review come from hand-built verifier probes instead.

未审查(原文为英文):did not verify whether create-sub-session.ts:1131's unpinned new SessionService(boundWorkspace).removeSession(...) runs inside a runWithWorkspaceRuntimeStorage scope at its call site — if it does not, a sub-session deletion resolves the primary runtime and silently no-ops the new snapshot release — stopped at the agent tool budget.

未审查(原文为英文):verifying that config.getSessionId() (the snapshot reference key written at artifact-tool.ts:270) cannot diverge from the SessionArtifactStore's fixed this.sessionId used by every retain/release call — Config.startNewSession() reassigns it — stopped at the agent tool budget.

未审查(原文为英文):whether toolName on a tool-recorded artifact can be caller-supplied through record_artifact params, which decides whether a model-driven record_artifact call can mint a full getWebPreviewSnapshotId descriptor and drive retain/delete against an arbitrary snapshot id — stopped at the agent tool budget.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 2)"determining the numeric limit passed to extractCappedArtifactInputs for tool_call_update artifacts (traced the parameter to bridgeClient.ts:443 / :3334 …"agent reverse-audit (round 2)"verifying that config.getSessionId() (the snapshot reference key written at artifact-tool.ts:270 ) cannot diverge from the SessionArtifactStore 's fixed th…"agent reverse-audit (round 2)"running packages/core/src/tools/artifact/html.test.ts to confirm the ten new characterization cases pass — I hand-evaluated each against the unchanged valida…"agent reverse-audit (round 1)"Firefox behaviour of connect-src 'self' against same-origin ws:// — the installed firefox-1532 build would not launch under this tree's playwright-core …"agent reverse-audit (round 2)"did not confirm referential stability of useWorkspace().client in daemon-react-sdk (it sits in the effect dep array; I inferred stability from the pre-exist…,另有 6 条。

⚠️ 循环结束时仍有 3 条发现带着 — [unverified] 标记——验证者从未对它们作出裁决,它们不算已确认。

收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 18 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

@wenshao wenshao removed the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Sep 10, 2026
qwen-code-ci-bot and others added 2 commits September 10, 2026 05:17
…ew-history

# Conflicts:
#	packages/cli/src/acp-integration/acpAgent.test.ts
#	packages/cli/src/serve/server/telemetry-catalog.test.ts
#	packages/cli/src/serve/server/telemetry.test.ts
#	packages/cli/src/serve/web-shell-static.test.ts
#	packages/cli/src/serve/web-shell-static.ts
#	packages/web-shell/client/App.tsx
@wenshao

wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Review follow-up and E2E verification — September 10

Merged current main (13b69b3313) and resolved all six conflicts, preserving the new session-source controls, Web preview state, and both the saved-content route and upstream route telemetry. The latest remote restore and CI-fixture repairs are preserved.

Missing snapshots blocking conversation forks — fixed. Reproduced the deferred Critical from review round 8: removing a saved page's file or directory made the entire fork fail with ENOENT. Forking now preserves the conversation and historical descriptor and emits a warning when snapshot storage is missing. Other ownership errors still abort before committing the child. No latest-page fallback was added.

Restore record loss — independently reverified. The fix for R3-1 retains the saved-page record after a real ENOTDIR bookkeeping failure. Writing another actual JSONL snapshot and restoring a fresh store preserves its ID, URL, timestamps, tool-call ID and SHA-256; readable original HTML remains readable.

Documentation and remaining comments. Both live-preview and saved-version designs now have complete, linked English and Chinese versions, including the missing-storage fork behavior. This resolves D8-15. The remaining Suggestions listed in rounds 7/8 remain acknowledged follow-ups under the repository's late-review scope rule: test-strengthening and fault-injection coverage, transient-I/O presentation, diagnostics, Windows-specific runtime validation, preview memoization/lazy mounting, CSP synchronization and abort handling. The review's explicitly unverified incidental hypotheses are not counted as confirmed defects. The previously closed Criticals were checked against the merged code again; resolved flags alone were not used as evidence.

Actual daemon verification

The test engineer ran the newly built node dist/cli.js with isolated storage and actual persisted conversation/artifact records. No branch API, snapshot-content API or persistence implementation was mocked.

Snapshot state Branch response Saved content in child
Healthy 201 200, exact original HTML
Missing HTML file 201, descriptor retained 404, unavailable
Missing snapshot directory 201, descriptor retained 404, unavailable
Missing reference directory 201, descriptor retained 200 while original bytes remain
Reference path is a regular file 500, ENOTDIR; direct probe verifies no child transcript committed No successful child

The installed global CLI does not contain this feature, so it established the unsupported baseline; deterministic recorder fixtures and the rebuilt daemon supplied the before/after proof. This does not claim model-generated publication or Windows execution. Both temporary daemons were stopped and their ports verified closed.

Pre-commit checks

  • Full build, bundle, typecheck, lint and scoped Prettier passed.
  • 5,397 related unit tests across 25 files passed.
  • Nine Chromium scenarios passed: live modules/storage/HMR, tab and viewport state, history reopening, redirect/script/descendant containment, offline interaction and navigation blocking, saved-version refresh, unavailable content and PDF blob framing.
  • Reverse audits read the full PR diff and merge outcomes, then challenged the evidence. The missing-storage cases were red before the repair; a controlled mutation that swallowed non-ENOENT errors made the failure-path test go red. Production source was restored byte-for-byte and the targeted cases passed again. Independent review and real-daemon verification completed before commit.

The PR description retains all ten images hosted on the wenshao fork assets branch. The reviewer test plan now includes missing-storage forks.

中文说明

已合入 main 13b69b3313 并解决六处冲突,保留会话来源功能、网页预览状态和完整路由统计,也保留远端机器人已提交的恢复及 CI 测试修复。

本轮实际修复了第 8 轮评审中延后的 Critical:快照文件缺失不再阻止整个会话分叉。分叉保留对话和原始历史描述符并发出警告;其他引用错误仍会阻止提交不完整分支。没有增加最新网页回退。

独立复验确认 R3-1 的恢复修复有效:真实 ENOTDIR 错误下,历史记录经过再次写入 JSONL 和新实例恢复后,ID、URL、时间、工具调用和校验和均保留。两份设计文档已补齐完整中英文版本及互链,D8-15 已解决。第 7/8 轮其余建议按仓库后期评审规则保留为后续工作,未验证的推测不计为已确认缺陷。

真实本地 daemon 验证了健康和三种缺失场景均可分叉(201),缺失内容明确返回 404 不可用;ENOTDIR 仍失败且不提交子对话。该验证使用实际持久化记录和 API,不模拟分叉、内容读取或持久化实现;不声称覆盖模型生成或 Windows。临时 daemon 已停止。

提交前完整构建、打包、类型检查、Lint、格式检查、5,397 项相关单元测试和 9 项 Chromium 场景均通过。多轮反向审计、使测试变红的受控变异验证、独立代码审查及真实 daemon 复验均完成后才提交。PR 描述保留 wenshao fork 图床的全部 10 张配图。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Address-review round — PR #11276

Feedback disposition

  • [rv:5162610418] @qwen-code-ci-bot (COMMENTED, partial review with disclosed gaps) — No new actionable findings were posted this round: the review body states the 7 confirmed Suggestion-level findings are already reported on the PR (all carried round-7 deferrals at their locations), the 18 findings under qwen-review-deferred are explicitly "recorded, not requested in this round", and the Inline comments, Issue-level comments, and Failed checks sections are empty. Per the convergence-posture rule that section is an audit record, not work, so no code change, thread resolution, or comment reply was made for those items. Decision: no review-driven code change this round.

  • --conflict true: requested base-conflict resolution — Merged origin/main (005fc97, 12 incoming commits) into the PR branch as merge commit 83c97ff9e2. Six files conflicted; each was resolved by understanding both sides:

    1. packages/cli/src/serve/web-shell-static.ts — Kept the PR's deliberate CSP change (frame-src http: https: blob:). The PR's design (docs/design/web-shell/web-shell-live-preview.md, "Server" row) intentionally replaces the host-pinned loopback frame-src so the shell can frame validated HTTP/HTTPS development origins, with the inner script-free wrapper pinning the child source. Main's only change here (from feat(web-shell): add unified session sources #11262) added blob: to the pinned form, which the PR's directive already includes; verified via git grep on origin/main that the removed loopbackSandboxOrigins/portFromHostHeader exports have no other consumers.
    2. packages/cli/src/serve/web-shell-static.test.ts — Kept the PR's assertions (wide frame-src including blob:). Main's pinning test was dropped because the API it exercised was deliberately removed by the PR; the surviving suite still pins the merged CSP shape.
    3. packages/web-shell/client/App.tsx — Union: kept both new serializeArtifactPanelTabs switch cases — the PR's web_preview (persisted with url/viewport) and main's source (not persisted). Both kinds exist in the merged ArtifactPanelTab union; typecheck confirms exhaustiveness.
    4. packages/cli/src/acp-integration/acpAgent.test.ts (2 hunks) — Union of mock-config factory entries: the PR's setArtifactSnapshotsEnabled plus main's setSessionSourceServiceFactory/registerSessionSourceTool/getSessionSourceService.
    5. packages/cli/src/serve/server/telemetry-catalog.test.ts and packages/cli/src/serve/server/telemetry.test.ts — The merged legacySessionTelemetryRoutes is the exact union of both sides (verified programmatically): HEAD's 70 routes + main's 3 session-sources routes + the PR's 1 artifact-content route = 73 routes with a 71/2 handler_resolved/pre_resolved split. Assertions updated from 70 (HEAD) / 72 (main) to 73 with the 71/2 split; both suites pass against the merged registration code.

No guards, branches, or behaviors were added beyond combining the two merged sides, so no new witnesses were required; every resolved hunk is covered by the pre-existing suites listed below (the CSP directive by web-shell-static.test.ts, the route counts by the two telemetry suites, the App switch by App.test.tsx, the mock union by acpAgent.test.ts).

Files changed this round

Merge commit 83c97ff9e2 only (conflict resolutions in the six files above).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • cd packages/cli && npx vitest run src/serve/web-shell-static.test.ts src/serve/server/telemetry.test.ts src/serve/server/telemetry-catalog.test.ts — 72 passed
  • cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts — 689 passed
  • cd packages/web-shell && npx vitest run client/App.test.tsx — 856 passed
  • cd packages/core && npx vitest run src/services/session-artifact-persistence.test.ts src/services/sessionService.test.ts — 289 passed
  • cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.ts — 159 passed
  • cd packages/acp-bridge && npx vitest run src/bridge.test.ts — 920 passed
  • cd packages/web-shell && npx vitest run client/components/artifacts/ArtifactPanel.test.tsx client/components/WorkspaceSessionProvider.test.tsx — 97 passed
中文说明

审查处理轮次 — PR #11276

反馈处置

  • [rv:5162610418] @qwen-code-ci-bot(COMMENTED,披露缺口的局部审查) — 本轮没有新的可执行发现:审查正文说明 7 条已确认的建议级发现已在 PR 上报告过(均已在第 7 轮按所在位置延后),qwen-review-deferred 下的 18 条发现明确标注"已记录,本轮不要求修改",且 Inline comments、Issue-level comments、Failed checks 各节均为空。按照收敛姿态规则,该 section 是审计记录而非工作项,因此本轮未针对这些条目修改代码、解决线程或回复评论。决定:本轮无需由审查驱动的代码改动。

  • --conflict true:请求解决基线冲突 — 将 origin/main(005fc97,12 个新提交)合并进 PR 分支,合并提交为 83c97ff9e2。六个文件发生冲突,均在理解双方意图后解决:

    1. packages/cli/src/serve/web-shell-static.ts — 保留 PR 有意为之的 CSP 改动(frame-src http: https: blob:)。PR 的设计(docs/design/web-shell/web-shell-live-preview.md 的 "Server" 行)刻意用经过校验的 HTTP/HTTPS 开发源替换按 Host 钉住的 loopback frame-src,内部无脚本包装层再钉住子框架源。main 在此处唯一的改动(来自 feat(web-shell): add unified session sources #11262)是在钉住形式中加入 blob:,而 PR 的指令已包含它;已用 git greporigin/main 上确认被移除的 loopbackSandboxOrigins/portFromHostHeader 导出没有其他使用者。
    2. packages/cli/src/serve/web-shell-static.test.ts — 保留 PR 的断言(含 blob: 的宽 frame-src)。main 的钉住测试被丢弃,因为它测试的 API 已被 PR 刻意移除;保留的测试套件仍然钉住合并后的 CSP 形态。
    3. packages/web-shell/client/App.tsx — 取并集:在 serializeArtifactPanelTabs 的 switch 中同时保留两个新增分支——PR 的 web_preview(随 url/viewport 持久化)与 main 的 source(不持久化)。两种 kind 都存在于合并后的 ArtifactPanelTab 联合类型中,typecheck 确认 switch 穷尽。
    4. packages/cli/src/acp-integration/acpAgent.test.ts(2 处)— mock 配置工厂取并集:PR 的 setArtifactSnapshotsEnabled 加上 main 的 setSessionSourceServiceFactory/registerSessionSourceTool/getSessionSourceService
    5. packages/cli/src/serve/server/telemetry-catalog.test.tspackages/cli/src/serve/server/telemetry.test.ts — 合并后的 legacySessionTelemetryRoutes 恰好是双方的并集(已用程序验证):HEAD 的 70 条 + main 的 3 条 session-sources 路由 + PR 的 1 条 artifact-content 路由 = 73 条路由,attribution 为 71/2(handler_resolved/pre_resolved)。断言从 70(HEAD)/ 72(main)更新为 73 及 71/2 拆分;两个套件对合并后的路由注册代码均通过。

本轮除合并双方外未新增任何守卫、分支或行为,因此无需新增见证测试;每个解决的冲突块都由下列既有套件覆盖(CSP 指令由 web-shell-static.test.ts 覆盖,路由计数由两个 telemetry 套件覆盖,App 的 switch 由 App.test.tsx 覆盖,mock 并集由 acpAgent.test.ts 覆盖)。

本轮改动的文件

仅合并提交 83c97ff9e2(上述六个文件的冲突解决)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • cd packages/cli && npx vitest run src/serve/web-shell-static.test.ts src/serve/server/telemetry.test.ts src/serve/server/telemetry-catalog.test.ts — 72 通过
  • cd packages/cli && npx vitest run src/acp-integration/acpAgent.test.ts — 689 通过
  • cd packages/web-shell && npx vitest run client/App.test.tsx — 856 通过
  • cd packages/core && npx vitest run src/services/session-artifact-persistence.test.ts src/services/sessionService.test.ts — 289 通过
  • cd packages/acp-bridge && npx vitest run src/sessionArtifacts.test.ts — 159 通过
  • cd packages/acp-bridge && npx vitest run src/bridge.test.ts — 920 通过
  • cd packages/web-shell && npx vitest run client/components/artifacts/ArtifactPanel.test.tsx client/components/WorkspaceSessionProvider.test.tsx — 97 通过

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

⚠️ The branch received new commits while this round ran; they were merged into this push, but this round's verification predates that merge — re-check anything that landed mid-run. · 本轮运行期间分支收到了新的提交;本次推送已将其合并,但本轮验证在合并之前完成——请复查运行期间落地的改动。

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/模型 kimi-k3 · CLI 0.23.2

@wenshao

wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up at d8785f3dfd6f6e325470df9222c3fd9e3670b6b5:

The CI freshness failure was resolved by incorporating the updated main gate. The next complete unit run then exposed eight reproducible failures in two upstream Web Shell test fixtures: startup now makes an independent branding request, and version tooltips are intentionally absent in the default compact sidebar. This follow-up supplies the existing branding response and allows only that request alongside capability discovery, then sets an expanded width in the two tooltip assertions. The initial session-request gate, single-load/stream checks, default-brand checks, and compact-footer regressions remain intact. No production behavior or workflow was changed by this follow-up.

Validation: full repository build and typecheck passed; the complete Web Shell suite passed 302 files / 7,166 tests, without retries. Independent reproduction matched all eight CI failures before the fix; all 20 focused tests, including unchanged compact-footer regressions, passed afterward. Scoped lint/format passed. Two consecutive reverse-audit passes and two independent passes found no further issue before committing. The earlier webpage snapshot fixes and all ten description images are preserved.

Remote CI for this exact commit is now successful: Qwen Code CI (full unit suite, static checks, integration tests, desktop checks and both browser gates), Serve A/B, Web-shell Visuals, SDK Java, and TUI parity. The CI log confirms the previously failing loading and branding files pass, and all 7,166 Web Shell tests pass. No unresolved review threads remain. The automatic PR review is still running and the previous changes-requested review has not yet been replaced by approval; the PR is conflict-free, awaiting review approval.


本次补充先同步 main 的新 CI 门禁,再修复完整单测发现的两处上游测试前提不匹配:启动时的独立品牌请求,以及紧凑侧栏隐藏版本提示。仅修改测试,保留会话提前请求、单次恢复和窄栏布局的全部原有断言。全仓构建、类型检查及 Web Shell 全套 302 个文件 / 7,166 项测试通过;提交前两轮反向审计和两轮独立审计均未发现新问题。当前提交的代码 CI 已全部通过,包括全量单测、静态检查、集成测试及两个浏览器检查。当前无合并冲突、无未解决评论线程;自动审查仍在运行,之前的“请求修改”状态仍待审核者更新。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

Not reviewed: three verifier-incidental candidates the audit loop never reached a verifier for — trailing-dot hostname rejection undocumented (packages/web-shell/client/components/preview/web-preview.ts:33), protectedOrigin matches spellings rather than name resolution (web-preview.ts:9), and the snapshot exemption keyed on a lookup that is undefined while the artifact list loads (packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:1060).

Not reviewed: the three new Playwright specs (web-shell.live-preview / pdf-preview / saved-preview.spec.ts) — no command in this run executes them; packages/web-shell/vitest.config.ts excludes client/e2e/**, so the green 302-file web-shell suite does not include them.

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

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI, and this diff adds an fs-flags path (packages/core/src/tools/artifact/artifact-snapshots.ts:94) whose Windows behaviour no run on this Linux host could exercise.

Not explored to full depth (tool budget reached): chunk 18: none — I did not run the Playwright specs (no browser/dev-server setup in this review tree); the static selector and assertion tracing above plus the head-commi…; "agent reverse-audit (round 1)": the default byte cap for GET /file when maxBytes is omitted — I confirmed only the explicit 256 * 1024 bound at packages/cli/src/serve/routes/workspace-fi…; "agent reverse-audit (round 1)": executing web-shell.live-preview.spec.ts under Playwright (browser install + vite dev server) to measure empirically how often the single-shot window.innerWi…; "agent reverse-audit (round 2)": the README claims 390px 手机宽度 and 地址必须使用主机名或 IPv4 were not verified against the web-shell preview code — my grep for 390 returned SVG asset noise and I sto…; "agent reverse-audit (round 2)": the App.test.tsx assertions that consume the new readSessionArtifactContent mock (my chunk ends mid-file at the latestMessageListProps type addition) were l…, and 1 more.

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

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

  • docs/design/web-shell/web-shell-live-preview.md:40 — [review] The design record for what the diff itself calls a process-global shell-document policy change claims the other CSP directives are preserved, but the same diff adds a s
  • docs/design/web-shell/web-shell-live-preview.md:94 — [review] The "Files affected" list omits two files this PR changes that carry the very policies the design's own prose specifies — packages/web-shell/vite.config.ts ("Vite dev
  • docs/design/web-shell/web-shell-preview-snapshots.md:15 — [review] The design states snapshot creation as an unconditional consequence of a successful publication and never states the shipped partial-failure semantics (publication succ
  • packages/acp-bridge/src/sessionArtifacts.test.ts:6244 — [probe] The only test in this chunk that round-trips a saved-webpage record through recordEvent → rebuildSessionArtifactSnapshot → restore asserts that publishedUrl sur
  • packages/acp-bridge/src/sessionArtifacts.test.ts:6563 — [probe] The new suite pins "never reclaim snapshot bytes for a removal that was not durably persisted" for the restore-prune path only ( if (persistenceWarnings.length === 0) ,
  • packages/acp-bridge/src/sessionArtifacts.test.ts:6598 — [probe] The test named uses the runtime captured before the ambient environment changes never lets the ambient environment reach the store: the store() helper always passes
  • packages/acp-bridge/src/sessionArtifacts.ts:1989 — [probe] The new reclamation path is silent twice over — deleteArtifactSnapshot already swallows every error internally (packages/core/src/tools/artifact/artifact-snapshots.ts
  • packages/cli/src/serve/routes/session.ts:6418 — [probe] The bare catch collapses *every* readArtifactSnapshot failure — including transient I/O faults — into the permanent 404 artifact_snapshot_unavailable , and the onl
  • packages/cli/src/serve/run-qwen-serve.ts:6124 — [review] Nothing pins the three-way identity of the runtime directory that saved-webpage bytes travel through — the child's QWEN_RUNTIME_DIR (where ArtifactTool writes via
  • packages/core/src/services/session-artifact-persistence.ts:123 — [review] The source clause silently carries a second, unrelated rule — a record with source: 'tool' and no toolName is rejected only because artifact.toolName?.toLowerC…
  • packages/core/src/services/sessionService.test.ts:5795 — [probe] No test pins that deletion-time collection unions the artifacts of *every* artifact record instead of replaying the transcript as a whole, so collapsing the per-record
  • packages/core/src/services/sessionService.ts:3274 — [probe] The transcript scan that drives snapshot reclamation swallows every read error with no warning, unlike the sibling usage-salvage read at the same call site, so a sessio
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:57 — [review] No test in this file pins the mode: 0o600 that saveArtifactSnapshot sets on the snapshot bytes and on each reference file, so the only thing keeping saved page co…
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:80 — [review] The "oversized" case cannot fail without the size guard, because line 75 already overwrote the file with 'changed' — the sha256 mismatch rejects it too, so stat.si…
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:156 — [probe] Every retainArtifactSnapshot call in the entire repo's tests passes an operationId , so the !operationId form used by *both* non-fork production callers — the re…
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:206 — [review] This test pins only the fork half of retainArtifactSnapshot 's documented two-sided contract ( if (code !== 'EEXIST' && !(code === 'ENOENT' && !operationId)) throw …
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:290 — [review] The foreign-runtime url case in never deletes outside the exact file the descriptor points at never creates that foreign tree, and its only assertion is that the…
  • packages/core/src/tools/artifact/artifact-snapshots.ts:97 — [review] On Windows constants.O_NOFOLLOW / O_NONBLOCK are undefined and the bitwise OR coerces them to 0, so the open silently collapses to a plain symlink-following O_RDON
  • packages/core/src/tools/artifact/artifact-tool.test.ts:50 — [probe] The new QWEN_RUNTIME_DIR stub and the new mock storage.getRuntimeBaseDir() are pointed at the *same* directory ( path.join(outDir, 'runtime') ), so the file's new s
  • packages/core/src/tools/artifact/artifact-tool.ts:63 — [probe] The static DESCRIPTION promises snapshot saving and preservation-first tool choice unconditionally, while the behaviour it describes is gated on isArtifactSnapshotsE
  • …and 8 more (see the run report)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

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

未审查(原文为英文):three verifier-incidental candidates the audit loop never reached a verifier for — trailing-dot hostname rejection undocumented (packages/web-shell/client/components/preview/web-preview.ts:33), protectedOrigin matches spellings rather than name resolution (web-preview.ts:9), and the snapshot exemption keyed on a lookup that is undefined while the artifact list loads (packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:1060).

未审查(原文为英文):the three new Playwright specs (web-shell.live-preview / pdf-preview / saved-preview.spec.ts) — no command in this run executes them; packages/web-shell/vitest.config.ts excludes client/e2e/**, so the green 302-file web-shell suite does not include them.

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

未审查(原文为英文):build-and-test — Test (windows-latest, Node 22.x) was skipped in CI, and this diff adds an fs-flags path (packages/core/src/tools/artifact/artifact-snapshots.ts:94) whose Windows behaviour no run on this Linux host could exercise.

未探索到全部深度(达到工具调用预算):chunk 18:none — I did not run the Playwright specs (no browser/dev-server setup in this review tree); the static selector and assertion tracing above plus the head-commi…"agent reverse-audit (round 1)"the default byte cap for GET /file when maxBytes is omitted — I confirmed only the explicit 256 * 1024 bound at packages/cli/src/serve/routes/workspace-fi…"agent reverse-audit (round 1)"executing web-shell.live-preview.spec.ts under Playwright (browser install + vite dev server) to measure empirically how often the single-shot window.innerWi…"agent reverse-audit (round 2)"the README claims 390px 手机宽度 and 地址必须使用主机名或 IPv4 were not verified against the web-shell preview code — my grep for 390 returned SVG asset noise and I sto…"agent reverse-audit (round 2)"the App.test.tsx assertions that consume the new readSessionArtifactContent mock (my chunk ends mid-file at the latestMessageListProps type addition) were l…,另有 1 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

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

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

Comment thread packages/acp-bridge/src/sessionArtifacts.ts Outdated
Comment thread packages/core/src/tools/artifact/artifact-tool.ts
@wenshao

wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Round 9 fixes and conflict resolution are in 874042c0dd. Main at 76635386e4 is incorporated; the branding test conflict follows main’s completed fix, and the previous preview feature changes are preserved. The PR description’s 10 screenshots remain unchanged.

  • R9-1: failed artifact batches release uncommitted snapshot ownership, including strict persistence, durable-transition rollback, capacity overflow, and failure after writing a reference. Existing live/fork owners remain intact.
  • R9-2: cancellation after publication or during snapshot saving releases only the new version, returns no descriptors, and preserves the successfully published URL and older snapshots.
  • Reverse audits reproduced additional data-loss paths involving failed pruning, partial restores (mismatched ID and invalid title with the same ID), and a later successful batch dropping a still-durable version. The common reclamation path now protects those references until a corresponding durable change or complete snapshot/restore succeeds.

Validation: full build, full typecheck, bundle and lint passed; core 326, ACP 1,096, CLI 944, and Web Shell 7,203 tests passed. All three Playwright specs ran explicitly: 9 Chromium scenarios passed, covering live modules/storage/HMR, navigation isolation, original saved-version cards and inline interaction after reopening/reloading, refresh from memory, and PDF blob framing. Browser daemon responses use fixtures; the separate fault-injection probes invoke the real tool, publisher, store, snapshot filesystem, and session fork/deletion implementation. They do not claim a model-driven scheduler E2E. Windows was not exercised on this macOS host.

Every reproduced bug went red before its fix. Audit passes 1–3 found issues, pass 4 was clean, and pass 5 caught a Windows-specific test assumption. We disclosed the failure to converge in five passes, replaced the platform-dependent fault injection, and then completed consecutive clean passes 6 and 7 on the final diff. The test engineer independently verified rebuilt artifacts. All five code workflows completed successfully for exactly 874042c0ddbf33f86915599cda13e9463cc71edb: Qwen Code CI, Serve A/B, Web-shell Visuals, SDK Java, and tui-parity. The final CI browser job passed 6 transcript-document tests and 73 Web Shell smoke scenarios; the separate visual workflow passed 47 scenarios on the PR head. Automatic review is still queued; its previous CHANGES_REQUESTED state has not been dismissed.

Convergence disposition: the review’s 28 deferred Suggestions remain recorded in the round 9 review rather than expanding this PR. The snapshot partial-failure wording and relevant lifecycle tests were updated as part of the Critical fixes; the remaining Suggestions are deferred to follow-up work and are not silently treated as implemented. This update does not merge the PR or substitute for maintainer review.

中文:本次处理了 main 冲突及 R9-1/R9-2,并在提交前的反向审计中复现、修复了部分恢复和后续淘汰导致的历史快照误删。两条原评论及新增边界均先有失败证据,再验证修复。第 1–3 遍发现问题,第 4 遍干净,第 5 遍发现 Windows 测试假设不成立;已说明前五遍未收敛,修正后第 6、7 遍连续无新发现;独立测试工程师验证了真实构建产物。build、typecheck、bundle、lint、9,569 个相关单元测试及 9 个 Chromium 场景通过。浏览器场景使用 daemon 数据夹具,另有真实文件、工具、存储和分叉/删除探针,不声称覆盖模型驱动的完整调度流程或 Windows。PR 的 10 张配图保持不变;其余非阻断建议按第 9 轮收敛规则留待后续处理。提交 874042c 的五个代码工作流已全部成功;主 CI 最后的文档浏览器检查 6 项、Web Shell 冒烟场景 73 项通过,独立视觉工作流的 PR 分支 47 项通过。自动审查仍在排队,之前的 CHANGES_REQUESTED 状态未被撤销,PR 未合并。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs.

@wenshao

wenshao commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

September 11 conflict and review follow-up — 2ff9bb8613 incorporates main c46cb85cf2.

Both ACP conflicts preserve the trusted Web Shell snapshot capability and the upstream Goal-proposal capability before session initialization. All 58 files retain the same authored addition/deletion lines as the previous PR head; this maintenance update does not expand the feature. The original PR description and its 10 screenshots remain unchanged.

No new unresolved review threads were present. The latest automatic review result reports a 21,600-second timeout, not a new code finding. The 15 historical Critical threads and the missing-snapshot fork case were checked against the merged implementation, rather than assuming resolved threads prove a fix. Previously recorded Suggestions remain deferred under the repository's late-review convergence rule. No review was dismissed and the PR has not been merged.

Before committing, two consecutive independent reverse-audit passes found no new verified Critical; parent review and an additional inspection of the actual validation logs and assertions agreed. Full build, typecheck (including integration), bundle, lint and formatting passed. Package-scoped unit tests passed: core 993, ACP 1,098, CLI 1,088 and Web Shell 1,048 — 4,227 total. The three explicitly selected preview Playwright specs passed all 9 Chromium cases with no retries.

The test engineer reran 12 real filesystem/publication/store/fork outcomes using the newly installed dependencies and rebuilt modules. All passed, including failed persistence, partial restore and both cancellation windows. A supplemental mocked ACP lifecycle matrix passed 6 combinations of trusted-parent and Goal capability; actual Config objects passed 8 combinations of recording, snapshot opt-in and Goal host support. Actual Artifact publication of v1 and v2 to the same latest URL created distinct saved versions; after deleting both the source and latest file, both original saved HTML versions remained exactly readable. Fourteen source/output/test/lockfile hashes stayed stable through verification.

Browser daemon responses use fixtures; the independent filesystem probes use real backend implementations. Neither is presented as model-driven scheduler E2E or native Windows validation. All probe processes exited normally.

All five code workflows completed successfully for exactly 2ff9bb861308e0a6c2852dbc4ce8112d46034419: Qwen Code CI, Serve A/B, Web-shell Visuals, SDK Java, and tui-parity. The final CI browser job passed 6 transcript-document tests and 74 Web Shell smoke scenarios. Visuals passed 47 PR-head and 43 base scenarios. Automatic review remains queued; the previous CHANGES_REQUESTED state has not been dismissed.

中文说明

本次合入 main c46cb85cf2,两处 ACP 冲突同时保留可信 Web Shell 的历史快照开关和上游的目标提案能力,两者均在会话初始化前配置。PR 原有 58 个文件的逐文件增删内容保持一致,没有扩展功能范围;描述及 10 张配图均保留。

没有新的未解决评论。最新自动审查是达到 21,600 秒上限后超时,并未报告新的代码问题。已按当前合并代码重新核对 15 条历史 Critical 及快照缺失时的分叉问题;原有非阻断建议继续按后期收敛规则留待后续,未撤销审查或合并 PR。

提交前两轮独立反向审计连续无新发现,并复核实际日志和断言。完整构建、类型检查、打包、Lint、格式检查、4,227 项相关单元测试和 9 个 Chromium 场景通过。独立测试重新执行的 12 个真实文件场景、6 种 ACP 模拟生命周期组合、8 种真实 Config 组合均通过;真实连续发布两版后,删除源文件及最新发布文件,两份历史 HTML 仍精确保留。14 项源码、产物、测试和锁文件哈希全程稳定。

浏览器场景使用 daemon 数据夹具,后端探针调用真实实现;不声称覆盖模型驱动的完整调度流程或 Windows。提交 2ff9bb8 的五个代码工作流已全部成功,最后的文档浏览器检查 6 项、Web Shell 冒烟场景 74 项通过;视觉检查的 PR 分支 47 项、main 对照 43 项通过。自动审查仍在排队,旧的 CHANGES_REQUESTED 状态未被撤销,PR 未合并。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally; this diff adds an fs-flags path whose Windows behaviour no run on this Linux host could exercise.

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

Not reviewed: the three new Playwright specs (web-shell.live-preview / pdf-preview / saved-preview) — packages/web-shell/vitest.config.ts excludes client/e2e/**, so no command in this run executed them.

Not reviewed: test-efficacy probe — harnessValidated was null (the positive control never produced a verdict; all 25 revert probes came back inconclusive because the scratch tree has no built dist), so no revert/mutant/hunk evidence exists for this PR either way.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": the README's live-preview behaviour claims in my chunk (tab-switch state preservation, per-workspace/session URL and width persistence, 390px toggle, cross-orig…; chunk 17: none — I did not run the Playwright suite (no browser/dev-server run in this review), so all test-pass reasoning above is from static reading of the specs and t….

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

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

  • packages/core/src/tools/artifact/artifact-snapshots.ts:250 — [review] A lost ownership race surfaces as bare ENOENT, indistinguishable from absent storage, so the fork caller warns and commits a branch whose saved version is permanently una…
  • packages/core/src/tools/artifact/artifact-tool.ts:66 — [review] The tool DESCRIPTION promises saved-version history unconditionally while the behaviour is gated on isArtifactSnapshotsEnabled()
  • packages/acp-bridge/src/bridge.ts:7157 (+1 locations) — [review] No test joins the artifactSnapshotRuntimeBaseDir wiring to store behaviour; a dropped forwarding degrades to a silent no-op
  • packages/web-shell/vite.config.ts:101 — [review] The dev-server CSP is a hand-copy of the shipped policy with no drift guard, and the three new acceptance specs assert the copy
  • packages/web-shell/client/components/artifacts/TurnOutputs.tsx:464 — [review] The transcript card's new saved-version label has no test; the fallback leaks the internal token web_preview_snapshot to the user
  • packages/acp-bridge/src/sessionArtifacts.test.ts:6925 — [review] The runtime-capture test never reaches the ?? Storage.getRuntimeBaseDir() fallback it is named for, so a lazy-getter mutation survives all 172 tests
  • packages/core/src/config/config.test.ts:6081 — [review] The only assertion on the new shouldAutoOpenArtifact snapshot guard cannot fail under CI=true with no DISPLAY, so the guard is untested
  • packages/core/src/services/sessionService.ts:3247 — [review] collectSessionArtifactSnapshots re-implements jsonl.read, doubling the transcript read per deletion and swallowing failures with no diagnostic
  • packages/core/src/services/sessionService.ts:3358 — [review] Archive/unarchive conflict resolution deletes a same-ID transcript copy without reclaiming its snapshots, orphaning them through two successful operations
  • packages/core/src/services/sessionService.ts:4208 — [review] Fork-rollback reclamation failures are silent at both layers, unlike every sibling cleanup in the same finally block
  • packages/web-shell/client/components/artifacts/ArtifactPanel.test.tsx:73 — [review] The new readSessionArtifactContent mock is the only one afterEach never resets, leaking implementations into later tests
  • packages/core/src/tools/artifact/artifact-tool.test.ts:180 — [review] The cancel-test stub permits shouldAutoOpenArtifact and snapshots enabled at once, a configuration the shipped Config forbids
  • packages/web-shell/client/components/preview/SavedWebPreview.tsx:55 — [review] Every snapshot read failure collapses into one terminal 404, so a transient EMFILE permanently hides the only retry affordance
  • packages/web-shell/client/components/preview/WebPreviewPanel.tsx:127 — [review] The live preview srcDoc embeds the translated frame title, so a UI-language change rewrites it and force-reloads the framed app
  • packages/core/src/config/config.ts:7914 — [review] Managed-session publication writes into the process-global ~/.qwen/artifacts while snapshots write into the captured per-session runtime
  • packages/core/src/config/config.ts:7906 — [review] The live published file:// URL the tool tells the model to share has no renderer in the Web Shell the gate's premise names
  • packages/core/src/services/sessionService.ts:4175 — [review] No test separates the captured runtime dir from the ambient one at the fork retain and rollback sites; both mutants pass 269 tests
  • packages/core/src/tools/artifact/artifact-snapshots.ts:94 — [review] OR-ing O_NOFOLLOW in by hand collapses to a symlink-following O_RDONLY on Windows, the shape no-follow-open.ts forbids
  • packages/web-shell/client/App.test.tsx:3729 — [review] The only web_preview_snapshot routing fixture uses a file:// URL, so deleting the new routing clause keeps all 871 tests green
  • packages/web-shell/client/components/artifacts/turnOutputSelectors.test.ts:69 — [review] Per-turn savedUrls lets a record_artifact re-attribution resurrect the duplicate card, and no test covers the cross-turn case
  • …and 1 more (see the run report)

Convergence: round 10 posted 1 inline comment(s), 1 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: packages/acp-bridge/src/sessionArtifacts.ts (findings in round 9; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

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

未审查(原文为英文):build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally; this diff adds an fs-flags path whose Windows behaviour no run on this Linux host could exercise.

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

未审查(原文为英文):the three new Playwright specs (web-shell.live-preview / pdf-preview / saved-preview) — packages/web-shell/vitest.config.ts excludes client/e2e/**, so no command in this run executed them.

未审查(原文为英文):test-efficacy probe — harnessValidated was null (the positive control never produced a verdict; all 25 revert probes came back inconclusive because the scratch tree has no built dist), so no revert/mutant/hunk evidence exists for this PR either way.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"the README's live-preview behaviour claims in my chunk (tab-switch state preservation, per-workspace/session URL and width persistence, 390px toggle, cross-orig…;chunk 17:none — I did not run the Playwright suite (no browser/dev-server run in this review), so all test-pass reasoning above is from static reading of the specs and t…

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

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

收敛情况:第 10 轮发布了 1 条行内评论,其中 1 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:packages/acp-bridge/src/sessionArtifacts.ts(第 9 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

Comment thread packages/acp-bridge/src/sessionArtifacts.ts
@wenshao

wenshao commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

R10 verification report for e180a24.

The latest Critical is fixed: a non-strict snapshot-reference I/O failure no longer leaves silently accepted history after the bridge drops its notifications. The four-file change preserves strict rollback, adds focused regressions and synchronizes the English/Chinese design explanation.

Verification Result
Reproduce before fixing Real same-owner EACCES: notifications/live/later durable records = 0/2/2
Verify rebuilt fix Notifications/live/durable = 2/2/2, matching IDs, one artifact-specific warning
Strict controls strict, validationStrict-only and persistenceStrict-only each reject and leave zero live/durable records
Healthy controls Two matching notifications/records; no new warning on healthy or idempotent ingestion
Independent filesystem verification Six main scenarios and twelve prior ownership/rollback/partial-restore/fork/cancellation scenarios passed
Committed regressions Two intended red tests before the fix; all 178 store tests and 1,236 focused ACP tests passed after the fix
Build and static checks Full build, bundle, full typecheck including integration, lint:ci, Prettier and diff whitespace checks passed
Reverse audits before commit Parent and independent reviewer each completed two consecutive clean passes; 23 source/output/test/doc hashes remained stable during independent verification

The real-I/O test-script fallback executes Artifact publication, store mutations, BridgeClient extraction/ingestion, JSONL persistence/rebuild and SessionService fork handling. Its configuration and event receiver are fixtures. The reference-read failure is native EACCES; the separate unit acquisition-write failures inject ENOSPC before/after a real write. This independent local probe does not claim a full model/daemon/browser run or native Windows filesystem verification. The remote CI checks below are separate executed checks. The unchanged feature-specific nine Chromium scenarios remain documented in the preceding verification report.

The latest review's 21 Suggestions remain explicitly deferred under the round-10 convergence posture; this change addresses the Critical without widening the feature. The original bilingual PR description and all ten images hosted on the wenshao fork assets branch are preserved. No merge or review dismissal was performed.

All five code CI workflows completed successfully on e180a24:

Workflow Final result
Qwen Code CI Passed: static checks, full unit tests, required no-AK integration and final Web Shell browser gates
Serve A/B Passed for PR head and base
Web-shell Visuals Passed: 47 PR-head cases and 43 base cases
SDK Java Passed: platform/JDK matrix and real daemon E2E
TUI parity Passed: parity snapshots and no-flicker gate

The main CI unit log contains 23 suite summaries totaling 74,124 passed and 116 skipped tests, including all 178 store regressions. The final browser job passed the six-case transcript document gate and all 74 smoke cases. Main CI's macOS/Windows unit jobs and model-dependent CLI integration job were skipped by the workflow; they are not presented as executed coverage. The PR is mergeable with zero unresolved review threads. The separate automatic code review is still in progress; its previous changes-requested decision has not been dismissed.

中文说明

本轮修复了非严格写入时引用 I/O 错误导致的通知与历史记录不一致。修复前真实权限错误得到“通知 0、内存 2、后续落盘 2”;修复后变为 2/2/2,ID 一致且准确报告警告。三种严格模式仍全部回滚,正常及幂等路径不新增警告。

独立验证的 6 个主场景和 12 个历史保护场景全部通过。两个新增回归先在旧代码上失败;修复后 178 个存储测试及合计 1,236 个相关 ACP 测试通过。完整构建、打包、类型检查、lint、格式及差异检查均通过。提交前主审计与独立审计各连续两轮未发现新的 Critical,独立验证期间 23 项源码及产物哈希保持一致。

本轮独立探针使用真实文件系统、产物发布和 JSONL 持久化,事件接收端为测试 fixture;该本地探针没有宣称执行完整模型/daemon/浏览器链路或原生 Windows 文件系统验证。远端 CI 的执行结果单独列于上表。按本轮审查的收敛要求,21 条 Suggestions 保留为后续工作。PR 原有双语描述和 wenshao fork assets 分支托管的 10 张图均保留,未合并 PR 或撤销评审。

新提交的五组代码 CI 已全部成功完成。主 CI 单元测试日志的 23 组摘要合计 74,124 项通过、116 项跳过,其中本轮存储测试 178 项全部执行并通过。最后的会话文档检查 6 项、浏览器冒烟 74 项均通过;视觉检查为 PR 版本 47 项、主分支对照 43 项通过。主 CI 的 macOS/Windows 单测及依赖模型密钥的 CLI 集成任务按工作流跳过,未将其记为已执行覆盖。PR 无冲突,未解决评论线程为 0;自动代码复审仍在进行中,上一轮请求修改的评审状态未被撤销。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally; the Windows-only no-follow-open behaviour was checked against a constants model, not a run.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally.

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

Not explored to full depth (tool budget reached): chunk 18: prettier --check on the three territory files (the run timed out before producing output, so CJK line-break formatting is unverified — formatting is formatter…; "agent reverse-audit (round 1)": did not read isTrustedManagedParent() 's body ( packages/cli/src/acp-integration/acpAgent.ts :3638), so I confirmed only that the new acpAgent.test.ts case's…; "agent reverse-audit (round 1)": did not open normalizeInput 's metadataBudget: 'persisted' filtering to confirm qwen.snapshot.references is preserved for restored records; I inferred it f…; "agent reverse-audit (round 2)": end-to-end confirmation that the ACP emitter labels the post-settle cancel cancelled (not failed ) in createTranscriptToolCallResultUpdate , and an executed…; "agent reverse-audit (round 1)": none — all six layers above were walked to a conclusion; nothing in chunk 12 was left unread., and 6 more.

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

12 Suggestion(s) were drafted inline past the resolved critical posting floor; the CLI moved them into the deferral list below (floor enforcement).

Deferred under the convergence posture (round 11, not a blocker) — recorded, not requested in this round; 1 Critical(s) among them are deferred by their axes — fails-closed on new surface, where no wrong result is certified and the merge base had neither the surface nor the defect — and remain follow-up work recorded in the findings artifact:

  • packages/core/src/services/sessionService.ts:3267 — [probe] Critical [fails-closed] [new-surface] snapshot bytes unreclaimable whenever the descriptor never reaches a scanned location; no orphan sweep
  • packages/acp-bridge/src/sessionArtifacts.test.ts:6526 — [review] R11-1: The only parent/fork shared-snapshot test never asserts that the fork acquired its own reference (nor that its remove /eviction actually dropped the record), so every …
  • packages/acp-bridge/src/sessionArtifacts.ts:1176 — [review] R11-2: pendingSnapshotRemovals is a suppress-only deferral set; deferred reclamation is never performed. Store is at maxArtifacts and an upsert batch evicts a web-preview artifac…
  • packages/cli/src/serve/routes/session.ts:6420 — [review] R11-3: content route collapses every FS failure into the terminal artifact_snapshot_unavailable code; client renders it as permanent with no retry. The daemon is at its descriptor lim…
  • packages/core/src/config/config.test.ts:6083 — [review] R11-4: The only assertion covering the new auto-open suppression ( config.ts:8012 if (this.isArtifactSnapshotsEnabled()) return false; ) is satisfied by ambient CI environment, so de…
  • packages/core/src/config/config.ts:7914 — [review] R11-5: Relaxing the non-interactive gate for managed preview sessions neutralizes only the "opens a browser" half of the gate's own stated rationale ("Publishing writes outside the project …
  • packages/core/src/services/sessionService.ts:3314 — [review] R11-6: Every session deletion now JSON-parses each transcript identity a second time, unconditionally, in the same loop that already fully reads the same file for usage salvage. …
  • packages/core/src/services/sessionService.ts:3360 — [review] R11-7: Snapshot reclamation is the first operation that can throw *after* the transcript has been removed and durable-synced, so its ownership-assert failure is reclassified by th…
  • packages/core/src/tools/artifact/artifact-tool.ts:274 — [review] R11-8: The cancel path writes a fully durable snapshot and only then discovers the request was cancelled — signal.aborted is checked after saveArtifactSnapshot returns, so…
  • packages/web-shell/client/components/artifacts/turnOutputSelectors.test.ts:70 — [review] R11-9: The duplicate-card suppression this PR adds builds its savedUrls set **per turn** ( turnOutputSelectors.ts:74-95 ), so a local file:// publi…
  • packages/web-shell/client/components/preview/web-preview.ts:44 — [review] R11-10: The protected-origin comparison is one-directional: it blocks an http: candidate that *upgrades* into a protected https: origin, but never an https: can…
  • packages/web-shell/client/e2e/web-shell.live-preview.spec.ts:390 — [review] R11-11: The new "recorded links open metadata without automatically loading a live frame" test pins frame *absence* with toHaveCount(0) , which resolves on its fir…
  • packages/web-shell/client/e2e/web-shell.saved-preview.spec.ts:79 — [review] R11-12: The browser-level containment test builds the wrapper document in the Node test process, where DOMParser is undefined, so Chromium is handed the output of…
  • packages/acp-bridge/src/bridge.ts:7157 — [review] runtime base dir plumbed write-side vs resolved read-side independently, silent ambient fallback
  • packages/acp-bridge/src/sessionArtifacts.test.ts:6655 — [review] it.each(['before','after']) parametrization changes only the spy's internal behaviour - no assertion depends on the parameter, so the corner it exists to cover is unpinned
  • packages/core/src/tools/artifact/artifact-snapshots.test.ts:274 — [review] The reclamation path's symlink-containment guard — the four realpath(...) !== path.join(realRoot, ...) comparisons in snapshotDirectory (packages/core/src/tools/…
  • packages/core/src/tools/artifact/artifact-snapshots.ts:93 — [review] Hand-rolled O_NOFOLLOW|O_NONBLOCK expression collapses to a plain symlink-following open on Windows, the exact pattern the repo's no-follow-open helper exists to forbid
  • packages/web-shell/client/App.tsx:6104 — [review] no sweep removes an already-mounted web_preview tab when webPreviewAvailable flips false
  • packages/web-shell/client/App.tsx:6581 — [review] The deterministic web-preview tab id and the replace-vs-append dedupe that depends on it have no test
  • packages/web-shell/client/components/artifacts/ArtifactPanel.tsx:3643 — [review] The new artifactPreviewDocument wrapper builder is called inline in JSX, so every panel re-render re-parses and re-escapes the entire HTML artifact document - …
  • …and 1 more (see the run report)
中文说明

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

未审查(原文为英文):build-and-test — Test (windows-latest, Node 22.x) was skipped in CI and its suite did not run locally; the Windows-only no-follow-open behaviour was checked against a constants model, not a run.

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) was skipped in CI and its suite did not run locally.

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

未探索到全部深度(达到工具调用预算):chunk 18:prettier --check on the three territory files (the run timed out before producing output, so CJK line-break formatting is unverified — formatting is formatter…"agent reverse-audit (round 1)"did not read isTrustedManagedParent() 's body ( packages/cli/src/acp-integration/acpAgent.ts :3638), so I confirmed only that the new acpAgent.test.ts case's…"agent reverse-audit (round 1)"did not open normalizeInput 's metadataBudget: 'persisted' filtering to confirm qwen.snapshot.references is preserved for restored records; I inferred it f…"agent reverse-audit (round 2)"end-to-end confirmation that the ACP emitter labels the post-settle cancel cancelled (not failed ) in createTranscriptToolCallResultUpdate , and an executed…"agent reverse-audit (round 1)"none — all six layers above were walked to a conclusion; nothing in chunk 12 was left unread.,另有 6 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

12 条 Suggestion 在已解析的 critical 发布下限之外被起草为行内评论;CLI 已将其移入下方延后清单(下限强制执行)。

收敛姿态下延后(第 11 轮,非阻断)——已记录,本轮不要求修改;其中 1 条 Critical 按其失败方向与对照基线延后——fails-closed 且 new-surface:未认证任何错误结果,且 merge base 既无该功能面也无该缺陷——作为后续工作记录在 findings 工件中:共 21 条(原文未翻译,列表见上方英文部分)。

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

@wenshao

wenshao commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

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: 143 passed · 0 failed · 143 total

Flakiness gate: ⚠️ timeout — only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:143 通过 · 0 失败 · 143 总计

抖动门:⚠️ timeout — only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report (report.md, truncated)

# PR #11276 deep verification — `feat(web-shell): add web previews with saved delivery history`

**Verdict: `findings`** — 143/143 scripted assertions passed (0 unexpected failures); the central
claim is proven load-bearing by A/B. No Critical, no regression, no blocker. Four non-blocking
findings for the reviewer, all in the browser-security surface rather than in the snapshot store:
(1) a deliberate CSP `frame-src` control carrying an explicit written security rationale was
**deleted** and broadened shell-wide and unconditionally, while the feature that motivated it is
gated/opt-in; (2) the live-preview **outer** wrapper iframe carries no `sandbox` attribute and is
therefore same-origin with the shell; (3) the live-URL validator has no private-network restriction
and an Artifact-published URL **auto-opens** the panel with no human confirmation; (4) two residual
runtime-base-dir paths where the write dir and the delete/read dir can diverge, outside the
`qwen serve` happy path (which I proved consistent). The snapshot store itself withstood 69 mock-free
cells, a 13-row mutation matrix, and three vacuity reverts with no defect.

> **Revision note.** This report was first shipped at the budget deadline with the browser-isolation
> surface listed as *Not covered*, because two research subagents I had dispatched had not returned.
> Both returned immediately afterwards. Their claims are folded in below, and every load-bearing one
> was **re-verified by me directly against the source or the compiled build** before being written in
> — specifically the emitted CSP string, both iframes' `sandbox` attributes, `escapeAttribute`'s
> escape set, and the absence of any runtime-dir pinning in `live-session-coordinator.ts`. Inspection-
> level claims are labelled as such. `assertions.json` is unchanged at 143/143: no new scripted
> assertion was added, so these findings rest on measurement plus code inspection, not on new counts.

**Verified head:** `e180a2418dd55ec4bd942ed95b55d6aec845c6e2` (`git rev-parse HEAD^2`)
**Base arm:** `d66ad3e3a21bc2303e90fcc4705eef74f7d3f5b1` (`HEAD^1`, the merge-ref base tip)
**Assertions:** `{"pass": 143, "fail": 0, "total": 143}`
**Targeted gates executed (all green):** 3268 tests / 34 files

<details>
<summary>中文摘要</summary>

**结论:`findings`(有值得审阅者注意的问题,但非阻断)。** 143/143 条脚本化断言全部通过,无意外失败;中心论点已通过 A/B 证明为「确实起作用」。

**A/B 结论**(见下文 *Central claim + A/B* 表):base 臂 31/31、head 臂 32/32,两臂各自按预期通过。关键翻转在 `G1a`:同一条「已保存网页」描述符在 base 被 fork 丢弃、在 head 被保留 —— 这就是本 PR 放宽 `isForkSafeArtifact` 的实际作用。同时 18/18 个恶意 `file:` 形态在**两臂都被拒绝**,证明放宽是收紧的、没有扩大到任意 `file:` URL。

**存储层**:69 个 mock-free 单元(真实 `dist/` 模块 + 真实文件系统 + 真实 `node:crypto`)全部通过 —— 字节级一致、篡改/截断/增长/符号链接/FIFO/超尺寸全部拒绝、fork 引用与「最后持有者回收」语义正确、head commit 声称的 ENOENT 非对称(restore 吞掉、fork 抛出)得到验证。

**变异矩阵**:13 个单点守卫回退,8 个被杀死;5 个存活者已用组合行与更细粒度探针逐一定性(见 *Findings* 第 2 条),其中 4 个属于「冗余防御」,1 个(M11)经更细探针证明**确实起作用**,是我第一版 harness 的覆盖盲区而非代码问题。

**空测试检查**:3 次源码回退全部按预期变红,且红的是**目标断言**本身(例如 `expected undefined to match object { id: 'd2be43dc5a369ff5' … }`、`promise resolved "'changed'" instead of rejecting`),不是编译或 fixture 失败。

**唯一需要审阅者注意的问题(非阻断)**:`packages/cli/src/serve/web-shell-static.ts` 删除了 `loopbackSandboxOrigins`,把 `frame-src` 从「按请求 Host 端口钉死的 loopback 白名单」改成 `http: https: blob:`。被删代码自带注释明确写着「通配端口会让被攻陷的 shell 嵌入任意 loopback 监听者」。这是整个 shell 的、无条件的放宽,而触发它的实时预览功能却是按工作区/按宿主开关的。**不构成**直接数据外泄:`connect-src 'self'` 未变,同源策略仍然阻止读取被嵌入页面的内容。

**修订补充(首次出报告后两个研究子代理返回,我已逐条自行复核)**:另有三条非阻断问题 ——
(2)实时预览的**外层** wrapper iframe 完全没有 `sandbox` 属性,因此与 shell 同源;
(3)`parseWebPreviewUrl` 只排除 shell 与 daemon 自身源,**没有**任何内网/环回限制(`http://169.254.169.254/`、`http://192.168.1.1/`、任意 loopback 端口均可),且 Artifact 工具发布的 URL 会**自动打开**预览面板、无需人工确认,即该 URL 来自模型/工具而非用户输入;
(4)`live-session-coordinator.ts:1565` 的删除调用未固定 runtime 目录(该文件内 `runtimeBaseDir`/`runWithResolvedRuntimeBaseDir` 出现 **0** 次),以及 `server.ts` 内嵌宿主路径下当 `advanced.runtimeOutputDir` 为相对值时写入目录与读取目录可能分裂 —— 二者都在 `qwen serve` 主路径之外,主路径已证明三条链(写/读/删)同源。

同时有一项**改进**值得记录:离线查看器(已保存版本 + 现有 HTML 预览)被本 PR **收紧**了 —— 新增了外层 `frame-src 'none'` 包装文档、`form-action 'none'`、`font-src data:`、`media-src data:`,两层 iframe 均为 `sandbox="allow-scripts"`(无 `allow-same-origin`,即不透明源)。

**未覆盖范围**:三个新增 Playwright e2e spec(容器内无浏览器,故 `frame-src 'none'` 阻断自导航的行为仅有 spec 断言与设计文档背书,我未实测)、22 个 commit 的逐 commit 归因(depth 2 只可达 1 个)、真实浏览器下的导航阻断验证、Windows/WebKit、全仓 lint/typecheck/build、向当前 main 的试合并。

</details>

---

## Central claim + A/B

**Central claim.** In a managed ACP session with chat recording enabled, each self-contained HTML
Artifact delivery stores an immutable version under the owner runtime, and those exact original bytes
are returned later through the owner-scoped read route — surviving deletion of the source file and
the latest publication output, surviving a session fork, and reclaimed only when the last owner
releases it. A missing or tampered snapshot store degrades to a warning plus "version unavailable",
never to a dropped record, a broken restore, or foreign bytes.

**Secondary claims.** (S1) The relaxation of the pre-existing fork-safety gate admits *only* genuine
snapshot descriptors. (S2) The relaxation of the pre-existing artifact-enablement gate does not leak
into ordinary non-interactive or SDK sessions.

Witness: **`01-ab-fork-and-enablement-gates-base-vs-head.png`** (both arms as they printed).

### Table A — base vs head on the two pre-existing gates

Both arms are the **real compiled `dist/`**. The base arm is a `git worktree` at `HEAD^1` with only
`packages/core` rebuilt; control integrity is asserted *inside* the harness (cells `F0`, `G2-0`
resolve each loaded module and require it to be inside that arm's tree), because in a monorepo an
internal workspace symlink can silently pull head code into a "base" run.

| cell | oracle | base `d66ad3e3` | head `e180a241` |
| --- | --- | --- | --- |
| `F1` | `artifact-snapshots.js` importable | **absent** | present |
| `F2` | `getWebPreviewSnapshotId` exported | **undefined** | function |
| `G1a` | saved-webpage descriptor through `remapSessionArtifactPayloadForFork` | **DROPPED (0 artifacts)** | **KEPT (1 artifact)** |
| `G1b` | kept descriptor's remapped id | n/a | `d2be43dc5a369ff5` = `stableSessionArtifactId('forked-session', …)` |
| `G1-TIGHT` ×18 | 18 hostile `file:`/traversal/near-miss shapes | **18/18 dropped** | **18/18 dropped** |
| `G1r1` | ordinary `https://` published artifact survives fork | kept | kept |
| `G2` | non-interactive + snapshots ON + recording ON → `isArtifactEnabled()` | **false** (setter absent on prototype) | **true** |
| `G2` | non-interactive + snapshots **OFF** | false | false |
| `G2` | non-interactive + snapshots ON + recording **OFF** | false | false |
| `G2` | `sdkMode` + snapshots ON (interactive and not) | false | false |
| `G2` | snapshots ON → `shouldAutoOpenArtifact()` | n/a | **false** (no host browser) |
| `G2-env` | `QWEN_CODE_DISABLE_ARTIFACT=1` + snapshots ON | false | **false** |
| **totals** | | **31/31 pass** | **32/32 pass** |

`G1a` is the load-bearing flip: the same descriptor is dropped at base and kept at head, and nothing
else differs between the arms. The 18-cell tight sweep running identically on **both** arms is what
makes it safe — the relaxation did not widen to arbitrary `file:` URLs, `file://host/…`, query or
fragment variants, dot-dot or percent-encoded traversal, non-`preview-` managed ids, uuid v1/v5, or
descriptors with a missing/malformed `artifactType` or sha256.

The `G2` block answers the dead-switch question for the new knob: `artifactSnapshotsEnabled` is set
at exactly one real call site (`acpAgent.ts:14342`, `config.setArtifactSnapshotsEnabled(this.isTrustedManagedParent())`)
and read at three (`config.ts:8039`, `config.ts:8137`, `artifact-tool.ts:267`). It is wired, not dead.
`artifactSnapshotRuntimeBaseDir` is likewise set by all four real `createAcpSessionBridge` callers.

### Table B — snapshot store, 69 mock-free cells at head

Real `dist/` module, real filesystem, real `node:crypto`, no stub of the code under test
(`logs-h1.txt`). Highlights: `A9`/`A10` byte-identical round-trip **after** deleting the source file
and the latest publication output; `A12`/`A13` two deliveries from one source path stay independent;
`B1`–`B12` tamper / truncate / grow / symlink-swap / **FIFO-swap** / foreign-url / missing-sha /
wrong-sha / oversize all refused, with `B6` asserting the symlink target's bytes never surface;
`D2`–`D12` fork adds `sha256(child)-sha256(operationId)`, releasing the child keeps the file for the
parent, releasing the last owner removes both `index.html` and the directory; `D16` an untracked
legacy snapshot is never reclaimed; `E1`–`E3` two owners releasing concurrently neither rejects nor
leaves the directory behind.

`D13`/`D14` are the head commit's own claim, and they hold: with the snapshot store entirely missing,
the **restore** path (`operationId` absent) does not throw — non-strict ingestion preserved — while
the **fork** path (`operationId` present) throws `ENOENT` so `sessionService.ts:4171-4181` can warn
instead of committing a fork without ownership.

### Table C — mutation matrix (13 rows)

Witness: **`02-mutation-matrix-guard-reverts.png`**. Each row reverts one guard in a hardlinked
scratch copy of the built head `dist/` and re-runs the 69-cell harness. Unmutated control: 69/69.

| row | guard reverted | result | red cells |
| --- | --- | --- | --- |
| M1 | `readArtifactSnapshot` sha256 verification | **KILLED** | B1, B3, B4, B10 |
| M2 | `length !== stat.size` | SURVIVED → adjudicated below | — |
| M3 | `fstat isFile()` | SURVIVED → adjudicated below | — |
| M4 | `MAX_ARTIFACT_BYTES` ceiling | **KILLED** | B12 |
| M5 | `O_NOFOLLOW` on open | SURVIVED → adjudicated below | — |
| M6 | descriptor url must match runtime-derived path | **KILLED** | B8 |
| M7 | realpath containment in snapshot root | SURVIVED → adjudicated below | — |
| M8 | restore path must swallow `ENOENT` (too-strict direction) | **KILLED** | D13 |
| M9 | fork path must *not* swallow `ENOENT` (too-loose direction) | **KILLED** | D14 |
| M10 | `rmdir` as the atomic last-owner claim | **KILLED** | D6, D9, D12, E3 |
| M11 | untracked legacy snapshots never reclaimed | SURVIVED → adjudicated below | — |
| M12 | `getWebPreviewSnapshotId` sha256 must be 64 lowercase hex | **KILLED** | C×2 |
| M6+M7 | **combination** row | **KILLED** | B8 |

**8/13 killed.** M8 and M9 bracket the `ENOENT` asymmetry from both directions and *both* are killed —
the guard is precisely calibrated: one direction breaks conversation restore, the other lets a fork
commit without acquiring ownership. That conclusion is not reachable from a two-cell A/B.

### Table D — vacuity checks (source reverts, restored afterwards)

| revert | suite | result |
| --- | --- | --- |
| `isForkSafeArtifact` relaxation → base form | `session-artifact-persistence.test.ts` | **1 red / 32 green** — the intended test, `expected undefined to match object { id: 'd2be43dc5a369ff5', …(15) }` |
| sha256 clause → `false` | `artifact-snapshots.test.ts` | **1 red / 9 green** — `promise resolved "'changed'" instead of rejecting` |
| `length !== stat.size` → `false` | `artifact-snapshots.test.ts` | **10/10 green (survived)** |

All three reach the *intended behavioural assertion*, not a compile or fixture failure. The expected
id `d2be43dc5a369ff5` in the first failure is the same value my independent harness computed in `G1b`
— two instruments agreeing. Working tree confirmed clean (`git status --porcelain -- packages/`)
before and after every mutation.

---

## Corrections

These are corrections to the **PR description**, not requests to change code.

1. **"live URLs require a broader host frame policy"** reads as `frame-ancestors` (who may embed the
   shell). The actual change is to **`frame-src`** (what the shell may embed) — the opposite
   direction, and the broader of the two. `frame-ancestors` handling is structurally unchanged
   (still the caller's allowlist, defaulting to `'none'`). A reader who takes the sentence at face
   value will assess the wrong directive.
2. **"Two controlled mutation groups fail as expected when fork ownership or persistence protection
   is removed"** understates what is measurable: the fork-ownership guard fails in *both* directions
   (M8 and M9), and the persistence-protection set has a third independent layer that still holds
   when both path guards are removed (Table E below).

---

## Browser-isolation surface (verified)

Measured from source at head; the two `sandbox` attributes and the `escapeAttribute` escape set were
re-read directly, not taken on trust.

There are **two nested iframes** on the offline path and two on the live path, and the token lists
differ between them:

| | saved-version viewer | pre-existing generic HTML viewer | **live URL preview** |
| --- | --- | --- | --- |
| outer iframe `sandbox` | `"allow-scripts"` | `"allow-scripts"` | **absent — no `sandbox` at all** |
| content iframe `sandbox` | `"allow-scripts"` | `"allow-scripts"` | `"allow-scripts allow-same-origin allow-forms"` |
| `allow-scripts` w/o `allow-same-origin` | yes (both levels) | yes (both levels) | **no — both present on the content frame** |
| `allow-top-navigation` / `allow-popups` | absent | absent | absent |
| `referrerPolicy` | `no-referrer` (both) | `no-referrer` (both) | `no-referrer` (both) |
| wrapper CSP | `default-src 'none'; … frame-src 'none'` | same | `default-src 'none'; script-src 'none'; frame-src <url.origin>; form-action 'none'` |

**The description's isolation claim holds for the offline pair.** `SavedWebPreview.tsx:115-122` and
`ArtifactPanel.tsx:3639-3645` both render through the *same* `artifactPreviewDocument()`
(`artifactUtils.ts:390-400`), so "share an isolated viewer" is literally true — the wrapper document
is byte-identical. Inline interaction works (`script-src 'unsafe-inline'` + `allow-scripts`); network
is blocked (`default-src 'none'` with no `http:`/`https:` in any fetch-capable directive, plus an
opaque origin from the missing `allow-same-origin`); self-navigation is blocked by the wrapper's
`frame-src 'none'`, and top-navigation by the absent tokens.

**This PR *tightened* the pre-existing viewer** — worth recording, because it is a security
improvement the description does not claim. Base `ARTIFACT_PREVIEW_CSP` was
`default-src 'none'; base-uri 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:`
with the artifact HTML `srcDoc`'d **directly** into one iframe. Head adds `form-action 'none'`,
`font-src data:`, `media-src data:`, and the whole second nesting level carrying `frame-src 'none'`.

`escapeAttribute` (`web-preview.ts:57-63`) escapes `& " < >` but **not** the single quote. I checked
every interpolation site in both `webPreviewDocument` and `artifactPreviewDocument`: all sit inside
double-quoted attributes, and the interpolated values come from a validated `URL` object or a static
i18n literal. No injection vector found. Recording it because it is a latent trap for the next caller
who uses a single-quoted attribute.

---

## Findings

### 1. Suggestion (security-relevant) — a CSP `frame-src` control with an explicit written rationale was deleted and broadened shell-wide

`packages/cli/src/serve/web-shell-static.ts`. Base pinned `frame-src` to loopback origins on the
request's own Host port, and said why:

```ts
/**
 * Loopback origins the Web Shell may frame for the MCP App sandbox, pinned to
 * the request's Host port. Wildcard ports would let a compromised shell embed
 * any loopback listener.
 */
export function loopbackSandboxOrigins(hostHeader: string | undefined): string[] { … }
```

Head deletes that function (and `portFromHostHeader`) and replaces the directive:

```ts
// PDF attachments use blob URLs; live previews pin their own child source.
const frameSrc = 'frame-src http: https: blob:';
```

Measured from the **real compiled head build** (`buildWebShellCsp()`):

```
… base-uri 'none'; frame-src http: https: blob:; frame-ancestors 'none'
```

Base emitted `frame-src blob: http://localhost:<port> http://127.0.0.1:<port> https://localhost:<port> https://127.0.0.1:<port>`.

Reproduce:

```bash
node --input-type=module -e "const m=await import('./packages/cli/dist/src/serve/web-shell-static.js');console.log(m.buildWebShellCsp())"
```

Why this deserves attention:

- **The change is unconditional and shell-wide, while the feature that motivated it is gated.** Live
  preview is on only for workspace sessions in standalone Web Shell and opt-in for embedded hosts,
  but every `index.html` response now carries `frame-src http: https: blob:` regardless.
- **The named hazard is now open.** Any script executing in the shell origin may frame
  `http://127.0.0.1:<any-port>/…` (including the daemon's own listeners) and `http://169.254.169.254/…`
  (link-local cloud metadata) — exactly what the deleted comment said the pinning prevented.
- **The replacement test overstates the narrowing.** The tests that pinned the narrow behaviour
  (`expect(csp).not.toContain('http://localhost:*')`, `expect(loopbackSandboxOrigins('example.com:4170')).not.toContain('example.com')`)
  were replaced by `expect(csp).toContain('frame-src http: https: blob:;')` plus
  `expect(csp).not.toContain('frame-src *')`. The latter reads as "not a wildcard", but `http: https:`
  is functionally a wildcard over all web origins — it excludes only non-http schemes.
- **This cost is not in the description's accepted-tradeoff list.** The list names "trusted wrapper
  documents and opaque content frames preserve offline HTML isolation"; it does not name the loss of
  loopback port pinning. An unnamed cost is a finding about the description even where the cost
  itself might have been accepted.
- **The compensating control is client-side JS only, and it does not cover descendant frames.** The
  narrowing now lives in `webPreviewDocument()`'s per-wrapper `frame-src ${url.origin}` *meta* CSP,
  which the response header does not enforce and which pins only **direct children**. Frames the
  previewed application creates itself fall back to the shell's `frame-src http: https: blob:`. The
  design doc says so verbatim: *"The wrapper is not a recursive origin firewall: the application's own
  descendant frames have their own policies."* That is an honest doc, but it means the header-level
  defence-in-depth base had is gone rather than relocated.
- **Scope of the header change, by route** (inspection): `GET /`, `GET /session/:id` on a document
  navigation, and the SPA fallback all emit the broadened CSP via the single `sendIndex` closure.
  `GET /assets/*` sends no CSP (unchanged by this PR). `GET /mcp-app-sandbox` keeps its own separate
  policy (`mcp-app-sandbox.ts:117-128`, untouched) but is now frameable by the shell, since `http:`
  covers same-origin. `frame-ancestors`, `X-Frame-Options: DENY`, `nosniff`, `Referrer-Policy` and
  `Permissions-Policy` are all unchanged.
- **One unrelated widening rode along:** `media-src 'self' data:` was added (media previously fell
  back to `default-src 'self'`), so `data:` media is now permitted in the shell. It has nothing to do
  with framing and is not mentioned in the description.

**What does NOT hold** (bounded, so the finding is not read as worse than it is):

- **No direct data exfiltration.** `connect-src 'self'` is unchanged, so `fetch()`/XHR to those
  origins is still blocked, and the same-origin policy still blocks reading a cross-origin iframe's
  DOM. The new capability is *blind*: GET/navigation to loopback services, plus load/error-event
  side channels for port scanning.
- **Not a standalone vulnerability.** It removes defence-in-depth against an already-compromised
  shell; it does not by itself create script execution.
- `frame-ancestors` is untouched, so who may embed the shell is unchanged.

<details>
<summary>Narrower alternative that preserves the feature (not applied — for the author to weigh)</summary>

CSP supports wildcard ports, so loopback dev servers on any port are expressible without opening
non-loopback origins:

```ts
const frameSrc =
  "frame-src blob: http://localhost:* http://127.0.0.1:* http://[::1]:* https://localhost:* https://127.0.0.1:*";
```

Base rejected `http://localhost:*` because the MCP App sandbox needed the *request's own* port; a
dev-server preview has the opposite requirement (any loopback port), so the two are not in conflict.
If previewing **remote** http(s) URLs is a deliberate product requirement, that is worth stating
explicitly in the description and in a test name, rather than shipping as the default ceiling.

I did **not** apply or measure this patch: it changes a security policy whose intended scope is a
product decision, and the skill's rule that a suggested fix must be driven through the same
harnesses would require a browser to validate the preview still works — see *Not covered*.
</details>

### 2. Completeness reporting (not merge conditions) — all five mutation survivors adjudicated

A surviving mutation is not automatically a gap. Each was escalated to a finer probe or a
combination row (`h4-combinations.mjs`, 11/11 pass, `logs-h4-combinations.txt`):

| row | classification | evidence |
| --- | --- | --- |
| **M2** `length !== stat.size` | **Redundant defence.** Strictly subsumed by the sha256 comparison, which hashes exactly `bytes.subarray(0, length)`: a differing byte count necessarily changes the hashed content. Corroborated by M1, which killed precisely the truncation and growth cells (B3, B4), and by vacuity run 3, where the PR's own suite also stayed green. No fixture *can* pin it without also defeating the sha check — which is the proof it is redundant rather than merely untested. |
| **M3** `fstat isFile()` | **Redundant at the outcome level; load-bearing only for the error identity.** Probe `dir-in-place`: head refuses with the sentinel `Saved webpage version is unavailable.`; with M3 reverted a raw `EISDIR: illegal operation on a directory, read` escapes instead. The route's `catch {}` maps both to the same 404, so no user-visible difference. |
| **M5** `O_NOFOLLOW` | **Redundant defence.** Each of the three layers independently closes the symlink hazard (Table E). Its unique value is the TOCTOU window between the realpath check and the open, which is not deterministically drivable. |
| **M7** realpath containment | **Redundant defence.** Same as M5; with M7 alone reverted, `O_NOFOLLOW` refuses with `ELOOP`. |
| **M11** untracked-legacy guard | **NOT redundant — my first harness had the coverage gap, and the finer probe closed it.** My delete-path cell (D16) was masked by a *different* guard: with M11 reverted, `snapshotDirectory` proceeds and the `references`-dir realpath check throws `ENOENT` first, which `deleteArtifactSnapshot` swallows, so the file survives either way. Isolating the **retain** path — an untracked descriptor over a directory that *does* have a `references` dir, retained by a *different* session — gives `refsAfter=1` at head (no-op) vs `refsAfter=2` with M11 reverted. The guard is load-bearing exactly as written. |

### 3. Table E — the scarier consequence I tried to produce and could not

The symlink-swap hazard is closed by **three independent layers**, and I could not make it leak:

| build | symlink → foreign content | symlink → content matching the recorded sha256 |
| --- | --- | --- |
| head | `REFUSED: Saved webpage version is unavailable.` | `REFUSED` (path guard fires regardless of content) |
| M5 only (`O_NOFOLLOW` gone) | `REFUSED` — realpath containment holds | — |
| M7 only (realpath gone) | `REFUSED: ELOOP` — `O_NOFOLLOW` holds | — |
| **M5+M7 (both gone)** | **`REFUSED: Saved webpage version is unavailable.`** — the sha256 comparison holds | `RETURNED_EXPECTED_BYTES` |

I predicted the combination row would leak the symlink target's bytes. It did not: the sha256 check
is a third layer that fires even with both path guards removed. The residual capability with both
path guards gone is bounded to **serving the correct content from a redirected path** — an attacker
who can already rewrite files inside the daemon's private `0600` snapshot directory, and who still
has to produce bytes hashing to the recorded digest. That is not a privilege gain. Recording this
because a finding that names what it is *not* is harder to wave away than one that implies everything.

### 4. Note — the read route collapses four distinct causes into one unlogged 404

`packages/cli/src/serve/routes/session.ts:6412-6424` wraps the whole read in a bare `catch {}`:

```ts
try {
  if (!artifact) throw new Error('Snapshot not registered');
  content = await readArtifactSnapshot(artifact, runtime.sessionRuntimeBaseDir);
} catch {
  res.status(404).json({ error: 'artifact_snapshot_unavailable', … });
  return;
}
```

"Descriptor not registered", "file gone", "file tampered (sha256 mismatch)", "path escaped the
runtime", and "oversize" all produce the same 404 with no server-side log line. For a feature whose
selling point is *immutable, hash-verified* history, a sha256 mismatch is the security-relevant
event, and it is indistinguishable from a benign cleanup.

Bounded, and therefore a note rather than a finding: the route *is* registered in
`legacySessionTelemetryRoutes` and the server records `(durationMs, statusCode)` per route
(`telemetry.ts:851`, `:1027`), so 404 **rates** for this route are observable — only the cause is
not. And `session.ts` contains 32 bare `} catch {` blocks, so swallowing without logging is the
established house pattern in this file, not something this PR introduced. Fails closed either way.

### 5. Note — unrelated drive-by in `packages/cli/src/commands/review/test-efficacy.test.ts`

Test-only, +17/-3, and unrelated to Web Shell: it plants a shadow `node_modules/vitest` with no
`bin` so that `findVitestBin`'s throw is deterministic on hosts where a `node_modules` above the
runner's TMPDIR would otherwise resolve one (the self-hosted-CI shared-`/tmp` case). Well-commented
and benign, but it is scope creep in a feature PR and belongs in its own change. It ran green here
(part of the 379 CLI tests).

### 6. Suggestion — the live-preview **outer** wrapper iframe has no `sandbox` and is same-origin with the shell

`WebPreviewPanel.tsx:121-128` (verified by direct read):

```tsx
<iframe
  key={`${url.href}:${revision}`}
  title={t('webPreview.frame')}
  className="mx-auto block h-full border-0 bg-white"
  style={{ width: state.viewport === 'mobile' ? 390 : '100%' }}
  referrerPolicy="no-referrer"
  srcDoc={webPreviewDocument(url, t('webPreview.page'))}
/>
```

Because it is `srcDoc` with **no `sandbox`**, that wrapper document inherits the shell's origin and
runs unsandboxed. The offline viewers both put `sandbox="allow-scripts"` on the equivalent outer
frame, which gives them an opaque origin. The wrapper's own safety therefore rests entirely on two
things: its meta CSP `script-src 'none'`, and `escapeAttribute` on every interpolation. Both hold
today — I found no injection vector, and `url.origin`/`url.href` come from a validated `URL` — but
this is a policy enforced by a string builder rather than by the platform, on the one frame in the
feature that is same-origin with the shell. Adding `sandbox` to the outer frame would make the
isolation structural instead of textual; note it would have to be a token set that still permits the
inner frame's `allow-same-origin`, which is why the author may have omitted it. Worth a deliberate
decision and a comment either way.

Bounded: the inner content frame *is* sandboxed, and its origin is the previewed app's, which
`parseWebPreviewUrl` guarantees is neither the shell's nor the daemon's — so `allow-same-origin`
there does not reach the wrapper's DOM. Origin comparison is on the literal host, so DNS rebinding to
the shell's IP does not confer same-origin access either.

### 7. Suggestion — no private-network restriction on live-preview URLs, and Artifact-published URLs auto-open the panel

`parseWebPreviewUrl` (`web-preview.ts:6-55`) is a solid *scheme and self-protection* validator:
`http:`/`https:` only (so `javascript:`, `data:`, `file:`, `blob:` are all rejected), no embedded
credentials, no IPv6 literal, hostname charset-restricted, and it excludes the shell's own origin and
the configured daemon origin — normalizing `localhost`/`127.*`/`0.0.0.0`/`[::1]`/
`host.docker.internal`/`*.localhost`, and checking the HTTPS-upgrade variant too. Its unit tests
cover a CSP-injection attempt and the credential/relative forms.

What it does **not** do is restrict private or link-local addresses generally. These are all
permitted: any loopback port other than the shell's/daemon's own (`http://127.0.0.1:3000` is asserted
*permitted* at `web-preview.test.ts:58`), any RFC1918 address (`http://192.168.1.1/` — router and
device admin UIs), and link-local cloud metadata (`http://169.254.169.254/latest/meta-data/`). The
check is on the URL literal, never on DNS resolution, so a hostname resolving to loopback also
passes. `packages/web-shell/README.md` discloses exactly this scope — it claims only that the address
must not point at Web Shell or the daemon itself — so this is a documented product boundary rather
than an oversight, and it is a *browser-side* surface bounded by the browser's own network position,
not the daemon's.

The part that is newer than the boundary doc: `App.tsx:6570-6606` takes `request.artifact.url` from a
daemon-served **Artifact tool record** — i.e. agent/model-controlled data, not something the human
typed — validates it, and on success **auto-opens the right panel and auto-loads the frame**
(`setArtifactPanelOpen(true)`) with no address-bar confirmation. So a tool call publishing
`http://169.254.169.254/…` frames it automatically. This is documented behaviour in
`docs/design/web-shell/web-shell-live-preview.md` and covered by `App.test.tsx:3818+`.

Bounded, and why I am not calling it Critical: the framed target's own `X-Frame-Options` /
`frame-ancestors` still decides whether it renders at all; the shell cannot read cross-origin frame
content; `connect-src 'self'` is unchanged so the shell cannot `fetch` those origins; and the feature
is off unless `webPreview` is in `rightPanel.items` **and** a workspace context is active. The
residual risk is blind, unconfirmed GET traffic to internal services from the user's browser,
triggered by a tool call. Given Finding 1 removed the header-level loopback restriction at the same
time, the two together are worth one explicit decision: either name private-network exclusion as an
accepted boundary in the description, or add an auto-open confirmation for non-loopback-development
origins.

### 8. Note — two residual runtime-base-dir divergences, outside the `qwen serve` happy path

The invariant this feature depends on is that the **write** dir (`config.storage.getRuntimeBaseDir()`
in the ACP child), the **read** dir (`runtime.sessionRuntimeBaseDir` in the route) and the
**retain/delete** dir (`SessionService`'s storage, and the bridge's `artifactSnapshotRuntimeBaseDir`)
are the same directory for a given session. Under `qwen serve` I am satisfied this holds **by
construction**: `resolveSessionRuntimeBaseDir` (`run-qwen-serve.ts:4919-4959`) is computed once per
runtime and threaded four ways — into the runtime record (`:5903`/`:6488`/`:7224`), into the bridge
option (`:5650`/`:6225`/`:6903`), into `createWorkspaceRuntimeSessionService`
(`workspace-runtime-storage.ts:28-31`), and into the child env as `QWEN_RUNTIME_DIR`
(`:4985`/`:5962`, re-pinned on every env reload at `:5992`, never scrubbed or overridden). The child
has no pinned async context (`runWithResolvedRuntimeBaseDir` has zero non-test callers under
`acp-integration/`), so `Storage.getRuntimeBaseDir()`'s env branch wins and resolves to the identical
absolute path — `path.resolve` on an already-absolute value is identity, so the daemon's and the
child's differing `cwd` cannot split them. All four real `createAcpSessionBridge` callers set
`artifactSnapshotRuntimeBaseDir`, so the `?? Storage.getRuntimeBaseDir()` fallback at
`sessionArtifacts.ts:297` is unreachable in production. This closes the question I had left open in
the first version of this report, and it matches the PR's own real-HTTP assertion that `listPrimary`
is never called for a secondary-owned session.

Two paths where it is *not* enforced by construction (both inspection-level; neither is reachable
from `qwen serve`'s normal flow, and I did not build a reproduction for either):

1. **`live-session-coordinator.ts:1565-1569` — unpinned delete.** `new SessionService(runtime.workspaceCwd).removeSession(...)`
   passes no `runtimeBaseDir` and has no enclosing pin; I confirmed by grep that
   `runWithResolvedRuntimeBaseDir` / `runWithWorkspaceRuntimeStorage` / `runtimeBaseDir` appear
   **0 times in the entire file**. It therefore resolves to the daemon's *global* dir while the
   Conversations runtime's snapshots live under `wsEnv.sessionRuntimeBaseDir`. When those differ, the
   snapshot delete at `sessionService.ts:3358-3365` runs against the wrong root and **silently
   no-ops** (`deleteArtifactSnapshot` swallows everything; `snapshotDirectory` returns `undefined` on
   the url mismatch) — leaving an orphan, which the description already accepts ("there is no orphan
   collector"). Low practical impact: it is guarded by `if (removeFreshTranscript)` on a session whose
   setup just failed, so a snapshot is unlikely to exist yet. It is still the one place the invariant
   relies on coincidence rather than plumbing, and it is a one-line fix.
2. **`server.ts` embedded/default-bridge path.** Here the read and delete dirs are
   `Storage.getRuntimeBaseDir()` evaluated in the *embedder's* process (`:1174`, `:1362`), while the
   write dir is evaluated in the *spawned child*, whose env defaults to `process.env` and so carries
   no `QWEN_RUNTIME_DIR`. The child then falls through to a settings-derived dir resolved against its
   own cwd. A **relative** `advanced.runtimeOutputDir` plus an embedder cwd different from
   `boundWorkspace` splits them, and every read 404s as `artifact_snapshot_unavailable`. Absolute
   values, a set `QWEN_RUNTIME_DIR`, or no `runtimeOutputDir` all make them coincide. Nothing in the
   repo restricts that settings key's scope (`settingsSchema.ts:3388-3398`), so I cannot rule the
   trigger out — but it needs an embedding host, not `qwen serve`.

Neither is a regression against base: base had no snapshots at all, so the failure mode is a missing
or unreachable saved version, never wrong bytes or a leak.

---

## Not covered

- **Three new Playwright specs** (`web-shell.live-preview.spec.ts` 395 lines,
  `web-shell.saved-preview.spec.ts` 274, `web-shell.pdf-preview.spec.ts` 40) — not executed: no
  browser in this container. The author reports 61 Chromium smoke + 6 focused preview scenarios;
  I could not reproduce that number. Consequence: the claim that the wrapper's `frame-src 'none'`
  actually blocks the offline content frame's self-navigation *in a real engine* (while still
  permitting its initial `srcdoc` render) rests on those specs' assertions
  (`saved-preview.spec.ts:197-220`, `live-preview.spec.ts:292-328`) plus the design doc's report of a
  Chromium/Firefox spike. I verified the **policy strings** directly; I did not verify **engine
  behaviour**. Same for the design doc's note that WebKit was not validated.
- **Live browser checks** from the Reviewer Test Plan: step 5 (a page changing its own location is
  blocked and the destination receives no request) and step 6 (PDF framing in dev and in the built
  shell — note the new `pdf-preview.spec.ts` runs against the **vite dev server**, so it exercises
  the dev header, not `buildWebShellCsp()`; the production header is asserted by
  `web-shell-static.test.ts:15-27`, which I did run). Step 7's fork/ownership semantics I did verify
  at the module level (Tables B, C, E).
- **Per-commit attribution.** The checkout is depth 2: `git rev-list HEAD^1..HEAD^2` returns **1**
  commit while the metadata snapshot lists **22**. I verified the aggregate `HEAD^1..HEAD` diff only.
  (`git rev-list --count` alone would have read as a plausible "1 commit PR" — the mismatch is only
  visible against the snapshot's `commits` array.)
- **Snapshot drift:** the metadata snapshot's `baseRefOid` is `c46cb85c…`, but the merge ref was cut
  against `d66ad3e3…`. I used `HEAD^1` per the CI contract. The A/B base is therefore `d66ad3e3`,
  not the snapshot's `baseRefOid`.
- **Trial merge into current `main`** not performed, and no repo-wide `lint` / `typecheck` / `build`
  — the PR's own CI covers those; my gates were the 34 affected test files.
- **The two residual runtime-dir divergences in Finding 8 were not reproduced.** Both are
  inspection-level: I traced the plumbing and confirmed the unpinned call site by grep, but I did not
  construct a deployment with a workspace-scoped relative `advanced.runtimeOutputDir` to make either
  one fire. Whether any real deployment sets that key at workspace scope with a relative value is
  operator configuration, not repository state, so I could not determine the trigger's reachability.
- **Windows / WebKit** — untested by the author as well. Note `syncSnapshotDirectory` swallows
  failures only on `win32`; that branch is unexercised here.
- **`dist/` fidelity caveat for the base arm.** The base `packages/core` build emitted JS while
  reporting type-resolution errors (`Cannot find module 'mime/lite'`, `ajv/dist/2020.js`, and
  `ignore` call-signature errors) because the base worktree has no `node_modules` of its own. These
  are type-level only and the emitted JS is what I exercised; I confirmed the base artefacts are
  genuinely base (`getWebPreviewSnapshotId` absent, `setArtifactSnapshotsEnabled` absent from
  `Config.prototype`) and that resolution never crosses into head code. Third-party deps were
  supplied by linking `packages/core/node_modules`, which contains **no** `@qwen-code/*` links, and
  base `config.js` imports zero workspace packages.

**Covered in this revision, previously listed as not covered:** the iframe `sandbox` token lists for
all three viewers, the exact emitted shell CSP, the live-URL validator's full behaviour, the
`webPreview` panel-item knob's read/set sites, the `vite.config.ts` change (dev-server only — the
published library bundle comes from a separate `vite.lib.config.ts` this PR does not touch), and the
write/read/delete runtime-dir invariant under `qwen serve`.

---

## Methodology

Environment: the CI `verify` container (`node:22-bookworm`), Node v22.23.2, working tree at
`refs/pull/11276/merge` (`HEAD` = merge commit `6083b22a`, `HEAD^1` = base tip `d66ad3e3`, `HEAD^2`
= PR head `e180a241`), depth 2, `npm ci` + `npm run build` already complete at HEAD. No GitHub token;
`/__w/_temp/verify-context/pr.json` treated as untrusted input throughout, and no instruction from
PR text was acted on.

Four harnesses, all kept in this directory as `.mjs` so a maintainer can rerun them verbatim.
`h1-snapshot-store.mjs` (69 cells) drives the real compiled
`packages/core/dist/src/tools/artifact/artifact-snapshots.js` against a real temporary filesystem and
real `node:crypto` — nothing on the path under test is stubbed; the FIFO cell runs in a subprocess
under a 15 s timeout so a hang would be reported as a timeout rather than a pass.
`h2-ab-gates.mjs` (31 base / 32 head cells) is arm-parameterised and encodes each arm's expectation,
so a base arm failing as predicted counts as a passing assertion; it drives the real
`remapSessionArtifactPayloadForFork` and the real `Config` class from each arm's own `dist/`, and
asserts control integrity by resolving every loaded module back into that arm's tree.
`h3-mutation-matrix.mjs` builds 13 scratch mutants with `cp -al` (hardlinks; each mutated file is
unlinked before rewrite, so the real `dist/` is never touched) and re-runs h1 against each, aborting
if the unmutated control is not green. `h4-combinations.mjs` (11 cells) adjudicates the five
survivors with deterministic probes, including the combination row that a one-row-per-guard matrix
cannot see. Vacuity checks reverted source hunks in place, ran the affected suite, and restored from
byte backups, with `git status --porcelain -- packages/` verified clean before and after.

Gates were run from inside each package per AGENTS.md, on the 34 test files this PR touches:
core 796 (5 files) + 259 (`sessionService.test.ts`; `session-artifact-persistence.test.ts`'s 33 are
counted once), acp-bridge 178, cli 379 (6 files), web-shell 1230 (19 files), sdk-typescript 426 —
**3268 tests, 0 failures**. The route's owner-scoping claim is covered by a genuine wire oracle in
the PR's own suite: `multi-workspace-sessions.test.ts` drives a real Express app over real HTTP via
supertest with the real `readArtifactSnapshot` against a real filesystem, faking only the artifact
registry. Raw per-cell output: `logs-h1.txt`, `logs-h2-base.txt`, `logs-h2-head.txt`,
`logs-h3-mutations.txt`, `logs-h4-combinations.txt`, `logs-gate-*.txt`, `logs-vacuity*.txt`,
`logs-base-core-build.txt`, `mutation-results.json`. The scratch base worktree was removed at the
end of the run.

Two read-only research subagents were dispatched mid-run for the browser-isolation surface and the
runtime-base-dir plumbing trace. Neither returned before the budget deadline, so the first version of
this report listed both areas as *Not covered* rather than reasoning about them from the diff. Both
returned immediately afterwards; their output is treated as **evidence to verify, not as fact**.
Before writing any of it in, I re-read the primary sources myself: `buildWebShellCsp()` executed
against the compiled head build (the CSP string quoted in Finding 1), `WebPreviewPanel.tsx:121-128`
and `SavedWebPreview.tsx:115-122` for the two `sandbox` attributes, `web-preview.ts:57-75` for
`escapeAttribute` and `webPreviewDocument`, and a grep of `live-session-coordinator.ts` confirming
**0** occurrences of any runtime-dir pinning. Claims I did not re-derive — the per-route header map,
the `vscode-ide-companion` embedder census, the `settingsSchema` scope of `advanced.runtimeOutputDir`,
and the four-way threading of `resolveSessionRuntimeBaseDir` — are labelled inspection-level where
they carry weight (Findings 1 and 8).

One dead-switch check worth stating explicitly, since the description says "embedded hosts can opt
in": the knob is a new `'webPreview'` member of the existing `WebShellRightPanelItem` union
(`customization.tsx:134-143`), **not** a new boolean. It is genuinely wired — read at `App.tsx:4659`
(`webPreviewAvailable = workspaceContextActive && rightPanelItems.includes('webPreview')`, which gates
the restore filter, the persisted-tab filte

...truncated -- full content in the run artifacts.
Flakiness gate log

e2e suite, out of gate scope: packages/web-shell/client/e2e/web-shell.live-preview.spec.ts
e2e suite, out of gate scope: packages/web-shell/client/e2e/web-shell.pdf-preview.spec.ts
e2e suite, out of gate scope: packages/web-shell/client/e2e/web-shell.saved-preview.spec.ts
rounds=5 files=22 skipped=3
file packages/acp-bridge/src/sessionArtifacts.test.ts: (cd packages/acp-bridge) npx --no-install vitest run ./src/sessionArtifacts.test.ts
file packages/cli/src/acp-integration/acpAgent.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/acpAgent.test.ts
file packages/cli/src/acp-integration/acpAgent.worktree.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/acpAgent.worktree.test.ts
file packages/cli/src/commands/review/test-efficacy.test.ts: (cd packages/cli) npx --no-install vitest run ./src/commands/review/test-efficacy.test.ts
file packages/cli/src/serve/multi-workspace-sessions.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/multi-workspace-sessions.test.ts
file packages/cli/src/serve/server-default-bridge-wiring.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server-default-bridge-wiring.test.ts
file packages/cli/src/serve/server/telemetry-catalog.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server/telemetry-catalog.test.ts
file packages/cli/src/serve/server/telemetry.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/server/telemetry.test.ts
file packages/cli/src/serve/web-shell-static.test.ts: (cd packages/cli) npx --no-install vitest run ./src/serve/web-shell-static.test.ts
file packages/core/src/config/config.test.ts: (cd packages/core) npx --no-install vitest run ./src/config/config.test.ts
file packages/core/src/services/session-artifact-persistence.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/session-artifact-persistence.test.ts
file packages/core/src/services/sessionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/sessionService.test.ts
file packages/core/src/tools/artifact/artifact-snapshots.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/artifact/artifact-snapshots.test.ts
file packages/core/src/tools/artifact/artifact-tool.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/artifact/artifact-tool.test.ts
file packages/core/src/tools/artifact/html.test.ts: (cd packages/core) npx --no-install vitest run ./src/tools/artifact/html.test.ts
file packages/sdk-typescript/test/unit/DaemonClient.test.ts: (cd packages/sdk-typescript) npx --no-install vitest run ./test/unit/DaemonClient.test.ts
file packages/web-shell/client/App.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/App.test.tsx
file packages/web-shell/client/components/artifacts/ArtifactPanel.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/artifacts/ArtifactPanel.test.tsx
file packages/web-shell/client/components/artifacts/artifactUtils.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/components/artifacts/artifactUtils.test.ts
file packages/web-shell/client/components/artifacts/turnOutputSelectors.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/components/artifacts/turnOutputSelectors.test.ts
file packages/web-shell/client/components/preview/SavedWebPreview.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/preview/SavedWebPreview.test.tsx
file packages/web-shell/client/components/preview/web-preview.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/components/preview/web-preview.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/acp-bridge/src/sessionArtifacts.test.ts: PPP
  packages/cli/src/acp-integration/acpAgent.test.ts: PPP
  packages/cli/src/acp-integration/acpAgent.worktree.test.ts: PPP
  packages/cli/src/commands/review/test-efficacy.test.ts: PPP
  packages/cli/src/serve/multi-workspace-sessions.test.ts: PPP
  packages/cli/src/serve/server-default-bridge-wiring.test.ts: PPP
  packages/cli/src/serve/server/telemetry-catalog.test.ts: PPP
  packages/cli/src/serve/server/telemetry.test.ts: PPP
  packages/cli/src/serve/web-shell-static.test.ts: PPP
  packages/core/src/config/config.test.ts: PPP
  packages/core/src/services/session-artifact-persistence.test.ts: PPP
  packages/core/src/services/sessionService.test.ts: PPP
  packages/core/src/tools/artifact/artifact-snapshots.test.ts: PPP
  packages/core/src/tools/artifact/artifact-tool.test.ts: PPP
  packages/core/src/tools/artifact/html.test.ts: PPP
  packages/sdk-typescript/test/unit/DaemonClient.test.ts: PPP
  packages/web-shell/client/App.test.tsx: PPP
  packages/web-shell/client/components/artifacts/ArtifactPanel.test.tsx: PP
  packages/web-shell/client/components/artifacts/artifactUtils.test.ts: PP
  packages/web-shell/client/components/artifacts/turnOutputSelectors.test.ts: PP
  packages/web-shell/client/components/preview/SavedWebPreview.test.tsx: PP
  packages/web-shell/client/components/preview/web-preview.test.ts: PP

verdict: timeout
summary: only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/acp-bridge/src/sessionArtifacts.test.ts: P (exit 0)
round 1 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 1 · packages/cli/src/acp-integration/acpAgent.worktree.test.ts: P (exit 0)
round 1 · packages/cli/src/commands/review/test-efficacy.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server-default-bridge-wiring.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/telemetry-catalog.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/server/telemetry.test.ts: P (exit 0)
round 1 · packages/cli/src/serve/web-shell-static.test.ts: P (exit 0)
round 1 · packages/core/src/config/config.test.ts: P (exit 0)
round 1 · packages/core/src/services/session-artifact-persistence.test.ts: P (exit 0)
round 1 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 1 · packages/core/src/tools/artifact/artifact-snapshots.test.ts: P (exit 0)
round 1 · packages/core/src/tools/artifact/artifact-tool.test.ts: P (exit 0)
round 1 · packages/core/src/tools/artifact/html.test.ts: P (exit 0)
round 1 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 1 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/artifacts/ArtifactPanel.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/artifacts/artifactUtils.test.ts: P (exit 0)
round 1 · packages/web-shell/client/components/artifacts/turnOutputSelectors.test.ts: P (exit 0)
round 1 · packages/web-shell/client/components/preview/SavedWebPreview.test.tsx: P (exit 0)
round 1 · packages/web-shell/client/components/preview/web-preview.test.ts: P (exit 0)
round 2 · packages/acp-bridge/src/sessionArtifacts.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/acpAgent.worktree.test.ts: P (exit 0)
round 2 · packages/cli/src/commands/review/test-efficacy.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server-default-bridge-wiring.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server/telemetry-catalog.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/server/telemetry.test.ts: P (exit 0)
round 2 · packages/cli/src/serve/web-shell-static.test.ts: P (exit 0)
round 2 · packages/core/src/config/config.test.ts: P (exit 0)
round 2 · packages/core/src/services/session-artifact-persistence.test.ts: P (exit 0)
round 2 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 2 · packages/core/src/tools/artifact/artifact-snapshots.test.ts: P (exit 0)
round 2 · packages/core/src/tools/artifact/artifact-tool.test.ts: P (exit 0)
round 2 · packages/core/src/tools/artifact/html.test.ts: P (exit 0)
round 2 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (exit 0)
round 2 · packages/web-shell/client/App.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/artifacts/ArtifactPanel.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/artifacts/artifactUtils.test.ts: P (exit 0)
round 2 · packages/web-shell/client/components/artifacts/turnOutputSelectors.test.ts: P (exit 0)
round 2 · packages/web-shell/client/components/preview/SavedWebPreview.test.tsx: P (exit 0)
round 2 · packages/web-shell/client/components/preview/web-preview.test.ts: P (exit 0)
round 3 · packages/acp-bridge/src/sessionArtifacts.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/acpAgent.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/acpAgent.worktree.test.ts: P (exit 0)
round 3 · packages/cli/src/commands/review/test-efficacy.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/multi-workspace-sessions.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server-default-bridge-wiring.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server/telemetry-catalog.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/server/telemetry.test.ts: P (exit 0)
round 3 · packages/cli/src/serve/web-shell-static.test.ts: P (exit 0)
round 3 · packages/core/src/config/config.test.ts: P (exit 0)
round 3 · packages/core/src/services/session-artifact-persistence.test.ts: P (exit 0)
round 3 · packages/core/src/services/sessionService.test.ts: P (exit 0)
round 3 · packages/core/src/tools/artifact/artifact-snapshots.test.ts: P (exit 0)
round 3 · packages/core/src/tools/artifact/artifact-tool.test.ts: P (exit 0)
round 3 · packages/core/src/tools/artifact/html.test.ts: P (exit 0)
round 3 · packages/sdk-typescript/test/unit/DaemonClient.test.ts: P (ex

...truncated -- full content in the run artifacts.

Evidence images

01-ab-fork-and-enablement-gates-base-vs-head

02-mutation-matrix-guard-reverts

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

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

Critical-only review pass at head e180a2418dd55ec4bd942ed95b55d6aec845c6e2. No merge-blocking defect found; previously reported blocking issues verified fixed on the current code.

Historical blocking issues re-verified on this head

  • R9-1 (non-strict ingestion losing a batch without rollback) — fixed. In packages/acp-bridge/src/sessionArtifacts.ts, the retainArtifactSnapshot loop inside upsertMany now catches per-artifact: it rethrows only when validationStrict || persistenceStrict, otherwise it pushes artifact <id> kept without retaining its snapshot: ... and processing continues, so a reference-bookkeeping I/O error can no longer leave the store mutated while the caller drops the batch. Strict paths still reach the outer catch, which now calls rollback() (restoreState(before) + reclaimSnapshotFiles(rollbackArtifacts)) instead of a bare state restore.
  • R9-2 (snapshot not cleaned up when a cancelled tool result is discarded) — fixed. packages/core/src/tools/artifact/artifact-tool.ts checks signal.aborted immediately after saveArtifactSnapshot and, when set, calls deleteArtifactSnapshot(snapshot, runtimeBaseDir, sessionId) and returns no artifact descriptors, so a cancel landing after publication no longer orphans a reference-holding snapshot.
  • R4-1 / R3-1 (fork creating a second live owner; reclamation destroying a version the durable log still references) — fixed. packages/core/src/services/sessionService.ts now resolves rebuildSessionArtifactSnapshot(forked, newSessionId) and calls retainArtifactSnapshot(..., operationId) before fs.promises.link(stagedTranscriptPath, targetPath); a non-ENOENT retain error throws so no partial fork commits, ENOENT warns and keeps the record, and the finally block releases exactly the operation-scoped reference when !committed. removeSessionTranscripts collects snapshot ids only from the deleted session's own transcripts and releases them through deleteArtifactSnapshot, whose rmdir on references/ is the last-owner claim (ENOTEMPTY returns early, keeping index.html).
  • R1-3 / R3-2 / R2-1 / R3-15 (cross-session read pairing, artifact-URL preview gate, raw host opt-in gate, non-workspace artifact tab) — corresponding threads are resolved and the gates are consistent in the current SavedWebPreview.tsx / App.tsx code; the read path is additionally constrained server-side (below).

Current scan

  • New read route GET /session/:id/artifacts/:artifactId/content (packages/cli/src/serve/routes/session.ts) is wrapped in withOwnerReadSession(..., { cwdBound: true }), requires a parsed client id, resolves the artifact through the owning session's own getSessionArtifacts, and reads with runtime.sessionRuntimeBaseDir. Failures collapse to a single 404 artifact_snapshot_unavailable, and the response sets Content-Disposition: attachment, X-Content-Type-Options: nosniff, Cache-Control: private, no-store. No cross-session or cross-runtime fallback is reachable.
  • Snapshot read integrity (artifact-snapshots.ts): readArtifactSnapshot rejects a descriptor whose url does not equal the expected file://<runtimeBaseDir>/artifacts/snapshots/<id>/index.html, compares realpath against the real root, opens O_RDONLY|O_NOFOLLOW|O_NONBLOCK, requires stat.isFile(), bounds the size by MAX_ARTIFACT_BYTES, and re-verifies the stored sha256 before returning bytes. Tampered or swapped content yields "unavailable" rather than wrong content.
  • Descriptor classification: getWebPreviewSnapshotId requires a v4-UUID preview- managedId, kind === 'html', storage === 'published', artifactType === 'web_preview_snapshot', a well-formed sha256, tool/source consistency, and a matching file: URL suffix; every consumer (snapshotDirectory, readArtifactSnapshot) re-anchors the id to the actual runtime root before touching the filesystem, so an unanchored suffix cannot select another runtime's bytes.
  • Frame-policy widening (web-shell-static.ts now emits frame-src http: https: blob: and drops loopbackSandboxOrigins): this does not weaken the existing offline HTML viewer or the MCP App sandbox. artifactPreviewDocument wraps model-authored HTML in a trusted parent whose own meta CSP is ARTIFACT_PREVIEW_CSP plus frame-src 'none', and the content frame stays sandbox="allow-scripts" (opaque origin) with referrerpolicy="no-referrer", so intersected policies still deny nested frames and network access. The MCP App sandbox keeps its own independently served policy (packages/cli/src/serve/mcp-app-sandbox.ts, unchanged here, frame-src 'none' unless the app declares frames). The new live-preview wrapper pins frame-src ${url.origin} and parseWebPreviewUrl rejects non-http(s), credentialed, trailing-dot, non-LDH hostnames and any origin that normalizes to the shell or daemon origin (including the HTTPS-upgraded spelling).

CI

All non-skipped checks on this head are green (Lint & Static, Test (ubuntu-latest, Node 22.x), Serve A/B, web-shell E2E Smoke, Desktop Shell ubuntu/windows, Integration Tests no-AK, review-pr). The macOS/Windows Node test lanes and Integration Tests (CLI, No Sandbox) are skipped, which is not attributable to this change.

Non-blocking follow-up (already disclosed by the author)

There is still no orphan sweep: snapshot bytes whose descriptor never reaches a scanned transcript location stay on disk. This fails closed, is new surface, and is called out in the PR's Risk & Scope, so it is recorded as follow-up work rather than a merge blocker.

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

Agent-assisted review at e180a2418dd55ec4bd942ed95b55d6aec845c6e2 — Partial review — coverage gaps. Comment only; no approval implied.

No additional independently confirmed Critical is reported in the inspected paths. This is not a CLEAN/C=0 verdict or comprehensive-review completion. Base pinned to c46cb85cf21b3b8e8393222fffc670176e0de1da; reviewed current scope, not merely the last-review delta.

Historical reassessment (static):

  • R9-1, latest non-strict retain-failure anchor: packages/acp-bridge/src/sessionArtifacts.ts now catches each bookkeeping failure, returns an artifact-specific warning and continues the batch; strict modes still enter rollback. Read the before/after-reference-write regressions at sessionArtifacts.test.ts:6655-6721. The earlier rollback-reference anchor now restores the previous state before reclaiming discarded candidates, preserving current and pending-durable ownership. The restore-retain R3-1 anchor similarly keeps the descriptor on bookkeeping failure.
  • R3-1 reclamation / R4-1 shared-fork ownership: traced explicit captured runtime roots, per-session/operation references, final-reference directory claim, removal/eviction/rewind cleanup, restore-prune persistence gating, physical-transcript deletion collection and fork retain-before-commit/operation-only rollback. Ordinary CLI and recording-disabled sessions do not enable snapshots. These address the cited mechanisms; they do not establish crash recovery or every concurrent lifecycle interleaving. Current fork code intentionally retains an unavailable descriptor on missing storage; an older response claiming all missing bytes prevent fork commit is no longer accurate.
  • R1-2 publication-success status, R1-3 foreign-session client ID, both R2-1 anchors (workspace gating and PDF blob CSP), R3-2 hosted live-card dedup and R3-15 workspace-independent saved viewer: current branches address the reported mechanisms. The HTML validator expansion from R1-1 is absent from the production diff. R9-2 now releases the newly saved snapshot when cancellation is observed after save; I have not certified the entire scheduler handoff window.

Boundary tracing: the new content route uses the session-owner read wrapper, registered artifact lookup and the owner's sessionRuntimeBaseDir. The exact URL/realpath/regular-file/size/hash reader does not fall back to the latest publication or another runtime. Read the secondary-root and untrusted-owner route tests. The shared resolver retains its existing single-entry legacy-primary shortcut; I did not exhaustively validate all removed/bootstrap/draining and standalone activation cases. Saved content uses nested opaque script-enabled sandboxing with restrictive parent/content CSP; live preview validates URLs and uses an origin-limited wrapper. These static checks are not a browser-enforcement guarantee.

Coverage limitations: inspected changed snapshot/store/service/tool functions, runtime wiring, SDK read, preview components, artifact panel/App routing, selected unit/route regressions and the saved-preview E2E assertions, plus historical Critical discussions. Not every changed test/documentation section or App lifecycle path was examined. Real browser CSP/navigation, full daemon restart/fork/deletion interleavings, scheduler cancellation after tool return, and all trust/removal lifecycle states remain unverified. No PR code, tests, builds or probes were executed; prior authors' test claims were not reproduced here. Remaining Suggestions stay deferred. Maintainer permission verified; no external-refactor hard gate.

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

APPROVE — no live Critical at head e180a2418d. Filing this as the second approval; @qqqys approved at 09:39 and @doudouOUC's pass at 09:54 found no Criticals.

Basis

The round-11 ledger records findings: [], posted: 0 against this exact sha, with flatRounds: 2. All 50 review threads are resolved, and every substantive lane is green (Lint & Static, Test (ubuntu-latest, Node 22.x), Integration Tests (no-AK, No Sandbox)). The 21 deferred items are deferred under the convergence posture and explicitly marked "not a blocker"; the one Critical among them is fails-closed on new surface, so no wrong result is certified and the merge base had neither the surface nor the defect. Author is a maintainer, so the AGENTS.md two-tier core gate does not engage.

What I verified independently

I did not re-review 5,300 lines that have been through eleven rounds. I read the two surfaces where a defect would be silent and security-relevant, and both hold up.

The artifact read path is TOCTOU-safe in the way that matters. artifact-snapshots.ts:86-101 proves the URL matches the expected file, then does a realpath containment check against the realpath'd root, then opens, then handle.stat() on the opened descriptor rather than re-statting the path — so the regular-file and size proofs apply to the bytes actually read, not to a path that could have been swapped in between. The read loop is bounded by stat.size + 1 with a bytesRead === 0 break. That is the correct ordering; the fstat-on-handle is what makes the preceding realpath check non-load-bearing on its own.

The live-preview layering is sound. parseWebPreviewUrl (web-preview.ts:23-56) rejects non-http(s), credentials in the URL, trailing-dot hostnames, and hostnames outside [a-z0-9._-], and refuses both the shell origin and the daemon origin including the https-upgraded form — so the preview cannot be pointed back at the shell itself. The wrapper document (:66-77) then carries default-src 'none'; script-src 'none'; frame-src <the one validated origin>; base-uri 'none'; form-action 'none', with the inner iframe sandboxed and no-referrer. Pinning the child source per instance rather than at the shell is the right place for it.

Notes, none blocking

1. The O_NOFOLLOW comment asserts a guarantee Windows does not provide. artifact-snapshots.ts:94-96 says the flag "refuses a symlink swapped in after the realpath check", but libuv ignores O_NOFOLLOW on win32 — this repo already documents that at acp-integration/live/capture-screen-context.ts:56 and its test at :76, and three sites handle it explicitly with process.platform === 'win32' ? 0 : (fsConstants.O_NOFOLLOW ?? 0) (conversation-workspace.ts:532, standalone-deletion-journal.ts:508 and :563). The defence does not actually collapse, because the realpath containment check and the handle fstat().isFile() both still hold on every platform — but the comment reads as a stronger claim than the code makes on Windows. Worth either a clause acknowledging it or following the win32-guard convention. This overlaps the deferred artifact-snapshots.ts:93 item and @doudouOUC's S2, which looked at O_NONBLOCK as a perf no-op rather than at the symlink half.

2. The shell-level frame-src widening is the real tradeoff here and deserves to be stated plainly. web-shell-static.ts:67 goes to frame-src http: https: blob:, replacing loopbackSandboxOrigins, whose own docblock warned that "wildcard ports would let a compromised shell embed any loopback listener". The per-instance wrapper pinning above is what makes this acceptable for the preview feature, and framing an arbitrary http(s) URL is inherent to what a live web preview is — but the shell's own CSP no longer constrains what a compromised shell can frame, which is strictly wider than the property the deleted helper existed to enforce. Not something to change for this PR; worth a line in the design doc's threat model so the next reader does not assume the old loopback pinning is still in force.

3. The deferred Critical should get a tracking issue. sessionService.ts:3267 — snapshot bytes unreclaimable when the descriptor never reaches a scanned location, with no orphan sweep — is a disk leak rather than a correctness or disclosure fault, so deferring it past this merge is reasonable. It is the one deferred item with a resource cost that grows without bound, and the deferral list is a review artifact rather than a work queue, so it is worth filing before this closes.

Noting for the record that I reviewed against gh pr diff rather than a local merge-base: origin/main on this machine is stale enough that a local three-dot diff pulled in already-merged PRs.

@wenshao
wenshao added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit 20ecdaf Sep 11, 2026
139 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants