`ctx.codeRuntime` seam 的容器隔离后端:每个 Code Mode 程序跑在全新容器里,无网络、根文件系统只读、丢弃全部 capability,内存/CPU/进程数上限由内核强制。
安装
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:tancheng33/dsh-code-runtime-container
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本。请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
English | 中文
DeepSeek Harness 代码执行 seam(ctx.codeRuntime)的容器隔离后端。Code Mode 程序跑在一个全新容器里:无网络、根文件系统只读、丢弃全部 capability,内存/CPU/进程数上限由内核强制。
为什么
seam 声明了三种 isolation,只交付了一种。引自 @deepseek-ai/dsh-code-runtime 自己的 README:
只有 worker-thread 后端交付了——
'process'/'container'是已声明的 well-knownisolation值,没有实现;一个硬安全边界有待容器后端。
而已交付的那个后端对自己的定位同样直白:
是收容,不是安全边界:信任姿态在设计上等同于 bash。
这个默认是合理的——Code Mode 程序是模型写的代码,bash 跑的东西也是。但它意味着一个 run_code 程序在 agent 自己的进程里执行,拥有 agent 的网络、agent 的文件系统,以及 agent 环境里的一切。本后端是给那些不能接受这一点的部署用的。
| worker-thread(官方) | 本后端 | |
|---|---|---|
| 载体 | agent 进程内的 Worker |
每次运行一个新容器 |
| 网络 | agent 的完整网络 | --network=none |
| 文件系统 | 整台主机,以 agent 用户身份 | 只读根,无主机挂载 |
| 权限 | agent 的权限 | --cap-drop=ALL、no-new-privileges、nobody |
| CPU 失控 | 实测忙时预算 | 内核 CPU 配额 + 墙钟 |
| 内存失控 | V8 堆上限 | cgroup 内存限制 → OOM kill |
| Fork 炸弹 | — | --pids-limit |
| 冷启动 | 毫秒级 | 约 200 毫秒 |
安装
dsh plugin --profile <name> add dsh-code-runtime-container
需要主机上有容器引擎,且镜像已就绪:
docker pull node:22-alpine
bundle patch 在插入本行时会禁用 worker-thread 的 code-runtime 行:ctx.codeRuntime 每个 context 只接受一个实现。
不需要构建镜像,也不需要 bind mount——容器内的 runner 是通过命令行传给 node -e 的,所以上游原版镜像可以直接用。podman 和 nerdctl 通过 dockerPath 同样可用。
配置
每个默认值都是最严格的那个;一个以隔离为卖点的后端,不该是"配置之后才安全"。
| 键 | 默认值 | 含义 |
|---|---|---|
dockerPath |
docker |
容器 CLI。任何与 docker run argv 兼容的都行。 |
image |
node:22-alpine |
只需要 PATH 上有 node,别的不需要。 |
network |
none |
Docker 网络模式。用本后端的首要理由。 |
memory |
512m |
内存上限;超出是 OOM kill,上报为 worker-exit。 |
cpus |
1 |
内核强制的 CPU 配额。 |
pidsLimit |
128 |
容器内进程数上限:fork 炸弹撞的是它,不是主机。 |
user |
65534:65534 |
nobody:nogroup。留空则用镜像默认。 |
readOnlyRootfs |
true |
只读根,/tmp 挂 tmpfs。 |
tmpfsMb |
64 |
该 tmpfs 的大小。 |
workspacePath |
'' |
挂到 /workspace 的主机绝对路径。留空则不挂。 |
workspaceReadOnly |
true |
工作区只读挂载。 |
extraArgs |
[] |
额外的 docker run 参数。一个能削弱上面所有默认值的逃生舱。 |
maxWallMs |
120000 |
单次运行的墙钟上限,含容器启动。 |
maxOutputBytes |
4194304 |
日志加完成值的合并上限。 |
需要读项目文件的程序要挂载:
- id: code-runtime-container
config:
workspacePath: /Users/me/projects/app
workspaceReadOnly: true
# …其余键照样重写;patch 会替换整行 config
威胁模型
seam 明确写了 isolation 是**"给部署和诊断用的标签,不是安全声明"**。所以下面是实际的声明,说窄不说宽。
程序做不到的事(均有针对真实容器的测试验证):
- 访问网络(
fetch失败;--network=none)。 - 写根文件系统(
EROFS)。 - 看到任何主机路径——不挂载时
/workspace根本不存在。 - 以 root 运行(
uid是65534)。 - 观察或影响另一次运行:每次运行都是新容器,上一次设的全局变量在下一次已经没了。
- 活过自己的预算:
while(true){}会被杀,而且是按名字杀容器,不只是杀docker run客户端。 - 伪造结果。每个控制帧都带一个每次运行随机的 nonce,经 stdin 送达、只存在于 runner 的模块作用域——不在任何全局上,不在
argv或env里(这两者程序都能读)。程序往 stdout 写{"t":"done","value":"FORGED"},那一行会被计为输出,不是控制。 - 通过替换内建函数破坏传输:
JSON.stringify和process.stdout.write在程序运行前就已被捕获。
它防不住的:
- 内核或容器运行时逃逸。 这是容器,不是虚拟机。内核、运行时或引擎的本地提权漏洞可以击穿它。如果你的威胁模型包含这个,请用 VM 支撑的引擎(把
dockerPath指向 Kata/Firecracker 兼容的 CLI)或独立主机。 - 你挂进去的东西。 可写的
workspacePath是一条通往你项目的真实写路径。这正是这个选项的用途,也是唯一一个会实质性放宽边界的设置。 extraArgs打开的口子。 它原样传给docker run;在那里写--network=host就把招牌特性废掉了。- binding 本身。 程序可以调用 consumer 暴露的每一个宿主函数,在预算内调多少次都行。隔离程序不等于收窄工具——那是工具注册表的门禁,不是本 seam 的事。工具调用侧见
dsh-egress-guard。 - agent 对 Docker socket 的访问。 本插件以 agent 用户身份运行 docker 客户端。能访问 Docker socket 的用户通常就能拿到主机 root——那是你 Docker 配置的性质,不是本插件的。
语义
seam 契约按原文遵守:
- 错误是结果字段,不是 reject。 所有程序层面的结局——异常、不可擦除的 TypeScript、超时、中止、OOM、有损完成值、输出超限——都以
{ logs, error: { kind, message } }resolve。run()只在契约误用时 reject:runtime 已销毁,或 binding 命名空间违反可移植标识符规则。 - 可移植标识符规则直接引用 seam 导出的集合(
PORTABLE_RESERVED_WORDS、RESERVED_BINDING_GLOBALS、RESERVED_ERROR_MEMBERS、DUNDER_MEMBER),而不是在这里重抄一遍——所以lambda在这个 TypeScript 后端上也会被拒,和 Python 后端一致;将来集合扩大,升一次依赖就同步了。 - binding 成员是 null 原型对象的自有属性,所以名为
__proto__或constructor的函数就是普通成员。 - 声明的
errorClass会在程序里被真正物化,所以e instanceof ToolCallError成立,失败的成员名会落在声明的属性上。 - 顶层
await和return可用。 程序先被包裹、再 strip、再按字节偏移切回来(mode: 'strip'保留位置),所以裸return不是语法错误。 - 只支持可擦除语法,与官方后端一致:
enum和 namespace 是程序失败,不是静默转换。 - 销毁到静默。 拆卸会把 runtime 标记为不可用、杀掉每个在途容器,并等待各自退出。
与 worker 后端的差异
computeMs没有等价物。 worker 后端靠实测事件循环忙时来防止热循环藏在一个待决派发后面。跨容器边界拿不到这个测量,所以 CPU 由内核(--cpus)限、耗时由maxWallMs限。这是一处真实的行为差异:一个长时间 sleep 的程序在这里消耗墙钟预算,而 worker 后端只会记它的忙时。- 每次运行冷启动约 200 毫秒,worker 大约是 1 毫秒。相对一次 LLM 往返这不算什么,但也不是免费的。
isolation报'container',seam 视其为信息性字段。
测试
63 个测试,其中 26 个跑在真实容器引擎上——覆盖上面每一条隔离声明、两种预算、取消、载体死亡,以及两个敌意程序用例(伪造协议帧、替换 JSON.stringify)。
npm test # 仅单元测试
docker pull node:22-alpine
DSH_CONTAINER_TEST=1 npm test # 加上实盘容器套件
限制
- 只支持 TypeScript。
language是'typescript'。seam 声明的另一个 well-known 值是'python',dsh-tools也已经带了 Python SDK 渲染器,但至今没有 Python 后端——包括本插件。 - 一次运行一个容器,不做池化。 这正是"跨运行状态不可表达"的来源;那 200 毫秒也花在这里。
- 不支持流式日志。 seam 的
run()是一次性的:日志随 resolve 的结果一起到。被杀的程序仍会显示它死前打印的内容。 - 不代管镜像拉取。 镜像缺失会以
worker-exit失败呈现并带上引擎输出;请提前拉好。 - stdio 不做多路复用。 协议与程序输出共用容器 stdout,靠 nonce 区分。程序的字节永远不会丢——它们会变成日志——但一个输出上 GB 的程序撞到的是
maxOutputBytes,而不是流背压。
许可证
MIT
链接
同类插件
strukto-ai/mirage#dsh★ 3453
把文件系统与 bash 提供者换成 mirage 虚拟工作区:文件工具与 shell 命令作用于挂载的资源(RAM、S3、Redis、Slack、Gmail、Notion、Postgres)而非宿主磁盘,支持按挂载点设置读/写/执行模式、按命令选择沙箱(进程内 monty、pyodide、quickjs;远程 docker、e2b、daytona),并可在虚拟终端中安装 CLI(git、gh、slack、linear、ntn、gws,或自行注册的程序树)作为命令头词。
hust-open-atom-club/oh-dsh★ 203
社区发行版:TUI、桌面端与 Web UI 统一体验,分层安装、一步到位。
Jayden-X-L/forkprobe★ 66
同一任务并行试跑多个技能,对比结果选出最优。
vlln/plugin-registry★ 44
插件生态基建:浏览器面板管理官方 repository 插件(0 patch)+ make-dsh-plugin 插件开发引导技能。
forrestchang/dsh-multica-runtime★ 38
让 dsh 运行时跑在 Multica 上。
omdsh-dev/dsh-plugin-check★ 21
插件健康检查:扫描清单协议/patch 格式/构建陷阱,零依赖只读。