fix(e2b): close proven provider boundary gaps

This commit is contained in:
Tianyi Cui
2026-07-29 22:04:55 +08:00
parent 917a7493f7
commit 677541d123
16 changed files with 167 additions and 70 deletions

View File

@@ -29,7 +29,7 @@ export async function readRemoteEnvironment(sandbox: Sandbox, signal?: AbortSign
// TODO(e2b-replace-environment): Remove this ambient probe when E2B can start
// a command with a replacement environment instead of merged overrides.
const result = await sandbox.commands.run(
'set -o pipefail; printf \'%s\' "$PWD" | base64 -w 0; printf \'\\n\'; env -0 | base64 -w 0',
'set -o pipefail; dsh_e2b_passwd="$(getent passwd "$(id -u)")"; IFS=: read -r _ _ _ _ _ dsh_e2b_home _ <<<"$dsh_e2b_passwd"; test -n "$dsh_e2b_home" -a -d "$dsh_e2b_home"; printf \'%s\' "$dsh_e2b_home" | base64 -w 0; printf \'\\n\'; env -0 | base64 -w 0',
{ envs: e2bControlEnvs(), ...(signal === undefined ? {} : { signal }) },
)
const lines = result.stdout.trim().split('\n')

View File

@@ -1,4 +1,7 @@
/** Package-owned invariant companion for `@deepseek-ai/dsh-subprocess-e2b`. */
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-subprocess-e2b`.
* @module @deepseek-ai/dsh-subprocess-e2b/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'

View File

@@ -104,7 +104,6 @@ function commandText(spec: SubprocessSpawnSpec, paths: RemotePaths): string {
: `2> >(${encoder} >&2 2>/dev/null)`
const inner = [
'set +e',
'umask 077',
'dsh_e2b_env_bin=$1',
'dsh_e2b_node=$2',
'dsh_e2b_ps=$3',
@@ -520,6 +519,8 @@ export class E2BSubprocessHandle implements SubprocessHandle {
() => true,
)
while (true) {
// TODO(e2b-publication-cancel): Join cancellation to the existing
// termination transaction before aborting an in-flight SDK file read.
const raw = await sandbox.files.read(this.paths.pid)
const value = raw.trim()
if (value.length > 0) {
@@ -543,6 +544,8 @@ export class E2BSubprocessHandle implements SubprocessHandle {
result => ({ kind: 'result', result }),
(error: unknown) => ({ kind: 'error', error }),
)
const hasPipeOutput = this.spec.stdio.stdout === 'pipe' || this.spec.stdio.stderr === 'pipe'
let completed = hasPipeOutput ? await settlement : undefined
while (true) {
const rawStatus = (await sandbox.files.read(this.paths.status)).trim()
if (rawStatus.length > 0) {
@@ -550,19 +553,19 @@ export class E2BSubprocessHandle implements SubprocessHandle {
if (!/^(?:0|[1-9][0-9]*)$/.test(rawStatus) || !Number.isSafeInteger(exitCode) || exitCode > 255) {
throw new Error(`subprocess-e2b: remote wrapper published invalid exit code ${JSON.stringify(rawStatus)}`)
}
if (this.spec.stdio.stdout === 'pipe' || this.spec.stdio.stderr === 'pipe') {
return this.commandOutcome(await settlement, exitCode)
}
const completed = await withinMs(settlement, this.spec.graceMs)
if (completed !== undefined) return this.commandOutcome(completed, exitCode)
const drained = await withinMs(settlement, this.spec.graceMs)
if (drained !== undefined) return this.commandOutcome(drained, exitCode)
this.outputDrainExpired = true
this.stdoutReader?.invalidateSpill()
this.stderrReader?.invalidateSpill()
await handle.disconnect()
return { exitCode, signal: null }
}
const completed = await Promise.race([settlement, waitTick().then(() => undefined)])
if (completed !== undefined) return this.commandOutcome(completed)
// TODO(e2b-status-watch): Replace collect/inherit polling when E2B can
// observe direct-command exit independently of descendant-held output.
completed = await Promise.race([settlement, waitTick().then(() => undefined)])
}
}

View File

@@ -345,6 +345,9 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
private topLevelExited = false
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(
@@ -364,17 +367,56 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
// TODO(e2b-pgid-identity): Replace retained numeric PTY/session ids when E2B
// exposes identity-bound input, foreground-signal, and cleanup operations.
/** @inheritdoc */
async write(data: string): Promise<void> {
if (this.topLevelExited) throw new Error('terminal process has exited')
await this.sandbox.pty.sendInput(this.pid, Buffer.from(data, 'utf8'))
write(data: string): Promise<void> {
return this.trackOperation(async (signal) => {
if (this.topLevelExited) throw new Error('terminal process has exited')
await this.sandbox.pty.sendInput(this.pid, Buffer.from(data, 'utf8'), { signal })
})
}
/** @inheritdoc */
async inspectForeground(): Promise<SubprocessTerminalForeground | undefined> {
inspectForeground(): Promise<SubprocessTerminalForeground | undefined> {
return this.trackOperation(signal => this.inspectForegroundOnce(signal))
}
/** @inheritdoc */
signalForeground(signal: SubprocessTerminalSignal): Promise<number> {
return this.trackOperation(async (operationSignal) => {
const foreground = await this.inspectForegroundOnce(operationSignal)
if (foreground === undefined) {
throw new Error(`subprocess-e2b: cannot resolve foreground process group for terminal ${this.pid}`)
}
if (signal === 'SIGKILL' && foreground.processGroupId === this.pid) {
throw new Error('refusing to SIGKILL the terminal shell; terminate the terminal session instead')
}
await this.sandbox.commands.run(
`kill -${signal.slice(3)} -- -${foreground.processGroupId}`,
commandOpts(this.controlEnvs, operationSignal),
)
return foreground.processGroupId
})
}
/** @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
void cleanup.catch((_cleanupFailure: unknown) => {
this.cleanup = undefined
})
return cleanup
}
private async inspectForegroundOnce(
signal: AbortSignal,
): Promise<SubprocessTerminalForeground | undefined> {
try {
const result = await this.sandbox.commands.run(
`ps -o tpgid= -p ${this.pid}`,
commandOpts(this.controlEnvs),
commandOpts(this.controlEnvs, signal),
)
return {
processGroupId: parsePositiveId(
@@ -391,31 +433,20 @@ export class E2BTerminalHandle implements SubprocessTerminalHandle {
}
}
/** @inheritdoc */
async signalForeground(signal: SubprocessTerminalSignal): Promise<number> {
const foreground = await this.inspectForeground()
if (foreground === undefined) {
throw new Error(`subprocess-e2b: cannot resolve foreground process group for terminal ${this.pid}`)
}
if (signal === 'SIGKILL' && foreground.processGroupId === this.pid) {
throw new Error('refusing to SIGKILL the terminal shell; terminate the terminal session instead')
}
await this.sandbox.commands.run(
`kill -${signal.slice(3)} -- -${foreground.processGroupId}`,
commandOpts(this.controlEnvs),
private trackOperation<T>(operation: (signal: AbortSignal) => Promise<T>): Promise<T> {
if (this.terminating) return Promise.reject(new Error('subprocess-e2b: terminal is terminating'))
const pending = operation(this.operationController.signal)
this.operations.add(pending)
void pending.then(
() => { this.operations.delete(pending) },
() => { this.operations.delete(pending) },
)
return foreground.processGroupId
return pending
}
/** @inheritdoc */
terminate(): Promise<void> {
if (this.cleanup !== undefined) return this.cleanup
const cleanup = this.closeOnce()
this.cleanup = cleanup
void cleanup.catch((_cleanupFailure: unknown) => {
this.cleanup = undefined
})
return cleanup
private async closeAfterOperations(): Promise<void> {
if (this.operations.size > 0) await Promise.allSettled(this.operations)
await this.closeOnce()
}
private async waitForCommand(): Promise<SubprocessOutcome> {