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

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/e2b/e2b/README.md
README.md: 402903184934903eceb36a04d670e4490879ac65
README.zh.md: 1fec6ab66858f04a47c8a2a7cb5f6a8907ab6cac
README.md: 6881556dc18497956966aff74adc085d8e8620d3
README.zh.md: aea13f341f903e6c47b7484f477bf54cebf101c6

View File

@@ -24,7 +24,7 @@ Shared lifecycle owner for one E2B sandbox. The filesystem and subprocess adapte
`apiKey` is optional and otherwise reads `E2B_API_KEY`; the key configures the host SDK connection and is never installed in the sandbox. `cwd` defaults to `/home/user/workspace` and must be an absolute POSIX path. `timeoutMs` defaults to five minutes. `onTimeout` is `pause` by default and accepts `pause | kill`; it applies only when this service creates a sandbox. Pause-on-timeout enables E2B auto-resume so the shared SDK handle wakes on its next operation. `onDispose` defaults to `kill` and accepts `kill | pause | leave`.
Set `sandboxId` to reconnect a running or paused sandbox instead of creating one. E2B resumes a paused sandbox during connect; `template` is creation-only and cannot accompany `sandboxId`. Omitting `template` uses E2B's default base template.
Set `sandboxId` to reconnect a running or paused sandbox instead of creating one. E2B resumes a paused sandbox during connect; `template` and `onTimeout` are creation-only and cannot accompany `sandboxId`. Omitting `template` uses E2B's default base template.
## Lifecycle and ownership

View File

@@ -24,7 +24,7 @@
`apiKey` 可省略;省略时读取 `E2B_API_KEY`。该密钥只配置宿主 SDK 连接,绝不会安装进沙箱。`cwd` 默认为 `/home/user/workspace`,并且必须是绝对 POSIX 路径。`timeoutMs` 默认为 5 分钟。`onTimeout` 默认为 `pause`,接受 `pause | kill`;它只在本服务创建沙箱时生效。超时时 pause 会启用 E2B 自动恢复,使共享 SDK 句柄在下一次操作时唤醒。`onDispose` 默认为 `kill`,接受 `kill | pause | leave`。
设置 `sandboxId` 可重新连接正在运行或已经暂停的沙箱,而不是创建新沙箱。连接时,E2B 会恢复已经暂停的沙箱;`template` 仅用于创建,不能与 `sandboxId` 同时使用。省略 `template` 时使用 E2B 的默认基础模板。
设置 `sandboxId` 可重新连接正在运行或已经暂停的沙箱,而不是创建新沙箱。连接时,E2B 会恢复已经暂停的沙箱;`template` 和 `onTimeout` 仅用于创建,不能与 `sandboxId` 同时使用。省略 `template` 时使用 E2B 的默认基础模板。
## 生命周期与所有权

View File

