DeepSeek Harness 的隔离边界:仓库三处明写「这不是安全边界」

2026-08-16

deepseek-ai/deepseek-harness 这个仓库的执行层时,最容易被跳过、又最不该跳过的,是散在三个不同文件里的同一句话:这不是安全边界

不是社区在 issue 里的吐槽,是仓库自己写在 README 和子系统文档正文里的。一个会在你本机跑 shell、起 PTY、执行模型生成代码的项目,主动把「我这层不是安全边界」写进文档,本身就是一条值得读者认真对待的信息。这篇不做安全评价,只做一件事:把这几处原文找齐、标好出处,再顺着仓库自己的词汇说清楚——它说的「隔离」到底指哪一层。

先把限定条件放前面。本文对应的快照是 47f9438,核对日 2026-08-16。这个仓库建立于 2026-08-13,我们采集时它只有三天历史,根 package.json 里的版本是 0.1.0-rc.5,GitHub 上没有任何 Release,README 自述处于开发者预览阶段并明确写了未来会有破坏兼容性的变更。下面提到的文件路径、字段名、默认值都可能随之改变。

三处原文,逐条对号

第一处在 packages/code-runtime/code-runtime-worker-thread/README.md 的开篇第一段。这是 run_code 背后那个把模型写的 TypeScript 丢进 Node worker 线程执行的后端,README 第一段里就写着「Containment, not a security boundary」,后面紧跟一句自述:trust posture is bash-equivalent by design——它把自己的信任姿态定为「与 bash 等价」。

第二处在 docs/subsystems/code-runtime.md:161。这一句里同时出现两个值域:language 的 well-known 值是 'typescript''python',而同一句紧接着写明只有 'typescript' 有已发布的后端;isolation 的取值是 'worker-thread' | 'process' | 'container',文档在同一句里把它定性为「a diagnostic label, not a security claim」——一个诊断标签,不是安全声明。这一条很关键:如果你写的消费代码打算根据 isolation 的值判断「这次执行安不安全」,文档已经先一步说了它不承担这个含义。

第三处是 packages/fs/fs-sandbox/README.md 里的一个小节标题,原文就叫「Threat model: a policy fence, not a kernel boundary」。正文把这道围栏描述成「a check in TRUSTED code over a MODEL-CONTROLLED path」——检查本身写在受信代码里,被模型控制的是那个目标路径。同一段还写了 This mirrors the code-runtime stance: containment, not a security boundary,并把「对不受信代码做内核级隔离」明确划给 ctx.shell。这道围栏的具体规则在同一份 README 的 :15-17read-only 拒绝一切变更,workspace-write 只允许目标规范化后落在 writableRoots() 里,danger-full-access 直接放行;而读在任何模式下都放行:5)。包含关系怎么判在 packages/fs/fs-sandbox/src/containment.ts:58-76:先走词法快路径(Windows 上大小写不敏感),拼不上时逐级向上 stat 比对 dev/ino 身份。

另有两处同源表述在扩展包里:packages/extensions/tool-cordis/README.md:23 写「The sandbox isolates globals but is not a security boundary.」,packages/extensions/cordis-host-runner/README.md:32 写「The vm sandbox isolates globals but is not a security boundary」。

那「containment」到底给了什么

仓库把 containment 和 security boundary 分得很开,前者是有具体内容的。以 worker-thread 后端为例,packages/code-runtime/code-runtime-worker-thread/src/index.ts:239-243 的配置默认值是:computeMs 60_000、maxWallMs 600_000、maxOutputBytes 67_108_864(64 MiB)、maxOldGenerationSizeMb 512,与 README 里那段 YAML 一致。两个时间预算分工不同,README 写 computeMs 计的是 worker 的实际忙时(用 worker.performance.eventLoopUtilization() 轮询),maxWallMs 兜住忙时看不见的情形,比如等一个永不 resolve 的 promise,两者都收敛到 worker.terminate()。worker 拿到的是空环境:env: {}execArgv: [],README 自称这比给 spawn 命令用的 scrubbed-env 规则更强。

