DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

LondonMoon /

LondonMoon/dsh-sandbox-approval-modal

Verified

Native + in-page sandbox approval for DeepSeek Harness

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHubProject homepage
READMESource: main@ed76804d

dsh-sandbox-approval-modal

CI Release License: BSD-3-Clause

English summary — Native + in-page sandbox-permission approval for DeepSeek Harness (DSH). A Task Scheduler–launched WinForms window is the primary surface for decide mode (so it can appear while DSH is in the background), with a fixed Web modal fallback, FIFO queue, one-shot allowed-once grants, reject-with-comment, host-enforced timeout, and audit-friendly decisions. It never grants persistent permissions.

沙盒外权限请求的置顶审批弹窗,并提供 Windows 原生后台提醒。它不新增授权通道,而是复用 DSH 现有的审批链路:

approval 服务(Host)
  → approval/request waterfall
  → 本插件 Host 侧(decide 原生审批 / notify 并行提醒;默认 decide)
  → api-remotes 转发到 Web Client
  → shipped ui-approval 的 PendingApproval(capability 不变)
  → 本插件的 fixed overlay(只负责呈现)
  → PendingApproval.answer('allowed-once' | 'rejected')
  → Host 追加 approval/decided,工具按结果放行/拒绝

notify 模式下原生提醒不参与决策,Web 弹窗始终是决策入口;decide 模式下原生窗口先回答, 只有 delegate/unavailable 才进入 Web 弹窗。因此没有“另一条提权路径”:任何 UI 都只能回答宿主 已经发出的那一次审批请求;allow 的唯一结果是 allowed-once,不会写全局策略,也不会创建持久授权。

触发条件

  • 只由权限审批事件驱动:approval/request 经宿主审批服务发出后,ui-approval 生成 PendingApproval 待处理交互;本插件的 overlay 只读取这些 kind === "approval" 的待处理 交互。普通 assistant 文本、普通 tool 消息、聊天正文都不会触发弹窗。
  • 同一时刻只渲染一个弹窗;其余请求按 PendingApproval.key 的创建序号 FIFO 排队,弹窗顶部 显示“还有 N 个请求按顺序等待”。
  • 文件写入/编辑的沙盒越权来自 write/edit 的 sandbox_permissions + justification; pwsh/bash 的越权同理。没有越权参数时不会出现弹窗。

呈现

  • 挂在框架级 shell.overlay,并通过 React Portal 固定到 document.body:
    • position: fixed; inset: 0; z-index: 2147483000
    • 高于会话视图、输入区、侧栏和既有浮层;滚动聊天、侧栏或窗口时位置不变
    • 长理由、长命令和 diff 预览放在弹窗内部的滚动区;底部“拒绝 / 允许(仅本次)”按钮始终可见
  • 标题固定为 “需要沙盒外权限”;正文完整展示模型理由;目标工具、权限模式、路径/命令 使用等宽字体;文件写入显示加入行数预览,edit 显示真实的删除/新增行 diff 摘要。
  • 授权范围固定显示 “仅本次”,并明确写出: “本次授权仅对当前操作有效,执行完成后自动收回。” “本弹窗不提供‘始终允许’或会话级持久授权。”
  • 无障碍与键盘:
    • role="alertdialog"、aria-modal="true"、aria-labelledby/aria-describedby
    • 打开时焦点落在安全默认项“拒绝”;Tab 焦点陷阱;Esc 拒绝(在“拒绝并附理由”编辑态下 先返回);关闭后恢复之前的焦点
    • 文案经 ctx.locale 命名空间 sandboxApproval 提供中/英双语,随当前界面语言切换

跳出浏览器:原生窗口(Windows)

Host 侧新增一个原生 WinForms 窗口(不是浏览器标签),通过 Task Scheduler 交互式令牌 在登录用户桌面启动,TopMost、任务栏可见、闪烁 + 提示音,位于所有程序之上。nativeMode 配置:

