feat(code-runtime): execute Node programs through confined processes

This commit is contained in:
Tianyi Cui committed 2026-09-12 15:03:01 +08:00
1 parent f95f7ec8dc
commit 75ed8da3e0
54 files changed
+1933 -2277

No files matched your search

@@ -18,7 +18,7 @@ import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import { NodeCodeRuntime } from '@deepseek-ai/dsh-code-runtime-node'
import { mountRuntime } from '../../../../../../packages/code-runtime/code-runtime-node/tests/setup.ts'
import LocalFileSystem from '@deepseek-ai/dsh-fs-local'
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
import * as WorkspaceContext from '@deepseek-ai/dsh-agent-instructions'
@@ -60,11 +60,11 @@ async function ptcModeHarness(cwd: string): Promise<Context> {
await harness.plugin(AgentRegistry)
await harness.plugin(AgentLoop, { agents: [] })
await harness.plugin(LlmDeepSeek)
await harness.plugin(LocalSubprocessRuntime)
if (harness.get('subprocess') === undefined) await harness.plugin(LocalSubprocessRuntime)
await harness.plugin(BashEnvPlugin)
await harness.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 })
await harness.plugin(ToolBash)
await harness.plugin(NodeCodeRuntime, {})
await mountRuntime(harness)
return harness
}
@@ -81,14 +81,14 @@ async function workspacePtcModeHarness(): Promise<Context> {
await harness.plugin(WorkspaceContext, { maxBytes: 65536 })
await harness.plugin(AgentLoop, { agents: [] })
await harness.plugin(LlmDeepSeek, { models: [{ id: 'deepseek-v4-flash' }] })
await harness.plugin(NodeCodeRuntime, {})
await mountRuntime(harness)
return harness
}
let keylessCall = 0
const testToolSignal = new AbortController().signal
/** Execute one outer PTC mode call through the real registry and worker. */
/** Execute one outer PTC mode call through the real registry and Node process. */
function runCode(
harness: Context,
code: string,
@@ -114,28 +114,28 @@ function completion(result: ToolExecutionResult): unknown {
return value.result
}
/** Keyless real-worker harness for direct typed-binding acceptance tests. */
/** Keyless real-process harness for direct typed-binding acceptance tests. */
async function typedPtcModeHarness(): Promise<Context> {
const harness = new Context()
await harness.plugin(SystemPrompt)
await harness.plugin(ToolRuntime, { mode: 'ptc' })
await harness.plugin(NodeCodeRuntime, {})
await mountRuntime(harness)
return harness
}
/** Keyless real-worker harness with the task-owned bash lifecycle. */
/** Keyless real-process harness with the task-owned bash lifecycle. */
async function backgroundPtcModeHarness(cwd: string): Promise<Context> {
const harness = await typedPtcModeHarness()
await harness.plugin(LocalJobRegistry)
await harness.plugin(ToolTasks, {})
await harness.plugin(LocalSubprocessRuntime)
if (harness.get('subprocess') === undefined) await harness.plugin(LocalSubprocessRuntime)
await harness.plugin(BashEnvPlugin)
await harness.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 })
await harness.plugin(ToolBash)
return harness
}
describe('PTC mode typed values: keyless real-worker contracts', () => {
describe('PTC mode typed values: keyless real-process contracts', () => {
it('crosses a large intermediate value intact and exposes only typed tool failure fields', async () => {
ctx = await typedPtcModeHarness()
ctx.tools.register(defineTool({
@@ -2,7 +2,7 @@
import { Context } from '@deepseek-ai/cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { ToolResultNode } from '@deepseek-ai/dsh-client-ui-chat/client'
import { NodeCodeRuntime } from '@deepseek-ai/dsh-code-runtime-node'
import { mountRuntime } from '../../../code-runtime/code-runtime-node/tests/setup.ts'
import { ToolCallId } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import { SpillLocator, SpillStore, type SaveTextSpill, type SpillRef } from '@deepseek-ai/dsh-spill'
@@ -40,7 +40,7 @@ async function executeShell(text: string, nested: boolean, name = 'bash', maxInl
await ctx.plugin(ToolRuntime, { mode: 'both' })
await ctx.plugin(MemorySpillStore)
await ctx.plugin(SpillPolicy, { maxInlineBytes })
if (nested) await ctx.plugin(NodeCodeRuntime, {})
if (nested) await mountRuntime(ctx, {})
ctx.effect(() => ctx.tools.register(defineContentToolFixture({
name,
description: 'Return deterministic shell text without spawning a process.',
@@ -1,6 +1,6 @@
{
"name": "@deepseek-ai/dsh-code-runtime-node",
"description": "Worker-thread implementation of the DeepSeek Harness code-execution seam",
"description": "Sandboxed Node process implementation of the DeepSeek Harness code-execution capability",
"version": "0.1.5-rc.2",
"publishConfig": {
"access": "public"
@@ -18,15 +18,14 @@
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./worker": {
"types": "./lib/types/worker.d.ts",
"default": "./lib/worker.cjs"
},
"./package.json": "./package.json"
"./package.json": "./package.json",
"./process": {
"default": "./lib/process.js"
}
},
"files": [
"lib/index.js",
"lib/worker.cjs",
"lib/process.js",
"lib/types/**/*.d.ts"
],
"license": "MIT",
@@ -34,7 +33,11 @@
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-code-runtime": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^"
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^"
},
"dependencies": {
"@deepseek-ai/dsh-util-values": "workspace:^",
@@ -44,6 +47,14 @@
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-code-runtime": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^"
"@deepseek-ai/dsh-timeout": "workspace:^",
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-subprocess": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-subprocess-local": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-sandbox-local": "workspace:^"
}
}
@@ -0,0 +1,51 @@
/** Validate and capture the binding names available to one program. */
import { DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from '@deepseek-ai/dsh-code-runtime'
import type { CodeBindingNamespace, CodeRunRequest } from '@deepseek-ai/dsh-code-runtime'
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/
/**
* Reject unusable namespaces before starting a process.
* @param request - Host-owned binding declarations.
* @returns Namespaces keyed by their declared global.
*/
export function validateBindings(request: CodeRunRequest): Map<string, CodeBindingNamespace> {
const bindings = new Map<string, CodeBindingNamespace>()
for (const namespace of request.bindings) {
if (!IDENTIFIER.test(namespace.global) || PORTABLE_RESERVED_WORDS.has(namespace.global)) {
throw new Error(`dsh-code-runtime-node: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`)
}
// RESERVED_BINDING_GLOBALS is the seam's shared backend-owned set:
// `console` is THIS backend's log-capture slot; the dunder entries exist
// for the Python side — its seeded/wrapped slots plus the `__debug__`
// compile-time constant — refused here too so the namespace list stays
// portable across backends. The seam declaration is the single home for
// why each entry is reserved.
if (RESERVED_BINDING_GLOBALS.has(namespace.global)) {
throw new Error(`dsh-code-runtime-node: reserved binding global ${JSON.stringify(namespace.global)}`)
}
if (bindings.has(namespace.global)) {
throw new Error(`dsh-code-runtime-node: duplicate binding global ${JSON.stringify(namespace.global)}`)
}
bindings.set(namespace.global, namespace)
}
const errorClassNames = new Set<string>()
for (const namespace of request.bindings) {
const descriptor = namespace.errorClass
if (!descriptor) continue
if (!IDENTIFIER.test(descriptor.name) || PORTABLE_RESERVED_WORDS.has(descriptor.name)) {
throw new Error(`dsh-code-runtime-node: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`)
}
if (RESERVED_BINDING_GLOBALS.has(descriptor.name)) {
throw new Error(`dsh-code-runtime-node: reserved binding global ${JSON.stringify(descriptor.name)}`)
}
if (bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) {
throw new Error(`dsh-code-runtime-node: duplicate injected global ${JSON.stringify(descriptor.name)}`)
}
const member = descriptor.memberNameProperty
if (member.length === 0 || RESERVED_ERROR_MEMBERS.has(member) || DUNDER_MEMBER.test(member)) {
throw new Error(`dsh-code-runtime-node: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`)
}
errorClassNames.add(descriptor.name)
}
return bindings
}
@@ -1,14 +1,12 @@
/**
* Worker-side execution logic, written as plain functions over an injected port so the unit
* suite can run every line IN-PROCESS against a fake port (a real worker thread is a separate
* V8 isolate the coverage provider cannot observe).
* Program evaluation, output capture, and host binding proxies over the process channel.
* @module @deepseek-ai/dsh-code-runtime-node/src/bootstrap
*/
import { inspect } from 'node:util'
import type { DoneMessage, ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts'
import type { DoneMessage, ReplyMessage, ProgramBootData, ProgramToHost } from './protocol.ts'
import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts'
import { decodeWorkerJson, encodeWorkerJson, snapshotCodeJsonValue } from './worker-json.ts'
import { decodeCodeJsonWire, encodeCodeJsonWire, snapshotCodeJsonValue } from './json-wire.ts'
const CapturedError = Error
const capturedObjectCreate = Object.create
@@ -24,7 +22,7 @@ function defineBindingErrorField(error: Error, key: string, value: string): void
/** The port API the bootstrap needs — satisfied by `parentPort` and by the tests' fake. */
export interface BootstrapPort {
postMessage(message: WorkerToHost): void
postMessage(message: ProgramToHost): void
on(event: 'message', listener: (message: ReplyMessage) => void): void
}
@@ -121,7 +119,7 @@ export function makeConsoleShim(logs: LogBuffer): Record<(typeof CONSOLE_LEVELS)
/**
* Redirect a stream's `write` into the log buffer (the program-visible
* `process.stdout`/`process.stderr` in the real worker), so raw writes land in emission order
* `process.stdout`/`process.stderr` in the Node child), so raw writes land in emission order
* alongside console output instead of racing down a pipe. It preserves Node's optional callback
* contract: the callback runs asynchronously after admission, even when the log budget drops
* the write.
@@ -186,7 +184,7 @@ export function prepareCompletion(
if (jsonValueBytesUpTo(snapshot, remainingOutputBytes) === undefined) {
return outputLimit(maxOutputBytes)
}
return { value: encodeWorkerJson(snapshot) }
return { value: encodeCodeJsonWire(snapshot) }
}
/** Build the fixed overflow fragment without carrying rejected variable bytes. */
@@ -265,7 +263,7 @@ function bindingFailure(errorClass: BindingErrorConstructor | undefined, memberN
* @returns constructors keyed by their owning namespace global.
*/
export function makeBindingErrorClasses(
data: Pick<WorkerBootData, 'namespaces'>,
data: Pick<ProgramBootData, 'namespaces'>,
): Map<string, BindingErrorConstructor> {
const classes = new Map<string, BindingErrorConstructor>()
for (const namespace of data.namespaces) {
@@ -278,7 +276,7 @@ export function makeBindingErrorClasses(
* Route host replies into the pending-call map: each reply settles its call
* at most once, and a reply for an unknown id (stray, or a duplicate answer
* to an id already settled) is ignored. Shared wiring between
* {@link runWorkerMain} and the tests that exercise {@link makeNamespaces}
* {@link runProgram} and the tests that exercise {@link makeNamespaces}
* standalone.
* @param port - the port whose `message` events carry the replies.
* @param pending - the id-keyed map of unsettled binding calls.
@@ -289,7 +287,7 @@ export function wireReplies(port: BootstrapPort, pending: Map<number, PendingCal
if (!entry) return
pending.delete(message.id)
if (message.ok) {
const value = decodeWorkerJson(message.value)
const value = decodeCodeJsonWire(message.value)
if (value === undefined) entry.reject(new CapturedError('binding resolution must be lossless JSON'))
else entry.resolve(value)
} else {
@@ -313,7 +311,7 @@ export function wireReplies(port: BootstrapPort, pending: Map<number, PendingCal
* @returns one namespace object per declaration, in declaration order.
*/
export function makeNamespaces(
data: Pick<WorkerBootData, 'namespaces'>,
data: Pick<ProgramBootData, 'namespaces'>,
port: BootstrapPort,
pending: Map<number, PendingCall>,
nextId: { value: number },
@@ -344,7 +342,7 @@ export function makeNamespaces(
},
})
try {
port.postMessage({ type: 'call', id, global, name, args: encodeWorkerJson(detached) })
port.postMessage({ type: 'call', id, global, name, args: encodeCodeJsonWire(detached) })
} catch (error: unknown) {
pending.delete(id)
const message = `binding arguments must be structured-cloneable: ${error instanceof CapturedError ? error.message : String(error)}`
@@ -366,9 +364,9 @@ export function makeNamespaces(
* @param streams - stdout/stderr objects captured as program logs.
* @returns after posting the done message.
*/
export async function runWorkerMain(
export async function runProgram(
port: BootstrapPort,
data: WorkerBootData,
data: ProgramBootData,
streams: { stdout: PatchableStream; stderr: PatchableStream },
): Promise<void> {
const logs = new LogBuffer(
@@ -0,0 +1,124 @@
/** Length-prefixed JSON transport with bounded input and queued writes. */
import { Buffer } from 'node:buffer'
import type { Duplex } from 'node:stream'
import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
import { jsonValueBytesUpTo } from './output-json.ts'
const stringify = JSON.stringify
const parse = JSON.parse
/** One co-shipped process channel; the consumer owns frame validation and terminal outcomes. */
export class JsonChannel {
private readonly stream: Duplex
private readonly maxBytes: number
private readonly receive: (message: unknown, bytes: number) => void
private readonly failure: (error: Error, kind: 'io' | 'protocol') => void
private readonly header = Buffer.alloc(4)
private headerBytes = 0
private payload: Buffer | undefined
private payloadBytes = 0
private queuedBytes = 0
private closed = false
private tail: Promise<void> = Promise.resolve()
constructor(
stream: Duplex,
maxBytes: number,
receive: (message: unknown, bytes: number) => void,
failure: (error: Error, kind: 'io' | 'protocol') => void,
) {
this.stream = stream
this.maxBytes = maxBytes
this.receive = receive
this.failure = failure
stream.on('data', this.onData)
stream.on('error', this.onError)
stream.on('end', this.onEnd)
}
private readonly onError = (error: Error): void => { if (!this.closed) this.failure(error, 'io') }
private readonly onEnd = (): void => { if (!this.closed) this.failure(new Error('control channel ended before the program settled'), 'io') }
private readonly onData = (chunk: Buffer): void => {
if (this.closed) return
try {
let offset = 0
while (offset < chunk.length && !this.closed) {
if (this.payload === undefined) {
const bytes = Math.min(4 - this.headerBytes, chunk.length - offset)
chunk.copy(this.header, this.headerBytes, offset, offset + bytes)
this.headerBytes += bytes
offset += bytes
if (this.headerBytes !== 4) continue
const length = this.header.readUInt32BE(0)
if (length === 0 || length > this.maxBytes) throw new Error(`control frame exceeds ${this.maxBytes} bytes or is empty`)
this.payload = Buffer.allocUnsafe(length)
this.payloadBytes = 0
this.headerBytes = 0
}
const bytes = Math.min(this.payload.length - this.payloadBytes, chunk.length - offset)
chunk.copy(this.payload, this.payloadBytes, offset, offset + bytes)
this.payloadBytes += bytes
offset += bytes
if (this.payloadBytes !== this.payload.length) continue
const frame = this.payload
this.payload = undefined
this.payloadBytes = 0
this.receive(parse(new TextDecoder('utf-8', { fatal: true }).decode(frame)), frame.length)
}
} catch (error: unknown) {
this.failure(error instanceof Error ? error : new Error(String(error)), 'protocol')
}
}
/**
* Queue a bounded frame and honor stream backpressure.
* @param message - JSON-only co-shipped protocol value.
* @returns Resolves when this frame has been written, or rejects after transport failure.
*/
send(message: unknown): Promise<void> {
if (this.closed) return Promise.reject(new Error('control channel is closed'))
const size = jsonValueBytesUpTo(message as CodeJsonValue, this.maxBytes)
if (size === undefined || this.queuedBytes + size > this.maxBytes) {
return Promise.reject(new Error(`control output exceeds ${this.maxBytes} queued bytes`))
}
const body = Buffer.from(stringify(message), 'utf8')
const header = Buffer.alloc(4)
header.writeUInt32BE(body.length)
this.queuedBytes += body.length
const write = async (): Promise<void> => {
if (this.closed) throw new Error('control channel is closed')
await new Promise<void>((resolve, reject) => {
const stop = (error?: Error): void => {
this.stream.off('error', onError)
this.stream.off('close', onClose)
if (error) reject(error)
else resolve()
}
const onError = (error: Error): void => { stop(error) }
const onClose = (): void => { stop(new Error('control channel closed during a write')) }
this.stream.once('error', onError)
this.stream.once('close', onClose)
this.stream.cork()
this.stream.write(header)
this.stream.write(body, (error) => { stop(error ?? undefined) })
this.stream.uncork()
})
}
const result = this.tail.then(write).finally(() => { this.queuedBytes -= body.length })
this.tail = result.catch(() => {})
return result
}
/** Stop reads and close the owned endpoint; pending writes reject on closure. */
close(): void {
if (this.closed) return
this.closed = true
this.payload = undefined
this.stream.off('data', this.onData)
this.stream.off('end', this.onEnd)
this.stream.destroy()
}
/** Wait for accepted writes to finish or fail. */
async drain(): Promise<void> { await this.tail }
}
@@ -1,560 +1,308 @@
/**
* Worker-thread code runtime: a fresh worker runs each host-type-stripped TypeScript program
* and bridges bindings over its message port. This is containment, not a security boundary:
* model code has bash-equivalent trust despite an empty environment, a heap cap, measured
* event-loop busy-time and wall-time budgets, and termination that also stops synchronous loops.
* @module @deepseek-ai/dsh-code-runtime-node
*/
import { Worker } from 'node:worker_threads'
/** Confined Node programs with host-owned bindings, output limits, and managed process cleanup. */
import { stripTypeScriptTypes } from 'node:module'
import type { Readable } from 'node:stream'
import { fileURLToPath } from 'node:url'
import { isAbsolute } from 'node:path'
import { StringDecoder } from 'node:string_decoder'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { CodeRuntime, DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from '@deepseek-ai/dsh-code-runtime'
import type { CodeBindingNamespace, CodeJsonValue, CodeRunFailure, CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeBindingNamespace, CodeJsonValue, CodeRunFailure, CodeRunRequest, CodeRunResult, CodeRunSandbox, CodeRunSpec } from '@deepseek-ai/dsh-code-runtime'
import { MAX_TIMER_DELAY_MS, clampTimeout } from '@deepseek-ai/dsh-timeout'
import { SandboxUnavailableError, classifyRunnerFailure, isRunnerSpawnFailure } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedArgv, SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'
import type { SubprocessHandle, SubprocessOutcome } from '@deepseek-ai/dsh-subprocess'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
import type {} from '@deepseek-ai/dsh-fs'
import { snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
import type { ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts'
import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts'
import { decodeWorkerJson, encodeWorkerJson } from './worker-json.ts'
import type { WorkerJsonWire } from './worker-json.ts'
import { validateBindings } from './bindings.ts'
import { JsonChannel } from './channel.ts'
import { bootstrapArgs } from './launch.ts'
import type { LaunchConfig } from './launch.ts'
import { OutputLedger } from './output-ledger.ts'
import { decodeCodeJsonWire, encodeCodeJsonWire } from './json-wire.ts'
import type { ProgramBootData } from './protocol.ts'
/** Plugin config: every execution cap, changeable from `cordis.yml` (no hardcoded tunables). */
export interface Config {
/**
* Busy-time budget in milliseconds: the run fails with kind `'timeout'`
* once the worker's MEASURED event-loop active time
* (`worker.performance.eventLoopUtilization()`) exceeds this. Metering
* measured busy time — not wall time, not host-side pending-call
* bookkeeping — is what makes the budget both fair (a program awaiting a
* slow tool accrues nothing) and ungameable (a hot loop accrues whether
* or not a decoy dispatch is in flight).
*/
computeMs?: number
/**
* Wall-clock ceiling in milliseconds; never pauses for anything. The
* backstop for what busy-time cannot see (a program awaiting a promise
* nobody will resolve). At most `2_147_483_647` (Node's maximum
* `setTimeout` delay, about 24.9 days): a longer value is rejected at load
* because `setTimeout` would clamp it to 1 ms.
*/
maxWallMs?: number
/**
* Hard cap for serialized log-array, completion-value, and failure-message payloads;
* fixed result-envelope syntax is excluded.
*/
/** Deployment-varying runtime bounds and launch choices. */
export interface Config extends LaunchConfig {
/** Default elapsed deadline, including nested tool and approval waits. */
timeoutMs?: number
/** Maximum elapsed deadline accepted by resolve. */
maxTimeoutMs?: number
/** Combined serialized logs, completion and diagnostic byte cap. */
maxOutputBytes?: number
/** The worker's max old-generation heap in MiB (`resourceLimits`); overflow kills the worker, surfacing as kind `'worker-exit'`. */
/** V8 old-generation heap limit in MiB; native allocations are excluded. */
maxOldGenerationSizeMb?: number
/** Maximum control frame, outstanding argument and queued control-output bytes. */
maxMessageBytes?: number
/** Maximum simultaneous host binding calls accepted from a program. */
maxPendingCalls?: number
/** Managed process termination and output-drain grace in milliseconds. */
graceMs?: number
}
/** {@link Config} after schemastery fills the defaults (every field present). */
type ResolvedConfig = Required<Config>
type ResolvedConfig = Required<Omit<Config, 'bootstrapPath'>> & Pick<Config, 'bootstrapPath'>
interface LiveRun { controller: AbortController; finished: Promise<void> }
const STRIP_PREFIX = 'async function __dsh_program__() {\n'
const STRIP_SUFFIX = '\n}'
/**
* How often the host samples the worker's event-loop utilization for the
* `computeMs` budget. An internal cadence, not config: the only effect of
* the interval is budget-expiry granularity (a run can overshoot by up to
* one interval), and nothing a deployment could tune here improves that
* without burning host CPU.
*/
const ELU_POLL_INTERVAL_MS = 25
function messageOf(error: unknown): string { return error instanceof Error ? error.message : String(error) }
function record(value: unknown): value is Record<string, unknown> { return typeof value === 'object' && value !== null && !Array.isArray(value) }
/** Smallest cap that can represent the counted payloads: an empty logs array plus an empty JSON failure message. */
const MIN_OUTPUT_BYTES = 4
/**
* The seam's language-portable identifier subset (see
* `CodeBindingNamespace.global`): no `$`, which is JS-only spelling — the same
* namespace list must be usable against every backend regardless of language.
*/
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/
/**
* The shell a program is wrapped in for the type-strip, matching the
* grammatical context it will execute in (an async function body, where
* top-level `return` and `await` are legal — a bare module parse would
* reject the `return`). Strip mode is position-preserving (removed syntax
* becomes whitespace, nothing shifts), so the wrapper survives the strip
* byte-identical and the body slices back out with the model's own
* line/column positions intact.
*/
const STRIP_WRAP = { prefix: 'async function __dsh_program__() {\n', suffix: '\n}' } as const
/** One in-flight run's host-side state, tracked for disposal. */
interface LiveRun {
worker: Worker
settle(failure: CodeRunFailure): void
finished: Promise<void>
}
/**
* The worker entry path. Source runs unbuilt (`src/worker.ts`, loadable
* directly on this repo's Node range via native type stripping — the file
* is erasable-only with type-only relative imports); the built package
* ships it as a sibling CommonJS bundle (`lib/worker.cjs`, its own tsdown
* entry) because pkg's VFS Worker hook compiles string-path entries as
* CommonJS.
* The URL *pathname*'s extension says which world this module is in —
* pathname, because dev-time module runners (vitest) may suffix
* `import.meta.url` with a query string; relative resolution drops it. Worker
* receives a filesystem string so pkg's VFS Worker hook can resolve it.
*/
/* v8 ignore next -- the './worker.cjs' arm is the built-lib world, unreachable unbuilt by construction; the built-lib e2e pins it. */
const WORKER_PATH = fileURLToPath(new URL(new URL(import.meta.url).pathname.endsWith('.ts') ? './worker.ts' : './worker.cjs', import.meta.url))
/** Render an unknown thrown value as a message, `Error` or not. */
function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
/** Resolve after a worker pipe emits all queued data, or closes/errors during termination. */
function waitForPipeDrain(stream: Readable): Promise<void> {
if (stream.readableEnded || stream.destroyed) return Promise.resolve()
return new Promise((resolve) => {
const done = (): void => {
stream.off('end', done)
stream.off('close', done)
stream.off('error', done)
resolve()
}
stream.once('end', done)
stream.once('close', done)
stream.once('error', done)
// Close the event-registration race if termination finished between the
// initial state check and the listeners above.
/* v8 ignore next -- this race cannot be scheduled deterministically between the adjacent state check and listener registration. */
if (stream.readableEnded || stream.destroyed) done()
})
}
/**
* Runtime shape gate for inbound port traffic. The peer runs MODEL CODE and
* can post anything — `null`, primitives, objects with poisoned fields — so
* the compile-time `WorkerToHost` type means nothing here: everything is
* re-validated and REBUILT field by field (a forged extra field never rides
* along; a non-number call id can never be echoed into a reply). Junk returns
* `undefined` and is dropped — a throw in the host's `message` listener would
* crash the host process.
*/
function parseWorkerMessage(raw: unknown): WorkerToHost | undefined {
if (typeof raw !== 'object' || raw === null) return undefined
const m = raw as Record<string, unknown>
switch (m.type) {
case 'call': {
if (typeof m.id !== 'number' || typeof m.global !== 'string' || typeof m.name !== 'string') return undefined
return { type: 'call', id: m.id, global: m.global, name: m.name, args: m.args as WorkerJsonWire }
}
case 'log': {
if (typeof m.text !== 'string') return undefined
return { type: 'log', text: m.text }
}
case 'output-limit': return { type: 'output-limit' }
case 'done': {
if (m.error === undefined) return { type: 'done', ...m.value !== undefined ? { value: m.value as WorkerJsonWire } : {} }
const error = m.error
if (typeof error !== 'object' || error === null) return undefined
const { kind, message } = error as Record<string, unknown>
if ((kind !== 'exception' && kind !== 'invalid-output' && kind !== 'output-limit') || typeof message !== 'string') return undefined
return { type: 'done', error: { kind, message } }
}
default: return undefined
}
}
/** One run's combined outer-output ledger; binding values never enter it. */
class OutputLedger {
private bytes = 2 // JSON serialization of the empty logs array: []
private entries = 0
constructor(private readonly maxBytes: number) {}
/** Admit one exact log entry, or report that the hard cap was crossed. */
admit(text: string, sink: string[]): boolean {
const separatorBytes = this.entries > 0 ? 1 : 0
const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes)
if (stringBytes === undefined) return false
this.bytes += stringBytes + separatorBytes
this.entries += 1
sink.push(text)
return true
}
/** Finalize a successful absent-or-JSON completion against the combined cap. */
success(logs: string[], value?: CodeJsonValue): CodeRunResult {
if (value !== undefined && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
return { logs, ...value !== undefined ? { value } : {} }
}
/** Finalize a failure diagnostic, with output-limit taking precedence when combined bytes exceed the cap. */
failure(logs: string[], error: CodeRunFailure): CodeRunResult {
if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
return { logs, error }
}
/** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */
limit(logs: string[]): CodeRunResult {
const fullMessage = `outer output exceeded ${this.maxBytes} bytes`
// The fixed diagnostic is ASCII, so every character is one byte plus the quotes.
const messageBytes = fullMessage.length + 2
const retained: string[] = []
let retainedBytes = 2
const logBudget = this.maxBytes - messageBytes
for (const text of logs) {
const separatorBytes = retained.length > 0 ? 1 : 0
const availableBytes = logBudget - retainedBytes - separatorBytes
const stringBytes = jsonStringBytesUpTo(text, availableBytes)
if (stringBytes !== undefined) {
retained.push(text)
retainedBytes += stringBytes + separatorBytes
continue
}
const prefix = truncateJsonStringBytes(text, availableBytes)
if (prefix.length > 0) {
const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes)
/* v8 ignore next -- truncateJsonStringBytes guarantees its returned prefix fits the same budget. */
if (prefixBytes === undefined) throw new Error('output ledger produced an oversized log prefix')
retained.push(prefix)
retainedBytes += prefixBytes + separatorBytes
}
break
}
const availableMessageBytes = this.maxBytes - retainedBytes
const message = truncateJsonStringBytes(fullMessage, availableMessageBytes)
return { logs: retained, error: { kind: 'output-limit', message } }
}
}
/**
* The shipped {@link CodeRuntime} backend (`ctx.codeRuntime`). Registers as
* the `codeRuntime` service; every cap comes from validated config. See the
* module doc for the containment model and the Service Definition's class JSDoc for
* the contract this implements (error-as-field, hostile-peer port,
* no cross-run state, dispose to quiescence).
*/
/** Node provider; direct file effects use the same sandbox service as Bash. */
export class NodeCodeRuntime extends CodeRuntime {
static inject = ['fs', 'subprocess', 'sandbox', 'sandboxPolicy']
static Config: z<Config> = z.object({
computeMs: z.number().default(60_000),
maxWallMs: z.number().default(600_000),
timeoutMs: z.number().default(120_000),
maxTimeoutMs: z.number().default(600_000),
maxOutputBytes: z.number().default(67_108_864),
maxOldGenerationSizeMb: z.number().default(512),
maxMessageBytes: z.number().default(134_217_728),
maxPendingCalls: z.number().default(128),
graceMs: z.number().default(3_000),
nodeExecutable: z.string(),
bootstrapPath: z.string(),
})
readonly language = 'typescript'
readonly isolation = 'worker-thread'
readonly isolation = 'process'
private readonly config: ResolvedConfig
private readonly live = new Set<LiveRun>()
private disposed = false
constructor(ctx: Context, config: Config) {
super(ctx)
// Schemastery filled the defaults; the cast records that. Positivity is a
// semantic check the schema's plain number type does not carry.
this.config = config as ResolvedConfig
this.config = { ...config, nodeExecutable: config.nodeExecutable ?? process.execPath } as ResolvedConfig
for (const [key, value] of Object.entries(this.config)) {
if (!(Number.isFinite(value) && value > 0)) throw new Error(`dsh-code-runtime-node: config.${key} must be a positive number, got ${String(value)}`)
if (typeof value === 'number' && (!Number.isFinite(value) || value <= 0)) throw new Error(`code-runtime-node: ${key} must be positive and finite`)
}
if (!Number.isSafeInteger(this.config.maxOutputBytes) || this.config.maxOutputBytes < MIN_OUTPUT_BYTES) {
throw new Error(`dsh-code-runtime-node: config.maxOutputBytes must be a safe integer of at least ${MIN_OUTPUT_BYTES}, got ${String(this.config.maxOutputBytes)}`)
for (const key of ['timeoutMs', 'maxTimeoutMs', 'graceMs'] as const) {
if (this.config[key] > MAX_TIMER_DELAY_MS) throw new Error(`code-runtime-node: ${key} exceeds the supported timer range`)
}
// maxWallMs reaches setTimeout, which clamps any delay above
// MAX_TIMER_DELAY_MS to 1 ms; the positivity check above accepts such a
// value, so a 25-day ceiling would time the run out immediately.
if (this.config.maxWallMs > MAX_TIMER_DELAY_MS) {
throw new Error(`dsh-code-runtime-node: config.maxWallMs must be at most ${MAX_TIMER_DELAY_MS} (Node clamps a longer setTimeout delay to 1ms), got ${String(this.config.maxWallMs)}`)
if (!Number.isSafeInteger(this.config.maxOutputBytes) || this.config.maxOutputBytes < 4) throw new Error('code-runtime-node: maxOutputBytes must be an integer of at least 4')
if (!Number.isSafeInteger(this.config.maxMessageBytes) || this.config.maxMessageBytes > 0xffff_ffff) throw new Error('code-runtime-node: maxMessageBytes must fit an unsigned 32-bit frame length')
if (!Number.isSafeInteger(this.config.maxPendingCalls)) throw new Error('code-runtime-node: maxPendingCalls must be an integer')
if (!Number.isSafeInteger(this.config.maxOldGenerationSizeMb)) throw new Error('code-runtime-node: maxOldGenerationSizeMb must be an integer')
if (this.config.nodeExecutable.length === 0) throw new Error('code-runtime-node: nodeExecutable must be non-empty')
if (this.config.bootstrapPath !== undefined && !isAbsolute(this.config.bootstrapPath)) throw new Error('code-runtime-node: bootstrapPath must be absolute')
ctx.effect(() => async () => {
this.disposed = true
const active = [...this.live]
for (const run of active) run.controller.abort('runtime disposed')
await Promise.all(active.map(run => run.finished))
}, 'Node code-runtime cleanup')
}
override get sandboxMode(): SandboxMode { return this.ctx.sandboxPolicy.defaultMode }
/**
* Resolve an execution under explicit or deployment policy.
* @param request - Program, bindings, optional cwd/deadline, and resolved authority.
* @returns Complete execution inputs with a capped deadline.
*/
resolve(request: CodeRunRequest): CodeRunSpec {
const sandboxPolicy = request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
const cwd = request.cwd ?? sandboxPolicy.workspaceRoot
if (!isAbsolute(cwd)) throw new Error('code-runtime-node: cwd must be absolute')
return {
...request,
cwd,
timeoutMs: clampTimeout(request.timeoutMs, this.config.timeoutMs, this.config.maxTimeoutMs, 'code-runtime-node: timeoutMs'),
sandboxPolicy,
}
ctx.effect(() => () => this.teardown(), 'worker code-runtime teardown')
}
/**
* Dispose to quiescence: mark the service unusable, fail every in-flight
* run as aborted, and AWAIT each worker's exit so no worker outlives the
* fiber.
* Run a resolved program in a fresh managed and confined Node process.
* @param spec - Inputs returned by resolve; missing authority is caller misuse.
* @returns Output and file-confinement facts after managed cleanup.
*/
private async teardown(): Promise<void> {
this.disposed = true
const runs = [...this.live]
for (const run of runs) run.settle({ kind: 'abort', message: 'runtime disposed' })
await Promise.all(runs.map(run => run.finished))
}
/**
* Execute one program in a fresh worker. Program outcomes — including a
* type-strip syntax error, which never spawns a worker — resolve with
* `result.error`; the method rejects only for Service Definition contract misuse (a disposed
* runtime, an invalid binding namespace).
* @param request - the program, its bindings, and the abort signal.
* @returns the run's outcome per the seam contract.
*/
async run(request: CodeRunRequest): Promise<CodeRunResult> {
if (this.disposed) throw new Error('dsh-code-runtime-node: run() after disposal')
const bindings = this.validateBindings(request)
if (request.signal?.aborted) {
return this.failureBeforeWorker({ kind: 'abort', message: String(request.signal.reason) })
}
let code: string
async run(spec: CodeRunSpec): Promise<CodeRunResult> {
if (this.disposed) throw new Error('code-runtime-node: run after disposal')
if (spec.sandboxPolicy === undefined) throw new Error('code-runtime-node: run requires a resolved sandbox policy')
if (!isAbsolute(spec.cwd) || !Number.isFinite(spec.timeoutMs) || spec.timeoutMs <= 0 || spec.timeoutMs > this.config.maxTimeoutMs) throw new Error('code-runtime-node: run requires resolved cwd and timeout')
const bindings = validateBindings(spec)
const controller = new AbortController()
const completion = Promise.withResolvers<void>()
const live = { controller, finished: completion.promise }
this.live.add(live)
try {
const stripped = stripTypeScriptTypes(STRIP_WRAP.prefix + request.program + STRIP_WRAP.suffix)
code = stripped.slice(STRIP_WRAP.prefix.length, stripped.length - STRIP_WRAP.suffix.length)
} catch (error: unknown) {
// A program that does not survive the type-strip (syntax error,
// non-erasable syntax like `enum`) is a program failure, reported the
// same way a thrown exception would be — and no worker ever spawns.
return this.failureBeforeWorker({ kind: 'exception', message: messageOf(error) })
return await this.execute(spec, spec.sandboxPolicy, bindings, controller)
} finally {
this.live.delete(live)
completion.resolve()
}
return await this.execute(request, code, bindings)
}
/** Apply the outer-output ledger to failures that occur before a worker owns one. */
private failureBeforeWorker(error: CodeRunFailure): CodeRunResult {
return new OutputLedger(this.config.maxOutputBytes).failure([], error)
}
/** Reject malformed binding globals or typed-error declarations as Service Definition contract misuse. */
private validateBindings(request: CodeRunRequest): Map<string, CodeBindingNamespace> {
const bindings = new Map<string, CodeBindingNamespace>()
for (const namespace of request.bindings) {
if (!IDENTIFIER.test(namespace.global) || PORTABLE_RESERVED_WORDS.has(namespace.global)) {
throw new Error(`dsh-code-runtime-node: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`)
}
// RESERVED_BINDING_GLOBALS is the seam's shared backend-owned set:
// `console` is THIS backend's log-capture slot; the dunder entries exist
// for the Python side — its seeded/wrapped slots plus the `__debug__`
// compile-time constant — refused here too so the namespace list stays
// portable across backends. The seam declaration is the single home for
// why each entry is reserved.
if (RESERVED_BINDING_GLOBALS.has(namespace.global)) {
throw new Error(`dsh-code-runtime-node: reserved binding global ${JSON.stringify(namespace.global)}`)
}
if (bindings.has(namespace.global)) {
throw new Error(`dsh-code-runtime-node: duplicate binding global ${JSON.stringify(namespace.global)}`)
}
bindings.set(namespace.global, namespace)
}
const errorClassNames = new Set<string>()
for (const namespace of request.bindings) {
const descriptor = namespace.errorClass
if (!descriptor) continue
if (!IDENTIFIER.test(descriptor.name) || PORTABLE_RESERVED_WORDS.has(descriptor.name)) {
throw new Error(`dsh-code-runtime-node: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`)
}
if (RESERVED_BINDING_GLOBALS.has(descriptor.name)) {
throw new Error(`dsh-code-runtime-node: reserved binding global ${JSON.stringify(descriptor.name)}`)
}
if (bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) {
throw new Error(`dsh-code-runtime-node: duplicate injected global ${JSON.stringify(descriptor.name)}`)
}
const member = descriptor.memberNameProperty
if (member.length === 0 || RESERVED_ERROR_MEMBERS.has(member) || DUNDER_MEMBER.test(member)) {
throw new Error(`dsh-code-runtime-node: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`)
}
errorClassNames.add(descriptor.name)
}
return bindings
}
/** Spawn the worker for one validated, type-stripped run and drive it to settlement. */
private execute(
request: CodeRunRequest,
code: string,
private async execute(
spec: CodeRunSpec,
policy: SandboxExecutionPolicy,
bindings: Map<string, CodeBindingNamespace>,
controller: AbortController,
): Promise<CodeRunResult> {
const bootData: WorkerBootData = {
code,
namespaces: [...bindings].map(([global, namespace]) => ({
global,
names: Object.keys(namespace.functions),
...namespace.errorClass ? { errorClass: namespace.errorClass } : {},
})),
maxOutputBytes: this.config.maxOutputBytes,
}
const worker = new Worker(WORKER_PATH, {
workerData: bootData,
// Model code gets NO ambient environment — stronger than the scrubbed
// env the defensive-patterns rule requires for spawned commands.
env: {},
// Hermetic flags too: without this the worker inherits the host process's execArgv (a
// test runner's or tsx's loader hooks), which a bare isolate with an empty environment
// cannot satisfy.
execArgv: [],
resourceLimits: { maxOldGenerationSizeMb: this.config.maxOldGenerationSizeMb },
// Backstop capture: the bootstrap patches JS-level writes into its own
// ordered buffer, so these pipes normally stay silent; anything that
// still arrives (native-level writes) is appended after the done logs.
stdout: true,
stderr: true,
})
return new Promise<CodeRunResult>((resolve) => {
let settled = false
const answered = new Set<number>()
const logs: string[] = []
const strayLogs: string[] = []
const output = new OutputLedger(this.config.maxOutputBytes)
let terminalOverride: CodeRunResult | undefined
// Pipe and message-port delivery are independent. Continue bounded pipe
// capture after a terminal message while worker termination drains bytes
// that were already queued; `finish` materializes the result only after
// termination completes.
const captureStray = (chunk: Buffer): void => {
/* v8 ignore next -- a second post-overflow chunk races immediate worker termination; the first overflow path is covered. */
if (terminalOverride !== undefined) return
const text = chunk.toString('utf8')
if (!output.admit(text, strayLogs)) {
const limited = output.limit([...logs, ...strayLogs, text])
terminalOverride = limited
finish(limited)
}
}
worker.stdout.on('data', captureStray)
worker.stderr.on('data', captureStray)
// Exactly one outcome wins. Every path cleans up, terminates, and awaits the worker;
// logs captured before timeout, abort, or failure remain in the result.
let finishResolve!: () => void
const finished = new Promise<void>((done) => { finishResolve = done })
const finish = (finalize: CodeRunResult | (() => CodeRunResult)): void => {
if (settled) return
settled = true
clearInterval(eluTimer)
clearTimeout(wallTimer)
request.signal?.removeEventListener('abort', onAbort)
this.live.delete(live)
// Let the poll phase deliver pipe bytes already queued independently
// of the terminal port message before termination closes the streams.
void new Promise<void>((resume) => { setImmediate(resume) }).then(async () => {
const stdoutDrained = waitForPipeDrain(worker.stdout)
const stderrDrained = waitForPipeDrain(worker.stderr)
await Promise.all([worker.terminate(), stdoutDrained, stderrDrained])
const result = terminalOverride ?? (typeof finalize === 'function' ? finalize() : finalize)
finishResolve()
resolve(result)
})
}
const onDone = (message: WorkerToHost): void => {
if (message.type !== 'done') return
if (message.error) {
const error = message.error
finish(() => output.failure([...logs, ...strayLogs], error))
return
}
if (message.value === undefined) {
finish(() => output.success([...logs, ...strayLogs]))
return
}
const value = decodeWorkerJson(message.value)
if (value === undefined) {
finish(() => output.failure([...logs, ...strayLogs], { kind: 'invalid-output', message: 'program completion must be lossless JSON' }))
} else {
finish(() => output.success([...logs, ...strayLogs], value))
}
}
const onCall = (message: WorkerToHost): void => {
if (message.type !== 'call' || settled) return
// Hostile-peer rules: a duplicate id is ignored, an unknown name is
// answered with a failure, and a binding throw/reject becomes the
// program-side rejection — contained here, never a host crash.
if (answered.has(message.id)) return
answered.add(message.id)
const reply = (payload: ReplyMessage): void => {
if (settled) return
// Canonical resolutions were snapshotted as lossless JSON before
// this point, so this payload is structured-cloneable by contract.
worker.postMessage(payload)
}
const record = bindings.get(message.global)?.functions
// Own-property lookup only: a forged name like 'constructor' or
// 'hasOwnProperty' must not walk the record's prototype chain and
// reach a callable the consumer never declared.
const fn = record && Object.hasOwn(record, message.name) ? record[message.name] : undefined
if (typeof fn !== 'function') {
reply({ type: 'reply', id: message.id, ok: false, message: `unknown binding ${JSON.stringify(`${message.global}.${message.name}`)}` })
return
}
const args = decodeWorkerJson(message.args)
if (args === undefined) {
reply({ type: 'reply', id: message.id, ok: false, message: 'binding arguments must be lossless JSON' })
return
}
void (async () => {
const output = new OutputLedger(this.config.maxOutputBytes)
const logs: string[] = []
const sandbox: CodeRunSandbox = { mode: policy.mode, denied: false }
const result = Promise.withResolvers<CodeRunResult>()
const signal = spec.signal === undefined ? controller.signal : AbortSignal.any([spec.signal, controller.signal])
let handle: SubprocessHandle | undefined
let channel: JsonChannel | undefined
let confined: ConfinedArgv | undefined
let settled = false
let timedOut = false
let outputOverflow = false
let stderr = ''
let parsing = true
const wallTimer = setTimeout(() => { timedOut = true; controller.abort('execution deadline reached') }, spec.timeoutMs)
const finish = (failure?: CodeRunFailure, value?: CodeJsonValue): void => {
if (settled) return
settled = true
clearTimeout(wallTimer)
signal.removeEventListener('abort', onAbort)
channel?.close()
void (async () => {
if (handle !== undefined) {
handle.terminate()
try {
const resolved = await fn(args)
let value: CodeJsonValue | undefined
try {
value = snapshotJsonValue(resolved)
} catch {
value = undefined
}
if (value === undefined) {
reply({ type: 'reply', id: message.id, ok: false, message: 'binding resolution must be lossless JSON' })
} else {
reply({ type: 'reply', id: message.id, ok: true, value: encodeWorkerJson(value) })
}
await Promise.all([handle.done.catch(() => {}), handle.waitForExit()])
} catch (error: unknown) {
reply({ type: 'reply', id: message.id, ok: false, message: messageOf(error) })
failure = { kind: 'worker-exit', message: `managed process cleanup failed: ${messageOf(error)}` }
} finally {
handle.stdout?.destroy()
handle.stderr?.destroy()
}
})()
}
const outcome = outputOverflow ? output.limit(logs)
: failure === undefined ? output.success(logs, value) : output.failure(logs, failure)
result.resolve({ ...outcome, sandbox: { ...sandbox } })
})()
}
const onAbort = (): void => {
finish(timedOut
? { kind: 'timeout', message: `execution deadline reached (${spec.timeoutMs}ms)` }
: { kind: 'abort', message: messageOf(signal.reason) })
}
signal.addEventListener('abort', onAbort, { once: true })
if (signal.aborted) onAbort()
try {
if (settled) return await result.promise
const stripped = stripTypeScriptTypes(STRIP_PREFIX + spec.program + STRIP_SUFFIX)
parsing = false
const data: ProgramBootData = {
code: stripped.slice(STRIP_PREFIX.length, stripped.length - STRIP_SUFFIX.length),
namespaces: [...bindings.values()].map(binding => ({
global: binding.global,
names: Object.keys(binding.functions),
...binding.errorClass ? { errorClass: binding.errorClass } : {},
})),
maxOutputBytes: this.config.maxOutputBytes,
}
worker.on('message', (raw: unknown) => {
// Parse before touching: the peer can post ANY shape, and a throw in
// this listener would crash the host process. Junk drops silently.
const message = parseWorkerMessage(raw)
if (!message) return
if (message.type === 'log' && !settled && !output.admit(message.text, logs)) {
const limited = output.limit([...logs, ...strayLogs, message.text])
finish(limited)
const executable = await this.ctx.subprocess.resolveExecutable(this.config.nodeExecutable, undefined, signal)
if (settled) return await result.promise
const argv = [executable, `--max-old-space-size=${this.config.maxOldGenerationSizeMb}`, ...bootstrapArgs(this.ctx.fs, this.config, this.config.maxMessageBytes)]
confined = policy.mode === 'danger-full-access' ? undefined : this.ctx.sandbox.confine(argv, { ...policy, mode: policy.mode })
if (confined !== undefined) sandbox.enforcement = confined.enforcement
const env: NodeJS.ProcessEnv = Object.fromEntries(Object.keys(process.env).map(key => [key, undefined]))
if ('pkg' in process && this.config.bootstrapPath === undefined) env.DSH_CODE_RUNTIME_NODE = '1'
handle = this.ctx.subprocess.spawn({ argv: confined?.argv ?? argv, cwd: spec.cwd, env, stdio: { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe', control: 'pipe' }, graceMs: this.config.graceMs, signal })
const launched = handle
if (launched.control === undefined) throw new Error('subprocess provider did not supply the requested control channel')
const admit = (text: string): void => {
if (outputOverflow || text.length === 0) return
if (!output.admit(text, logs)) {
outputOverflow = true
finish({ kind: 'output-limit', message: `outer output exceeded ${this.config.maxOutputBytes} bytes` })
}
}
const stdoutDecoder = new StringDecoder('utf8')
const stderrDecoder = new StringDecoder('utf8')
handle.stdout?.on('data', (chunk: Buffer) => { admit(stdoutDecoder.write(chunk)) })
handle.stdout?.on('end', () => { admit(stdoutDecoder.end()) })
handle.stderr?.on('data', (chunk: Buffer) => {
const text = stderrDecoder.write(chunk)
stderr = (stderr + text).slice(-this.config.maxOutputBytes)
admit(text)
})
handle.stderr?.on('end', () => { admit(stderrDecoder.end()) })
let ready = false
let nextId = 1
let pending = 0
let pendingBytes = 0
const protocolFailure = (message: string): void => { finish({ kind: 'protocol', message }) }
const processFinished = (outcome: SubprocessOutcome): void => {
if (settled) return
finish((confined !== undefined && classifyRunnerFailure(outcome.exitCode, stderr, confined.runnerFailureRules) !== undefined)
? { kind: 'sandbox-unavailable', message: stderr }
: { kind: 'worker-exit', message: `Node process exited before completing (${String(outcome.exitCode)})${stderr ? `: ${stderr}` : ''}` })
}
const transport: JsonChannel = new JsonChannel(launched.control, this.config.maxMessageBytes, (raw, bytes) => {
if (settled) return
if (!record(raw)) { protocolFailure('invalid control frame'); return }
if (!ready) {
if (raw.type !== 'ready') { protocolFailure('program frame arrived before bootstrap readiness'); return }
ready = true
void transport.send({ type: 'boot', data }).catch((error) => { protocolFailure(messageOf(error)) })
return
}
if (message.type === 'output-limit' && !settled) {
const limited = output.limit([...logs, ...strayLogs])
finish(limited)
return
switch (raw.type) {
case 'log':
if (typeof raw.text !== 'string') { protocolFailure('invalid log frame'); return }
admit(raw.text)
return
case 'output-limit': outputOverflow = true; finish(); return
case 'done': {
if (raw.error !== undefined) {
if (!record(raw.error) || typeof raw.error.message !== 'string' || (raw.error.kind !== 'exception' && raw.error.kind !== 'invalid-output' && raw.error.kind !== 'output-limit')) { protocolFailure('invalid terminal error'); return }
const failure = { kind: raw.error.kind, message: raw.error.message } as CodeRunFailure
if (confined !== undefined) {
sandbox.denied = confined.denialSignatures.some(signature =>
failure.message.toLowerCase().includes(signature.toLowerCase()))
}
if (failure.kind === 'output-limit') outputOverflow = true
finish(failure)
return
}
const value = raw.value === undefined ? undefined : decodeCodeJsonWire(raw.value)
if (raw.value !== undefined && value === undefined) finish({ kind: 'invalid-output', message: 'program completion must be lossless JSON' })
else finish(undefined, value)
return
}
case 'call': {
if (!Number.isSafeInteger(raw.id) || raw.id !== nextId || typeof raw.global !== 'string' || typeof raw.name !== 'string') { protocolFailure('invalid binding call identity'); return }
nextId += 1
const functions = bindings.get(raw.global)?.functions
const fn = functions !== undefined && Object.hasOwn(functions, raw.name) ? functions[raw.name] : undefined
if (typeof fn !== 'function') { protocolFailure('program requested an undeclared binding'); return }
const args = decodeCodeJsonWire(raw.args)
if (args === undefined) { protocolFailure('binding arguments must be lossless JSON'); return }
if (++pending > this.config.maxPendingCalls || (pendingBytes += bytes) > this.config.maxMessageBytes) { protocolFailure('pending binding calls exceed configured limits'); return }
const id = raw.id as number
void (async () => {
let reply: unknown
try {
const value = snapshotJsonValue(await fn(args))
if (value === undefined) throw new Error('binding resolution must be lossless JSON')
reply = { type: 'reply', id, ok: true, value: encodeCodeJsonWire(value) }
} catch (error: unknown) {
reply = { type: 'reply', id, ok: false, message: messageOf(error) }
} finally {
pending -= 1
pendingBytes -= bytes
}
if (!settled) await transport.send(reply)
})().catch((error) => { protocolFailure(messageOf(error)) })
return
}
default: protocolFailure('unknown control message')
}
onCall(message)
onDone(message)
}, (error, kind) => {
if (kind === 'protocol') protocolFailure(messageOf(error))
else if (ready) finish({ kind: 'worker-exit', message: messageOf(error) })
else void launched.done.then(processFinished, (failure) => { finish({ kind: 'worker-exit', message: messageOf(failure) }) })
})
worker.on('error', (error: Error) => {
finish(() => output.failure([...logs, ...strayLogs], { kind: 'worker-exit', message: `worker error: ${error.message}` }))
})
worker.on('exit', (exitCode: number) => {
finish(() => output.failure([...logs, ...strayLogs], { kind: 'worker-exit', message: `worker exited with code ${exitCode} before completing` }))
})
// The compute budget reads the worker's own measured busy time, so a
// hot loop expires it no matter what dispatches are in flight, while a
// program idling on a slow binding accrues nothing.
const eluTimer = setInterval(() => {
const elu = worker.performance.eventLoopUtilization()
if (elu.active > this.config.computeMs) {
finish(() => output.failure([...logs, ...strayLogs], { kind: 'timeout', message: `compute budget exhausted (${this.config.computeMs}ms busy)` }))
}
}, ELU_POLL_INTERVAL_MS)
const wallTimer = setTimeout(() => {
finish(() => output.failure([...logs, ...strayLogs], { kind: 'timeout', message: `wall-clock ceiling reached (${this.config.maxWallMs}ms)` }))
}, this.config.maxWallMs)
const onAbort = (): void => {
finish(() => output.failure([...logs, ...strayLogs], { kind: 'abort', message: String(request.signal?.reason) }))
}
request.signal?.addEventListener('abort', onAbort, { once: true })
const live: LiveRun = {
worker,
finished,
settle: (failure: CodeRunFailure) => { finish(() => output.failure([...logs, ...strayLogs], failure)) },
}
this.live.add(live)
})
channel = transport
void launched.done.then((outcome) => { setImmediate(() => { processFinished(outcome) }) }, (error) => { finish({ kind: confined !== undefined && isRunnerSpawnFailure(error, confined.argv[0], spec.cwd) ? 'sandbox-unavailable' : 'worker-exit', message: messageOf(error) }) })
} catch (error: unknown) {
finish({ kind: error instanceof SandboxUnavailableError ? 'sandbox-unavailable' : parsing ? 'exception' : 'worker-exit', message: messageOf(error) })
}
return await result.promise
}
}
@@ -1,6 +1,6 @@
/**
* Lossless-JSON snapshots for the dependency-free source worker closure.
* @module @deepseek-ai/dsh-code-runtime-node/worker-json
* @module @deepseek-ai/dsh-code-runtime-node/json-wire
*/
import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime'
@@ -249,15 +249,15 @@ type WorkerJsonToken = null | boolean | number | string | ArrayWireToken | Objec
* markers and scalar leaves share one flat token array, so `worker_threads`
* never has to structured-clone the value's application nesting.
*/
export type WorkerJsonWire = WorkerJsonToken[]
export type CodeJsonWire = WorkerJsonToken[]
/**
* Flatten one validated JSON value for the worker-thread message port.
* @param value - the lossless JSON value to transport.
* @returns a pre-order token stream whose own nesting is bounded.
*/
export function encodeWorkerJson(value: CodeJsonValue): WorkerJsonWire {
const wire: WorkerJsonWire = []
export function encodeCodeJsonWire(value: CodeJsonValue): CodeJsonWire {
const wire: CodeJsonWire = []
const pending: CodeJsonValue[] = [value]
for (let current = takeLast(pending); current !== undefined; current = takeLast(pending)) {
if (current === null || typeof current === 'boolean' || typeof current === 'number' || typeof current === 'string') {
@@ -346,7 +346,7 @@ function containerToken(value: object): ArrayWireToken | ObjectWireToken | undef
* @param input - untrusted message-port payload.
* @returns the detached JSON value, or `undefined` when the wire is invalid.
*/
export function decodeWorkerJson(input: unknown): CodeJsonValue | undefined {
export function decodeCodeJsonWire(input: unknown): CodeJsonValue | undefined {
try {
if (!intrinsicArrayIsArray(input) || !isDenseArray(input) || input.length === 0) return undefined
const wire = input as unknown[]
@@ -0,0 +1,37 @@
/** Select source or built bootstrap assets in the mounted execution world. */
import { dirname, resolve } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import type { FileSystem } from '@deepseek-ai/dsh-fs'
/** Deployment-owned Node executable and optional preinstalled built bootstrap. */
export interface LaunchConfig {
/** Executable in the subprocess world; defaults to the current Node executable. */
nodeExecutable?: string
/** Absolute preinstalled built bootstrap in the execution world. */
bootstrapPath?: string
}
/**
* Select explicit arguments without inheriting host loader or inspector flags.
* @param fs - Filesystem mapping host bootstrap assets into the process world.
* @param config - Optional preinstalled built bootstrap.
* @param maxMessageBytes - Validated frame and queued-write limit.
* @returns Arguments following the resolved Node executable.
*/
export function bootstrapArgs(fs: FileSystem, config: LaunchConfig, maxMessageBytes: number): string[] {
if (config.bootstrapPath !== undefined) return [config.bootstrapPath, String(maxMessageBytes)]
if ('pkg' in process) return [String(maxMessageBytes)]
const mapped = (path: string): string => {
const result = fs.processPathFromHostPath(path)
if (result === undefined) throw new Error(`code runtime bootstrap is unavailable in the subprocess execution world: ${path}`)
return result
}
if (!new URL(import.meta.url).pathname.endsWith('.ts')) {
return [mapped(fileURLToPath(new URL('./process.js', import.meta.url))), String(maxMessageBytes)]
}
const entry = mapped(fileURLToPath(new URL('./process.ts', import.meta.url)))
const subprocess = dirname(fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-subprocess/package.json')))
const helper = mapped(resolve(subprocess, 'src/control.ts'))
const source = `const {openInheritedControlChannel}=await import(${JSON.stringify(pathToFileURL(helper).href)});const {runNodeMain}=await import(${JSON.stringify(pathToFileURL(entry).href)});await runNodeMain(openInheritedControlChannel(),${maxMessageBytes},process);`
return ['--input-type=module', '--eval', source]
}
@@ -0,0 +1,66 @@
/** Combined byte accounting for retained program output. */
import type { CodeJsonValue, CodeRunFailure, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts'
/** One run's combined outer-output ledger; binding values never enter it. */
export class OutputLedger {
private bytes = 2 // JSON serialization of the empty logs array: []
private entries = 0
constructor(private readonly maxBytes: number) {}
/** Admit one exact log entry, or report that the hard cap was crossed. */
admit(text: string, sink: string[]): boolean {
const separatorBytes = this.entries > 0 ? 1 : 0
const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes)
if (stringBytes === undefined) return false
this.bytes += stringBytes + separatorBytes
this.entries += 1
sink.push(text)
return true
}
/** Finalize a successful absent-or-JSON completion against the combined cap. */
success(logs: string[], value?: CodeJsonValue): CodeRunResult {
if (value !== undefined && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
return { logs, ...value !== undefined ? { value } : {} }
}
/** Finalize a failure diagnostic, with output-limit taking precedence when combined bytes exceed the cap. */
failure(logs: string[], error: CodeRunFailure): CodeRunResult {
if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
return { logs, error }
}
/** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */
limit(logs: string[]): CodeRunResult {
const fullMessage = `outer output exceeded ${this.maxBytes} bytes`
// The fixed diagnostic is ASCII, so every character is one byte plus the quotes.
const messageBytes = fullMessage.length + 2
const retained: string[] = []
let retainedBytes = 2
const logBudget = this.maxBytes - messageBytes
for (const text of logs) {
const separatorBytes = retained.length > 0 ? 1 : 0
const availableBytes = logBudget - retainedBytes - separatorBytes
const stringBytes = jsonStringBytesUpTo(text, availableBytes)
if (stringBytes !== undefined) {
retained.push(text)
retainedBytes += stringBytes + separatorBytes
continue
}
const prefix = truncateJsonStringBytes(text, availableBytes)
if (prefix.length > 0) {
const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes)
/* v8 ignore next -- truncateJsonStringBytes guarantees its returned prefix fits the same budget. */
if (prefixBytes === undefined) throw new Error('output ledger produced an oversized log prefix')
retained.push(prefix)
retainedBytes += prefixBytes + separatorBytes
}
break
}
const availableMessageBytes = this.maxBytes - retainedBytes
const message = truncateJsonStringBytes(fullMessage, availableMessageBytes)
return { logs: retained, error: { kind: 'output-limit', message } }
}
}
@@ -0,0 +1,5 @@
/** Private built Node-runtime child entry; source execution calls runNodeMain directly. */
import { openInheritedControlChannel } from '@deepseek-ai/dsh-subprocess/control'
import { runNodeMain } from './process.ts'
await runNodeMain(openInheritedControlChannel(), Number(process.argv[2]), process)
@@ -0,0 +1,69 @@
/** Node child execution over an inherited control channel; no Harness services run here. */
import type { Duplex } from 'node:stream'
import { JsonChannel } from './channel.ts'
import { runProgram } from './bootstrap.ts'
import type { PatchableStream } from './bootstrap.ts'
import type { ReplyMessage, ProgramBootData, ProgramToHost } from './protocol.ts'
/** Process-owned environment and output streams consumed by the child bootstrap. */
export interface ProgramProcess {
env: NodeJS.ProcessEnv
stdout: PatchableStream
stderr: PatchableStream
exitCode: string | number | null | undefined
}
/**
* Run one host-supplied program after the control handshake.
* @param stream - Inherited, already-adopted control endpoint.
* @param maxMessageBytes - Host-validated maximum frame and queued-write bytes.
* @param processState - Environment, output streams and exit status of this Node child.
* @returns After the program has settled and its control output has flushed.
*/
export async function runNodeMain(stream: Duplex, maxMessageBytes: number, processState: ProgramProcess): Promise<void> {
if (!Number.isSafeInteger(maxMessageBytes) || maxMessageBytes <= 0 || maxMessageBytes > 0xffff_ffff) throw new Error('invalid control message limit')
for (const key of Object.keys(processState.env)) Reflect.deleteProperty(processState.env, key)
const boot = Promise.withResolvers<ProgramBootData>()
const listeners: Array<(message: ReplyMessage) => void> = []
let started = false
let failed = false
const channel = new JsonChannel(stream, maxMessageBytes, (raw) => {
if (!started) {
started = true
if (typeof raw !== 'object' || raw === null || (raw as { type?: unknown }).type !== 'boot') {
boot.reject(new Error('expected program boot frame'))
return
}
boot.resolve((raw as { data: ProgramBootData }).data)
return
}
for (const listener of listeners) listener(raw as ReplyMessage)
}, (error) => {
failed = true
boot.reject(error)
channel.close()
processState.exitCode = 1
})
const pending = new Set<Promise<void>>()
const send = (message: ProgramToHost): void => {
const task = channel.send(message).catch(() => {
failed = true
channel.close()
processState.exitCode = 1
}).finally(() => { pending.delete(task) })
pending.add(task)
}
try {
await channel.send({ type: 'ready' })
const data = await boot.promise
await runProgram({
postMessage: send,
on: (_event, listener) => { listeners.push(listener) },
}, data, { stdout: processState.stdout, stderr: processState.stderr })
while (pending.size > 0) await Promise.all(pending)
await channel.drain()
} finally {
channel.close()
if (failed) processState.exitCode = 1
}
}
@@ -1,14 +1,13 @@
/**
* Versionless, structured-clone wire protocol between co-shipped host and worker code. The host
* treats inbound traffic as hostile because model code can forge `parentPort` messages; the
* worker trusts host replies.
* JSON message vocabulary carried by the bounded process channel. The host validates every
* message because model code can write directly to the inherited descriptor.
* @module @deepseek-ai/dsh-code-runtime-node/src/protocol
*/
import type { WorkerJsonWire } from './worker-json.ts'
import type { CodeJsonWire } from './json-wire.ts'
/** What the host hands the worker at spawn, via `workerData`. */
export interface WorkerBootData {
/** What the host hands the program at spawn, after the readiness handshake. */
export interface ProgramBootData {
/** The type-stripped (plain JS) program body. */
code: string
/** Binding namespaces to materialize; functions themselves stay host-side. */
@@ -21,32 +20,32 @@ export interface WorkerBootData {
maxOutputBytes: number
}
/** Worker → host: one bridged binding call. */
/** Program → host: one bridged binding call. */
interface CallMessage {
type: 'call'
/** Worker-issued correlation id; the host answers each id at most once and ignores duplicates. */
/** Program-issued correlation id; the host answers each id at most once and ignores duplicates. */
id: number
/** The namespace global the call targets. */
global: string
/** The function name within the namespace. */
name: string
/** The single argument as a flat lossless-JSON wire value. */
args: WorkerJsonWire
args: CodeJsonWire
}
/** Worker → host: captured text, streamed eagerly so output survives a mid-run termination (timeout, abort, OOM). */
/** Program → host: captured text, streamed eagerly so output survives a mid-run termination (timeout, abort, OOM). */
interface LogMessage {
type: 'log'
text: string
}
/** Worker → host: worker-side capture or completion measurement exceeded the outer cap. */
/** Program → host: program-side capture or completion measurement exceeded the outer cap. */
interface OutputLimitMessage {
type: 'output-limit'
}
/**
* Worker → host: the program settled. `error` carries a program exception,
* Program → host: the program settled. `error` carries a program exception,
* invalid completion, or output overflow (budgets, aborts, and substrate death
* are observed host-side). `value` is present only on a clean completion that
* produced one, as a flat wire value already lossless and admitted against
@@ -55,14 +54,14 @@ interface OutputLimitMessage {
*/
export interface DoneMessage {
type: 'done'
value?: WorkerJsonWire
value?: CodeJsonWire
error?: { kind: 'exception' | 'invalid-output' | 'output-limit'; message: string }
}
/** Every message the worker sends. */
export type WorkerToHost = CallMessage | LogMessage | OutputLimitMessage | DoneMessage
/** Every message the program sends. */
export type ProgramToHost = CallMessage | LogMessage | OutputLimitMessage | DoneMessage
/** Host → worker: the answer to one {@link CallMessage}. */
/** Host → program: the answer to one {@link CallMessage}. */
export type ReplyMessage =
| { type: 'reply'; id: number; ok: true; value: WorkerJsonWire }
| { type: 'reply'; id: number; ok: true; value: CodeJsonWire }
| { type: 'reply'; id: number; ok: false; message: string }
@@ -1,14 +0,0 @@
/**
* Spawn-only worker entrypoint over {@link runWorkerMain}. Executable logic stays in
* `bootstrap.ts` for in-process coverage; real-worker tests cover this glue.
* @module @deepseek-ai/dsh-code-runtime-node/src/worker
*/
import { parentPort, workerData } from 'node:worker_threads'
import { runWorkerMain } from './bootstrap.ts'
import type { WorkerBootData } from './protocol.ts'
// A worker always has a parent port; guard loudly rather than run detached.
if (!parentPort) throw new Error('dsh-code-runtime-node: worker entry loaded outside a worker thread')
void runWorkerMain(parentPort, workerData as WorkerBootData, { stdout: process.stdout, stderr: process.stderr })
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import { validateBindings } from '../src/bindings.ts'
describe('program binding validation', () => {
it.each(['$name', '', 'while', 'lambda', 'console'])('rejects unusable global %s', (global) => {
expect(() => validateBindings({ program: '', bindings: [{ global, functions: {} }] })).toThrow()
})
it('rejects duplicated globals and accepts arbitrary own member names', () => {
const functions = Object.create(null) as Record<string, () => Promise<string>>
functions.__proto__ = async () => 'own'
expect(validateBindings({ program: '', bindings: [{ global: 'tools', functions }] }).get('tools')?.functions).toBe(functions)
expect(() => validateBindings({ program: '', bindings: [{ global: 'tools', functions }, { global: 'tools', functions }] })).toThrow('duplicate')
})
it.each(['$name', 'while', 'console', 'tools'])('rejects invalid error class name %s', (name) => {
expect(() => validateBindings({ program: '', bindings: [{ global: 'tools', functions: {}, errorClass: { name, memberNameProperty: 'member' } }] })).toThrow()
})
it.each(['', 'name', '__member__'])('rejects invalid error member %s', (memberNameProperty) => {
expect(() => validateBindings({ program: '', bindings: [{ global: 'tools', functions: {}, errorClass: { name: 'ToolError', memberNameProperty } }] })).toThrow()
})
it('rejects two injected constructors with the same name', () => {
expect(() => validateBindings({ program: '', bindings: ['first', 'second'].map(global => ({ global, functions: {}, errorClass: { name: 'ToolError', memberNameProperty: 'member' } })) })).toThrow('duplicate')
})
})
@@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest'
import { EventEmitter } from 'node:events'
import { LogBuffer, makeBindingErrorClasses, makeConsoleShim, makeNamespaces, captureStreamWrites, prepareCompletion, prepareException, runWorkerMain, wireReplies } from '../src/bootstrap.ts'
import { LogBuffer, makeBindingErrorClasses, makeConsoleShim, makeNamespaces, captureStreamWrites, prepareCompletion, prepareException, runProgram, wireReplies } from '../src/bootstrap.ts'
import type { BootstrapPort, PatchableStream, PendingCall } from '../src/bootstrap.ts'
import type { ReplyMessage, WorkerToHost } from '../src/protocol.ts'
import { decodeWorkerJson, encodeWorkerJson } from '../src/worker-json.ts'
import type { ReplyMessage, ProgramToHost } from '../src/protocol.ts'
import { decodeCodeJsonWire, encodeCodeJsonWire } from '../src/json-wire.ts'
/**
* An in-process stand-in for the worker's parentPort: the test plays the
@@ -12,12 +12,12 @@ import { decodeWorkerJson, encodeWorkerJson } from '../src/worker-json.ts'
* isolate (real-worker behavior is pinned by runtime.spec.ts).
*/
class FakePort implements BootstrapPort {
sent: WorkerToHost[] = []
sent: ProgramToHost[] = []
private readonly emitter = new EventEmitter()
/** Host-scripted responder; return undefined to leave the call pending. */
respond: (message: WorkerToHost) => ReplyMessage | undefined = () => undefined
respond: (message: ProgramToHost) => ReplyMessage | undefined = () => undefined
postMessage(message: WorkerToHost): void {
postMessage(message: ProgramToHost): void {
this.sent.push(message)
const reply = this.respond(message)
if (reply) queueMicrotask(() => this.emitter.emit('message', reply))
@@ -35,13 +35,13 @@ class FakePort implements BootstrapPort {
return this.sent.filter(message => message.type === 'log').map(message => message.text)
}
done(): WorkerToHost | undefined {
done(): ProgramToHost | undefined {
return this.sent.find(message => message.type === 'done')
}
doneValue(): unknown {
const done = this.done()
return done?.type === 'done' && done.value !== undefined ? decodeWorkerJson(done.value) : undefined
return done?.type === 'done' && done.value !== undefined ? decodeCodeJsonWire(done.value) : undefined
}
}
@@ -140,7 +140,7 @@ describe('captureStreamWrites', () => {
describe('prepareCompletion', () => {
it('omits undefined and passes lossless JSON values exactly', () => {
expect(prepareCompletion(undefined, 100)).toEqual({})
expect(prepareCompletion({ a: [1, 'two'] }, 100)).toEqual({ value: encodeWorkerJson({ a: [1, 'two'] }) })
expect(prepareCompletion({ a: [1, 'two'] }, 100)).toEqual({ value: encodeCodeJsonWire({ a: [1, 'two'] }) })
})
it('turns every lossy completion shape into invalid-output', () => {
@@ -162,7 +162,7 @@ describe('prepareCompletion', () => {
})
it('measures the exact JSON serialization at and over the boundary', () => {
expect(prepareCompletion('€', 5)).toEqual({ value: encodeWorkerJson('€') })
expect(prepareCompletion('€', 5)).toEqual({ value: encodeCodeJsonWire('€') })
expect(prepareCompletion('€', 4)).toEqual({
error: { kind: 'output-limit', message: 'outer output exceeded 4 bytes' },
})
@@ -216,7 +216,7 @@ describe('makeNamespaces', () => {
it('exposes prototype-colliding names as ordinary own properties', async () => {
const port = new FakePort()
port.respond = message => message.type === 'call'
? { type: 'reply', id: message.id, ok: true, value: encodeWorkerJson(`${message.name}-ok`) }
? { type: 'reply', id: message.id, ok: true, value: encodeCodeJsonWire(`${message.name}-ok`) }
: undefined
const pending = new Map<number, PendingCall>()
wireReplies(port, pending)
@@ -311,15 +311,15 @@ describe('makeNamespaces', () => {
})
})
describe('runWorkerMain', () => {
describe('runProgram', () => {
it('runs a program end-to-end: bindings, console, return value', async () => {
const port = new FakePort()
port.respond = (message) => {
if (message.type !== 'call') return undefined
const args = decodeWorkerJson(message.args) as { n: number }
return { type: 'reply', id: message.id, ok: true, value: encodeWorkerJson(args.n * 2) }
const args = decodeCodeJsonWire(message.args) as { n: number }
return { type: 'reply', id: message.id, ok: true, value: encodeCodeJsonWire(args.n * 2) }
}
await runWorkerMain(port, {
await runProgram(port, {
...BOOT,
code: 'const doubled = await tools.double({ n: 21 }); console.log("got", doubled); return { doubled };',
namespaces: [{ global: 'tools', names: ['double'] }],
@@ -330,7 +330,7 @@ describe('runWorkerMain', () => {
it('reports worker-side log capture overflow before completing', async () => {
const port = new FakePort()
await runWorkerMain(port, {
await runProgram(port, {
maxOutputBytes: 4,
code: 'console.log("12345"); return null',
namespaces: [],
@@ -345,7 +345,7 @@ describe('runWorkerMain', () => {
it('reports a thrown program error on the done message', async () => {
const port = new FakePort()
await runWorkerMain(port, { ...BOOT, code: 'throw new Error("boom")', namespaces: [] }, fakeStreams())
await runProgram(port, { ...BOOT, code: 'throw new Error("boom")', namespaces: [] }, fakeStreams())
const done = port.done()
expect(done?.type).toBe('done')
expect(done?.type === 'done' ? done.error?.kind : undefined).toBe('exception')
@@ -355,17 +355,17 @@ describe('runWorkerMain', () => {
it('renders non-Error throws and stack-less Errors on the done message', async () => {
const rawPort = new FakePort()
await runWorkerMain(rawPort, { ...BOOT, code: 'throw "raw-throw"', namespaces: [] }, fakeStreams())
await runProgram(rawPort, { ...BOOT, code: 'throw "raw-throw"', namespaces: [] }, fakeStreams())
expect(rawPort.done()).toEqual({ type: 'done', error: { kind: 'exception', message: 'raw-throw' } })
const barePort = new FakePort()
await runWorkerMain(barePort, { ...BOOT, code: 'const e = new Error("bare"); e.stack = undefined; throw e', namespaces: [] }, fakeStreams())
await runProgram(barePort, { ...BOOT, code: 'const e = new Error("bare"); e.stack = undefined; throw e', namespaces: [] }, fakeStreams())
expect(barePort.done()).toEqual({ type: 'done', error: { kind: 'exception', message: 'bare' } })
})
it('replaces giant thrown strings and Error stacks before posting the done message', async () => {
const rawPort = new FakePort()
await runWorkerMain(rawPort, {
await runProgram(rawPort, {
maxOutputBytes: 64,
code: 'throw "x".repeat(1_000_000)',
namespaces: [],
@@ -376,7 +376,7 @@ describe('runWorkerMain', () => {
})
const stackPort = new FakePort()
await runWorkerMain(stackPort, {
await runProgram(stackPort, {
maxOutputBytes: 64,
code: 'throw new Error("x".repeat(1_000_000))',
namespaces: [],
@@ -390,7 +390,7 @@ describe('runWorkerMain', () => {
it('surfaces a host failure reply as a program-side rejection it can catch', async () => {
const port = new FakePort()
port.respond = message => message.type === 'call' ? { type: 'reply', id: message.id, ok: false, message: 'denied by host' } : undefined
await runWorkerMain(port, {
await runProgram(port, {
...BOOT,
code: 'try { await tools.x({}) } catch (error) { return { caught: error instanceof ToolCallError, name: error.name, toolName: error.toolName, message: error.message } }',
namespaces: [toolNamespace(['x'])],
@@ -403,7 +403,7 @@ describe('runWorkerMain', () => {
port.respond = message => message.type === 'call'
? { type: 'reply', id: message.id, ok: false, message: 'helper denied' }
: undefined
await runWorkerMain(port, {
await runProgram(port, {
...BOOT,
code: 'try { await helpers.x({}) } catch (error) { return { caught: error instanceof HelperCallError, name: error.name, helperName: error.helperName, message: error.message } }',
namespaces: [{
@@ -420,10 +420,10 @@ describe('runWorkerMain', () => {
port.respond = (message) => {
if (message.type !== 'call') return undefined
// Deliver a stray reply first; the real one follows.
port.deliver({ type: 'reply', id: 9_999, ok: true, value: encodeWorkerJson('stray') })
return { type: 'reply', id: message.id, ok: true, value: encodeWorkerJson('real') }
port.deliver({ type: 'reply', id: 9_999, ok: true, value: encodeCodeJsonWire('stray') })
return { type: 'reply', id: message.id, ok: true, value: encodeCodeJsonWire('real') }
}
await runWorkerMain(port, {
await runProgram(port, {
...BOOT,
code: 'return await tools.x({})',
namespaces: [{ global: 'tools', names: ['x'] }],
@@ -434,7 +434,7 @@ describe('runWorkerMain', () => {
it('captures raw stream writes through the patched process streams', async () => {
const port = new FakePort()
const streams = fakeStreams()
await runWorkerMain(port, { ...BOOT, code: 'return 1', namespaces: [] }, streams)
await runProgram(port, { ...BOOT, code: 'return 1', namespaces: [] }, streams)
streams.stdout.write('never seen — already restored? no: patch persists in worker')
// The patch stays installed for the worker's lifetime; writes during the
// program landed in order. Here the program wrote nothing via streams, so
@@ -1,95 +0,0 @@
/** Host budget decisions use controlled clocks and ELU samples; worker execution and binding transport stay real. */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { NodeCodeRuntime } from '@deepseek-ai/dsh-code-runtime-node'
import type { CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
const meter = vi.hoisted(() => ({ sample: vi.fn() }))
vi.mock('node:worker_threads', async (importOriginal) => {
const original = await importOriginal<typeof import('node:worker_threads')>()
return {
...original,
Worker: class extends original.Worker {
constructor(...args: ConstructorParameters<typeof original.Worker>) {
super(...args)
this.performance.eventLoopUtilization = meter.sample
}
},
}
})
describe('worker budgets with controlled ELU samples and real binding transport', () => {
let ctx: Context
let controller: AbortController
let run: Promise<CodeRunResult> | undefined
let release: (() => void) | undefined
beforeEach(() => {
ctx = new Context()
controller = new AbortController()
run = undefined
release = undefined
meter.sample.mockReset().mockReturnValue({ active: 10, idle: 0, utilization: 1 })
// Worker bootstrap and scheduling contribute to ELU active time; only the
// measured input and host deadlines are controlled, not worker execution.
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'] })
})
afterEach(async () => {
const owned = { ctx, controller, run, release }
try {
owned.controller.abort('test cleanup')
owned.release?.()
} finally {
vi.useRealTimers()
}
try {
await owned.run
} finally {
await owned.ctx.fiber.dispose()
}
})
async function pendingBinding(): Promise<void> {
await ctx.plugin(NodeCodeRuntime, { computeMs: 1_000, maxWallMs: 30_000 })
let entered!: () => void
const ready = new Promise<void>((resolve) => { entered = resolve })
const binding = new Promise<string>((resolve) => { release = () => { resolve('slow-done') } })
run = ctx.codeRuntime.run({
program: 'return await tools.slow({})',
bindings: [{ global: 'tools', functions: { slow: () => { entered(); return binding } } }],
signal: controller.signal,
})
await Promise.race([
ready,
run.then((result) => { throw new Error('Worker settled before binding entry: ' + JSON.stringify(result)) }),
])
}
it('does not charge a binding wait longer than the compute budget', async () => {
await pendingBinding()
const settled = vi.fn()
void run!.then(settled, settled)
meter.sample.mockReturnValue({ active: 10, idle: 1_500, utilization: 10 / 1_510 })
await vi.advanceTimersByTimeAsync(1_500)
expect(meter.sample).toHaveBeenCalled()
expect(settled).not.toHaveBeenCalled()
release!()
expect(await run).toEqual({ logs: [], value: 'slow-done' })
})
it('expires active time even while a binding is pending', async () => {
await pendingBinding()
meter.sample.mockReturnValue({ active: 1_001, idle: 1_500, utilization: 1_001 / 2_501 })
await vi.advanceTimersByTimeAsync(25)
expect(await run).toEqual({ logs: [], error: { kind: 'timeout', message: 'compute budget exhausted (1000ms busy)' } })
})
it('expires the wall ceiling while active time remains below the compute budget', async () => {
await pendingBinding()
meter.sample.mockReturnValue({ active: 10, idle: 30_000, utilization: 10 / 30_010 })
await vi.advanceTimersByTimeAsync(30_000)
expect(await run).toEqual({ logs: [], error: { kind: 'timeout', message: 'wall-clock ceiling reached (30000ms)' } })
})
})
@@ -6,23 +6,29 @@ import { describe, expect, it } from 'vitest'
/**
* Keyless built-artifact smoke: plain Node imports the package by name through its exports map,
* then exercises type stripping, sibling `worker.cjs` loading, bindings, and logs. Unit tests use
* `src/worker.ts`; this pins the downstream `lib/index.js` path. It skips when `lib/` is absent,
* then exercises type stripping, sibling `process.js` loading, bindings, and logs. Unit tests use
* `src/process.ts`; this pins the downstream `lib/index.js` path. It skips when `lib/` is absent,
* and CI runs it after the build.
*/
const pkgDir = fileURLToPath(new URL('..', import.meta.url))
const built = ['lib/index.js', 'lib/worker.cjs'].every(file => existsSync(join(pkgDir, file)))
const built = ['lib/index.js', 'lib/process.js'].every(file => existsSync(join(pkgDir, file)))
&& existsSync(join(pkgDir, '../code-runtime/lib/index.js'))
describe.skipIf(!built)('built lib real load path (plain node)', () => {
it('runs a TypeScript program with a binding through lib/index.js and its lib/worker.cjs entry', async () => {
it('runs a TypeScript program with a binding through lib/index.js and its lib/process.js entry', async () => {
const script = `
const { Context } = await import('@deepseek-ai/cordis')
const { NodeCodeRuntime } = await import('@deepseek-ai/dsh-code-runtime-node')
const ctx = new Context()
for (const name of ['session-projection', 'fs-local', 'subprocess-local', 'sandbox-local']) {
const plugin = await import('@deepseek-ai/dsh-' + name)
await ctx.plugin(plugin.default, {})
}
const { default: SandboxPolicy } = await import('@deepseek-ai/dsh-sandbox-policy')
await ctx.plugin(SandboxPolicy, { mode: 'read-only' })
await ctx.plugin(NodeCodeRuntime, {})
const result = await ctx.codeRuntime.run({
const result = await ctx.codeRuntime.run(ctx.codeRuntime.resolve({
program: 'const doubled: number = await tools.double({ n: 21 }); console.log("halfway", doubled); let failure; try { await tools.fail({}) } catch (error) { failure = { typed: error instanceof ToolCallError, name: error.name, toolName: error.toolName, message: error.message } } return { doubled, failure };',
bindings: [{
global: 'tools',
@@ -32,9 +38,9 @@ describe.skipIf(!built)('built lib real load path (plain node)', () => {
},
errorClass: { name: 'ToolCallError', memberNameProperty: 'toolName' },
}],
})
}))
await ctx.fiber.dispose()
console.log(JSON.stringify(result))
process.exit(0)
`
const { exitCode, stdout, stderr } = await execa(process.execPath, ['--input-type=module', '-e', script], {
cwd: pkgDir,
@@ -0,0 +1,156 @@
import { Duplex, PassThrough } from 'node:stream'
import { describe, expect, it, onTestFinished } from 'vitest'
import { JsonChannel } from '../src/channel.ts'
function pair() {
const left = new PassThrough()
const right = new PassThrough()
const a = Duplex.from({ readable: left, writable: right })
const b = Duplex.from({ readable: right, writable: left })
// Duplex.from forwards peer destruction as ABORT_ERR; channel owners observe their own failures.
a.on('error', () => {})
b.on('error', () => {})
onTestFinished(() => { a.destroy(); b.destroy() })
return { a, b }
}
function frame(value: unknown): Buffer {
const body = Buffer.from(JSON.stringify(value))
const header = Buffer.alloc(4)
header.writeUInt32BE(body.length)
return Buffer.concat([header, body])
}
describe('bounded process frames', () => {
it('reassembles split binary headers and UTF-8 bodies', async () => {
const { a, b } = pair()
const received = Promise.withResolvers<unknown>()
const channel = new JsonChannel(a, 1000, value => received.resolve(value), error => received.reject(error))
onTestFinished(() => channel.close())
const bytes = frame({ text: '你好🙂' })
for (const byte of bytes) b.write(Buffer.from([byte]))
expect(await received.promise).toEqual({ text: '你好🙂' })
})
it('accepts consecutive frames and writes complete responses', async () => {
const { a, b } = pair()
const received: unknown[] = []
const done = Promise.withResolvers<undefined>()
const channel = new JsonChannel(a, 1000, (value) => {
received.push(value)
if (received.length === 2) done.resolve(undefined)
}, error => done.reject(error))
const peer = new JsonChannel(b, 1000, () => {}, error => done.reject(error))
onTestFinished(() => { channel.close(); peer.close() })
await Promise.all([peer.send({ n: 1 }), peer.send({ n: 2 })])
await done.promise
expect(received).toEqual([{ n: 1 }, { n: 2 }])
await peer.drain()
})
it.each([0, 65])('rejects a declared %i-byte frame before accepting its body', async (length) => {
const { a, b } = pair()
const failure = Promise.withResolvers<Error>()
const channel = new JsonChannel(a, 64, () => { throw new Error('must not dispatch') }, error => failure.resolve(error))
onTestFinished(() => channel.close())
const header = Buffer.alloc(4)
header.writeUInt32BE(length)
b.write(header)
expect((await failure.promise).message).toContain('control frame')
})
it('contains malformed JSON, invalid UTF-8 and receiver failures', async () => {
for (const payload of [Buffer.from('{'), Buffer.from([0xff]), Buffer.from('{}')]) {
const { a, b } = pair()
const failure = Promise.withResolvers<Error>()
const channel = new JsonChannel(a, 64, () => { throw new Error('receiver failed') }, error => failure.resolve(error))
const header = Buffer.alloc(4)
header.writeUInt32BE(payload.length)
b.write(Buffer.concat([header, payload]))
expect(await failure.promise).toBeInstanceOf(Error)
channel.close()
}
})
it('rejects oversized writes and closed channels', async () => {
const { a } = pair()
const channel = new JsonChannel(a, 20, () => {}, () => {})
await expect(channel.send({ text: 'x'.repeat(30) })).rejects.toThrow('queued bytes')
channel.close()
channel.close()
await expect(channel.send({})).rejects.toThrow('closed')
})
it('reports peer EOF while a program is active', async () => {
const { a, b } = pair()
const failure = Promise.withResolvers<Error>()
const channel = new JsonChannel(a, 64, () => {}, error => failure.resolve(error))
onTestFinished(() => channel.close())
b.end()
expect((await failure.promise).message).toContain('ended')
})
})
it('retains a partial header and partial payload across distinct stream deliveries', () => {
const { a } = pair()
const values: unknown[] = []
const channel = new JsonChannel(a, 64, (value) => { values.push(value) }, (error) => { throw error })
const bytes = frame({ value: 1 })
a.emit('data', bytes.subarray(0, 2))
a.emit('data', bytes.subarray(2, 6))
a.emit('data', bytes.subarray(6))
expect(values).toEqual([{ value: 1 }])
channel.close()
})
it('contains a receiver throwing a non-Error value', () => {
const { a } = pair()
const failures: string[] = []
const channel = new JsonChannel(a, 64, () => { throw 'receiver failure' }, (error) => { failures.push(error.message) })
a.emit('data', frame({}))
expect(failures).toEqual(['receiver failure'])
channel.close()
})
it('ignores callbacks already captured by an emission when an earlier listener closes the channel', () => {
for (const event of ['data', 'end']) {
const { a } = pair()
let failures = 0
const channel = new JsonChannel(a, 64, () => { throw new Error('closed channel dispatched') }, () => { failures += 1 })
a.prependOnceListener(event, () => channel.close())
a.emit(event, frame({}))
a.emit('error', new Error('late closed stream error'))
expect(failures).toBe(0)
}
})
it('rejects queued writes if the owner closes before they start', async () => {
const { a } = pair()
const channel = new JsonChannel(a, 64, () => {}, () => {})
const pending = channel.send({ n: 1 })
channel.close()
await expect(pending).rejects.toThrow('closed')
await channel.drain()
})
it('bounds queued frames while a receiver is not reading', async () => {
const stream = new Duplex({ read() {}, write(_chunk, _encoding, _callback) {} })
const channel = new JsonChannel(stream, 32, () => {}, () => {})
const pending = channel.send({ value: '1234567890' })
await expect(channel.send({ value: '1234567890' })).rejects.toThrow('queued bytes')
channel.close()
await expect(pending).rejects.toThrow('closed')
})
it.each(['error', 'close'])('settles a blocked write when the stream emits %s', async (event) => {
const entered = Promise.withResolvers<undefined>()
const stream = new Duplex({ read() {}, write(_chunk, _encoding, _callback) { entered.resolve(undefined) } })
let failure: Error | undefined
const channel = new JsonChannel(stream, 64, () => {}, (error) => { failure = error })
const pending = channel.send({ value: 1 })
await entered.promise
stream.emit(event, new Error('transport failed'))
await expect(pending).rejects.toThrow(event === 'error' ? 'transport failed' : 'closed')
if (event === 'error') expect(failure?.message).toBe('transport failed')
channel.close()
})
@@ -1,7 +1,7 @@
import { runInNewContext } from 'node:vm'
import { describe, expect, it } from 'vitest'
import { snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
import { decodeWorkerJson, encodeWorkerJson, snapshotCodeJsonValue } from '../src/worker-json.ts'
import { decodeCodeJsonWire, encodeCodeJsonWire, snapshotCodeJsonValue } from '../src/json-wire.ts'
describe('snapshotCodeJsonValue', () => {
it('matches the canonical scalar boundary', () => {
@@ -176,9 +176,9 @@ describe('flat worker JSON wire', () => {
for (const value of values) {
const snapshot = snapshotCodeJsonValue(value)
expect(snapshot).not.toBeUndefined()
expect(decodeWorkerJson(encodeWorkerJson(snapshot!))).toEqual(snapshot)
expect(decodeCodeJsonWire(encodeCodeJsonWire(snapshot!))).toEqual(snapshot)
}
const decoded = decodeWorkerJson(encodeWorkerJson(snapshotCodeJsonValue(withPrototypeKey)!)) as Record<string, unknown>
const decoded = decodeCodeJsonWire(encodeCodeJsonWire(snapshotCodeJsonValue(withPrototypeKey)!)) as Record<string, unknown>
expect(Object.hasOwn(decoded, '__proto__')).toBe(true)
expect(decoded.__proto__).toEqual({ safe: true })
})
@@ -187,10 +187,10 @@ describe('flat worker JSON wire', () => {
let value: unknown = 'leaf'
for (let depth = 0; depth < 5_000; depth++) value = [value]
const snapshot = snapshotCodeJsonValue(value)!
const wire = encodeWorkerJson(snapshot)
const wire = encodeCodeJsonWire(snapshot)
expect(wire).toHaveLength(5_001)
let cursor = decodeWorkerJson(wire)
let cursor = decodeCodeJsonWire(wire)
for (let depth = 0; depth < 5_000; depth++) {
expect(Array.isArray(cursor)).toBe(true)
cursor = Array.isArray(cursor) ? cursor[0] : undefined
@@ -246,12 +246,12 @@ describe('flat worker JSON wire', () => {
[{ kind: 'object', keys: ['x'] }],
[{ kind: 'object', keys: [], extra: true }],
]) {
expect(decodeWorkerJson(value)).toBeUndefined()
expect(decodeCodeJsonWire(value)).toBeUndefined()
}
})
it('rejects invalid values passed through a forged static type', () => {
expect(() => encodeWorkerJson([undefined] as never)).toThrow(/sparse JSON array/)
expect(() => encodeWorkerJson({ value: undefined } as never)).toThrow(/undefined JSON object property/)
expect(() => encodeCodeJsonWire([undefined] as never)).toThrow(/sparse JSON array/)
expect(() => encodeCodeJsonWire({ value: undefined } as never)).toThrow(/undefined JSON object property/)
})
})
@@ -0,0 +1,66 @@
import { Duplex, PassThrough } from 'node:stream'
import { expect, it, onTestFinished } from 'vitest'
import { JsonChannel } from '../src/channel.ts'
import { runNodeMain } from '../src/process.ts'
import type { ProgramProcess } from '../src/process.ts'
import { decodeCodeJsonWire, encodeCodeJsonWire } from '../src/json-wire.ts'
function endpoints() {
const first = new PassThrough()
const second = new PassThrough()
const child = Duplex.from({ readable: first, writable: second })
const host = Duplex.from({ readable: second, writable: first })
child.on('error', () => {})
host.on('error', () => {})
onTestFinished(() => { child.destroy(); host.destroy() })
return { child, host }
}
function processState(): ProgramProcess { return { env: { FIXTURE_SECRET: 'test' }, stdout: { write: () => true }, stderr: { write: () => true }, exitCode: undefined } }
it('clears process environment, dispatches a binding reply and flushes the terminal frame', async () => {
const { child, host } = endpoints()
const state = processState()
const messages: Record<string, unknown>[] = []
const peer = new JsonChannel(host, 4096, (raw) => {
const message = raw as Record<string, unknown>
messages.push(message)
if (message.type === 'ready') void peer.send({ type: 'boot', data: { code: 'console.log("ready"); return await tools.echo({});', namespaces: [{ global: 'tools', names: ['echo'] }], maxOutputBytes: 1024 } })
if (message.type === 'call') void peer.send({ type: 'reply', id: message.id, ok: true, value: encodeCodeJsonWire(42) })
}, () => {})
onTestFinished(() => peer.close())
await runNodeMain(child, 4096, state)
expect(state.env).toEqual({})
expect(state.exitCode).toBeUndefined()
expect(decodeCodeJsonWire(messages.find(message => message.type === 'done')?.value)).toBe(42)
})
it.each([0, -1, 1.5, 4294967296])('rejects an invalid bootstrap frame limit %i', async (limit) => {
const { child } = endpoints()
await expect(runNodeMain(child, limit, processState())).rejects.toThrow('invalid control message limit')
})
it('rejects an unexpected first control frame', async () => {
const { child, host } = endpoints()
const peer = new JsonChannel(host, 4096, () => { void peer.send({ type: 'reply' }) }, () => {})
onTestFinished(() => peer.close())
await expect(runNodeMain(child, 4096, processState())).rejects.toThrow('expected program boot')
})
it('records an I/O failure before the boot frame', async () => {
const { child, host } = endpoints()
const state = processState()
const peer = new JsonChannel(host, 4096, () => { peer.close() }, () => {})
await expect(runNodeMain(child, 4096, state)).rejects.toBeInstanceOf(Error)
expect(state.exitCode).toBe(1)
})
it('contains program writes that exceed queued control output', async () => {
const { child, host } = endpoints()
const state = processState()
const peer = new JsonChannel(host, 1024, (raw) => {
if ((raw as { type: string }).type === 'ready') void peer.send({ type: 'boot', data: { code: 'for(let i=0;i<30;i++) console.log("x".repeat(100));', namespaces: [], maxOutputBytes: 8000 } })
}, () => {})
onTestFinished(() => peer.close())
await runNodeMain(child, 1024, state)
expect(state.exitCode).toBe(1)
})
@@ -0,0 +1,47 @@
import { spawn } from 'node:child_process'
import { copyFile, mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import type { Duplex } from 'node:stream'
import { expect, it, onTestFinished } from 'vitest'
import { JsonChannel } from '../src/channel.ts'
import { decodeCodeJsonWire, encodeCodeJsonWire } from '../src/json-wire.ts'
it('boots an unbuilt source closure outside the workspace and exchanges tool replies', async () => {
const directory = await mkdtemp(join(tmpdir(), 'dsh-node-source-'))
onTestFinished(async () => { await rm(directory, { recursive: true, force: true }) })
for (const file of ['process.ts', 'bootstrap.ts', 'channel.ts', 'json-wire.ts', 'output-json.ts', 'protocol.ts']) {
await copyFile(new URL(`../src/${file}`, import.meta.url), join(directory, file))
}
const source = `import {Socket} from 'node:net';import {runNodeMain} from ${JSON.stringify(pathToFileURL(join(directory, 'process.ts')).href)};await runNodeMain(new Socket({fd:7,readable:true,writable:true}),100000,process);`
const child = spawn(process.execPath, ['--input-type=module', '--eval', source], {
env: { PLACEHOLDER_SECRET: 'fixture-only' },
stdio: ['ignore', 'pipe', 'pipe', 'ignore', 'ignore', 'ignore', 'ignore', 'pipe'],
})
let stderr = ''
child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8') })
const finished = new Promise<void>((resolve) => { child.once('close', () => { resolve() }) })
const completed = Promise.withResolvers<unknown>()
child.once('error', error => completed.reject(error))
child.once('exit', (code) => { if (code !== 0) completed.reject(new Error(`child exit ${code}: ${stderr}`)) })
const channel = new JsonChannel(child.stdio[7] as Duplex, 100_000, (raw) => {
const message = raw as { type: string; id?: number; args?: unknown; value?: unknown; error?: unknown }
if (message.type === 'ready') {
void channel.send({ type: 'boot', data: {
code: 'const answer = await tools.echo({ n: 21 }); return { answer, env: { ...process.env } }',
namespaces: [{ global: 'tools', names: ['echo'] }],
maxOutputBytes: 10_000,
} }).catch(error => completed.reject(error))
} else if (message.type === 'call') {
expect(decodeCodeJsonWire(message.args)).toEqual({ n: 21 })
void channel.send({ type: 'reply', id: message.id, ok: true, value: encodeCodeJsonWire(42) }).catch(error => completed.reject(error))
} else if (message.type === 'done') {
if (message.error !== undefined) completed.reject(new Error(JSON.stringify(message.error)))
else completed.resolve(decodeCodeJsonWire(message.value))
}
}, error => completed.reject(error))
onTestFinished(async () => { channel.close(); child.kill(); await finished })
expect(await completed.promise).toEqual({ answer: 42, env: {} })
await finished
})
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,19 @@
import { Context } from '@deepseek-ai/cordis'
import FileSystem from '@deepseek-ai/dsh-fs-local'
import Subprocess from '@deepseek-ai/dsh-subprocess-local'
import Sandbox from '@deepseek-ai/dsh-sandbox-local'
import SandboxPolicy from '@deepseek-ai/dsh-sandbox-policy'
import SessionProjections from '@deepseek-ai/dsh-session-projection'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
import NodeRuntime from '../src/index.ts'
import type { Config } from '../src/index.ts'
export async function mountRuntime(ctx: Context, config: Config = {}, policy: { mode?: SandboxMode; workspaceRoot?: string } = {}) {
if (!ctx.get('fs')) await ctx.plugin(FileSystem)
if (!ctx.get('subprocess')) await ctx.plugin(Subprocess)
if (!ctx.get('sandbox')) await ctx.plugin(Sandbox, {})
if (!ctx.get('sessionProjections')) await ctx.plugin(SessionProjections)
if (!ctx.get('sandboxPolicy')) await ctx.plugin(SandboxPolicy, { mode: 'danger-full-access', ...policy })
await ctx.plugin(NodeRuntime, config)
return ctx.codeRuntime as NodeRuntime
}
@@ -1,39 +0,0 @@
import { copyFile, mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { Worker } from 'node:worker_threads'
import { expect, it } from 'vitest'
import { decodeWorkerJson } from '../src/worker-json.ts'
/**
* Prove the unbuilt worker is a self-contained source closure. Copying it out
* of the workspace makes any package runtime import fail even when local
* `lib/` artifacts happen to exist.
*/
it('boots the source worker without workspace package outputs', async () => {
const directory = await mkdtemp(join(tmpdir(), 'dsh-code-source-worker-'))
let worker: Worker | undefined
try {
const files = ['worker.ts', 'bootstrap.ts', 'protocol.ts', 'worker-json.ts', 'output-json.ts']
await Promise.all(files.map(async (file) => {
await copyFile(new URL(`../src/${file}`, import.meta.url), join(directory, file))
}))
worker = new Worker(join(directory, 'worker.ts'), {
workerData: { code: 'return { answer: 42 }', namespaces: [], maxOutputBytes: 65_536 },
env: {},
execArgv: [],
})
const message = await new Promise<unknown>((resolve, reject) => {
worker?.once('message', resolve)
worker?.once('error', reject)
})
expect(message).toMatchObject({ type: 'done' })
const value = typeof message === 'object' && message !== null ? (message as { value?: unknown }).value : undefined
expect(decodeWorkerJson(value)).toEqual({ answer: 42 })
} finally {
if (worker) await worker.terminate()
await rm(directory, { recursive: true, force: true })
}
})
@@ -25,6 +25,18 @@
},
{
"path": "../../util/timeout"
},
{
"path": "../../fs/fs"
},
{
"path": "../../subprocess/subprocess"
},
{
"path": "../../sandbox/sandbox"
},
{
"path": "../../sandbox/sandbox-policy"
}
]
}
@@ -1,29 +1,7 @@
import { defineConfig } from 'tsdown'
/**
* Build the index and worker as separate single-entry bundles. The sibling `worker.cjs` is loaded
* by file and must be CommonJS for pkg's VFS Worker hook. A multi-entry build emits an unlisted
* shared chunk omitted by the package's exact `files` whitelist; separate builds inline it.
*/
/** Separate entry bundles keep the private process bootstrap self-contained. */
export default defineConfig([
{
entry: ['lib/types/index.js'],
outDir: 'lib',
format: ['esm'],
platform: 'node',
target: 'es2024',
fixedExtension: false,
dts: false,
clean: false,
},
{
entry: ['lib/types/worker.js'],
outDir: 'lib',
format: ['cjs'],
platform: 'node',
target: 'es2024',
fixedExtension: false,
dts: false,
clean: false,
},
{ entry: ['lib/types/index.js'], outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false },
{ entry: { process: 'lib/types/process-entry.js' }, outDir: 'lib', format: ['esm'], platform: 'node', target: 'es2024', fixedExtension: false, dts: false, clean: false, noExternal: ['@deepseek-ai/dsh-subprocess/control'] },
])
@@ -27,9 +27,11 @@
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:^"
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^"
}
}
@@ -5,7 +5,8 @@
*/
import { Context, Service } from '@deepseek-ai/cordis'
import type { CodeRunRequest, CodeRunResult } from './types.ts'
import type { CodeRunRequest, CodeRunResult, CodeRunSpec } from './types.ts'
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
export type {
CodeBindingErrorClass,
@@ -15,6 +16,8 @@ export type {
CodeRunFailure,
CodeRunRequest,
CodeRunResult,
CodeRunSpec,
CodeRunSandbox,
} from './types.ts'
/**
@@ -118,20 +121,27 @@ export abstract class CodeRuntime extends Service {
*/
abstract readonly isolation: string
/** Deployment file-policy mode, or undefined for a provider without confinement support. */
get sandboxMode(): SandboxMode | undefined { return undefined }
constructor(ctx: Context) {
super(ctx, 'codeRuntime')
}
/**
* Execute one program against the request's bindings and capture what it
* emitted. See the class doc for the resolution contract (error is a result
* field; rejection means Service Definition contract misuse only).
* @param request - the program, its bindings, and the abort signal; the
* request carries everything the runtime acts on, with no hidden defaults.
* @returns the run's outcome: completion value (when transferable), the
* ordered log capture, and the failure (if any).
* Resolve supported options and provider defaults before execution.
* @param request - Program, bindings, cancellation and optional execution choices.
* @returns Complete directory, deadline and supported authority for run.
* @throws When an explicit choice is invalid or unsupported by this provider.
*/
abstract run(request: CodeRunRequest): Promise<CodeRunResult>
abstract resolve(request: CodeRunRequest): CodeRunSpec
/**
* Execute resolved inputs; program outcomes resolve as result fields.
* @param spec - directory, deadline, program, bindings, cancellation and supported policy.
* @returns Captured output and the execution outcome.
*/
abstract run(spec: CodeRunSpec): Promise<CodeRunResult>
}
export default CodeRuntime
@@ -6,6 +6,8 @@
* @module @deepseek-ai/dsh-code-runtime/src/types
*/
import type { SandboxEnforcement, SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox'
/**
* One host-side function exposed to the program as an async callable. The
* runtime bridges calls to it (possibly across a serialization boundary), so
@@ -65,10 +67,8 @@ export interface CodeBindingNamespace {
}
/**
* One run: the program source plus everything the runtime acts on. Per the
* explicit-over-implicit convention, defaulting (time budgets, output caps)
* is the implementation's validated config — a request carries no optional
* tuning knobs for a hidden `??` to fill in.
* Caller inputs for one program. The provider's resolve method validates supported
* options and supplies directory, deadline, and authority before execution.
*/
export interface CodeRunRequest {
/**
@@ -80,6 +80,12 @@ export interface CodeRunRequest {
program: string
/** Host functions exposed to the program, one global object per namespace. */
bindings: CodeBindingNamespace[]
/** Working directory in the mounted filesystem and subprocess execution world. */
cwd?: string
/** Requested elapsed execution time; the provider's resolver validates and caps it. */
timeoutMs?: number
/** Resolved authority for this execution. Providers without confinement reject an explicit policy. */
sandboxPolicy?: SandboxExecutionPolicy
/**
* Abort the run: the runtime stops the program (hard, even mid-loop) and
* resolves with a {@link CodeRunFailure} of kind `'abort'`. In-flight
@@ -88,6 +94,24 @@ export interface CodeRunRequest {
signal?: AbortSignal
}
/** Fully resolved execution inputs; run never supplies a missing directory or timeout. */
export interface CodeRunSpec extends CodeRunRequest {
/** Absolute directory in the provider's execution world. */
cwd: string
/** Positive finite execution deadline in milliseconds, after provider capping. */
timeoutMs: number
}
/** File confinement applied to a program, independently of its terminal outcome. */
export interface CodeRunSandbox {
/** File-effect mode used for this execution. */
mode: SandboxMode
/** Whether an observed failure matches the selected backend's denial diagnostics. */
denied: boolean
/** Completeness reported by the selected confining backend; absent for full access. */
enforcement?: SandboxEnforcement
}
/**
* Why a run failed. The kinds are orthogonal outcomes reported independently
* (per docs/defensive-patterns.md): a budget expiry is not an exception, an
@@ -102,7 +126,7 @@ export interface CodeRunRequest {
*/
export interface CodeRunFailure {
/** The failure class (see the interface doc for each kind's meaning). */
kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'
kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit' | 'protocol' | 'sandbox-unavailable'
/** Human-readable detail, suitable for feeding back to a model to self-correct. */
message: string
}
@@ -113,6 +137,8 @@ export interface CodeRunFailure {
* an exception path.
*/
export interface CodeRunResult {
/** Applied file policy and observed denial, when the provider enforces file policy. */
sandbox?: CodeRunSandbox
/**
* The program's completion value (its top-level `return`), when it ran to
* completion and the value crossed the runtime's lossless-JSON boundary.
@@ -10,6 +10,8 @@ import type { CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtim
* the smallest subclass that honors it.
*/
class StubRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language = 'typescript'
readonly isolation = 'in-process-stub'
requests: CodeRunRequest[] = []
@@ -43,10 +45,10 @@ describe('CodeRuntime service seam', () => {
expect(runtime.isolation).toBe('in-process-stub')
const calls: unknown[] = []
const result = await runtime.run({
const result = await runtime.run(runtime.resolve({
program: 'return 1',
bindings: [{ global: 'tools', functions: { probe: async (args) => { calls.push(args); return null } } }],
})
}))
expect(result).toEqual({ logs: [] })
expect(calls).toEqual([{ from: 'stub' }])
expect(runtime.requests).toHaveLength(1)
@@ -58,7 +60,7 @@ describe('CodeRuntime service seam', () => {
logs: ['boom'],
error: { kind: 'exception', message: 'boom' },
}
const result = await runtime.run({ program: 'throw new Error("boom")', bindings: [] })
const result = await runtime.run(runtime.resolve({ program: 'throw new Error("boom")', bindings: [] }))
expect(result.error).toEqual({ kind: 'exception', message: 'boom' })
expect(result.value).toBeUndefined()
})
@@ -67,7 +69,7 @@ describe('CodeRuntime service seam', () => {
const { runtime } = await setup()
const controller = new AbortController()
controller.abort('cancelled')
const result = await runtime.run({ program: 'return 1', bindings: [], signal: controller.signal })
const result = await runtime.run(runtime.resolve({ program: 'return 1', bindings: [], signal: controller.signal }))
expect(result.error).toEqual({ kind: 'abort', message: 'cancelled' })
})
@@ -13,6 +13,9 @@
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../sandbox/sandbox"
}
]
}
@@ -699,6 +699,8 @@ describe('tool-call scheduler: failure quiescence', () => {
describe('PTC mode native-tool denial through the agent loop', () => {
/** A minimal in-process code runtime for test purposes — never actually runs. */
class FakeCodeRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language = 'typescript'
readonly isolation = 'fake' as const
async run(_request: CodeRunRequest): Promise<CodeRunResult> {
@@ -19,6 +19,8 @@ import { apply, Config, inject, name } from '@deepseek-ai/dsh-agent-tool-present
/** A runtime that never runs anything: presentation never dispatches. */
class StubRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language = 'typescript'
readonly isolation = 'stub'
+6 -2
View File
@@ -49,7 +49,9 @@
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^"
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^"
},
"dependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
@@ -66,6 +68,8 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-projection": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-user-approval": "workspace:^"
"@deepseek-ai/dsh-user-approval": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^"
}
}
+6
View File
@@ -15,6 +15,7 @@ import type { UserMessage } from '@deepseek-ai/dsh-session'
import { assertNever, deepFreeze, snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values'
import type { PromptSection, ToolProviderResult } from '@deepseek-ai/dsh-system-prompt'
import type { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
// Type-only: makes `ctx.get('approval')` resolve to the ApprovalService
// augmentation. The seam stays optional at runtime — see `serviceAsk`.
import type {} from '@deepseek-ai/dsh-user-approval'
@@ -915,6 +916,11 @@ export class ToolRuntime extends Service {
private requireCodeTransport(): ToolDefinition {
this.ptcTransport ??= createRunCodeTool(this, {
requireRuntime: () => this.requireCodeRuntime(this.defaultMode),
resolveSandboxPolicy: (exec) => {
const policy = this.ctx.get('sandboxPolicy')
if (policy === undefined) throw new Error('dsh-tools: confined code runtime requires sandboxPolicy')
return policy.resolve({ session: exec.agent?.session })
},
// The language-aware description/parameters getters read the runtime
// without demanding one, so a native-default process can still project
// the transport for an agent that chose code.
+24 -5
View File
@@ -9,7 +9,8 @@
import { brandString } from '@deepseek-ai/dsh-brand'
import { createUserMessage, HarnessError } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, ToolCallId } from '@deepseek-ai/dsh-llm'
import type { CodeBindingFunction, CodeRunResult, CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { CodeBindingFunction, CodeRunResult, CodeRunSandbox, CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox'
import { snapshotJsonValue, type JsonValue } from '@deepseek-ai/dsh-util-values'
import { defineTool, parameterSchemaSpecToJsonSchema } from './schema.ts'
import { TOOL_RUNTIME_SCHEDULER } from './index.ts'
@@ -256,7 +257,7 @@ function renderValue(value: JsonValue): string {
}
/** Canonical value returned by the outer PTC mode transport. */
type RunCodeOutput = { logs: string[]; result?: JsonValue }
type RunCodeOutput = { logs: string[]; result?: JsonValue; sandbox?: CodeRunSandbox }
/**
* Registry-private capabilities the bridge receives at construction — the
@@ -264,6 +265,8 @@ type RunCodeOutput = { logs: string[]; result?: JsonValue }
* off its public service API and flow here as closures instead.
*/
export interface RunCodeBridgeOptions {
/** Resolves standing Session authority only for a runtime that enforces file policy. */
resolveSandboxPolicy: (exec: ToolRunContext) => SandboxExecutionPolicy
/** Resolves `ctx.codeRuntime` or throws the loud misconfiguration error (shared with the registry's assembly-time checks). */
requireRuntime: () => CodeRuntime
/**
@@ -316,11 +319,22 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO
properties: {
logs: { type: 'array', required: true, items: { type: 'string' } },
result: { type: 'json' },
sandbox: {
type: 'object',
additionalProperties: false,
properties: {
mode: { type: 'string', required: true, enum: ['read-only', 'workspace-write', 'danger-full-access'] },
denied: { type: 'boolean', required: true },
enforcement: { type: 'string', enum: ['full', 'partial'] },
},
},
},
},
render: (_args, value) => {
const rendered = value.result === undefined ? '' : renderValue(value.result)
const parts = [value.logs.join('\n'), rendered].filter(part => part.length > 0)
if (value.sandbox?.enforcement === 'partial') parts.push('File sandbox enforcement is partial on this host.')
if (value.sandbox?.denied) parts.push(`The ${value.sandbox.mode} file sandbox denied an operation.`)
return [{ type: 'text', text: parts.length > 0 ? parts.join('\n') : '(run_code completed with no output)' }]
},
},
@@ -616,7 +630,7 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO
try {
let result: CodeRunResult
try {
result = await runtime.run({
result = await runtime.run(runtime.resolve({
program: args.code,
bindings: [{
global: 'tools',
@@ -624,7 +638,9 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO
errorClass: { name: 'ToolCallError', memberNameProperty: 'toolName' },
}],
signal: runController.signal,
})
...exec.agent?.session.header.cwd !== undefined ? { cwd: exec.agent.session.header.cwd } : {},
...runtime.sandboxMode !== undefined ? { sandboxPolicy: options.resolveSandboxPolicy(exec) } : {},
}))
} finally {
// Abort sub-dispatches and drain every in-flight dispatch before
// closing the turn (queued-unstarted ones are abandoned unlogged).
@@ -635,10 +651,13 @@ export function createRunCodeTool(registry: ToolRuntime, options: RunCodeBridgeO
if (result.error) {
const logsText = result.logs.length > 0 ? `\nCaptured output:\n${result.logs.join('\n')}` : ''
throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}`)
const sandboxText = result.sandbox === undefined ? ''
: `\nFile sandbox: ${result.sandbox.mode}${result.sandbox.enforcement === undefined ? '' : `; enforcement: ${result.sandbox.enforcement}`}${result.sandbox.denied ? '; operation denied' : ''}.`
throw new CodeRunFailedError(`code run failed (${result.error.kind}): ${result.error.message}${logsText}${sandboxText}`)
}
return {
logs: result.logs,
...result.sandbox === undefined ? {} : { sandbox: result.sandbox },
...result.value !== undefined ? { result: result.value } : {},
}
} finally {
+3 -1
View File
@@ -25,6 +25,8 @@ const testToolSignal = new AbortController().signal
/** A scriptable in-repo CodeRuntime: each test sets `behavior` to drive the bindings however it needs. */
class FakeRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language: string
readonly isolation = 'fake'
behavior: (request: CodeRunRequest) => Promise<CodeRunResult> = () => Promise.resolve({ logs: [] })
@@ -62,7 +64,7 @@ async function setup(options: SetupOptions = {}) {
/** Mint an agent scope configured like production that can register scoped tool policy. */
async function mintAgentScope(ctx: Context, name = 'scoped'): Promise<{ scope: Scope; agent: Agent }> {
const agent = { id: SessionId(name) } as Agent
const agent = { id: SessionId(name), session: { header: {}, append: () => {} } } as unknown as Agent
let scope!: Scope
await ctx.plugin(Object.assign((inner: Context) => { scope = createScope(inner, agent) },
{ inject: ['tools', 'systemPrompt'] }))
+6
View File
@@ -37,6 +37,12 @@
},
{
"path": "../../runtime-diagnostics/invariants"
},
{
"path": "../../sandbox/sandbox"
},
{
"path": "../../sandbox/sandbox-policy"
}
]
}
@@ -20,7 +20,7 @@ import type { Duplex } from 'node:stream'
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { CodeRuntime, DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from '@deepseek-ai/dsh-code-runtime'
import type { CodeBindingErrorClass, CodeBindingFunction, CodeJsonValue, CodeRunFailure, CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
import type { CodeBindingErrorClass, CodeBindingFunction, CodeJsonValue, CodeRunFailure, CodeRunRequest, CodeRunResult, CodeRunSpec } from '@deepseek-ai/dsh-code-runtime'
import { snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import type { BootMessage, ChildToHost, ReplyMessage } from './protocol.ts'
@@ -1030,13 +1030,26 @@ export class PythonCodeRuntime extends CodeRuntime {
}
/**
* Execute one program in a fresh Python subprocess. Success resolves with
* `result.value` (and no `result.error`); failure — parse failure, thrown
* exception, invalid completion, output overflow, budget expiry, abort, or
* substrate death — resolves with `result.error` set (classified by
* `CodeRunFailure.kind`). The method rejects only for seam misuse.
* Resolve directory and the experimental provider's configured wall deadline.
* @param request - Program inputs; explicit sandbox or timeout overrides are unsupported.
* @returns Complete inputs for the provider's run method.
* @throws When a requested override is unsupported or cwd is relative.
*/
async run(request: CodeRunRequest): Promise<CodeRunResult> {
resolve(request: CodeRunRequest): CodeRunSpec {
if (request.sandboxPolicy !== undefined) throw new Error('dsh-code-runtime-python: sandbox policy is unsupported')
if (request.timeoutMs !== undefined) throw new Error('dsh-code-runtime-python: per-call timeout is unsupported')
const cwd = request.cwd ?? process.cwd()
if (!isAbsolute(cwd)) throw new Error('dsh-code-runtime-python: cwd must be absolute')
return { ...request, cwd, timeoutMs: this.config.maxWallMs }
}
/**
* Execute a resolved Python program; this experimental provider has no file confinement.
* @param request - Resolved cwd, provider deadline, program and bindings.
* @returns Captured output and the program outcome.
*/
async run(request: CodeRunSpec): Promise<CodeRunResult> {
if (request.sandboxPolicy !== undefined || request.timeoutMs !== this.config.maxWallMs) throw new Error('dsh-code-runtime-python: unsupported execution policy or timeout')
if (this.disposed) throw new Error('dsh-code-runtime-python: run() after disposal')
const bindings = this.validateBindings(request)
if (request.signal?.aborted) {
@@ -1155,7 +1168,7 @@ export class PythonCodeRuntime extends CodeRuntime {
/** Spawn the child for one validated run and drive it to settlement. */
private execute(
request: CodeRunRequest,
request: CodeRunSpec,
bindings: Map<string, ValidatedNamespace>,
bootstrapPath: string,
): Promise<CodeRunResult> {
@@ -1182,6 +1195,7 @@ export class PythonCodeRuntime extends CodeRuntime {
// run. The `_LogStream` replacement of `sys.stdout`/`sys.stderr` is
// unaffected (it is a Python object, not the C-level stdio buffer).
child = spawn(this.pythonBin, ['-u', '-I', bootstrapPath], {
cwd: request.cwd,
// Preserve only the platform temp directory. macOS system Python emits a
// startup warning when TMPDIR is absent; ambient credentials, PATH, HOME,
// and other host state remain unavailable to model code.
@@ -146,10 +146,10 @@ describe('PythonCodeRuntime — controlled subprocess pipes', () => {
const ctx = new Context()
const fiber = await ctx.plugin(PythonCodeRuntime)
const runtime = ctx.codeRuntime as InstanceType<typeof PythonCodeRuntime>
const run = runtime.run({
const run = runtime.run(runtime.resolve({
program: 'return 1',
bindings: [{ global: 'tools', functions: { echo: async (value: unknown) => value as number } }],
})
}))
onTestFinished(async () => {
await fiber.dispose()
await run
@@ -210,7 +210,7 @@ describe('PythonCodeRuntime — controlled subprocess pipes', () => {
const fiber = await ctx.plugin(PythonCodeRuntime)
const runtime = ctx.codeRuntime as InstanceType<typeof PythonCodeRuntime>
const result = await runtime.run({ program: 'return 1', bindings: [] })
const result = await runtime.run(runtime.resolve({ program: 'return 1', bindings: [] }))
expect(result.error?.kind).toBe('worker-exit')
expect(result.error?.message).toContain('failed to boot python subprocess')
@@ -240,7 +240,7 @@ describe('PythonCodeRuntime — controlled subprocess pipes', () => {
const fiber = await ctx.plugin(PythonCodeRuntime)
const runtime = ctx.codeRuntime as InstanceType<typeof PythonCodeRuntime>
const result = await runtime.run({ program: 'return 1', bindings: [] })
const result = await runtime.run(runtime.resolve({ program: 'return 1', bindings: [] }))
expect(result.error?.kind).toBe('worker-exit')
expect(result.error?.message).toContain('python spawn error')
@@ -258,7 +258,7 @@ describe('PythonCodeRuntime — controlled subprocess pipes', () => {
const fiber = await ctx.plugin(PythonCodeRuntime)
const runtime = ctx.codeRuntime as InstanceType<typeof PythonCodeRuntime>
const result = await runtime.run({ program: 'return 1', bindings: [] })
const result = await runtime.run(runtime.resolve({ program: 'return 1', bindings: [] }))
expect(result.error?.kind).toBe('worker-exit')
expect(result.error?.message).toContain('failed to boot python subprocess')
@@ -275,7 +275,7 @@ describe('PythonCodeRuntime — controlled subprocess pipes', () => {
const fiber = await ctx.plugin(PythonCodeRuntime)
const runtime = ctx.codeRuntime as InstanceType<typeof PythonCodeRuntime>
const result = await runtime.run({ program: 'return 1', bindings: [] })
const result = await runtime.run(runtime.resolve({ program: 'return 1', bindings: [] }))
expect(result.error?.kind).toBe('worker-exit')
expect(result.error?.message).toContain('python spawn error')
@@ -302,10 +302,10 @@ describe('PythonCodeRuntime — controlled subprocess pipes', () => {
const fiber = await ctx.plugin(PythonCodeRuntime, { maxWallMs: 3000 })
const runtime = ctx.codeRuntime as InstanceType<typeof PythonCodeRuntime>
const result = await runtime.run({
const result = await runtime.run(runtime.resolve({
program: 'return 1',
bindings: [{ global: 'tools', functions: { f: async () => 'x'.repeat(4 * 1024 * 1024) } }],
})
}))
expect(result.error?.kind).toBe('timeout')
// The drain wait settled on `close` and cleaned up after itself. The
File diff suppressed because it is too large. Load diff
@@ -39,10 +39,10 @@ it('seals stray fragments without recopying the sealed prefix', async () => {
return realConcat(list, total)
})
try {
const result = await ctx.codeRuntime.run({
const result = await ctx.codeRuntime.run(ctx.codeRuntime.resolve({
program: 'import os\nos.write(1, b"x" * 60000 + b"\\n")\nreturn "done"',
bindings: [],
})
}))
expect(result.error).toBeUndefined()
expect(result.value).toBe('done')
expect(result.logs).toEqual(['x'.repeat(60_000)])
@@ -70,10 +70,10 @@ it.each([
return merged
})
try {
const result = await ctx.codeRuntime.run({
const result = await ctx.codeRuntime.run(ctx.codeRuntime.resolve({
program: `import os\nos.write(1, ${payload})\nreturn None`,
bindings: [],
})
}))
expect(result.error).toBeUndefined()
expect(result.logs.at(-1)).toBe(logTruncationMarker(3072))
// Each raw byte decodes to U+FFFD (three UTF-8 bytes), so a 3072-byte
@@ -71,6 +71,8 @@ class CatalogAdapter extends LlmAdapter {
/** In-process PTC mode seam fake that invokes the real registry bindings. */
class FakeRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language = 'typescript'
readonly isolation = 'fake'
behavior: (request: CodeRunRequest) => Promise<CodeRunResult> = () => Promise.resolve({ logs: [] })
+2
View File
@@ -1049,6 +1049,8 @@ function withPersona(...sections: string[]): string {
/** Schema assembly only: these cases never execute user code. */
class GuidanceCodeRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language = 'typescript'
readonly isolation = 'fake'
run() { return Promise.resolve({ logs: [] }) }
@@ -509,6 +509,8 @@ describe('the soft layer', () => {
// Minimal scriptable runtime: the SDK section resolves ctx.codeRuntime at
// assembly time (the ptc.spec fake's shape).
class FakeRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language = 'typescript'
readonly isolation = 'fake'
run(_request: CodeRunRequest): Promise<CodeRunResult> { return Promise.resolve({ logs: [] }) }
@@ -531,6 +533,8 @@ describe('the soft layer', () => {
it('keeps native wire schemas and the SDK in step under mode both', async () => {
class FakeRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language = 'typescript'
readonly isolation = 'fake'
run(_request: CodeRunRequest): Promise<CodeRunResult> { return Promise.resolve({ logs: [] }) }
@@ -553,6 +557,8 @@ describe('the soft layer', () => {
it('keeps the PTC mode SDK byte-identical across mode switches', async () => {
class FakeRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language = 'typescript'
readonly isolation = 'fake'
run(_request: CodeRunRequest): Promise<CodeRunResult> { return Promise.resolve({ logs: [] }) }
@@ -926,6 +932,8 @@ describe('exit_plan_mode', () => {
it('carries the exact plan through a PTC mode review and logs the nested dispatch', async () => {
const plan = '# PTC mode plan\n\nUse the existing seam.'
class ExitRuntime extends CodeRuntime {
resolve(request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest): import('@deepseek-ai/dsh-code-runtime').CodeRunSpec { return { ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: request.timeoutMs ?? 120_000 } }
readonly language = 'typescript'
readonly isolation = 'fake'
async run(request: CodeRunRequest): Promise<CodeRunResult> {
+100
View File
@@ -0,0 +1,100 @@
/** Classify observed sandbox runner failures and file-effect denials. */
import { accessSync, constants, statSync } from 'node:fs'
import type { RunnerFailureRule } from './index.ts'
/** Node-local spawn codes proven to identify executable resolution or permission failure. */
const EXECUTABLE_SPAWN_CODES = new Set(['EACCES', 'ENOENT'])
/** Whether the caller-owned spawn cwd can be entered. */
function isUsableWorkdir(path: string): boolean {
try {
if (!statSync(path).isDirectory()) return false
accessSync(path, constants.X_OK)
return true
} catch {
return false
}
}
/**
* Attribute only Node ENOENT/EACCES failures whose error path equals argv[0]
* after independently ruling out the caller-owned cwd. A supplied error path
* must exactly identify the runner; without one, the syscall must. With a
* usable cwd, these codes describe resolution or execute permission for that
* argv[0] or its shebang interpreter.
* The workdir is checked at classification time, not atomically with spawn;
* concurrent path replacement may change attribution but cannot permit an
* unconfined execution.
* @param error - the original spawn rejection.
* @param runnerProgram - provider argv[0], the executable that establishes confinement.
* @param workdir - the caller-owned spawn cwd, checked independently for usability.
* @returns whether the rejection has executable-specific runner evidence.
*/
export function isRunnerSpawnFailure(
error: unknown,
runnerProgram: string | undefined,
workdir: string,
): boolean {
if (runnerProgram === undefined || !isUsableWorkdir(workdir)) return false
if (typeof error !== 'object' || error === null) return false
const { code, path, syscall } = error as { code?: unknown; path?: unknown; syscall?: unknown }
if (typeof code !== 'string' || !EXECUTABLE_SPAWN_CODES.has(code)) return false
if (typeof syscall !== 'string') return false
const exactSyscall = `spawn ${runnerProgram}`
if (path === undefined) return syscall === exactSyscall
if (typeof path !== 'string' || path.length === 0 || path !== runnerProgram) return false
return syscall === 'spawn' || syscall === exactSyscall
}
/** Fatal runner evidence retained for infrastructure-error detail. */
interface RunnerFailureMatch {
/** The original stderr line that matched a fatal signature. */
detail: string
}
/**
* Classify one settled process against the selected backend's structured
* runner-failure rules. Each rule requires a nonzero exit, its optional
* exit-code gate, and a fatal signature on one stderr line after exact
* informational lines are excluded.
* @param exitCode - process exit code; null means signal termination.
* @param stderr - collected stderr text, left unchanged.
* @param rules - structured runner-failure rules from the active wrap.
* @returns the first matching fatal line, or undefined when evidence is insufficient.
*/
export function classifyRunnerFailure(
exitCode: number | null,
stderr: string,
rules: readonly RunnerFailureRule[],
): RunnerFailureMatch | undefined {
if (exitCode === null || exitCode === 0) return undefined
const lines = stderr.split(/\r?\n/)
for (const rule of rules) {
if (rule.allowedExitCodes !== undefined && !rule.allowedExitCodes.includes(exitCode)) continue
const informationalLines = new Set((rule.informationalLines ?? []).map(line => line.toLowerCase()))
// An empty or whitespace-only substring is not meaningful runner evidence.
// Ignore it while keeping any valid signatures beside it active.
const fatalSignatures = rule.fatalSignatures
.filter(signature => signature.trim().length > 0)
.map(signature => signature.toLowerCase())
for (const line of lines) {
const lowered = line.toLowerCase()
if (informationalLines.has(lowered)) continue
if (fatalSignatures.some(signature => lowered.includes(signature))) return { detail: line }
}
}
return undefined
}
/**
* Match a non-zero exit against case-insensitive stderr signatures.
* @param exitCode - process exit code; null means signal termination.
* @param stderr - collected stderr text.
* @param signatures - substrings identifying the selected backend's dialect.
* @returns whether this is a non-zero exit whose stderr matches a signature.
*/
export function matchesSignature(exitCode: number | null, stderr: string, signatures: readonly string[]): boolean {
if (exitCode === null || exitCode === 0) return false
const lowered = stderr.toLowerCase()
return signatures.some(signature => lowered.includes(signature.toLowerCase()))
}
+2
View File
@@ -176,3 +176,5 @@ export abstract class SandboxProvider extends Service {
}
export default SandboxProvider
export { classifyRunnerFailure, isRunnerSpawnFailure, matchesSignature } from './diagnostics.ts'
+3 -105
View File
@@ -1,62 +1,7 @@
/**
* Internal sandbox-result classification helpers.
*
* @module @deepseek-ai/dsh-bash-sandbox/helpers
*/
import { accessSync, constants, statSync } from 'node:fs'
/** Shell-result projection over shared sandbox diagnostics. */
import type { ShellRunResult } from '@deepseek-ai/dsh-shell'
import type { RunnerFailureRule } from '@deepseek-ai/dsh-sandbox'
/** Node-local spawn codes proven to identify executable resolution or permission failure. */
const EXECUTABLE_SPAWN_CODES = new Set(['EACCES', 'ENOENT'])
/** Whether the caller-owned spawn cwd can be entered. */
function isUsableWorkdir(path: string): boolean {
try {
if (!statSync(path).isDirectory()) return false
accessSync(path, constants.X_OK)
return true
} catch {
return false
}
}
/**
* Attribute only Node ENOENT/EACCES failures whose error path equals argv[0]
* after independently ruling out the caller-owned cwd. A supplied error path
* must exactly identify the runner; without one, the syscall must. With a
* usable cwd, these codes describe resolution or execute permission for that
* argv[0] or its shebang interpreter.
* The workdir is checked at classification time, not atomically with spawn;
* concurrent path replacement may change attribution but cannot permit an
* unconfined execution.
* @param error - the original spawn rejection.
* @param runnerProgram - provider argv[0], the executable that establishes confinement.
* @param workdir - the caller-owned spawn cwd, checked independently for usability.
* @returns whether the rejection has executable-specific runner evidence.
*/
export function isRunnerSpawnFailure(
error: unknown,
runnerProgram: string | undefined,
workdir: string,
): boolean {
if (runnerProgram === undefined || !isUsableWorkdir(workdir)) return false
if (typeof error !== 'object' || error === null) return false
const { code, path, syscall } = error as { code?: unknown; path?: unknown; syscall?: unknown }
if (typeof code !== 'string' || !EXECUTABLE_SPAWN_CODES.has(code)) return false
if (typeof syscall !== 'string') return false
const exactSyscall = `spawn ${runnerProgram}`
if (path === undefined) return syscall === exactSyscall
if (typeof path !== 'string' || path.length === 0 || path !== runnerProgram) return false
return syscall === 'spawn' || syscall === exactSyscall
}
/** Fatal runner evidence retained for infrastructure-error detail. */
interface RunnerFailureMatch {
/** The original stderr line that matched a fatal signature. */
detail: string
}
import { matchesSignature } from '@deepseek-ai/dsh-sandbox'
export { isRunnerSpawnFailure, classifyRunnerFailure, matchesSignature } from '@deepseek-ai/dsh-sandbox'
/**
* Classify a failed run against the selected backend's denial dialect.
@@ -67,50 +12,3 @@ interface RunnerFailureMatch {
export function classifyDenial(result: ShellRunResult, signatures: readonly string[]): boolean {
return matchesSignature(result.exitCode, result.stderr.text, signatures)
}
/**
* Classify one settled process against the selected backend's structured
* runner-failure rules. Each rule requires a nonzero exit, its optional
* exit-code gate, and a fatal signature on one stderr line after exact
* informational lines are excluded.
* @param exitCode - process exit code; null means signal termination.
* @param stderr - collected stderr text, left unchanged.
* @param rules - structured runner-failure rules from the active wrap.
* @returns the first matching fatal line, or undefined when evidence is insufficient.
*/
export function classifyRunnerFailure(
exitCode: number | null,
stderr: string,
rules: readonly RunnerFailureRule[],
): RunnerFailureMatch | undefined {
if (exitCode === null || exitCode === 0) return undefined
const lines = stderr.split(/\r?\n/)
for (const rule of rules) {
if (rule.allowedExitCodes !== undefined && !rule.allowedExitCodes.includes(exitCode)) continue
const informationalLines = new Set((rule.informationalLines ?? []).map(line => line.toLowerCase()))
// An empty or whitespace-only substring is not meaningful runner evidence.
// Ignore it while keeping any valid signatures beside it active.
const fatalSignatures = rule.fatalSignatures
.filter(signature => signature.trim().length > 0)
.map(signature => signature.toLowerCase())
for (const line of lines) {
const lowered = line.toLowerCase()
if (informationalLines.has(lowered)) continue
if (fatalSignatures.some(signature => lowered.includes(signature))) return { detail: line }
}
}
return undefined
}
/**
* Match a non-zero exit against case-insensitive stderr signatures.
* @param exitCode - process exit code; null means signal termination.
* @param stderr - collected stderr text.
* @param signatures - substrings identifying the selected backend's dialect.
* @returns whether this is a non-zero exit whose stderr matches a signature.
*/
export function matchesSignature(exitCode: number | null, stderr: string, signatures: readonly string[]): boolean {
if (exitCode === null || exitCode === 0) return false
const lowered = stderr.toLowerCase()
return signatures.some(signature => lowered.includes(signature.toLowerCase()))
}
@@ -21,7 +21,7 @@ import type { PostToolDecision, ToolExecution, ToolExecutionToken } from '@deeps
import { SpillLocator, SpillStore } from '@deepseek-ai/dsh-spill'
import type { SaveTextSpill, SpillRef } from '@deepseek-ai/dsh-spill'
import * as SpillPolicy from '@deepseek-ai/dsh-spill-policy'
import { NodeCodeRuntime } from '@deepseek-ai/dsh-code-runtime-node'
import { mountRuntime } from '../../../code-runtime/code-runtime-node/tests/setup.ts'
const testToolSignal = new AbortController().signal
@@ -193,7 +193,7 @@ describe('outer PTC mode failure capture', () => {
await ctx.plugin(ToolRuntime, { mode: 'ptc' })
await ctx.plugin(StubStore)
await ctx.plugin(SpillPolicy, { maxInlineBytes: 200 })
await ctx.plugin(NodeCodeRuntime, { maxOutputBytes: 500 })
await mountRuntime(ctx, { maxOutputBytes: 500 })
const events: unknown[] = []
const agent = {
session: {
@@ -242,7 +242,7 @@ describe('the durable dispatch-log arm', () => {
await ctx.plugin(ToolRuntime, { mode: 'ptc' })
await ctx.plugin(StubStore)
await ctx.plugin(SpillPolicy, { maxInlineBytes })
await ctx.plugin(NodeCodeRuntime, {})
await mountRuntime(ctx, {})
const events: { type: string; data: unknown }[] = []
const agent = {
session: {
@@ -316,7 +316,7 @@ describe('the durable dispatch-log arm', () => {
await ctx.plugin(ToolRuntime, { mode: 'ptc' })
await ctx.plugin(StubStore)
await ctx.plugin(SpillPolicy, { maxInlineBytes: 100 })
await ctx.plugin(NodeCodeRuntime, {})
await mountRuntime(ctx, {})
// A spill backend that hangs until released.
let releaseSave!: () => void
const gate = new Promise<void>((resolve) => { releaseSave = resolve })
@@ -380,7 +380,7 @@ describe('the durable dispatch-log arm', () => {
await ctx.plugin(ToolRuntime, { mode: 'ptc', maxParallelSubCalls: 1 })
await ctx.plugin(StubStore)
await ctx.plugin(SpillPolicy, { maxInlineBytes: 100 })
await ctx.plugin(NodeCodeRuntime, {})
await mountRuntime(ctx, {})
const store = ctx.spillStore as StubStore
const releases: (() => void)[] = []
store.gate = () => new Promise<void>((resolve) => { releases.push(resolve) })
@@ -433,7 +433,7 @@ describe('the durable dispatch-log arm', () => {
await ctx.plugin(ToolRuntime, { mode: 'ptc' })
await ctx.plugin(StubStore)
await ctx.plugin(SpillPolicy, { maxInlineBytes: 100 })
await ctx.plugin(NodeCodeRuntime, {})
await mountRuntime(ctx, {})
;(ctx.spillStore as StubStore).fail = true
const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {})
const events: { type: string; data: unknown }[] = []
@@ -63,6 +63,7 @@ async function setup(script: Script, options: SetupOptions = {}) {
ctx.provide('codeRuntime', {
language: 'typescript',
isolation: 'test',
resolve: (request: import('@deepseek-ai/dsh-code-runtime').CodeRunRequest) => ({ ...request, cwd: request.cwd ?? process.cwd(), timeoutMs: 120_000 }),
run: options.codeRun ?? (() => Promise.resolve({ logs: [] })),
} as never)
}
+33
View File
@@ -4584,6 +4584,9 @@ importers:
'@deepseek-ai/cordis':
specifier: workspace:^
version: link:../../../vendor/cordis
'@deepseek-ai/dsh-sandbox':
specifier: workspace:^
version: link:../../sandbox/sandbox
packages/code-runtime/code-runtime-node:
dependencies:
@@ -4600,9 +4603,33 @@ importers:
'@deepseek-ai/dsh-code-runtime':
specifier: workspace:^
version: link:../code-runtime
'@deepseek-ai/dsh-fs':
specifier: workspace:^
version: link:../../fs/fs
'@deepseek-ai/dsh-fs-local':
specifier: workspace:^
version: link:../../fs/fs-local
'@deepseek-ai/dsh-sandbox':
specifier: workspace:^
version: link:../../sandbox/sandbox
'@deepseek-ai/dsh-sandbox-local':
specifier: workspace:^
version: link:../../sandbox/sandbox-local
'@deepseek-ai/dsh-sandbox-policy':
specifier: workspace:^
version: link:../../sandbox/sandbox-policy
'@deepseek-ai/dsh-session':
specifier: workspace:^
version: link:../../core/session
'@deepseek-ai/dsh-session-projection':
specifier: workspace:^
version: link:../../session/session-projection
'@deepseek-ai/dsh-subprocess':
specifier: workspace:^
version: link:../../subprocess/subprocess
'@deepseek-ai/dsh-subprocess-local':
specifier: workspace:^
version: link:../../subprocess/subprocess-local
'@deepseek-ai/dsh-timeout':
specifier: workspace:^
version: link:../../util/timeout
@@ -5225,6 +5252,12 @@ importers:
'@deepseek-ai/dsh-llm':
specifier: workspace:^
version: link:../../llm/llm
'@deepseek-ai/dsh-sandbox':
specifier: workspace:^
version: link:../../sandbox/sandbox
'@deepseek-ai/dsh-sandbox-policy':
specifier: workspace:^
version: link:../../sandbox/sandbox-policy
'@deepseek-ai/dsh-scope':
specifier: workspace:^
version: link:../scope
+4 -1
View File
@@ -4,7 +4,10 @@
const selectorName = 'DSH_SUBPROCESS_RUNNER'
const selection = process.env[selectorName]
if (selection === undefined) {
if (process.env.DSH_CODE_RUNTIME_NODE === '1') {
Reflect.deleteProperty(process.env, 'DSH_CODE_RUNTIME_NODE')
await import('@deepseek-ai/dsh-code-runtime-node/process')
} else if (selection === undefined) {
const { runCli } = await import('@deepseek-ai/dsh/lib/bin.js')
await runCli()
} else {
+2
View File
@@ -210,6 +210,8 @@ export default defineConfig({
'packages/*/*/src/types.ts',
'packages/*/*/src/bin.ts',
'packages/*/*/src/worker.ts',
// The built Node entry invokes the independently covered process bootstrap through fd 7.
'packages/code-runtime/code-runtime-node/src/process-entry.ts',
// Dynamic Host/Client composition is covered by its focused lifecycle
// tests and assembled application checks rather than per-file coverage.
'packages/self-modification/*/src/**/*.{ts,tsx}',