但它同样把限制写在了同一份 README 的已知限制小节里,其中最该被读到的一条是:程序 spawn 出去的 OS 进程会活过 worker 终止——worker.terminate() 只结束线程,原文写它「weaker than bash-local’s process-group kill」,孤儿进程清理在容器后端出现之前属于部署方的事。另外类型剥离依赖 Node 的 experimental stripTypeScriptTypescomputeMs 到期可能超调一个轮询周期,忙时采样固定 25 ms 是内部常量不是配置项;中间绑定值没有字节上限;那个 64 MiB 是拒绝边界而不是可恢复存储。

fs-sandbox 那一侧的诚实也在同一段里:README 明说残留的 TOCTOU(重新规范化与 syscall 之间被换掉的祖先 symlink)被收窄但没有消除,并且是被接受的

内核级那一半在哪里

顺着「Kernel-grade isolation of untrusted CODE stays ctx.shell’s job」这句往下走,就到了 packages/sandbox/

模式枚举只有三个,定义在 packages/sandbox/sandbox/src/index.ts:29

type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'

同文件 :32 还有 ConfinedSandboxMode = Exclude<SandboxMode, 'danger-full-access'>——只有前两个模式会被送进 provider,danger-full-access 的消费者根本不调 ctx.sandbox,直接 spawn 原始 argv(docs/subsystems/sandbox.md:23)。

真正需要读者注意的是 SandboxEnforcementpackages/sandbox/sandbox/src/index.ts:59),取值 'full' | 'partial'。文档把 enforcement 说成「a reported fact」:partial 意味着后端或较老的内核 ABI 只能管住一部分文件效果,而「consumers that require the absolute promise must reject or surface that distinction」(docs/subsystems/sandbox.md:30)。也就是说,这个词汇表本身就把「我只做到一部分」当成一种要如实上报、并由消费者自行决定拒不拒绝的状态,而不是藏起来。

四个本地后端按平台成链(packages/sandbox/sandbox-local/src/index.ts:159-166):linux 是 ['bwrap', 'landlock'],darwin 是 ['seatbelt'],win32 是 ['windows-acl']。未探测时各后端声明的 enforcement 在同文件 :177-187:bwrap、landlock、seatbelt 都是 'full''windows-acl''partial'。注释解释了原因:WRITE_RESTRICTED 令牌必须在两个限制列表里保留 Everyone,且 NTFS 硬链接可以把 workspace 内的文件别名到外面。

失败姿态也写死了:拿不到可用后端时抛 SANDBOX_UNAVAILABLEpackages/sandbox/sandbox/src/index.ts:124),seam 文档原文是「Silent unconfined passthrough is never legal for a confined policy.」(docs/subsystems/sandbox.md:154)。

词汇表之外的东西,比词汇表本身更值得记

docs/subsystems/sandbox.md:11 最后一句是整份文档里最该被划线的:「Network and process visibility are outside this vocabulary.」沙箱模式只管文件效果,网络与进程可见性不在这套词汇里。Windows 那一侧 packages/sandbox/sandbox-windows-acl/README.md 也有一条同向的说明:「Read-side confinement and network policy are out of scope」——WRITE_RESTRICTED 只拦写。