模式 行为
decide(默认) 原生窗口提供“允许(仅本次)/ 拒绝 / 在浏览器中处理”,先于 Web answerer;只有原生不可用或用户选择“在浏览器中处理”时才调用 Web 弹窗。窗口通过 Task Scheduler 交互式令牌 启动,因此即使 DSH Host 在后台/隐藏桌面,也会出现在当前登录用户桌面。
notify Web 弹窗仍是唯一决策入口;同时弹出原生后台提醒,展示理由 / 工具 / 目标 / 倒计时 / 排队数,按钮为“打开浏览器处理”和“关闭提醒”。它不会允许或拒绝,因此不会出现两个决策入口。
off 仅 Web 弹窗。
  • 原生窗口在上层程序前台时仍然可见;decide/notify 的“打开浏览器处理”会 restore/focus 标题含 DeepSeek 的浏览器窗口并闪烁任务栏。
  • 浏览器后台通知(跨平台兜底):Web 弹窗出现时,如果 Notification.permission 仍是默认值, 弹窗底部会显示“开启后台通知”。授权后,当页面处于后台/最小化(visibilityState === 'hidden') 且有活动审批,浏览器会发一条 requireInteraction 的系统通知;点击通知会聚焦窗口。这样即使 原生窗口在当前桌面不可见(或在非 Windows 上),用户仍能收到系统级提醒。
  • 失败降级:非 Windows、PowerShell/WinForms 不可用、启动失败、窗口返回 unavailable → 回退 Web 弹窗;绝不 fail-open 到 allow。
  • 原生超时默认 120s(Host guard 130s 兜底):decide 模式超时 = 拒绝;notify 模式超时只关闭提醒, Web 弹窗继续等待 / 由自身超时拒绝。
  • 原生窗口同样只有 allowed-once,没有“始终允许”或持久授权。

决策回传

用户动作 回传 结果
允许(仅本次) PendingApproval.answer('allowed-once') 只放行被询问的这一次操作
拒绝 PendingApproval.answer('rejected') 工具收到拒绝错误,不会执行
拒绝并附理由 先 rejected,再把理由作为审批备注注入会话 理由进入下一轮模型上下文
超时 PendingApproval.answer('rejected') 自动拒绝,绝不静默放行
  • 审核备注(实现见 lib/client.js 的 sendReviewerReason 与 reviewHeader/reviewBody): DSH 的 ApprovalOutcome 只有四个值,没有 reason 字段;本插件用一个最小适配层,把用户填写的 理由作为一条明确标注的用户消息 queue 进该会话,例如: 【沙盒外权限审批备注】... 理由:<用户理由> ... 请勿重试或绕过... 它只影响下一轮模型上下文,不改变“已拒绝”的决定。
  • 超时有两层:
    • Client:默认 120s;超时后自动 reject,并显示“审批已超时,已自动拒绝(不会静默放行)。”
    • Host:apply(ctx, config) 以最高优先级预置一个 approval/request 监听器。notify 模式先调用 next() 打开 Web 弹窗并并行启动原生提醒;decide 模式先询问原生窗口,再在需要时调用 next()。整个 answerer 链统一受 timeoutMs(默认 130s)约束:超时取消原生窗口并返回 rejected。这样即使 Web 页面关闭/断连,也不会永久挂起。
  • 审计:审计对由宿主 approval 服务写入会话日志,本插件不伪造、不跳过:
    • approval/asked:id、toolName、callId、reason
    • approval/decided:id、outcome

审计查询

会话日志是分帧 zstd。随包脚本会解出全部帧,打印 asked/decided 对:

node scripts/read-approval-audit.mjs <session-id>
# 或
node scripts/read-approval-audit.mjs "C:\Users\<you>\.dsh\sessions\--F-project-xxx--\session-<id>\session.v4.jsonl.zstd" --json

实际输出示例(来自本仓库的 E2E 验证):

ASKED   2026-11-13T12:06:41.668Z  pwsh  call_00_...
  reason: escalate sandbox to danger-full-access: 第五次审批弹窗:聊天滚动固定性测试
DECIDED 2026-11-13T12:06:42.943Z  rejected

安装

本包既可以作为 profile bundle 安装,也可以在开发环境用 dsh-super-injector 运行时注入。

从 GitHub 安装(推荐)

git clone https://github.com/LondonMoon/dsh-sandbox-approval-modal.git <path-to-clone>

在 DSH 中把克隆目录作为 bundle 安装:

plugin_manager install_bundle target=<absolute path to the clone>

也可以从 Releases 下载 *.tgz,解压后以其目录作为同一安装命令的 target。

开发态运行时注入(需要 dsh-super-injector):

dev_inject_plugin <path-to-clone>