@@ -91,7 +91,6 @@ interface ResolvedConfig {
interface SchemaResolvedConfig extends Config {
cwd: string
timeoutMs: number
onTimeout: E2BTimeoutMode
onDispose: E2BDisposeMode
}
@@ -113,7 +112,7 @@ export class E2BSandboxService extends Service {
template: z.string(),
cwd: z.string().default('/home/user/workspace'),
timeoutMs: z.number().default(300_000),
onTimeout: z.union(['kill', 'pause'] as const).default('pause'),
onTimeout: z.union(['kill', 'pause'] as const),
onDispose: z.union(['kill', 'pause', 'leave'] as const).default('kill'),
})
@@ -121,17 +120,12 @@ export class E2BSandboxService extends Service {
readonly cwd: string
/** Remote directory reserved for adapter-owned process and terminal state. */
readonly runtimeRoot: string
/** Whether this service creates a sandbox rather than reconnecting one. */
readonly created: boolean
/** Configured action when a newly created sandbox reaches its lifetime. */
readonly timeoutMode: E2BTimeoutMode
/** Configured final sandbox disposition. */
readonly disposeMode: E2BDisposeMode
/** Sandbox id once E2B has created or resolved the remote runtime. */
readonly sandboxId: Promise<E2BSandboxId>
private readonly config: ResolvedConfig
private readonly ready: Promise<Sandbox>
private readonly created: boolean
private failedSetupSandbox: Sandbox | undefined
private disposed = false
@@ -144,17 +138,15 @@ export class E2BSandboxService extends Service {
apiKey: apiKey ?? '',
cwd: resolved.cwd,
timeoutMs: resolved.timeoutMs,
onTimeout: resolved.onTimeout,
onTimeout: config.onTimeout ?? 'pause',
onDispose: resolved.onDispose,
...(config.sandboxId !== undefined ? { sandboxId: config.sandboxId } : {}),
...(config.template !== undefined ? { template: config.template } : {}),
}
this.validate()
this.validate(config)
this.cwd = this.config.cwd
this.runtimeRoot = posix.join(this.cwd, '.dsh-e2b')
this.created = this.config.sandboxId === undefined
this.timeoutMode = this.config.onTimeout
this.disposeMode = this.config.onDispose
this.ready = this.open()
// A deployment may load the owner before any adapter uses it. Keep a
// failed eager connection observed; getSandbox() still returns the error.
@@ -218,7 +210,7 @@ export class E2BSandboxService extends Service {
return sandbox
}
private validate(): void {
private validate(input: Config): void {
if (this.config.apiKey.length === 0) {
throw new Error('dsh-e2b: configure apiKey or set E2B_API_KEY')
}
@@ -234,6 +226,9 @@ export class E2BSandboxService extends Service {
if (this.config.sandboxId !== undefined && this.config.template !== undefined) {
throw new Error('dsh-e2b: template applies only when creating; omit it when sandboxId reconnects')
}
if (this.config.sandboxId !== undefined && input.onTimeout !== undefined) {
throw new Error('dsh-e2b: onTimeout applies only when creating; omit it when sandboxId reconnects')
}
}
private async open(): Promise<Sandbox> {

View File

@@ -90,9 +90,6 @@ describe('E2BSandboxService', () => {
await expect(service.sandboxId).resolves.toBe(E2BSandboxId('sandbox-1'))
expect(service.cwd).toBe('/home/user/workspace')
expect(service.runtimeRoot).toBe('/home/user/workspace/.dsh-e2b')
expect(service.created).toBe(true)
expect(service.timeoutMode).toBe('pause')
expect(service.disposeMode).toBe('kill')
expect(sdk.create).toHaveBeenCalledWith({
apiKey: 'test-key',
timeoutMs: 300_000,
@@ -225,7 +222,6 @@ describe('E2BSandboxService', () => {
ctx.logger.error = ((error: unknown) => { errors.push(error) }) as typeof ctx.logger.error
const fiber = await ctx.plugin(E2BSandboxService, {
apiKey: 'test-key',
onTimeout: reconnect ? 'kill' : 'pause',
onDispose: 'pause',
...(reconnect ? { sandboxId: 'existing' } : {}),
})
@@ -248,7 +244,6 @@ describe('E2BSandboxService', () => {
})
await ctx.e2b.getSandbox()
expect(ctx.e2b.created).toBe(false)
expect(sdk.connect).toHaveBeenCalledWith('existing', { apiKey: 'test-key', timeoutMs: 90_000 })
expect(sdk.create).not.toHaveBeenCalled()
await fiber.dispose()
@@ -330,6 +325,7 @@ describe('E2BSandboxService', () => {
[{ apiKey: 'x', timeoutMs: 0 }, /positive finite/],
[{ apiKey: 'x', sandboxId: '' }, /sandboxId must be non-empty/],
[{ apiKey: 'x', sandboxId: 'one', template: 'two' }, /template applies only/],
[{ apiKey: 'x', sandboxId: 'one', onTimeout: 'kill' }, /onTimeout applies only/],
] as const)('fails self-contained configuration before opening E2B: %j', async (config, message) => {
vi.stubEnv('E2B_API_KEY', '')
const ctx = new Context()