dshkit

DeepSeek Harness 的沙箱:三种模式、各平台后端,以及它保护不了什么

dsh 怎么约束进程执行——三种沙箱模式、以日志事件形式存在的会话级覆盖、bwrap/Landlock/Seatbelt/Windows ACL 后端、fail-closed 行为,以及项目自己诚实记录的「部分强制」边界。

更新于 2026-08-144 min
一句话结论

沙箱模式有三种:read-only(fail-safe 默认值)、workspace-write、danger-full-access,由 ctx.sandboxPolicy 逐次调用解析。会话级覆盖是一条只追加的 sandbox/mode 事件,所以重启后靠重放依然生效。本地后端在 Linux 是 bwrap 或 Landlock、macOS 是 Seatbelt、Windows 是 ACL 受限令牌;不支持的平台以 SANDBOX_UNAVAILABLE fail closed,而不是无约束地跑下去。

多数 agent 工具用营销语言描述自己的沙箱。DeepSeek Harness 记录了自己的沙箱在哪些地方是部分的—— 这比任何「完全隔离」的说法都更能说明工程水准。

三个包

角色
dsh-sandbox定义进程沙箱服务与共享的升级词汇ctx.sandbox
dsh-sandbox-local各平台的本地约束后端注册到 ctx.sandbox
dsh-sandbox-policy解析持久的会话级策略ctx.sandboxPolicy

这个家族覆盖的是同一世界内的子进程。隔离环境是整体替换掉能力实现,而不是注册到这里——这个区分值得 记住,因为它意味着这里的「沙箱」讲的是约束本地进程执行,而不是把你的 agent 挪到别处去跑。

三种模式

模式效果
read-only默认值。经 DSH 文件沙箱的操作不能修改文件。
workspace-write允许在会话工作区根目录下写。
danger-full-access不做约束。

read-only 是部署默认值,源码里称它 fail-safe——对一个能在你仓库里执行 shell 命令的系统来说, 这是正确的默认。

为什么策略只有一个家

文件系统工具、一次性 bash 命令、终端会话,可能以不同组合强制同一套模式词汇。如果各自解析各自的模式和 工作区根,它们就会漂移成割裂的两个世界——一个工具以为自己是只读的,另一个正在写。

所以 ctx.sandboxPolicy 是唯一的所有者。每个强制方在每次调用都拿到一份解析好的「模式 + 根目录」策略:

ctx.sandboxPolicy.resolve({ session?, mode? })

优先级:显式批准的 mode 压过会话最后一条 sandbox/mode 事件,后者压过 defaultMode。会话不可变的 cwd 会先按文件系统语义规范化,再成为 workspaceRoot;否则用配置的回退值。

配置

默认含义
moderead-only部署默认模式,加载时校验
workspaceRootprocess.cwd()无 agent 调用、或没有 cwd 的会话所用的回退写入根

正常的 agent 调用用的是自己会话头里那个不可变的 cwd,不走回退值。

模式切换是一个事件

setSandboxMode(session, mode)   // 恰好追加一条 sandbox/mode 事件
effectiveSandboxMode(events)    // 纯折叠;最后一次切换胜出

这是 harness 的事件溯源纪律用在了权限上。切换本身就是它的事件——没有任何东西在带外改这个模式。 于是:

  • 覆盖靠重放挺过重启
  • 两个会话永远看不到彼此的状态
  • 策略输入始终可重建,因为 agent loop 会把组装好的运行时上下文快照作为带来源的 user/message 记进日志

可选的 ./invariant 伴生模块会拒绝伪造的、取值落在封闭词汇表之外的持久 sandbox/mode 事件。

模型被告知了什么

sandbox:policy 这条贡献出现在每个 agent 会话的运行时上下文快照里。它陈述该模式在文件层面的 能力中立契约,并且刻意不枚举已挂载的能力

read-only 那段文本很值得学:

Current DSH file policy: read-only. Any available operation enforced by the DSH file sandbox cannot modify files in the standing mode. Do not refuse a required modification from this policy alone: try an available tool normally and follow any denial and escalation guidance it returns.