安装后重启/刷新 DSH;默认 Web profile 的 shipped @deepseek-ai/dsh-client-ui-approval 必须保持启用。 本插件消费它生成的 PendingApproval;它不替代审批服务,也不关闭任何 shipped 插件。 本插件的 composer 影子条目(priority 0)只负责阻止旧的 composer 卡片重复显示,审批能力本身 仍由 ui-approval 和宿主 approval 服务提供。

Host 侧超时与原生模式可通过 bundle patch 覆盖:

- id: sandbox-approval-modal
  name: '@dsh-external/dsh-sandbox-approval-modal'
  config:
    timeoutMs: 130000
    nativeMode: decide    # decide(默认)| notify | off
    nativeTimeoutMs: 120000

测试

npm test          # 直接运行全部 node:test 文件(沙盒下避免 node --test 的子进程)
npm run check     # 四份 JS 的 node --check
  • test/core.test.mjs:从实际发布的 lib/client.js 载入工厂(而不是复制逻辑)并测试:
    • 只有 approval 交互会触发,prose 不会
    • FIFO 排序与排队计数
    • allow → allowed-once,reject → rejected,没有第三种授权
    • 超时到期、已应答不重复、状态同步清理
    • 拒绝理由文本包含工具/目标并禁止绕过
    • 写文件/编辑的路径、请求模式、diff 摘要
    • fixed/z-index/内部滚动的 CSS 契约
    • Tab 焦点环绕
    • 注册面:shell.overlay + priority 0 的 composer 影子 + sandboxApproval 字典 + 样式/清理
    • 浏览器后台通知门控:只有 hidden + granted 才发通知;reason 截断到 ≤240 字符
  • test/host.test.mjs:Host 超时守卫对正常决定透传、对无人应答按截止时间 rejected; notify 模式始终打开 Web 决策并在其结束时关闭原生提醒;decide 模式的原生决定优先; 监听器 { prepend: true } 且随 effect 清理。
  • test/native-broker.test.mjs:原生窗口 FIFO 排队、队首唯一、排队计数、delegate 放行、 取消 active/queued、dispose、异常窗口降级。
  • test/native-window.test.mjs:原生展示元数据(理由/权限/路径/命令/diff 计数)、 当前 turn 边界内的 tool/call 查找、平台安全、PowerShell 文件 ASCII-only 与无 auto-allow 路径。
  • test 合计 13 + 9 + 7 + 6 = 35 个用例。

原生窗口实测

  • scripts/native-approval.ps1 经 Windows PowerShell 语法解析通过。
  • windowMode: notify + 6s 超时:脚本正常结束并写出 {"decision":"dismissed"}。
  • windowMode: decide + 6s 超时:脚本正常结束并写出 {"decision":"rejected"}。
  • 在 Web 弹窗打开时把 document.visibilityState 临时置为 hidden 并注入 Fake Notification: 客户端创建了 title="需要沙盒外权限"、body=<完整理由>、requireInteraction=true 的通知, 验证了“后台页通知”路径(真实系统 toast 的视觉呈现取决于操作系统通知设置)。
  • 交互式任务启动实测:Host 用 schtasks /Create /XML(LogonType=InteractiveToken)创建 一次性任务后,原生窗口出现在登录用户桌面;用户点击“打开浏览器处理”后,starter 收到 {"decision":"delegate"}。Host 自身的窗口站不可见并不影响这条路径——这正是解决 “弹窗还在网页里”的关键:窗口由交互式任务在用户桌面创建,而不是直接在 Host 进程里创建。
  • 如果任务计划程序不可用,会回退到直接 PowerShell 子进程 / Web 弹窗;notify / off 模式 始终保留 Web 弹窗。
  • 浏览器通知的权限/隐藏页触发路径已由单测和 Fake Notification 实测覆盖;浏览器权限提示 本身是用户手势行为,未在无头测试中自动授权。

真实 GUI E2E(本机已执行)

