Pi 教程第 1 章讲过 Pi 的立场:不做权限弹窗,用容器化当安全边界。dsh 走了相反的路:沙箱强制 + 审批确认 + 权限预设三层结构。这一章拆开看它是怎么在四种操作系统上做到"默认拒绝、fail-closed"的。
本章所有关键行为都有本机实测支撑 —— 包括一次真实的沙箱拦截。
给 agent 的 shell 加沙箱,难点不在"拦截写操作",而在三个工程问题:
dsh 把沙箱拆成两个 seam(第 4 章的清单里有):dsh-sandbox(服务定义)和 dsh-sandbox-local(实现)。
dsh-sandbox-local 支持四种后端 [官方文档]:
| 后端 | 平台 | 机制 |
|---|---|---|
| bwrap(bubblewrap) | Linux | 用户命名空间沙箱 |
| landlock-run | Linux | Landlock LSM 自限制后执行(node-addon-landlock-run 提供) |
| Seatbelt | macOS | macOS 原生沙箱 |
| Windows ACL | Windows | 受限令牌 + 能力 SID 写白名单 |
关键设计是探测 + fail-closed:
本地进程沙箱后端:bwrap、npm 分发的 landlock-run 启动器、macOS Seatbelt、或 Windows ACL 受限令牌 runner —— 功能性探测,fail-closed。 —— dsh-sandbox-local/README.zh.md
理解:"功能性探测"= 启动时实际试一下后端能不能跑,而不是只看平台猜。探测失败或后端缺席时抛 SANDBOX_UNAVAILABLE —— 工具调用直接失败,不会"降级为无沙箱"。
为什么 fail-closed 是对的? 沙箱不可用时放行 = 安全边界悄悄消失。dsh 宁可让 agent 报错,也不让它在无防护下运行。

