refactor(e2b): keep code runtime host-local
This commit is contained in:
+2
-2
@@ -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 .agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md
|
||||
2026-07-28-portable-execution-world-consumers.md: 57c3a8a8b211279593359b02def865644052f549
|
||||
2026-07-28-portable-execution-world-consumers.zh.md: 8231913c26f75361add7a28bc40f7723c9b18728
|
||||
2026-07-28-portable-execution-world-consumers.md: fcc30d553f07a28b44227f614f62c2027f8f3dd3
|
||||
2026-07-28-portable-execution-world-consumers.zh.md: 5e1e73aee7b96ffe1ae9862f72114a48152ad218
|
||||
+3
-3
@@ -8,7 +8,7 @@ English | [中文](2026-07-28-portable-execution-world-consumers.zh.md)
|
||||
|
||||
The filesystem and subprocess seams made file and ordinary process access replaceable, but PTY and LSP still reached host Node APIs directly. A remote execution provider therefore appeared to need separate PTY and LSP packages even though their domain behavior did not change. Those packages would be shallow adapters: each would duplicate an existing consumer merely to replace its file and process operations.
|
||||
|
||||
A remote coding world is useful only when file operations, commands, terminals, language servers, and model-written programs share one sandbox identity. Moving the complete harness into that sandbox would also entangle provider experimentation with plugin loading, credentials, model transport, session durability, supervision, and deployment.
|
||||
A remote coding world is useful only when file operations, commands, terminals, and language servers share one sandbox identity. Moving the complete harness into that sandbox would also entangle provider experimentation with plugin loading, credentials, model transport, session durability, supervision, and deployment.
|
||||
|
||||
Ordinary pipes do not cover one requirement. A persistent terminal needs PTY allocation, foreground-process-group inspection and signalling, and cleanup of the complete terminal session. Pretending those operations can be rebuilt in `dsh-pty-local` from an ordinary `spawn()` handle would either leak provider internals or weaken its lifecycle contract.
|
||||
|
||||
@@ -30,7 +30,7 @@ Generic consumers use that execution world:
|
||||
|
||||
The opt-in E2B realization has exactly three provider-specific packages under `packages/e2b/`: `dsh-e2b` creates one sandbox and deletes it on timeout or disposal, `dsh-fs-e2b` implements `ctx.fs`, and `dsh-subprocess-e2b` implements `ctx.subprocess` over E2B Commands, PTYs, and remote Linux process groups. The two adapters obtain the sole SDK handle from the owner and never create private sandboxes.
|
||||
|
||||
E2B owns the mutable filesystem, managed command and Bash processes, terminal allocation and terminal-session groups, language-server processes and source reads, subprocess Code Runtime processes, and adapter-private files under `.dsh-e2b`. The host owns Cordis and plugin objects, the agent loop, agent/session/goal state, session logs and persistence, LLM calls, prompts and tools, authority, skills, subagent orchestration, PTY buffers and readiness, LSP protocol state, Code Runtime program/binding/output policy, and E2B SDK/network buffers. The overlay neither uploads nor synchronizes the host workspace.
|
||||
E2B owns the mutable filesystem, managed command and Bash processes, terminal allocation and terminal-session groups, language-server processes and source reads, and adapter-private files under `.dsh-e2b`. The host owns Cordis and plugin objects, the agent loop, agent/session/goal state, session logs and persistence, LLM calls, prompts and tools, authority, skills, subagent orchestration, PTY buffers and readiness, LSP protocol state, the worker-backed Code Runtime and its program/binding/output policy, and E2B SDK/network buffers. The overlay neither uploads nor synchronizes the host workspace.
|
||||
|
||||
The adapters retain only substrate mechanics. Filesystem canonicalization crosses the SDK's decoded command transport as strict base64-encoded NUL framing; streamed reads leave byte ceilings with consumers. Subprocess command output and environment snapshots use ASCII/base64 where SDK chunk decoding would otherwise lose bytes, while private control shells isolate profiles and later launches blank discovered credential-shaped names. Process and terminal cleanup uses remote groups and proves quiescence before settlement.
|
||||
|
||||
@@ -38,7 +38,7 @@ Sandbox state is deliberately ephemeral: timeout and disposal delete the remote
|
||||
|
||||
## Verification
|
||||
|
||||
Focused package suites pin sandbox lifecycle, canonical path framing, filesystem metadata and atomic versions, subprocess publication/rollback, terminal text I/O and session cleanup, output limits, cancellation, disposal, and invariant registration. A credential-gated Loader composition exercises the same three-package provider through source imports and built exports, including FS/Bash visibility, hostile login profiles, byte-split UTF-8 output, process and terminal cleanup, LSP queries, Code Runtime bindings and cleanup, host-workspace isolation, and final sandbox deletion.
|
||||
Focused package suites pin sandbox lifecycle, canonical path framing, filesystem metadata and atomic versions, subprocess publication/rollback, terminal text I/O and session cleanup, output limits, cancellation, disposal, and invariant registration. A credential-gated Loader composition exercises the same three-package provider through source imports and built exports, including FS/Bash visibility, hostile login profiles, byte-split UTF-8 output, process and terminal cleanup, LSP queries, host-workspace isolation, and final sandbox deletion.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@ Status: implemented
|
||||
|
||||
文件系统与进程管理 seam 使文件访问和普通进程访问具备可替换性,但若干上层能力仍直接调用宿主 Node API。因此,即使领域行为没有变化,远程执行提供方看起来仍需要独立的 PTY、LSP 与代码运行时包(package)。这些包只会成为浅层适配器:每个包都仅为替换文件与进程操作而复制一个现有消费方。
|
||||
|
||||
只有文件操作、命令、终端、语言服务器和模型编写的程序共享同一个沙箱身份时,远程编码世界才有用。若把完整 harness 移入该沙箱,还会把提供方实验与插件加载、凭据、模型传输、会话持久性、监督和部署纠缠在一起。
|
||||
只有文件操作、命令、终端和语言服务器共享同一个沙箱身份时,远程编码世界才有用。若把完整 harness 移入该沙箱,还会把提供方实验与插件加载、凭据、模型传输、会话持久性、监督和部署纠缠在一起。
|
||||
|
||||
普通管道无法满足其中一项要求。持久终端需要分配 PTY、检查前台进程组并发送信号,以及清理完整的终端会话。如果假设可以在 `dsh-pty-local` 中基于普通 `spawn()` 句柄重建这些操作,最终不是泄漏提供方内部细节,就是削弱其生命周期契约。
|
||||
|
||||
@@ -33,7 +33,7 @@ Status: implemented
|
||||
|
||||
可选启用的 E2B 实现在 `packages/e2b/` 下恰好只有三个提供方专用包:`dsh-e2b` 创建一个沙箱,并在超时或资源释放时将其删除;`dsh-fs-e2b` 实现 `ctx.fs`;`dsh-subprocess-e2b` 基于 E2B Commands、PTY 和远程 Linux 进程组实现 `ctx.subprocess`。两个适配器都从所有者取得唯一的 SDK 句柄,绝不创建私有沙箱。
|
||||
|
||||
E2B 负责可变文件系统、受管命令与 Bash 进程、终端分配与终端会话组、语言服务器进程与源文件读取、子进程代码运行时进程,以及 `.dsh-e2b` 下的适配器私有文件。宿主负责 Cordis 与插件对象、agent loop(智能体循环)、agent(智能体)状态、会话状态与目标状态、会话日志与持久化、LLM(大语言模型)调用、提示词与工具、权限、skill(技能)、subagent 编排、PTY 缓冲区与就绪状态、LSP 协议状态、代码运行时程序/绑定/输出策略,以及 E2B SDK/网络缓冲区。该叠加层既不上传,也不同步宿主工作区。
|
||||
E2B 负责可变文件系统、受管命令与 Bash 进程、终端分配与终端会话组、语言服务器进程与源文件读取,以及 `.dsh-e2b` 下的适配器私有文件。宿主负责 Cordis 与插件对象、agent loop(智能体循环)、agent(智能体)状态、会话状态与目标状态、会话日志与持久化、LLM(大语言模型)调用、提示词与工具、权限、skill(技能)、subagent 编排、PTY 缓冲区与就绪状态、LSP 协议状态、基于 worker 的代码运行时及其程序/绑定/输出策略,以及 E2B SDK/网络缓冲区。该叠加层既不上传,也不同步宿主工作区。
|
||||
|
||||
适配器只保留执行基底机制。文件系统规范化以严格的 base64 加 NUL 分帧穿过 SDK 已解码的命令传输;流式读取把字节上限留给消费方执行。进程管理命令输出与环境快照采用 ASCII/base64,避免 SDK 分片解码丢失字节;私有控制 shell 隔离 profile,后续启动会把已发现且名称呈凭据特征的环境变量置空。进程与终端清理使用远程进程组,并在结算前证明完全停稳。
|
||||
|
||||
@@ -41,7 +41,7 @@ E2B 负责可变文件系统、受管命令与 Bash 进程、终端分配与终
|
||||
|
||||
## 验证
|
||||
|
||||
聚焦的包测试套件锁定了沙箱生命周期、规范化路径分帧、文件系统元数据与原子版本、进程管理发布/回滚、终端文本 I/O 与会话清理、输出上限、取消、资源释放和不变式注册。一项受凭据门控的 Loader 组合通过源代码导入与构建后导出运行同一套三包提供方组合,其中包括 FS/Bash 可见性、恶意登录 profile、跨字节边界拆分的 UTF-8 输出、进程与终端清理、LSP 查询、代码运行时绑定与清理、宿主工作区隔离,以及最终沙箱删除。
|
||||
聚焦的包测试套件锁定了沙箱生命周期、规范化路径分帧、文件系统元数据与原子版本、进程管理发布/回滚、终端文本 I/O 与会话清理、输出上限、取消、资源释放和不变式注册。一项受凭据门控的 Loader 组合通过源代码导入与构建后导出运行同一套三包提供方组合,其中包括 FS/Bash 可见性、恶意登录 profile、跨字节边界拆分的 UTF-8 输出、进程与终端清理、LSP 查询、宿主工作区隔离,以及最终沙箱删除。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# POC overlay: keep the advanced headless agent and model-facing tools, but
|
||||
# place its filesystem and process substrate in one short-lived E2B sandbox;
|
||||
# the generic Bash, PTY, LSP, and Code Runtime consumers compose above them.
|
||||
# the generic Bash, PTY, and LSP consumers compose above them.
|
||||
- id: base
|
||||
name: '@cordisjs/plugin-include'
|
||||
config:
|
||||
@@ -12,9 +12,6 @@
|
||||
- id: fs-local
|
||||
name: '@deepseek-ai/dsh-fs-local'
|
||||
disabled: true
|
||||
- id: code-runtime
|
||||
name: '@deepseek-ai/dsh-code-runtime-worker'
|
||||
disabled: true
|
||||
- insert:
|
||||
- id: e2b
|
||||
name: '@deepseek-ai/dsh-e2b'
|
||||
@@ -25,8 +22,6 @@
|
||||
name: '@deepseek-ai/dsh-subprocess-e2b'
|
||||
- id: fs-e2b
|
||||
name: '@deepseek-ai/dsh-fs-e2b'
|
||||
- id: code-runtime-subprocess
|
||||
name: '@deepseek-ai/dsh-code-runtime-subprocess'
|
||||
- id: sandbox-policy
|
||||
name: '@deepseek-ai/dsh-sandbox-policy'
|
||||
config:
|
||||
|
||||
@@ -3,7 +3,6 @@ import { resolve } from 'node:path'
|
||||
import { boot } from '@deepseek-ai/dsh-app-boot'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {} from '@deepseek-ai/dsh-code-runtime-subprocess'
|
||||
import type {} from '@deepseek-ai/dsh-fs-e2b'
|
||||
import type {} from '@deepseek-ai/dsh-bash-local'
|
||||
import type {} from '@deepseek-ai/dsh-lsp-local'
|
||||
@@ -175,52 +174,6 @@ try {
|
||||
const terminalTreeCleanup = stubbornProbe.stdout === 'gone'
|
||||
if (!terminalTreeCleanup) throw new Error(`E2B PTY left process ${stubbornPid} alive after close`)
|
||||
|
||||
const code = await ctx.codeRuntime.run({
|
||||
program: `
|
||||
console.log('remote-log 你好', 42)
|
||||
const doubled: number = await bridge.double({ value: 21 })
|
||||
let typed = false
|
||||
try {
|
||||
await bridge.fail({ reason: 'expected' })
|
||||
} catch (error) {
|
||||
typed = error instanceof BridgeError && (error as { member: string }).member === 'fail'
|
||||
}
|
||||
return { doubled, typed }
|
||||
`,
|
||||
bindings: [{
|
||||
global: 'bridge',
|
||||
errorClass: { name: 'BridgeError', memberNameProperty: 'member' },
|
||||
functions: {
|
||||
double: async (args) => {
|
||||
const value = (args as { value: number }).value
|
||||
return value * 2
|
||||
},
|
||||
fail: async () => { throw new Error('binding rejected') },
|
||||
},
|
||||
}],
|
||||
})
|
||||
const descendantPipe = await ctx.codeRuntime.run({
|
||||
program: `
|
||||
const childProcess = await import('node:child_process')
|
||||
const child = childProcess.spawn(
|
||||
process.execPath,
|
||||
['-e', 'setInterval(() => {}, 1000)', 'dsh-code-runtime-descendant'],
|
||||
{ stdio: ['ignore', 'inherit', 'inherit'] },
|
||||
)
|
||||
return child.pid > 0
|
||||
`,
|
||||
bindings: [],
|
||||
})
|
||||
const descendantProcesses = await sandbox.commands.list()
|
||||
const descendantCleanup = !descendantProcesses.some(processInfo =>
|
||||
JSON.stringify([processInfo.cmd, processInfo.args]).includes('dsh-code-runtime-descendant'),
|
||||
)
|
||||
if (!descendantCleanup) throw new Error('E2B Code Runtime left a pipe-holding descendant alive')
|
||||
const remoteProcesses = await (await ctx.e2b.getSandbox()).commands.list()
|
||||
const lingeringCodeRunners = remoteProcesses.filter(processInfo =>
|
||||
JSON.stringify([processInfo.cmd, processInfo.args]).includes('code-runtime-runner.mjs'),
|
||||
)
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
sandboxId: (await ctx.e2b.getSandbox()).sandboxId,
|
||||
bashRead: bashRead.stdout.text,
|
||||
@@ -237,10 +190,6 @@ try {
|
||||
treeCleanup: terminalTreeCleanup,
|
||||
scrollback: terminalScrollback.text,
|
||||
},
|
||||
code,
|
||||
descendantPipe,
|
||||
descendantCleanup,
|
||||
lingeringCodeRunners: lingeringCodeRunners.length,
|
||||
})}\n`)
|
||||
} finally {
|
||||
if (terminalId !== undefined) await ctx.pty.kill(owner, terminalId, 'fixture cleanup').catch(() => false)
|
||||
|
||||
@@ -52,12 +52,3 @@
|
||||
.ts: typescript
|
||||
shutdownTimeoutMs: 1000
|
||||
killGraceMs: 500
|
||||
|
||||
- id: code-runtime-subprocess
|
||||
name: '@deepseek-ai/dsh-code-runtime-subprocess'
|
||||
config:
|
||||
computeMs: 500
|
||||
maxWallMs: 15000
|
||||
maxOutputBytes: 4096
|
||||
maxOldGenerationSizeMb: 128
|
||||
killGraceMs: 500
|
||||
@@ -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/README.md
|
||||
README.md: 1fd0c9c2385f3169791a28f3ef50bb42792760af
|
||||
README.zh.md: f9f7d5ae8839ea7033a349ac7a0083a97780a21c
|
||||
README.md: a9a121d0cb13cd5e2148da8d754967359be06bab
|
||||
README.zh.md: c6500ebb2abce14fabf68b79ec20f5c23cf5d5f7
|
||||
@@ -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/README.md
|
||||
README.md: 3c85b2790d3414af73625e65732df58ff9fec566
|
||||
README.zh.md: a759539fc054dd68e75f3c7660ccc3b762985af5
|
||||
README.md: adc002649e44faefd2f6bd84b7b45fb9fc6fa41d
|
||||
README.zh.md: d941de8da2cb831a69a165c86a825def68c2b319
|
||||
@@ -10,6 +10,6 @@ An experimental provider-composition POC that places one filesystem/process exec
|
||||
| [`fs-e2b`](fs-e2b/README.md) (`@deepseek-ai/dsh-fs-e2b`) | `ctx.fs` | Implement the filesystem seam over E2B Filesystem APIs |
|
||||
| [`subprocess-e2b`](subprocess-e2b/README.md) (`@deepseek-ai/dsh-subprocess-e2b`) | `ctx.subprocess` | Implement executable lookup, managed process groups and stdio, remote spill files, and terminal sessions over E2B Commands and PTY APIs |
|
||||
|
||||
The existing [`dsh-bash-local`](../bash/bash-local/README.md), [`dsh-pty-local`](../pty/pty-local/README.md), [`dsh-lsp-local`](../lsp/lsp-local/README.md), and [`dsh-code-runtime-subprocess`](../code-runtime/code-runtime-subprocess/README.md) need no E2B-specific forks. They delegate every execution-world operation to `ctx.fs` and `ctx.subprocess`, so mounting the two E2B adapters places their mutable work in the same sandbox.
|
||||
The existing [`dsh-bash-local`](../bash/bash-local/README.md), [`dsh-pty-local`](../pty/pty-local/README.md), and [`dsh-lsp-local`](../lsp/lsp-local/README.md) need no E2B-specific forks. They delegate every execution-world operation to `ctx.fs` and `ctx.subprocess`, so mounting the two E2B adapters places their mutable work in the same sandbox. The worker-backed Code Runtime remains host-local and reaches that world only through the tools its programs call.
|
||||
|
||||
This boundary does not move the harness process, Cordis objects, model calls, agent/session state, session persistence, skills, higher-level protocol state, or E2B SDK buffers. The [portable execution-world decision](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md) owns both the generic composition and this POC boundary.
|
||||
@@ -10,6 +10,6 @@
|
||||
| [`fs-e2b`](fs-e2b/README.md)(`@deepseek-ai/dsh-fs-e2b`) | `ctx.fs` | 通过 E2B Filesystem API 实现文件系统 seam |
|
||||
| [`subprocess-e2b`](subprocess-e2b/README.md)(`@deepseek-ai/dsh-subprocess-e2b`) | `ctx.subprocess` | 通过 E2B Commands 与 PTY API 实现可执行文件查找、受管进程组与 stdio、远程 spill 文件及终端会话 |
|
||||
|
||||
现有的 [`dsh-bash-local`](../bash/bash-local/README.md)、[`dsh-pty-local`](../pty/pty-local/README.md)、[`dsh-lsp-local`](../lsp/lsp-local/README.md) 及 [`dsh-code-runtime-subprocess`](../code-runtime/code-runtime-subprocess/README.md) 无需 E2B 专用 fork。它们把执行环境中的所有操作委托给 `ctx.fs` 和 `ctx.subprocess`,因此挂载这两个 E2B 适配器后,它们执行的可变操作都发生在同一个沙箱内。
|
||||
现有的 [`dsh-bash-local`](../bash/bash-local/README.md)、[`dsh-pty-local`](../pty/pty-local/README.md) 和 [`dsh-lsp-local`](../lsp/lsp-local/README.md) 无需 E2B 专用 fork。它们把执行环境中的所有操作委托给 `ctx.fs` 和 `ctx.subprocess`,因此挂载这两个 E2B 适配器后,它们执行的可变操作都发生在同一个沙箱内。基于 worker 的代码运行时仍在宿主本地运行,只会通过其程序调用的工具触达该执行世界。
|
||||
|
||||
该边界不会迁移 harness 进程、Cordis 对象、模型调用、agent(智能体)/会话状态、会话持久化、skill(技能)、更高层协议状态或 E2B SDK 缓冲。[可移植执行世界决策](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md)同时界定通用组合和此 POC 边界。
|
||||
@@ -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: 7ade7c3d6522d8fa6d54d7011766238451b17c9a
|
||||
README.zh.md: b683f33d2211106cf422e780b2b37904b0c640ad
|
||||
README.md: 52f99178ce8c06e61c13ea9364769ac887db4410
|
||||
README.zh.md: 98ba7e79841f548ff6465fda4bc37170c17bc6e0
|
||||
@@ -38,7 +38,7 @@ No direct invalidation; this package does not contribute request tokens.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **This is not a whole-harness runtime** — Cordis services, agent/session state, session logs, LLM requests, skills, and SDK-side buffers stay in the host process.
|
||||
- **This is not a whole-harness runtime** — Cordis services, the worker-backed Code Runtime, agent/session state, session logs, LLM requests, skills, and SDK-side buffers stay in the host process.
|
||||
- **Sandbox state is ephemeral** — disposal and timeout delete the sandbox; reconnect, pause/leave retention, templates, volumes, and snapshots are outside this POC.
|
||||
- **No deployment platform is configured** — network policy, host-workspace synchronization, and sandbox discovery are outside this POC.
|
||||
- **`cwd` is a resolution convention, not containment** — adapters and commands can address other sandbox paths; E2B network access retains the base image's policy.
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
## 已知限制与延后工作
|
||||
|
||||
- **这不是完整的 harness 运行时**:Cordis 服务、agent(智能体)/会话状态、会话日志、LLM(大语言模型)请求、skill(技能)和 SDK 侧缓冲仍留在宿主进程中。
|
||||
- **这不是完整的 harness 运行时**:Cordis 服务、基于 worker 的代码运行时、agent(智能体)/会话状态、会话日志、LLM(大语言模型)请求、skill(技能)和 SDK 侧缓冲仍留在宿主进程中。
|
||||
- **沙箱状态是短暂的**:资源释放和超时都会删除沙箱;重新连接、pause/leave 保留、模板、卷和快照均不在本 POC 范围内。
|
||||
- **没有配置部署平台**:网络策略、宿主工作区同步和沙箱发现均不在本 POC 范围内。
|
||||
- **`cwd` 是解析约定,而不是包含边界**:适配器和命令可以访问沙箱中的其他路径;E2B 网络访问也继续采用基础镜像的策略。
|
||||
@@ -57,7 +57,7 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => {
|
||||
const ptyFiber = await ctx.plugin(PtyService)
|
||||
const subprocessFiber = await ctx.plugin(E2BSubprocessService)
|
||||
const node = await ctx.subprocess.resolveExecutable('node')
|
||||
const relativeNodePath = posix.relative(ctx.subprocess.cwd, posix.dirname(node)) || '.'
|
||||
const relativeNodePath = posix.relative(ctx.e2b.cwd, posix.dirname(node)) || '.'
|
||||
await expect(ctx.subprocess.resolveExecutable('node', { PATH: relativeNodePath })).resolves.toBe(node)
|
||||
await expect(sandbox.files.read(profileLeakPath)).rejects.toBeInstanceOf(FileNotFoundError)
|
||||
const environmentProbe = ctx.subprocess.spawn({
|
||||
@@ -117,7 +117,7 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => {
|
||||
}
|
||||
}, 70_000)
|
||||
|
||||
it('runs FS, Bash, PTY, LSP, and Code Runtime in one sandbox and deletes it', async () => {
|
||||
it('runs FS, Bash, PTY, and LSP in one sandbox and deletes it', async () => {
|
||||
const { stdout, stderr } = await runLoaderSmoke({
|
||||
label: 'E2B composition',
|
||||
tempDirPrefix: 'dsh-e2b-composition-',
|
||||
@@ -157,9 +157,6 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => {
|
||||
interrupted: { sessionStatus: { kind: 'running' } },
|
||||
treeCleanup: true,
|
||||
},
|
||||
descendantPipe: { value: true, logs: [] },
|
||||
descendantCleanup: true,
|
||||
lingeringCodeRunners: 0,
|
||||
})
|
||||
const terminalMotd = (output.terminal as { motd: string }).motd
|
||||
expect(terminalMotd.length).toBeGreaterThan(0)
|
||||
@@ -171,10 +168,6 @@ describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => {
|
||||
expect(['stdin_read', 'inferred_idle']).toContain(
|
||||
(output.terminal as { interrupted: { waitReason: string } }).interrupted.waitReason,
|
||||
)
|
||||
expect(output.code).toEqual({
|
||||
value: { doubled: 42, typed: true },
|
||||
logs: ['remote-log 你好 42'],
|
||||
})
|
||||
const apiKey = process.env.E2B_API_KEY
|
||||
if (apiKey === undefined) throw new Error('E2B_API_KEY disappeared during the live composition test')
|
||||
await expect(Sandbox.getInfo(String(output.sandboxId), { apiKey })).rejects.toBeInstanceOf(SandboxNotFoundError)
|
||||
|
||||
@@ -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/subprocess-e2b/README.md
|
||||
README.md: 3ddfe5bc47309ba56d3f8af7720fcbe71ec8a302
|
||||
README.zh.md: c4db7bf8bb8a0f4b2f147dd22868978be5d1fcf4
|
||||
README.md: 3bb9802c3afa41f3949fceeb73e76ea5cebce744
|
||||
README.zh.md: 520f0ea1cde18ed523ba80015d3d4700730aba7a
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
E2B implementation of the [`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.md) seam. It has no config: load [`@deepseek-ai/dsh-e2b`](../e2b/README.md) first, then this service in place of `dsh-subprocess-local`. Existing Bash, PTY, LSP, and subprocess Code Runtime consumers then execute in the shared remote sandbox without E2B-specific capability packages.
|
||||
E2B implementation of the [`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.md) seam. It has no config: load [`@deepseek-ai/dsh-e2b`](../e2b/README.md) first, then this service in place of `dsh-subprocess-local`. Existing Bash, PTY, and LSP consumers then execute in the shared remote sandbox without E2B-specific capability packages.
|
||||
|
||||
## Behavior
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
[`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.md) seam 的 E2B 实现。它没有配置:先加载 [`@deepseek-ai/dsh-e2b`](../e2b/README.md),再用本服务取代 `dsh-subprocess-local`。现有的 Bash、PTY、LSP 以及使用 subprocess 的 Code Runtime 消费方随后会在共享远程沙箱中执行,无需 E2B 专用的功能包(package)。
|
||||
[`@deepseek-ai/dsh-subprocess`](../../subprocess/subprocess/README.md) seam 的 E2B 实现。它没有配置:先加载 [`@deepseek-ai/dsh-e2b`](../e2b/README.md),再用本服务取代 `dsh-subprocess-local`。现有的 Bash、PTY 和 LSP 消费方随后会在共享远程沙箱中执行,无需 E2B 专用的功能包(package)。
|
||||
|
||||
## 行为
|
||||
|
||||
|
||||
@@ -31,17 +31,9 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
private readonly terminalSetups = new Map<Promise<void>, AbortController>()
|
||||
private disposing = false
|
||||
|
||||
/** @inheritdoc */
|
||||
readonly cwd: string
|
||||
|
||||
/** @inheritdoc */
|
||||
readonly runtimeRoot: string
|
||||
|
||||
/** Create the E2B subprocess service and bind its disposal policy. */
|
||||
constructor(ctx: Context) {
|
||||
super(ctx)
|
||||
this.cwd = ctx.e2b.cwd
|
||||
this.runtimeRoot = ctx.e2b.runtimeRoot
|
||||
ctx.effect(() => async () => {
|
||||
this.disposing = true
|
||||
for (const controller of this.terminalSetups.values()) {
|
||||
@@ -89,14 +81,14 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
const prefix = path === undefined ? '' : `PATH=${quoteE2BShellArg(path)} `
|
||||
const result = await sandbox.commands.run(
|
||||
`${prefix}command -v -- ${quoteE2BShellArg(command)}`,
|
||||
{ cwd: this.cwd, envs: e2bControlEnvs(), ...signalOpts(signal) },
|
||||
{ cwd: this.ctx.e2b.cwd, envs: e2bControlEnvs(), ...signalOpts(signal) },
|
||||
)
|
||||
signal?.throwIfAborted()
|
||||
const executable = result.stdout.trim()
|
||||
if (executable.includes('\n') || (!posix.isAbsolute(executable) && !executable.includes('/'))) {
|
||||
throw new Error(`subprocess-e2b: executable ${JSON.stringify(command)} did not resolve to one absolute path`)
|
||||
}
|
||||
return posix.resolve(this.cwd, executable)
|
||||
return posix.resolve(this.ctx.e2b.cwd, executable)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
@@ -138,7 +130,7 @@ export class E2BSubprocessService extends SubprocessService {
|
||||
}
|
||||
}
|
||||
spec.signal?.throwIfAborted()
|
||||
const stateDir = posix.join(this.runtimeRoot, 'terminals', randomUUID())
|
||||
const stateDir = posix.join(this.ctx.e2b.runtimeRoot, 'terminals', randomUUID())
|
||||
const setup = Promise.withResolvers<void>()
|
||||
const setupController = new AbortController()
|
||||
const setupSignal = spec.signal === undefined
|
||||
|
||||
@@ -775,10 +775,8 @@ describe('E2B subprocess terminal service', () => {
|
||||
return { ctx, fiber, fake }
|
||||
}
|
||||
|
||||
it('publishes execution-world coordinates and resolves remote executables', async () => {
|
||||
it('resolves remote executables', async () => {
|
||||
const { ctx, fake } = await service()
|
||||
expect(ctx.subprocess.cwd).toBe('/workspace')
|
||||
expect(ctx.subprocess.runtimeRoot).toBe('/workspace/.dsh-e2b')
|
||||
await expect(ctx.subprocess.resolveExecutable('/bin/bash')).resolves.toBe('/bin/bash')
|
||||
await expect(ctx.subprocess.resolveExecutable('node', { PATH: '/custom/bin' }, new AbortController().signal))
|
||||
.resolves.toBe('/usr/bin/node')
|
||||
|
||||
Reference in New Issue
Block a user