DeepSeek Harness 的沙箱默认策略:schema 只读、示例可写工作区

2026-08-16

deepseek-harness 的沙箱这一层时,很容易先在文档和代码注释里读到一句让人安心的话:默认是 read-only,注释还专门把它叫作 fail-safe default。然后你去看仓库自带的示例组合,会发现同一个字段在那里被设成了别的值。

这不是谁写错了的问题,而是**「默认值」这个词在这个仓库里至少落在三层上**:schema 默认、部署组合、会话事件。看漏其中任何一层,你对「我这次工具调用到底能不能写文件」的判断就会偏。这篇就把这两处位置摊开摆一遍。

先把限定条件写在前面:deepseek-harness 仓库建立于 2026-08-13,我们采集是 2026-08-16,前后只差三天;根 package.json 里版本是 0.1.0-rc.5,GitHub 上一个 Release 都没有,README 自述处于开发者预览阶段并明写未来会出现破坏兼容性的变更。下面提到的每一个字段名与默认值都可能随之变动,请以仓库最新内容为准。另外,我们没有安装过 dsh,没有跑过任何命令,没有起过任何沙箱,文中所有数值都是仓库里写死的默认值或常量,不是运行观测值。

两处位置,先各自标清楚

第一处是 schema 默认。 packages/sandbox/sandbox-policy/src/index.ts 的 93 到 98 行是 SandboxPolicyService 的静态 Config

static Config: z<Config> = z.object({
  mode: z.union(['read-only', 'workspace-write', 'danger-full-access'] as const).default('read-only'),
  workspaceRoot: z.string(),   // 无 schema 默认
})

同文件 62 到 65 行的注释把 mode: 'read-only' 称作 the fail-safe default,并写「a deployment that wants a workspace-writable agent opts in explicitly」——想要一个能写工作区的 agent,部署方要显式选进来。注意 workspaceRoot 是没有 schema 默认的,构造函数里走 config.workspaceRoot ?? process.cwd() 再取绝对路径(同文件 110 行)。

第二处是示例组合。 examples/acp-agent/cordis.yml 第 31 行,sandbox-policy 这个插件的 mode 被设成了一段由环境变量决定的表达式:

process.env.DSH_PERMISSION_MODE ?? (process.env.DSH_SNAPSHOT === undefined ? 'workspace-write' : 'danger-full-access')

也就是说,用这份组合起来的时候,mode 这个字段是被显式给了值的,schema 默认不会生效。两处不一致:schema 层写的是 read-only,这份示例组合层给的是 workspace-writedanger-full-access。以上是我们在 2026-08-16 快照 47f9438 上实读到的两处原文,具体位置都已标出,孰对孰错、为何如此,我们不做推断。

同一份 cordis.yml 里还有一处同类差异:第 40 行把 bash 执行器的 timeoutMs 设为 60000,而执行器自身的 schema 默认(packages/shell/bash-local/src/index.ts:105-112)是 120_000。判断「我的 bash 调用多久会超时」时,同样得先确认你用的是哪一层的值。

为什么这一个字符串值值得较真

因为 mode 不是一个装饰性的标签,它在几条代码路径上直接决定行为。

它决定可写根集合。 唯一的定义处在 packages/sandbox/sandbox/src/roots.ts 的 52 到 55 行:

export function writableRoots(policy: SandboxExecutionPolicy): string[] {
  if (policy.mode !== 'workspace-write') return []
  return [...new Set([policy.workspaceRoot, '/tmp', tmpdir()].map(canonicalPath))]
}

read-only 下这个集合是空数组;workspace-write 下是「workspace 根 + /tmp + os.tmpdir()」三者去重后的规范路径。该模块的 JSDoc(5 到 11 行)说这一份定义同时喂给 Seatbelt profile 与进程内 fs 围栏,目的是让「the write tool cannot write /tmp but bash can」这种不对称不会出现在两者之间。

它决定 fs 围栏怎么判。 packages/fs/fs-sandbox/README.md 的 15 到 17 行写:read-only 拒绝一切变更;workspace-write 只允许目标规范化后落在 writableRoots() 里;danger-full-access 直接放行。读操作在任何模式下都放行(同 README 第 5 行)。

它决定要不要走沙箱后端。 packages/shell/bash-sandbox/src/index.ts 的 91 到 93 行显示,danger-full-access 直接走父类、只补一个 sandbox: { mode, denied: false };受限模式才去调 this.ctx.sandbox.confine(['bash', '-c', command], policy)(177 到 179 行)。docs/subsystems/sandbox.md:23 也写了 danger-full-access 的消费者根本不调用 ctx.sandbox

所以「schema 默认是 read-only」这句话,在一份把 mode 显式设成别的值的组合里,是不产生任何效果的。

生效值到底由谁决定:三顺位

packages/sandbox/sandbox-policy/src/index.ts 的 135 到 142 行写明了优先级:一次性审批过的显式 mode > 会话最后一条 sandbox/mode 事件 > 部署默认。

会话侧那一层很轻:setSandboxMode 只做 session.append('sandbox/mode', { mode })packages/sandbox/sandbox-policy/src/session-mode.ts:69-71),读取就是取最后一条(52 到 56 行)。换句话说,部署默认排在第三位,它只是「没有别的东西说话时」的兜底。你要回答「这次调用是什么模式」,得把三层都看一遍,而不是只翻 schema。

