The line-aggregating stray capture from the previous round regressed three ways the review caught. Rewrite it on the fd-3 reader's raw-Buffer-chunk shape: accumulate chunks with a byte counter and split on the raw 0x0a byte, so a large newline-free write no longer re-copies the residual and re-scans from index 0 per chunk (both O(N^2)). Meter each admitted entry by serialized cost through a new jsonStringCostUpTo that walks to the cap and stops, so a near-budget control-char-dense line never allocates the sixfold-inflated JSON.stringify result the old ledger did (the critical: ~1.6 GiB transient under a large maxLogBytes). Flush the residual explicitly in the closeDeadline handler before it destroys the streams, so a setsid escapee's path (which fires no end) does not drop a leader's final newline-free diagnostic. Harden the sync-spawn leak assertion to a set difference against a pre-run snapshot, immune to a parallel worker's concurrent tmpdir create/delete. Decline the round-2 request to enforce the fd-3 ceiling per-frame: the counter check must precede Buffer.concat to prevent ~2x memory doubling (two regression tests assert this), and the batch-edge false reject it would fix is reachable only at a maxLogBytes/maxValueBytes configured within one pipe read of the 256 MiB ceiling, far past the defaults. Documented at the check and in the note Alternatives. Add flood, NUL-flood, short-escape, and closeDeadline-flush regression tests (restoring per-file 100% coverage); update the Agent Note and zh pair.
description, kind
| description | kind |
|---|---|
| CPython subprocess implementation of the DeepSeek Harness code-execution seam, with fd-3 bindings, resource limits, log capture, and process-group teardown. | package-reference |
@deepseek-ai/dsh-code-runtime-python
English | 中文
CPython-subprocess implementation of the @deepseek-ai/dsh-code-runtime seam. Companion to @deepseek-ai/dsh-code-runtime-worker-thread; trades the Node worker thread for a fresh python3 subprocess so model code is Python instead of TypeScript.
The package ships PythonCodeRuntime as its default export. The plugin registers as codeRuntime with language: 'python' and isolation: 'process'. Each run() spawns a fresh python3 -I process, sends a boot frame and the program over fd 3, and resolves a CodeRunResult for every program outcome — rejecting only for seam misuse (a malformed binding namespace or non-positive config). The child runs the program as the body of an async function, so top-level await and return both work; binding calls travel back over fd 3 as JSON-lines. Containment (not a security boundary — model code has bash-equivalent trust) comes from an empty environment, RLIMIT_CPU/RLIMIT_AS, a wall-clock ceiling, and a SIGTERM→grace→SIGKILL teardown on the child's process group.
Wire protocol
The host and the CPython subprocess exchange a versionless, JSON-lines protocol on the child's fd 3 — one JSON object per line, leaving stdout/stderr free for the program's own output. src/protocol.ts is the host side; py/protocol.py mirrors its message shapes and the shared truncation-marker text on the Python side.
- fd 3, not stdout — Node pins the channel positionally with
stdio: ['pipe','pipe','pipe','pipe']; the Python bootstrap reads the samePROTOCOL_FDconstant. JSON-lines framing. - Host treats every inbound frame as hostile — model code has full access to fd 3 and can post anything through it, so
validateChildFrameshape-validates and REBUILDS each frame before the host reads it: forged extra fields never ride along, a non-number call id can never be echoed into a reply, and junk drops toundefinedrather than throwing in the host's message handler. The Python side trusts host replies (the host is not model-controlled). - Lossless-JSON crossing — completion values and binding arguments cross as exact JSON.
encodeJsonPlainserializes aJSON.parse-produced value without recursion, so a deep value below the byte budget crosses intact instead of dying onJSON.stringify's stack limit;checkDoneValuemeters a forged completion value's byte length AND number losslessness in one bounded traversal that rejects an over-budget payload before enqueuing its children;hasUnsafeIntegerTokenreads the raw frame text to catch an integer token thatJSON.parsewould silently round;hasNonLosslessNumberrejects a non-finite or negative-zero number in unboundedcall.args. Beyond-safe-range integral doubles serialize throughBigIntdigits so the exact integer crosses, not the roundedString()form. - Shared truncation marker —
logTruncationMarker(maxBytes)produces byte-identical text on both sides, so a truncated log run reads the same however the cap was hit. Thelogframe'struncatedflag distinguishes the child ledger's own marker from program output.
Configuration
Every cap is a validated Config field with a default, changeable from cordis.yml (no hardcoded tunables). cpuSeconds (default 60) is the RLIMIT_CPU whole-second budget; the child sets the soft limit to cpuSeconds and the hard limit to cpuSeconds + 1, so the kernel's SIGXCPU at the soft limit classifies as a timeout while the +1s hard limit is a SIGKILL backstop. maxWallMs (default 600000) is the wall-clock ceiling that backstops CPU time for a program awaiting a promise nobody resolves. addressSpaceMb (default 512) is the RLIMIT_AS cap, not applied on Darwin (the dyld shared cache mapped into every process exceeds any practical cap there; cpuSeconds and maxWallMs still bound the run). maxLogBytes (default 65536) is the shared captured-log byte budget; maxValueBytes (default 32768) caps the completion value; graceMs (default 3000) is the SIGTERM→SIGKILL grace window; pythonBin (default python3) is the interpreter, resolved against PATH before the child spawns with an empty environment.
Model Experience
Indirectly, through Code Mode in dsh-tools, which renders this backend's exact completion value when it fits (or an explicit invalid-output / output-limit failure), plus the exact [dsh-code-runtime-python] log capture truncated at <maxLogBytes> bytes log marker, into a retained run_code result.
KV Cache effect
No direct invalidation; the named consumer owns any request-prefix changes.
Known Limitations and Deferred Work
- The cross-language guard covers executed values and frame field sets, not field types —
tests/protocol-mirror.e2e.tscomparesPROTOCOL_FD, the log truncation marker, and eachTypedDict's required and optional fields against a realpython3. Comparing field types across TypeScript and Python has no mechanical equivalent here, so review plus the backend's real-subprocess suite owns type-level drift. RLIMIT_ASis not enforced on macOS — the dyld shared cache mapped into every process at exec exceeds any practical address-space cap, and the kernel rejects thesetrlimitcall, soaddressSpaceMbis skipped there.cpuSecondsandmaxWallMsstill bound every run.- A descendant that calls
setsid()/start_new_session=Trueescapes teardown. Termination signals the child's process group withkill(-pid); a descendant that moves itself into a fresh session is no longer in that group and no signal reaches it. If it also releases the inherited stdout/stderr/fd-3 pipes, the leader'sclosestill settles the run, and after thecloseDeadlinebound the fiber goes quiescent while that orphan keeps running. This is the containment boundary, not a security one — model code has bash-equivalent trust, and a bash tool cansetsidaway just the same. Reaching such an orphan would require tracking every descendant pid (as the bash-local backend's process-inspector does) and is deferred; the process-group teardown reaps everything that stays in the group.