the ptc-runtime-python fd-3 frame protocol
the ptc-runtime-python fd-3 frame protocol
`@deepseek-ai/dsh-experimental-ptc-runtime-python` owns the wire protocol intended for a CPython ptc-runtime provider. Such a provider runs each model program in a fresh `python3 -I` subprocess and bridges binding calls and completion values over the child's fd 3. The host cannot trust that channel: model code has full access to fd 3 and can forge any frame, so every inbound frame is hostile input that the host must
English
Problem
@deepseek-ai/dsh-experimental-ptc-runtime-python owns the wire protocol intended for a CPython ptc-runtime provider. Such a provider runs each model program in a fresh python3 -I subprocess and bridges binding calls and completion values over the child's fd 3. The host cannot trust that channel: model code has full access to fd 3 and can forge any frame, so every inbound frame is hostile input that the host must validate and rebuild before reading. The protocol also has to carry lossless JSON without the depth limit JSON.stringify and json.dumps impose, because the seam's PtcJsonValue is depth-unbounded.
The experimental package contains both the protocol and runtime implementation: PythonPtcRuntime (the plugin's default export), the python3 -I subprocess path, and the Python-side JSON codec all live in @deepseek-ai/dsh-experimental-ptc-runtime-python. The protocol builds on the portable identifier seam.
Decision
src/protocol.ts is the host side of the wire vocabulary and its hostile-frame codec:
validateChildFrameshape-validates and REBUILDS every inbound frame. The compile-time union means nothing on fd 3 — a forged frame can carrynull, poisoned fields, or omit required ones — so each accepted frame is reconstructed field by field: forged extras never ride along, a non-finite call id can never be echoed into a reply, and junk returnsundefinedto be dropped rather than throwing in the host's message handler.encodeJsonPlain/checkDoneValue/hasUnsafeIntegerToken/hasNonLosslessNumberare the lossless-JSON codec and meters. They traverse iteratively (an explicit stack, not recursion) so a deep value below the byte budget crosses intact;checkDoneValuefolds byte-metering and number-losslessness into one walk that rejects an over-budget payload before the incremental work it would otherwise add — the enqueued children; strings and keys are metered by a non-allocating escaped-size scan (jsonStringBytesUpTo), so the escaped copy is never materialized. It does not re-bound the frame's own width:done.valueis alreadyJSON.parse'd when the check runs, so a consuming runtime must cap fd-3 bytes before parsing. Beyond-safe-range integral doubles serialize throughBigIntdigits so the exact integer crosses, notString()'s rounded form.logTruncationMarkerproduces the in-band marker text a log ledger emits when it exhausts its byte budget.
py/protocol.py mirrors the message shapes as TypedDicts and re-declares the two surfaces both sides EXECUTE against — PROTOCOL_FD = 3 and log_truncation_marker — with byte-identical text.
The package ships the runtime alongside the protocol; it remains independently buildable. check-workspace-constraints reads every packages/<group>/<pkg>/package.json unconditionally, while the coverage check exercises the package as soon as its directory exists.
Wire contract
Frames are JSON-lines on fd 3, one object per line, leaving stdout/stderr free for the program's own output. Child → host: boot-ack, call, log, done. Host → child: boot (first frame), run (after boot-ack), and one reply per call. The log frame's truncated flag marks the frame that IS the child ledger's own truncation marker, so the host stops capturing at the same point the child did instead of inferring it from its own budget. The log frame's open flag marks an unterminated line committed by an explicit flush: the host holds it and appends the next frame to the same entry, so an explicit flush followed by more text reads back as one line rather than a fake newline. The one exception is truncation: when a later over-budget frame trips the ledger, the already-billed prefix is committed as its own entry and the truncation marker follows it (marker last, no re-charge). The merged entry's wire cost is billed exactly once, split incrementally across its fragments on both sides (O(k) for k fragments, never a re-walk of the whole hold): the FIRST fragment pays the full JSON-string cost plus the separator, each continuation and the closing frame pay only their content; the host's exact-cost caps are logBudget - 1 for a first fragment (the ledger's reserved byte, matching admit) and logBudget + 2 for a continuation or closing frame (billed without the two quotes), and jsonStringCostUpTo returns undefined below a 2-byte cap; the child keys its split billing off _open_started alone, so a closing frame bills as the merged tail. done.error.kind is one of exception, invalid-output, output-limit, or timeout from the CPU check after model code returns; wall deadlines, aborts, and substrate death are observed host-side. The post-return CPU check attempts a diagnostic on fd 3 before mandatory signal termination because a sandbox launcher can convert that signal into an ambiguous exit code. Ordinary soft-limit expiry retains default SIGXCPU termination without a Python handler, so native calls cannot postpone enforcement until Python resumes. These frames remain untrusted outcome reports; kernel resource limits enforce the CPU ceiling independently.
Mirror alignment
py/protocol.py and src/protocol.ts agree that LogMessage carries truncated, DoneMessage.error carries kind, and Namespace may carry errorClass. tests/protocol-mirror.e2e.ts spawns a real python3 and asserts PROTOCOL_FD, log_truncation_marker, and each TypedDict's required and optional wire field sets against src/protocol.ts. A renamed or dropped field, or a required/optional mismatch, fails the test. Field types are not compared across the language boundary; review and the runtime's real-subprocess suite (runtime.spec.ts) own that gap.
Alternatives considered
Require a future Python JSON codec (_encode_json_plain / _decode_json_plain) to live in py/protocol.py for cross-side symmetry with protocol.ts. Rejected. The repository's "prefer symmetry for parallel values" rule points at genuinely parallel values; these are not. The host-side codec in protocol.ts validates hostile input and is self-contained. A child-side codec would produce trusted output and belong with bootstrap-owned emission and cost accounting; forcing only its entry points into protocol.py would couple the vocabulary mirror to runtime internals or create an import cycle. protocol.py remains a pure wire-vocabulary mirror; the codec (_encode_json_plain / _decode_json_plain) lives in bootstrap.py with the runtime it serves.
Keep the protocol files outside a buildable package until a runtime ships. Rejected: the workspace-constraint and coverage checks require every directory under packages/<group>/<pkg> to be a buildable package, and the protocol has independent tests and a public wire vocabulary.
Consequences
Bought: the fd-3 protocol and its hostile-input codec form a self-contained, fully unit-covered layer, with an executing guard against TypeScript/Python field-set drift. The runtime built on it (bootstrap.py) consumes the reviewed wire contract.
Cost: the package name denotes a Python runtime family and src/index.ts exports the full PythonPtcRuntime implementation, so the protocol vocabulary is only one part of the package surface. The mirror e2e compares field names and required/optional status across the two sides but not field types; comparing type declarations across TypeScript and Python has no mechanical equivalent, so review and the runtime's real-subprocess suite retain that responsibility.
中文
Problem
@deepseek-ai/dsh-experimental-ptc-runtime-python 负责供 CPython ptc-runtime 提供方使用的 wire protocol。这样的提供方会在全新的 python3 -I 子进程中运行每个模型程序,并通过子进程 fd 3 桥接 binding 调用与完成值。Host 不能信任这条通道:模型代码可以完全访问 fd 3 并伪造任意帧,因此 host 必须把每个入站帧视为敌意输入,先校验并重建后才能读取。协议还必须承载无深度限制的 lossless JSON,因为 seam 的 PtcJsonValue 深度无界,而 JSON.stringify 和 json.dumps 都有递归深度限制。
这个实验包同时包含协议与 runtime 实现:PythonPtcRuntime(插件的默认导出)、python3 -I 子进程路径与 Python 侧 JSON codec 都在 @deepseek-ai/dsh-experimental-ptc-runtime-python 中。协议建立在可移植标识符 seam之上。
Decision
src/protocol.ts 是 wire vocabulary 的 host 侧及其敌意帧编解码:
validateChildFrame对每个入站帧做形状校验并重建。编译期 union 在 fd 3 上毫无意义——伪造帧可携带null、被污染的字段,或省略必需字段——所以每个被接受的帧都逐字段重建:伪造的额外字段绝不随行,非有限的 call id 绝不会被回显进 reply,垃圾返回undefined被丢弃,而不是在 host 的 message handler 里抛错。encodeJsonPlain/checkDoneValue/hasUnsafeIntegerToken/hasNonLosslessNumber是 lossless-JSON 编解码器与计量器。它们迭代遍历(显式栈,非递归),使低于字节预算的深层值能完整穿越;checkDoneValue把字节计量和数字无损性折进一次遍历,在新增入栈子节点之前就拒绝超预算 payload;字符串与 key 由非分配的转义尺寸扫描(jsonStringBytesUpTo)计量,从不物化转义副本。它不会重新约束帧自身的宽度:done.value在检查运行时已经过JSON.parse,因此消费 runtime 必须在解析前限制 fd-3 字节数。超出安全范围的整数型 double 通过BigInt数字序列化,穿越的是精确整数而非String()的舍入形式。logTruncationMarker产出日志 ledger 耗尽字节预算时发出的带内标记文本。
py/protocol.py 用 TypedDict 镜像消息形状,并重新声明两侧都会 EXECUTE 的两个面——PROTOCOL_FD = 3 与 log_truncation_marker——文本逐字节一致。
该包随协议一起交付 runtime,同时保持独立可构建。check-workspace-constraints 会无条件读取每个 packages/<group>/<pkg>/package.json,coverage 检查则会在包目录存在时立即覆盖该包。
Wire contract
帧是 fd 3 上的 JSON-lines,每行一个对象,让 stdout/stderr 空出给程序自己的输出。Child → host:boot-ack、call、log、done。Host → child:boot(首帧)、run(在 boot-ack 之后)、以及每个 call 对应一个 reply。log 帧的 truncated 标志标记那个本身就是子进程 ledger 截断标记的帧,使 host 在与子进程相同的点停止捕获,而不是从自己的预算去推断。log 帧的 open 标志标记由显式 flush 提交的未结束行:宿主持有它并把下一个帧追加到同一条目,因此显式 flush 后接更多文本读回为一行而不是假换行。唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。合并条目的线上成本恰好计费一次,在两侧按片段增量分摊(k 个片段 O(k),绝不对整个持有重走):首片段付完整 JSON 字符串成本加分隔符,每个续接与闭合帧只付内容;宿主精确成本 cap 是首片段 logBudget - 1(账本预留字节,与 admit 一致)、续接或闭合帧 logBudget + 2(不含两个引号计费),且 jsonStringCostUpTo 在低于 2 字节 cap 时返回 undefined;子进程按 _open_started 单独键控拆分计费,因此闭合帧按合并尾部计费。done.error.kind 是 exception、invalid-output、output-limit 或模型代码返回后 CPU 检查产生的 timeout 之一;wall 截止、abort 和 substrate 死亡在 host 侧观测。返回后的 CPU 检查会在强制信号终止前尝试通过 fd 3 报告诊断,因为沙箱 launcher 可能把该信号转换为有歧义的退出码。普通软限制到期保留默认的 SIGXCPU 终止行为,不安装 Python handler,因此原生调用不能把强制执行延后到 Python 恢复运行。这些帧仍是不可信的结果报告;内核资源限制独立强制执行 CPU 上限。
Mirror alignment
py/protocol.py 与 src/protocol.ts 一致规定:LogMessage 携带 truncated,DoneMessage.error 携带 kind,Namespace 可以携带 errorClass。tests/protocol-mirror.e2e.ts 启动真实 python3,对照 src/protocol.ts 断言 PROTOCOL_FD、log_truncation_marker 以及每个 TypedDict 的必填和可选 wire 字段集。字段改名、删除或必填/可选性不一致都会使测试失败。字段类型不跨语言边界比较;这项缺口由评审和 runtime 的真实子进程套件(runtime.spec.ts)负责。
Alternatives considered
**要求未来的 Python JSON codec(_encode_json_plain / _decode_json_plain)放进 py/protocol.py,以便与 protocol.ts 跨侧对称。**拒绝。仓库的 “prefer symmetry for parallel values” 规则指向真正平行的值;这两者不是。protocol.ts 中的 host 侧 codec 校验敌意输入且自包含。Child 侧 codec 会产出受信任输出,应与 bootstrap 拥有的发出逻辑和成本核算放在一起;只把入口强塞进 protocol.py 会让 vocabulary 镜像耦合 runtime 内部实现,或制造 import 环。protocol.py 保持纯 wire-vocabulary 镜像;codec(_encode_json_plain/_decode_json_plain)与它所服务的 runtime 一起位于 bootstrap.py。
**在 runtime 交付前把协议文件放在不可构建的包外。**拒绝:workspace-constraint 与 coverage 检查要求 packages/<group>/<pkg> 下的每个目录都是可构建包,而协议本身拥有独立测试与公开 wire vocabulary。
Consequences
收获:fd-3 协议及其敌意输入 codec 构成自包含、unit 全覆盖的一层,并由执行中的 guard 防止 TypeScript/Python 字段集漂移。基于它构建的 runtime(bootstrap.py)消费经过评审的 wire contract。
代价:包名表示 Python runtime 家族,而 src/index.ts 导出完整的 PythonPtcRuntime 实现,协议 vocabulary 只是包表面的一部分。mirror e2e 会比较两侧字段名与必填/可选状态,但不比较字段类型;跨 TypeScript 与 Python 比较类型声明没有机械等价物,因此评审与 runtime 的真实子进程套件继续负责这项检查。