toby-bridges/api-relay-audit
toby-bridges/api-relay-audit从 DeepSeek Harness 对 AI API 中转站和 LLM 代理运行本地安全审计,生成 Markdown 报告,覆盖提示词注入、模型替换信号、工具调用改写、错误泄漏、流完整性和按 profile 启用的 Web3 风险。
`ctx.codeRuntime` seam 的容器隔离后端:每个 Code Mode 程序跑在全新容器里,无网络、根文件系统只读、丢弃全部 capability,内存/CPU/进程数上限由内核强制。
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 根本不存在。uid 是 65534)。while(true){} 会被杀,而且是按名字杀容器,不只是杀 docker run 客户端。argv 或 env 里(这两者程序都能读)。程序往 stdout 写 {"t":"done","value":"FORGED"},那一行会被计为输出,不是控制。JSON.stringify 和 process.stdout.write 在程序运行前就已被捕获。它防不住的:
dockerPath 指向 Kata/Firecracker 兼容的 CLI)或独立主机。workspacePath 是一条通往你项目的真实写路径。这正是这个选项的用途,也是唯一一个会实质性放宽边界的设置。extraArgs 打开的口子。 它原样传给 docker run;在那里写 --network=host 就把招牌特性废掉了。dsh-egress-guard。seam 契约按原文遵守:
{ logs, error: { kind, message } } resolve。run() 只在契约误用时 reject:runtime 已销毁,或 binding 命名空间违反可移植标识符规则。PORTABLE_RESERVED_WORDS、RESERVED_BINDING_GLOBALS、RESERVED_ERROR_MEMBERS、DUNDER_MEMBER),而不是在这里重抄一遍——所以 lambda 在这个 TypeScript 后端上也会被拒,和 Python 后端一致;将来集合扩大,升一次依赖就同步了。__proto__ 或 constructor 的函数就是普通成员。errorClass 会在程序里被真正物化,所以 e instanceof ToolCallError 成立,失败的成员名会落在声明的属性上。await 和 return 可用。 程序先被包裹、再 strip、再按字节偏移切回来(mode: 'strip' 保留位置),所以裸 return 不是语法错误。enum 和 namespace 是程序失败,不是静默转换。computeMs 没有等价物。 worker 后端靠实测事件循环忙时来防止热循环藏在一个待决派发后面。跨容器边界拿不到这个测量,所以 CPU 由内核(--cpus)限、耗时由 maxWallMs 限。这是一处真实的行为差异:一个长时间 sleep 的程序在这里消耗墙钟预算,而 worker 后端只会记它的忙时。isolation 报 'container',seam 视其为信息性字段。63 个测试,其中 26 个跑在真实容器引擎上——覆盖上面每一条隔离声明、两种预算、取消、载体死亡,以及两个敌意程序用例(伪造协议帧、替换 JSON.stringify)。
npm test # 仅单元测试
docker pull node:22-alpine
DSH_CONTAINER_TEST=1 npm test # 加上实盘容器套件
language 是 'typescript'。seam 声明的另一个 well-known 值是 'python',dsh-tools 也已经带了 Python SDK 渲染器,但至今没有 Python 后端——包括本插件。run() 是一次性的:日志随 resolve 的结果一起到。被杀的程序仍会显示它死前打印的内容。worker-exit 失败呈现并带上引擎输出;请提前拉好。maxOutputBytes,而不是流背压。MIT