fix(desktop): allow trusted response copy - #8587
Conversation
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR! Template looks good ✓ Problem: observed bug with solid evidence — #8538 reports the Desktop response copy button silently doing nothing on Windows (clipboard unchanged, reproduced across restarts), and the Direction: aligned — copying an assistant response is a baseline expectation, and clipboard-copy reliability is a recurring fix area in peer agents' changelogs (multiple Size: no core paths touched ( Approach: the scope feels right. The fix lands at the root cause (the session permission gate) rather than at one button — this also repairs the ~10 other Risk: no high-risk path matches; no elevated risk signals. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测到的 bug,证据充分 —— #8538 报告 Desktop 回复复制按钮在 Windows 上静默无效(剪贴板不变,重启后仍可复现),且 Desktop/ACP 模式下无法用 方向:对齐 —— 复制助手回复是基本预期;剪贴板复制可靠性也是同类产品 changelog 中的常见修复项(claude-code CHANGELOG 中有多条 规模:未触及核心路径(仅 方案:范围合理。修复落在根因(会话权限门)而不是单个按钮 —— 这同时修复了 desktop UI 中约 10 处因同样原因失效的 风险:高风险路径检测无命中;无升级风险信号。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewI wrote down my independent proposal before reading the diff — a narrow Things I verified against the base tree (not taken from the PR description):
No blockers found. Testing — the PR's own CI, via APIFinal CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Reading this table needs two caveats, both verified in the workflow files:
Net: no CI job executes the 9 permission-policy + 2 copy tests this PR adds. The author reports they pass on Linux — that is the author's claim, not independently re-run here, and this environment does not run PR-derived code. Even if the whole table lands green, the suite will not have exercised the change. Sandboxed verification would settle part of this: a maintainer-sponsored 中文说明代码审查在看 diff 之前我先写下了自己的独立方案:将 以下是在基线代码树中亲自核实过的(不是照抄 PR 描述):
未发现阻塞问题。 测试证据CI 表格见上(finalize 任务会在 CI 结束后原地更新)。两点必须说明(均已核对 workflow 文件):
结论:没有任何 CI 任务会执行本 PR 新增的 9 项权限策略测试 + 2 项复制测试。 作者报告这些测试在 Linux 上通过——这是作者自述,本审查未独立复跑(此环境不执行 PR 代码)。即使表格全绿,套件也并未真正验证该改动。 沙盒验证可以解决其中一部分:维护者可以发起 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — a clean, minimal fix for a real reproduced bug; my two reservations are about the verification infrastructure around it, not about the code. This is the PR I would have written for #8538. Before reading the diff I'd sketched the same shape: grant What keeps this at 4 rather than 5, named plainly:
Verdict: approve. CI is still running on this commit, though, so approval is deferred until CI lands green on 中文说明Confidence: 4/5 —— 对一个真实且可复现的 bug 来说,这是干净、最小的修复;两点保留意见针对的是它周围的验证基础设施,而不是代码本身。 这正是我为 #8538 会写出的 PR。读 diff 之前我勾勒过相同的方案:窄授权 为什么是 4 而不是 5,直说:
结论:approve。但该 commit 的 CI 仍在运行,因此批准将推迟到 CI 在该 commit 上全绿之后;finalize 任务届时会提交绑定该 commit 的批准。若有检查变红,则会改为标记提示。 — Qwen Code · qwen3.8-max Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline.
中文说明
已审查——无阻断问题。 建议见行内评论。
— qwen3.8-max via Qwen Code /review (v0.21.5)
| if (canWriteClipboard(wc, permission, details)) { | ||
| callback(true) | ||
| return |
There was a problem hiding this comment.
[Suggestion] The default-session handler wiring — both setPermissionRequestHandler and setPermissionCheckHandler delegating to canWriteClipboard before the non-voice deny path — has no test; only the extracted pure policy function canUseDefaultSessionClipboard is unit-tested. — Failure scenario: a future refactor that deletes or reorders either early return (or drops the check handler's while keeping the request handler's) lets clipboard-sanitized-write fall into the !VOICE_PERMISSIONS.has(permission) deny path — response copying silently fails on Windows again, the exact bug this PR fixes, while both new test files stay green.
Suggested fix: extract the handler decision logic into a testable form (mirroring the frame-trust extraction pattern) and test that a trusted workspace main frame is granted before the voice gate, that the check handler agrees, and that a non-workspace window is denied.
中文说明
默认会话处理器的接线部分——setPermissionRequestHandler 和 setPermissionCheckHandler 在非语音拒绝路径之前都委托给 canWriteClipboard——没有任何测试;目前只有被提取出来的纯策略函数 canUseDefaultSessionClipboard 有单元测试。失败场景:如果未来重构删除或调换了其中任一提前返回(或只保留了请求处理器的分支而删掉了检查处理器的),clipboard-sanitized-write 就会落入 !VOICE_PERMISSIONS.has(permission) 拒绝路径——回复复制会在 Windows 上再次静默失败(正是本 PR 修复的 bug),而两个新测试文件依然全绿。
建议修复:将处理器决策逻辑提取为可测试的形式(参照 frame-trust 的提取模式),并测试可信的工作区顶层 frame 在语音门之前被授权、检查处理器与请求处理器结论一致、未登记窗口被拒绝。
— qwen3.8-max via Qwen Code /review (v0.21.5)
| if (canWriteClipboard(wc, permission, details)) { | ||
| callback(true) | ||
| return |
There was a problem hiding this comment.
[Suggestion] The default-session scoping comment above these handlers ("Scope the grant to mic/media only — do NOT broaden the default session to every permission", lines 417-421) is now stale: this diff adds the clipboard-sanitized-write grant beneath it without updating it, so the only written statement of the default-session policy no longer matches the code. — Failure scenario: a maintainer auditing the handlers reads the stale comment and either concludes the clipboard grant is an unreviewed policy violation and removes it (re-breaking response copy), or treats the stale rule as authoritative and rejects a future narrow grant consistent with this precedent.
Suggested fix: update the comment alongside the grant — e.g. note that sanitized clipboard writes are likewise scoped to the trusted registered main-frame renderer (see canUseDefaultSessionClipboard) while every other default-session permission stays denied.
中文说明
这两个处理器上方的默认会话授权范围注释("Scope the grant to mic/media only — do NOT broaden the default session to every permission",第 417-421 行)现已过时:本 diff 在其下方新增了 clipboard-sanitized-write 授权却没有更新该注释,导致默认会话策略的唯一书面声明与代码不再一致。失败场景:维护者审计处理器时读到这条过时注释,要么认为剪贴板授权是未经审查的策略违规而将其移除(再次破坏回复复制),要么把过时的规则当作权威,拒绝未来与本先例一致的窄授权。
建议修复:在添加授权的同时更新注释——例如说明净化剪贴板写入同样限定于可信的已登记顶层 renderer frame(见 canUseDefaultSessionClipboard),其他所有默认会话权限仍保持拒绝。
— qwen3.8-max via Qwen Code /review (v0.21.5)
| }); | ||
|
|
||
| it.each([ | ||
| ['clipboard reads', { permission: 'clipboard-read' }], |
There was a problem hiding this comment.
[Suggestion] The rejection matrix never pins the distinction between clipboard-sanitized-write and the unsanitized clipboard-write, so any mutation broadening the permission comparison survives the suite. Probe-verified during this review: applying permission === 'clipboard-sanitized-write' || permission === 'clipboard-write' keeps 9/9 tests green while granting the raw write permission; adding the row below catches the mutation. — Failure scenario: a one-line broadening grants the renderer the raw, unsanitized pasteboard-write permission, and every current test still passes — the sanitized variant was chosen precisely to avoid that grant, but the suite does not encode it.
| ['clipboard reads', { permission: 'clipboard-read' }], | |
| ['clipboard reads', { permission: 'clipboard-read' }], | |
| ['unsanitized clipboard writes', { permission: 'clipboard-write' }], |
中文说明
拒绝矩阵没有钉住 clipboard-sanitized-write 与未净化的 clipboard-write 之间的区别,因此任何放宽权限比较的变异都能通过整个测试套件。本次审查中已用探针验证:应用 permission === 'clipboard-sanitized-write' || permission === 'clipboard-write' 变异后,9/9 测试仍然全绿,但实际上已授予了原始写入权限;加上下面这一行即可捕获该变异。失败场景:一行放宽就会让 renderer 获得未净化的原始粘贴板写入权限,而现有测试全部通过——选择净化变体正是为了避免这种授权,但测试套件没有把它固定下来。
— qwen3.8-max via Qwen Code /review (v0.21.5)
| import { describe, expect, it } from 'bun:test'; | ||
| import { canUseDefaultSessionClipboard } from '../default-session-permissions'; |
There was a problem hiding this comment.
[Suggestion] This new test file is unreachable from every test command this repository runs: packages/desktop is excluded from the npm workspaces ("!packages/desktop" in the root package.json), and no CI workflow in this repo executes the desktop bun suite (verified at the base commit). The test passes when run manually, but nothing in this repo gates the change it covers (also applies to copy-response.test.ts). — Failure scenario: a future commit reverts or breaks the clipboard permission wiring, and response copy silently stops working again — every CI check on this repo stays green because none of them collects this test; it is gated only if the upstream openwork repo's CI happens to run it.
Suggested fix: if desktop validation intentionally lives in the upstream openwork repo, confirm its CI runs this suite; otherwise wire a desktop bun-test job into this repo's CI.
中文说明
这个新测试文件对本仓库运行的所有测试命令都不可达:packages/desktop 被排除在 npm workspaces 之外(根 package.json 中的 "!packages/desktop"),且本仓库没有任何 CI 工作流执行 desktop 的 bun 测试套件(已在 base 提交上核实)。手动运行该测试可以通过,但本仓库没有任何门禁覆盖它所保护的改动(同样适用于 copy-response.test.ts)。失败场景:未来某个提交还原或破坏了剪贴板权限接线,回复复制再次静默失效——本仓库的所有 CI 检查依然全绿,因为没有任何一个会收集这个测试;只有当上游 openwork 仓库的 CI 恰好运行它时才有门禁。
建议修复:如果 desktop 验证有意放在上游 openwork 仓库,请确认其 CI 会运行该套件;否则在本仓库 CI 中接入一个 desktop bun-test 任务。
— qwen3.8-max via Qwen Code /review (v0.21.5)
| import { describe, expect, it, mock } from 'bun:test'; | ||
| import { copyResponseText } from '../copy-response'; |
There was a problem hiding this comment.
[Suggestion] Same unreachability as the permission-policy test: this file sits outside every npm workspace and no CI workflow in this repo runs the desktop bun suite, so the copy-failure status path it covers has no gate in this repository's pipeline (also applies to default-session-permissions.test.ts). — Failure scenario: if copyResponseText regresses — e.g. a refactor rethrows clipboard errors instead of returning { status: 'failed' } — TurnCard's localized copy-failure state disappears and copy clicks surface unhandled rejections, while this repo's CI remains green because nothing here ever executes the test.
Suggested fix: confirm the desktop suite runs in the upstream openwork repo's CI, or add a desktop job here.
中文说明
与权限策略测试相同的不可达问题:该文件位于所有 npm workspaces 之外,本仓库没有任何 CI 工作流运行 desktop 的 bun 套件,因此它所覆盖的复制失败状态路径在本仓库流水线中没有门禁(同样适用于 default-session-permissions.test.ts)。失败场景:如果 copyResponseText 退化——例如重构后重新抛出剪贴板错误而不是返回 { status: 'failed' }——TurnCard 的本地化复制失败状态会消失,复制点击会出现未处理的 rejection,而本仓库 CI 依然全绿,因为这里没有任何环节执行该测试。
建议修复:确认上游 openwork 仓库的 CI 会运行 desktop 套件,或在本仓库添加 desktop 任务。
— qwen3.8-max via Qwen Code /review (v0.21.5)
|
Thanks for the contribution, and apologies for the long wait. Closing because the Electron desktop app ( The current desktop is a thin Tauri shell around Web Shell. Copy/clipboard behavior now lives in the Web Shell layer — see #9485 for the clipboard work there. If the trusted-response-copy problem still reproduces in the current desktop, please open a new issue against the Web Shell experience and we can re-target the fix. |
What this PR does
Allows the Desktop app renderer to request
clipboard-sanitized-writefrom the default Electron session while keeping clipboard reads and unrelated permissions denied. The grant is limited to a registered workspace window, its top-level frame, and the trusted packaged or configured Vite renderer origin. Failed response-copy attempts now show a localized failure state on the copy action instead of failing only in the console.Why it is needed
The response copy button calls
navigator.clipboard.writeText(), but the default-session handlers currently deny every non-voice permission. Chromium therefore rejects the clipboard write before the response reaches the system clipboard.Reviewer Test Plan
How to verify
cd packages/desktop/apps/electron && bun test src/main/__tests__/default-session-permissions.test.tscd packages/desktop/packages/ui && bun test src/components/chat/__tests__/copy-response.test.tsThe policy tests confirm that clipboard read, subframes, unregistered windows, external origins, mismatched dev origins, and unrelated permissions remain denied.
Evidence (Before & After)
Before: clicking the response copy action leaves the Windows clipboard unchanged because
clipboard-sanitized-writeis denied; failures are console-only.After: the trusted top-level Desktop renderer can write the complete response, and a rejected write produces a visible localized failure state. Runtime Windows verification is still requested because this environment only provided Linux desktop access.
Tested on
Environment (optional)
Ubuntu 26.04. Verified 9 permission-policy tests and 2 copy-behavior tests, Electron and shared UI TypeScript checks, Electron main-process bundling, renderer production build, and
git diff --check. A live Linux Clipboard API probe was inconclusive because the automated Electron document could not acquire desktop focus.Risk & Scope
/copycommand.Linked Issues
Fixes #8538
中文说明
本 PR 做了什么
允许 Desktop 应用 renderer 向 Electron 默认会话请求
clipboard-sanitized-write,同时继续拒绝剪贴板读取和其他无关权限。该授权仅适用于已登记工作区窗口的顶层 frame,以及可信的打包页面或已配置的 Vite renderer 来源。回复复制失败时,复制操作现在会显示本地化失败状态,而不再只在控制台中静默失败。为什么需要
回复复制按钮调用
navigator.clipboard.writeText(),但默认会话权限处理器目前拒绝所有非语音权限,因此 Chromium 会在内容写入系统剪贴板前拒绝请求。审阅者测试计划
如何验证
cd packages/desktop/apps/electron && bun test src/main/__tests__/default-session-permissions.test.tscd packages/desktop/packages/ui && bun test src/components/chat/__tests__/copy-response.test.ts权限策略测试确认剪贴板读取、子 frame、未登记窗口、外部来源、不匹配的开发来源和其他权限仍被拒绝。
Before 与 After 证据
Before:由于
clipboard-sanitized-write被拒绝,点击回复复制操作后 Windows 剪贴板保持不变;失败只写入控制台。After:可信的顶层 Desktop renderer 可以写入完整回复;写入被拒绝时会显示用户可见的本地化失败状态。由于当前环境仅提供 Linux 桌面,仍请审阅者补充 Windows 运行验证。
测试平台
环境
Ubuntu 26.04。已验证 9 项权限策略测试、2 项复制行为测试、Electron 与共享 UI TypeScript 检查、Electron 主进程打包、renderer 生产构建和
git diff --check。Linux 上的实时 Clipboard API 探测因自动化 Electron document 无法取得桌面焦点而无法得出结论。风险与范围
/copy命令。关联 Issue
Fixes #8538