442 lines
18 KiB
TypeScript
442 lines
18 KiB
TypeScript
/** E2B process/worker implementation of the harness code-runtime seam. */
|
|
|
|
import { posix } from 'node:path'
|
|
import { stripTypeScriptTypes } from 'node:module'
|
|
import type { Context } from 'cordis'
|
|
import z from 'schemastery'
|
|
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
|
import type {
|
|
CodeBindingNamespace,
|
|
CodeJsonValue,
|
|
CodeRunFailure,
|
|
CodeRunRequest,
|
|
CodeRunResult,
|
|
} from '@deepseek-ai/dsh-code-runtime'
|
|
import {
|
|
E2BFrameDecoder,
|
|
encodeE2BFrame,
|
|
quoteE2BShellArg,
|
|
resolveE2BExecutable,
|
|
} from '@deepseek-ai/dsh-e2b'
|
|
import {
|
|
decodeWorkerJson,
|
|
encodeWorkerJson,
|
|
OutputLedger,
|
|
} from '@deepseek-ai/dsh-code-runtime-worker'
|
|
import type { WorkerJsonWire } from '@deepseek-ai/dsh-code-runtime-worker'
|
|
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
|
|
import E2BSubprocessService from '@deepseek-ai/dsh-subprocess-e2b'
|
|
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
|
import { CODE_RUNNER_SOURCE } from './runner-source.ts'
|
|
|
|
/** Runtime configuration; every execution and bridge bound is deployment-tunable. */
|
|
export interface Config {
|
|
/** Remote worker measured event-loop busy-time budget. */
|
|
computeMs?: number
|
|
/** Host-observed wall-clock ceiling. */
|
|
maxWallMs?: number
|
|
/** Combined serialized outer logs/value/diagnostic cap. */
|
|
maxOutputBytes?: number
|
|
/** Remote worker old-generation heap cap in MiB. */
|
|
maxOldGenerationSizeMb?: number
|
|
/** Largest decoded bridge frame, including binding traffic. */
|
|
maxFrameBytes?: number
|
|
/** Remote process-group TERM-to-KILL grace. */
|
|
killGraceMs?: number
|
|
}
|
|
|
|
type ResolvedConfig = Required<Config>
|
|
|
|
interface LiveRun {
|
|
settle(failure: CodeRunFailure): void
|
|
finished: Promise<void>
|
|
}
|
|
|
|
interface CallMessage {
|
|
type: 'call'
|
|
id: number
|
|
global: string
|
|
name: string
|
|
args: WorkerJsonWire
|
|
}
|
|
|
|
interface LogMessage {
|
|
type: 'log'
|
|
text: string
|
|
}
|
|
|
|
interface DoneMessage {
|
|
type: 'done'
|
|
value?: WorkerJsonWire
|
|
error?: CodeRunFailure
|
|
}
|
|
|
|
type RunnerMessage = CallMessage | LogMessage | DoneMessage | { type: 'output-limit' }
|
|
|
|
const STRIP_WRAP = { prefix: 'async function __dsh_program__() {\n', suffix: '\n}' } as const
|
|
const MIN_OUTPUT_BYTES = 4
|
|
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
/* jscpd:ignore-start -- Backends enforce the same injected-global vocabulary without coupling lifecycle implementations. */
|
|
const RESERVED_WORDS = new Set([
|
|
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
|
|
'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in',
|
|
'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
|
|
'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package',
|
|
'private', 'protected', 'public', 'arguments', 'eval',
|
|
])
|
|
const RESERVED_ERROR_PROPERTIES = new Set(['name', 'message', 'stack'])
|
|
/* jscpd:ignore-end */
|
|
const FAILURE_KINDS = new Set<CodeRunFailure['kind']>([
|
|
'exception', 'timeout', 'abort', 'worker-exit', 'invalid-output', 'output-limit',
|
|
])
|
|
|
|
function messageOf(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error)
|
|
}
|
|
|
|
function parseRunnerMessage(raw: unknown): RunnerMessage | undefined {
|
|
if (typeof raw !== 'object' || raw === null) return undefined
|
|
const record = raw as Record<string, unknown>
|
|
if (record.type === 'output-limit') return { type: 'output-limit' }
|
|
if (record.type === 'log') return typeof record.text === 'string' ? { type: 'log', text: record.text } : undefined
|
|
if (record.type === 'call') {
|
|
if (!Number.isSafeInteger(record.id) || (record.id as number) < 1 || typeof record.global !== 'string' || typeof record.name !== 'string' || !Array.isArray(record.args)) return undefined
|
|
return { type: 'call', id: record.id as number, global: record.global, name: record.name, args: record.args as WorkerJsonWire }
|
|
}
|
|
if (record.type !== 'done') return undefined
|
|
if (record.error === undefined) {
|
|
return { type: 'done', ...record.value === undefined ? {} : { value: record.value as WorkerJsonWire } }
|
|
}
|
|
if (typeof record.error !== 'object' || record.error === null) return undefined
|
|
const error = record.error as Record<string, unknown>
|
|
if (typeof error.kind !== 'string' || !FAILURE_KINDS.has(error.kind as CodeRunFailure['kind']) || typeof error.message !== 'string') return undefined
|
|
return { type: 'done', error: { kind: error.kind as CodeRunFailure['kind'], message: error.message } }
|
|
}
|
|
|
|
/** E2B-backed runtime: host-side type stripping, remote worker execution, host binding dispatch. */
|
|
export class E2BCodeRuntime extends CodeRuntime {
|
|
static inject = ['e2b', 'subprocess']
|
|
|
|
static Config: z<Config> = z.object({
|
|
computeMs: z.number().default(60_000),
|
|
maxWallMs: z.number().default(600_000),
|
|
maxOutputBytes: z.number().default(67_108_864),
|
|
maxOldGenerationSizeMb: z.number().default(512),
|
|
maxFrameBytes: z.number().default(268_435_456),
|
|
killGraceMs: z.number().default(2_000),
|
|
})
|
|
|
|
readonly language = 'typescript'
|
|
readonly isolation = 'container'
|
|
|
|
private readonly config: ResolvedConfig
|
|
private readonly ready: Promise<{ node: string; runner: string }>
|
|
private readonly live = new Set<LiveRun>()
|
|
private readonly subprocess: E2BSubprocessService
|
|
private disposed = false
|
|
|
|
constructor(ctx: Context, config: Config) {
|
|
super(ctx)
|
|
if (!(ctx.subprocess instanceof E2BSubprocessService)) {
|
|
throw new Error('code-runtime-e2b requires @deepseek-ai/dsh-subprocess-e2b as ctx.subprocess')
|
|
}
|
|
this.subprocess = ctx.subprocess
|
|
this.config = config as ResolvedConfig
|
|
for (const [key, value] of Object.entries(this.config)) {
|
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
throw new Error(`code-runtime-e2b: config.${key} must be a positive safe integer`)
|
|
}
|
|
}
|
|
if (this.config.maxOutputBytes < MIN_OUTPUT_BYTES) {
|
|
throw new Error(`code-runtime-e2b: config.maxOutputBytes must be at least ${MIN_OUTPUT_BYTES}`)
|
|
}
|
|
if (this.config.maxWallMs > MAX_TIMER_DELAY_MS) {
|
|
throw new Error(`code-runtime-e2b: config.maxWallMs must be at most ${MAX_TIMER_DELAY_MS}`)
|
|
}
|
|
if (this.config.maxFrameBytes < this.config.maxOutputBytes) {
|
|
throw new Error('code-runtime-e2b: config.maxFrameBytes must be at least maxOutputBytes')
|
|
}
|
|
this.ready = this.prepare()
|
|
void this.ready.catch(() => {})
|
|
ctx.effect(() => () => this.teardown(), 'E2B code-runtime teardown')
|
|
}
|
|
|
|
/* jscpd:ignore-start -- Seam-level abort and type-strip results remain identical across execution substrates. */
|
|
/** Execute one type-stripped program in a fresh E2B worker process. */
|
|
async run(request: CodeRunRequest): Promise<CodeRunResult> {
|
|
if (this.disposed) throw new Error('code-runtime-e2b: run() after disposal')
|
|
const bindings = this.validateBindings(request)
|
|
if (request.signal?.aborted === true) {
|
|
return this.failure({ kind: 'abort', message: String(request.signal.reason) })
|
|
}
|
|
let code: string
|
|
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) {
|
|
return this.failure({ kind: 'exception', message: messageOf(error) })
|
|
}
|
|
let runtime: Awaited<typeof this.ready>
|
|
try {
|
|
runtime = await this.ready
|
|
} catch (error: unknown) {
|
|
return this.failure({ kind: 'worker-exit', message: `E2B runtime setup failed: ${messageOf(error)}` })
|
|
}
|
|
// Disposal can race the awaited remote setup after the pre-await check.
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
if (this.disposed) return this.failure({ kind: 'abort', message: 'runtime disposed' })
|
|
return await this.execute(request, code, bindings, runtime)
|
|
}
|
|
/* jscpd:ignore-end */
|
|
|
|
private async prepare(): Promise<{ node: string; runner: string }> {
|
|
const sandbox = await this.ctx.e2b.getSandbox()
|
|
const runner = posix.join(this.ctx.e2b.runtimeRoot, 'code-runtime-runner.mjs')
|
|
await sandbox.files.write([{ path: runner, data: CODE_RUNNER_SOURCE }])
|
|
await sandbox.commands.run(`chmod 600 -- ${quoteE2BShellArg(runner)}`)
|
|
const node = await resolveE2BExecutable(sandbox, 'node')
|
|
return { node, runner }
|
|
}
|
|
|
|
private failure(error: CodeRunFailure): CodeRunResult {
|
|
return new OutputLedger(this.config.maxOutputBytes).failure([], error)
|
|
}
|
|
|
|
/* jscpd:ignore-start -- Binding names have one seam contract while dispatch and teardown remain backend-owned. */
|
|
private validateBindings(request: CodeRunRequest): Map<string, CodeBindingNamespace> {
|
|
const bindings = new Map<string, CodeBindingNamespace>()
|
|
for (const namespace of request.bindings) {
|
|
if (!IDENTIFIER.test(namespace.global) || RESERVED_WORDS.has(namespace.global)) {
|
|
throw new Error(`code-runtime-e2b: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`)
|
|
}
|
|
if (namespace.global === 'console' || bindings.has(namespace.global)) {
|
|
throw new Error(`code-runtime-e2b: 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 === undefined) continue
|
|
if (!IDENTIFIER.test(descriptor.name) || RESERVED_WORDS.has(descriptor.name)) {
|
|
throw new Error(`code-runtime-e2b: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`)
|
|
}
|
|
if (descriptor.name === 'console' || bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) {
|
|
throw new Error(`code-runtime-e2b: duplicate injected global ${JSON.stringify(descriptor.name)}`)
|
|
}
|
|
if (descriptor.memberNameProperty.length === 0 || RESERVED_ERROR_PROPERTIES.has(descriptor.memberNameProperty)) {
|
|
throw new Error(`code-runtime-e2b: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`)
|
|
}
|
|
errorClassNames.add(descriptor.name)
|
|
}
|
|
return bindings
|
|
}
|
|
/* jscpd:ignore-end */
|
|
|
|
private async execute(
|
|
request: CodeRunRequest,
|
|
code: string,
|
|
bindings: Map<string, CodeBindingNamespace>,
|
|
runtime: { node: string; runner: string },
|
|
): Promise<CodeRunResult> {
|
|
const handle = this.subprocess.spawn({
|
|
argv: [runtime.node, runtime.runner],
|
|
cwd: this.ctx.e2b.cwd,
|
|
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: { maxBytes: this.config.maxOutputBytes } },
|
|
graceMs: this.config.killGraceMs,
|
|
...request.signal === undefined ? {} : { signal: request.signal },
|
|
env: {},
|
|
})
|
|
if (handle.stdin === undefined || handle.stdout === undefined) {
|
|
handle.terminate()
|
|
await Promise.allSettled([handle.done])
|
|
try {
|
|
await handle.waitForExit()
|
|
} catch (error: unknown) {
|
|
return this.failure({ kind: 'worker-exit', message: `E2B runtime cleanup failed: ${messageOf(error)}` })
|
|
}
|
|
return this.failure({ kind: 'worker-exit', message: 'E2B subprocess dropped a piped runtime stream' })
|
|
}
|
|
const stdin = handle.stdin
|
|
const stdout = handle.stdout
|
|
|
|
return new Promise<CodeRunResult>((resolve) => {
|
|
const output = new OutputLedger(this.config.maxOutputBytes)
|
|
const logs: string[] = []
|
|
const answered = new Set<number>()
|
|
const decoder = new E2BFrameDecoder(this.config.maxFrameBytes)
|
|
let settled = false
|
|
let finishResolve!: () => void
|
|
const finished = new Promise<void>((done) => { finishResolve = done })
|
|
const wallTimer: { current: NodeJS.Timeout | undefined } = { current: undefined }
|
|
const live: LiveRun = {
|
|
finished,
|
|
settle: (failure) => { finish(() => output.failure(logs, failure)) },
|
|
}
|
|
|
|
const finish = (result: CodeRunResult | (() => CodeRunResult)): void => {
|
|
if (settled) return
|
|
settled = true
|
|
clearTimeout(wallTimer.current)
|
|
request.signal?.removeEventListener('abort', onAbort)
|
|
this.live.delete(live)
|
|
void new Promise<void>((resume) => { setImmediate(resume) }).then(async () => {
|
|
handle.terminate()
|
|
await handle.done.catch(() => {})
|
|
let cleanupError: unknown
|
|
try {
|
|
await handle.waitForExit()
|
|
} catch (error: unknown) {
|
|
cleanupError = error
|
|
}
|
|
try {
|
|
decoder.finish()
|
|
} catch (error: unknown) {
|
|
result = output.failure(logs, { kind: 'worker-exit', message: messageOf(error) })
|
|
}
|
|
if (cleanupError !== undefined) {
|
|
result = output.failure(logs, { kind: 'worker-exit', message: `E2B runtime cleanup failed: ${messageOf(cleanupError)}` })
|
|
}
|
|
const final = typeof result === 'function' ? result() : result
|
|
finishResolve()
|
|
resolve(final)
|
|
})
|
|
}
|
|
|
|
const sendReply = (message: unknown): void => {
|
|
if (settled) return
|
|
stdin.write(encodeE2BFrame(message), (error?: Error | null) => {
|
|
if (error !== undefined && error !== null) {
|
|
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime bridge write failed: ${error.message}` }))
|
|
}
|
|
})
|
|
}
|
|
|
|
/* jscpd:ignore-start -- Host binding resolution mirrors worker semantics over a different transport. */
|
|
const onCall = (message: CallMessage): void => {
|
|
if (answered.has(message.id)) return
|
|
answered.add(message.id)
|
|
const functions = bindings.get(message.global)?.functions
|
|
const fn = functions !== undefined && Object.hasOwn(functions, message.name) ? functions[message.name] : undefined
|
|
if (typeof fn !== 'function') {
|
|
sendReply({ 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) {
|
|
sendReply({ type: 'reply', id: message.id, ok: false, message: 'binding arguments must be lossless JSON' })
|
|
return
|
|
}
|
|
void (async () => {
|
|
try {
|
|
const resolved = await fn(args)
|
|
let value: CodeJsonValue | undefined
|
|
try {
|
|
value = snapshotJsonValue(resolved)
|
|
} catch {
|
|
value = undefined
|
|
}
|
|
if (value === undefined) {
|
|
sendReply({ type: 'reply', id: message.id, ok: false, message: 'binding resolution must be lossless JSON' })
|
|
} else {
|
|
sendReply({ type: 'reply', id: message.id, ok: true, value: encodeWorkerJson(value) })
|
|
}
|
|
} catch (error: unknown) {
|
|
sendReply({ type: 'reply', id: message.id, ok: false, message: messageOf(error) })
|
|
}
|
|
})()
|
|
}
|
|
/* jscpd:ignore-end */
|
|
|
|
const onMessage = (raw: unknown): void => {
|
|
if (settled) return
|
|
const message = parseRunnerMessage(raw)
|
|
if (message === undefined) return
|
|
if (message.type === 'log') {
|
|
if (!output.admit(message.text, logs)) finish(output.limit([...logs, message.text]))
|
|
return
|
|
}
|
|
if (message.type === 'output-limit') {
|
|
finish(output.limit(logs))
|
|
return
|
|
}
|
|
if (message.type === 'call') {
|
|
onCall(message)
|
|
return
|
|
}
|
|
if (message.error !== undefined) {
|
|
finish(() => output.failure(logs, message.error as CodeRunFailure))
|
|
} else if (message.value === undefined) {
|
|
finish(() => output.success(logs))
|
|
} else {
|
|
const value = decodeWorkerJson(message.value)
|
|
if (value === undefined) finish(() => output.failure(logs, { kind: 'invalid-output', message: 'program completion must be lossless JSON' }))
|
|
else finish(() => output.success(logs, value))
|
|
}
|
|
}
|
|
|
|
stdout.on('data', (chunk: Buffer) => {
|
|
if (settled) return
|
|
try {
|
|
for (const frame of decoder.push(chunk.toString('utf8'))) onMessage(frame)
|
|
} catch (error: unknown) {
|
|
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime bridge failed: ${messageOf(error)}` }))
|
|
}
|
|
})
|
|
stdout.on('error', (error: Error) => {
|
|
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime stdout failed: ${error.message}` }))
|
|
})
|
|
stdin.on('error', (error: Error) => {
|
|
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime stdin failed: ${error.message}` }))
|
|
})
|
|
void handle.done.then(
|
|
() => {
|
|
if (!settled) {
|
|
const stderr = handle.collected.stderr?.readFrom(0).text.trim()
|
|
finish(() => output.failure(logs, { kind: 'worker-exit', message: stderr === undefined || stderr === '' ? 'E2B runtime exited before completing' : `E2B runtime exited before completing: ${stderr}` }))
|
|
}
|
|
},
|
|
(error: unknown) => {
|
|
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime spawn failed: ${messageOf(error)}` }))
|
|
},
|
|
)
|
|
|
|
const onAbort = (): void => {
|
|
finish(() => output.failure(logs, { kind: 'abort', message: String(request.signal?.reason) }))
|
|
}
|
|
request.signal?.addEventListener('abort', onAbort, { once: true })
|
|
wallTimer.current = setTimeout(() => {
|
|
finish(() => output.failure(logs, { kind: 'timeout', message: `wall-clock ceiling reached (${this.config.maxWallMs}ms)` }))
|
|
}, this.config.maxWallMs)
|
|
this.live.add(live)
|
|
if (request.signal?.aborted === true) {
|
|
onAbort()
|
|
return
|
|
}
|
|
sendReply({
|
|
type: 'boot',
|
|
code,
|
|
namespaces: [...bindings].map(([global, namespace]) => ({
|
|
global,
|
|
names: Object.keys(namespace.functions),
|
|
...namespace.errorClass === undefined ? {} : { errorClass: namespace.errorClass },
|
|
})),
|
|
computeMs: this.config.computeMs,
|
|
maxOutputBytes: this.config.maxOutputBytes,
|
|
maxOldGenerationSizeMb: this.config.maxOldGenerationSizeMb,
|
|
})
|
|
})
|
|
}
|
|
|
|
/* jscpd:ignore-start -- Code-runtime backends share the service lifecycle but own different child identities. */
|
|
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))
|
|
}
|
|
/* jscpd:ignore-end */
|
|
}
|
|
|
|
export default E2BCodeRuntime
|