description: "CPython-subprocess code runtime: the dsh-code-runtime seam implementation for Python model code, with the fd-3 wire protocol it speaks."
English | 中文
dsh-experimental-code-runtime-python provides the private source-checkout PythonCodeRuntime, a CPython-subprocess implementation of the dsh-code-runtime seam. It registers as codeRuntime with language: 'python' and isolation: 'process', spawning a fresh CPython 3.10+ child per run() and executing the program as an async function body over a versionless JSON-lines protocol on the child's fd 3 (stdout/stderr stay free for the program's own output). The host side (src/protocol.ts) treats every inbound frame as hostile and rebuilds it before reading; the Python side (py/protocol.py) mirrors the message vocabulary. Containment — not a security boundary, model code has bash-equivalent trust — comes from a tempdir-only environment, RLIMIT_CPU/RLIMIT_AS, a wall-clock ceiling, and SIGTERM→grace→SIGKILL process-group teardown, with all caps validated at plugin load.
Choose this private experimental package only in an explicit source-checkout composition. Register PythonCodeRuntime beside dsh-tools and run() executes each program in a fresh CPython 3.10+ subprocess, resolving with result.value on success and result.error on failure (the orthogonal CodeRunFailure.kind taxonomy classifies parse failures, thrown exceptions, invalid completions, output overflows, budget expiry, aborts, and substrate death). It rejects only for seam misuse — a malformed binding namespace, or a call after disposal. Configuration is rejected at load: a non-Unix platform; an explicit pythonBin that is not an executable regular file or a bare name that does not resolve on PATH; a non-CPython, pre-3.10, or probe-failing interpreter; a non-positive or non-integer budget; a maxLogBytes below the truncation-marker floor (64); a timer value setTimeout would clamp; a budget larger than the effective fd-3 frame cap (lowered when the host heap cannot safely parse a near-cap frame); or an addressSpaceMb/output-budget pair whose worst-case peak would breach RLIMIT_AS.
The package's default export is the PythonCodeRuntime plugin. Its public surface also re-exports the host-side protocol vocabulary: validateChildFrame (rebuilds every inbound frame), the lossless-JSON codec and meters (encodeJsonPlain, checkDoneValue, hasUnsafeIntegerToken, hasNonLosslessNumber), logTruncationMarker (the shared truncation-marker text), plus resolvePythonBin (interpreter lookup against the current PATH), readProcessStart (process-start statistics for tests), detachResidual (a test seam for the settled run's resource cleanup), and hostFrameParseCeiling (the heap-derived frame parse cap a given heap limit admits). Every cap is a validated Config field with a default: cpuSeconds (60), maxWallMs (600000), addressSpaceMb (512, not applied on Darwin), maxLogBytes (65536), maxValueBytes (32768), graceMs (3000), and pythonBin (python3, resolved, executable-checked, version-probed under a five-second force-kill deadline, and frozen at load). Each child receives only TMPDIR; ambient credentials, PATH, HOME, and other host state stay unavailable.
Frames travel on the child's fd 3 as JSON-lines — one object per line — so stdout/stderr stay clear for the program's own output. Child → host: boot-ack, call, log, done. Host → child: boot (first frame, carrying every cap and the namespace declarations), run (after boot-ack, carrying only the program body), and one reply per call. A forged frame can carry both value and error on done, so a consumer must check error first and ignore value when it is set. A log frame's open flag marks an unterminated line committed by an explicit flush: the host appends the next log frame to the same entry, so print('a', end='', flush=True); print('b') reads back as one 'ab' entry rather than a fake newline (the split-billing arithmetic lives in the fd-3 protocol Agent Note's wire-contract section). The one exception to merging 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 (the marker stays last, with no re-charge).
Host-side validation drops junk without throwing, so a malformed or forged frame never crashes the host process: validateChildFrame returns undefined for anything that does not rebuild cleanly, a non-number call id can never be echoed into a reply, and forged extra fields never ride along. A completion value that is not lossless JSON, or that exceeds the configured byte budget, is rejected explicitly (non-lossless / over-budget) rather than silently rounded or truncated. An fd-3 frame whose raw length exceeds the effective frame parse cap (64 MiB, or lower when the host's configured heap cannot safely parse a near-cap frame — see hostFrameParseCeiling) settles the run as a worker-exit (the receive path caps raw frames before toString/JSON.parse so a compact wide frame cannot decode to far more host memory than its wire bytes admitted).
Read these when the runtime contract is not enough. They move from the seam definition to the design record and the companion backend.
Indirectly, through PTC mode in dsh-tools when an explicit source-checkout composition mounts this provider; it renders the program's completion value or failure into a retained run_code result, and no shipped profile mounts this private package.
No direct invalidation; the named consumer owns any request-prefix changes.
These limits define what the package does and does not cover; they are current package constraints, not a task backlog.
cpuSeconds is an int on both sides; a type-level drift is caught by review plus the backend's real-subprocess suite.setsid() is not reaped by the group teardown — kill(-pid) cannot reach it; the run still settles on the value the done frame decided, and the close-deadline backstop forces settlement if the orphan holds the pipes open, but the orphan itself outlives the fiber until it exits on its own.log frame that arrives after settlement is dropped — once the run has settled, host-side capture is closed; a late fd-3 log frame (from a thread that outlived the done frame) is discarded rather than appended to logs.maxValueBytes meters only the done frame's completion value; a wide binding reply is rebuilt host-side (snapshotJsonValue traversal) and encoded whole, bounded on both sides only by process memory (like a binding argument, which has no child-side budget either).ptc-python-turn snapshot replaces the headless PTC runtime through the real Loader; released profiles continue to use the worker-thread backend.result.logs may differ.ctx.codeRuntime is registered.[dsh-code-runtime-python] log capture truncated at <N> bytes and the dsh-code-runtime-python- tempdir prefix are byte-anchored by tests and are independent of the npm package name; promotion (dropping the experimental- prefix) does not rename them.run() is one-shot — logs become available only after CodeRunResult resolves; there is no streaming-log or progress interface for output produced by a running program.hostFrameParseCeiling); maxLogBytes/maxValueBytes are load-bounded to the same cap so an honest child's frames always fit; a model-constructed binding ARGUMENT above the cap (a value with no seam-level budget) trips it too — an accepted residual of the OOM guard.drain when the pipe is full; a child that keeps sending calls without consuming replies would otherwise grow the retained backlog (and the binding results it pins) until the wall clock, so the backlog cap fails the run early. Binding results carry no seam-level byte cap, so this is a count bound, not a byte bound.worker-exit, containment holds, and only the failure classification is degraded.ulimit -t 1 CPU overrun is reported as worker-exit, not a timeout — when the host starts under a hard CPU limit equal to the soft and that limit is 1, _clamped cannot lower the soft, so the kernel SIGKILLs the busy loop and SIGXCPU is never delivered; containment holds, only the classification is degraded.