在独立的 Electron 浏览器会话中打开当前 DSH Web GUI,创建一个由该 Client 拥有的新会话, 让 agent 真实调用带 sandbox_permissions: danger-full-access 的 pwsh:

  • 弹窗 DOM:[data-sandbox-approval-modal] 存在,标题“需要沙盒外权限”, position: fixed、z-index: 2147483000
  • 滚动 [data-conversation-scroll](3758 → 3258)与侧栏(0 → 250)后, 弹窗 rect 完全一致:x=351, y=138.5, w=682, h=520
  • 点击“拒绝”后弹窗消失;审计为 approval/decided: rejected,工具返回用户拒绝错误
  • 点击“拒绝并附理由…”填入理由后发送: 审计为 rejected,下一轮用户消息中出现“【沙盒外权限审批备注】…理由:<理由>…”, agent 明确表示不重试、不绕过
  • 把 client 超时设为 2s/5s 且不操作:弹窗先显示倒计时,随后自动消失; 审计为 rejected,toast 显示“审批已超时,已自动拒绝(不会静默放行)。”
  • 打开时焦点在“拒绝”按钮(安全默认);Esc/Tab 行为由 useDialogFocus 与单测覆盖
  • 一次被允许的 pwsh 只执行一次;后续同类请求仍会重新询问(没有“始终允许”)

限制与边界

  • 跨会话并发:弹窗汇总当前 Client 的 uiSession.sessionStatus 中所有 approval 待处理 交互,并按 approval:<monotonic> 排序。DSH 的单个 agent 工具循环天然串行;同一会话内 同时出现多个 approval 不是常规路径。FIFO 计数与排序逻辑由 core.test.mjs 覆盖。
  • 审核备注走 session.prompt(..., 'queue'),因此会以“用户消息”的形式进入下一轮上下文; 这是 DSH 当前审批结果词表没有 reason 字段时的最小适配。若会话已不可用,拒绝仍生效, 只会在 console 记录理由发送失败。
  • 本插件不改变 approval/policy;如果会话策略是 never,宿主在分发前就自动拒绝,不会 经过本弹窗。
  • 同一页面只应有一个本插件实例;重复注册会由 slot 的 id/priority 规则暴露冲突,而不会 产生两个授权通道。
  • 原生窗口可见性:它通过 Task Scheduler 的 InteractiveToken 在登录用户桌面启动;需要任务 计划服务可用且用户已登录。任务创建/运行失败时会回退到直接子进程或 Web;notify 不会因此 阻断 Web 弹窗;decide 模式下窗口不可见时只能等待窗口超时自动拒绝。需要完全避免这种依赖时 用 nativeMode: off。
  • notify 模式不在原生窗口里允许/拒绝,这是刻意的:避免和 Web 弹窗形成两个决策入口。 需要在浏览器外直接决定时设 nativeMode: decide;该模式已在单测覆盖原生优先与 delegate。
  • 系统通知依赖浏览器仍在运行且站点通知权限为 granted;浏览器完全退出时不会有通知,审批仍按 Host 130s 兜底超时拒绝。需要“完全关掉浏览器也能处理”的部署请使用 nativeMode: decide (前提是 Host 桌面对用户可见)。

文件

  • lib/host.js:Host 侧原生模式选择 + 超时守卫(approval/request prepend;notify 并行提醒, decide 原生优先,正常链路保留 next(),超时 rejected)
  • lib/native-broker.js:单窗口 FIFO 队列、取消、dispose
  • lib/native-window-task.js:Windows 原生窗口启动(Task Scheduler 交互式令牌优先,直接子进程回退)、 展示元数据、tool/call 参数提取
  • scripts/native-approval.ps1:ASCII-only WinForms 窗口;notify 提醒 / decide 审批两种模式
  • lib/client.js:fixed overlay、composer 影子、FIFO/超时注册表、diff 摘要、焦点陷阱、中英字典、 可查询的 window.__dshSandboxApproval 检查面(不含授权能力)
  • cordis.patch.yml:bundle 装配行
  • locale/{zh,en}.json、assets/icon.svg:展示元数据
  • scripts/read-approval-audit.mjs:从 .zstd 会话日志查询审批审计
  • test/*.test.mjs:客户端、Host、原生队列、原生窗口四组测试

发布

推送 v* tag 会触发 .github/workflows/release.yml: 在 Ubuntu runner 上执行 npm run check、npm test、npm pack,然后把生成的 .tgz 附加到同 tag 的 GitHub Release。日常 push/PR 由 .github/workflows/ci.yml 做语法、测试与打包内容校验。

git tag -a v0.1.0 -m "v0.1.0"
git push origin v0.1.0

许可证与安全

BSD-3-Clause,见 LICENSE。如果发现可能导致审批绕过、伪造 allowed-once、重放或隐藏拒绝的问题,请通过仓库的 Security → Report a vulnerability 私密报告;不要用公开 issue 披露可利用细节,见 SECURITY.md。

—/ 5

No ratings yet

Verified DSH bundle

Commit ed76804da0b4

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout