Files
deepseek-harness/packages/code-runtime/code-runtime-python/README.zh.md
T
Chinesezjc fe3ba24057 docs(code-runtime-python): update the no-fail-before count to ten and document the hard==1 CPU blind spot
Addresses the keep-current review findings:
- The settlement note's Problem/Consequences count is nine -> ten, adding the
  log-fragment seal to the no-fail-before enumeration (its 25 M-scale OOM is not
  deterministically constructible in CI); the new Decision section title now
  names all four mechanisms and the double blank line is removed.
- README Known Limitations (en + zh) documents the 1-second dual-limit
  ulimit -t 1 CPU overrun being reported as worker-exit (the hard >= 2 guard
  cannot lower a 1-second soft to 0); pairings re-recorded and consistent.
2026-08-31 14:34:09 +08:00

8.5 KiB
Raw Blame History

description, kind
description kind
DeepSeek Harness 代码执行 seam 的 CPython 子进程实现,提供 fd-3 binding、资源限制、日志捕获与进程组拆卸。 package-reference

@deepseek-ai/dsh-code-runtime-python

English | 中文

@deepseek-ai/dsh-code-runtime seam 的 CPython 子进程实现。与 @deepseek-ai/dsh-code-runtime-worker-thread 配套;以全新的 python3 子进程取代 Node worker 线程,让模型代码从 TypeScript 换成 Python。

本包持有该 seam 的 wire protocolhost 侧的帧编解码,以及 Python 侧对同一套消息词汇的镜像。在该协议之上,本包交付 PythonCodeRuntime(插件的默认导出),它以 language: 'python'isolation: 'process' 注册为 codeRuntime。每次 run() 启动一个全新的 python3 -I 进程,通过 fd 3 发送 boot 帧和程序,并为每个程序结果 resolve 一个 CodeRunResult——run() 仅在 seam 被误用时才 reject,例如 binding 命名空间不合法,或对 fiber 已被 dispose 的 runtime 发起调用。配置错误在更早的插件加载期被拒绝:非 Unix 平台、非正或非整数的预算、会被 setTimeout 截断的定时器值、超过单个 fd-3 帧承载能力的预算,以及最坏峰值会突破 RLIMIT_ASaddressSpaceMb/输出预算组合,都从构造器抛出,因此配置错误在装配时就失败,而不是等到之后某次运行。子进程把程序作为 async 函数体运行,因此顶层 awaitreturn 都可用;binding 调用经 fd 3 以 JSON-lines 回传。containment 不是安全边界——模型代码具有等同 bash 的信任级别;空环境、RLIMIT_CPURLIMIT_AS、墙钟上限与对子进程进程组的 SIGTERM→grace→SIGKILL 拆卸共同提供 containment。

Wire protocol

host 与 CPython 子进程在子进程的 fd 3 上交换一个无版本号的 JSON-lines 协议——每行一个 JSON 对象,让 stdout/stderr 空出给程序自己的输出。src/protocol.ts 是 host 侧;py/protocol.py 在 Python 侧镜像其帧词汇与共享的截断标记文本。

  • fd 3,而非 stdout —— Node 通过 stdio: ['pipe','pipe','pipe','pipe'] 按位置钉住通道;Python bootstrap 读取相同的 PROTOCOL_FD 常量。JSON-lines 帧。
  • host 把每个入站帧当作敌意输入 —— 模型代码对 fd 3 有完全访问权、可通过它发送任意内容,所以 validateChildFrame 在 host 读取前对每个帧做形状校验并重建:伪造的额外字段绝不随行,非数字的 call id 绝不会被回显进 reply,垃圾降为 undefined 被丢弃,而不是在 host 的 message handler 里抛错。Python 侧信任 host 回复(host 不受模型控制)。
  • lossless-JSON 穿越 —— 完成值与 binding 参数以精确 JSON 穿越。encodeJsonPlain 无递归地序列化一个 JSON.parse 产出的值,使低于字节预算的深层值能完整穿越,而不是死在 JSON.stringify 的栈限制上;checkDoneValue 在一次有界遍历中同时计量伪造完成值的字节长度与数字无损性,在把子节点入栈之前就拒绝超预算 payload;hasUnsafeIntegerToken 读取原始帧文本,捕获 JSON.parse 会静默舍入的整数 tokenhasNonLosslessNumber 拒绝无字节上限的 call.args 中的非有限数或负零。超出安全范围的整数型 double 通过 BigInt 数字序列化,穿越的是精确整数而非 String() 的舍入形式。
  • 共享截断标记 —— logTruncationMarker(maxBytes) 在两侧产出逐字节一致的文本,使被截断的日志运行无论从哪侧触达上限都读起来一致。log 帧的 truncated 标志把子进程 ledger 自身的标记与程序输出区分开。

Configuration

