mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
refactor(e2b): narrow the sandbox POC
This commit is contained in:
@@ -106,7 +106,7 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
|
||||
/** @inheritdoc */
|
||||
spawn(spec: SubprocessSpawnSpec): SubprocessHandle {
|
||||
if (this.isDisposing()) throw new Error('subprocess-e2b: service is disposing')
|
||||
if (this.disposing) throw new Error('subprocess-e2b: service is disposing')
|
||||
const program = spec.argv[0]
|
||||
if (program === undefined || program.length === 0) {
|
||||
throw new Error('invalid argv: expected a non-empty program name at argv[0]')
|
||||
@@ -115,7 +115,7 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
throw new Error('subprocess-e2b: graceMs must be a positive finite number')
|
||||
}
|
||||
if (spec.signal?.aborted === true) {
|
||||
throw new Error(`aborted before spawn: ${String(spec.signal.reason ?? 'aborted')}`)
|
||||
throw new Error(`aborted before spawn: ${String(spec.signal.reason)}`)
|
||||
}
|
||||
const stateDir = posix.join(this.ctx.e2b.runtimeRoot, 'processes', randomUUID())
|
||||
const handle = new E2BSubprocessHandle(this.ctx.e2b, spec, stateDir)
|
||||
@@ -132,7 +132,7 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
|
||||
/** @inheritdoc */
|
||||
async spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle> {
|
||||
if (this.isDisposing()) throw new Error('subprocess-e2b: service is disposing')
|
||||
if (this.disposing) throw new Error('subprocess-e2b: service is disposing')
|
||||
const program = spec.argv[0]
|
||||
if (program === undefined || program.length === 0) {
|
||||
throw new Error('subprocess-e2b: terminal argv must contain a program')
|
||||
@@ -158,7 +158,8 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
(cleanup) => { this.failedTerminalSetupCleanups.add(cleanup) },
|
||||
)
|
||||
this.terminals.add(terminal)
|
||||
if (this.isDisposing()) {
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- Remote allocation yields to disposal.
|
||||
if (this.disposing) {
|
||||
await terminal.terminate()
|
||||
this.terminals.delete(terminal)
|
||||
throw new Error('subprocess-e2b: service disposed during terminal setup')
|
||||
@@ -176,10 +177,6 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
setup.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
private isDisposing(): boolean {
|
||||
return this.disposing
|
||||
}
|
||||
}
|
||||
|
||||
export default E2BSubprocessService
|
||||
|
||||
@@ -115,9 +115,6 @@ export class E2BOutputReader implements SubprocessOutputReader {
|
||||
|
||||
/** @inheritdoc */
|
||||
readFrom(fromByte: number): SubprocessOutputRead {
|
||||
if (!Number.isSafeInteger(fromByte) || fromByte < 0) {
|
||||
throw new Error('subprocess output offset must be a non-negative safe integer')
|
||||
}
|
||||
const retained = Buffer.concat(this.chunks, this.retainedBytes)
|
||||
const firstRetained = this.totalBytes - this.retainedBytes
|
||||
const lossy = fromByte < firstRetained
|
||||
|
||||
@@ -149,10 +149,6 @@ function commandOpts(
|
||||
return { envs: e2bControlEnvs(envs), ...(signal === undefined ? {} : { signal }) }
|
||||
}
|
||||
|
||||
function isAborted(signal: AbortSignal | undefined): boolean {
|
||||
return signal?.aborted === true
|
||||
}
|
||||
|
||||
function waitTick(signal?: AbortSignal): Promise<boolean> {
|
||||
if (signal?.aborted === true) return Promise.resolve(false)
|
||||
return new Promise<boolean>((resolve) => {
|
||||
@@ -173,7 +169,7 @@ const WAIT_ABORTED = Symbol('wait aborted')
|
||||
function waitWithSignal<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T | typeof WAIT_ABORTED> {
|
||||
if (signal === undefined) return promise
|
||||
if (signal.aborted) return Promise.resolve(WAIT_ABORTED)
|
||||
return new Promise<T | typeof WAIT_ABORTED>((resolve, reject) => {
|
||||
return new Promise<T | typeof WAIT_ABORTED>((resolve) => {
|
||||
const onAbort = (): void => { cleanup(); resolve(WAIT_ABORTED) }
|
||||
const cleanup = (): void => { signal.removeEventListener('abort', onAbort) }
|
||||
signal.addEventListener('abort', onAbort, { once: true })
|
||||
@@ -181,10 +177,7 @@ function waitWithSignal<T>(promise: Promise<T>, signal: AbortSignal | undefined)
|
||||
onAbort()
|
||||
return
|
||||
}
|
||||
void promise.then(
|
||||
(value) => { cleanup(); resolve(value) },
|
||||
(error: unknown) => { cleanup(); reject(asError(error)) },
|
||||
)
|
||||
void promise.then((value) => { cleanup(); resolve(value) })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -206,12 +199,9 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
private readonly paths: RemotePaths
|
||||
private controlEnvs: Record<string, string> = {}
|
||||
private remotePid = -1
|
||||
private commandHandle: CommandHandle | undefined
|
||||
private outputTransportError: Error | undefined
|
||||
private outputDrainExpired = false
|
||||
private stateDirectoryCreated = false
|
||||
private preparing = true
|
||||
private terminationStarted = false
|
||||
private quiescenceProven = false
|
||||
private terminationAttempt: Promise<void> | undefined
|
||||
private terminationFailure: Error | undefined
|
||||
@@ -265,7 +255,6 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
/** @inheritdoc */
|
||||
terminate(): void {
|
||||
if (this.quiescenceProven || this.terminationAttempt !== undefined) return
|
||||
this.terminationStarted = true
|
||||
this.terminationController.abort(new Error('subprocess-e2b: command terminated'))
|
||||
this.stdout?.destroy()
|
||||
this.stderr?.destroy()
|
||||
@@ -285,7 +274,7 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
async waitForExit(signal?: AbortSignal): Promise<boolean> {
|
||||
if (this.quiescenceProven) return true
|
||||
let handle: CommandHandle | undefined
|
||||
if (this.terminationStarted) {
|
||||
if (this.terminationController.signal.aborted) {
|
||||
const observed = await waitWithSignal(this.commandState.promise, signal)
|
||||
if (observed === WAIT_ABORTED) return false
|
||||
handle = observed
|
||||
@@ -295,22 +284,23 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
}
|
||||
if (this.remotePid <= 0) {
|
||||
const attempt = this.terminationAttempt
|
||||
if (attempt !== undefined && await waitWithSignal(attempt, signal) === WAIT_ABORTED) return false
|
||||
if (attempt !== undefined && await waitWithSignal(attempt.catch(() => undefined), signal) === WAIT_ABORTED) {
|
||||
return false
|
||||
}
|
||||
this.throwTerminationFailure()
|
||||
// Successful pre-publication termination records quiescence; its only other outcome is the failure above.
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const observed = await waitWithSignal(this.readyState.promise, signal)
|
||||
if (observed === WAIT_ABORTED) return false
|
||||
handle = observed
|
||||
} catch {
|
||||
handle = this.commandHandle
|
||||
if (handle === undefined) {
|
||||
this.markQuiescent()
|
||||
return true
|
||||
}
|
||||
const observed = await waitWithSignal(
|
||||
this.readyState.promise.catch(() => this.commandState.promise),
|
||||
signal,
|
||||
)
|
||||
if (observed === WAIT_ABORTED) return false
|
||||
handle = observed
|
||||
if (handle === undefined) {
|
||||
this.markQuiescent()
|
||||
return true
|
||||
}
|
||||
}
|
||||
this.throwTerminationFailure()
|
||||
@@ -318,7 +308,7 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
try {
|
||||
sandbox = await this.runtime.getSandbox()
|
||||
} catch (error: unknown) {
|
||||
if (isAborted(signal)) return false
|
||||
if (signal?.aborted === true) return false
|
||||
if (error instanceof SandboxNotFoundError) {
|
||||
this.markQuiescent()
|
||||
return true
|
||||
@@ -331,7 +321,7 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
if (!await waitTick(signal)) return false
|
||||
}
|
||||
this.throwTerminationFailure()
|
||||
if (isAborted(signal)) return false
|
||||
if (signal?.aborted === true) return false
|
||||
this.markQuiescent()
|
||||
return true
|
||||
}
|
||||
@@ -345,10 +335,11 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
|
||||
private async run(): Promise<SubprocessOutcome> {
|
||||
let sandbox: Sandbox | undefined
|
||||
let preparing = true
|
||||
try {
|
||||
sandbox = await this.runtime.getSandbox()
|
||||
await this.prepareState(sandbox)
|
||||
this.preparing = false
|
||||
preparing = false
|
||||
const handle = await sandbox.commands.run(
|
||||
commandText(this.spec, this.paths),
|
||||
{
|
||||
@@ -361,7 +352,6 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
onStderr: async (data) => { await this.dispatchOutput('stderr', data) },
|
||||
},
|
||||
)
|
||||
this.commandHandle = handle
|
||||
const completion = handle.wait()
|
||||
void completion.catch(() => {})
|
||||
if (!isValidProcessId(handle.pid)) {
|
||||
@@ -369,7 +359,6 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
try {
|
||||
await handle.kill()
|
||||
this.markQuiescent()
|
||||
this.commandHandle = undefined
|
||||
} catch (cleanupError: unknown) {
|
||||
this.terminationFailure = asError(cleanupError)
|
||||
this.commandState.resolve(handle)
|
||||
@@ -404,9 +393,7 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
await this.finalizeSpills(sandbox)
|
||||
return outcome
|
||||
} catch (error: unknown) {
|
||||
const canceledPreparation = this.preparing
|
||||
&& this.terminationStarted
|
||||
&& this.terminationController.signal.aborted
|
||||
const canceledPreparation = preparing && this.terminationController.signal.aborted
|
||||
let failure = await this.rollbackPublishedFailure(error)
|
||||
if (sandbox !== undefined && this.stateDirectoryCreated) {
|
||||
try {
|
||||
@@ -423,7 +410,6 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
if (canceledPreparation && failure === error) return { exitCode: null, signal: 'SIGTERM' }
|
||||
throw failure
|
||||
} finally {
|
||||
this.preparing = false
|
||||
this.spec.signal?.removeEventListener('abort', this.onAbort)
|
||||
this.stdout?.end()
|
||||
this.stderr?.end()
|
||||
@@ -583,7 +569,7 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
}
|
||||
|
||||
private async rollbackPublishedFailure(error: unknown): Promise<unknown> {
|
||||
if (this.remotePid <= 0 || this.commandHandle === undefined || this.quiescenceProven) return error
|
||||
if (this.remotePid <= 0 || this.quiescenceProven) return error
|
||||
this.terminate()
|
||||
try {
|
||||
await this.waitForExit()
|
||||
@@ -626,7 +612,6 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
if (!isValidProcessId(handle.pid) && this.remotePid <= 0) {
|
||||
await handle.kill()
|
||||
this.markQuiescent()
|
||||
this.commandHandle = undefined
|
||||
return
|
||||
}
|
||||
const sandbox = await this.runtime.getSandbox()
|
||||
@@ -730,7 +715,7 @@ export class E2BSubprocessHandle implements SubprocessHandle {
|
||||
const size = (reader as E2BOutputReader).size
|
||||
if (this.outputDrainExpired || size <= mode.maxBytes || size > mode.spill.maxBytes) {
|
||||
removals.push(sandbox.files.remove(path).catch((_adapterPrivateSpillRemovalFailure: unknown) => {
|
||||
// The command outcome is authoritative; a retained sandbox tolerates private residue.
|
||||
// The command outcome is authoritative; owner teardown bounds private residue.
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ const TERMINAL_RUNNER_SOURCE = [
|
||||
' exit 125',
|
||||
'fi',
|
||||
'printf \'%s\' "$dsh_output_marker"',
|
||||
"printf 'ready\\n' > \"$dsh_state/ready\"",
|
||||
'exec env -i -- "${dsh_env[@]}" "${dsh_argv[@]}"',
|
||||
'',
|
||||
].join('\n')
|
||||
@@ -51,7 +50,6 @@ interface TerminalPaths {
|
||||
environment: string
|
||||
argv: string
|
||||
outputMarker: string
|
||||
ready: string
|
||||
}
|
||||
|
||||
function signalOpts(signal: AbortSignal | undefined): { signal?: AbortSignal } {
|
||||
@@ -165,26 +163,6 @@ async function terminalSessionId(
|
||||
return parsePositiveId(result.stdout, `subprocess-e2b: cannot resolve process session for terminal ${pid}`)
|
||||
}
|
||||
|
||||
async function waitUntilReady(
|
||||
sandbox: Sandbox,
|
||||
paths: TerminalPaths,
|
||||
completion: Promise<CommandResult>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
const settled = completion.then(() => true, () => true)
|
||||
for (;;) {
|
||||
signal?.throwIfAborted()
|
||||
try {
|
||||
if ((await sandbox.files.read(paths.ready, signalOpts(signal))).trim() === 'ready') return
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof FileNotFoundError)) throw error
|
||||
}
|
||||
if (await Promise.race([settled, delay(POLL_MS).then(() => false)])) {
|
||||
throw new Error('subprocess-e2b: terminal exited before publishing readiness')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sessionProcessGroups(
|
||||
sandbox: Sandbox,
|
||||
sessionId: number,
|
||||
@@ -241,8 +219,13 @@ async function awaitSessionEmpty(
|
||||
const deadline = Date.now() + graceMs
|
||||
for (;;) {
|
||||
const groups = await sessionProcessGroups(sandbox, sessionId, envs)
|
||||
if (groups.length === 0 || Date.now() >= deadline) return groups
|
||||
if (kill) await signalGroups(sandbox, groups, 'KILL', envs)
|
||||
if (groups.length === 0) return groups
|
||||
if (kill) {
|
||||
await signalGroups(sandbox, groups, 'KILL', envs)
|
||||
if (Date.now() >= deadline) return await sessionProcessGroups(sandbox, sessionId, envs)
|
||||
} else if (Date.now() >= deadline) {
|
||||
return groups
|
||||
}
|
||||
await delay(Math.min(POLL_MS, Math.max(1, deadline - Date.now())))
|
||||
}
|
||||
}
|
||||
@@ -277,7 +260,6 @@ async function rollbackUnpublishedTerminal(
|
||||
groups = await awaitSessionEmpty(sandbox, sessionId, envs, graceMs)
|
||||
}
|
||||
if (groups.length > 0) {
|
||||
await signalGroups(sandbox, groups, 'KILL', envs)
|
||||
await awaitSessionEmpty(sandbox, sessionId, envs, graceMs, true)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
@@ -285,25 +267,13 @@ async function rollbackUnpublishedTerminal(
|
||||
}
|
||||
}
|
||||
// Completion can settle while any awaited provider cleanup above is running.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- Provider cleanup yields to completion.
|
||||
if (!topLevelExited) {
|
||||
if (validPid) {
|
||||
try {
|
||||
await sandbox.pty.kill(handle.pid)
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SandboxNotFoundError) return
|
||||
attemptFailures.push(asError(error))
|
||||
}
|
||||
}
|
||||
// The awaited PTY fallback can settle completion before the SDK fallback.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!topLevelExited) {
|
||||
try {
|
||||
await handle.kill()
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SandboxNotFoundError) return
|
||||
attemptFailures.push(asError(error))
|
||||
}
|
||||
try {
|
||||
await handle.kill()
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SandboxNotFoundError) return
|
||||
attemptFailures.push(asError(error))
|
||||
}
|
||||
await Promise.race([completion.catch(() => undefined), delay(graceMs)])
|
||||
}
|
||||
@@ -321,7 +291,7 @@ async function rollbackUnpublishedTerminal(
|
||||
}
|
||||
}
|
||||
// The bounded completion race above updates this callback-owned state.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- The callback mutates this after a race.
|
||||
if (!topLevelExited) {
|
||||
proofFailures.push(new Error(`subprocess-e2b: terminal setup rollback failed; surviving pid: ${handle.pid}`))
|
||||
}
|
||||
@@ -347,7 +317,6 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
|
||||
private cleanup: Promise<void> | undefined
|
||||
private readonly operationController = new AbortController()
|
||||
private readonly operations = new Set<Promise<unknown>>()
|
||||
private terminating = false
|
||||
private terminationSignal: NodeJS.Signals | null = null
|
||||
|
||||
constructor(
|
||||
@@ -400,7 +369,6 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
|
||||
/** @inheritdoc */
|
||||
terminate(): Promise<void> {
|
||||
if (this.cleanup !== undefined) return this.cleanup
|
||||
this.terminating = true
|
||||
this.operationController.abort(new Error('subprocess-e2b: terminal is terminating'))
|
||||
const cleanup = this.closeAfterOperations()
|
||||
this.cleanup = cleanup
|
||||
@@ -434,7 +402,9 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
|
||||
}
|
||||
|
||||
private trackOperation<T>(operation: (signal: AbortSignal) => Promise<T>): Promise<T> {
|
||||
if (this.terminating) return Promise.reject(new Error('subprocess-e2b: terminal is terminating'))
|
||||
if (this.operationController.signal.aborted) {
|
||||
return Promise.reject(new Error('subprocess-e2b: terminal is terminating'))
|
||||
}
|
||||
const pending = operation(this.operationController.signal)
|
||||
this.operations.add(pending)
|
||||
void pending.then(
|
||||
@@ -445,7 +415,7 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
|
||||
}
|
||||
|
||||
private async closeAfterOperations(): Promise<void> {
|
||||
if (this.operations.size > 0) await Promise.allSettled(this.operations)
|
||||
await Promise.allSettled(this.operations)
|
||||
await this.closeOnce()
|
||||
}
|
||||
|
||||
@@ -481,7 +451,7 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
|
||||
this.terminationSignal = 'SIGKILL'
|
||||
if (!this.topLevelExited) {
|
||||
try {
|
||||
await this.sandbox.pty.kill(this.pid)
|
||||
await this.handle.kill()
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SandboxNotFoundError) return
|
||||
throw error
|
||||
@@ -504,7 +474,7 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
|
||||
try {
|
||||
await this.sandbox.files.remove(this.stateDir)
|
||||
} catch (_adapterPrivateStateRemovalFailure) {
|
||||
// The terminal is quiescent; a retained sandbox tolerates private residue.
|
||||
// The terminal is quiescent; owner teardown bounds private residue.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,7 +501,6 @@ export async function spawnE2BTerminal(
|
||||
environment: posix.join(stateDir, 'environment'),
|
||||
argv: posix.join(stateDir, 'argv'),
|
||||
outputMarker: posix.join(stateDir, 'output-marker'),
|
||||
ready: posix.join(stateDir, 'ready'),
|
||||
}
|
||||
const outputMarker = Buffer.from(`dsh-e2b-bootstrap:${randomUUID()}`)
|
||||
const output = new PassThrough()
|
||||
@@ -577,7 +546,6 @@ export async function spawnE2BTerminal(
|
||||
}
|
||||
const command = `exec /bin/bash ${quoteE2BShellArg(paths.runner)} ${quoteE2BShellArg(stateDir)}\r`
|
||||
await sandbox.pty.sendInput(handle.pid, Buffer.from(command), signalOpts(spec.signal))
|
||||
await waitUntilReady(sandbox, paths, completion, spec.signal)
|
||||
await waitForBootstrapOutput(outputFilter.ready, completion, spec.signal)
|
||||
const sessionId = await terminalSessionId(sandbox, handle.pid, controlEnvs, spec.signal)
|
||||
return new E2BTerminalHandle(
|
||||
|
||||
Reference in New Issue
Block a user