refactor(e2b): group remote providers
This commit is contained in:
81 files changed
+171
-249
No files matched your search
@@ -0,0 +1,441 @@
|
||||
/** 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
|
||||
Reference in New Issue
Block a user