Files
deepseek-harness/packages/code-runtime/code-runtime-python/README.zh.md
T
Chinesezjc 6f58f9c336 fix(code-runtime-python): pace concurrent binding replies against fd 3
`sendReply` ignored `proto.write`'s `false` return, so a program resolving
several large values in one `asyncio.gather` round encoded every reply in the
same turn and queued all of them in fd 3's writable buffer. Binding resolution
carries no seam-level byte cap to bound that, and the failure kills the host
process rather than failing the run: measured on a 64 KiB-highWaterMark pipe,
eight 4 MiB replies buffered 32.0 MiB at once against 0.0 MiB once paced.

Replies now go through a queue that encodes and writes one frame at a time,
awaiting `drain` when the pipe is full. The encode happens inside the loop, so a
queued reply the run no longer needs is dropped by the `settled` check without
ever being serialized.

This was previously deferred on the grounds that serializing would narrow the
seam's concurrency contract. That reasoning was wrong: the child matches each
reply to its `call` by id from a pump that reads fd 3 continuously, so arrival
order was never observable, and the bindings still run concurrently. Only the
host's peak memory and the flush timing change. The README entry recording the
deferral is removed and the Agent Note records the mechanism instead.
2026-08-31 14:33:31 +08:00

7.9 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 仍然成立,只是失败分类失真。
  • 宽 binding 回复会按成员展开宿主侧状态。 回复经由 @deepseek-ai/dsh-sessionsnapshotJsonValue 穿越,其 walkJsonValue 为每个成员压入一个任务帧,而 binding 回复在 seam 层没有字节上限。因此一个数百万元素的合法回复可以耗尽宿主堆。该性质属于那个共享遍历,而不属于本后端——worker-thread 后端消费同一个函数——所以修复应落在 packages/core/session,让所有消费方一并受益。