DeepSeek Harness 的代码执行残留:terminate 只结束线程
先把前提摆在最前面:deepseek-harness 这个仓库建立于 2026-08-13,我们采集快照是 2026-08-16,前后只差三天;根 package.json 里的版本是 0.1.0-rc.5,GitHub 上一个 Release 都没有,README 自述处于开发者预览阶段并明写未来会出现破坏兼容性的变更。下面提到的每一个字段名、默认值、退出码,都可能在下一次改动里变掉,请以仓库最新内容为准。本文全部内容来自对快照 47f9438 的静态阅读,我们没有安装这个项目,也没有运行过其中任何一行代码。
一句被写进 README 的限制
ctx.codeRuntime 这个 seam 干的事是让模型写一段程序、由 harness 执行它。仓库里已发布的后端只有一个:packages/code-runtime/code-runtime-worker-thread/,把程序放进 Node 的 worker thread 里跑。
这个包的 README 在「已记录限制」一节(:49-:54)里第一条就写着:程序 spawn 出去的 OS 进程会活过 worker 的终止,worker.terminate() 只结束线程;原文把这个状态描述为 weaker than bash-local's process-group kill; orphan cleanup is a deployment concern until a container backend exists。
反直觉的地方在这儿:run_code 从工具语义上看是「有超时的」,而且超时值定得很具体。但超时管的是那个线程,不是那个线程 spawn 出来的东西。工具调用返回了、结果里带着 timeout,不等于这一次执行留下的所有东西都停了。
两个预算,一个终止动作
配置默认值在 packages/code-runtime/code-runtime-worker-thread/src/index.ts:239-243,与 README :13-:16 给的 YAML 一致(这一点两处是对得上的):
| 字段 | 默认值 |
|---|---|
computeMs | 60_000 |
maxWallMs | 600_000 |
maxOutputBytes | 67_108_864(64 MiB) |
maxOldGenerationSizeMb | 512 |
这两个时间预算不是重复设置。README :27 说明了分工:computeMs 计的是 worker 的实际忙时,靠轮询 worker.performance.eventLoopUtilization() 得到;maxWallMs 兜住忙时看不见的情形,比如程序在等一个永远不会 resolve 的 promise——那种情况下忙时几乎不涨,只有墙上时钟在走。
关键是下一句:两者都收敛到 worker.terminate()。也就是说,无论你把 computeMs 调小还是把 maxWallMs 调小,最终执行的动作是同一个,而那个动作的边界就是线程。另外 maxWallMs 在加载时会被范围检查,超过 MAX_TIMER_DELAY_MS 直接抛错,理由写在 src/index.ts:264-268:setTimeout 会把更长的延迟夹到 1ms。堆溢出则不是超时,会表现为另一种失败:kind: 'worker-exit'。
顺带说一下失败是怎么表达的。docs/subsystems/code-runtime.md:153 里,失败不是 reject,而是结果对象上的 CodeRunResult.error 字段,种类一共六种:'exception'、'timeout'、'abort'、'worker-exit'、'invalid-output'、'output-limit'。看清楚你拿到的是哪一种,是后面判定的起点。
对照组:bash 那条路径上终止是进程组级的
同一个仓库里,ctx.subprocess 的本地实现走的是另一套。packages/subprocess/subprocess-local/README.md:9 描述进程树与信号:POSIX 上子进程是 detached 的、自建进程组,终止时用负 pgid 发信号并回落到直接子进程;Windows 上走 taskkill /PID <pid> /T /F。terminate() 是这一层唯一的终止动词,顺序是 SIGTERM → grace → SIGKILL。代码对应位置分别是 packages/subprocess/subprocess-local/src/spawn.ts:360(detached: platform !== 'win32')、:281(taskkill)、:452(grace 之后发 SIGKILL)。那个 grace 在 bash 执行器侧的默认值是 graceMs 为 3_000(packages/shell/bash-local/src/index.ts 的 schema 默认,常量 DEFAULT_GRACE_MS 在 :35)。
所以 README 那句 weaker than bash-local's process-group kill 不是修辞,它指的就是这两条路径在终止语义上的差别:一边收 argv 起的进程树,一边收一个 JS 线程。
补一句边界,免得读者顺着推过头。worker 侧 README :30 写的是 worker 自己拿 env: {} 与 execArgv: [],README 称其比 spawn 命令时的 scrubbed-env 规则更强。至于程序从 worker 里再 spawn 出去的进程按什么规则取环境,README 这几行没有交代,我们也没有在本次核对范围内找到对应说明,因此不做推断。
怎么在自己的组合里判定
我们没有运行过任何东西,所以这里给的是能靠读文件完成的判定动作,不是运行观测。
第一步,确认你这套组合到底有没有这条路径。看你用的 cordis.yml 里装没装 @deepseek-ai/dsh-code-runtime-worker-thread。仓库自带的两个示例组合本身就不一样:examples/acp-agent/cordis.yml 与 examples/headless-agent/cordis.yml 在沙箱姿态上就有明显差异(前者装了 dsh-sandbox-local、dsh-bash-sandbox、dsh-fs-sandbox,后者我们 grep sandbox 一条都没匹配到)。别默认「文档里写了」就等于「你这一套装了」。
第二步,看失败种类。拿到的是 'timeout' 还是 'worker-exit',对应的是两个时间预算还是堆上限,处置方向不一样。
第三步,把三层配置对一遍:代码 schema 默认(src/index.ts:239-243)、README 的 YAML 示例(:13-:16)、你自己组合文件里的实际取值。这三层在这个包里前两层是一致的,但你自己的组合可能覆盖了。顺带提醒:同类覆盖在这个仓库里确实存在——examples/acp-agent/cordis.yml:40 把 bash 执行器的 timeoutMs 设成 60000,而执行器 schema 默认是 120_000。这里只陈述两处取值不同,不推断哪个更该被采纳。
第四步,如果确认残留是从 run_code 里 spawn 出去的,README 已经把处置口径写死了:orphan cleanup is a deployment concern until a container backend exists。也就是说,这不是一个「改个参数就好」的问题,项目把回收责任放在部署侧,且并没有给出通用值或推荐做法。要在部署侧做进程回收,那属于通用运维范畴、不是本项目官方文档的内容,本文不构成安全方案建议。
第五步,验证方式只能是再回到文件上比对:你改的那个字段是不是就是 src/index.ts:239-243 里那几个名字,改完之后组合文件里的值和你以为的值是不是同一个。运行侧的效果我们没有依据描述。
什么情况说明不是这个原因
- 你的残留进程如果是 PTY 会话相关的,那是另一条路径。
packages/terminal/terminal-bash/的 README:33-:36记着:终端会话不跨 harness 进程退出存活;而packages/subprocess/subprocess-local/README.md:28-:33又写着终端进程巡检只支持 Linux/macOS,守护化的终端后代仍可能逃出可观测边界。这些是终端那条线自己的限制,与worker.terminate()无关。 - 语言服务器进程也是单独一条:
packages/lsp/lsp-stdio通过ctx.subprocess起 server,协议 shutdown 失败后走 subprocess seam 杀后代树(README:14)。 - 如果 harness 进程自己是被
SIGKILL、OOM 或原生崩溃带走的,那么按packages/subprocess/subprocess-local/README.md:31的说法,进程内清理依赖 JavaScript 可观测的退出,这些情形一律绕过它,需要外部 supervisor——这跟 worker 终止边界不是同一个问题。 - 还有一类「残留」根本不是进程:已完成的 spill 文件不会被删除,会在 OS 临时目录下累积到有外部东西来清(同 README
:33)。看到临时目录里有东西堆着,先分清是文件还是进程。
别把 isolation 当成保证
最后回到这个后端的自我定位。packages/code-runtime/code-runtime-worker-thread/README.md:5 第一段写的是 Containment, not a security boundary,并明说 trust posture is bash-equivalent by design。而 docs/subsystems/code-runtime.md:161 里,isolation 这个字段(取值 'worker-thread' | 'process' | 'container')被同一句定性为 a diagnostic label, not a security claim;同一处还写着 'typescript' 与 'python' 都是 well-known 值,但只有 'typescript' 有已发布的后端。
同类表述在仓库里不止一处:packages/fs/fs-sandbox/README.md:19 的小节标题是 Threat model: a policy fence, not a kernel boundary。这些都是仓库自述,我们照实转录,不由此得出任何「所以安全」或「所以不安全」的结论。
把这几条放在一起看,本文开头那句限制就不显得突兀了:run_code 从设计上就没打算做成一道进程边界,它的时间预算管的是线程,进程遗留被显式记成部署侧的事。知道边界在哪儿,比知道超时值是多少更要紧。
延伸阅读
- 从头读起:DeepSeek Harness 是什么:建仓三天、13 万 star 的 Agent 框架
- 本专题共 45 篇,完整分组目录见专题页
- DeepSeek Harness 的沙箱默认策略:schema 只读、示例可写工作区
- DeepSeek Harness 的文件搜索:打包 ripgrep 与 unconfined spawn
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-16,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库建立于 2026-08-13,README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。