想往宽了走还得过一道审批。packages/sandbox/sandbox/src/escalation.ts 里,严格变宽表 WIDER_MODES(28 到 31 行)规定 read-only 只能升到 workspace-writedanger-full-accessworkspace-write 只能升到 danger-full-access;对模型暴露的目标枚举 ESCALATION_TARGETS 只有后两个(41 行)。approveEscalation 的顺序是先查是否严格变宽、再查有没有 approval 服务、再查有没有 agent,才发审批请求,任何一步不过就 throw,而且不会弹审批框(157 到 188 行)。

自己怎么核对:四个动作

如果你正在判断「我这套配置到底是什么模式」,可以按这个顺序看,都是静态阅读、不需要跑任何东西:

  1. 看你实际用的那份 cordis.ymlsandbox-policy 条目有没有 config.mode 有就以它为准,schema 默认不参与。
  2. 看这份组合里装的是哪一路 shell 与 fs 插件。 examples/acp-agent/cordis.yml 装的是 @deepseek-ai/dsh-sandbox-local(25 行)+ dsh-bash-sandbox(38 行)+ dsh-fs-sandbox(166 行);而 examples/headless-agent/cordis.yml 装的是 @deepseek-ai/dsh-bash-local(39 行)与 @deepseek-ai/dsh-fs-local(157 行),我们在这个文件里 grep sandbox 一条都没有匹配到。两份示例组合的隔离姿态本来就不同——只陈述这个差异。
  3. 看权限预设表。 packages/interaction/permission-presets/src/index.ts 的 168 到 174 行,默认预设只有 workspace-write(配 ask)与 danger-full-access(配 never)两项,没有 read-only 预设。这是本篇差异的第三处落点:同一个枚举,在 schema 默认那一层是首选项,在预设表这一层没有条目。
  4. 看环境变量。 上面第 31 行那段表达式读的是 DSH_PERMISSION_MODEDSH_SNAPSHOT。这两个变量在你的环境里取什么值,直接决定这份组合最终把 mode 给成 workspace-write 还是 danger-full-access,所以核对配置文件之外还得核对环境。

至于「应该配成哪个模式」,仓库没有给一个通用答案,本文也不给。它取决于你让这个 agent 干什么、在什么机器上跑,项目自身没有给出通用值。

处置之后怎么验证,以及什么情况说明不是这个原因

生效模式会被写进系统提示。sandbox-policy 会往系统提示里塞一段 sandbox:policy 的运行时上下文,order 是 110(同文件 112 到 123 行),三种模式对应的文案分别在 41、43、45 行。模型侧看到的拒绝标记文本是 `[sandbox: file access denied under ${mode} mode]`escalation.ts:71-73),这段文字里就带着当前模式名。fs 侧的策略拒绝有独立错误码 FS_SANDBOX_DENIED,与宿主内核拒绝的 FS_PERMISSION_DENIED 明确区分(docs/subsystems/filesystem.md:254-270)。

反过来,下面这几种情况说明你遇到的不是「模式配错了」:

  • 报的是 SANDBOX_UNAVAILABLE 这是沙箱后端不可用时的 fail-closed 路径(packages/sandbox/sandbox/src/index.ts:124,错误类在 131 行)。seam 文档原文写「Silent unconfined passthrough is never legal for a confined policy.」(docs/subsystems/sandbox.md:154)。这条与 mode 取值无关,得去看平台链:packages/sandbox/sandbox-local/src/index.ts:159-166 里 linux 是 ['bwrap','landlock']、darwin 是 ['seatbelt']、win32 是 ['windows-acl'],平台没有链就是 unavailable
  • 被拦住的是 glob / grep 这两个工具背后是随包发布的 ripgrep 二进制,packages/fs/tool-fs-search/src/index.ts:70inject 只有 ['tools', 'systemPrompt', 'subprocess']——没有 fs,也没有 sandbox;README 第 5 行自己把这条路径称作 the unconfined spawn。它不走这套模式判定。
  • 问题出在网络或进程可见性上。 docs/subsystems/sandbox.md:11 原文写「Network and process visibility are outside this vocabulary.」,packages/sandbox/sandbox-windows-acl/README.md:97 也写「Read-side confinement and network policy are out of scope」。改 mode 不会改变这两件事。
  • 你在 Windows 上,而现象是「部分管住了」。 未探测时各后端声明的 enforcement 里,bwraplandlockseatbeltfull'windows-acl'partialpackages/sandbox/sandbox-local/src/index.ts:177-187)。SandboxEnforcement 在文档里被定性为 a reported fact,partial 意味着后端只能管住一部分文件效果,「consumers that require the absolute promise must reject or surface that distinction」(docs/subsystems/sandbox.md:30)。

最后一句必须说清楚:这一层是真的会在本机执行外部程序的——bash 工具的命令走 bash -cpackages/shell/bash-local/src/index.ts:212),由 ctx.subprocess spawn 成 detached 进程树(packages/subprocess/subprocess-local/src/spawn.ts:360)。而仓库自己在多处写明这不是安全边界:packages/fs/fs-sandbox/README.md:19 的小节标题是「Threat model: a policy fence, not a kernel boundary」,packages/code-runtime/code-runtime-worker-thread/README.md:5 写「Containment, not a security boundary」,docs/subsystems/code-runtime.md:161isolation 是「a diagnostic label, not a security claim」。把 mode 配成哪一个值,都不改变这些自述的边界,我们也不由此给出任何「安全」或「不安全」的结论。

延伸阅读


本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的 架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-16,对应仓库快照 47f9438(版本 0.1.0-rc.5)。 本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目, 因此不涉及界面外观、操作手感与运行速度的任何描述。 该仓库建立于 2026-08-13,README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更, 文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。 安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。