再往执行层看,还有几条路径按仓库自述根本不经过沙箱:

  • glob / grep 不走 ctx.fs 也不走沙箱。它们背后是随包发布的 ripgrep 二进制(@vscode/ripgrep),每次调用经 ctx.subprocess 用固定 argv spawn。该包的 inject 只有 ['tools', 'systemPrompt', 'subprocess']packages/fs/tool-fs-search/src/index.ts:70),没有 fs,也没有 sandbox;README 自己把这条路径称作「the unconfined spawn」(packages/fs/tool-fs-search/README.md:5),并说明前置 --no-config 是为了避免宿主的 RIPGREP_CONFIG_PATH 往这条路径里注入 --pre 预处理器。
  • 凭据清洗只有一条正则:export const SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/ipackages/subprocess/subprocess/src/index.ts:44)。scrubbedParentEnv() 剔除命中它的名字和所有 DSH_ 前缀的名字,其余原样保留,JSDoc 明说 PATHHOME、locale、proxy 变量会活下来。packages/subprocess/subprocess-local/README.md 在限制里直说这只是名字启发式,像 *PASSPHRASE* 这样名字不同的 secret 会继续传下去。
  • 已完成的 spill 文件不会被删除,会在 OS tmpdir 下累积到有外部东西来清(同一份 README 的限制列表)。进程内清理依赖 JavaScript 可观测的退出,未处理的 SIGTERM/SIGINT/SIGHUPSIGKILL、OOM、process.abort()、原生崩溃都绕过它,需要外部 supervisor。

还有一个容易望文生义的地方:e2b 不是 ctx.sandbox 的一个后端docs/subsystems/sandbox.md:5 原文写「Containers, microVMs, and remote execution are sibling implementations of whole capability seams, not providers of ctx.sandbox.」,e2b 家族分别注册的是 ctx.e2bctx.fsctx.subprocesspackages/e2b/README.md:9-11),而且该家族被自述为「An experimental provider-composition POC」(packages/e2b/README.md:5)。它的 README 里还有一句同类语气的话:「cwd is a resolution convention, not containment」。

读代码的人可以怎么用这几句话

不做安全结论的前提下,这几处原文至少给了三个可执行的核对动作。

第一,看 inject 而不是看名字。 一个包叫不叫 sandbox、目录在不在 packages/sandbox/ 下,都不决定它过不过沙箱;决定它的是包的 inject 数组。packages/shell/bash-sandbox/src/index.ts:45static override inject = ['subprocess', 'sandbox', 'sandboxPolicy'],而 tool-fs-search 的 inject 里没有 sandbox——同一个仓库里两种姿态并存,代码里一眼可辨。

第二,看 enforcement 的实际取值而不是「有没有开沙箱」。 在 Windows 上默认后端声明的是 partial,文档要求消费者对这个区分「reject or surface」。你自己写的消费代码有没有处理 partial,是可以现在就回去看一眼的。

第三,看组合文件而不是看默认印象。 仓库里两个示例组合的隔离姿态并不一样:examples/acp-agent/cordis.yml 装的是 @deepseek-ai/dsh-sandbox-localdsh-bash-sandboxdsh-fs-sandbox;而 examples/headless-agent/cordis.yml 装的是 @deepseek-ai/dsh-bash-local:39)与 @deepseek-ai/dsh-fs-local:157),我们在这个文件里 grep sandbox 一条都没有匹配到。另外,packages/sandbox/sandbox-policy/src/index.ts:94 的 schema 默认模式是 'read-only',注释称其为 fail-safe default;而 examples/acp-agent/cordis.yml:31 把 mode 设成了由环境变量决定、并在未设 DSH_SNAPSHOT 时取 'workspace-write' 的表达式。这两处不一致我们只陈述差异、标出位置,以实读的仓库状态为准,不推断原因。

顺带一提,packages/interaction/permission-presets/src/index.ts:168-174 的默认预设表只有 workspace-write(配 ask)与 danger-full-access(配 never)两项,没有 read-only 预设

最后回到那句话本身。仓库把「containment」和「security boundary」当成两个不同的词在用,并且在三个地方主动划清了界线;同时它也在 README 里明确说了这一层会在本机跑 bash -c、会起 /bin/bash --noprofile --norc -i 的 PTY、会执行模型写的代码。这两件事是并列陈述的事实。至于在你的环境里该怎么部署,属于通用运维范畴、不是本项目官方文档的内容,本文不给建议。

延伸阅读


本文依据 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?报名体系课或加入会员,照着学、照着用。