fix(e2b): close proven provider boundary gaps
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 的默认基础模板。
|
||||
|
||||
## 生命周期与所有权
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user