每个上限都是带默认值的、经校验的 Config 字段,可从 cordis.yml 修改(无硬编码可调项)。cpuSeconds(默认 60)是 RLIMIT_CPU 的整秒预算;子进程把软限设为 cpuSeconds、硬限设为 cpuSeconds + 1,因此内核在软限处发出的 SIGXCPU 被归类为 timeout,而 +1 秒的硬限是 SIGKILL 兜底。maxWallMs(默认 600000)是墙钟上限,为一个在等待无人 resolve 的 promise 的程序兜住 CPU 时间。addressSpaceMb(默认 512)是 RLIMIT_AS 上限,在 Darwin 上不施加(那里映射进每个进程的 dyld 共享缓存超过任何实际上限;cpuSecondsmaxWallMs 仍约束运行)。maxLogBytes(默认 65536)是共享的捕获日志字节预算;maxValueBytes(默认 32768)为完成值设上限;graceMs(默认 3000)是 SIGTERMSIGKILL 的 grace 窗口;pythonBin(默认 python3)是解释器,在子进程以空环境启动前先对 PATH 解析。

Model Experience

间接触达:经由 dsh-tools 中的 Code Mode——它把本后端精确的完成值(在放得下时)或一个明确的 invalid-output output-limit 失败,连同精确的 [dsh-code-runtime-python] log capture truncated at <maxLogBytes> bytes 日志标记,一并渲染进一条被保留的 run_code 结果。

KV Cache effect

不直接造成失效;任何对请求前缀的改动由上述具名 Consumer 负责。

Known Limitations and Deferred Work

  • 跨语言 guard 覆盖执行值与帧字段集,但不覆盖字段类型 —— tests/protocol-mirror.e2e.ts 使用真实 python3 比较 PROTOCOL_FD、日志截断标记,以及每个 TypedDict 的必填和可选字段。跨 TypeScript 与 Python 比较字段类型在此没有机械等价物,因此类型级漂移由 review 加后端真子进程套件负责。
  • RLIMIT_AS 在 macOS 上不施加 —— 在 exec 时映射进每个进程的 dyld 共享缓存超过任何实际的地址空间上限,内核会拒绝该 setrlimit 调用,故 addressSpaceMb 在那里被跳过。cpuSecondsmaxWallMs 仍约束每一次运行。
  • 调用 setsid() start_new_session=True 的后代会逃出 teardown。 终止是用 kill(-pid) 向子进程的进程组发信号;一个把自己移入新会话的后代已不在该进程组内,任何信号都到不了它。若它同时释放了继承而来的 stdoutstderrfd-3 管道,leader 的 close 仍会结算该次运行,在 closeDeadline 到界之后 fiber 变为完全停稳,而那个孤儿仍在运行。这是 containment 边界,而非安全边界——模型代码具有等同 bash 的信任级别,一个 bash 工具同样能 setsid 逃逸。要够到这样的孤儿需要追踪每一个后代 pid(如 bash-local 后端的 process-inspector 所做),此项已推迟;进程组 teardown 会回收所有留在组内的进程。
  • 日志与完成值的叠加峰值未被加载门建模。 每项预算都是各自对照 addressSpaceMb 检查的。模型的 daemon 线程可以在完成值被计量并分帧的窗口内持续写入、把日志 pending 重填到接近 maxLogBytes,于是两个峰值以任何门都不曾放行也不曾拒绝的方式相加。对 (maxLogBytes + maxValueBytes) 设门的方案经评估后推迟:它的判别用例无法在 RLIMIT_AS 之下确定性地构造出来,因此该门只能证明自己的算术。叠加峰值被触及时该次运行死为 worker-exit——containment 仍然成立,只是失败分类失真。
  • 1 秒双限 ulimit -t 1 下的 CPU 超限会被报告为 worker-exit,而非超时。 当宿主在一个硬 CPU 限制等于软限制(ulimit -t N 同时设置两者)且该限制为 1 的环境下启动时,_clamped 无法把软限制降到 0,因此内核在同一 tick 直接 SIGKILL 忙循环,SIGXCPU 永不送达。宿主只在 signal === 'SIGXCPU' 时把 CPU 超限分类为超时,因此该超限被报告为 worker-exit。当双限为 2 或更大时,软限制会被降低一个单位,SIGXCPU 触发,该次运行成为超时。两种情况 containment 都成立;只是分类被降级。
  • 宽 binding 回复会按成员展开宿主侧状态。 回复经由 @deepseek-ai/dsh-sessionsnapshotJsonValue 穿越,其 walkJsonValue 为每个成员压入一个任务帧,而 binding 回复在 seam 层没有字节上限。因此一个数百万元素的合法回复可以耗尽宿主堆。该性质属于那个共享遍历,而不属于本后端——worker-thread 后端消费同一个函数——所以修复应落在 packages/core/session,让所有消费方一并受益。