feat(code-runtime): complete sandboxed Node execution and Session fixtures

This commit is contained in:
Tianyi Cui committed 2026-09-12 15:03:01 +08:00
1 parent f6904f4615
commit 8e19ec4962
82 files changed
+2580 -348

No files matched your search

@@ -0,0 +1,354 @@
import { Duplex, PassThrough } from 'node:stream'
import { setImmediate } from 'node:timers/promises'
import { Context } from '@deepseek-ai/cordis'
import { describe, expect, it, onTestFinished, vi } from 'vitest'
import type { CodeBindingFunction, CodeRunRequest } from '@deepseek-ai/dsh-code-runtime'
import type { SubprocessHandle, SubprocessOutcome } from '@deepseek-ai/dsh-subprocess'
import { SandboxUnavailableError } from '@deepseek-ai/dsh-sandbox'
import type { ConfinedArgv } from '@deepseek-ai/dsh-sandbox'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import type { Config } from '../src/index.ts'
import { JsonChannel } from '../src/channel.ts'
import { encodeCodeJsonWire } from '../src/json-wire.ts'
import { mountRuntime } from './setup.ts'
const request: CodeRunRequest = { program: 'return 1', bindings: [] }
const NO_INITIAL_FRAME = Symbol('no initial frame')
async function setup(config: Config = {}, mode: 'read-only' | 'danger-full-access' = 'danger-full-access') {
const ctx = new Context()
const runtime = await mountRuntime(ctx, config, { mode, workspaceRoot: process.cwd() })
const control = new Duplex({
read() {},
write(chunk: Buffer, _encoding, callback) {
if (childControl.destroyed) { callback(new Error('peer closed')); return }
childControl.push(chunk)
callback()
},
})
const childControl = new Duplex({
read() {},
write(chunk: Buffer, _encoding, callback) {
if (control.destroyed) { callback(new Error('peer closed')); return }
control.push(chunk)
callback()
},
})
const stdout = new PassThrough()
const stderr = new PassThrough()
const direct = Promise.withResolvers<SubprocessOutcome>()
const messages: unknown[] = []
const handle: SubprocessHandle = {
stdin: undefined,
stdout,
stderr,
control,
collected: {},
done: direct.promise,
terminate: vi.fn(() => {
stdout.end()
stderr.end()
direct.resolve({ exitCode: 0, signal: null })
}),
waitForExit: vi.fn(async () => true),
}
const writes = new Set<Promise<void>>()
let receive: (message: unknown) => void = () => {}
const peer = new JsonChannel(childControl, 1024 * 1024, (message) => {
messages.push(message)
receive(message)
}, () => {})
const emit = (message: unknown): void => {
const write = peer.send(message).catch(() => {}).finally(() => { writes.delete(write) })
writes.add(write)
}
const resolveExecutable = vi.spyOn(ctx.subprocess, 'resolveExecutable').mockResolvedValue(process.execPath)
const spawn = vi.spyOn(ctx.subprocess, 'spawn').mockReturnValue(handle)
onTestFinished(async () => {
peer.close()
control.destroy()
stdout.destroy()
stderr.destroy()
direct.resolve({ exitCode: 0, signal: null })
await Promise.all(writes)
})
const start = (input: CodeRunRequest = request, first: unknown = { type: 'ready' }) => {
const result = runtime.run(runtime.resolve(input))
if (first !== NO_INITIAL_FRAME) queueMicrotask(() => { emit(first) })
return result
}
const onBoot = (callback: () => void): void => {
receive = (message) => {
if (typeof message === 'object' && message !== null && 'type' in message && message.type === 'boot') callback()
}
}
return {
ctx, runtime, handle, direct, stdout, stderr, control, peer, messages, spawn, resolveExecutable, emit, start, onBoot,
receive: (callback: typeof receive) => { receive = callback },
}
}
function call(id: number, value: string = '') {
return { type: 'call', id, global: 'tools', name: 'test', args: encodeCodeJsonWire(value) }
}
function withBinding(fn: CodeBindingFunction): CodeRunRequest {
return { ...request, bindings: [{ global: 'tools', functions: { test: fn } }] }
}
function confinement(argv: string[]): ConfinedArgv {
return { argv, enforcement: 'partial', denialSignatures: ['EACCES'], runnerFailureRules: [{ fatalSignatures: ['sandbox-fatal:'] }] }
}
describe('Node runtime host failures', () => {
it.each<[Config, string]>([
[{ timeoutMs: 0 }, 'timeoutMs'],
[{ maxPendingCalls: -1 }, 'maxPendingCalls'],
[{ maxOldGenerationSizeMb: Infinity }, 'maxOldGenerationSizeMb'],
[{ timeoutMs: MAX_TIMER_DELAY_MS + 1 }, 'timeoutMs'],
[{ maxTimeoutMs: MAX_TIMER_DELAY_MS + 1 }, 'maxTimeoutMs'],
[{ graceMs: MAX_TIMER_DELAY_MS + 1 }, 'graceMs'],
[{ maxOutputBytes: 3 }, 'maxOutputBytes'],
[{ maxOutputBytes: 4.5 }, 'maxOutputBytes'],
[{ maxMessageBytes: 1.5 }, 'maxMessageBytes'],
[{ maxMessageBytes: 0x1_0000_0000 }, 'maxMessageBytes'],
[{ maxPendingCalls: 1.5 }, 'maxPendingCalls'],
[{ maxOldGenerationSizeMb: 1.5 }, 'maxOldGenerationSizeMb'],
[{ nodeExecutable: '' }, 'nodeExecutable'],
[{ bootstrapPath: 'relative-bootstrap.js' }, 'bootstrapPath'],
])('rejects deployment configuration %j', async (config, field) => {
await expect(mountRuntime(new Context(), config)).rejects.toThrow(field)
})
it('reports the deployment mode and rejects unsupported resolved inputs before spawning', async () => {
const h = await setup({}, 'read-only')
expect(h.runtime.sandboxMode).toBe('read-only')
expect(() => h.runtime.resolve({ ...request, cwd: 'relative' })).toThrow('cwd must be absolute')
await expect(h.runtime.run({ ...request, cwd: process.cwd(), timeoutMs: 1 })).rejects.toThrow('resolved sandbox policy')
const spec = h.runtime.resolve(request)
await expect(h.runtime.run({ ...spec, timeoutMs: Infinity })).rejects.toThrow('resolved cwd and timeout')
expect(h.spawn).not.toHaveBeenCalled()
await h.ctx.fiber.dispose()
expect(() => h.runtime.resolve(request)).toThrow('resolve after disposal')
await expect(h.runtime.run(spec)).rejects.toThrow('run after disposal')
})
it('fails a required unavailable sandbox before spawning', async () => {
const h = await setup({}, 'read-only')
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(() => { throw new SandboxUnavailableError('read-only') })
expect((await h.start()).error?.kind).toBe('sandbox-unavailable')
expect(h.spawn).not.toHaveBeenCalled()
})
it('keeps partial enforcement separate from a successful program', async () => {
const h = await setup({}, 'read-only')
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([...argv]))
h.onBoot(() => { h.emit({ type: 'done', value: encodeCodeJsonWire(42) }) })
expect(await h.start()).toEqual({ logs: [], value: 42, sandbox: { mode: 'read-only', denied: false, enforcement: 'partial' } })
})
it.each([['EACCES: blocked', true], ['EPERM: unrelated dialect', false]] as const)('uses only the selected denial dialect for %s', async (message, denied) => {
const h = await setup({}, 'read-only')
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([...argv]))
h.onBoot(() => { h.emit({ type: 'done', error: { kind: 'exception', message } }) })
const result = await h.start()
expect(result.error).toEqual({ kind: 'exception', message })
expect(result.sandbox).toEqual({ mode: 'read-only', denied, enforcement: 'partial' })
})
it('distinguishes fatal sandbox startup output from a program denial', async () => {
const h = await setup({}, 'read-only')
vi.spyOn(h.ctx.sandbox, 'confine').mockImplementation(argv => confinement([...argv]))
h.onBoot(() => {
h.stderr.write('sandbox-fatal: runner could not initialize')
h.direct.resolve({ exitCode: 1, signal: null })
})
const result = await h.start()
expect(result.error?.kind).toBe('sandbox-unavailable')
expect(result.sandbox?.denied).toBe(false)
})
it('reports bootstrap assets that cannot map into the execution world', async () => {
const h = await setup()
vi.spyOn(h.ctx.fs, 'processPathFromHostPath').mockReturnValue(undefined)
expect((await h.start()).error?.message).toContain('bootstrap is unavailable')
expect(h.spawn).not.toHaveBeenCalled()
})
it('cleans up a provider that fails to supply its requested control pipe', async () => {
const h = await setup()
h.spawn.mockReturnValue({ ...h.handle, control: undefined })
expect((await h.start()).error?.message).toContain('did not supply the requested control')
expect(h.handle.terminate).toHaveBeenCalledOnce()
expect(h.handle.waitForExit).toHaveBeenCalledOnce()
})
it('reports an executable lookup failure without allocating a process', async () => {
const h = await setup()
h.resolveExecutable.mockRejectedValue(new Error('Node executable missing'))
expect((await h.start()).error).toEqual({ kind: 'worker-exit', message: 'Node executable missing' })
expect(h.spawn).not.toHaveBeenCalled()
})
it('honors an already-aborted run without beginning executable lookup', async () => {
const h = await setup()
const result = await h.start({ ...request, signal: AbortSignal.abort('already stopped') }, NO_INITIAL_FRAME)
expect(result.error).toEqual({ kind: 'abort', message: 'already stopped' })
expect(h.resolveExecutable).not.toHaveBeenCalled()
})
it('does not launch after cancellation races executable lookup completion', async () => {
const h = await setup()
const controller = new AbortController()
h.resolveExecutable.mockImplementation(async () => {
controller.abort('lookup canceled')
return process.execPath
})
expect((await h.start({ ...request, signal: controller.signal }, NO_INITIAL_FRAME)).error?.kind).toBe('abort')
expect(h.spawn).not.toHaveBeenCalled()
})
it('reports an early control EOF using the direct process result', async () => {
const h = await setup()
h.spawn.mockImplementation(() => {
queueMicrotask(() => {
h.control.push(null)
h.direct.resolve({ exitCode: 7, signal: null })
})
return h.handle
})
expect((await h.start(request, NO_INITIAL_FRAME)).error).toEqual({
kind: 'worker-exit', message: 'Node process exited before completing (7)',
})
})
it('reports startup transport loss when the direct process outcome rejects', async () => {
const h = await setup()
h.spawn.mockImplementation(() => {
queueMicrotask(() => {
h.control.emit('error', new Error('control transport closed'))
h.direct.reject(new Error('runner connection broken'))
})
return h.handle
})
expect((await h.start(request, NO_INITIAL_FRAME)).error).toEqual({ kind: 'worker-exit', message: 'runner connection broken' })
})
it('reports control loss after readiness as a substrate failure', async () => {
const h = await setup()
h.onBoot(() => { h.control.emit('error', new Error('control transport closed')) })
expect((await h.start()).error).toEqual({ kind: 'worker-exit', message: 'control transport closed' })
})
it('classifies invalid UTF-8 on the live control stream as a protocol failure', async () => {
const h = await setup()
h.onBoot(() => { h.control.push(Buffer.from([0, 0, 0, 1, 0xff])) })
expect((await h.start()).error?.kind).toBe('protocol')
})
it('rejects traffic before readiness without invoking host bindings', async () => {
const h = await setup()
const binding = vi.fn(async () => null)
const result = await h.start(withBinding(binding), call(1))
expect(result.error?.message).toContain('before bootstrap readiness')
expect(binding).not.toHaveBeenCalled()
})
it.each<[unknown, string]>([
[null, 'invalid control frame'],
[{ type: 'log', text: 1 }, 'invalid log frame'],
[{ type: 'done', error: null }, 'invalid terminal error'],
[{ type: 'done', error: { kind: 'exception', message: 1 } }, 'invalid terminal error'],
[{ type: 'done', error: { kind: 'timeout', message: 'forged' } }, 'invalid terminal error'],
[{ type: 'call', id: 0, global: 'tools', name: 'test' }, 'invalid binding call identity'],
[{ ...call(1), name: 'constructor' }, 'undeclared binding'],
[{ type: 'call', id: 1, global: 'tools', name: 'test' }, 'arguments must be lossless JSON'],
[{ type: 'unknown' }, 'unknown control message'],
])('rejects malformed program frame %j', async (frame, message) => {
const h = await setup()
const binding = vi.fn(async () => null)
h.onBoot(() => { h.emit(frame) })
const result = await h.start(withBinding(binding))
expect(result.error).toEqual({ kind: 'protocol', message: expect.stringContaining(message) })
expect(binding).not.toHaveBeenCalled()
})
it('refuses a repeated call id instead of dispatching it twice', async () => {
const h = await setup()
const binding = vi.fn(async () => null)
h.onBoot(() => { h.emit(call(1)); h.emit(call(1)) })
expect((await h.start(withBinding(binding))).error?.kind).toBe('protocol')
expect(binding).toHaveBeenCalledOnce()
})
it.each<Config>([{ maxPendingCalls: 1 }, { maxMessageBytes: 512 }])('bounds unresolved host calls with %j', async (config) => {
const h = await setup(config)
const release = Promise.withResolvers<null>()
onTestFinished(() => { release.resolve(null) })
const binding = vi.fn(async () => await release.promise)
h.onBoot(() => { h.emit(call(1, 'a'.repeat(300))); h.emit(call(2, 'b'.repeat(300))) })
expect((await h.start(withBinding(binding))).error?.message).toContain('pending binding calls exceed')
expect(binding).toHaveBeenCalledOnce()
release.resolve(null)
await setImmediate()
})
it('rejects an untransferable completion instead of returning a substituted value', async () => {
const h = await setup()
h.onBoot(() => { h.emit({ type: 'done', value: { invalid: true } }) })
expect((await h.start()).error?.kind).toBe('invalid-output')
})
it('returns non-lossless binding resolutions as program-visible binding failures', async () => {
const h = await setup()
h.receive((message) => {
if (typeof message !== 'object' || message === null || !('type' in message)) return
if (message.type === 'boot') h.emit(call(1))
if (message.type === 'reply') h.emit({ type: 'done' })
})
expect((await h.start(withBinding(async () => Number.NaN))).error).toBeUndefined()
expect(h.messages).toContainEqual({ type: 'reply', id: 1, ok: false, message: expect.stringContaining('lossless JSON') })
})
it('does not publish a late binding reply after the program has settled', async () => {
const h = await setup()
const release = Promise.withResolvers<null>()
onTestFinished(() => { release.resolve(null) })
h.onBoot(() => { h.emit(call(1)); h.emit({ type: 'done' }) })
expect((await h.start(withBinding(async () => await release.promise))).error).toBeUndefined()
release.resolve(null)
await setImmediate()
expect(h.messages).not.toContainEqual(expect.objectContaining({ type: 'reply' }))
expect(h.control.destroyed).toBe(true)
})
it('reports failed managed cleanup even when the program returns successfully', async () => {
const h = await setup()
vi.mocked(h.handle.waitForExit).mockRejectedValue(new Error('range cannot be observed'))
h.onBoot(() => { h.emit({ type: 'done', value: encodeCodeJsonWire(42) }) })
expect((await h.start()).error).toEqual({ kind: 'worker-exit', message: 'managed process cleanup failed: range cannot be observed' })
})
it('fails when the configured frame budget cannot carry bootstrap data', async () => {
const h = await setup({ maxMessageBytes: 32 })
expect((await h.start()).error?.message).toContain('control output exceeds 32 queued bytes')
})
it('turns an oversized binding reply into a protocol failure', async () => {
const h = await setup({ maxMessageBytes: 256 })
h.onBoot(() => { h.emit(call(1)) })
const result = await h.start(withBinding(async () => 'x'.repeat(512)))
expect(result.error).toEqual({ kind: 'protocol', message: 'control output exceeds 256 queued bytes' })
})
it('retains admitted logs when the program reports its own output limit', async () => {
const h = await setup()
h.onBoot(() => {
h.emit({ type: 'log', text: 'retained' })
h.emit({ type: 'done', error: { kind: 'output-limit', message: 'child ledger exhausted' } })
})
const result = await h.start()
expect(result.error?.kind).toBe('output-limit')
expect(result.logs).toEqual(['retained'])
})
})
@@ -0,0 +1,25 @@
import { expect, it } from 'vitest'
import type { FileSystem } from '@deepseek-ai/dsh-fs'
import { bootstrapArgs } from '../src/launch.ts'
it('uses an explicit installed bootstrap without pretending it maps the host file', () => {
const fs = { processPathFromHostPath: () => { throw new Error('must not map') } } as unknown as FileSystem
expect(bootstrapArgs(fs, { bootstrapPath: '/remote/process.js' }, 2048)).toEqual(['/remote/process.js', '2048'])
})
it('fails when the execution world cannot read the source bootstrap', () => {
const fs = { processPathFromHostPath: () => undefined } as unknown as FileSystem
expect(() => bootstrapArgs(fs, {}, 2048)).toThrow('unavailable in the subprocess execution world')
})
it('selects the private packaged bootstrap through the existing executable', () => {
const prior = Object.getOwnPropertyDescriptor(process, 'pkg')
try {
Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
const fs = { processPathFromHostPath: () => { throw new Error('pkg does not expose a host bootstrap file') } } as unknown as FileSystem
expect(bootstrapArgs(fs, {}, 2048)).toEqual(['2048'])
} finally {
if (prior === undefined) Reflect.deleteProperty(process, 'pkg')
else Object.defineProperty(process, 'pkg', prior)
}
})
@@ -0,0 +1,26 @@
import { expect, it } from 'vitest'
import { OutputLedger } from '../src/output-ledger.ts'
it('accounts for exact JSON escaping, separators and optional completion values', () => {
const ledger = new OutputLedger(20)
const logs: string[] = []
expect(ledger.admit('first', logs)).toBe(true)
expect(ledger.admit('second', logs)).toBe(true)
expect(ledger.admit('third', logs)).toBe(false)
expect(ledger.success(logs)).toEqual({ logs: ['first', 'second'] })
expect(ledger.success(logs, 1)).toEqual({ logs: ['first', 'second'], value: 1 })
expect(ledger.success(logs, 'too long').error?.kind).toBe('output-limit')
})
it('retains a bounded prefix when logs or a failure diagnostic exceed the limit', () => {
for (const maxBytes of [4, 8, 40, 80]) {
const ledger = new OutputLedger(maxBytes)
const result = ledger.limit(['a', 'b', '你好🙂'.repeat(50)])
expect(result.error?.kind).toBe('output-limit')
const bytes = Buffer.byteLength(JSON.stringify(result.logs)) + Buffer.byteLength(JSON.stringify(result.error?.message))
expect(bytes).toBeLessThanOrEqual(maxBytes)
}
const ledger = new OutputLedger(80)
expect(ledger.failure([], { kind: 'exception', message: 'short' })).toEqual({ logs: [], error: { kind: 'exception', message: 'short' } })
expect(ledger.failure([], { kind: 'exception', message: 'x'.repeat(100) }).error?.kind).toBe('output-limit')
})
@@ -0,0 +1,35 @@
import { PassThrough } from 'node:stream'
import { afterEach, expect, it, vi } from 'vitest'
import { drainOutput } from '../src/output-stream.ts'
afterEach(() => vi.useRealTimers())
it('waits for queued output and accepts an already ended or absent stream', async () => {
const stream = new PassThrough()
const chunks: string[] = []
stream.on('data', chunk => chunks.push(String(chunk)))
const pending = drainOutput(stream, 1000)
stream.end('last output')
expect(await pending).toBe(true)
expect(chunks).toEqual(['last output'])
expect(await drainOutput(stream, 1000)).toBe(true)
expect(await drainOutput(undefined, 1000)).toBe(true)
})
it('bounds an output descriptor retained after the process exits', async () => {
vi.useFakeTimers()
const stream = new PassThrough()
const pending = drainOutput(stream, 100)
await vi.advanceTimersByTimeAsync(100)
expect(await pending).toBe(false)
stream.destroy()
expect(await drainOutput(stream, 100)).toBe(false)
})
it.each(['close', 'error'])('reports incomplete output on %s', async (event) => {
const stream = new PassThrough()
const pending = drainOutput(stream, 1000)
stream.emit(event, new Error('pipe failed'))
expect(await pending).toBe(false)
stream.destroy()
})
@@ -3,7 +3,7 @@ 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 { Duplex } from 'node:stream'
import { expect, it, onTestFinished } from 'vitest'
import { JsonChannel } from '../src/channel.ts'
import { decodeCodeJsonWire, encodeCodeJsonWire } from '../src/json-wire.ts'
@@ -25,7 +25,9 @@ it('boots an unbuilt source closure outside the workspace and exchanges tool rep
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 control = Array.from(child.stdio)[7]
if (!(control instanceof Duplex)) { child.kill(); await finished; throw new Error('missing child control channel') }
const channel = new JsonChannel(control, 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: {
@@ -100,12 +100,14 @@ describe('Node program process', () => {
it('disposes active programs and rejects later execution', async () => {
const { ctx, run, runtime } = await setup()
const spec = runtime.resolve({ program: '', bindings: [] })
const entered = Promise.withResolvers<undefined>()
const active = run({ program: 'await tools.enter({}); await new Promise(() => {})', bindings: bindings({ enter: async () => { entered.resolve(undefined); return null } }) })
await entered.promise
await ctx.fiber.dispose()
expect((await active).error?.kind).toBe('abort')
await expect(run({ program: '', bindings: [] })).rejects.toThrow('disposal')
await expect(runtime.run(spec)).rejects.toThrow('disposal')
expect(() => runtime.resolve({ program: '', bindings: [] })).toThrow('disposal')
expect(runtime.isolation).toBe('process')
})
@@ -140,3 +142,14 @@ describe('Node program process', () => {
await expect(readFile(join(outside, 'denied.txt'))).rejects.toMatchObject({ code: 'ENOENT' })
})
})
it('preserves empty console entries and bounds native output overflow', async () => {
const { run } = await setup({ maxOutputBytes: 100 })
const lines = await run({ program: 'console.log("a"); console.log(""); console.log("b")', bindings: [] })
expect(lines.logs).toEqual(['a', '', 'b'])
const overflow = await run({ program: 'const fs=await import("node:fs"); fs.writeSync(1,"HEAD-"+"x".repeat(100000));', bindings: [] })
expect(overflow.error?.kind).toBe('output-limit')
expect(overflow.logs.join('')).toContain('HEAD-')
const bytes = Buffer.byteLength(JSON.stringify(overflow.logs)) + Buffer.byteLength(JSON.stringify(overflow.error?.message))
expect(bytes).toBeLessThanOrEqual(100)
})
@@ -1,4 +1,6 @@
import { Context } from '@deepseek-ai/cordis'
import { onTestFinished } from 'vitest'
import SessionStore from '@deepseek-ai/dsh-session'
import FileSystem from '@deepseek-ai/dsh-fs-local'
import Subprocess from '@deepseek-ai/dsh-subprocess-local'
import Sandbox from '@deepseek-ai/dsh-sandbox-local'
@@ -9,6 +11,8 @@ 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 } = {}) {
onTestFinished(async () => { await ctx.fiber.dispose() })
if (!ctx.get('sessions')) await ctx.plugin(SessionStore)
if (!ctx.get('fs')) await ctx.plugin(FileSystem)
if (!ctx.get('subprocess')) await ctx.plugin(Subprocess)
if (!ctx.get('sandbox')) await ctx.plugin(Sandbox, {})