最后那句是很讲究的提示词工程。只告诉模型「你是只读的」,它往往会预防性地拒绝其实做得到的活。 这句指令把它推向「先尝试、再读拒绝信息」,而不是从策略标签去做推理。

各平台后端

dsh-sandbox-local 选择并缓存一个平台 runner:

  • Linux —— 先试可用的 bwrap,然后 Landlock
  • macOS —— Seatbelt
  • Windows —— ACL 受限令牌 runner

有多个候选时按顺序探测;只有一个候选时直接选定。

每次 wrap 都会报告强制的完整程度,以及后端特有的拒绝签名和 runner 失败规则,好让消费方能 区分「沙箱坏了」和「命令失败了」。Landlock 要求退出码 125 加一行 landlock-run: 致命输出; bubblewrap 和 Seatbelt 只能靠签名判断,因为它们的公开契约都没有保留「启动器失败」的状态码。

macOS 的 Seatbelt profile 是 allow-default 加 (deny file-write*) 再加写入白名单。read-only 只放行 /dev/null 这一个字面量;workspace-write 追加工作区根、/tmp 和当前用户的 darwin 临时目录 ——每个根都做了规范化,因为 Seatbelt 匹配的是解析后的路径(/tmp 就是 /private/tmp)。

项目自己声明的边界

这部分才是你依赖它之前该读的。

Windows 的 ACL 强制是部分的。 受限令牌必须保留 Everyone 才能完成进程初始化,所以对 Everyone 开放写权限的外部对象仍然可写;NTFS 硬链接也会让同一个文件对象在工作区内外互为别名。provider 报告 enforcement: 'partial',没有把边界说满。

Landlock 可能是部分的。 较老的受支持内核 ABI 只能约束它们暴露出来的访问类别,同样报告为 partial

Seatbelt 依赖已被弃用的 sandbox-exec Apple 把这个 CLI 标记为 deprecated,但每个 macOS 仍然 带着它;如果哪天变了,靠的是功能探测来 fail closed。

runner 选择在 provider 生命周期内被缓存。 安装、移除或修复某个 runner 之后,必须重载插件才会改变选择。

runnerCommand 是运维方的断言。 配置了自定义 runner 就会跳过功能探测,并假定它诚实地实现了 bwrap 兼容 profile。还有个尖锐的细节:如果这个 runner 本身是个 Bash 脚本, 它的解释器启动发生在脚本施加约束之前。

你实际该怎么做

在 CI 里有意识地设置模式。 一个持有仓库写权限的无人值守 agent,是一项部署决策。沙箱和审批策略是 dsh-base 里可 patch 的行——这是把双刃剑。见 patch 插件配置,把编码这项决策的 patch 放进版本控制,让 reviewer 看得见。

别把 Windows 上的 workspace-write 当成隔离。 部分强制那条注记写得很明确,硬链接别名也不是理论问题。

别把 workflow 的 worker 线程当安全层——它不是,而沙箱 seam 是完全另一套机制。

常见问题

默认沙箱模式是什么?

read-only,作为 fail-safe 默认值。另外两个值是 workspace-write 和 danger-full-access,在加载时校验。

不支持的平台上会怎样?

以 SANDBOX_UNAVAILABLE fail closed。执行绝不会悄悄地在无约束状态下继续——正是这条性质让这个 seam 值得信任。

Windows 上的沙箱是完整的吗?

不是,而且项目自己说了。受限令牌必须保留 Everyone 才能完成进程初始化,所以对 Everyone 开放写权限的外部对象仍然可写;NTFS 硬链接也会让同一个文件对象在多条路径上互为别名。provider 报告的是 enforcement: 'partial',没有夸大。

会话级的模式切换怎么存?

作为该会话上恰好一条只追加的 sandbox/mode 事件。切换本身就是它的事件——没有任何东西会在带外改这个模式。生效值的优先级是:显式授权 → 事件折叠结果 → 部署默认值。

接着看