配图说明:后端层(bwrap / landlock-run / Seatbelt / Windows ACL,功能性探测,fail-closed;本机走 Windows ACL 受限令牌 + 能力 SID 白名单)→ 语义层(SandboxMode 三档,只管文件写,默认 workspace-write)→ 强制路径三条(fs / pwsh / bash 沙箱变体,读取放行写入围栏)。底部是审批与预设:approval 一次性授权(fail-closed,无应答即拒绝),权限预设把沙箱+审批打包(workspace-write + ask / danger-full-access + never)。
dsh-sandbox 定义的词汇 [官方文档]:
注意:SandboxMode 只管文件操作。 Shell 和进程的隔离是另一条线(沙箱 runner 的令牌/命名空间层面)。
SandboxExecutionPolicy 是每次调用的完整模式 + 工作区根目录。SandboxPolicy 是其中受限制的子集。
本教程写作过程中的实际取证 [实测]:
这就是 workspace-write 模式的边界:工作区根(本机 D:\variFlight_work\VariFlightWork)及平台临时区可写,其余位置写入被拒。
沙箱模式的来源也可以实测 —— 会话日志里有 sandbox/mode 事件:
还有配套的 approval/policy(ask)和 permission/preset(workspace-write)事件。
沙箱在三个能力 seam 上强制(第 4 章讲过 seam 的沙箱变体):
| 能力 | 无沙箱提供方 | 沙箱提供方 |
|---|---|---|
| 文件 | dsh-fs-local | dsh-fs-sandbox |
| PowerShell | dsh-pwsh-local | dsh-pwsh-sandbox |
| Bash | dsh-bash-local | dsh-bash-sandbox |
第 3 章实测过选择逻辑 —— base patch 用 !!js process.platform 动态分流:
读操作拦不拦? 不拦。dsh-fs-sandbox 的说明是"fences write/edit by the per-call sandbox mode … while reads pass through" —— 读取放行,写入/编辑按模式围栏。
理解:沙箱是写保护,不是读保护。这符合"agent 需要读代码库"的用例 —— 读是常态,写才需要防护。
dsh-sandbox-policy 是策略解析的唯一位置:
沙箱策略解析的唯一归属位置:部署默认 SandboxMode 与回退根目录,加上每个会话的持久模式覆盖和不可变工作区根目录。每项负责强制执行的能力在每次调用时都会收到一项解析完成的模式与根目录策略;模型在每次请求前会收到当前策略,而不会另收一份能力清单。 —— dsh-sandbox-policy/README.zh.md
配置的默认值(base patch 实测):
两个细节:默认模式 workspace-write(不是 full-access);工作区根 = 启动时的工作目录。会话日志里的 sandbox/mode 事件就是它写的。
沙箱管"能不能写",审批管"要不要问人"。dsh-user-approval:
与通道无关的一次性审批 seam。ctx.approval.request(req) 返回 allowed-once、rejected、cancelled 或 unavailable;应答者缺失或失败时会以拒绝方式关闭,授权也只适用于所请求的操作。 —— dsh-user-approval/README.zh.md
四种返回值:
| 返回值 | 含义 |
|---|---|
| allowed-once | 允许这一次 |
| rejected | 用户拒绝 |
| cancelled | 请求被取消 |
| unavailable | 没有应答者(fail-closed → 视为拒绝) |
理解:allowed-once 是一次性授权 —— 不是"记住我"。每次危险操作都要重新问。这是对 Pi "弹窗疲劳"论点的正面回应:dsh 认为疲劳可以用权限预设缓解(见下节),而不是放弃审批。
本机会话日志实测:
审批策略是 ask(询问)。dsh-user-approval 的请求走"approval/request waterfall",默认 fail-closed —— 应答者缺失或失败 = 拒绝。
dsh-permission-presets 把"沙箱模式 + 审批策略"打包成用户可理解的选择:
通过 ctx.permissionPresets 提供面向用户的权限预设。每个配置名称都会将 sandbox/mode 与 approval/policy 组成一组;默认项为 workspace-write(workspace-write + ask)和 danger-full-access(danger-full-access + never)。UI 适配器可以将该表作为单个选择器公开。 —— dsh-permission-presets/README.zh.md
| 预设名 | 沙箱模式 | 审批策略 | 一句话 |
|---|---|---|---|
| workspace-write | workspace-write | ask | 工作区可写,出界要问 |
| danger-full-access | danger-full-access | never | 完全访问,不问 |
注意 danger-full-access 配的是 never(不问) —— 完全访问 + 免审批是一对。如果用户选了完全访问却还要逐个确认,那既危险又烦人。
会话日志里两个预设的切换都会落账(permission/preset 事件),审计可查。
本机是 Windows,所以真正起作用的是 dsh-sandbox-windows-acl。它的原理:
Windows ACL 写限制沙箱后端(受限令牌 spawn + 能力 SID 写白名单) —— dsh-sandbox-windows-acl/README.zh.md
受限令牌(restricted token):以受限 SID 启动子进程,让进程只能访问白名单内的资源。能力 SID 写白名单:把工作区根、临时区等路径的能力 SID 加入白名单,白名单外的写操作被操作系统拒绝。
这正是第 1 章实测"家目录写入被拒"的物理机制 —— 不是 dsh 代码里 if 判断,而是 Windows 的 ACL 拒绝。
README 还列出了受限令牌固有的边界("非本移植引入"):
已验证边界(受限令牌固有,非本移植引入):…
这些限制意味着:ACL 沙箱能拦住写操作,但拦不住同用户下绕过 ACL 的路径(例如某些系统调用、DLL 注入等)。它提供的是一层实用的写保护,不是强隔离 —— 与 Linux 命名空间沙箱的强度不同。
诚实说明 这部分(Windows ACL 的具体限制清单)本教程未能完整转述 —— 官方 README 有专门小节,但内容较长且涉及安全细节。如果你要深入,请直接读 dsh-sandbox-windows-acl/README.zh.md 的「已验证边界」小节。
沙箱拦截后,agent 怎么合法地拿到更大权限?dsh 的设计:
当命令被拒绝且更宽的模式能使其成功时,立即以相同命令重试一次,并附带 sandbox_permissions(最窄的足够宽的模式)加一句说明。不要先绕道聊天区请求许可 —— 那次重试触发的批准提示就是用户的同意方式。 —— 本机会话的 pwsh 工具说明(实测)
流程:
关键约束:升级必须"同一命令",且拒绝后不允许换一种方式绕过。这让"沙箱 + 审批"成为一个闭环:拒绝是真实边界,不是可以绕过的路障。
沙箱与权限是 dsh 对"agent 安全"的完整回答:
与 Pi 的"YOLO + 容器"路线相比,dsh 选择了"默认拒绝 + 显式确认"。两种路线的代价不同 —— Pi 的代价是"没护栏时用户自负",dsh 的代价是"每次出界都打断流程"。第 13 章会做完整对照。
下一章是多 agent 的世界:委派与编排 —— subagent、workflow、ralph。