@deepseek-ai/dsh-code-runtime-worker
Worker-thread implementation of the @deepseek-ai/dsh-code-runtime seam: WorkerCodeRuntime runs each program in ONE fresh Node worker_threads.Worker — TypeScript in, type-stripped host-side, bindings bridged over the message port, { value, logs, error? } out. Containment, not a security boundary: trust posture is bash-equivalent by design (the Code Mode Agent Note § Trust posture), with containment bash does not have — separate isolate, empty environment, heap cap, hard termination.
Config
- id: code-runtime
name: '@deepseek-ai/dsh-code-runtime-worker'
config:
computeMs: 60000 # busy-time budget (measured event-loop active time)
maxWallMs: 600000 # wall-clock ceiling; never pauses for anything
maxOutputBytes: 67108864 # combined serialized outer-output cap (64 MiB)
maxOldGenerationSizeMb: 512 # worker heap cap (resourceLimits)
Every field is validated and defaulted; maxOutputBytes is a safe integer of at least four bytes, the remaining fields are positive finite numbers, and there are no other tunables.
Design
- One fresh worker per run, no pooling — a program's world dies with its worker: no cross-run state to log, state bleed unrepresentable, runs reconstructable from the session log alone.
- Type-strip host-side, in execution context — the program is wrapped in an async-function shell, stripped with
node:module'sstripTypeScriptTypes(erasable syntax only —enum/namespaces are rejected as a programexceptionand no worker spawns), and sliced back out byte-positioned; it then executes as the body of anAsyncFunction, so top-levelawait/returnwork. - The port assumes a hostile peer — model code can reach
parentPortand forge traffic, so every inbound message is shape-validated and REBUILT before anything reads it (null, primitives, junk types, and malformed payloads drop without a throw; forged extra fields never ride along), the host answers each call id at most once, resolves binding names as OWN properties only (a forgedconstructorcannot walk a prototype chain), drops post-settlement replies, and validates every binding resolution and completion as lossless JSON. Forgedlog/donemessages cannot bypass the outer cap: the host repeats validation and accounts every admitted log plus the completion or diagnostic. Worker-side namespaces are null-prototype withdefineProperty, so__proto__-shaped binding names are ordinary keys. - Two independent budgets, because the peer is hostile —
computeMsmeters the worker's MEASURED busy time (worker.performance.eventLoopUtilization()polling): a hot loop cannot hide behind a pending decoy dispatch, and a program awaiting a slow tool accrues nothing.maxWallMsbackstops what busy time cannot see (awaiting a promise nobody resolves). Both funnel intoworker.terminate(), which ends hot synchronous loops too; heap overflow surfaces as the worker's OOM exit (kind: 'worker-exit'). - Intermediate binding values are complete JSON — binding arguments and resolutions undergo iterative lossless-JSON validation, flatten into a bounded-depth pre-order wire value for structured clone, and rebuild iteratively on the other side. They have no byte, JavaScript call-stack, or nested structured-clone depth cap. They never enter the outer-output ledger or model context; provider/executor acquisition bounds and process/worker memory remain the limits.
- Logs stream eagerly into one outer ledger — console/stdout/stderr text crosses the port in emission order, so a timed-out or killed program still shows what it printed. Native writes that bypass the patched stream slots arrive on pipes independent of the completion port; settlement therefore continues bounded pipe capture until worker termination completes before materializing the result.
maxOutputBytesaccounts the JSON serialization of the outerlogsarray plus the completion value or failure-message payload; fixedCodeRunResultfield names, braces, the bounded error-kind tag, and later presentation whitespace are outside that variable-payload ledger. At or below the cap the exact value returns; a lossy completion isinvalid-output, and a combined overflow isoutput-limitrather than a substituted inspected string. The failure retains the fitting captured prefix and later follows the normal outerrun_codespill policy. - Empty environment — the worker gets
env: {}andexecArgv: []: no ambient credentials (stronger than the scrubbed-env rule for spawned commands) and no inherited loader flags. - Dispose to quiescence — teardown fails in-flight runs as
abortand AWAITS each worker's exit before resolving.
The worker entry, unbuilt and built
Source mode loads erasable-only src/worker.ts through Node's native type stripping. Its transitive runtime closure contains only Node built-ins and relative source modules, so a fresh checkout never requires a sibling workspace package's unbuilt lib/ export. The worker-local JSON snapshotter is parity-tested against the session-owned canonical boundary; both sides flatten and rebuild validated values around the message port so application nesting never reaches structured clone. Built mode passes the sibling lib/worker.cjs as a filesystem path because pkg's VFS Worker hook expects CommonJS; the same path works under ordinary Node. tests/built-lib.e2e.ts pins the real load path required by docs/testing.md.
The SDK surface is the default/named WorkerCodeRuntime class plus Config. The operational ./worker subpath exists only as the packaged spawn entry; the wire protocol and bootstrap helpers are source-private implementation details.
Model Experience
Indirectly, through Code Mode in dsh-tools, which renders the exact outer value when it fits or an explicit invalid-output / output-limit failure. Only the outer run_code result enters model context and its ordinary spill policy; binding traffic and intermediate values remain execution-local.
KV Cache effect
No direct invalidation; the named consumer owns any request-prefix changes.
Known Limitations and Deferred Work
- OS processes a program spawns survive termination —
worker.terminate()ends the thread only, weaker than bash-local's process-group kill; orphan cleanup is a deployment concern until a container backend exists. - Type-strip rides Node's experimental
stripTypeScriptTypesAPI — the relied-on behavior is pinned by unit tests, with amaro/sucrase as named drop-in replacements if it shifts. computeMsexpiry can overshoot by up to one poll interval — busy time is sampled every 25 ms (an internal constant, deliberately not config).- Programs get a five-method
consoleshim (log/info/warn/error/debug) — deliberately not Node's full console surface. - Intermediate binding values have no byte cap — a program can exhaust process or worker memory with a value that never becomes outer output.
- The 64 MiB default is a rejection boundary, not recoverable storage — outer spill can save only the bounded logs and diagnostic returned after
output-limit; bytes rejected beyond the runtime cap never reach the spill layer.