Sfoglia il codice sorgente

Merge pull request #4036 from deepseek-harness/worktree/ptc-sandbox/remove-e2b

refactor(e2b): retire the provider experiment and preserve remote seams
Tianyi Cui 1 settimana fa
parent
commit
09a6fbc388
93 ha cambiato i file con 188 aggiunte e 8977 eliminazioni
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.i18n.yaml
  2. 6 10
      .agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md
  3. 6 10
      .agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml
  5. 4 6
      .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md
  6. 4 6
      .agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.i18n.yaml
  8. 1 1
      .agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.md
  9. 1 1
      .agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.zh.md
  10. 2 2
      .agents/notes/implemented/architecture/2026-09-05-workspace-files-service.i18n.yaml
  11. 3 3
      .agents/notes/implemented/architecture/2026-09-05-workspace-files-service.md
  12. 3 3
      .agents/notes/implemented/architecture/2026-09-05-workspace-files-service.zh.md
  13. 6 0
      .agents/notes/implemented/simplification/2026-09-11-remove-e2b-providers.i18n.yaml
  14. 51 0
      .agents/notes/implemented/simplification/2026-09-11-remove-e2b-providers.md
  15. 51 0
      .agents/notes/implemented/simplification/2026-09-11-remove-e2b-providers.zh.md
  16. 0 58
      .github/workflows/e2b-e2e.yml
  17. 0 1
      AGENTS.md
  18. 0 1
      THIRD_PARTY_NOTICES.md
  19. 2 2
      docs/capability-seams.i18n.yaml
  20. 2 12
      docs/capability-seams.md
  21. 2 12
      docs/capability-seams.zh.md
  22. 2 2
      docs/config-catalog.i18n.yaml
  23. 0 35
      docs/config-catalog.md
  24. 0 35
      docs/config-catalog.zh.md
  25. 2 2
      docs/module-graph.i18n.yaml
  26. 0 14
      docs/module-graph.md
  27. 0 14
      docs/module-graph.zh.md
  28. 2 2
      docs/subsystems/subprocess.i18n.yaml
  29. 0 17
      docs/subsystems/subprocess.md
  30. 0 17
      docs/subsystems/subprocess.zh.md
  31. 2 2
      packages/README.i18n.yaml
  32. 1 2
      packages/README.md
  33. 1 2
      packages/README.zh.md
  34. 0 6
      packages/e2b/README.i18n.yaml
  35. 0 50
      packages/e2b/README.md
  36. 0 50
      packages/e2b/README.zh.md
  37. 0 6
      packages/e2b/e2b/README.i18n.yaml
  38. 0 145
      packages/e2b/e2b/README.md
  39. 0 145
      packages/e2b/e2b/README.zh.md
  40. 0 56
      packages/e2b/e2b/package.json
  41. 0 26
      packages/e2b/e2b/src/api-url.ts
  42. 0 191
      packages/e2b/e2b/src/index.ts
  43. 0 186
      packages/e2b/e2b/tests/composition.e2e.ts
  44. 0 238
      packages/e2b/e2b/tests/e2b.spec.ts
  45. 0 75
      packages/e2b/e2b/tests/egress.spec.ts
  46. 0 213
      packages/e2b/e2b/tests/fixtures/composition/bin.ts
  47. 0 60
      packages/e2b/e2b/tests/fixtures/composition/cordis.yml
  48. 0 85
      packages/e2b/e2b/tests/fixtures/composition/fixture-lsp.mjs
  49. 0 30
      packages/e2b/e2b/tsconfig.json
  50. 0 6
      packages/e2b/fs-e2b/README.i18n.yaml
  51. 0 138
      packages/e2b/fs-e2b/README.md
  52. 0 138
      packages/e2b/fs-e2b/README.zh.md
  53. 0 39
      packages/e2b/fs-e2b/package.json
  54. 0 628
      packages/e2b/fs-e2b/src/index.ts
  55. 0 851
      packages/e2b/fs-e2b/tests/filesystem.spec.ts
  56. 0 22
      packages/e2b/fs-e2b/tsconfig.json
  57. 0 6
      packages/e2b/subprocess-e2b/README.i18n.yaml
  58. 0 169
      packages/e2b/subprocess-e2b/README.md
  59. 0 169
      packages/e2b/subprocess-e2b/README.zh.md
  60. 0 44
      packages/e2b/subprocess-e2b/package.json
  61. 0 104
      packages/e2b/subprocess-e2b/src/environment.ts
  62. 0 231
      packages/e2b/subprocess-e2b/src/index.ts
  63. 0 131
      packages/e2b/subprocess-e2b/src/output.ts
  64. 0 693
      packages/e2b/subprocess-e2b/src/process.ts
  65. 0 97
      packages/e2b/subprocess-e2b/src/remote.ts
  66. 0 567
      packages/e2b/subprocess-e2b/src/terminal.ts
  67. 0 1810
      packages/e2b/subprocess-e2b/tests/subprocess.spec.ts
  68. 0 942
      packages/e2b/subprocess-e2b/tests/terminal.spec.ts
  69. 0 30
      packages/e2b/subprocess-e2b/tsconfig.json
  70. 0 24
      packages/extensions/tool-cordis/src/api-catalog.ts
  71. 2 2
      packages/fs/README.i18n.yaml
  72. 2 3
      packages/fs/README.md
  73. 2 3
      packages/fs/README.zh.md
  74. 2 2
      packages/fs/fs-local/README.i18n.yaml
  75. 2 2
      packages/fs/fs-local/README.md
  76. 2 2
      packages/fs/fs-local/README.zh.md
  77. 2 2
      packages/fs/fs/README.i18n.yaml
  78. 2 2
      packages/fs/fs/README.md
  79. 2 2
      packages/fs/fs/README.zh.md
  80. 2 2
      packages/subprocess/subprocess/README.i18n.yaml
  81. 0 1
      packages/subprocess/subprocess/README.md
  82. 0 1
      packages/subprocess/subprocess/README.zh.md
  83. 2 2
      packages/util/http-proxy/README.i18n.yaml
  84. 2 4
      packages/util/http-proxy/README.md
  85. 2 4
      packages/util/http-proxy/README.zh.md
  86. 0 214
      pnpm-lock.yaml
  87. 0 27
      scripts/ci-workflow.spec.ts
  88. 0 1
      scripts/client-build-environment.client.spec.ts
  89. 0 2
      scripts/gen-cordis-catalog.ts
  90. 2 11
      scripts/gen-doc-graphs.ts
  91. 0 3
      scripts/verify-package-readme-model-experience.ts
  92. 0 3
      tsconfig.base.json
  93. 0 3
      tsconfig.host.json

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.i18n.yaml

@@ -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: 787fe341e58cc212c99e0f35f07eea8e83daf000
-2026-07-28-portable-execution-world-consumers.zh.md: a558a5af64437b8743e741ace4ccf27079501721
+2026-07-28-portable-execution-world-consumers.md: 3375a38d41724aee9b3f7591b014d6eea6979857
+2026-07-28-portable-execution-world-consumers.zh.md: 62d7338d237358b05c0c52f97720312474b74717

+ 6 - 10
.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md

@@ -26,19 +26,15 @@ Generic consumers use that execution world:
 - `dsh-lsp-stdio` reads and contains source through `ctx.fs`, resolves and launches language servers through `ctx.subprocess`, and carries provider-owned file URIs through initialization and result rendering. One provider-lifetime signal aborts filesystem and protocol work during disposal, including workspace lookup before queue ownership; its JSON-RPC, pooling, synchronization, and normalization stay unchanged.
 - `dsh-terminal-bash` maps persistent-shell semantics onto `ctx.subprocess.spawnTerminal()`. The local `node-pty` and process-inspection implementation moves into `dsh-subprocess-local`; another subprocess provider supplies the same primitive. `danger-full-access` needs no `ctx.sandbox`; a confined mode requires a same-world sandbox provider and fails before spawn when none is mounted. Prompt and silence evidence collected during asynchronous pre-write inspection is discarded when the provider write begins. Cancellation retains the send reservation while an in-flight write settles and then signals the foreground group, so late bytes or the signal cannot target a successor; an in-flight readiness poll cannot release that reservation, and a rejected write sends no signal. The absolute deadline remains armed throughout cancellation. A signal failure becomes terminal transport failure. Completion of a stale inspection resumes polling for the current send. Startup cancellation begins terminal rollback without waiting for a stalled readiness or signalling call. Close rejects new public signals and delegates provider-managed session quiescence to the handle's awaited termination operation.
 
-## E2B POC boundary
+## Remote provider ownership
 
-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.
+The [E2B provider removal](../simplification/2026-09-11-remove-e2b-providers.md) supersedes the E2B realization of this decision. The filesystem/subprocess agreement and asynchronous terminal contracts remain in force for remote implementations.
 
-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, 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.
-
-Sandbox state is deliberately ephemeral: timeout and disposal delete the remote files and unmanaged state. The POC adds no reconnect or pause/leave retention, session-persistence backend, template builder, volume, snapshot, network-policy layer, sandbox catalog, workspace synchronization, durable remote handles, or whole-harness execution.
+A remote provider owns mutable files, command and terminal processes, language-server processes, and provider-private runtime files. The host owns Cordis and plugin objects, the agent loop, agent/session/goal state, session logs and persistence, model transport, authority, skills, subagent orchestration, terminal readiness and LSP protocol state. Moving execution does not imply workspace synchronization or durable remote handles.
 
 ## 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, host-workspace isolation, and final sandbox deletion.
+The local filesystem, subprocess, terminal and LSP suites cover path identity, executable lookup, managed cleanup, output limits, cancellation and disposal. Terminal consumer tests retain delayed provider writes, foreground inspection and signalling to verify asynchronous ownership without requiring a remote service.
 
 ## Alternatives considered
 
@@ -60,7 +56,7 @@ Focused package suites pin sandbox lifecycle, canonical path framing, filesystem
 
 **Implement remote filesystem operations only through shell commands.** Rejected because that discards structured filesystem identity, errors, streaming, version guards, and atomic mutation semantics already consumed by the file tools.
 
-**Add a generic distributed-runtime abstraction or reconnect live handles.** Rejected because the existing capability seams carry the demonstrated contracts, while remote identity alone cannot reconstruct callbacks, pending promises, authority, protocol state, or output cursors. A new layer would speculate about persistence and synchronization beyond the POC.
+**Add a generic distributed-runtime abstraction or reconnect live handles.** Rejected because the existing capability seams carry the demonstrated contracts, while remote identity alone cannot reconstruct callbacks, pending promises, authority, protocol state, or output cursors. A new layer would speculate about persistence and synchronization beyond the demonstrated consumer contracts.
 
 ## Consequences
 
@@ -70,4 +66,4 @@ The fundamental interfaces are wider, and a filesystem/subprocess pair must agre
 
 The local implementation absorbs `node-pty` and platform process inspection because it owns local terminal mechanics. On supported Linux hosts, the user-systemd scope retains descendants that call `setsid` or reparent, while process inspection continues to own foreground attribution and synchronous fallback evidence. Other hosts use the observational teardown: disposal sweeps descendants before and after terminating the top-level shell, waits for exact PID-identity-fenced descendants retained during foreground inspection, and retains Linux session members that survive top-level exit. macOS cannot enumerate a POSIX session after its leader exits, so a child that reparents between inspection snapshots remains an explicit local-provider limitation rather than a reason to move process mechanics back into the PTY consumer.
 
-The E2B composition demonstrates that a shared sandbox owner plus filesystem and subprocess adapters are sufficient to move the mutable coding world off-host while leaving higher capabilities provider-neutral. Its POC limits remain explicit: the SDK retains complete command transport in host memory, remote startup cannot publish a PID synchronously, exact terminal stdin-wait and independent signal facts are unavailable, numeric PID/PGID operations are not identity-fenced, the initial environment probe cannot hide unknown sandbox-default secrets from already-running same-UID processes, and adapter artifacts remain until sandbox deletion. These are provider constraints, not justification for compatibility shims or more E2B packages.
+Remote implementations must preserve the existing consumer contracts or reject unsupported operations explicitly. Transport-specific buffering, identity and disconnect limits belong to the provider; they do not justify duplicate Bash, PTY or LSP implementations.

+ 6 - 10
.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md

@@ -26,19 +26,15 @@ Status: implemented
 - `dsh-lsp-stdio` 通过 `ctx.fs` 读取源文件并验证包含关系,通过 `ctx.subprocess` 解析和启动语言服务器,并让由提供方负责的文件 URI 贯穿初始化与结果渲染。一个提供方生命周期信号会在资源释放期间中止文件系统与协议操作,包括取得队列所有权之前的工作区查找;其 JSON-RPC、池化、同步和规范化保持不变。
 - `dsh-terminal-bash` 把持久 shell 语义映射到 `ctx.subprocess.spawnTerminal()`。本地 `node-pty` 与进程检查实现移入 `dsh-subprocess-local`;其他进程管理提供方则提供相同原语。`danger-full-access` 不需要 `ctx.sandbox`;受限模式要求同一执行世界中存在沙箱提供方,未挂载时会在 spawn 前失败。提供方开始写入时,系统会丢弃异步写入前检查期间收集的提示符与静默证据。取消会在在途写入结算期间保留发送预留,随后向前台进程组发送信号,因此延迟字节和该信号都无法落到后续发送;在途就绪检查无法释放该预留,写入被拒绝时也不会发送信号。绝对截止时间会在整个取消期间保持启用。信号发送失败会成为终结性传输失败。陈旧检查完成后,会针对当前发送恢复轮询。启动取消会立即开始终端回滚,而不等待停滞的就绪检查或信号发送调用。关闭操作会拒绝新的公开信号,并把由提供方管理的会话完全停稳委托给句柄上须等待的终止操作。
 
-## E2B POC 边界
+## 远程提供方的职责
 
-可选启用的 E2B 实现在 `packages/e2b/` 下恰好只有三个提供方专用包:`dsh-e2b` 创建一个沙箱,并在超时或资源释放时将其删除;`dsh-fs-e2b` 实现 `ctx.fs`;`dsh-subprocess-e2b` 基于 E2B Commands、PTY 和远程 Linux 进程组实现 `ctx.subprocess`。两个适配器都从所有者取得唯一的 SDK 句柄,绝不创建私有沙箱
+[E2B 提供方移除决策](../simplification/2026-09-11-remove-e2b-providers.zh.md)取代本决策中的 E2B 实现。文件系统/子进程约定与异步终端约定继续适用于远程实现
 
-E2B 负责可变文件系统、受管命令与 Bash 进程、终端分配与终端会话组、语言服务器进程与源文件读取,以及 `.dsh-e2b` 下的适配器私有文件。宿主负责 Cordis 与插件对象、agent loop(智能体循环)、agent 状态、会话状态与目标状态、会话日志与持久化、LLM(大语言模型)调用、提示词与工具、权限、skill(技能)、subagent 编排、PTY 缓冲区与就绪状态、LSP 协议状态,以及 E2B SDK/网络缓冲区。该叠加层既不上传,也不同步宿主工作区。
-
-适配器只保留执行基底机制。文件系统规范化以严格的 base64 加 NUL 分帧穿过 SDK 已解码的命令传输;流式读取把字节上限留给消费方执行。进程管理命令输出与环境快照采用 ASCII/base64,避免 SDK 分片解码丢失字节;私有控制 shell 隔离 profile,后续启动会把已发现且名称呈凭据特征的环境变量置空。进程与终端清理使用远程进程组,并在结算前证明完全停稳。
-
-沙箱状态有意保持短暂:超时与资源释放会删除远程文件和非托管状态。该 POC 不提供重新连接、pause/leave 保留、会话持久化后端、模板构建器、卷、快照、网络策略层、沙箱目录、工作区同步、持久远程句柄,也不会在其中运行整个 harness。
+远程提供方负责可变文件、命令与终端进程、语言服务器进程,以及提供方私有运行时文件。宿主负责 Cordis 与插件对象、智能体循环、智能体/会话/目标状态、会话日志与持久化、模型传输、权限、技能、子智能体编排、终端就绪判断和 LSP 协议状态。移动执行位置不意味着工作区同步或持久远程句柄。
 
 ## 验证
 
-聚焦的包测试套件锁定了沙箱生命周期、规范化路径分帧、文件系统元数据与原子版本、进程管理发布/回滚、终端文本 I/O 与会话清理、输出上限、取消、资源释放和不变式注册。一项受凭据门控的 Loader 组合通过源代码导入与构建后导出运行同一套三包提供方组合,其中包括 FS/Bash 可见性、恶意登录 profile、跨字节边界拆分的 UTF-8 输出、进程与终端清理、LSP 查询、宿主工作区隔离,以及最终沙箱删除
+本地文件系统、子进程、终端与 LSP 测试覆盖路径身份、可执行文件查找、受管清理、输出上限、取消与资源释放。终端消费方测试保留延迟的提供方写入、前台检查和信号发送,以便在无需远程服务时验证异步所有权。
 
 ## 考虑过的替代方案
 
@@ -60,7 +56,7 @@ E2B 负责可变文件系统、受管命令与 Bash 进程、终端分配与终
 
 **只通过 shell 命令实现远程文件系统操作。** 不予采纳,因为这会丢弃现有文件工具已消费的结构化文件系统身份、错误、流式输出、版本保护和原子变更语义。
 
-**新增通用分布式运行时抽象,或重新连接活跃句柄。** 不予采纳,因为现有能力 seam 已承载经证实的约定,而仅凭远程身份无法重建回调、待处理 promise、权限、协议状态或输出游标。新增一层只会推测 POC 边界之外的持久化与同步问题。
+**新增通用分布式运行时抽象,或重新连接活跃句柄。** 不予采纳,因为现有能力 seam 已承载经证实的约定,而仅凭远程身份无法重建回调、待处理 promise、权限、协议状态或输出游标。新增一层只会推测 已验证消费方约定之外的持久化与同步问题。
 
 ## 后果
 
@@ -70,4 +66,4 @@ E2B 负责可变文件系统、受管命令与 Bash 进程、终端分配与终
 
 本地实现承接 `node-pty` 和平台进程检查,因为它负责本地终端机制。在受支持的 Linux 宿主上,user-systemd scope 会保留调用 `setsid` 或发生 reparent 的后代,进程检查则继续负责前台归属与同步 fallback 证据。其他宿主使用观察型拆卸:dispose(资源释放)会在终止顶层 shell 前后清理后代进程,等待前台检查期间保留下来且受精确 PID 身份围栏保护的后代进程,并继续追踪在顶层进程退出后仍存活的 Linux 会话成员。macOS 无法在 POSIX 会话 leader 退出后枚举该会话,因此在两次检查快照之间重新设定父进程的子进程仍是明确的本地提供方限制,而不是把进程机制移回 PTY 消费方的理由。
 
-E2B 组合证明,共享沙箱所有者加上文件系统与进程管理适配器,就足以在保持上层能力与提供方无关的同时,把可变编码世界移出宿主。其 POC 限制仍明确在案:SDK 会把完整命令传输内容保留在宿主内存中;远程启动无法同步发布 PID;无法获得精确的终端 stdin 等待状态与独立信号事实;基于数值 PID/PGID 的操作没有身份围栏;初始环境探测无法向已在运行的同 UID 进程隐藏未知的沙箱默认 secret;适配器产物会一直保留到沙箱删除。这些是提供方限制,不是引入兼容性 shim 或更多 E2B 包的理由。
+远程实现必须保留现有消费方约定,或明确拒绝不支持的操作。传输特定的缓冲、身份与断线限制归提供方负责;这些限制不是复制 Bash、PTY 或 LSP 实现的理由。

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.i18n.yaml

@@ -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-08-27-outbound-proxy-policy.md
-2026-08-27-outbound-proxy-policy.md: 67927a9d1404e0b14c6e420cc5bea462be5c87ad
-2026-08-27-outbound-proxy-policy.zh.md: cf0b203a061522450a2a1b0c337a060b0c387684
+2026-08-27-outbound-proxy-policy.md: 015b6edd3f4153f29db4084c92e8e8da8cad70e2
+2026-08-27-outbound-proxy-policy.zh.md: c7bdcaee138ac41a55733d76da1063b25f9ba633

+ 4 - 6
.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.md

@@ -6,7 +6,7 @@ English | [中文](2026-08-27-outbound-proxy-policy.zh.md)
 
 ## Problem
 
-Node's built-in `fetch` ignores `HTTP_PROXY` and `HTTPS_PROXY`. Every other tool a developer runs — curl, git, npm, pip — honours them, so a user behind a proxy exports the variables once and expects everything to follow. The harness did not: `setGlobalDispatcher`, `ProxyAgent`, and `EnvHttpProxyAgent` appeared zero times across `packages/` and `apps/`, so the model request, every web search, `web_fetch`, MCP over HTTP, the OTLP exporter, and the E2B SDK all connected directly, silently, with no diagnostic anywhere.
+Node's built-in `fetch` ignores `HTTP_PROXY` and `HTTPS_PROXY`. Every other tool a developer runs — curl, git, npm, pip — honours them, so a user behind a proxy exports the variables once and expects everything to follow. The harness did not: `setGlobalDispatcher`, `ProxyAgent`, and `EnvHttpProxyAgent` appeared zero times across `packages/` and `apps/`, so the model request, every web search, `web_fetch`, MCP over HTTP, and the OTLP exporter all connected directly, silently, with no diagnostic anywhere.
 
 The repository had briefly had an answer and lost it without noticing. PR #971 set `NODE_USE_ENV_PROXY=1` in `bin/dsh`; eleven days later `bbb1b1cc38 cleanup: remove managed source installer` deleted that launcher wholesale, taking the flag with it. What survived was one sentence in `apps/cli/reference/README.md` telling the reader to set a variable that nothing consumed any more.
 
@@ -24,9 +24,7 @@ An earlier revision put it in a new `net/` package group, reasoning that dependi
 
 The plugin that revision shipped is gone with it. It let a composition declare the policy in `cordis.yml`, but no shipped bundle mounted it, so the launcher's path was the only reachable one — and its `Config` was the sole supplier of a configuration branch nothing else could reach.
 
-**Four functions, because the call sites converged rather than the package growing an export each.** An earlier revision exported six: a dispatcher factory, a `node:http` agent factory, a proxy-URL lookup, a policy accessor, an installer, and a child-environment builder. Each existed for one SDK's transport, which is how a transport-policy package turns into a catalogue of other packages' constraints. Review asked whether the call sites could converge instead; they could, and each removal took a whole shape with it. Telemetry stopped being routed at all, retiring the `node:http` factory. `web-fetch-http` builds its own pinning agent under an annotated exemption, retiring the dispatcher factory. E2B reads `route.proxy`, retiring the proxy-URL lookup.
-
-What remains is `installProxyFromEnvironment`, `proxyRouteFor`, `proxyEnvironmentForChild`, and `clearedProxyEnv` — one per way a caller can need the policy, none per SDK. Installation absorbed resolution and diagnostic reporting, which no caller needed apart: a resolved policy that is not installed routes nothing.
+**One operation per caller need.** `installProxyFromEnvironment`, `proxyRouteFor`, `proxyEnvironmentForChild`, and `clearedProxyEnv` cover installation, per-request routing, child inheritance and fixture isolation. SDK-specific factories would expose individual transport constraints through the shared API. `web-fetch-http` consumes the resolved route when constructing its pinning transport; the OTLP exporter remains direct. Installation includes resolution and diagnostic reporting because callers need one operation that resolves and installs routing.
 
 `proxyRouteFor` also closes a defect the old accessor made expressible. `web-fetch-http` read the policy to decide whether to pin, then read it again to build a transport; an unmount between the two returned a direct, unpinned agent for a URL the first read had cleared as proxied. A route carries both, so the branch and the request cannot disagree. Its dispatcher is the process-wide one, closed rather than destroyed on disposal, so a request already in flight when a policy is unmounted still finishes.
 
@@ -48,13 +46,13 @@ The child keeps the user's own values, and that is what once broke it. Node pars
 
 This accepts a documented seam. Such a context matches bypass entries by Node's rules, which differ from this package's in separators and IPv4-range support, and the flag exists only on Node 22.21+ and 24+.
 
-**Two SDKs do not reach `globalThis.fetch`, and reading their code said otherwise.** The audit first classified the OTLP exporter and the E2B SDK as covered, on a grep that found `globalThis.fetch` in `@opentelemetry/otlp-exporter-base`. That match is the *browser* transport; on Node the delegate selects `http-exporter-transport`, which posts through `node:http` — where a global dispatcher does not reach. E2B is a second shape again: it builds its own undici `Agent`/`ProxyAgent` and takes a `proxy` URL that it never reads from the environment. Both were measured direct. E2B is handed `route.proxy` from `proxyRouteFor`, the same call `web-fetch-http` makes. Telemetry is deliberately left direct, and that exclusion is the more interesting half.
+**SDK transports need independent verification.** The OTLP exporter selects `http-exporter-transport` on Node, which posts through `node:http` and bypasses the global fetch dispatcher; a `globalThis.fetch` reference in its browser implementation does not prove Node routing. The [E2B removal](../simplification/2026-09-11-remove-e2b-providers.md) retires a second SDK transport integration without changing this requirement.
 
 **Telemetry stays direct on purpose.** Routing it needs one of two things, and both cost more than the channel is worth. An `http.Agent` reads the environment through `proxyEnv`, which arrived in Node 22.21 and 24.5 — inside the engines range, so 22.19, 22.20, and 24.0–24.4 would stay direct regardless, and the proxy package would have to keep a `createNodeHttpAgent` export for a path that works on some runtimes. Replacing the transport with the SDK's `fetch` delegate covers every runtime, but that delegate has no compression, and the shipped `base` bundle enables gzip: a realistic OTLP batch measures 6.4x smaller with it. An attempt that refused `exporter.compression` instead broke every test that boots the shipped bundle, and one that gzipped at the serializer worked but put transport code in a telemetry plugin to keep it working.
 
 Weighed against that, telemetry is the one outbound channel whose loss costs the user nothing: no tool, no model request, and no session depends on it, and an export that cannot connect is already dropped silently. A user behind a mandatory proxy is left exactly where they were before this change rather than regressed. `egress.spec.ts` now asserts the exclusion — an SDK upgrade that moved the exporter onto `fetch` would start routing telemetry through a proxy silently, and that case is what makes it visible.
 
-**Every call site carries an egress test, because reading the code was not enough.** `egress.spec.ts` in each owning package drives that site's real code path at an unresolvable `.invalid` host through a fake proxy and asserts the proxy saw the request. Nine of them cover the search backends, pi-ai discovery, MCP over HTTP, E2B, a spawned child Node, a worker thread, and telemetry's exclusion. The gate below cannot see inside a dependency; these can, and they are what turns "an SDK changed its transport" from a silent regression into a failing test.
+**Every call site carries an egress test.** Each owning package's `egress.spec.ts` drives its actual transport through a fake proxy and checks the observed route. These tests cover search backends, pi-ai discovery, MCP over HTTP, child Node processes, worker threads and telemetry's direct-route exception. They detect dependency transport changes that a static call-site check cannot observe.
 
 **A gate keeps the defect from returning.** `verify-no-bare-dispatcher` parses the TypeScript AST — `scripts/AGENTS.md` requires syntax-aware discovery, and a line-wise regex missed both the `{ dispatcher }` shorthand this repository already uses and a `new Alias(...)` behind a renamed import. It rejects an undici agent construction and an explicit `dispatcher` option outside the owning package. `proxyRouteFor(url)` is the sanctioned replacement, and the one call site that genuinely owns its transport — `web-fetch-http`, pinning a request to addresses it validated — says so with a `proxy-exempt:` comment. The rule exists because `web-fetch-http`'s original `new Agent` was entirely reasonable when it was written — proxying simply did not exist yet, and nothing would have caught it.
 

+ 4 - 6
.agents/notes/implemented/architecture/2026-08-27-outbound-proxy-policy.zh.md

@@ -6,7 +6,7 @@ Status: implemented
 
 ## Problem
 
-Node 内置的 `fetch` 会忽略 `HTTP_PROXY` 与 `HTTPS_PROXY`。开发者运行的其他工具——curl、git、npm、pip——都遵循它们,所以代理后面的用户导出一次变量就期待一切随之生效。Harness 并没有:`setGlobalDispatcher`、`ProxyAgent` 与 `EnvHttpProxyAgent` 在 `packages/` 与 `apps/` 中出现次数为零,因此模型请求、每次 web 搜索、`web_fetch`、走 HTTP 的 MCP、OTLP 导出器与 E2B SDK 全部直连,且是静默的,任何地方都没有诊断。
+Node 内置的 `fetch` 会忽略 `HTTP_PROXY` 与 `HTTPS_PROXY`。开发者运行的其他工具——curl、git、npm、pip——都遵循它们,所以代理后面的用户导出一次变量就期待一切随之生效。Harness 并没有:`setGlobalDispatcher`、`ProxyAgent` 与 `EnvHttpProxyAgent` 在 `packages/` 与 `apps/` 中出现次数为零,因此模型请求、每次 web 搜索、`web_fetch`、走 HTTP 的 MCP 与 OTLP 导出器全部直连,且是静默的,任何地方都没有诊断。
 
 仓库曾短暂拥有过答案,又在无人察觉时弄丢了。PR #971 在 `bin/dsh` 里设置了 `NODE_USE_ENV_PROXY=1`;十一天后 `bbb1b1cc38 cleanup: remove managed source installer` 整体删除了那个启动器,把该标志一并带走。留下的只有 `apps/cli/reference/README.md` 里的一句话,让读者去设置一个已经无人消费的变量。
 
@@ -24,9 +24,7 @@ Node 内置的 `fetch` 会忽略 `HTTP_PROXY` 与 `HTTPS_PROXY`。开发者运
 
 那次修订一并引入的插件也随之删除。它让某个组合可以把策略写进 `cordis.yml`,但没有任何随附 bundle 挂载它,因此启动器那条路径是唯一可达的——而它的 `Config` 是那条配置分支唯一的供给方,别处无从到达。
 
-**四个函数——收敛的是调用方,而不是让本包为每个 SDK 各加一个导出。** 早先一版导出六个:dispatcher 工厂、`node:http` agent 工厂、代理 URL 查询、策略访问器、安装器与子进程环境构造器。每一个都为某个 SDK 的传输而存在,而这正是一个传输策略包退化成「别的包的约束目录」的过程。Review 问能不能反过来让调用方收敛;能,而且每删掉一个导出都带走了一整种写法。遥测不再被路由,`node:http` agent 工厂随之退场。`web-fetch-http` 在带注释的豁免下自建 pin agent,dispatcher 工厂随之退场。E2B 读 `route.proxy`,代理 URL 查询随之退场。
-
-剩下的是 `installProxyFromEnvironment`、`proxyRouteFor`、`proxyEnvironmentForChild` 与 `clearedProxyEnv`——按「调用方需要策略的方式」各一个,而不是按 SDK 各一个。安装吸收了解析与诊断上报,因为没有调用方需要把它们分开:解析出来却不安装的策略什么也路由不了。
+**每种调用需求对应一项操作。** `installProxyFromEnvironment`、`proxyRouteFor`、`proxyEnvironmentForChild` 与 `clearedProxyEnv` 分别负责安装、逐请求路由、子进程继承和 fixture 隔离。特定于 SDK 的工厂会通过共享 API 暴露各自的传输约束。`web-fetch-http` 在构造地址固定传输时使用已解析路由;OTLP 导出器保持直连。安装包含解析与诊断上报,因为调用方需要一项同时解析并安装路由的操作。
 
 `proxyRouteFor` 还堵掉了旧访问器让人写得出来的一个缺陷。`web-fetch-http` 先读策略决定是否 pin,再读一次去构造传输;两次读取之间发生卸载,就会为第一次读取已判定走代理的 URL 返回一个直连且未 pin 的 agent。路由把两者一起交出,分支与请求便无从分歧。它携带的是进程级 dispatcher,dispose 时是 close 而非 destroy,因此策略被卸载时已经发出的请求仍会跑完。
 
@@ -48,13 +46,13 @@ URL 层策略未受影响:仅 `http(s)`、禁止内嵌凭据、长度上限与
 
 这接受了一处已记录的接缝。此类上下文按 Node 自己的规则匹配绕过条目,其分隔符与 IPv4 区间支持与本包不同,且该标志仅存在于 Node 22.21+ 与 24+。
 
-**有两个 SDK 并不落到 `globalThis.fetch`,而读代码给出的答案是相反的。** 审计最初把 OTLP 导出器与 E2B SDK 判为已覆盖,依据是在 `@opentelemetry/otlp-exporter-base` 里 grep 到了 `globalThis.fetch`。那处命中属于**浏览器**传输;在 Node 上 delegate 选择的是 `http-exporter-transport`,它通过 `node:http` 投递——那里全局 dispatcher 触及不到。E2B 又是另一种形态:它自建 undici `Agent`/`ProxyAgent`,并接受一个自己从不从环境读取的 `proxy` URL。两者都实测为直连。E2B 接收 `proxyRouteFor` 给出的 `route.proxy`,与 `web-fetch-http` 调的是同一个函数。遥测则被有意保留为直连,而这个排除项才是更值得说的一半
+**SDK 传输需要独立验证。** OTLP 导出器在 Node 上选择 `http-exporter-transport`,通过 `node:http` 投递并绕过全局 fetch dispatcher;其浏览器实现中的 `globalThis.fetch` 引用不能证明 Node 路由。[E2B 移除决策](../simplification/2026-09-11-remove-e2b-providers.zh.md)撤下另一项 SDK 传输集成,但不改变这一要求
 
 **遥测的直连是有意为之。** 要让它走代理只有两条路,代价都超过这条通道本身的价值。`http.Agent` 通过 `proxyEnv` 读取环境,而该选项自 Node 22.21 与 24.5 才有——落在 engines 范围之内,因此 22.19、22.20 与 24.0–24.4 无论如何仍是直连,而代理包还得为一条只在部分运行时生效的路径保留 `createNodeHttpAgent` 导出。改用 SDK 的 `fetch` delegate 替换传输可以覆盖所有运行时,但该 delegate 没有压缩能力,而随附的 `base` bundle 启用了 gzip:实测一批真实规模的 OTLP 数据启用后体积只有 1/6.4。曾有一版转而在加载期拒绝 `exporter.compression`,结果凡是启动随附 bundle 的测试全部失败;另一版在 serializer 处 gzip 确实能跑通,但代价是把传输层代码塞进了遥测插件。
 
 与之相比,遥测是唯一一条丢失了对用户毫无代价的出网通道:没有任何工具、模型请求或会话依赖它,而连不上的导出本就被静默丢弃。处在强制代理后的用户,只是停留在本次改动之前的状态,而不是被弄坏。`egress.spec.ts` 现在断言这一排除——若某次 SDK 升级把导出器挪到 `fetch` 上,遥测就会开始静默走代理,而该用例正是让这件事暴露出来的东西。
 
-**每个出网点都配一份出网测试,因为读代码不够。** 各所属包中的 `egress.spec.ts` 驱动该点的真实代码路径,目标是无法解析的 `.invalid` 主机,穿过一个假代理,并断言代理确实收到了请求。九份测试覆盖搜索后端、pi-ai 发现、走 HTTP 的 MCP、E2B、派生的子 Node、worker 线程,以及遥测的排除。下面那条门禁看不进依赖内部;这些能,它们把「某个 SDK 换了传输」从静默回归变成失败的测试
+**每个出网点都配有出网测试。** 各所属包中的 `egress.spec.ts` 通过假代理驱动实际传输,并检查观察到的路由。这些测试覆盖搜索后端、pi-ai 发现、走 HTTP 的 MCP、子 Node 进程、worker 线程和遥测的直连例外。它们可发现静态调用点检查无法观察到的依赖传输变化
 
 **用门禁防止该缺陷复现。** `verify-no-bare-dispatcher` 解析 TypeScript AST——`scripts/AGENTS.md` 要求 source-ownership 门禁使用语法感知发现,而逐行正则漏掉了本仓库已在使用的 `{ dispatcher }` 简写,以及重命名导入后的 `new Alias(...)`。它在所属包之外拒绝 undici agent 构造与显式 `dispatcher` 选项。`proxyRouteFor(url)` 是受支持的替代;唯一一处确实自有传输的调用点——`web-fetch-http`,它把请求钉在已校验的地址上——用 `proxy-exempt:` 注释说明。这条规则之所以存在,是因为 `web-fetch-http` 里原本那行 `new Agent` 在写下时完全合理——那时根本还没有代理这回事,也没有任何机制会拦下它。
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.i18n.yaml

@@ -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-08-28-subprocess-native-containment.md
-2026-08-28-subprocess-native-containment.md: cf3507886f575893e19f72cde6e3f7bf8a1fbdbc
-2026-08-28-subprocess-native-containment.zh.md: 1e825262f786067d03c0cc65b4e59bfef1555741
+2026-08-28-subprocess-native-containment.md: 1b0fd162be77356001bcd9224bb2ee2889e6d6c0
+2026-08-28-subprocess-native-containment.zh.md: f33643f98a6ea6a90c0780432cbe11a35a868562

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.md

@@ -59,7 +59,7 @@ This note owns the current native-containment mechanism. It partially updates th
 - A keyless [`bash-startup-timeout`](../../../../snapshots/session/bash-startup-timeout/snapshot.yml) Session snapshot pins the model-facing timeout result. A Linux user-systemd fixture holds the launch request unconsumed at an input barrier and verifies cancellation plus range settlement.
 - Real Linux user-systemd tests run one ordinary and one `node-pty` `setsid`/reparent scenario through the production entry. They prove scope signalling and collection, bare executable lookup, escaped-descendant termination, range settlement, and unchanged PTY PID, session, controlling-terminal, foreground-input, `/dev/tty`, readiness, and startup-failure semantics.
 - Native Windows tests prove suspended creation, Job assignment before resume, inherited stdio, default descendant inheritance, direct result, termination, active-process zero, abnormal/disconnected runner cleanup, kill-on-close, and synchronous host-exit termination. Source, built, and Python packaged smokes enter the same runner core.
-- Public seam types, local and E2B providers, LSP and subagent consumers, shell fixtures, READMEs, the Cordis catalog, and the keyless subprocess API snapshot contain no ordinary PID; terminal PID remains.
+- Public seam types, local provider, LSP and subagent consumers, shell fixtures, READMEs, the Cordis catalog, and the keyless subprocess API snapshot contain no ordinary PID; terminal PID remains.
 
 ## Alternatives considered
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-28-subprocess-native-containment.zh.md

@@ -59,7 +59,7 @@ selector 是 per-spawn locator 或 sentinel,不是凭据或持久格式。Linu
 - 无需密钥的 [`bash-startup-timeout`](../../../../snapshots/session/bash-startup-timeout/snapshot.yml) Session 快照固定模型可见的超时结果。Linux user-systemd fixture 通过输入屏障保持启动请求未消费,并验证取消与 range settlement。
 - 真实 Linux user-systemd 测试会分别通过生产入口运行一条普通命令与一条 `node-pty` `setsid`/reparent 场景。它们证明 scope signalling 与 collection、裸可执行文件查找、逃逸后代终止、range settlement,以及不变的 PTY PID、session、控制终端、前台输入、`/dev/tty`、readiness 与 startup-failure 语义。
 - native Windows 测试证明 suspended creation、resume 前 Job assignment、继承 stdio、默认后代继承、direct result、termination、active-process zero、异常/disconnected runner cleanup、kill-on-close 与同步 host-exit termination。source、built 与 Python packaged 冒烟测试进入同一 runner core。
-- 公共 seam 类型、local 与 E2B provider、LSP 与 subagent 消费方、shell fixture、README、Cordis catalog 与 keyless subprocess API snapshot 都不包含普通 PID;terminal PID 保留。
+- 公共 seam 类型、local provider、LSP 与 subagent 消费方、shell fixture、README、Cordis catalog 与 keyless subprocess API snapshot 都不包含普通 PID;terminal PID 保留。
 
 ## Alternatives considered
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.i18n.yaml

@@ -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-09-05-workspace-files-service.md
-2026-09-05-workspace-files-service.md: b2dbdfc99388d8c2f18991a7704599d2d95f070b
-2026-09-05-workspace-files-service.zh.md: d860d220af50a24a6e0f40b640d0a8fb534c4741
+2026-09-05-workspace-files-service.md: 40157508a83abc61245845a1dbe4f1749e175563
+2026-09-05-workspace-files-service.zh.md: 51de0ad5b644f27e30557ea6b11ba62b8ef9a715

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.md

@@ -95,7 +95,7 @@ abstract readByteRange(target: FsTarget, range: { offset: number; length: number
 
 It returns the bytes at `[offset, offset + length)`, shorter when the file ends inside the window and empty when `offset` lies at or past the end. The window is the bound: a backend transfers at most `length` bytes beyond the prefix it skips to reach `offset` and never buffers the whole file, so the caller's cap on `length` is the guard against unbounded buffering, sitting beside `readBytes`'s bound rather than replacing it. The parameter order follows `readText`, `streamText`, and `listDir` — target, then the operation's own arguments, then an optional signal — rather than `readBytes`'s signal-in-the-middle form, which is the one exception in the class. Both `offset` and `length` are non-negative integers by precondition; the seam is a typed same-process boundary and validates nothing, and the Remote method validates at the wire.
 
-`fs-local` opens `createReadStream(targetKey, { start: offset, end: offset + length - 1 })` after the same regular-file stat as its other reads, returning an empty array for `length` 0 without opening a stream; `fs-sandbox` extends `LocalFileSystem` and inherits it. `fs-e2b` has an SDK that streams only from a file's start, so it skips `offset` bytes, copies `length` into the window, and cancels the stream the moment the window is full, transferring no more than the window beyond the skipped prefix; a stream that ends first is left to close. The four test doubles that extend `FileSystem` implement the method too.
+`fs-local` opens `createReadStream(targetKey, { start: offset, end: offset + length - 1 })` after the same regular-file stat as its other reads, returning an empty array for `length` 0 without opening a stream; `fs-sandbox` extends `LocalFileSystem` and inherits it. The four test doubles that extend `FileSystem` implement the method too.
 
 ### The Client `file` provider
 
@@ -133,7 +133,7 @@ The [resource model](2026-09-05-client-resource-model.md) owns `ctx.resources`,
 
 - Workspace file access belongs to the Host/Client faces of `api/workspace-files`; the Session Controller carries neither implementation, and compiler and runtime entries stay separate. Header-only Session scope lets ordinary, subagent, live, and cold Sessions resolve their own relative paths without an Agent lifecycle or parent fallback.
 - A file of any size opens: text by line page, anything by byte window, each costing one page or window of memory on the Host; complete reads instead enforce `maxFileBytes`; the cost is that a consumer assembles pages itself and that a single line above `maxBytes` has no page at all, because pages are cut by lines.
-- Every filesystem provider now offers a windowed raw read. `fs-e2b` pays for it by transferring the skipped prefix, since its SDK cannot seek; `fs-local` seeks.
+- Every filesystem provider offers windowed raw reads; `fs-local` seeks directly to the requested offset.
 - Paths on the wire are canonical: `absolutePath` and change frames spell a file with symlinks resolved. A follower binds to successful `stat.absolutePath`, so another spelling of the same file — a workspace root reached through a symlink — uses that canonical change key.
 - Change frames report the agent's own operations only. A file edited by the user's editor, a shell, or a subprocess raises no frame; an agent merely reading a file that something else changed does raise one, because the read observes a new version.
 - File-kind inspection precedes backend reads, and `list` reports kind before an outside position. A page's `version` may be one write behind its content, and a stalled `changes` consumer grows Host memory because a generation's queue is unbounded; each is a known trade-off recorded in the package README.
@@ -142,7 +142,7 @@ The [resource model](2026-09-05-client-resource-model.md) owns `ctx.resources`,
 
 ## Testing
 
-Host specs in `packages/api/workspace-files/tests` exercise header-only scope resolution for live and cold subagent Sessions, the deployment fallback, missing identities, and lookup disposal; the paged read (whole file, nested path, empty file, multi-byte UTF-8, the line window's edges, defaults and refused limits, carriage returns kept); the byte window (defaults, a middle window with more following, tail windows exact and short, past-end and empty files, NUL and invalid UTF-8 round-tripping through base64, version parity with `stat`, the cap as `too-large`, bad ranges, a window of a file far above the cap, and `eof` inferred without a size); `stat`; outside-workspace reads and backend refusals; `list` with containment, truncation, symlink children, and `not-directory`; and the `changes` stream driven by `fs/observed` and filtered by root. Client specs cover the provider's frames, the change feed, unsupported addresses, and registration and disposal. `fs/fs`, `fs-local`, and `fs-e2b` specs pin `readByteRange`; `dsh-util-workspace-path` specs pin the file-address grammar. The connection fixture serves `stat`, paged `read`, `list`, and an opt-in `changes` frame for the web e2e suite.
+Host specs in `packages/api/workspace-files/tests` exercise header-only scope resolution for live and cold subagent Sessions, the deployment fallback, missing identities, and lookup disposal; the paged read (whole file, nested path, empty file, multi-byte UTF-8, the line window's edges, defaults and refused limits, carriage returns kept); the byte window (defaults, a middle window with more following, tail windows exact and short, past-end and empty files, NUL and invalid UTF-8 round-tripping through base64, version parity with `stat`, the cap as `too-large`, bad ranges, a window of a file far above the cap, and `eof` inferred without a size); `stat`; outside-workspace reads and backend refusals; `list` with containment, truncation, symlink children, and `not-directory`; and the `changes` stream driven by `fs/observed` and filtered by root. Client specs cover the provider's frames, the change feed, unsupported addresses, and registration and disposal. `fs/fs` and `fs-local` specs pin `readByteRange`; `dsh-util-workspace-path` specs pin the file-address grammar. The connection fixture serves `stat`, paged `read`, `list`, and an opt-in `changes` frame for the web e2e suite.
 
 ## Deferred
 

+ 3 - 3
.agents/notes/implemented/architecture/2026-09-05-workspace-files-service.zh.md

@@ -95,7 +95,7 @@ abstract readByteRange(target: FsTarget, range: { offset: number; length: number
 
 它返回 `[offset, offset + length)` 处的字节,文件在窗内结束则变短,`offset` 位于或越过末尾则为空。窗口即界:后端最多传输为到达 `offset` 而跳过的前缀之外的 `length` 字节,从不缓冲整个文件,因此调用方对 `length` 的上限就是防无界缓冲的守卫,与 `readBytes` 的界并列而非取代它。参数顺序遵循 `readText`、`streamText` 与 `listDir`——先目标,再操作自己的参数,最后可选 signal——而不是 `readBytes` 把 signal 放中间的形式,那是该类中唯一的例外。`offset` 与 `length` 按前置条件都是非负整数;seam 是类型化的同进程边界,不做任何校验,由 Remote 方法在线路处校验。
 
-`fs-local` 在与其他读取相同的普通文件 stat 之后打开 `createReadStream(targetKey, { start: offset, end: offset + length - 1 })`,对 `length` 为 0 直接返回空数组而不开流;`fs-sandbox` 继承 `LocalFileSystem`,随之继承该方法。`fs-e2b` 的 SDK 只能从文件开头开始流式读取,于是它跳过 `offset` 字节、把 `length` 字节拷入窗口,并在窗口填满的那一刻取消流,除跳过的前缀外传输量不超过窗口;先行结束的流则任其关闭。继承 `FileSystem` 的四个测试替身也实现了该方法。
+`fs-local` 在与其他读取相同的普通文件 stat 之后打开 `createReadStream(targetKey, { start: offset, end: offset + length - 1 })`,对 `length` 为 0 直接返回空数组而不开流;`fs-sandbox` 继承 `LocalFileSystem`,随之继承该方法。继承 `FileSystem` 的四个测试替身也实现了该方法。
 
 ### Client `file` 提供者
 
@@ -133,7 +133,7 @@ Client 导出向 `ctx.resources` 注册一个 `ResourceProvider<'file'>`,存
 
 - 工作区文件访问由 `api/workspace-files` 的 Host/Client 两面共同承担;Session Controller 不携带其中任何实现,两面的编译与运行时入口保持独立。header-only Session scope 让普通、subagent、live 与 cold Session 都能解析自己的相对路径,不需要 Agent 生命周期,也不回退父 Session。
 - 任意大小的文件都能打开:文本按行页、任何文件按字节窗口,在 Host 上各自只花一页或一窗内存;全文读取则受 `maxFileBytes` 约束;代价是消费者自己拼装页面,且单行超过 `maxBytes` 的行没有任何页,因为页按行切。
-- 每个文件系统提供者现在都提供开窗的原始读取。`fs-e2b` 为此付出传输被跳过前缀的代价,因为其 SDK 不能 seek;`fs-local` 能 seek
+- 每个文件系统提供者都提供开窗的原始读取;`fs-local` 直接定位到所请求的偏移量
 - 线路上的路径是规范的:`absolutePath` 与变更帧以符号链接已解析的拼法命名文件。跟随者绑定到成功的 `stat.absolutePath`,因此同一文件的另一种拼法——经符号链接到达的工作区根——也使用该规范变更键。
 - 变更帧只报告 agent 自己的操作。用户编辑器、shell 或子进程改动的文件不产生帧;agent 仅仅读取一个被别处改动的文件却会产生帧,因为读取观察到了新版本。
 - 文件类型检查先于后端读取,`list` 也先报种类再报根外位置。页的 `version` 可能落后内容一次写入,停滞的 `changes` 消费者会让 Host 内存增长,因为一代流的队列无界;每一条都是包 README 记录在册的已知取舍。
@@ -142,7 +142,7 @@ Client 导出向 `ctx.resources` 注册一个 `ResourceProvider<'file'>`,存
 
 ## Testing
 
-`packages/api/workspace-files/tests` 中的 Host spec 覆盖 live 与 cold subagent Session 的 header-only scope 解析、部署 fallback、缺失身份与 lookup 释放;分页读取(整文件、嵌套路径、空文件、多字节 UTF-8、行窗口边界、缺省与拒绝的 limit、保留回车);字节窗口(缺省、中段与尾窗、越界与空文件、base64 往返、版本、上限、坏范围以及无大小时的 `eof`);`stat`;工作区外读取及后端拒绝;`list` 的包含、截断、符号链接与 `not-directory`;以及由 `fs/observed` 驱动并按根过滤的 `changes`。Client spec 覆盖提供者帧、变更流、不支持地址及注册与释放。`fs/fs`、`fs-local` 与 `fs-e2b` spec 钉住 `readByteRange`;`dsh-util-workspace-path` spec 钉住文件地址语法。connection fixture 为 web e2e 套件提供 `stat`、分页 `read`、`list` 与一帧可选启用的 `changes`。
+`packages/api/workspace-files/tests` 中的 Host spec 覆盖 live 与 cold subagent Session 的 header-only scope 解析、部署 fallback、缺失身份与 lookup 释放;分页读取(整文件、嵌套路径、空文件、多字节 UTF-8、行窗口边界、缺省与拒绝的 limit、保留回车);字节窗口(缺省、中段与尾窗、越界与空文件、base64 往返、版本、上限、坏范围以及无大小时的 `eof`);`stat`;工作区外读取及后端拒绝;`list` 的包含、截断、符号链接与 `not-directory`;以及由 `fs/observed` 驱动并按根过滤的 `changes`。Client spec 覆盖提供者帧、变更流、不支持地址及注册与释放。`fs/fs` 与 `fs-local` spec 钉住 `readByteRange`;`dsh-util-workspace-path` spec 钉住文件地址语法。connection fixture 为 web e2e 套件提供 `stat`、分页 `read`、`list` 与一帧可选启用的 `changes`。
 
 ## Deferred
 

+ 6 - 0
.agents/notes/implemented/simplification/2026-09-11-remove-e2b-providers.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# 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/simplification/2026-09-11-remove-e2b-providers.md
+2026-09-11-remove-e2b-providers.md: 87bec5b728f64443e7132aa9b40feb684f9a0d0f
+2026-09-11-remove-e2b-providers.zh.md: 3048a61ed28232bda2f86ddc6ba6b003060ff87d

+ 51 - 0
.agents/notes/implemented/simplification/2026-09-11-remove-e2b-providers.md

@@ -0,0 +1,51 @@
+# Agent Note: Remove the E2B execution providers
+
+Status: implemented
+
+English | [中文](2026-09-11-remove-e2b-providers.zh.md)
+
+## Problem
+
+The E2B experiment supplied one remote filesystem/process world for file tools, Bash, PTY and language servers while the Harness, credentials, model transport and session state stayed on the host. Its three providers demonstrated that those consumers can share the existing filesystem and subprocess interfaces without provider-specific tools.
+
+PTC Node confinement requires bidirectional control traffic separate from arbitrary program stdin/stdout/stderr. The pinned `e2b@2.29.1` SDK exposed standard process streams and PTY output, with no extra descriptor transport. Creating a descriptor inside the VM does not expose it to the Harness host; a remote relay or a separate authenticated stream must bridge it.
+
+Such a bridge owns independent progress when output blocks, bounded retention, channel closure, startup publication, command identity, cancellation and disconnect cleanup. The SDK retained complete stdout/stderr even when a consumer applied backpressure, so stream-level queue bounds alone could not bound host memory. Extending that adapter would turn the experiment into a separate remote transport project.
+
+## Decision
+
+The repository excludes the E2B sandbox owner, filesystem provider and subprocess provider, their SDK dependency, integration fixtures, opt-in workflow and generated registrations. It supplies no E2B execution backend. The removal trades that integration for a smaller maintained implementation set; it does not imply that E2B cannot support a suitable transport or that published packages have no external users.
+
+The [portable execution-world decision](../architecture/2026-07-28-portable-execution-world-consumers.md) remains authoritative for `ctx.fs` and `ctx.subprocess`. The pair describes one path namespace and process world. Providers own path conversion, executable lookup, ordinary streams, terminal allocation and managed cleanup; consumers retain tool semantics, policy and presentation. A local/remote metadata label adds no authority to those operations.
+
+Remote execution remains an intended capability. Terminal allocation, writes, foreground inspection and signalling retain their Promise contracts and pending-operation cancellation guarantees. A local implementation completing these calls synchronously does not justify removing the asynchronous behavior required by remote providers. Process completion, terminal readiness, emulator parsing and managed-range quiescence also retain their existing owners.
+
+This decision removes one provider family. It changes neither PTC execution nor the shared filesystem, subprocess and terminal interfaces. Direct Node confinement and validation of program-authored control traffic require their own implementation and evidence.
+
+## Alternatives considered
+
+**Retain E2B and refuse the control-channel request.** This preserves the existing remote functionality with little new transport code, but retains its SDK and remote lifecycle obligations while excluding the new execution capability. Retirement makes that maintenance tradeoff explicit.
+
+**Multiplex the channel over E2B standard streams.** A relay can frame stdin/control input and stdout/stderr/control output. It also needs channel-specific flow control and closure, bounded queues, and a solution to cumulative SDK output retention. These are remote transport semantics, not descriptor plumbing.
+
+**Use a separate authenticated stream.** This can avoid the SDK's retained-output path and let control traffic progress independently. It adds endpoint access, authentication, network policy, startup coordination and connection-failure cleanup. The removal does not introduce that transport.
+
+**Make terminal operations synchronous.** This simplifies local wrappers but removes delayed-operation behavior needed by remote providers. The shared asynchronous interface and its cancellation/ordering tests remain useful independently of E2B.
+
+## Consequences
+
+Custom compositions that depend on the repository's E2B packages lose that supplied backend. External adoption is unknown. Removing the integration also removes its live remote composition evidence; remaining local and delayed-provider tests protect their own contracts, not an unimplemented replacement.
+
+The [native containment](../architecture/2026-08-28-subprocess-native-containment.md), [outbound proxy](../architecture/2026-08-27-outbound-proxy-policy.md) and [workspace-file service](../architecture/2026-09-05-workspace-files-service.md) decisions remain active. Their E2B implementation and verification inventories are retired while their process ownership, routing and file-access requirements remain intact. The portability decision is partially superseded only in its E2B realization.
+
+## Reintroduction conditions
+
+A remote provider needs a concrete execution use case and evidence for shared file/process coordinates, policy enforcement, bounded transport retention, independent control progress, precise channel closure and managed cancellation. Source and built compositions must exercise those behaviors. A connection loss cannot justify replaying a possibly executed program or claiming unobserved cleanup succeeded.
+
+SSH remains a possible transport for those providers: binary channels avoid the E2B command SDK's retained-output path, but ordinary SSH exec does not map arbitrary child descriptors. A remote helper still owns control-stream bridging, file semantics, process lifetime and remote policy enforcement. The retained interfaces permit that work without claiming a replacement is already available.
+
+## Verification
+
+The removal inventory finds no live E2B package, workflow, import, dependency or catalog entry. Every surviving lockfile importer and package resolution matches its original value. The shared subprocess and terminal implementation, delayed-provider tests and PTC source are unchanged.
+
+The remaining filesystem, subprocess, Bash, terminal and LSP suites pass alongside the affected generator and workflow tests. `pnpm run build`, `pnpm run test:docs`, `pnpm run doc-sync`, `pnpm run lint` and `pnpm run hygiene` pass for the removal. These checks validate the resulting tree; deleted E2B tests provide no evidence for it.

+ 51 - 0
.agents/notes/implemented/simplification/2026-09-11-remove-e2b-providers.zh.md

@@ -0,0 +1,51 @@
+# Agent Note: 移除 E2B 执行提供方
+
+Status: implemented
+
+[English](2026-09-11-remove-e2b-providers.md) | 中文
+
+## 问题
+
+E2B 实验为文件工具、Bash、PTY 和语言服务器提供了一个远程文件系统/进程世界,而 Harness、凭据、模型传输和会话状态留在宿主。三个提供方证明,这些消费方可以共享现有文件系统与子进程接口,无需特定于提供方的工具。
+
+PTC Node 约束需要独立于任意程序 stdin/stdout/stderr 的双向控制通信。固定版本的 `e2b@2.29.1` SDK 提供标准进程流与 PTY 输出,没有额外描述符传输。在 VM 中创建描述符不会使其暴露给 Harness 宿主;必须通过远程中继或独立的认证流进行桥接。
+
+这样的桥接负责输出阻塞时的独立推进、有界保留、通道关闭、启动发布、命令身份、取消和断线清理。即使消费方施加反压,SDK 仍保留完整的 stdout/stderr,因此仅限制流级队列无法约束宿主内存。扩展这一适配器会把实验变成独立的远程传输项目。
+
+## 决策
+
+仓库不包含 E2B 沙箱所有者、文件系统提供方和子进程提供方,也不包含其 SDK 依赖、集成 fixture、可选工作流和生成注册项。仓库不提供 E2B 执行后端。移除用放弃该集成换取更小的维护范围;这不意味着 E2B 无法支持合适的传输,也不意味着已发布包没有外部用户。
+
+[可移植执行世界决策](../architecture/2026-07-28-portable-execution-world-consumers.zh.md)继续负责 `ctx.fs` 与 `ctx.subprocess` 的约定。两者描述同一个路径命名空间与进程世界。提供方负责路径转换、可执行文件查找、普通流、终端分配和受管清理;消费方保留工具语义、策略和呈现。local/remote 元数据标签不会为这些操作增加权限依据。
+
+远程执行仍是预期能力。终端分配、写入、前台检查和信号发送保留 Promise 约定,以及待处理操作的取消保证。本地实现同步完成这些调用,不足以支持移除远程提供方所需的异步行为。进程完成、终端就绪判断、模拟器解析和受管范围停稳也保留现有职责归属。
+
+本决策移除一个提供方族。它既不改变 PTC 执行,也不改变共享文件系统、子进程和终端接口。直接 Node 约束与程序生成的控制通信验证需要各自的实现和证据。
+
+## 考虑过的替代方案
+
+**保留 E2B 并拒绝控制通道请求。** 这只需少量新传输代码即可保留现有远程功能,但仍保留 SDK 与远程生命周期维护责任,同时排除新的执行能力。移除明确表达这一维护取舍。
+
+**通过 E2B 标准流多路复用通道。** 中继可以对 stdin/control 输入和 stdout/stderr/control 输出分帧。它还需要逐通道流量控制与关闭、有界队列,以及累计 SDK 输出保留的解决方案。这些是远程传输语义,不是描述符接线。
+
+**使用独立的认证流。** 这可以避开 SDK 保留输出的路径,并让控制通信独立推进。它增加端点访问、认证、网络策略、启动协调和连接失败清理。本次移除不引入该传输。
+
+**把终端操作改为同步。** 这可以简化本地包装,但会移除远程提供方需要的延迟操作行为。共享异步接口及其取消/排序测试在没有 E2B 时仍然有用。
+
+## 后果
+
+依赖仓库 E2B 包的自定义组合失去该后端。外部采用情况未知。移除集成也会移除其真实远程组合证据;保留的本地与延迟提供方测试保护各自约定,并不验证尚未实现的替代方案。
+
+[原生约束](../architecture/2026-08-28-subprocess-native-containment.zh.md)、[出站代理](../architecture/2026-08-27-outbound-proxy-policy.zh.md)和[工作区文件服务](../architecture/2026-09-05-workspace-files-service.zh.md)决策保持有效。它们的 E2B 实现与验证清单被撤下,进程所有权、路由和文件访问要求保持完整。可移植性决策仅有 E2B 实现部分被取代。
+
+## 重新引入条件
+
+远程提供方需要具体的执行用例,并证明共享文件/进程坐标、策略强制、有界传输保留、独立控制推进、精确通道关闭和受管取消。源代码与构建后组合必须执行这些行为。连接丢失不能成为重放可能已执行程序的理由,也不能据此声称未观察到的清理已经成功。
+
+SSH 仍可作为这些提供方的传输:二进制通道避开 E2B 命令 SDK 保留输出的路径,但普通 SSH exec 不映射任意子进程描述符。远程辅助进程仍负责控制流桥接、文件语义、进程生命周期和远程策略强制。保留的接口允许开展这项工作,但不声称替代方案已经可用。
+
+## 验证
+
+移除清点未发现有效的 E2B 包、工作流、导入、依赖或目录条目。锁文件中每个保留的 importer 与包解析结果都保持原值。共享子进程与终端实现、延迟提供方测试和 PTC 源代码保持不变。
+
+保留的文件系统、子进程、Bash、终端与 LSP 套件,以及受影响的生成器与工作流测试均通过。移除后的 `pnpm run build`、`pnpm run test:docs`、`pnpm run doc-sync`、`pnpm run lint` 与 `pnpm run hygiene` 均通过。这些检查验证得到的代码树;已删除的 E2B 测试不为其提供证据。

+ 0 - 58
.github/workflows/e2b-e2e.yml

@@ -1,58 +0,0 @@
-name: E2E (E2B sandbox)
-
-# This suite provisions external E2B sandboxes and is intentionally opt-in.
-# It has no push, pull_request, schedule, or workflow_call trigger.
-on:
-  workflow_dispatch:
-
-permissions:
-  contents: read
-
-env:
-  # CI runs must never report to the production telemetry endpoint baked
-  # into apps/cli/cordis.yml (AppCLIEntry disables the row when set).
-  DSH_TELEMETRY_DISABLED: '1'
-
-jobs:
-  e2b:
-    runs-on: ubuntu-latest
-    name: E2B live Loader composition
-    timeout-minutes: 30
-    steps:
-      - uses: actions/checkout@v6
-
-      - uses: pnpm/action-setup@v4
-
-      - uses: actions/setup-node@v6
-        with:
-          node-version: 24
-          cache: pnpm
-
-      - name: Install (immutable)
-        run: pnpm install --frozen-lockfile
-
-      # The tests self-skip locally when the credential is absent. A manually
-      # dispatched run must fail instead of reporting an all-skipped green.
-      - name: Preflight (require E2B API key)
-        env:
-          E2B_API_KEY: ${{ secrets.E2B_API_KEY_EXTERNAL }}
-        run: |
-          set -euo pipefail
-          if [ -z "${E2B_API_KEY:-}" ]; then
-            echo "::error::E2B_API_KEY is empty. Configure the E2B_API_KEY_EXTERNAL repository secret."
-            exit 1
-          fi
-          echo "E2B_API_KEY present."
-
-      # The Loader smoke runs package exports under plain Node in lib mode.
-      - name: Build (lib for the E2B Loader smoke)
-        run: pnpm run build:official
-
-      - name: E2B tests (live sandbox)
-        env:
-          E2B_API_KEY: ${{ secrets.E2B_API_KEY_EXTERNAL }}
-          DSH_E2E_MAX_WORKERS: '1'
-          DSH_EXAMPLE_MODE: lib
-        run: >-
-          pnpm exec vitest run --config vitest.e2e.config.ts
-          packages/e2b/e2b/tests/composition.e2e.ts

+ 0 - 1
AGENTS.md

@@ -17,7 +17,6 @@ packages/    @deepseek-ai/dsh-<pkg> workspaces at packages/<group>/<pkg>/
   api/         Remote BFF assembly and Typert RPC gateway
   typert/      type graph generator, loader, and runtime registry
   llm/         LLM capability: Service Definition/Consumer + DeepSeek providers
-  e2b/         E2B POC: sandbox + FS/subprocess adapters
   shell/        bash capability: Service Definition + local/pwsh providers + shell Consumers
   subprocess/  subprocess capability + local process-tree provider + shared Win32 library
   terminal/         persistent sessions

+ 0 - 1
THIRD_PARTY_NOTICES.md

@@ -66,7 +66,6 @@ External packages installed for runtime use or distributed inside the prebuilt b
 | [`commander`](https://github.com/tj/commander.js) | MIT |
 | [`compression`](https://github.com/expressjs/compression) | MIT |
 | [`diff`](https://github.com/kpdecker/jsdiff) | BSD-3-Clause |
-| [`e2b`](https://github.com/e2b-dev/e2b) | MIT |
 | [`electron-updater`](https://github.com/electron-userland/electron-builder) | MIT |
 | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT |
 | [`fflate`](https://github.com/101arrowz/fflate) | MIT |

+ 2 - 2
docs/capability-seams.i18n.yaml

@@ -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 docs/capability-seams.md
-capability-seams.md: 9cc7a6fc50b81e9f82d58600c84d1ff6494634c9
-capability-seams.zh.md: e898bdf3c2c728a1bae427bbb737eb5269a1c500
+capability-seams.md: 547414830411d1d80781c614e75d839f8a9547fb
+capability-seams.zh.md: 8b89c0240a9881625f3d6a65bb0f1a31e887e1ef

+ 2 - 12
docs/capability-seams.md

@@ -133,10 +133,6 @@ flowchart LR
   pkg_sdk_minimal["sdk-minimal"]
   pkg_goal["goal"]
   svc_goals["ctx.goals<br/>Same-session goal domain"]
-  pkg_e2b["e2b"]
-  svc_e2b["ctx.e2b<br/>E2B sandbox lifecycle owner"]
-  pkg_fs_e2b["fs-e2b"]
-  pkg_subprocess_e2b["subprocess-e2b"]
   pkg_subprocess["subprocess"]
   svc_subprocess["ctx.subprocess<br/>Subprocess seam"]
   pkg_subprocess_local["subprocess-local"]
@@ -256,13 +252,11 @@ flowchart LR
   pkg_credentials --> svc_credentials
   pkg_credentials_local --> svc_credentials
   pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions
-  pkg_e2b --> svc_e2b
   pkg_experimental_agent_team --> svc_agentTeams
   pkg_experimental_code_runtime_python --> svc_codeRuntime
   pkg_file_reference --> svc_fileReferences
   pkg_file_reference_local --> svc_fileReferences
   pkg_fs --> svc_fs
-  pkg_fs_e2b --> svc_fs
   pkg_fs_local --> svc_fs
   pkg_fs_sandbox --> svc_fs
   pkg_goal --> svc_goals
@@ -323,7 +317,6 @@ flowchart LR
   pkg_subagent_fork_in_process --> svc_subagents
   pkg_subagent_spawn_in_process --> svc_subagents
   pkg_subprocess --> svc_subprocess
-  pkg_subprocess_e2b --> svc_subprocess
   pkg_subprocess_local --> svc_subprocess
   pkg_system_prompt --> svc_systemPrompt
   pkg_terminal --> svc_terminals
@@ -370,8 +363,6 @@ flowchart LR
   svc_deepseekLlmApiExtensions --> pkg_llm_deepseek
   svc_directoryPicker --> pkg_api_workspace_controller
   svc_dynamicCordisRunner --> pkg_tool_cordis
-  svc_e2b --> pkg_fs_e2b
-  svc_e2b --> pkg_subprocess_e2b
   svc_fileReferences --> pkg_api_session_controller
   svc_fileUploads --> pkg_api_session_controller
   svc_fs --> pkg_tool_fs
@@ -520,8 +511,7 @@ flowchart LR
 | `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | Layers the default ModelSelection through settings so direct and Host-backed Agent entry points share one state owner. |
 | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
 | `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | Folds revisioned objective state from the session log and keeps live continuation activation process-local. |
-| `ctx.e2b` | `core` | [`e2b`](../packages/e2b/e2b) | - | [`fs-e2b`](../packages/e2b/fs-e2b), [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | - | Owns one shared E2B SDK handle, remote working directory, and final sandbox disposition so both fundamental E2B providers inhabit the same Linux runtime. |
-| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local), [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash), [`lsp-stdio`](../packages/lsp/lsp-stdio), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the PTY shell backend, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation. |
+| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash), [`lsp-stdio`](../packages/lsp/lsp-stdio), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | The bash executors, the PTY shell backend, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation. |
 | `ctx.shell` | `seam` | [`shell`](../packages/shell/shell) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`pwsh-local`](../packages/shell/pwsh-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing shell tools and hook bridges consume this seam; sandboxed, remote, or PowerShell executors replace bash-local without touching them. |
 | `ctx.shellEnv` | `core` | [`shell-env`](../packages/shell/shell-env) | - | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh) | - | Plugins declare effect-scoped DSH_* facts; each shell tool collects one trusted snapshot per execution and its executor rebuilds the namespace. |
 | `ctx.terminals` | `seam` | [`terminal`](../packages/terminal/terminal) | [`terminal-bash`](../packages/terminal/terminal-bash) | [`tool-terminal`](../packages/terminal/tool-terminal) | - | The registry owns exact-Agent session identity and cleanup; backends own terminal mechanics, while tool-terminal exposes the owner-scoped model tools. |
@@ -530,7 +520,7 @@ flowchart LR
 | `ctx.approval` | `seam` | [`user-approval`](../packages/interaction/user-approval) | - | [`tools`](../packages/core/tools), [`tool-bash`](../packages/shell/tool-bash), [`acp`](../packages/acp/acp) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. |
 | `ctx.permissionPresets` | `core` | [`permission-presets`](../packages/interaction/permission-presets) | - | - | - | User-facing preset table (`workspace-write`/`danger-full-access`) bundling the sandbox-mode and approval-policy knobs; a switch writes one `permission/preset` event through to both knob events. |
 | `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker-thread`](../packages/code-runtime/code-runtime-worker-thread), [`experimental-code-runtime-python`](../packages/experimental/code-runtime-python) | [`tools`](../packages/core/tools) | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for PTC mode). |
-| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate. |
+| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate. |
 | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | The basic backend consumes post-step pressure and request-error recovery events; there is no model-facing compact tool. |
 | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | Providers implement transports; the service also owns optional Activation-based continuation orchestration, tool-subagent selects one-shot or continuable delegation, tool-subagent-control delivers follow-ups, and tool-ralph requires one fresh structured-output route. |
 | `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | - | Owns the implicit-root roster, durable peer mailbox, shared task DAG, continuable-child lifecycle, and generated Team Remote methods; tool-agent-team contributes model controls and client-ui-agent-team mounts the browser contribution. |

+ 2 - 12
docs/capability-seams.zh.md

@@ -135,10 +135,6 @@ flowchart LR
   pkg_sdk_minimal["sdk-minimal"]
   pkg_goal["goal"]
   svc_goals["ctx.goals<br/>Same-session goal domain"]
-  pkg_e2b["e2b"]
-  svc_e2b["ctx.e2b<br/>E2B sandbox lifecycle owner"]
-  pkg_fs_e2b["fs-e2b"]
-  pkg_subprocess_e2b["subprocess-e2b"]
   pkg_subprocess["subprocess"]
   svc_subprocess["ctx.subprocess<br/>Subprocess seam"]
   pkg_subprocess_local["subprocess-local"]
@@ -258,13 +254,11 @@ flowchart LR
   pkg_credentials --> svc_credentials
   pkg_credentials_local --> svc_credentials
   pkg_deepseek_llm_api_extensions --> svc_deepseekLlmApiExtensions
-  pkg_e2b --> svc_e2b
   pkg_experimental_agent_team --> svc_agentTeams
   pkg_experimental_code_runtime_python --> svc_codeRuntime
   pkg_file_reference --> svc_fileReferences
   pkg_file_reference_local --> svc_fileReferences
   pkg_fs --> svc_fs
-  pkg_fs_e2b --> svc_fs
   pkg_fs_local --> svc_fs
   pkg_fs_sandbox --> svc_fs
   pkg_goal --> svc_goals
@@ -325,7 +319,6 @@ flowchart LR
   pkg_subagent_fork_in_process --> svc_subagents
   pkg_subagent_spawn_in_process --> svc_subagents
   pkg_subprocess --> svc_subprocess
-  pkg_subprocess_e2b --> svc_subprocess
   pkg_subprocess_local --> svc_subprocess
   pkg_system_prompt --> svc_systemPrompt
   pkg_terminal --> svc_terminals
@@ -372,8 +365,6 @@ flowchart LR
   svc_deepseekLlmApiExtensions --> pkg_llm_deepseek
   svc_directoryPicker --> pkg_api_workspace_controller
   svc_dynamicCordisRunner --> pkg_tool_cordis
-  svc_e2b --> pkg_fs_e2b
-  svc_e2b --> pkg_subprocess_e2b
   svc_fileReferences --> pkg_api_session_controller
   svc_fileUploads --> pkg_api_session_controller
   svc_fs --> pkg_tool_fs
@@ -522,8 +513,7 @@ flowchart LR
 | `ctx.agentDefaultModel` | `core` | [`agent-default-model`](../packages/core/agent-default-model) | - | [`api-session-controller`](../packages/api/session-controller), [`headless`](../packages/bundle/headless) | - | 通过 settings 分层默认 `ModelSelection`,让直接入口与 Host 支撑的 Agent 入口共享同一个状态所有者。 |
 | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`base`](../packages/bundle/base), [`sdk-minimal`](../packages/bundle/sdk-minimal) | - | 唯一的具体循环插件;扩展包依赖 dsh-agent 的事件和服务,而不依赖此包。 |
 | `ctx.goals` | `core` | [`goal`](../packages/goal/goal) | - | - | - | 从会话日志折叠带修订版本的目标状态,并将实时延续激活保留在进程本地。 |
-| `ctx.e2b` | `core` | [`e2b`](../packages/e2b/e2b) | - | [`fs-e2b`](../packages/e2b/fs-e2b), [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | - | 拥有一个共享的 E2B SDK 句柄、远程工作目录和最终沙箱处置,使两个基础 E2B 提供方处于同一个 Linux 运行时中。 |
-| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local), [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash), [`lsp-stdio`](../packages/lsp/lsp-stdio), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | Bash 执行器、PTY shell 后端、LSP Host,以及进程外 ACP、Codex 和 Claude Code subagent 后端都通过 ctx.subprocess 执行 spawn;该服务负责进程坐标、进程树/会话生命周期、stdio 处置、终端机制和 kill 升级。 |
+| `ctx.subprocess` | `seam` | [`subprocess`](../packages/subprocess/subprocess) | [`subprocess-local`](../packages/subprocess/subprocess-local) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`terminal-bash`](../packages/terminal/terminal-bash), [`lsp-stdio`](../packages/lsp/lsp-stdio), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code) | - | Bash 执行器、PTY shell 后端、LSP Host,以及进程外 ACP、Codex 和 Claude Code subagent 后端都通过 ctx.subprocess 执行 spawn;该服务负责进程坐标、进程树/会话生命周期、stdio 处置、终端机制和 kill 升级。 |
 | `ctx.shell` | `seam` | [`shell`](../packages/shell/shell) | [`bash-local`](../packages/shell/bash-local), [`bash-sandbox`](../packages/shell/bash-sandbox), [`pwsh-local`](../packages/shell/pwsh-local) | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) | - | 面向模型的 shell 工具和钩子桥接消费此 seam;沙箱、远程或 PowerShell 执行器可以替换 bash-local,而无需改动这些消费方。 |
 | `ctx.shellEnv` | `core` | [`shell-env`](../packages/shell/shell-env) | - | [`tool-bash`](../packages/shell/tool-bash), [`tool-pwsh`](../packages/shell/tool-pwsh) | - | 插件声明限定于 effect 作用域的 DSH_* 事实;每个 shell 工具在每次执行时收集一份可信快照,其执行器据此重建命名空间。 |
 | `ctx.terminals` | `seam` | [`terminal`](../packages/terminal/terminal) | [`terminal-bash`](../packages/terminal/terminal-bash) | [`tool-terminal`](../packages/terminal/tool-terminal) | - | 注册表负责精确到 Agent 的会话身份和清理;后端负责终端机制,tool-terminal 则提供限定于所有者作用域的模型接口。 |
@@ -532,7 +522,7 @@ flowchart LR
 | `ctx.approval` | `seam` | [`user-approval`](../packages/interaction/user-approval) | - | [`tools`](../packages/core/tools), [`tool-bash`](../packages/shell/tool-bash), [`acp`](../packages/acp/acp) | - | 一次性权限决策通过 `approval/request` waterfall(瀑布式事件)分派;回答方是监听器(即 ACP 为自身 agent 提供的桥接),没有回答方时以 `unavailable` 关闭失败。 |
 | `ctx.permissionPresets` | `core` | [`permission-presets`](../packages/interaction/permission-presets) | - | - | - | 面向用户的预设表(`workspace-write`/`danger-full-access`),将沙箱模式与审批策略选项组合在一起;一次切换会写入一个 `permission/preset` 事件,并贯通到两个选项事件。 |
 | `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker-thread`](../packages/code-runtime/code-runtime-worker-thread), [`experimental-code-runtime-python`](../packages/experimental/code-runtime-python) | [`tools`](../packages/core/tools) | - | 使用 Host 提供的异步绑定运行一段由模型编写的程序;各后端采用不同的基础环境和语言(工具注册表在 PTC mode 下消费该服务)。 |
-| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox), [`fs-e2b`](../packages/e2b/fs-e2b) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs 通过 ctx.fs 执行读取/写入/编辑;fs-sandbox 按共享沙箱模式限制变更;fs-observation-policy 通过 fs/* 事件门禁贡献基于观测状态的检查。 |
+| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local), [`fs-sandbox`](../packages/fs/fs-sandbox) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) | tool-fs 通过 ctx.fs 执行读取/写入/编辑;fs-sandbox 按共享沙箱模式限制变更;fs-observation-policy 通过 fs/* 事件门禁贡献基于观测状态的检查。 |
 | `ctx.compaction` | `seam` | [`compaction`](../packages/compaction/compaction) | [`compaction-basic`](../packages/compaction/compaction-basic) | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 基础后端消费步骤后的压力事件和请求错误恢复事件;不存在面向模型的压缩工具。 |
 | `ctx.subagents` | `seam` | [`subagent`](../packages/subagent/subagent) | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process), [`subagent-fork-in-process`](../packages/subagent/subagent-fork-in-process), [`subagent-acp`](../packages/subagent/subagent-acp), [`subagent-codex`](../packages/subagent/subagent-codex), [`subagent-claude-code`](../packages/subagent/subagent-claude-code), [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-subagent-control`](../packages/subagent/tool-subagent-control), [`tool-ralph`](../packages/workflow/tool-ralph) | - | 提供方实现传输;该服务还负责可选的、基于 Activation 的延续编排,tool-subagent 选择一次性或可延续委派,tool-subagent-control 传递后续消息,而 tool-ralph 要求一条全新的结构化输出路由。 |
 | `ctx.agentTeams` | `core` | [`experimental-agent-team`](../packages/experimental/agent-team) | - | [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team), [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | - | 负责隐式 Root roster、持久 peer mailbox、共享任务 DAG、continuable child 生命周期与生成式 Team Remote method;tool-agent-team 提供模型控制工具,client-ui-agent-team 挂载浏览器 contribution。 |

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -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 docs/config-catalog.md
-config-catalog.md: 5dc9a5a23d38a812626e2ac42a4c32fc8671ccc8
-config-catalog.zh.md: caf689325567af3480d6aab6da080d227b978093
+config-catalog.md: 7e53ba35aa57d87f2ab5f6456f21b28070be6162
+config-catalog.zh.md: 4010623711532fbff2db578604af4fe07fb0e4d0

+ 0 - 35
docs/config-catalog.md

@@ -540,24 +540,6 @@ export interface Config {
 
 Source: [`packages/credentials/credentials-local/src/index.ts:64`](../packages/credentials/credentials-local/src/index.ts)
 
-<a id="deepseek-aidsh-e2b"></a>
-
-## `@deepseek-ai/dsh-e2b`
-
-```ts config-catalog
-/** Configuration for the shared E2B sandbox owner. */
-export interface Config {
-  /** API key; omission reads `E2B_API_KEY`. It is never forwarded into the sandbox. */
-  apiKey?: string
-  /** Shared remote working directory, created before adapters receive the sandbox. */
-  cwd?: string
-  /** E2B sandbox lifetime in milliseconds; expiry always deletes the sandbox. */
-  timeoutMs?: number
-}
-```
-
-Source: [`packages/e2b/e2b/src/index.ts:45`](../packages/e2b/e2b/src/index.ts)
-
 <a id="deepseek-aidsh-experimental-agent-team"></a>
 
 ## `@deepseek-ai/dsh-experimental-agent-team`
@@ -2574,22 +2556,6 @@ export interface Config {
 
 Source: [`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts)
 
-<a id="deepseek-aidsh-subprocess-e2b"></a>
-
-## `@deepseek-ai/dsh-subprocess-e2b`
-
-Requires: `e2b`
-
-```ts config-catalog
-/** Configuration for the E2B subprocess adapter. */
-export interface Config {
-  /** Remote status/liveness poll cadence in milliseconds; each tick is one control-plane request. */
-  pollMs?: number
-}
-```
-
-Source: [`packages/e2b/subprocess-e2b/src/index.ts:26`](../packages/e2b/subprocess-e2b/src/index.ts)
-
 <a id="deepseek-aidsh-system-prompt"></a>
 
 ## `@deepseek-ai/dsh-system-prompt`
@@ -3512,7 +3478,6 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
 - `@deepseek-ai/dsh-cordis-client-runner` ([`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts))
 - `@deepseek-ai/dsh-deepseek-llm-api-extensions` ([`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts))
 - `@deepseek-ai/dsh-experimental-client-ui-agent-team` ([`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts))
-- `@deepseek-ai/dsh-fs-e2b` — requires `e2b` ([`packages/e2b/fs-e2b/src/index.ts`](../packages/e2b/fs-e2b/src/index.ts))
 - `@deepseek-ai/dsh-fs-observation-policy` ([`packages/fs/fs-observation-policy/src/index.ts`](../packages/fs/fs-observation-policy/src/index.ts))
 - `@deepseek-ai/dsh-goal-round-driver` — requires `agents` · `goals` · `sessions` ([`packages/goal/goal-round-driver/src/index.ts`](../packages/goal/goal-round-driver/src/index.ts))
 - `@deepseek-ai/dsh-host-directory-picker-auto` — requires `webServer` · `loader` ([`packages/host/directory-picker-auto/src/index.ts`](../packages/host/directory-picker-auto/src/index.ts))

+ 0 - 35
docs/config-catalog.zh.md

@@ -542,24 +542,6 @@ export interface Config {
 
 来源:[`packages/credentials/credentials-local/src/index.ts:64`](../packages/credentials/credentials-local/src/index.ts)
 
-<a id="deepseek-aidsh-e2b"></a>
-
-## `@deepseek-ai/dsh-e2b`
-
-```ts config-catalog
-/** Configuration for the shared E2B sandbox owner. */
-export interface Config {
-  /** API key; omission reads `E2B_API_KEY`. It is never forwarded into the sandbox. */
-  apiKey?: string
-  /** Shared remote working directory, created before adapters receive the sandbox. */
-  cwd?: string
-  /** E2B sandbox lifetime in milliseconds; expiry always deletes the sandbox. */
-  timeoutMs?: number
-}
-```
-
-来源:[`packages/e2b/e2b/src/index.ts:45`](../packages/e2b/e2b/src/index.ts)
-
 <a id="deepseek-aidsh-experimental-agent-team"></a>
 
 ## `@deepseek-ai/dsh-experimental-agent-team`
@@ -2576,22 +2558,6 @@ export interface Config {
 
 来源:[`packages/subagent/subagent-spawn-in-process/src/index.ts:25`](../packages/subagent/subagent-spawn-in-process/src/index.ts)
 
-<a id="deepseek-aidsh-subprocess-e2b"></a>
-
-## `@deepseek-ai/dsh-subprocess-e2b`
-
-需要:`e2b`
-
-```ts config-catalog
-/** Configuration for the E2B subprocess adapter. */
-export interface Config {
-  /** Remote status/liveness poll cadence in milliseconds; each tick is one control-plane request. */
-  pollMs?: number
-}
-```
-
-来源:[`packages/e2b/subprocess-e2b/src/index.ts:26`](../packages/e2b/subprocess-e2b/src/index.ts)
-
 <a id="deepseek-aidsh-system-prompt"></a>
 
 ## `@deepseek-ai/dsh-system-prompt`
@@ -3514,7 +3480,6 @@ export interface Config {
 - `@deepseek-ai/dsh-cordis-client-runner`([`packages/extensions/cordis-client-runner/src/index.ts`](../packages/extensions/cordis-client-runner/src/index.ts))
 - `@deepseek-ai/dsh-deepseek-llm-api-extensions`([`packages/llm/deepseek-llm-api-extensions/src/index.ts`](../packages/llm/deepseek-llm-api-extensions/src/index.ts))
 - `@deepseek-ai/dsh-experimental-client-ui-agent-team`([`packages/experimental/client-ui-agent-team/src/index.ts`](../packages/experimental/client-ui-agent-team/src/index.ts))
-- `@deepseek-ai/dsh-fs-e2b` — 需要 `e2b`([`packages/e2b/fs-e2b/src/index.ts`](../packages/e2b/fs-e2b/src/index.ts))
 - `@deepseek-ai/dsh-fs-observation-policy`([`packages/fs/fs-observation-policy/src/index.ts`](../packages/fs/fs-observation-policy/src/index.ts))
 - `@deepseek-ai/dsh-goal-round-driver` — 需要 `agents` · `goals` · `sessions`([`packages/goal/goal-round-driver/src/index.ts`](../packages/goal/goal-round-driver/src/index.ts))
 - `@deepseek-ai/dsh-host-directory-picker-auto` — 需要 `webServer` · `loader`([`packages/host/directory-picker-auto/src/index.ts`](../packages/host/directory-picker-auto/src/index.ts))

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -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 docs/module-graph.md
-module-graph.md: 403636b274beac322de092deea39defb56a20c56
-module-graph.zh.md: 68fa1059560ac908d4004dbd61313bbd3f99a903
+module-graph.md: d2f7fd72e9026a7f7eb248a1c4ae234125f3571a
+module-graph.zh.md: cc85a23a7866411aa69edf47a1b437f05270a930

+ 0 - 14
docs/module-graph.md

@@ -210,11 +210,6 @@ flowchart TD
     pkg_credentials["credentials"]
     pkg_credentials_local["credentials-local"]
   end
-  subgraph group_e2b["packages/e2b"]
-    pkg_e2b["e2b"]
-    pkg_fs_e2b["fs-e2b"]
-    pkg_subprocess_e2b["subprocess-e2b"]
-  end
   subgraph group_experimental["packages/experimental"]
     pkg_experimental_agent_team["experimental-agent-team"]
     pkg_experimental_agent_team_profile["experimental-agent-team-profile"]
@@ -379,7 +374,6 @@ flowchart TD
   pkg_web --> pkg_llm
   pkg_attachment --> pkg_brand
   pkg_credentials --> pkg_invariants
-  pkg_e2b --> pkg_http_proxy
   pkg_experimental_code_runtime_python --> pkg_code_runtime
   pkg_experimental_code_runtime_python --> pkg_timeout
   pkg_experimental_code_runtime_python --> pkg_util_values
@@ -429,9 +423,6 @@ flowchart TD
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_launch_environment
-  pkg_subprocess_e2b --> pkg_e2b
-  pkg_subprocess_e2b --> pkg_subprocess
-  pkg_subprocess_e2b --> pkg_timeout
   pkg_subprocess_local --> pkg_subprocess
   pkg_subprocess_local --> pkg_timeout
   pkg_skill_badge --> pkg_skill
@@ -569,8 +560,6 @@ flowchart TD
   pkg_tmux_context --> pkg_session
   pkg_tmux_context --> pkg_session_projection
   pkg_tmux_context --> pkg_shell
-  pkg_fs_e2b --> pkg_e2b
-  pkg_fs_e2b --> pkg_fs
   pkg_commands --> pkg_agent
   pkg_commands --> pkg_attachment
   pkg_commands --> pkg_brand
@@ -1289,7 +1278,6 @@ flowchart TD
 | [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
 | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
 | [`credentials`](../packages/credentials/credentials) | `credentials` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`e2b`](../packages/e2b/e2b) | `e2b` | [`http-proxy`](../packages/util/http-proxy) |
 | [`experimental-code-runtime-python`](../packages/experimental/code-runtime-python) | `experimental` | [`code-runtime`](../packages/code-runtime/code-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
 | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
 | [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
@@ -1313,7 +1301,6 @@ flowchart TD
 | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) |
 | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
-| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
 | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
@@ -1353,7 +1340,6 @@ flowchart TD
 | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
-| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
 | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |

+ 0 - 14
docs/module-graph.zh.md

@@ -212,11 +212,6 @@ flowchart TD
     pkg_credentials["credentials"]
     pkg_credentials_local["credentials-local"]
   end
-  subgraph group_e2b["packages/e2b"]
-    pkg_e2b["e2b"]
-    pkg_fs_e2b["fs-e2b"]
-    pkg_subprocess_e2b["subprocess-e2b"]
-  end
   subgraph group_experimental["packages/experimental"]
     pkg_experimental_agent_team["experimental-agent-team"]
     pkg_experimental_agent_team_profile["experimental-agent-team-profile"]
@@ -381,7 +376,6 @@ flowchart TD
   pkg_web --> pkg_llm
   pkg_attachment --> pkg_brand
   pkg_credentials --> pkg_invariants
-  pkg_e2b --> pkg_http_proxy
   pkg_experimental_code_runtime_python --> pkg_code_runtime
   pkg_experimental_code_runtime_python --> pkg_timeout
   pkg_experimental_code_runtime_python --> pkg_util_values
@@ -431,9 +425,6 @@ flowchart TD
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_launch_environment
-  pkg_subprocess_e2b --> pkg_e2b
-  pkg_subprocess_e2b --> pkg_subprocess
-  pkg_subprocess_e2b --> pkg_timeout
   pkg_subprocess_local --> pkg_subprocess
   pkg_subprocess_local --> pkg_timeout
   pkg_skill_badge --> pkg_skill
@@ -571,8 +562,6 @@ flowchart TD
   pkg_tmux_context --> pkg_session
   pkg_tmux_context --> pkg_session_projection
   pkg_tmux_context --> pkg_shell
-  pkg_fs_e2b --> pkg_e2b
-  pkg_fs_e2b --> pkg_fs
   pkg_commands --> pkg_agent
   pkg_commands --> pkg_attachment
   pkg_commands --> pkg_brand
@@ -1291,7 +1280,6 @@ flowchart TD
 | [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
 | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand) |
 | [`credentials`](../packages/credentials/credentials) | `credentials` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`e2b`](../packages/e2b/e2b) | `e2b` | [`http-proxy`](../packages/util/http-proxy) |
 | [`experimental-code-runtime-python`](../packages/experimental/code-runtime-python) | `experimental` | [`code-runtime`](../packages/code-runtime/code-runtime), [`timeout`](../packages/util/timeout), [`util-values`](../packages/util/values) |
 | [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
 | [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
@@ -1315,7 +1303,6 @@ flowchart TD
 | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) |
 | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
-| [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
 | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
@@ -1355,7 +1342,6 @@ flowchart TD
 | [`file-reference`](../packages/context/file-reference) | `context` | [`agent`](../packages/core/agent) |
 | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection) |
 | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`shell`](../packages/shell/shell) |
-| [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs) |
 | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
 | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
 | [`user-questions`](../packages/interaction/user-questions) | `interaction` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) |

+ 2 - 2
docs/subsystems/subprocess.i18n.yaml

@@ -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 docs/subsystems/subprocess.md
-subprocess.md: 782623760e70648a108ac51252b610e27bffbe45
-subprocess.zh.md: 7b8e590a843899b5f00a309a913e1dcb152c5c1b
+subprocess.md: b62a9c19cfdf9b557ce21cf5bcc9e7e1a01a580f
+subprocess.zh.md: d9206833c14f39ed8e8bb9687b8d06c1b51b42f7

+ 0 - 17
docs/subsystems/subprocess.md

@@ -254,23 +254,6 @@ The abstract [`SubprocessRuntime`](../../packages/subprocess/subprocess/src/inde
 
 Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
 
-<a id="ctxe2b--e2bruntime"></a>
-
-### `ctx.e2b` — `E2BRuntime`
-
-Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal. Creation begins at plugin construction; adapters await getSandbox before their first operation.
-
-```ts cordis-catalog
-/**
- * Return the shared live SDK handle.
- * @returns the created sandbox after the configured cwd exists.
- * @throws when E2B rejects creation or the service is disposing.
- */
-async getSandbox(): Promise<Sandbox>
-```
-
-Source: [`packages/e2b/e2b/src/index.ts`](../../packages/e2b/e2b/src/index.ts)
-
 <a id="ctxsubprocess--subprocessruntime-abstract-seam"></a>
 
 ### `ctx.subprocess` — `SubprocessRuntime` (abstract seam)

+ 0 - 17
docs/subsystems/subprocess.zh.md

@@ -254,23 +254,6 @@ interface SubprocessOutcome {
 
 Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
 
-<a id="ctxe2b--e2bruntime"></a>
-
-### `ctx.e2b` — `E2BRuntime`
-
-Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal. Creation begins at plugin construction; adapters await getSandbox before their first operation.
-
-```ts cordis-catalog
-/**
- * Return the shared live SDK handle.
- * @returns the created sandbox after the configured cwd exists.
- * @throws when E2B rejects creation or the service is disposing.
- */
-async getSandbox(): Promise<Sandbox>
-```
-
-Source: [`packages/e2b/e2b/src/index.ts`](../../packages/e2b/e2b/src/index.ts)
-
 <a id="ctxsubprocess--subprocessruntime-abstract-seam"></a>
 
 ### `ctx.subprocess` — `SubprocessRuntime` (abstract seam)

+ 2 - 2
packages/README.i18n.yaml

@@ -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: 9c298d3f2426bab92a6d602495d836ef8ee9fb44
-README.zh.md: db8977a279713ac7a2f86144323a54125743a46a
+README.md: 22e440b98f8440548c02d48ca3d56890718eafb6
+README.zh.md: 60f033cf01124187c123692883635b64d2bc9754

+ 1 - 2
packages/README.md

@@ -36,7 +36,6 @@ Every package lives in exactly one group; new packages join existing groups, and
 | [`feedback/`](feedback/README.md) | Human feedback capture and command |
 | [`identity/`](identity/README.md) | Shared anonymous identity |
 | [`llm/`](llm/README.md) | LLM capability family: abstract service + provider adapters |
-| [`e2b/`](e2b/README.md) | E2B remote-runtime providers |
 | [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition + local process-tree provider |
 | [`shell/`](shell/README.md) | Bash capability family: executor seam, local impl, model-facing tools |
 | [`terminal/`](terminal/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, model-facing tools |
@@ -83,7 +82,7 @@ Every package lives in exactly one group; new packages join existing groups, and
 <a id="release-expectations"></a>
 ## Release expectations
 
-Most groups are product — stable API. The exceptions: `e2b/` is a POC, `experimental/` is unreleased, and `test-support/`, `runtime-diagnostics/`, and `util/` are support with lower compatibility expectations.
+Most groups are product — stable API. The exceptions: `experimental/` is unreleased, and `test-support/`, `runtime-diagnostics/`, and `util/` are support with lower compatibility expectations.
 
 -----
 

+ 1 - 2
packages/README.zh.md

@@ -36,7 +36,6 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
 | [`feedback/`](feedback/README.zh.md) | 人类反馈的采集与命令 |
 | [`identity/`](identity/README.zh.md) | 共享匿名身份 |
 | [`llm/`](llm/README.zh.md) | LLM(大语言模型)能力系列:抽象服务 + 提供方适配器 |
-| [`e2b/`](e2b/README.zh.md) | E2B 远程运行时提供方 |
 | [`subprocess/`](subprocess/README.zh.md) | 子进程能力系列:Service Definition + 本地进程树提供方 |
 | [`shell/`](shell/README.zh.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 |
 | [`terminal/`](terminal/README.zh.md) | 持久 PTY 能力系列:限定所有者范围的会话、本地实现、面向模型的工具 |
@@ -83,7 +82,7 @@ harness 由 `packages/` 下的 npm 包组装而成,按能力系列分组:会
 <a id="release-expectations"></a>
 ## 发布预期
 
-大多数组属于产品组,提供稳定 API。例外:`e2b/` 是 POC,`experimental/` 不发布,`test-support/`、`runtime-diagnostics/` 与 `util/` 是兼容性预期较低的支持组。
+大多数组属于产品组,提供稳定 API。例外:`experimental/` 不发布,`test-support/`、`runtime-diagnostics/` 与 `util/` 是兼容性预期较低的支持组。
 
 -----
 

+ 0 - 6
packages/e2b/README.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# 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: d4be37306737704e7db461c23782a2b5c6167791
-README.zh.md: 3b4267cd1cbd9c46f91d4c11686ab8afdee4fde6

+ 0 - 50
packages/e2b/README.md

@@ -1,50 +0,0 @@
----
-description: "The E2B remote-runtime group map: file and command work inside one remote Linux sandbox, for users and maintainers of the E2B family."
-kind: "package-group"
----
-
-# packages/e2b
-
-English | [中文](README.zh.md)
-
-## Summary
-
-The E2B family lets agents read and edit files, run shell commands, and use terminals inside one remote Linux sandbox instead of on the host machine. It keeps filesystem work separate from command and terminal execution while both use the same sandbox. Existing shell, terminal, and language-server features continue to work without E2B-specific tools. The harness, model calls, and session state remain local; the sandbox is ephemeral, experimental, and absent from shipped compositions by default.
-
-## Table of Contents
-
-- [Packages](#packages)
-- [Related documentation](#related-documentation)
-- [Dev Note](#dev-note)
-
------
-
-<a id="packages"></a>
-## Packages
-
-| Package | Role | ctx key |
-|---|---|---|
-| [`e2b`](e2b/README.md) | One shared remote Linux sandbox that file and command work runs in | `ctx.e2b` |
-| [`fs-e2b`](fs-e2b/README.md) | File reads, writes, edits, and listings inside the remote sandbox | `ctx.fs` |
-| [`subprocess-e2b`](subprocess-e2b/README.md) | Shell commands and interactive terminals inside the remote sandbox | `ctx.subprocess` |
-
------
-
-<a id="related-documentation"></a>
-## Related documentation
-
-- [Portable execution-world decision](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md) — why the execution world can move without moving the harness, and what stays local.
-- [Subprocess subsystem](../../docs/subsystems/subprocess.md) — the subprocess seam contract and the generated Cordis surface, including `ctx.e2b`.
-- [Filesystem subsystem](../../docs/subsystems/filesystem.md) — the filesystem seam contract and the generated Cordis surface.
-
------
-
-<a id="dev-note"></a>
-## Dev Note
-
-<details>
-<summary>Working context for maintainers — click to expand</summary>
-
-None.
-
-</details>

+ 0 - 50
packages/e2b/README.zh.md

@@ -1,50 +0,0 @@
----
-description: "E2B 远程运行时组映射:把文件与命令工作放进一个远程 Linux 沙箱,供 E2B 家族的用户与维护者浏览。"
-kind: "package-group"
----
-
-# packages/e2b
-
-[English](README.md) | 中文
-
-## 概述
-
-E2B 家族让 agent(智能体)在一个远程 Linux 沙箱中读取和编辑文件、运行 shell 命令并使用终端,而不是在主机上执行这些工作。文件系统工作与命令和终端执行保持分离,但两者使用同一个沙箱。现有的 shell、终端与语言服务器功能无需 E2B 专用工具即可继续工作。harness 进程、模型调用与会话状态仍在本地;沙箱是临时性的实验环境,且默认不包含在已发布的组合中。
-
-## 目录
-
-- [包](#packages)
-- [相关文档](#related-documentation)
-- [开发备注](#dev-note)
-
------
-
-<a id="packages"></a>
-## 包
-
-| 包(package) | 职责 | ctx 键 |
-|---|---|---|
-| [`e2b`](e2b/README.zh.md) | 承载文件操作与命令执行的共享远程 Linux 沙箱 | `ctx.e2b` |
-| [`fs-e2b`](fs-e2b/README.zh.md) | 远程沙箱内的文件读取、写入、编辑与列表 | `ctx.fs` |
-| [`subprocess-e2b`](subprocess-e2b/README.zh.md) | 远程沙箱内的 shell 命令与交互式终端 | `ctx.subprocess` |
-
------
-
-<a id="related-documentation"></a>
-## 相关文档
-
-- [可移植执行世界决策](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md)——执行世界为何可以在不移动 harness 的情况下迁移,以及哪些内容留在本地。
-- [子进程子系统](../../docs/subsystems/subprocess.zh.md)——子进程 seam 约定与生成的 Cordis 接口,包括 `ctx.e2b`。
-- [文件系统子系统](../../docs/subsystems/filesystem.zh.md)——文件系统 seam 约定与生成的 Cordis 接口。
-
------
-
-<a id="dev-note"></a>
-## 开发备注
-
-<details>
-<summary>维护者的工作上下文——点击展开</summary>
-
-无。
-
-</details>

+ 0 - 6
packages/e2b/e2b/README.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# 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: 09c23e4540b708ada5fc6d485806058793937a09
-README.zh.md: 4bf1c6f220242b366b8f09118a6fd0fee6151d11

+ 0 - 145
packages/e2b/e2b/README.md

@@ -1,145 +0,0 @@
----
-description: "One shared remote Linux sandbox for E2B-backed file and command work: configuration, lifetime, and what happens at startup and shutdown."
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-e2b
-
-English | [中文](README.zh.md)
-
-## Summary
-
-`dsh-e2b` runs the agent's file operations, shell commands, and terminals in one shared remote Linux sandbox instead of on your machine. The sandbox is created at startup and deleted when its configured lifetime expires or the app shuts down, so everything it holds is ephemeral. Configure an API key, an absolute remote working directory, and the sandbox lifetime. Use it with `dsh-fs-e2b` and `dsh-subprocess-e2b`; by itself it adds no user-visible capability. It sends nothing to the model, and no shipped composition enables E2B by default.
-
-## Table of Contents
-
-- [Use this package](#use-this-package)
-- [Understand the implementation](#understand-the-implementation)
-- [Further Exploration](#further-exploration)
-- [Model Experience](#model-experience)
-- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
-- [Dev Note](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## Use this package
-
-Use this package when you want the agent's file and command work to run in a remote Linux sandbox rather than on your machine. It is the foundation of the E2B family: with the filesystem and subprocess packages mounted, all of that work shares one remote working directory and process world.
-
-### When to choose it
-
-Choose the E2B family when work should be isolated from the host machine — for example, when you want the agent's file edits and command runs to happen somewhere disposable. Choose the local filesystem and subprocess packages when running on the host is fine. This package is invisible to the model and adds no request cost.
-
-### Minimal configuration
-
-Three settings matter: an API key (or the `E2B_API_KEY` environment variable), an absolute remote working directory, and the sandbox lifetime. A bad key, a relative working directory, or an invalid lifetime rejects startup before any remote work happens.
-
-```yaml
-- name: '@deepseek-ai/dsh-e2b'
-  config:
-    apiKey: <E2B API key>
-    cwd: /home/user/workspace
-    timeoutMs: 300000
-
-- name: '@deepseek-ai/dsh-subprocess-e2b'
-- name: '@deepseek-ai/dsh-fs-e2b'
-```
-
-| Field | Default | Meaning |
-|---|---|---|
-| `apiKey` | `E2B_API_KEY` | API key for the host SDK connection; never installed in the sandbox |
-| `cwd` | `/home/user/workspace` | Remote working directory the family shares; absolute POSIX path |
-| `timeoutMs` | `300,000` | Sandbox lifetime in milliseconds; the sandbox is deleted when it expires |
-
-The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-e2b) is the exhaustive source for every accepted field and its JSDoc.
-
-### What you get
-
-With this package mounted, file reads and writes, shell commands, and terminals all operate inside the sandbox's working directory, so the agent sees one consistent remote world: what it writes with the file features is what its commands can read, and vice versa. The remote working directory is created if it does not exist yet.
-
-### Starting and stopping the sandbox
-
-Loading the plugin starts the sandbox in the background; the filesystem and subprocess features are ready once it is up. The sandbox lives for the configured lifetime (default five minutes) unless the app stops first — in both cases it is deleted, so save anything you still need before then. If the sandbox disappears while running (expired or removed elsewhere), the family treats that as a clean end rather than an error.
-
------
-
-<a id="understand-the-implementation"></a>
-## Understand the implementation
-
-<details>
-<summary>Implementation internals — click to expand</summary>
-
-This section explains the design decisions behind the owner and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
-
-### Design philosophy
-
-- **One sandbox, one handle.** All adapters await the same `getSandbox()` promise, so filesystem and process operations share one remote Linux world.
-- **Secure by construction.** The sandbox is created with `secure: true` and `lifecycle: { onTimeout: 'kill' }`, so expiry always deletes it.
-- **Isolated control shells.** `e2bControlEnvs()` gives every internal command shell a fresh randomized `HOME`, and `quoteE2BShellArg()` preserves opaque arguments through the SDK's unavoidable `/bin/bash -l -c` layer.
-
-### Source map
-
-| File | Role |
-|---|---|
-| [`src/index.ts`](src/index.ts) | Plugin entry: `E2BRuntime` service, `Config` schema, validation, sandbox open and teardown |
-| — | No runtime invariant companion is published; sandbox creation and teardown have one SDK promise and no independent event or mutable-data relationship to cross-check. |
-
-### Lifecycle
-
-`open()` creates the sandbox, prepares `cwd` and the private runtime root, rejects a non-directory or symlink runtime root, and applies `chmod 700`. Disposal prevents new handle acquisition, awaits setup, and deletes the sandbox, accepting `SandboxNotFoundError` as quiescence. `getSandbox()` re-checks the disposed flag after awaiting readiness, so disposal racing readiness still rejects acquisition; an eager connection failure stays observed but does not reject plugin load, and `getSandbox()` surfaces it.
-
-### Setup failure handling
-
-Any directory-setup failure makes one deletion attempt and preserves the original error; a failed rollback is bounded by E2B's configured sandbox timeout (see the Dev Note). Provider plugins must load after this owner and dispose before it, because every adapter awaits the same handle.
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## Further Exploration
-
-Read these pages when the package-level contract is not enough. They move from the family composition to the subprocess seam surface and the decision evidence behind the remote execution world.
-
-- [E2B provider family map](../README.md) — the three packages and the opt-in composition.
-- [Subprocess subsystem](../../../docs/subsystems/subprocess.md) — the subprocess seam contract and the generated Cordis surface, including `ctx.e2b`.
-- [Portable execution-world decision](../../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md) — why consumers delegate to `ctx.fs` and `ctx.subprocess`, and what stays in the host.
-- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-e2b) — every accepted config field and its source declaration.
-
------
-
-<a id="model-experience"></a>
-## Model Experience
-
-None, as the shared remote-runtime owner registers no model context; provider adapters and consumers own rendered effects.
-
-#### KV Cache effect
-
-No direct invalidation: the owner contributes no request tokens and never mutates a request prefix, so provider cache reuse is unaffected.
-
-## Known Limitations and Deferred Work
-
-<a id="known-limitations-and-deferred-work"></a>
-
-
-These limits define when the E2B family is a poor fit or needs special operational care. They are current package constraints, not a task backlog.
-
-- **Not a whole-harness runtime** — Cordis services, 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.
-
-<a id="dev-note"></a>
-### Dev Note
-
-<details>
-<summary>Working context for maintainers — click to expand</summary>
-
-This Dev Note is working context for maintainers: open questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above and the package code.
-
-#### Open: sandbox setup rollback
-
-The `open()` failure path makes a single deletion attempt and preserves the original setup failure. Retry state stays deferred unless a real double failure outlives E2B's configured sandbox timeout (TODO(e2b-setup-rollback)).
-
-</details>

+ 0 - 145
packages/e2b/e2b/README.zh.md

@@ -1,145 +0,0 @@
----
-description: "E2B 文件与命令工作的共享远程 Linux 沙箱:配置、生命周期,以及启动与关闭时会发生什么。"
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-e2b
-
-[English](README.md) | 中文
-
-## 概述
-
-`dsh-e2b` 让 agent(智能体)的文件操作、shell 命令与终端在一个共享的远程 Linux 沙箱内运行,而不是在你的机器上。应用启动时会创建沙箱,并在配置的生命周期到期或应用关闭时删除它,因此其中保存的一切都是短暂的。请配置 API 密钥、绝对远程工作目录与沙箱生命周期。请与 `dsh-fs-e2b`、`dsh-subprocess-e2b` 一起使用;单独使用它不会带来任何用户可见的能力。它不会向模型发送任何内容,而且任何已发布的组合都不会默认启用 E2B。
-
-## 目录
-
-- [使用本包](#use-this-package)
-- [理解实现](#understand-the-implementation)
-- [进一步探索](#further-exploration)
-- [模型体验](#model-experience)
-- [已知限制与延期工作](#known-limitations-and-deferred-work)
-- [开发备注](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## 使用本包
-
-当你希望 agent 的文件操作与命令执行在远程 Linux 沙箱而非你的机器上进行时,使用本包。它是 E2B 家族的基础:挂载文件系统与子进程包之后,所有这些工作都会共享同一个远程工作目录与进程环境。
-
-### 何时选择
-
-当工作应与宿主机器隔离时——例如你希望 agent 的文件编辑与命令运行发生在某个可丢弃的环境中——选择 E2B 家族。当在宿主上运行没有问题的时候,选择本地的文件系统与子进程包。本包对模型不可见,也不增加任何请求成本。
-
-### 最小配置
-
-三个设置很重要:API 密钥(或 `E2B_API_KEY` 环境变量)、绝对远程工作目录与沙箱生命周期。密钥错误、相对工作目录或无效生命周期都会在任何远程工作开始前拒绝启动。
-
-```yaml
-- name: '@deepseek-ai/dsh-e2b'
-  config:
-    apiKey: <E2B API key>
-    cwd: /home/user/workspace
-    timeoutMs: 300000
-
-- name: '@deepseek-ai/dsh-subprocess-e2b'
-- name: '@deepseek-ai/dsh-fs-e2b'
-```
-
-| 字段 | 默认值 | 含义 |
-|---|---|---|
-| `apiKey` | `E2B_API_KEY` | 宿主 SDK 连接的 API 密钥;绝不会安装进沙箱 |
-| `cwd` | `/home/user/workspace` | 家族共享的远程工作目录;必须是绝对 POSIX 路径 |
-| `timeoutMs` | `300,000` | 沙箱生命周期(毫秒);到期后沙箱被删除 |
-
-生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-e2b)完整列出了每个受支持字段及其 JSDoc,是这些信息的真源。
-
-### 你能得到什么
-
-挂载本包后,文件读写、shell 命令与终端都会在沙箱的工作目录内运行,因此 agent 看到的是一个一致的远程世界:它用文件功能写入的内容,正是它的命令能够读取的内容,反之亦然。远程工作目录若不存在,会自动创建。
-
-### 沙箱的启动与停止
-
-加载插件会在后台启动沙箱;文件系统与子进程功能在其就绪后即可使用。沙箱存活时间为配置的生命周期(默认五分钟),除非应用先停止——两种情况下沙箱都会被删除,因此请在此之前保存你仍需要的内容。如果运行期间沙箱消失(到期或被别处删除),家族会将其视为正常终止,而不是错误。
-
------
-
-<a id="understand-the-implementation"></a>
-## 理解实现
-
-<details>
-<summary>实现细节——点击展开</summary>
-
-本节解释所有者背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
-
-### 设计理念
-
-- **一个沙箱,一个句柄。** 所有适配器都等待同一个 `getSandbox()` promise,因此文件系统与进程操作共享同一个远程 Linux 世界。
-- **构造即安全。** 沙箱以 `secure: true` 和 `lifecycle: { onTimeout: 'kill' }` 创建,因此超时必定删除它。
-- **隔离的控制 shell。** `e2bControlEnvs()` 为每个内部命令 shell 提供全新随机生成的 `HOME`,`quoteE2BShellArg()` 则通过 SDK 不可避免的 `/bin/bash -l -c` 层保留不透明参数。
-
-### 源码索引
-
-| 文件 | 职责 |
-|---|---|
-| [`src/index.ts`](src/index.ts) | 插件入口:`E2BRuntime` 服务、`Config` schema、校验、沙箱创建与拆除 |
-| — | 不发布运行时不变式伴生入口;沙箱创建与拆除只有一个 SDK promise,没有可交叉核对的独立事件或可变数据关系。 |
-
-### 生命周期
-
-`open()` 创建沙箱、准备 `cwd` 与私有运行时根目录、拒绝非目录或符号链接的运行时根目录,并执行 `chmod 700`。dispose(资源释放)会阻止新的句柄获取、等待初始化完成并删除沙箱,把 `SandboxNotFoundError` 视为完全停稳。`getSandbox()` 在等待就绪后重新检查已释放标志,因此与就绪发生竞态的资源释放仍会拒绝获取句柄;预先发起的连接即使失败也会保持可观察状态,但不会导致插件加载失败;`getSandbox()` 会将该失败暴露给调用方。
-
-### 初始化失败处理
-
-任何目录初始化失败都会尝试删除一次并保留原始错误;回滚失败由 E2B 配置的沙箱超时约束(见开发备注)。提供方插件必须在该所有者之后加载、并在其之前 dispose,因为每个适配器都等待同一个句柄。
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## 进一步探索
-
-当包级约定不够用时阅读以下页面。它们从家族组合逐步进入子进程 seam 表面,以及远程执行世界背后的决策证据。
-
-- [E2B 提供方家族地图](../README.zh.md)——三个包与可选组合。
-- [子进程子系统](../../../docs/subsystems/subprocess.zh.md)——子进程 seam 约定与生成的 Cordis 表面,包括 `ctx.e2b`。
-- [可移植执行世界决策](../../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md)——消费方为何委托给 `ctx.fs` 与 `ctx.subprocess`,以及留在宿主中的内容。
-- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-e2b)——每个受支持配置字段及其源声明。
-
------
-
-<a id="model-experience"></a>
-## 模型体验
-
-无。本共享远程运行时所有者不注册任何模型上下文;提供方适配器与消费方拥有所有渲染效果。
-
-#### KV Cache 影响
-
-不会直接失效:所有者不贡献任何请求 token,也从不改变请求前缀,因此提供方缓存复用不受影响。
-
-## 已知限制与延期工作
-
-<a id="known-limitations-and-deferred-work"></a>
-
-
-这些限制说明 E2B 家族何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。
-
-- **不是完整的 harness 运行时**:Cordis 服务、agent/会话状态、会话日志、LLM(大语言模型)请求、skill(技能)和 SDK 侧缓冲仍留在宿主进程中。
-- **沙箱状态是短暂的**:dispose 与超时都会删除沙箱;重新连接、pause/leave 保留、模板、卷和快照均不在本 POC 范围内。
-- **没有配置部署平台**:网络策略、宿主工作区同步与沙箱发现均不在本 POC 范围内。
-- **`cwd` 是解析约定,而不是包含边界**:适配器与命令可以访问沙箱中的其他路径;E2B 网络访问也继续采用基础镜像的策略。
-
-<a id="dev-note"></a>
-### 开发备注
-
-<details>
-<summary>维护者的工作上下文——点击展开</summary>
-
-本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文和包代码为准。
-
-#### 开放:沙箱初始化回滚
-
-`open()` 的失败路径只会尝试删除一次,并保留原始初始化失败。除非真实的双重失败超出 E2B 配置的沙箱超时,否则重试状态保持延后(TODO(e2b-setup-rollback))。
-
-</details>

+ 0 - 56
packages/e2b/e2b/package.json

@@ -1,56 +0,0 @@
-{
-  "name": "@deepseek-ai/dsh-e2b",
-  "description": "Shared E2B sandbox lifecycle for DeepSeek Harness provider adapters",
-  "version": "0.1.5-rc.2",
-  "publishConfig": {
-    "access": "public"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
-    "directory": "packages/e2b/e2b"
-  },
-  "type": "module",
-  "main": "lib/index.js",
-  "types": "lib/types/index.d.ts",
-  "exports": {
-    ".": {
-      "types": "./lib/types/index.d.ts",
-      "default": "./lib/index.js"
-    },
-    "./src/*": "./src/*",
-    "./package.json": "./package.json"
-  },
-  "files": [
-    "lib/index.js",
-    "lib/types/**/*.d.ts"
-  ],
-  "license": "MIT",
-  "peerDependencies": {
-    "@deepseek-ai/cordis": "workspace:^",
-    "@deepseek-ai/dsh-http-proxy": "workspace:^"
-  },
-  "dependencies": {
-    "@deepseek-ai/schemastery": "workspace:^",
-    "e2b": "2.29.1"
-  },
-  "devDependencies": {
-    "@deepseek-ai/cordis": "workspace:^",
-    "@deepseek-ai/dsh-agent": "workspace:^",
-    "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
-    "@deepseek-ai/dsh-app-boot": "workspace:^",
-    "@deepseek-ai/dsh-bash-local": "workspace:^",
-    "@deepseek-ai/dsh-fs-e2b": "workspace:^",
-    "@deepseek-ai/dsh-loader-smoke": "workspace:^",
-    "@deepseek-ai/dsh-lsp": "workspace:^",
-    "@deepseek-ai/dsh-lsp-stdio": "workspace:^",
-    "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
-    "@deepseek-ai/dsh-session-projection": "workspace:^",
-    "@deepseek-ai/dsh-subprocess-e2b": "workspace:^",
-    "@deepseek-ai/dsh-terminal": "workspace:^",
-    "@deepseek-ai/dsh-terminal-bash": "workspace:^",
-    "@deepseek-ai/dsh-http-proxy": "workspace:^",
-    "@deepseek-ai/dsh-launch-environment": "workspace:^"
-  }
-}

+ 0 - 26
packages/e2b/e2b/src/api-url.ts

@@ -1,26 +0,0 @@
-/**
- * The E2B control-plane URL, derived the way the SDK derives it.
- * @module @deepseek-ai/dsh-e2b/src/api-url.ts
- */
-
-/** The SDK's own default control-plane domain; `E2B_DOMAIN` overrides it there and here alike. */
-const E2B_DEFAULT_DOMAIN = 'e2b.app'
-
-/** The debug control plane the SDK substitutes, on loopback and plain HTTP. */
-const E2B_DEBUG_API_URL = 'http://localhost:3000'
-
-/**
- * The control-plane URL the SDK will actually call, derived the way the SDK derives it: an explicit
- * `E2B_API_URL` first, then the debug substitute, then the domain default. Choosing a proxy for
- * anything else would pick the wrong scheme's proxy, ignore a bypass entry naming the real host, and
- * — for the loopback debug plane — hand a proxy the control-plane traffic and its API key.
- *
- * @param env - the process environment to read; overridable so tests need no ambient state.
- * @returns the absolute control-plane URL.
- */
-export function e2bApiUrl(env: NodeJS.ProcessEnv = process.env): string {
-  const explicit = env.E2B_API_URL
-  if (explicit !== undefined && explicit !== '') return explicit
-  if ((env.E2B_DEBUG ?? 'false').toLowerCase() === 'true') return E2B_DEBUG_API_URL
-  return `https://api.${env.E2B_DOMAIN ?? E2B_DEFAULT_DOMAIN}`
-}

+ 0 - 191
packages/e2b/e2b/src/index.ts

@@ -1,191 +0,0 @@
-/**
- * Shared ownership of one E2B sandbox. Capability adapters await the same SDK
- * handle, so filesystem and process operations inhabit one remote Linux world.
- * @module @deepseek-ai/dsh-e2b
- */
-
-import { randomUUID } from 'node:crypto'
-import { posix } from 'node:path'
-import { Context, Service } from '@deepseek-ai/cordis'
-import z from '@deepseek-ai/schemastery'
-import { FileType, Sandbox, SandboxNotFoundError } from 'e2b'
-import { proxyRouteFor } from '@deepseek-ai/dsh-http-proxy'
-import { e2bApiUrl } from './api-url.ts'
-
-export {
-  CommandExitError,
-  FileNotFoundError,
-  FileType,
-  Sandbox,
-  SandboxNotFoundError,
-} from 'e2b'
-export type { CommandHandle, CommandResult, EntryInfo } from 'e2b'
-
-/**
- * Quote one opaque argument for the SDK's unavoidable `/bin/bash -l -c` layer.
- * @param value - Exact argument value to preserve.
- * @returns A single shell word with no interpolation.
- */
-export function quoteE2BShellArg(value: string): string {
-  return `'${value.replaceAll('\'', "'\"'\"'")}'`
-}
-
-/**
- * Isolate E2B's hard-coded login shell behind a fresh randomized home path.
- * @param overrides - Additional environment entries for the internal command.
- * @returns A fresh mutable map that the E2B SDK may extend.
- */
-export function e2bControlEnvs(
-  overrides: Readonly<Record<string, string>> = {},
-): Record<string, string> {
-  return { ...overrides, HOME: `/.dsh-e2b-control-${randomUUID()}` }
-}
-
-/** Configuration for the shared E2B sandbox owner. */
-export interface Config {
-  /** API key; omission reads `E2B_API_KEY`. It is never forwarded into the sandbox. */
-  apiKey?: string
-  /** Shared remote working directory, created before adapters receive the sandbox. */
-  cwd?: string
-  /** E2B sandbox lifetime in milliseconds; expiry always deletes the sandbox. */
-  timeoutMs?: number
-}
-
-interface ResolvedConfig {
-  apiKey: string
-  cwd: string
-  timeoutMs: number
-}
-
-interface SchemaResolvedConfig extends Config {
-  cwd: string
-  timeoutMs: number
-}
-
-declare module '@deepseek-ai/cordis' {
-  interface Context {
-    e2b: E2BRuntime
-  }
-}
-
-
-/**
- * Creates one lazily consumable E2B SDK handle and deletes the sandbox at
- * timeout or disposal. Creation begins at plugin construction; adapters await
- * {@link getSandbox} before their first operation.
- */
-export class E2BRuntime extends Service {
-  static Config: z<Config> = z.object({
-    apiKey: z.string(),
-    cwd: z.string().default('/home/user/workspace'),
-    timeoutMs: z.number().default(300_000),
-  })
-
-  /** Validated remote working directory shared by provider adapters. */
-  readonly cwd: string
-  /** Remote directory reserved for adapter-owned process and terminal state. */
-  readonly runtimeRoot: string
-
-  private readonly config: ResolvedConfig
-  private readonly ready: Promise<Sandbox>
-  private disposed = false
-
-  constructor(ctx: Context, config: Config) {
-    super(ctx, 'e2b')
-    // Schemastery fills these fields before construction; the type does not encode that step.
-    const resolved = config as SchemaResolvedConfig
-    const apiKey = config.apiKey ?? process.env.E2B_API_KEY
-    this.config = {
-      apiKey: apiKey ?? '',
-      cwd: resolved.cwd,
-      timeoutMs: resolved.timeoutMs,
-    }
-    this.validate()
-    this.cwd = this.config.cwd
-    this.runtimeRoot = posix.join(this.cwd, '.dsh-e2b')
-    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.
-    void this.ready.catch(() => {})
-
-    ctx.effect(() => async () => {
-      this.disposed = true
-      let sandbox: Sandbox
-      try {
-        sandbox = await this.ready
-      } catch (_sandboxSetupFailure) {
-        // open() either acquired no sandbox or already made the POC's one rollback attempt.
-        return
-      }
-      try {
-        await sandbox.kill()
-      } catch (error: unknown) {
-        if (!(error instanceof SandboxNotFoundError)) throw error
-      }
-    }, 'e2b sandbox teardown')
-  }
-
-  /**
-   * Return the shared live SDK handle.
-   * @returns the created sandbox after the configured cwd exists.
-   * @throws when E2B rejects creation or the service is disposing.
-   */
-  async getSandbox(): Promise<Sandbox> {
-    if (this.disposed) throw new Error('E2B sandbox service is disposing')
-    const sandbox = await this.ready
-    // Disposal can race the awaited sandbox readiness despite the synchronous precheck.
-    // oxlint-disable-next-line typescript/no-unnecessary-condition -- Awaiting readiness yields to disposal.
-    if (this.disposed) throw new Error('E2B sandbox service is disposing')
-    return sandbox
-  }
-
-  private validate(): void {
-    if (this.config.apiKey.length === 0) {
-      throw new Error('dsh-e2b: configure apiKey or set E2B_API_KEY')
-    }
-    if (!posix.isAbsolute(this.config.cwd)) {
-      throw new Error(`dsh-e2b: cwd must be an absolute Linux path: ${this.config.cwd}`)
-    }
-    if (!Number.isFinite(this.config.timeoutMs) || this.config.timeoutMs <= 0) {
-      throw new Error('dsh-e2b: timeoutMs must be a positive finite number')
-    }
-  }
-
-  private async open(): Promise<Sandbox> {
-    // The SDK builds its own undici dispatcher, so the global one never reaches it; it takes a proxy
-    // URL instead and reads no environment of its own. The decision is made against the URL the SDK
-    // will really call, so a bypass entry naming that host is honored and a loopback debug plane
-    // stays direct.
-    const route = proxyRouteFor(new URL(e2bApiUrl()))
-    const sandbox = await Sandbox.create({
-      apiKey: this.config.apiKey,
-      timeoutMs: this.config.timeoutMs,
-      secure: true,
-      lifecycle: { onTimeout: 'kill' },
-      ...route.proxied ? { proxy: route.proxy } : {},
-    })
-    try {
-      await sandbox.files.makeDir(this.cwd)
-      await sandbox.files.makeDir(this.runtimeRoot)
-      const runtimeRoot = await sandbox.files.getInfo(this.runtimeRoot)
-      if (runtimeRoot.type !== FileType.DIR || runtimeRoot.symlinkTarget !== undefined) {
-        throw new Error(`dsh-e2b: runtime root must be a real directory: ${this.runtimeRoot}`)
-      }
-      await sandbox.commands.run(
-        `chmod 700 -- ${quoteE2BShellArg(this.runtimeRoot)}`,
-        { envs: e2bControlEnvs() },
-      )
-      return sandbox
-    } catch (error: unknown) {
-      try {
-        await sandbox.kill()
-      } catch (_sandboxSetupRollbackFailure) {
-        // TODO(e2b-setup-rollback): Add retry state only if a real double failure
-        // outlives E2B's configured sandbox timeout.
-      }
-      throw error
-    }
-  }
-}
-
-export default E2BRuntime

+ 0 - 186
packages/e2b/e2b/tests/composition.e2e.ts

@@ -1,186 +0,0 @@
-import { access } from 'node:fs/promises'
-import { join, posix } from 'node:path'
-import { fileURLToPath } from 'node:url'
-import { Context } from '@deepseek-ai/cordis'
-import { describe, expect, it } from 'vitest'
-import type { Agent } from '@deepseek-ai/dsh-agent'
-import { runLoaderSmoke } from '@deepseek-ai/dsh-loader-smoke'
-import {
-  FileNotFoundError,
-  Sandbox,
-  SandboxNotFoundError,
-} from '@deepseek-ai/dsh-e2b'
-import TerminalSessionService, { TerminalSessionId } from '@deepseek-ai/dsh-terminal'
-import { BashTerminalBackend } from '@deepseek-ai/dsh-terminal-bash'
-import SandboxPolicyService from '@deepseek-ai/dsh-sandbox-policy'
-import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
-import { Session, SessionId } from '@deepseek-ai/dsh-session'
-import E2BSubprocessRuntime from '@deepseek-ai/dsh-subprocess-e2b'
-import { unsupportedInbox } from '@deepseek-ai/dsh-agent-loop-testkit'
-
-const fixtureRoot = fileURLToPath(new URL('./fixtures/composition/', import.meta.url))
-const binScript = join(fixtureRoot, 'bin.ts')
-const configPath = join(fixtureRoot, 'cordis.yml')
-const tsconfigPath = fileURLToPath(new URL('../../../../tsconfig.json', import.meta.url))
-
-describe.skipIf(!process.env.E2B_API_KEY)('E2B live Loader composition', () => {
-  it('scrubs credentials before actual E2B command and PTY login shells', async () => {
-    const apiKey = process.env.E2B_API_KEY
-    if (apiKey === undefined) throw new Error('E2B_API_KEY disappeared before the PTY environment test')
-    const sandbox = await Sandbox.create({
-      apiKey,
-      envs: { NPM_TOKEN: 'sentinel-secret', DSH_STALE: 'sentinel-stale', KEEP: 'visible' },
-      timeoutMs: 60_000,
-      secure: true,
-      lifecycle: { onTimeout: 'kill' },
-    })
-    try {
-      const profileLeakPath = '/home/user/dsh-e2b-bootstrap-profile-leak'
-      const hostileProfile = [
-        'if [[ "${NPM_TOKEN-}" == "sentinel-secret" ]]; then',
-        `  printf leaked > ${profileLeakPath}`,
-        'fi',
-        '',
-      ].join('\n')
-      await sandbox.files.write([
-        { path: '/home/user/.bash_profile', data: hostileProfile },
-        { path: '/home/user/.profile', data: hostileProfile },
-        { path: '/home/user/.bashrc', data: hostileProfile },
-      ])
-      const ctx = new Context()
-      ctx.provide('e2b', {
-        cwd: '/home/user',
-        runtimeRoot: '/home/user/.dsh-e2b',
-        getSandbox: async () => sandbox,
-      } as never)
-      await ctx.plugin(SessionProjectionRegistry)
-      const sandboxPolicyFiber = await ctx.plugin(SandboxPolicyService, {
-        mode: 'danger-full-access',
-        workspaceRoot: '/home/user',
-      })
-      const ptyFiber = await ctx.plugin(TerminalSessionService)
-      const subprocessFiber = await ctx.plugin(E2BSubprocessRuntime)
-      const node = await ctx.subprocess.resolveExecutable('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({
-        argv: ['/bin/bash', '-c', [
-          'dsh_leak=0',
-          'for dsh_pid in "$PPID" $(ps -o pid= --ppid "$PPID"); do',
-          '  [[ "$dsh_pid" == "$$" ]] && continue',
-          '  if tr "\\0" "\\n" < "/proc/$dsh_pid/environ" 2>/dev/null | grep -Fqx "NPM_TOKEN=sentinel-secret"; then dsh_leak=1; fi',
-          'done',
-          'printf "DIRECT=<%s> LEAK=<%s>\\n" "${NPM_TOKEN-}" "$dsh_leak"',
-        ].join('\n')],
-        cwd: '/home/user',
-        stdio: { stdin: 'ignore', stdout: { maxBytes: 1_024 }, stderr: { maxBytes: 1_024 } },
-        graceMs: 500,
-        env: {},
-      })
-      await expect(environmentProbe.done).resolves.toEqual({ exitCode: 0, signal: null })
-      expect(environmentProbe.collected.stdout?.readFrom(0).text).toBe('DIRECT=<> LEAK=<0>\n')
-      await expect(sandbox.files.read(profileLeakPath)).rejects.toBeInstanceOf(FileNotFoundError)
-      const ownerId = SessionId('e2b-pty-env-owner')
-      const ownerSession = Session.create(ownerId)
-      const owner: Agent = {
-        id: ownerId,
-        options: {},
-        session: ownerSession,
-        inbox: unsupportedInbox(),
-        status: 'idle',
-        ctx,
-        send() {},
-        followup() {},
-        steer() {},
-        inject() {},
-        cancel() {},
-        runMaintenance: task => task(new AbortController().signal),
-        whenIdle: () => Promise.resolve(),
-      }
-      const backend = new BashTerminalBackend(ctx, {
-        backendType: 'shell', shellDialect: 'bash', shellPath: '/bin/bash', shellArgs: ['--noprofile', '--norc', '-i'],
-        rows: 24, cols: 80,
-        scrollbackLines: 100, scrollbackMaxBytes: 65_536, maxReadBytes: 16_384,
-        pollIntervalMs: 25, exactProbeAfterMs: 150, idleSilenceMs: 1_000,
-        handoffGraceMs: 500, timeoutMs: 5_000, disposeGraceMs: 1_000,
-      })
-      const session = await backend.spawn({ sessionId: TerminalSessionId('env'), owner, type: 'shell' })
-      const result = await session.startSend({
-        text: "printf 'NPM=<%s> DSH=<%s> KEEP=<%s>\\n' \"$NPM_TOKEN\" \"$DSH_STALE\" \"$KEEP\"",
-        submit: true,
-      }).done
-      expect(result.viewport).toContain('NPM=<> DSH=<> KEEP=<visible>')
-      expect(result.viewport).not.toContain('sentinel-secret')
-      expect(result.viewport).not.toContain('sentinel-stale')
-      await expect(sandbox.files.read(profileLeakPath)).rejects.toBeInstanceOf(FileNotFoundError)
-      await session.close('environment test complete')
-      await subprocessFiber.dispose()
-      await ptyFiber.dispose()
-      await sandboxPolicyFiber.dispose()
-
-    } finally {
-      await sandbox.kill().catch(() => false)
-    }
-  }, 70_000)
-
-  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-',
-      binScript,
-      libBinScript: binScript,
-      configPath,
-      tsconfigPath,
-      env: {
-        NODE_OPTIONS: [process.env.NODE_OPTIONS, '--disable-warning=ExperimentalWarning'].filter(Boolean).join(' '),
-      },
-      processTimeoutMs: 180_000,
-      inspect: async (cwd) => {
-        for (const name of ['from-fs.txt', 'from-bash.txt', 'multibyte # file.ts', 'fixture-lsp.mjs']) {
-          await expect(access(join(cwd, name))).rejects.toMatchObject({ code: 'ENOENT' })
-        }
-      },
-    })
-
-    expect(stderr).toBe('')
-    const output = JSON.parse(stdout) as Record<string, unknown>
-    expect(output).toMatchObject({
-      bashRead: 'versioned-by-fs\n',
-      fsRead: 'written-by-bash\n',
-      explicitEnvironment: true,
-      splitUtf8Output: '你好',
-      hover: {
-        kind: 'hover',
-        hover: { contents: '**remote hover** 你好 café' },
-      },
-      definition: {
-        kind: 'locations',
-        locations: [{ range: { start: { line: 0, character: 6 }, end: { line: 0, character: 10 } } }],
-      },
-      terminal: {
-        echo: { waitReason: 'stdin_read', sessionStatus: { kind: 'running' } },
-        signal: { delivered: true },
-        interrupted: { sessionStatus: { kind: 'running' } },
-        treeCleanup: true,
-      },
-    })
-    const terminalMotd = (output.terminal as { motd: string }).motd
-    expect(terminalMotd.length).toBeGreaterThan(0)
-    expect(terminalMotd).not.toContain('exec /bin/bash')
-    expect(terminalMotd).not.toContain('.dsh-e2b/terminals/')
-    expect((output.terminal as { echo: { viewport: string } }).echo.viewport).toContain('PTY-你好')
-    expect((output.terminal as { scrollback: string }).scrollback).toContain('PTY-你好')
-    expect((output.terminal as { signal: { targetPgid: number } }).signal.targetPgid).toBeGreaterThan(0)
-    expect(['stdin_read', 'inferred_idle']).toContain(
-      (output.terminal as { interrupted: { waitReason: string } }).interrupted.waitReason,
-    )
-    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)
-    await expect.poll(async () => {
-      const sandboxes = await Sandbox.list({ apiKey }).nextItems()
-      return sandboxes.some(sandbox => sandbox.sandboxId === output.sandboxId)
-    }, { interval: 250, timeout: 5_000 }).toBe(false)
-  }, 195_000)
-})

+ 0 - 238
packages/e2b/e2b/tests/e2b.spec.ts

@@ -1,238 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import type { Mock } from 'vitest'
-import { Context } from '@deepseek-ai/cordis'
-import type { Sandbox as SandboxType } from 'e2b'
-import E2BRuntime, {
-  e2bControlEnvs,
-  FileType,
-  SandboxNotFoundError,
-  quoteE2BShellArg,
-} from '@deepseek-ai/dsh-e2b'
-
-const sdk = vi.hoisted(() => ({
-  create: vi.fn(),
-}))
-
-vi.mock('e2b', async (importOriginal) => {
-  const actual = await importOriginal<typeof import('e2b')>()
-  // The mock replaces only the SDK's static factory surface and is never constructed.
-  // oxlint-disable-next-line typescript/no-extraneous-class -- The SDK contract is a class with a static factory.
-  class FakeSandbox {
-    static create(...args: unknown[]): unknown {
-      return sdk.create(...args)
-    }
-  }
-  return { ...actual, Sandbox: FakeSandbox }
-})
-
-interface SandboxFixture {
-  sandbox: SandboxType
-  makeDir: ReturnType<typeof vi.fn>
-  getInfo: ReturnType<typeof vi.fn>
-  run: Mock<RunCommand>
-  kill: ReturnType<typeof vi.fn>
-}
-
-type RunCommand = (
-  command: string,
-  options?: { envs?: Record<string, string> },
-) => Promise<{ exitCode: number; stdout: string; stderr: string }>
-
-function fakeSandbox(id = 'sandbox-1'): SandboxFixture {
-  const makeDir = vi.fn().mockResolvedValue(true)
-  const getInfo = vi.fn().mockResolvedValue({ type: FileType.DIR })
-  const run = vi.fn<RunCommand>().mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' })
-  const kill = vi.fn().mockResolvedValue(undefined)
-  const sandbox = {
-    sandboxId: id,
-    files: { makeDir, getInfo },
-    commands: { run },
-    kill,
-  } as unknown as SandboxType
-  return { sandbox, makeDir, getInfo, run, kill }
-}
-
-beforeEach(() => {
-  sdk.create.mockReset()
-  vi.unstubAllEnvs()
-})
-
-describe('E2BRuntime', () => {
-  it('gives each SDK login shell a fresh non-overridable control home', () => {
-    const first = e2bControlEnvs({ HOME: '/hostile', NPM_TOKEN: '' })
-    const second = e2bControlEnvs()
-
-    expect(first.HOME).toMatch(/^\/\.dsh-e2b-control-/)
-    expect(first).toEqual({ HOME: first.HOME, NPM_TOKEN: '' })
-    expect(first.HOME).not.toBe(second.HOME)
-  })
-
-  it('creates one protected shared sandbox and kills it on default disposal', async () => {
-    const fixture = fakeSandbox()
-    sdk.create.mockResolvedValue(fixture.sandbox)
-    const ctx = new Context()
-    const fiber = await ctx.plugin(E2BRuntime, { apiKey: 'test-key' })
-
-    const service = ctx.e2b
-    await expect(service.getSandbox()).resolves.toBe(fixture.sandbox)
-    expect(service.cwd).toBe('/home/user/workspace')
-    expect(service.runtimeRoot).toBe('/home/user/workspace/.dsh-e2b')
-    expect(sdk.create).toHaveBeenCalledWith({
-      apiKey: 'test-key',
-      timeoutMs: 300_000,
-      secure: true,
-      lifecycle: { onTimeout: 'kill' },
-    })
-    expect(fixture.makeDir).toHaveBeenNthCalledWith(1, '/home/user/workspace')
-    expect(fixture.makeDir).toHaveBeenNthCalledWith(2, '/home/user/workspace/.dsh-e2b')
-    expect(fixture.getInfo).toHaveBeenCalledWith('/home/user/workspace/.dsh-e2b')
-    const runOptions = fixture.run.mock.calls[0]?.[1]
-    expect(runOptions?.envs?.HOME).toMatch(/^\/\.dsh-e2b-control-/)
-    expect(fixture.run).toHaveBeenCalledWith(
-      "chmod 700 -- '/home/user/workspace/.dsh-e2b'",
-      { envs: { HOME: runOptions?.envs?.HOME } },
-    )
-
-    await fiber.dispose()
-    expect(fixture.kill).toHaveBeenCalledOnce()
-    await expect(service.getSandbox()).rejects.toThrow(/disposing/)
-  })
-
-  it('rejects handle acquisition when disposal starts during setup', async () => {
-    const fixture = fakeSandbox()
-    const opening = Promise.withResolvers<SandboxType>()
-    sdk.create.mockReturnValue(opening.promise)
-    const ctx = new Context()
-    const fiber = await ctx.plugin(E2BRuntime, { apiKey: 'test-key' })
-
-    const acquisition = ctx.e2b.getSandbox()
-    const disposing = fiber.dispose()
-    opening.resolve(fixture.sandbox)
-
-    await expect(acquisition).rejects.toThrow(/disposing/)
-    await expect(disposing).resolves.toBeUndefined()
-    expect(fixture.kill).toHaveBeenCalledOnce()
-  })
-
-  it('reads the key from the environment and honors the configured cwd and lifetime', async () => {
-    vi.stubEnv('E2B_API_KEY', 'environment-key')
-    const fixture = fakeSandbox('configured-sandbox')
-    sdk.create.mockResolvedValue(fixture.sandbox)
-    const ctx = new Context()
-    const fiber = await ctx.plugin(E2BRuntime, {
-      cwd: '/workspace/project',
-      timeoutMs: 60_000,
-    })
-    await ctx.e2b.getSandbox()
-
-    expect(sdk.create).toHaveBeenCalledWith({
-      apiKey: 'environment-key',
-      timeoutMs: 60_000,
-      secure: true,
-      lifecycle: { onTimeout: 'kill' },
-    })
-    expect(ctx.e2b.cwd).toBe('/workspace/project')
-    await fiber.dispose()
-    expect(fixture.kill).toHaveBeenCalledOnce()
-  })
-
-  it('accepts a missing sandbox when disposal itself requests deletion', async () => {
-    const fixture = fakeSandbox()
-    fixture.kill.mockRejectedValue(new SandboxNotFoundError('already deleted'))
-    sdk.create.mockResolvedValue(fixture.sandbox)
-    const ctx = new Context()
-    const errors: unknown[] = []
-    ctx.logger.error = ((error: unknown) => { errors.push(error) }) as typeof ctx.logger.error
-    const fiber = await ctx.plugin(E2BRuntime, { apiKey: 'test-key' })
-    await ctx.e2b.getSandbox()
-
-    await fiber.dispose()
-    expect(fixture.kill).toHaveBeenCalledOnce()
-    expect(errors).toEqual([])
-  })
-
-  it('does not classify other disposal failures as an already-gone sandbox', async () => {
-    const fixture = fakeSandbox()
-    const failure = new Error('disposition unknown')
-    fixture.kill.mockRejectedValue(failure)
-    sdk.create.mockResolvedValue(fixture.sandbox)
-    const ctx = new Context()
-    const errors: unknown[] = []
-    ctx.logger.error = ((error: unknown) => { errors.push(error) }) as typeof ctx.logger.error
-    const fiber = await ctx.plugin(E2BRuntime, { apiKey: 'test-key' })
-    await ctx.e2b.getSandbox()
-    await expect(fiber.dispose()).resolves.toBeUndefined()
-    expect(fixture.kill).toHaveBeenCalledOnce()
-    expect(errors).toContain(failure)
-  })
-
-  it('kills a newly created sandbox when remote directory setup fails', async () => {
-    const fixture = fakeSandbox()
-    fixture.makeDir.mockRejectedValueOnce(new Error('setup failed'))
-    sdk.create.mockResolvedValue(fixture.sandbox)
-    const ctx = new Context()
-    const fiber = await ctx.plugin(E2BRuntime, { apiKey: 'test-key' })
-
-    await expect(ctx.e2b.getSandbox()).rejects.toThrow('setup failed')
-    expect(fixture.kill).toHaveBeenCalledOnce()
-    await fiber.dispose()
-  })
-
-  it('preserves the setup failure after its one rollback attempt fails', async () => {
-    const fixture = fakeSandbox()
-    fixture.run.mockRejectedValueOnce(new Error('chmod failed'))
-    fixture.kill.mockRejectedValueOnce(new Error('cleanup failed'))
-    sdk.create.mockResolvedValue(fixture.sandbox)
-    const ctx = new Context()
-    const fiber = await ctx.plugin(E2BRuntime, { apiKey: 'test-key' })
-    await expect(ctx.e2b.getSandbox()).rejects.toThrow('chmod failed')
-    expect(fixture.kill).toHaveBeenCalledOnce()
-
-    await fiber.dispose()
-    expect(fixture.kill).toHaveBeenCalledOnce()
-  })
-
-  it.each([
-    ['symbolic link', { type: FileType.DIR, symlinkTarget: '/tmp/redirected' }],
-    ['regular file', { type: FileType.FILE }],
-  ])('rejects a reserved runtime root that is a %s', async (_label, info) => {
-    const fixture = fakeSandbox()
-    fixture.getInfo.mockResolvedValueOnce(info)
-    sdk.create.mockResolvedValue(fixture.sandbox)
-    const ctx = new Context()
-    await ctx.plugin(E2BRuntime, { apiKey: 'test-key' })
-
-    await expect(ctx.e2b.getSandbox()).rejects.toThrow('runtime root must be a real directory')
-    expect(fixture.run).not.toHaveBeenCalled()
-    expect(fixture.kill).toHaveBeenCalledOnce()
-  })
-
-  it.each([
-    [{ apiKey: '' }, /configure apiKey/],
-    [{ apiKey: 'x', cwd: 'relative' }, /absolute Linux path/],
-    [{ apiKey: 'x', timeoutMs: 0 }, /positive finite/],
-  ] as const)('fails self-contained configuration before opening E2B: %j', async (config, message) => {
-    vi.stubEnv('E2B_API_KEY', '')
-    const ctx = new Context()
-    await expect(ctx.plugin(E2BRuntime, config)).rejects.toThrow(message)
-    expect(sdk.create).not.toHaveBeenCalled()
-  })
-
-  it('requires a key when both config and the environment omit it', async () => {
-    const original = process.env.E2B_API_KEY
-    delete process.env.E2B_API_KEY
-    try {
-      const ctx = new Context()
-      await expect(ctx.plugin(E2BRuntime, {})).rejects.toThrow(/configure apiKey/)
-    } finally {
-      if (original === undefined) delete process.env.E2B_API_KEY
-      else process.env.E2B_API_KEY = original
-    }
-  })
-})
-
-describe('E2B helpers', () => {
-  it('quotes opaque shell arguments without interpolation', () => {
-    expect(quoteE2BShellArg("a'b $HOME")).toBe("'a'\"'\"'b $HOME'")
-  })
-})

+ 0 - 75
packages/e2b/e2b/tests/egress.spec.ts

@@ -1,75 +0,0 @@
-import { createServer, type Server } from 'node:http'
-import type { AddressInfo } from 'node:net'
-import { afterAll, beforeAll, describe, expect, it } from 'vitest'
-import { installProxyFromEnvironment } from '@deepseek-ai/dsh-http-proxy'
-
-let seen: string[] = []
-let proxy: Server
-let proxyUrl: string
-
-beforeAll(async () => {
-  proxy = createServer((request, response) => {
-    seen.push(`REQ ${request.url ?? ''}`)
-    response.writeHead(502); response.end('fake-proxy')
-  })
-  proxy.on('connect', (request, socket) => {
-    seen.push(`CONNECT ${request.url ?? ''}`)
-    socket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n'); socket.end()
-  })
-  const a = await new Promise<AddressInfo>((r) => { proxy.listen(0, '127.0.0.1', () => { r(proxy.address() as AddressInfo) }) })
-  proxyUrl = `http://127.0.0.1:${String(a.port)}`
-})
-afterAll(async () => { await new Promise<void>((r) => { proxy.close(() => { r() }) }) })
-
-/** The launch environment of a user who exported one proxy for both schemes. */
-function proxyEnv(): { get(name: string): { value: string } | undefined } {
-  return { get: name => (name === 'HTTP_PROXY' || name === 'HTTPS_PROXY' ? { value: proxyUrl } : undefined) }
-}
-async function observe(run: () => Promise<unknown>): Promise<string[]> {
-  seen = []
-  const dispose = await installProxyFromEnvironment(proxyEnv(), () => undefined)
-  try { await run().catch(() => undefined) } finally { await dispose() }
-  return seen
-}
-import { Context } from '@deepseek-ai/cordis'
-import E2bRuntime from '../src/index.ts'
-
-describe('e2b egress', () => {
-  it('reaches the control plane through the proxy', async () => {
-    const observed = await observe(async () => {
-      const ctx = new Context()
-      const fiber = await ctx.plugin(E2bRuntime, { apiKey: `e2b_${'0'.repeat(40)}`, cwd: '/home/user', timeoutMs: 5_000 })
-      await ctx.e2b.getSandbox().catch(() => undefined)
-      await fiber.dispose()
-    })
-    expect(observed.join('|')).toContain('api.e2b.app:443')
-  })
-})
-
-describe('e2b control-plane URL', () => {
-  it('follows the SDK precedence so the proxy decision matches the real target', async () => {
-    const { e2bApiUrl } = await import('../src/api-url.ts')
-    expect(e2bApiUrl({})).toBe('https://api.e2b.app')
-    expect(e2bApiUrl({ E2B_DOMAIN: 'e2b.dev' })).toBe('https://api.e2b.dev')
-    expect(e2bApiUrl({ E2B_DEBUG: 'TRUE' })).toBe('http://localhost:3000')
-    expect(e2bApiUrl({ E2B_API_URL: 'https://api.internal.example', E2B_DEBUG: 'true' }))
-      .toBe('https://api.internal.example')
-  })
-
-  it('keeps the loopback debug plane direct instead of sending its API key to a proxy', async () => {
-    const { e2bApiUrl } = await import('../src/api-url.ts')
-    const { proxyRouteFor } = await import('@deepseek-ai/dsh-http-proxy')
-    const { createLaunchEnvironmentSnapshot } = await import('@deepseek-ai/dsh-launch-environment')
-    // A real launch installs from the environment, and the resolved policy always bypasses loopback.
-    const dispose = await installProxyFromEnvironment(
-      createLaunchEnvironmentSnapshot([{ source: 'process', values: { HTTP_PROXY: proxyUrl } }]),
-      () => undefined,
-    )
-    try {
-      expect(proxyRouteFor(new URL(e2bApiUrl({ E2B_DEBUG: 'true' })))).toEqual({ proxied: false })
-      expect(proxyRouteFor(new URL(e2bApiUrl({})))).toMatchObject({ proxied: true, proxy: proxyUrl })
-    } finally {
-      await dispose()
-    }
-  })
-})

+ 0 - 213
packages/e2b/e2b/tests/fixtures/composition/bin.ts

@@ -1,213 +0,0 @@
-import { readFile } from 'node:fs/promises'
-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-fs-e2b'
-import type {} from '@deepseek-ai/dsh-bash-local'
-import type {} from '@deepseek-ai/dsh-lsp-stdio'
-import type {} from '@deepseek-ai/dsh-terminal-bash'
-
-const configPath = process.argv[2]
-if (configPath === undefined) throw new Error('usage: bin.ts <cordis.yml>')
-
-const ctx = await boot('e2b-composition', resolve(configPath))
-const ownerFiber = ctx.plugin(() => {})
-const ownerId = SessionId('e2b-live-owner')
-const session = Session.create(ownerId)
-const unsupportedInboxMutation = (): never => {
-  throw new Error('the E2B composition owner does not support Inbox mutations')
-}
-const owner: Agent = {
-  id: ownerId,
-  options: {},
-  session,
-  inbox: {
-    nextTurn: [],
-    nextStep: [],
-    clear: unsupportedInboxMutation,
-    append: unsupportedInboxMutation,
-    prepend: unsupportedInboxMutation,
-    replace: unsupportedInboxMutation,
-    remove: unsupportedInboxMutation,
-    splice: unsupportedInboxMutation,
-  },
-  status: 'idle',
-  ctx: ownerFiber.ctx,
-  send() {},
-  followup() {},
-  steer() {},
-  inject() {},
-  cancel() {},
-  runMaintenance: task => task(new AbortController().signal),
-  whenIdle: () => Promise.resolve(),
-}
-const unregisterOwner = ctx.agents.register(owner)
-let terminalId: Awaited<ReturnType<typeof ctx.terminals.spawn>>['sessionId'] | undefined
-try {
-  const sandbox = await ctx.e2b.getSandbox()
-  const fromFs = await ctx.fs.resolve('from-fs.txt')
-  const written = await ctx.fs.writeText(fromFs, 'written-by-fs\n', { kind: 'createIfAbsent' })
-  const observed = await ctx.fs.stat(fromFs)
-  if (observed?.version !== written.version) {
-    throw new Error(`E2B rename did not preserve version metadata: ${JSON.stringify({ written, observed })}`)
-  }
-  await ctx.fs.editText(
-    fromFs,
-    { oldString: 'written-by-fs', newString: 'versioned-by-fs', replaceAll: false },
-    { version: observed.version },
-  )
-  const bashRead = await ctx.shell.run(ctx.shell.resolve({ command: 'cat from-fs.txt' }))
-  if (bashRead.exitCode !== 0 || bashRead.stdout.text !== 'versioned-by-fs\n') {
-    throw new Error(`E2B Bash could not read the FS write: ${JSON.stringify(bashRead)}`)
-  }
-
-  const bashWrite = await ctx.shell.run(ctx.shell.resolve({ command: "printf 'written-by-bash\\n' > from-bash.txt" }))
-  if (bashWrite.exitCode !== 0) {
-    throw new Error(`E2B Bash could not write the shared filesystem: ${JSON.stringify(bashWrite)}`)
-  }
-  const fromBash = await ctx.fs.resolve('from-bash.txt')
-  const fsRead = await ctx.fs.readText(fromBash)
-
-  const environmentHandle = ctx.subprocess.spawn({
-    argv: ['env'],
-    cwd: process.cwd(),
-    stdio: { stdin: 'ignore', stdout: { maxBytes: 65_536 }, stderr: { maxBytes: 4_096 } },
-    graceMs: 500,
-    env: {
-      'FOO-BAR': 'hyphen-value',
-      DSH_EXPLICIT: 'managed-value',
-      TOKEN_EXPLICIT: 'credential-value',
-    },
-  })
-  const environmentOutcome = await environmentHandle.done
-  const environmentText = environmentHandle.collected.stdout?.readFrom(0).text
-  if (environmentOutcome.exitCode !== 0 || environmentText === undefined) {
-    throw new Error(`E2B subprocess environment probe failed: ${JSON.stringify(environmentOutcome)}`)
-  }
-  const environmentLines = new Set(environmentText.trimEnd().split('\n'))
-  const explicitEnvironment = [
-    'FOO-BAR=hyphen-value',
-    'DSH_EXPLICIT=managed-value',
-    'TOKEN_EXPLICIT=credential-value',
-  ].every(entry => environmentLines.has(entry))
-  if (!explicitEnvironment) throw new Error(`E2B subprocess dropped an explicit environment entry: ${environmentText}`)
-
-  const splitUtf8Handle = ctx.subprocess.spawn({
-    argv: ['bash', '-c', "printf '\\344'; sleep 0.05; printf '\\275'; sleep 0.05; printf '\\240'; sleep 0.05; printf '\\345'; sleep 0.05; printf '\\245'; sleep 0.05; printf '\\275'"],
-    cwd: process.cwd(),
-    stdio: { stdin: 'ignore', stdout: { maxBytes: 32 }, stderr: { maxBytes: 4_096 } },
-    graceMs: 500,
-    env: {},
-  })
-  const splitUtf8Outcome = await splitUtf8Handle.done
-  const splitUtf8Output = splitUtf8Handle.collected.stdout?.readFrom(0).text
-  if (splitUtf8Outcome.exitCode !== 0 || splitUtf8Output !== '你好') {
-    throw new Error(`E2B subprocess corrupted split UTF-8 output: ${JSON.stringify({ splitUtf8Outcome, splitUtf8Output })}`)
-  }
-
-  const outputDrainStarted = Date.now()
-  const outputDrainHandle = ctx.subprocess.spawn({
-    argv: ['bash', '-c', "bash -c 'exec -a dsh-output-drain-descendant sleep 30' & printf 'leader-done\\n'"],
-    cwd: process.cwd(),
-    stdio: { stdin: 'ignore', stdout: { maxBytes: 64 }, stderr: { maxBytes: 4_096 } },
-    graceMs: 250,
-    env: {},
-  })
-  const outputDrainOutcome = await outputDrainHandle.done
-  const outputDrainText = outputDrainHandle.collected.stdout?.readFrom(0).text
-  const outputDrainElapsedMs = Date.now() - outputDrainStarted
-  outputDrainHandle.terminate()
-  const outputDrainExited = await outputDrainHandle.waitForExit(AbortSignal.timeout(5_000))
-  const outputDrainProcesses = await sandbox.commands.list()
-  const outputDrainClean = !outputDrainProcesses.some(processInfo =>
-    JSON.stringify([processInfo.cmd, processInfo.args]).includes('dsh-output-drain-descendant'),
-  )
-  if (outputDrainOutcome.exitCode !== 0 || outputDrainText !== 'leader-done\n'
-    || outputDrainElapsedMs >= 10_000 || !outputDrainExited || !outputDrainClean) {
-    throw new Error(`E2B subprocess did not bound descendant-held output: ${JSON.stringify({
-      outputDrainOutcome, outputDrainText, outputDrainElapsedMs, outputDrainExited, outputDrainClean,
-    })}`)
-  }
-
-  const lspFixture = await readFile(new URL('./fixture-lsp.mjs', import.meta.url), 'utf8')
-  const remoteLspFixture = await ctx.fs.resolve('fixture-lsp.mjs')
-  await ctx.fs.writeText(remoteLspFixture, lspFixture, { kind: 'createIfAbsent' })
-  const remoteSource = await ctx.fs.resolve('multibyte # file.ts')
-  await ctx.fs.writeText(remoteSource, 'const café = "你好"\nconsole.log(café)\n', { kind: 'createIfAbsent' })
-  const hover = await ctx.lsp.query({
-    operation: 'hover',
-    filePath: 'multibyte # file.ts',
-    position: { line: 0, character: 7 },
-    workspaceRoot: process.cwd(),
-  })
-  const definition = await ctx.lsp.query({
-    operation: 'goToDefinition',
-    filePath: 'multibyte # file.ts',
-    position: { line: 0, character: 7 },
-    workspaceRoot: process.cwd(),
-  })
-
-  const terminal = await ctx.terminals.spawn(owner, { type: 'shell' })
-  terminalId = terminal.sessionId
-  const terminalEcho = await ctx.terminals.startSend(owner, terminal.sessionId, {
-    text: "printf 'PTY-你好\\n'",
-    submit: true,
-  }).done
-  const sleeping = ctx.terminals.startSend(owner, terminal.sessionId, {
-    text: "printf 'DSH_SLEEP_%s\\n' READY; sleep 30",
-    submit: true,
-  })
-  let sleepReadyOutput = ''
-  const sleepReadyDeadline = Date.now() + 5_000
-  while (!sleepReadyOutput.includes('DSH_SLEEP_READY\n')) {
-    sleepReadyOutput += sleeping.readOutput().delta
-    if (sleepReadyOutput.includes('DSH_SLEEP_READY\n')) break
-    const settled = await Promise.race([
-      sleeping.done.then(result => ({ result })),
-      new Promise<undefined>(resolveDelay => setTimeout(() => { resolveDelay(undefined) }, 25)),
-    ])
-    if (settled !== undefined) {
-      throw new Error(`E2B PTY successor settled before executing: ${JSON.stringify(settled.result)}`)
-    }
-    if (Date.now() >= sleepReadyDeadline) throw new Error(`E2B PTY successor did not execute: ${sleepReadyOutput}`)
-  }
-  const terminalSignal = await ctx.terminals.signal(owner, terminal.sessionId, 'SIGINT')
-  const interrupted = await sleeping.done
-  const stubborn = await ctx.terminals.startSend(owner, terminal.sessionId, {
-    text: "bash -c 'trap \"\" TERM; exec sleep 30' & printf 'DSH_STUBBORN_PID=%s\\n' \"$!\"",
-    submit: true,
-  }).done
-  const stubbornMatch = /DSH_STUBBORN_PID=([1-9][0-9]*)/.exec(stubborn.viewport)
-  if (stubbornMatch?.[1] === undefined) throw new Error(`E2B PTY did not report its stubborn child: ${stubborn.viewport}`)
-  const stubbornPid = Number(stubbornMatch[1])
-  const terminalScrollback = ctx.terminals.read(owner, terminal.sessionId, { count: 50 })
-  await ctx.terminals.kill(owner, terminal.sessionId, 'live E2B composition complete')
-  terminalId = undefined
-  const stubbornProbe = await sandbox.commands.run(`if kill -0 ${stubbornPid} 2>/dev/null; then printf alive; else printf gone; fi`)
-  const terminalTreeCleanup = stubbornProbe.stdout === 'gone'
-  if (!terminalTreeCleanup) throw new Error(`E2B PTY left process ${stubbornPid} alive after close`)
-
-  process.stdout.write(`${JSON.stringify({
-    sandboxId: (await ctx.e2b.getSandbox()).sandboxId,
-    bashRead: bashRead.stdout.text,
-    fsRead,
-    explicitEnvironment,
-    splitUtf8Output,
-    hover,
-    definition,
-    terminal: {
-      motd: terminal.motd,
-      echo: terminalEcho,
-      signal: terminalSignal,
-      interrupted,
-      treeCleanup: terminalTreeCleanup,
-      scrollback: terminalScrollback.text,
-    },
-  })}\n`)
-} finally {
-  if (terminalId !== undefined) await ctx.terminals.kill(owner, terminalId, 'fixture cleanup').catch(() => false)
-  unregisterOwner()
-  await ownerFiber.dispose()
-  await ctx.fiber.dispose()
-}

+ 0 - 60
packages/e2b/e2b/tests/fixtures/composition/cordis.yml

@@ -1,60 +0,0 @@
-# One-world invariant for the E2B package composition:
-# e2b.cwd and sandbox-policy.workspaceRoot must name the same remote directory,
-# which is also bash-local's implicit default workdir.
-- id: e2b
-  name: '@deepseek-ai/dsh-e2b'
-  config:
-    cwd: !!js process.cwd()
-    timeoutMs: 180000
-
-- id: subprocess-e2b
-  name: '@deepseek-ai/dsh-subprocess-e2b'
-
-- id: bash
-  name: '@deepseek-ai/dsh-bash-local'
-  config:
-    timeoutMs: 30000
-
-- id: fs-e2b
-  name: '@deepseek-ai/dsh-fs-e2b'
-
-- id: agents
-  name: '@deepseek-ai/dsh-agent'
-
-- id: session-projection
-  name: '@deepseek-ai/dsh-session-projection'
-
-- id: sandbox-policy
-  name: '@deepseek-ai/dsh-sandbox-policy'
-  config:
-    mode: danger-full-access
-    workspaceRoot: !!js process.cwd()
-
-- id: pty
-  name: '@deepseek-ai/dsh-terminal'
-
-- id: terminal-bash
-  name: '@deepseek-ai/dsh-terminal-bash'
-  config:
-    pollIntervalMs: 25
-    exactProbeAfterMs: 150
-    idleSilenceMs: 2000
-    handoffGraceMs: 500
-    timeoutMs: 5000
-    disposeGraceMs: 1000
-
-- id: lsp
-  name: '@deepseek-ai/dsh-lsp'
-
-- id: lsp-stdio
-  name: '@deepseek-ai/dsh-lsp-stdio'
-  config:
-    servers:
-      fixture:
-        command: node
-        args:
-          - !!js process.cwd() + '/fixture-lsp.mjs'
-        extensionToLanguage:
-          .ts: typescript
-        shutdownTimeoutMs: 1000
-        killGraceMs: 500

+ 0 - 85
packages/e2b/e2b/tests/fixtures/composition/fixture-lsp.mjs

@@ -1,85 +0,0 @@
-import { Buffer } from 'node:buffer'
-
-let pending = Buffer.alloc(0)
-let source = ''
-let sourceUri = ''
-
-function send(message) {
-  const body = Buffer.from(JSON.stringify(message))
-  process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`)
-  process.stdout.write(body)
-}
-
-function respond(id, result) {
-  send({ jsonrpc: '2.0', id, result })
-}
-
-function dispatch(message) {
-  switch (message.method) {
-    case 'initialize':
-      respond(message.id, {
-        capabilities: {
-          positionEncoding: 'utf-16',
-          textDocumentSync: { openClose: true, change: 1 },
-          definitionProvider: true,
-          referencesProvider: true,
-          implementationProvider: true,
-          hoverProvider: true,
-        },
-      })
-      return
-    case 'textDocument/didOpen':
-      source = message.params.textDocument.text
-      sourceUri = message.params.textDocument.uri
-      return
-    case 'textDocument/didClose':
-      source = ''
-      sourceUri = ''
-      return
-    case 'textDocument/hover':
-      if (!source.includes('const café = "你好"')) {
-        send({ jsonrpc: '2.0', id: message.id, error: { code: -32000, message: 'multibyte source was corrupted' } })
-        return
-      }
-      respond(message.id, {
-        contents: { kind: 'markdown', value: '**remote hover** 你好 café' },
-        range: { start: { line: 0, character: 6 }, end: { line: 0, character: 10 } },
-      })
-      return
-    case 'textDocument/definition':
-    case 'textDocument/references':
-    case 'textDocument/implementation':
-      respond(message.id, [{
-        uri: sourceUri,
-        range: { start: { line: 0, character: 6 }, end: { line: 0, character: 10 } },
-      }])
-      return
-    case 'shutdown':
-      respond(message.id, null)
-      return
-    case 'exit':
-      process.exit(0)
-      return
-  }
-}
-
-function drain() {
-  for (;;) {
-    const headerEnd = pending.indexOf('\r\n\r\n')
-    if (headerEnd < 0) return
-    const header = pending.subarray(0, headerEnd).toString('ascii')
-    const match = /(?:^|\r\n)Content-Length: ([0-9]+)(?:\r\n|$)/i.exec(header)
-    if (!match) throw new Error('missing Content-Length')
-    const length = Number(match[1])
-    const bodyStart = headerEnd + 4
-    if (pending.length < bodyStart + length) return
-    const body = pending.subarray(bodyStart, bodyStart + length)
-    pending = pending.subarray(bodyStart + length)
-    dispatch(JSON.parse(body.toString('utf8')))
-  }
-}
-
-process.stdin.on('data', chunk => {
-  pending = Buffer.concat([pending, chunk])
-  drain()
-})

+ 0 - 30
packages/e2b/e2b/tsconfig.json

@@ -1,30 +0,0 @@
-{
-  "extends": "../../../tsconfig.base.json",
-  "compilerOptions": {
-    "rootDir": "src",
-    "outDir": "lib/types"
-  },
-  "include": [
-    "src"
-  ],
-  "references": [
-    {
-      "path": "../../../vendor/cosmokit"
-    },
-    {
-      "path": "../../../vendor/cordis"
-    },
-    {
-      "path": "../../../vendor/schemastery"
-    },
-    {
-      "path": "../../util/brand"
-    },
-    {
-      "path": "../../util/http-proxy"
-    },
-    {
-      "path": "../../util/launch-environment"
-    }
-  ]
-}

+ 0 - 6
packages/e2b/fs-e2b/README.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# 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/fs-e2b/README.md
-README.md: 4f81902a8d36f0e7612eb7d34e411d0ddebda697
-README.zh.md: e20d01314497648e755026b7d9b84c72e24a3b37

+ 0 - 138
packages/e2b/fs-e2b/README.md

@@ -1,138 +0,0 @@
----
-description: "File operations inside the shared remote sandbox: what the agent can do with files there, when to use it, and what to expect — for deployers and maintainers of the E2B family."
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-fs-e2b
-
-English | [中文](README.zh.md)
-
-## Summary
-
-`dsh-fs-e2b` runs the agent's file operations inside the remote sandbox: the agent can read files, list directories, write new files, overwrite or edit existing ones, and get accurate metadata — all in the same remote world where its commands run. It needs no configuration; mounting it moves file work off the host machine. Use it together with `dsh-e2b` and `dsh-subprocess-e2b` so files and commands share one remote working directory. The host machine's files are never touched, and results look to the model exactly like local file results. Choose the local filesystem package instead when files should live on the host.
-
-## Table of Contents
-
-- [Use this package](#use-this-package)
-- [Understand the implementation](#understand-the-implementation)
-- [Further Exploration](#further-exploration)
-- [Model Experience](#model-experience)
-- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
-- [Dev Note](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## Use this package
-
-Use this package when the agent's file work — reading, writing, editing, listing — should happen inside the remote sandbox rather than on your machine. It is the filesystem half of the E2B family: what the agent writes here is what its commands can read in the same sandbox.
-
-### When to choose it
-
-Choose it when a composition already uses the E2B sandbox and you want file operations to run there. Choose the local filesystem package when files should stay on the host. There is no configuration to tune.
-
-### Mounting it
-
-Load the sandbox owner first, then this package; after that, file features operate on the sandbox:
-
-```yaml
-- name: '@deepseek-ai/dsh-e2b'
-- name: '@deepseek-ai/dsh-fs-e2b'
-```
-
-Mounting it does not copy or mirror your local files — the sandbox's working directory starts empty and fills as the agent works.
-
-### Reading files
-
-The agent can read a file's whole contents, stream large files, or read raw bytes up to a size cap or as one byte window. Binary files and files that are not valid UTF-8 text are refused with a clear message instead of being garbled; reads past the size cap fail with a message naming the limit.
-
-### Writing and editing files
-
-The agent can create files, overwrite them, or edit them by replacing a literal piece of text (optionally every occurrence), and can ask for a file to be created only if it does not exist yet. A write lands completely or not at all — a failed write never leaves a partial file. If the file changed since the agent last read it, the write is refused rather than clobbering the newer content, so two processes cannot overwrite each other's work unseen.
-
-### Paths in the sandbox
-
-Relative paths resolve against the caller's working directory or the sandbox's shared working directory, and are reported as the POSIX paths they are in the sandbox — what the agent reads and writes matches exactly what its commands see.
-
------
-
-<a id="understand-the-implementation"></a>
-## Understand the implementation
-
-<details>
-<summary>Implementation internals — click to expand</summary>
-
-This section explains the design decisions behind the provider and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
-
-### Design philosophy
-
-- **One remote world.** Paths and contents stay in the sandbox; the host workspace is never copied, mounted, or reconciled.
-- **Atomic publication.** Every mutation commits through same-filesystem rename or a guarded link, and the returned version comes from the committed entry, so no fallible metadata request follows the commit point.
-- **Strict transport framing.** Canonical paths and contents cross the SDK as ASCII base64 with NUL framing, so newline and multibyte data survive arbitrary decoding boundaries.
-
-### Source map
-
-| File | Role |
-|---|---|
-| [`src/index.ts`](src/index.ts) | Plugin entry: `E2BFileSystem` provider, canonicalization, reads, atomic writes, error mapping |
-| — | No runtime invariant companion is published; each operation returns the E2B controller's committed result directly, with no independent event or cache to cross-check. |
-
-### Canonical paths and transport framing
-
-Relative paths resolve as POSIX paths against the caller `cwd` or `ctx.e2b.cwd`; GNU `realpath -mz` supplies canonical target identity without requiring the final file to exist, and ASCII base64 plus strict NUL framing preserves newline and multibyte paths across the decoded SDK transport. `stat`, no-follow `lstat`, and stable one-level directory listings project E2B metadata into the seam; canonical targets expose absolute POSIX process paths, percent-encoded `file:` URIs, and provider-owned containment checks.
-
-### Write path
-
-Writes create a random sibling staging directory, set it to mode `0700` before uploading content, and preserve an existing file's POSIX mode; replacements publish through E2B's same-filesystem atomic rename, and a guarded `createIfAbsent` publishes with `ln -T` so the commit is atomically no-replace even when a directory appears at the destination. The `dsh-version` extended attribute plus the committed entry's metadata form the returned version; literal edits LF-normalize for matching and restore the dominant CRLF style, and mutations serialize per canonical target. Staging-cleanup failures after commit never turn a successful write into a failure.
-
-### Failure and cancellation
-
-E2B not-found, permission, abort, and other controller failures map to the existing `FsError` codes (`FS_NOT_FOUND`, `FS_PERMISSION_DENIED`, `FS_ABORTED`, `FS_IO_ERROR`), while text and byte reads add `FS_NOT_TEXT` and `FS_TOO_LARGE`. Cancellation is checked at SDK request boundaries and immediately before publication, but the signal is never forwarded into the rename or guarded-link commit, so cancellation cannot interrupt atomic publication or turn a committed write into a reported failure.
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## Further Exploration
-
-Read these pages when the package-level contract is not enough. They move from the family composition to the filesystem seam surface and the tools that render it.
-
-- [E2B provider family map](../README.md) — the sandbox owner and the three-package composition.
-- [Filesystem subsystem](../../../docs/subsystems/filesystem.md) — the filesystem seam contract and the generated Cordis surface.
-- [Filesystem provider contract](../../fs/fs/README.md) — the `FileSystem` interface this provider implements.
-- [File tools](../../fs/tool-fs/README.md) — the tools that render filesystem results to the model.
-
------
-
-<a id="model-experience"></a>
-## Model Experience
-
-Indirectly, through [`dsh-tool-fs`](../../fs/tool-fs/README.md), which renders remote UTF-8 content, directory results, mutation acknowledgements, and provider errors while E2B identity and transport remain internal.
-
-#### KV Cache effect
-
-No direct invalidation: `dsh-tool-fs` owns any request-prefix changes; the E2B transport never reaches a request.
-
-## Known Limitations and Deferred Work
-
-<a id="known-limitations-and-deferred-work"></a>
-
-
-These limits define when the provider is a poor fit or needs special operational care. They are current package constraints, not a task backlog.
-
-- **No host synchronization** — an empty E2B cwd stays empty until a tool, command, or external process populates it; local files are neither uploaded nor reflected back.
-- **Mutation coordination is host-process-local** — `createIfAbsent` preserves a remote creator racing publication, but another harness connection or command can still race replacement; version guards detect only metadata changes represented by E2B.
-- **Reads reopen canonical targets by path** — a concurrent remote path replacement between resolution and stream opening is not fenced by a stable file handle; no observed product defect justifies a provider-specific bounded-read protocol in this POC.
-- **Whole-file mutation costs remain** — overwrite diffs and literal edits read complete files into host memory, and every operation incurs E2B controller latency.
-- **The POC targets E2B's default Linux image** — it relies on GNU `realpath`/`base64`/`chmod`, same-filesystem rename, streaming reads, and metadata extended attributes; custom templates are outside this POC.
-
-<a id="dev-note"></a>
-### Dev Note
-
-<details>
-<summary>Working context for maintainers — click to expand</summary>
-
-None.
-
-</details>

+ 0 - 138
packages/e2b/fs-e2b/README.zh.md

@@ -1,138 +0,0 @@
----
-description: "共享远程沙箱内的文件操作:agent(智能体)可以在那里对文件做什么、何时使用,以及可以期待什么——面向 E2B 家族的部署方与维护者。"
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-fs-e2b
-
-[English](README.md) | 中文
-
-## 概述
-
-`dsh-fs-e2b` 让 agent 的文件操作在远程沙箱内运行:agent 可以读取文件、列出目录、写入新文件、覆盖或编辑现有文件,并获得准确的元数据——这些操作都发生在其命令运行的同一远程环境中。它不需要任何配置;挂载它就把文件工作从宿主机器上移走。请与 `dsh-e2b`、`dsh-subprocess-e2b` 一起使用,让文件与命令共享同一个远程工作目录。宿主机器上的文件永远不会被触及,模型看到的结果与本地文件结果完全一致。当文件应留在宿主上时,请选择本地的文件系统包。
-
-## 目录
-
-- [使用本包](#use-this-package)
-- [理解实现](#understand-the-implementation)
-- [进一步探索](#further-exploration)
-- [模型体验](#model-experience)
-- [已知限制与延期工作](#known-limitations-and-deferred-work)
-- [开发备注](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## 使用本包
-
-当 agent 的文件工作——读取、写入、编辑、列出——应在远程沙箱内而非你的机器上进行时,使用本包。它是 E2B 家族中的文件系统部分:agent 在这里写入的内容,正是它的命令能在同一个沙箱中读取的内容。
-
-### 何时选择
-
-当组合已经使用 E2B 沙箱且希望文件操作在其中运行时,选择本包。当文件应留在宿主上时,选择本地文件系统包。这里没有需要调优的配置。
-
-### 挂载
-
-先加载沙箱所有者,再加载本包;之后文件功能就会作用于沙箱:
-
-```yaml
-- name: '@deepseek-ai/dsh-e2b'
-- name: '@deepseek-ai/dsh-fs-e2b'
-```
-
-挂载它不会复制或镜像你的本地文件——沙箱的工作目录从空开始,并随 agent 的工作逐渐被填充。
-
-### 读取文件
-
-agent 可以读取文件的完整内容、流式读取大文件,或在大小上限内或按字节窗口读取原始字节。二进制文件与不是有效 UTF-8 文本的文件会被明确拒绝而不是乱码显示;超过大小上限的读取会以指明上限的消息失败。
-
-### 写入与编辑文件
-
-agent 可以创建文件、覆盖文件,或通过替换一段字面量文本(可选地替换所有出现处)来编辑文件,也可以要求仅在文件尚不存在时创建它。一次写入要么完整落地,要么完全不落地——失败的写入绝不会留下不完整的文件。如果文件在 agent 上次读取之后发生了变化,写入会被拒绝而不是覆盖较新的内容,因此两个进程不会在互不知情的情况下覆盖彼此的工作。
-
-### 沙箱中的路径
-
-相对路径会针对调用方的工作目录或沙箱共享工作目录解析,并按其在沙箱中的 POSIX 路径报告——agent 读写的内容与它的命令看到的内容完全一致。
-
------
-
-<a id="understand-the-implementation"></a>
-## 理解实现
-
-<details>
-<summary>实现细节——点击展开</summary>
-
-本节解释提供方背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
-
-### 设计理念
-
-- **同一远程环境。** 路径与内容都留在沙箱内;宿主工作区永远不会被复制、挂载或协调。
-- **原子发布。** 每次变更都通过同一文件系统 rename 或受防护链接提交,返回的版本来自已提交条目,因此提交点之后不会再有可能失败的元数据请求。
-- **严格的传输分帧。** 规范化路径与内容以带 NUL 分帧的 ASCII base64 形式跨越 SDK,因此换行与多字节数据能经受任意解码边界。
-
-### 源码地图
-
-| 文件 | 职责 |
-|---|---|
-| [`src/index.ts`](src/index.ts) | 插件入口:`E2BFileSystem` 提供方、规范化、读取、原子写入、错误映射 |
-| — | 不发布运行时不变式配套项;每个操作都直接返回 E2B 控制器的已提交结果,没有可用于交叉核验的独立事件或缓存。 |
-
-### 规范化路径与传输分帧
-
-相对路径以调用方 `cwd` 或 `ctx.e2b.cwd` 为基准,按照 POSIX 路径解析;GNU `realpath -mz` 提供规范化目标身份,且不要求最终文件存在;ASCII base64 加严格 NUL 分帧会在已解码的 SDK 传输中保留含换行符和多字节字符的路径。`stat`、不跟随链接的 `lstat` 与稳定的单层目录列表会把 E2B 元数据投影到 seam;规范化目标公开绝对 POSIX 进程路径、百分号编码的 `file:` URI,以及由提供方负责的包含关系检查。
-
-### 写入路径
-
-写入会创建随机的同级暂存目录,在上传内容前将其 mode 设为 `0700`,并保留现有文件的 POSIX mode;替换操作通过 E2B 的同一文件系统原子重命名发布,带防护的 `createIfAbsent` 改用 `ln -T` 发布,即使目标位置出现目录,也能使提交具备原子且不替换的语义。`dsh-version` 扩展属性与已提交条目的元数据共同构成返回的版本;字面量编辑匹配时会规范化为 LF,并恢复占主导的 CRLF 风格,变更按规范化目标串行执行。提交后的暂存清理失败绝不会把一次成功的写入变成失败。
-
-### 失败与取消
-
-E2B 的未找到、权限、中止及其他控制器故障会映射到现有 `FsError` 错误码(`FS_NOT_FOUND`、`FS_PERMISSION_DENIED`、`FS_ABORTED`、`FS_IO_ERROR`),文本与字节读取则增加 `FS_NOT_TEXT` 与 `FS_TOO_LARGE`。取消在 SDK 请求边界与发布前立即检查,但信号永远不会传入 rename 或受防护链接提交,因此取消无法中断原子发布,也不会把已提交的写入报告为失败。
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## 进一步探索
-
-当包级约定不够用时阅读以下页面。这些页面从家族组合讲起,逐步深入到文件系统 seam 接口及其渲染工具。
-
-- [E2B 提供方家族地图](../README.zh.md)——沙箱所有者与三包组合。
-- [文件系统子系统](../../../docs/subsystems/filesystem.zh.md)——文件系统 seam 约定与生成的 Cordis 接口。
-- [文件系统提供方约定](../../fs/fs/README.zh.md)——本提供方实现的 `FileSystem` 接口。
-- [文件工具](../../fs/tool-fs/README.zh.md)——向模型渲染文件系统结果的工具。
-
------
-
-<a id="model-experience"></a>
-## 模型体验
-
-通过 [`dsh-tool-fs`](../../fs/tool-fs/README.zh.md) 间接影响模型;该工具渲染远程 UTF-8 内容、目录结果、变更确认与提供方错误,而 E2B 身份与传输保持内部实现。
-
-#### KV Cache 影响
-
-不会直接失效:请求前缀变更由 `dsh-tool-fs` 负责;E2B 传输永远不会进入请求。
-
-## 已知限制与延期工作
-
-<a id="known-limitations-and-deferred-work"></a>
-
-
-这些限制说明本提供方何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是待办事项清单。
-
-- **不提供宿主同步**:空的 E2B cwd 会一直为空,直到工具、命令或外部进程填充它;本地文件既不会上传,也不会同步回本地。
-- **变更协调仅限宿主进程内**:`createIfAbsent` 会保留与发布发生竞态的远程创建者所写入的文件,但另一个 harness 连接或命令仍可能与替换操作发生竞态;版本防护只能检测 E2B 元数据所体现的变更。
-- **读取会按路径重新打开规范化目标**:在解析与打开流之间若并发替换远程路径,该操作没有稳定文件句柄提供围栏;在该 POC 中,没有已观察到的产品缺陷能够证明提供方专用的有界读取协议值得引入。
-- **仍需承担完整文件变更成本**:覆盖差异与字面量编辑会把完整文件读入宿主内存,每项操作也都会产生 E2B 控制器延迟。
-- **该 POC 面向 E2B 默认 Linux 镜像**:它依赖 GNU `realpath`/`base64`/`chmod`、同一文件系统内的 rename、流式读取与元数据扩展属性;自定义模板不在该 POC 范围内。
-
-<a id="dev-note"></a>
-### 开发备注
-
-<details>
-<summary>维护者的工作上下文——点击展开</summary>
-
-无。
-
-</details>

+ 0 - 39
packages/e2b/fs-e2b/package.json

@@ -1,39 +0,0 @@
-{
-  "name": "@deepseek-ai/dsh-fs-e2b",
-  "description": "E2B filesystem implementation for DeepSeek Harness",
-  "version": "0.1.5-rc.2",
-  "publishConfig": {
-    "access": "public"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
-    "directory": "packages/e2b/fs-e2b"
-  },
-  "type": "module",
-  "main": "lib/index.js",
-  "types": "lib/types/index.d.ts",
-  "exports": {
-    ".": {
-      "types": "./lib/types/index.d.ts",
-      "default": "./lib/index.js"
-    },
-    "./src/*": "./src/*",
-    "./package.json": "./package.json"
-  },
-  "files": [
-    "lib/index.js",
-    "lib/types/**/*.d.ts"
-  ],
-  "license": "MIT",
-  "peerDependencies": {
-    "@deepseek-ai/dsh-e2b": "workspace:^",
-    "@deepseek-ai/dsh-fs": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
-  },
-  "devDependencies": {
-    "@deepseek-ai/dsh-e2b": "workspace:^",
-    "@deepseek-ai/dsh-fs": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
-  }
-}

+ 0 - 628
packages/e2b/fs-e2b/src/index.ts

@@ -1,628 +0,0 @@
-/**
- * E2B provider for the filesystem capability seam. Paths, contents, and
- * atomic staging files remain inside the shared remote sandbox.
- * @module @deepseek-ai/dsh-fs-e2b
- */
-
-import { createHash, randomUUID } from 'node:crypto'
-import { Buffer } from 'node:buffer'
-import { posix } from 'node:path'
-import { FileSystem, FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
-import type {
-  FsDirEntry,
-  FsEditOutcome,
-  FsEditRequest,
-  FsInfo,
-  FsPathInfo,
-  FsTarget,
-  FsWriteIntent,
-  FsWriteOutcome,
-} from '@deepseek-ai/dsh-fs'
-import {
-  CommandExitError,
-  e2bControlEnvs,
-  FileNotFoundError,
-  FileType,
-  quoteE2BShellArg,
-} from '@deepseek-ai/dsh-e2b'
-import type { EntryInfo, Sandbox } from '@deepseek-ai/dsh-e2b'
-
-const VERSION_METADATA_KEY = 'dsh-version'
-const BINARY_SAMPLE_BYTES = 8192
-const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
-
-function assertNotAborted(signal: AbortSignal | undefined, operation: string): void {
-  if (signal?.aborted === true) throw new FsError(`${operation} aborted`, 'FS_ABORTED')
-}
-
-function normalizeLineEndings(value: string): string {
-  return value.replaceAll('\r\n', '\n')
-}
-
-function detectsCrlf(value: string): boolean {
-  const sample = value.slice(0, 4096)
-  const crlf = sample.split('\r\n').length - 1
-  const lf = sample.split('\n').length - 1 - crlf
-  return crlf > lf
-}
-
-function restoreLineEndings(value: string, crlf: boolean): string {
-  return crlf ? normalizeLineEndings(value).replaceAll('\n', '\r\n') : value
-}
-
-function decodeText(bytes: Uint8Array, displayPath: string, binarySampleBytes: number): string {
-  if (bytes.subarray(0, binarySampleBytes).includes(0)) {
-    throw new FsError(`cannot read "${displayPath}": binary file`, 'FS_NOT_TEXT')
-  }
-  try {
-    return new TextDecoder('utf-8', { fatal: true }).decode(bytes)
-  } catch (error: unknown) {
-    throw new FsError(`cannot read "${displayPath}": invalid UTF-8 text`, 'FS_NOT_TEXT', { cause: error })
-  }
-}
-
-function decodeCanonicalPath(encoded: string): string {
-  if (encoded.length === 0 || !BASE64.test(encoded)) {
-    throw new Error('fs-e2b: canonical path transport returned invalid base64')
-  }
-  const framed = Buffer.from(encoded, 'base64')
-  if (framed.toString('base64') !== encoded
-    || framed.length < 2
-    || framed.at(-1) !== 0
-    || framed.subarray(0, -1).includes(0)) {
-    throw new Error('fs-e2b: canonical path transport returned invalid NUL framing')
-  }
-  let path: string
-  try {
-    path = new TextDecoder('utf-8', { fatal: true }).decode(framed.subarray(0, -1))
-  } catch (error: unknown) {
-    throw new Error('fs-e2b: canonical path is not valid UTF-8', { cause: error })
-  }
-  if (!posix.isAbsolute(path)) throw new Error('fs-e2b: canonical path is not absolute')
-  return path
-}
-
-function signalOpts(signal: AbortSignal | undefined): { signal?: AbortSignal } {
-  return signal === undefined ? {} : { signal }
-}
-
-function commandOpts(signal: AbortSignal | undefined): { envs: Record<string, string>; signal?: AbortSignal } {
-  return { envs: e2bControlEnvs(), ...signalOpts(signal) }
-}
-
-async function openReadStream(
-  sandbox: Sandbox,
-  target: FsTarget,
-  signal: AbortSignal | undefined,
-): Promise<ReadableStream<Uint8Array>> {
-  try {
-    // The pinned SDK's stream overload lies for empty files: content-length 0
-    // returns '' instead of a ReadableStream.
-    const read = await sandbox.files.read(String(target.targetKey), { format: 'stream', ...signalOpts(signal) }) as
-      ReadableStream<Uint8Array> | string
-    return typeof read === 'string'
-      ? new ReadableStream<Uint8Array>({ start(controller) { controller.close() } })
-      : read
-  } catch (error: unknown) {
-    throw mapError(error, 'read', target.displayPath, signal)
-  }
-}
-
-function entryType(entry: EntryInfo): FsInfo['type'] {
-  switch (entry.type) {
-    case FileType.FILE:
-      return 'file'
-    case FileType.DIR:
-      return 'directory'
-    default:
-      return 'other'
-  }
-}
-
-function entryVersion(entry: EntryInfo): ReturnType<typeof FsVersion> {
-  const facts = JSON.stringify([
-    entry.metadata?.[VERSION_METADATA_KEY],
-    entry.path,
-    entry.type,
-    entry.size,
-    entry.mode,
-    entry.modifiedTime?.toISOString(),
-    entry.symlinkTarget,
-  ])
-  return FsVersion(`e2b:${createHash('sha256').update(facts).digest('hex')}`)
-}
-
-function mapError(error: unknown, operation: string, displayPath: string, signal?: AbortSignal): FsError {
-  if (error instanceof FsError) return error
-  if (signal?.aborted === true || (error instanceof DOMException && error.name === 'AbortError')) {
-    return new FsError(`${operation} aborted`, 'FS_ABORTED', { cause: error })
-  }
-  if (error instanceof FileNotFoundError) {
-    return new FsError(`cannot ${operation} "${displayPath}": not found`, 'FS_NOT_FOUND', { cause: error })
-  }
-  if (/permission denied|operation not permitted/i.test(String(error))) {
-    return new FsError(`cannot ${operation} "${displayPath}": permission denied`, 'FS_PERMISSION_DENIED', { cause: error })
-  }
-  return new FsError(`cannot ${operation} "${displayPath}": ${String(error)}`, 'FS_IO_ERROR', { cause: error })
-}
-
-function literalEdit(content: string, request: FsEditRequest, displayPath: string): string {
-  const oldString = normalizeLineEndings(request.oldString)
-  const newString = normalizeLineEndings(request.newString)
-  if (oldString.length === 0) {
-    throw new FsError(`cannot edit "${displayPath}": old_string must be non-empty`, 'FS_EDIT_NOT_FOUND')
-  }
-  let matches = 0
-  let offset = 0
-  while (true) {
-    const found = content.indexOf(oldString, offset)
-    if (found < 0) break
-    matches += 1
-    offset = found + oldString.length
-  }
-  if (matches === 0) throw new FsError(`cannot edit "${displayPath}": old_string was not found`, 'FS_EDIT_NOT_FOUND')
-  if (!request.replaceAll && matches !== 1) {
-    throw new FsError(`cannot edit "${displayPath}": old_string matched ${matches} times`, 'FS_AMBIGUOUS_EDIT')
-  }
-  return request.replaceAll ? content.split(oldString).join(newString) : content.replace(oldString, newString)
-}
-
-/** Remote filesystem backend sharing the sandbox owned by `ctx.e2b`. */
-export class E2BFileSystem extends FileSystem {
-  static inject = ['e2b']
-
-  private readonly locks = new Map<string, Promise<unknown>>()
-
-  override async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {
-    assertNotAborted(opts?.signal, 'resolve')
-    if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')
-    const displayPath = posix.resolve(opts?.cwd ?? this.ctx.e2b.cwd, path)
-    try {
-      const sandbox = await this.ctx.e2b.getSandbox()
-      const targetKey = await this.canonicalPath(sandbox, displayPath, opts?.signal)
-      assertNotAborted(opts?.signal, 'resolve')
-      return { targetKey: FsTargetKey(targetKey), displayPath }
-    } catch (error: unknown) {
-      throw mapError(error, 'resolve', displayPath, opts?.signal)
-    }
-  }
-
-  override processPath(target: FsTarget): string {
-    return String(target.targetKey)
-  }
-
-  override fileUrl(target: FsTarget): string {
-    const path = this.processPath(target)
-    if (!posix.isAbsolute(path)) throw new Error(`fs-e2b: expected an absolute process path: ${JSON.stringify(path)}`)
-    return `file://${path.split('/').map(segment => encodeURIComponent(segment)).join('/')}`
-  }
-
-  override contains(parent: FsTarget, child: FsTarget): boolean {
-    const relative = posix.relative(this.processPath(parent), this.processPath(child))
-    return relative === '' || (relative !== '..' && !relative.startsWith('../') && !posix.isAbsolute(relative))
-  }
-
-  override async stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined> {
-    assertNotAborted(signal, 'stat')
-    const entry = await this.probe(String(target.targetKey), target.displayPath, signal)
-    if (entry === undefined) return undefined
-    return {
-      version: entryVersion(entry),
-      type: entryType(entry),
-      ...(entry.type === FileType.FILE ? { size: entry.size } : {}),
-    }
-  }
-
-  override async lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined> {
-    assertNotAborted(signal, 'lstat')
-    if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')
-    const displayPath = posix.resolve(opts?.cwd ?? this.ctx.e2b.cwd, path)
-    const entry = await this.probe(displayPath, displayPath, signal)
-    if (entry === undefined) return undefined
-    const type = entry.symlinkTarget !== undefined
-      ? 'symlink' as const
-      : entry.type === FileType.FILE
-        ? 'file' as const
-        : entry.type === FileType.DIR
-          ? 'directory' as const
-          : 'other' as const
-    return {
-      version: entryVersion(entry),
-      type,
-      ...(entry.type === FileType.FILE ? { size: entry.size } : {}),
-    }
-  }
-
-  override async readText(target: FsTarget, signal?: AbortSignal): Promise<string> {
-    const sandbox = await this.ctx.e2b.getSandbox()
-    await this.requireRegular(target, signal)
-    try {
-      const bytes = await sandbox.files.read(String(target.targetKey), { format: 'bytes', ...signalOpts(signal) })
-      assertNotAborted(signal, 'read')
-      return decodeText(bytes, target.displayPath, BINARY_SAMPLE_BYTES)
-    } catch (error: unknown) {
-      throw mapError(error, 'read', target.displayPath, signal)
-    }
-  }
-
-  override async readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array> {
-    const sandbox = await this.ctx.e2b.getSandbox()
-    const info = await this.requireRegular(target, signal)
-    if (info.size !== undefined && info.size > maxBytes) {
-      throw new FsError(`cannot read "${target.displayPath}": ${info.size} bytes exceeds the ${maxBytes}-byte limit`, 'FS_TOO_LARGE')
-    }
-    const stream = await openReadStream(sandbox, target, signal)
-    const reader = stream.getReader()
-    const chunks: Uint8Array[] = []
-    let bytes = 0
-    let completed = false
-    try {
-      while (true) {
-        assertNotAborted(signal, 'read')
-        const next = await reader.read()
-        if (next.done) break
-        // The stat preflight covers the at-rest case; this streamed bound stops
-        // a post-stat grower without transferring past the first overflowing chunk.
-        bytes += next.value.byteLength
-        if (bytes > maxBytes) {
-          throw new FsError(`cannot read "${target.displayPath}": content exceeds the ${maxBytes}-byte limit`, 'FS_TOO_LARGE')
-        }
-        chunks.push(next.value)
-      }
-      completed = true
-    } catch (error: unknown) {
-      throw mapError(error, 'read', target.displayPath, signal)
-    } finally {
-      if (!completed) {
-        try {
-          await reader.cancel()
-        } catch (_streamCancellationFailure) {
-          // The read already failed; a cancellation failure on the abandoned
-          // remote stream adds nothing actionable for the caller.
-        }
-      }
-      reader.releaseLock()
-    }
-    const whole = new Uint8Array(bytes)
-    let offset = 0
-    for (const chunk of chunks) {
-      whole.set(chunk, offset)
-      offset += chunk.byteLength
-    }
-    return whole
-  }
-
-  override async readByteRange(target: FsTarget, range: { offset: number; length: number }, signal?: AbortSignal): Promise<Uint8Array> {
-    const sandbox = await this.ctx.e2b.getSandbox()
-    await this.requireRegular(target, signal)
-    if (range.length === 0) return new Uint8Array(0)
-    // The SDK streams only from the file's start: skip to `offset`, keep
-    // `length` bytes, and cancel the stream there, so no more than the window
-    // beyond the skipped prefix is ever transferred.
-    const stream = await openReadStream(sandbox, target, signal)
-    const reader = stream.getReader()
-    const window = new Uint8Array(range.length)
-    const end = range.offset + range.length
-    let position = 0
-    let filled = 0
-    let drained = false
-    try {
-      while (filled < range.length) {
-        assertNotAborted(signal, 'read')
-        const next = await reader.read()
-        if (next.done) {
-          drained = true
-          break
-        }
-        const from = Math.max(range.offset, position)
-        const to = Math.min(end, position + next.value.byteLength)
-        if (to > from) {
-          window.set(next.value.subarray(from - position, to - position), filled)
-          filled += to - from
-        }
-        position += next.value.byteLength
-      }
-    } catch (error: unknown) {
-      throw mapError(error, 'read', target.displayPath, signal)
-    } finally {
-      if (!drained) {
-        try {
-          await reader.cancel()
-        } catch (_streamCancellationFailure) {
-          // The window is complete or the read already failed; a cancellation
-          // failure on the abandoned remote stream adds nothing actionable.
-        }
-      }
-      reader.releaseLock()
-    }
-    return filled === range.length ? window : window.subarray(0, filled)
-  }
-
-  override async streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>> {
-    const sandbox = await this.ctx.e2b.getSandbox()
-    await this.requireRegular(target, signal)
-    const stream = await openReadStream(sandbox, target, signal)
-    const displayPath = target.displayPath
-    return {
-      async *[Symbol.asyncIterator](): AsyncGenerator<string> {
-        const reader = stream.getReader()
-        const decoder = new TextDecoder('utf-8', { fatal: true })
-        let sampledBytes = 0
-        let completed = false
-        try {
-          while (true) {
-            assertNotAborted(signal, 'read')
-            const next = await reader.read()
-            if (next.done) break
-            if (sampledBytes < BINARY_SAMPLE_BYTES) {
-              const sample = next.value.subarray(0, BINARY_SAMPLE_BYTES - sampledBytes)
-              if (sample.includes(0)) throw new FsError(`cannot read "${displayPath}": binary file`, 'FS_NOT_TEXT')
-              sampledBytes += sample.length
-            }
-            let text: string
-            try {
-              text = decoder.decode(next.value, { stream: true })
-            } catch (error: unknown) {
-              throw new FsError(`cannot read "${displayPath}": invalid UTF-8 text`, 'FS_NOT_TEXT', { cause: error })
-            }
-            if (text.length > 0) yield text
-          }
-          try {
-            decoder.decode()
-          } catch (error: unknown) {
-            throw new FsError(`cannot read "${displayPath}": invalid UTF-8 text`, 'FS_NOT_TEXT', { cause: error })
-          }
-          completed = true
-        } catch (error: unknown) {
-          throw mapError(error, 'read', displayPath, signal)
-        } finally {
-          if (!completed) {
-            try {
-              await reader.cancel()
-            } catch (_streamCancellationFailure) {
-              // The primary read outcome owns the result; cancellation is best-effort after early stop.
-            }
-          }
-          reader.releaseLock()
-        }
-      },
-    }
-  }
-
-  override async listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]> {
-    const info = await this.stat(target, signal)
-    if (info === undefined) throw new FsError(`cannot list "${target.displayPath}": not found`, 'FS_NOT_FOUND')
-    if (info.type !== 'directory') throw new FsError(`cannot list "${target.displayPath}": not a directory`, 'FS_NOT_DIRECTORY')
-    try {
-      const sandbox = await this.ctx.e2b.getSandbox()
-      const listed = await sandbox.files.list(String(target.targetKey), { depth: 1, ...signalOpts(signal) })
-      const entries: FsDirEntry[] = []
-      for (const entry of listed) {
-        const displayPath = posix.join(target.displayPath, entry.name)
-        const canonical = entry.symlinkTarget === undefined
-          ? entry.path
-          : await this.canonicalPath(sandbox, entry.path, signal)
-        const resolved = entry.symlinkTarget === undefined
-          ? entry
-          : await this.probe(canonical, displayPath, signal)
-        entries.push({
-          name: entry.name,
-          type: resolved === undefined ? 'other' : entryType(resolved),
-          target: { targetKey: FsTargetKey(canonical), displayPath },
-          ...(resolved !== undefined ? { version: entryVersion(resolved) } : {}),
-          ...(resolved?.type === FileType.FILE ? { size: resolved.size } : {}),
-        })
-      }
-      return entries.sort((left, right) => left.name.localeCompare(right.name))
-    } catch (error: unknown) {
-      throw mapError(error, 'list', target.displayPath, signal)
-    }
-  }
-
-  override async writeText(
-    target: FsTarget,
-    content: string,
-    expected?: FsWriteIntent,
-    signal?: AbortSignal,
-  ): Promise<FsWriteOutcome> {
-    return this.withLock(String(target.targetKey), async () => {
-      const existing = await this.probe(String(target.targetKey), target.displayPath, signal)
-      if (existing !== undefined && entryType(existing) !== 'file') {
-        throw new FsError(`cannot write "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
-      }
-      this.checkWriteIntent(existing, expected, target)
-      const before = existing === undefined ? null : await this.readForDiff(target, signal)
-      const version = await this.writeAtomic(
-        target,
-        content,
-        existing,
-        expected?.kind === 'createIfAbsent',
-        signal,
-      )
-      return {
-        operation: existing === undefined ? 'create' : 'update',
-        version,
-        before,
-        after: normalizeLineEndings(content),
-      }
-    })
-  }
-
-  override async editText(
-    target: FsTarget,
-    edit: FsEditRequest,
-    expected?: { version: ReturnType<typeof FsVersion> },
-    signal?: AbortSignal,
-  ): Promise<FsEditOutcome> {
-    return this.withLock(String(target.targetKey), async () => {
-      const existing = await this.probe(String(target.targetKey), target.displayPath, signal)
-      if (existing === undefined) {
-        throw new FsError(`cannot edit "${target.displayPath}": file changed since it was read`, 'FS_STALE_VERSION')
-      }
-      if (entryType(existing) !== 'file') {
-        throw new FsError(`cannot edit "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
-      }
-      if (expected !== undefined && entryVersion(existing) !== expected.version) {
-        throw new FsError(`cannot edit "${target.displayPath}": file changed since it was read`, 'FS_STALE_VERSION')
-      }
-      const raw = await this.readForEdit(target, signal)
-      const before = normalizeLineEndings(raw)
-      const after = literalEdit(before, edit, target.displayPath)
-      const storage = restoreLineEndings(after, detectsCrlf(raw))
-      const version = await this.writeAtomic(target, storage, existing, false, signal)
-      return { version, before, after }
-    })
-  }
-
-  private async withLock<T>(targetKey: string, operation: () => Promise<T>): Promise<T> {
-    const prior = this.locks.get(targetKey) ?? Promise.resolve()
-    const run = prior.then(operation, operation)
-    const tail = run.then(() => undefined, () => undefined)
-    this.locks.set(targetKey, tail)
-    try {
-      return await run
-    } finally {
-      if (this.locks.get(targetKey) === tail) this.locks.delete(targetKey)
-    }
-  }
-
-  private async canonicalPath(sandbox: Sandbox, path: string, signal?: AbortSignal): Promise<string> {
-    try {
-      const result = await sandbox.commands.run(
-        `set -o pipefail; realpath -mz -- ${quoteE2BShellArg(path)} | base64 -w0`,
-        commandOpts(signal),
-      )
-      return decodeCanonicalPath(result.stdout)
-    } catch (error: unknown) {
-      if (error instanceof CommandExitError) throw new Error(error.stderr || error.message, { cause: error })
-      throw error
-    }
-  }
-
-  private async probe(path: string, displayPath: string, signal?: AbortSignal): Promise<EntryInfo | undefined> {
-    assertNotAborted(signal, 'stat')
-    try {
-      const sandbox = await this.ctx.e2b.getSandbox()
-      const entry = await sandbox.files.getInfo(path, signalOpts(signal))
-      assertNotAborted(signal, 'stat')
-      return entry
-    } catch (error: unknown) {
-      if (error instanceof FileNotFoundError) return undefined
-      throw mapError(error, 'stat', displayPath, signal)
-    }
-  }
-
-  private async requireRegular(target: FsTarget, signal?: AbortSignal): Promise<FsInfo> {
-    const info = await this.stat(target, signal)
-    if (info === undefined) throw new FsError(`cannot read "${target.displayPath}": not found`, 'FS_NOT_FOUND')
-    if (info.type !== 'file') throw new FsError(`cannot read "${target.displayPath}": not a regular file`, 'FS_NOT_REGULAR_FILE')
-    return info
-  }
-
-  private checkWriteIntent(existing: EntryInfo | undefined, expected: FsWriteIntent | undefined, target: FsTarget): void {
-    if (expected?.kind === 'createIfAbsent' && existing !== undefined) {
-      throw new FsError(`cannot overwrite existing "${target.displayPath}" without reading it first`, 'FS_NOT_OBSERVED')
-    }
-    if (expected?.kind === 'replaceIfVersion') {
-      if (existing === undefined || entryVersion(existing) !== expected.version) {
-        throw new FsError(`cannot write "${target.displayPath}": file changed since it was read`, 'FS_STALE_VERSION')
-      }
-    }
-  }
-
-  private async readForDiff(target: FsTarget, signal?: AbortSignal): Promise<string | null> {
-    try {
-      const sandbox = await this.ctx.e2b.getSandbox()
-      const bytes = await sandbox.files.read(String(target.targetKey), { format: 'bytes', ...signalOpts(signal) })
-      assertNotAborted(signal, 'read')
-      return normalizeLineEndings(decodeText(bytes, target.displayPath, bytes.length))
-    } catch (error: unknown) {
-      if (error instanceof FsError && error.code === 'FS_NOT_TEXT') return null
-      throw mapError(error, 'read', target.displayPath, signal)
-    }
-  }
-
-  private async readForEdit(target: FsTarget, signal?: AbortSignal): Promise<string> {
-    try {
-      const sandbox = await this.ctx.e2b.getSandbox()
-      const bytes = await sandbox.files.read(String(target.targetKey), { format: 'bytes', ...signalOpts(signal) })
-      assertNotAborted(signal, 'edit')
-      return decodeText(bytes, target.displayPath, bytes.length)
-    } catch (error: unknown) {
-      throw mapError(error, 'edit', target.displayPath, signal)
-    }
-  }
-
-  private async writeAtomic(
-    target: FsTarget,
-    content: string,
-    existing: EntryInfo | undefined,
-    createIfAbsent: boolean,
-    signal?: AbortSignal,
-  ): Promise<ReturnType<typeof FsVersion>> {
-    assertNotAborted(signal, 'write')
-    const sandbox = await this.ctx.e2b.getSandbox()
-    const targetPath = String(target.targetKey)
-    const versionId = randomUUID()
-    const stagingDirectory = posix.join(posix.dirname(targetPath), `.dsh-${randomUUID()}.tmp`)
-    const temporary = posix.join(stagingDirectory, 'content')
-    let stagingDirectoryCreated = false
-    try {
-      const created = await sandbox.files.makeDir(stagingDirectory, signalOpts(signal))
-      if (!created) throw new Error('private staging directory already exists')
-      stagingDirectoryCreated = true
-      await sandbox.commands.run(`chmod 700 -- ${quoteE2BShellArg(stagingDirectory)}`, commandOpts(signal))
-      assertNotAborted(signal, 'write')
-      await sandbox.files.write(temporary, content, {
-        metadata: { [VERSION_METADATA_KEY]: versionId },
-        ...signalOpts(signal),
-      })
-      assertNotAborted(signal, 'write')
-      const mode = existing === undefined ? 0o600 : existing.mode & 0o777
-      await sandbox.commands.run(
-        `chmod ${mode.toString(8)} -- ${quoteE2BShellArg(temporary)}`,
-        commandOpts(signal),
-      )
-      assertNotAborted(signal, 'write')
-      let committed: EntryInfo
-      if (createIfAbsent) {
-        const staged = await sandbox.files.getInfo(temporary, signalOpts(signal))
-        assertNotAborted(signal, 'write')
-        const targetArg = quoteE2BShellArg(targetPath)
-        const publication = await sandbox.commands.run(
-          `if ln -T -- ${quoteE2BShellArg(temporary)} ${targetArg}; then printf created; elif test -e ${targetArg} || test -L ${targetArg}; then printf exists; else exit 1; fi`,
-          commandOpts(undefined),
-        )
-        if (publication.stdout === 'exists') {
-          throw new FsError(
-            `cannot overwrite existing "${target.displayPath}" without reading it first`,
-            'FS_NOT_OBSERVED',
-          )
-        }
-        if (publication.stdout !== 'created') {
-          throw new Error('guarded create returned an invalid publication result')
-        }
-        committed = { ...staged, name: posix.basename(targetPath), path: targetPath }
-      } else {
-        committed = await sandbox.files.rename(temporary, targetPath)
-      }
-      try {
-        await sandbox.files.remove(stagingDirectory)
-      } catch (_committedStagingCleanupFailure) {
-        // The target is already committed; an empty private directory cannot turn that write into a failure.
-      }
-      return entryVersion(committed)
-    } catch (error: unknown) {
-      if (stagingDirectoryCreated) {
-        try {
-          await sandbox.files.remove(stagingDirectory)
-        } catch (_stagingDirectoryAlreadyAbsentOrCleanupFailed) {
-          // Only the private staging directory is swallowed; the original failure owns the operation.
-        }
-      }
-      throw mapError(error, 'write', target.displayPath, signal)
-    }
-  }
-}
-
-export default E2BFileSystem

+ 0 - 851
packages/e2b/fs-e2b/tests/filesystem.spec.ts

@@ -1,851 +0,0 @@
-import { Buffer } from 'node:buffer'
-import { dirname, posix } from 'node:path'
-import { Context } from '@deepseek-ai/cordis'
-import {
-  CommandExitError,
-  FileNotFoundError,
-  FileType,
-  type EntryInfo,
-  type Sandbox,
-} from '@deepseek-ai/dsh-e2b'
-import type E2BRuntime from '@deepseek-ai/dsh-e2b'
-import { FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs'
-import E2BFileSystem from '@deepseek-ai/dsh-fs-e2b'
-import { describe, expect, it, vi } from 'vitest'
-
-interface RemoteNode {
-  type: FileType
-  data: Uint8Array
-  mode: number
-  modified: number
-  metadata?: Record<string, string>
-  symlinkTarget?: string
-}
-
-function bytes(value: string | readonly number[]): Uint8Array {
-  return typeof value === 'string' ? new TextEncoder().encode(value) : Uint8Array.from(value)
-}
-
-function commandError(exitCode: number, stderr = ''): CommandExitError {
-  return new CommandExitError({ exitCode, stdout: '', stderr, error: stderr })
-}
-
-class FakeRemote {
-  readonly nodes = new Map<string, RemoteNode>()
-  readonly writes: Array<{ path: string; data: string; metadata?: Record<string, string> }> = []
-  readonly writeParentModes: number[] = []
-  readonly renames: Array<{ from: string; to: string }> = []
-  readonly links: Array<{ from: string; to: string }> = []
-  readonly removals: string[] = []
-  readonly commands: string[] = []
-  readonly reads: Array<{ path: string; format: 'bytes' | 'stream' }> = []
-  streamChunks: Uint8Array[] | undefined
-  streamKeepOpen = false
-  readonly streamCancel = vi.fn()
-  nextCommandError: unknown
-  nextMakeDirResult: boolean | undefined
-  nextInfoError: unknown
-  nextListError: unknown
-  nextReadError: unknown
-  nextRenameError: unknown
-  nextRemoveError: unknown
-  canonicalOutput: string | undefined
-  abortAfterRename: AbortController | undefined
-  competitorBeforeLink:
-    | { path: string; kind: 'file'; data: string }
-    | { path: string; kind: 'directory' }
-    | undefined
-  guardedLinkOutput: string | undefined
-  disappearOnInfo = new Set<string>()
-  private clock = 1
-
-  constructor() {
-    this.dir('/')
-    this.dir('/workspace')
-  }
-
-  dir(path: string): void {
-    this.nodes.set(path, { type: FileType.DIR, data: bytes(''), mode: 0o755, modified: this.clock++ })
-  }
-
-  file(path: string, data: string | readonly number[], mode = 0o644): void {
-    this.nodes.set(path, { type: FileType.FILE, data: bytes(data), mode, modified: this.clock++ })
-  }
-
-  other(path: string): void {
-    this.nodes.set(path, { type: 'other' as FileType, data: bytes(''), mode: 0o600, modified: this.clock++ })
-  }
-
-  symlink(path: string, target: string): void {
-    this.nodes.set(path, {
-      type: FileType.FILE,
-      data: bytes(''),
-      mode: 0o777,
-      modified: this.clock++,
-      symlinkTarget: target,
-    })
-  }
-
-  mutate(path: string, data: string): void {
-    const node = this.required(path)
-    node.data = bytes(data)
-    node.modified = this.clock++
-  }
-
-  private required(path: string): RemoteNode {
-    const node = this.nodes.get(path)
-    if (node === undefined) throw new FileNotFoundError(`missing: ${path}`)
-    return node
-  }
-
-  private followed(path: string): { path: string; node: RemoteNode; link?: RemoteNode } {
-    const node = this.required(path)
-    if (node.symlinkTarget === undefined) return { path, node }
-    return { path: node.symlinkTarget, node: this.required(node.symlinkTarget), link: node }
-  }
-
-  private info(path: string): EntryInfo {
-    if (this.disappearOnInfo.delete(path)) throw new FileNotFoundError(`missing: ${path}`)
-    return this.rawInfo(path)
-  }
-
-  private rawInfo(path: string): EntryInfo {
-    const followed = this.followed(path)
-    const node = followed.node
-    return {
-      name: posix.basename(path),
-      path,
-      type: node.type,
-      size: node.data.byteLength,
-      mode: node.mode,
-      permissions: 'rw-------',
-      owner: 'user',
-      group: 'user',
-      modifiedTime: new Date(node.modified),
-      ...(node.metadata !== undefined ? { metadata: { ...node.metadata } } : {}),
-      ...(followed.link?.symlinkTarget !== undefined ? { symlinkTarget: followed.link.symlinkTarget } : {}),
-    }
-  }
-
-  private checkAbort(options: { signal?: AbortSignal } | undefined): void {
-    if (options?.signal?.aborted === true) throw new DOMException('aborted', 'AbortError')
-  }
-
-  readonly sandbox = {
-    sandboxId: 'fake',
-    files: {
-      makeDir: async (path: string, options?: { signal?: AbortSignal }): Promise<boolean> => {
-        this.checkAbort(options)
-        if (this.nextMakeDirResult !== undefined) {
-          const result = this.nextMakeDirResult
-          this.nextMakeDirResult = undefined
-          return result
-        }
-        if (this.nodes.has(path)) return false
-        this.dir(path)
-        return true
-      },
-      getInfo: async (path: string, options?: { signal?: AbortSignal }): Promise<EntryInfo> => {
-        this.checkAbort(options)
-        if (this.nextInfoError !== undefined) {
-          const error = this.nextInfoError
-          this.nextInfoError = undefined
-          throw error
-        }
-        return this.info(path)
-      },
-      read: async (path: string, options: { format: 'bytes' | 'stream'; signal?: AbortSignal }): Promise<Uint8Array | ReadableStream<Uint8Array> | string> => {
-        this.checkAbort(options)
-        this.reads.push({ path, format: options.format })
-        if (this.nextReadError !== undefined) {
-          const error = this.nextReadError
-          this.nextReadError = undefined
-          throw error
-        }
-        const data = this.followed(path).node.data
-        if (options.format === 'bytes') return data.slice()
-        // Pinned-SDK fidelity: a content-length-0 response returns '' even in stream format.
-        if (data.length === 0 && this.streamChunks === undefined) return ''
-        const chunks = this.streamChunks ?? [data.slice()]
-        return new ReadableStream<Uint8Array>({
-          start: (controller) => {
-            for (const chunk of chunks) controller.enqueue(chunk)
-            if (!this.streamKeepOpen) controller.close()
-            // SDK fidelity: an abort of the request signal fails the open stream.
-            options.signal?.addEventListener('abort', () => { controller.error(new DOMException('aborted', 'AbortError')) }, { once: true })
-          },
-          cancel: () => { this.streamCancel() },
-        })
-      },
-      list: async (path: string, options?: { depth?: number; signal?: AbortSignal }): Promise<EntryInfo[]> => {
-        this.checkAbort(options)
-        if (this.nextListError !== undefined) {
-          const error = this.nextListError
-          this.nextListError = undefined
-          throw error
-        }
-        this.required(path)
-        return [...this.nodes.keys()]
-          .filter(candidate => candidate !== path && dirname(candidate) === path)
-          .map(candidate => this.rawInfo(candidate))
-      },
-      write: async (path: string, data: string, options?: { metadata?: Record<string, string>; signal?: AbortSignal }): Promise<object> => {
-        this.checkAbort(options)
-        const parent = dirname(path)
-        if (!this.nodes.has(parent)) this.dir(parent)
-        this.writeParentModes.push(this.required(parent).mode)
-        this.nodes.set(path, {
-          type: FileType.FILE,
-          data: bytes(data),
-          mode: 0o644,
-          modified: this.clock++,
-          ...(options?.metadata !== undefined ? { metadata: { ...options.metadata } } : {}),
-        })
-        this.writes.push({ path, data, ...(options?.metadata !== undefined ? { metadata: options.metadata } : {}) })
-        return {}
-      },
-      rename: async (from: string, to: string, options?: { signal?: AbortSignal }): Promise<EntryInfo> => {
-        this.checkAbort(options)
-        if (this.nextRenameError !== undefined) {
-          const error = this.nextRenameError
-          this.nextRenameError = undefined
-          throw error
-        }
-        const node = this.required(from)
-        this.nodes.delete(from)
-        this.nodes.set(to, node)
-        this.renames.push({ from, to })
-        this.abortAfterRename?.abort('after commit')
-        this.checkAbort(options)
-        return this.info(to)
-      },
-      remove: async (path: string): Promise<void> => {
-        this.removals.push(path)
-        if (this.nextRemoveError !== undefined) {
-          const error = this.nextRemoveError
-          this.nextRemoveError = undefined
-          throw error
-        }
-        for (const candidate of this.nodes.keys()) {
-          if (candidate === path || candidate.startsWith(`${path}/`)) this.nodes.delete(candidate)
-        }
-      },
-    },
-    commands: {
-      run: async (
-        command: string,
-        options?: { envs?: Record<string, string>; signal?: AbortSignal },
-      ): Promise<{ exitCode: number; stdout: string; stderr: string }> => {
-        this.checkAbort(options)
-        const home = options?.envs?.HOME
-        expect(home).toMatch(/^\/\.dsh-e2b-control-/)
-        expect(options?.envs).toEqual({ HOME: home })
-        this.commands.push(command)
-        if (this.nextCommandError !== undefined) {
-          const error = this.nextCommandError
-          this.nextCommandError = undefined
-          throw error
-        }
-        const realpathPrefix = 'set -o pipefail; realpath -mz -- '
-        const realpathSuffix = ' | base64 -w0'
-        if (command.startsWith(realpathPrefix) && command.endsWith(realpathSuffix)) {
-          const quoted = command.slice(realpathPrefix.length, -realpathSuffix.length)
-          const input = quoted.slice(1, -1).replaceAll(String.raw`'"'"'`, '\'')
-          const node = this.nodes.get(input)
-          const canonical = `${node?.symlinkTarget ?? input}\0`
-          return {
-            exitCode: 0,
-            stdout: this.canonicalOutput ?? Buffer.from(canonical).toString('base64'),
-            stderr: '',
-          }
-        }
-        const chmod = /^chmod ([0-7]+) -- '([^']+)'$/.exec(command)
-        if (chmod !== null) this.required(chmod[2]!).mode = Number.parseInt(chmod[1]!, 8)
-        const guardedLink = new RegExp(
-          "^if ln -T -- '([^']+)' '([^']+)'; then printf created; "
-          + "elif test -e '[^']+' \\|\\| test -L '[^']+'; then printf exists; else exit 1; fi$",
-        ).exec(command)
-        if (guardedLink !== null) {
-          const from = guardedLink[1]!
-          const to = guardedLink[2]!
-          if (this.guardedLinkOutput !== undefined) {
-            const stdout = this.guardedLinkOutput
-            this.guardedLinkOutput = undefined
-            return { exitCode: 0, stdout, stderr: '' }
-          }
-          if (this.competitorBeforeLink?.path === to) {
-            if (this.competitorBeforeLink.kind === 'directory') this.dir(to)
-            else this.file(to, this.competitorBeforeLink.data)
-            this.competitorBeforeLink = undefined
-          }
-          if (this.nodes.has(to)) return { exitCode: 0, stdout: 'exists', stderr: '' }
-          this.nodes.set(to, this.required(from))
-          this.links.push({ from, to })
-          this.abortAfterRename?.abort('after commit')
-          return { exitCode: 0, stdout: 'created', stderr: '' }
-        }
-        const move = /^mv -f -- '([^']+)' '([^']+)'$/.exec(command)
-        if (move !== null) {
-          if (this.nextRenameError !== undefined) {
-            const error = this.nextRenameError
-            this.nextRenameError = undefined
-            throw error
-          }
-          const node = this.required(move[1]!)
-          this.nodes.delete(move[1]!)
-          this.nodes.set(move[2]!, node)
-          this.renames.push({ from: move[1]!, to: move[2]! })
-          this.abortAfterRename?.abort('after commit')
-        }
-        return { exitCode: 0, stdout: '', stderr: '' }
-      },
-    },
-  } as unknown as Sandbox
-}
-
-async function setup(remote = new FakeRemote()): Promise<{ ctx: Context; fs: E2BFileSystem; remote: FakeRemote }> {
-  const ctx = new Context()
-  const runtime = {
-    cwd: '/workspace',
-    runtimeRoot: '/workspace/.dsh-e2b',
-    getSandbox: async () => remote.sandbox,
-  } as unknown as E2BRuntime
-  ctx.provide('e2b', runtime)
-  await ctx.plugin(E2BFileSystem)
-  return { ctx, fs: ctx.fs as E2BFileSystem, remote }
-}
-
-async function expectCode(promise: Promise<unknown>, code: string): Promise<void> {
-  await expect(promise).rejects.toMatchObject({ code })
-}
-
-describe('E2BFileSystem identity, metadata, and reads', () => {
-  it('resolves remote paths, reports symlinks, and lists direct children in stable order', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/z.txt', 'z')
-    remote.file('/workspace/a.txt', 'a')
-    remote.dir('/workspace/dir')
-    remote.other('/workspace/special')
-    remote.file('/workspace/dir/nested.txt', 'nested')
-    remote.symlink('/workspace/link.txt', '/workspace/a.txt')
-    const { fs } = await setup(remote)
-
-    const link = await fs.resolve('link.txt')
-    expect(link).toEqual({ targetKey: '/workspace/a.txt', displayPath: '/workspace/link.txt' })
-    await expect(fs.lstat('link.txt')).resolves.toMatchObject({ type: 'symlink', size: 1 })
-    await expect(fs.lstat('a.txt')).resolves.toMatchObject({ type: 'file', size: 1 })
-    await expect(fs.lstat('dir')).resolves.toEqual(expect.objectContaining({ type: 'directory' }))
-    await expect(fs.lstat('special')).resolves.toEqual(expect.objectContaining({ type: 'other' }))
-    await expect(fs.lstat('missing')).resolves.toBeUndefined()
-    await expect(fs.stat(link)).resolves.toMatchObject({ type: 'file', size: 1 })
-    const directory = await fs.resolve('.')
-    const listed = await fs.listDir(directory)
-    expect(listed.map(entry => entry.name)).toEqual(['a.txt', 'dir', 'link.txt', 'special', 'z.txt'])
-    expect(listed.find(entry => entry.name === 'dir')).toMatchObject({ type: 'directory' })
-    expect(listed.find(entry => entry.name === 'link.txt')).toMatchObject({
-      type: 'file',
-      target: { targetKey: '/workspace/a.txt', displayPath: '/workspace/link.txt' },
-    })
-    expect(listed.some(entry => entry.name === 'nested.txt')).toBe(false)
-  })
-
-  it('projects canonical process paths, file URLs, and containment', async () => {
-    const remote = new FakeRemote()
-    remote.dir('/workspace/nested')
-    remote.file('/workspace/nested/multibyte # file.ts', 'text')
-    remote.file('/outside.ts', 'outside')
-    const { fs } = await setup(remote)
-    const workspace = await fs.resolve('/workspace')
-    const nested = await fs.resolve('/workspace/nested/multibyte # file.ts')
-    const outside = await fs.resolve('/outside.ts')
-
-    expect(fs.processPath(nested)).toBe('/workspace/nested/multibyte # file.ts')
-    expect(fs.processPathFromHostPath('/Users/alice/.dsh/attachments/object')).toBeUndefined()
-    expect(fs.fileUrl(nested)).toBe('file:///workspace/nested/multibyte%20%23%20file.ts')
-    expect(fs.contains(workspace, workspace)).toBe(true)
-    expect(fs.contains(workspace, nested)).toBe(true)
-    expect(fs.contains(nested, workspace)).toBe(false)
-    expect(fs.contains(workspace, outside)).toBe(false)
-    expect(() => fs.fileUrl({ targetKey: FsTargetKey('relative'), displayPath: 'relative' }))
-      .toThrow('expected an absolute process path')
-  })
-
-  it('preserves newline and multibyte canonical paths through strict ASCII framing', async () => {
-    const remote = new FakeRemote()
-    const path = '/workspace/你好\nfile.ts'
-    remote.file(path, 'text')
-    const { fs } = await setup(remote)
-
-    await expect(fs.resolve(path)).resolves.toEqual({ targetKey: path, displayPath: path })
-  })
-
-  it.each([
-    ['invalid base64', '!!!!'],
-    ['missing terminator', Buffer.from('/workspace/file').toString('base64')],
-    ['multiple records', Buffer.from('/workspace/file\0/other\0').toString('base64')],
-    ['invalid UTF-8', Buffer.from([47, 0xff, 0]).toString('base64')],
-    ['relative path', Buffer.from('workspace/file\0').toString('base64')],
-  ])('rejects %s from canonical path transport', async (_label, output) => {
-    const remote = new FakeRemote()
-    remote.canonicalOutput = output
-    const { fs } = await setup(remote)
-    await expectCode(fs.resolve('file'), 'FS_IO_ERROR')
-  })
-
-  it('reads whole and streamed UTF-8 across chunk boundaries', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/text.txt', 'A€B')
-    remote.streamChunks = [bytes([65, 0xe2]), bytes([0x82, 0xac, 66])]
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('text.txt')
-    await expect(fs.readText(target)).resolves.toBe('A€B')
-    let streamed = ''
-    for await (const chunk of await fs.streamText(target)) streamed += chunk
-    expect(streamed).toBe('A€B')
-
-    remote.streamChunks = [bytes([0xe2]), bytes([0x82, 0xac])]
-    let initiallyBuffered = ''
-    for await (const chunk of await fs.streamText(target)) initiallyBuffered += chunk
-    expect(initiallyBuffered).toBe('€')
-  })
-
-  it('streams an empty file even though the pinned SDK returns a non-stream value', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/empty.txt', '')
-    const { fs } = await setup(remote)
-    let streamed = ''
-    for await (const chunk of await fs.streamText(await fs.resolve('empty.txt'))) streamed += chunk
-    expect(streamed).toBe('')
-  })
-
-  it('cancels a remote stream when its consumer stops early', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/text.txt', 'ab')
-    remote.streamChunks = [bytes('a'), bytes('b')]
-    remote.streamKeepOpen = true
-    const { fs } = await setup(remote)
-    const stream = await fs.streamText(await fs.resolve('text.txt'))
-
-    for await (const chunk of stream) {
-      expect(chunk).toBe('a')
-      break
-    }
-
-    expect(remote.streamCancel).toHaveBeenCalledOnce()
-  })
-
-  it('matches local binary sampling while edits still reject any NUL byte', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/late-nul.txt', `${'a'.repeat(8192)}\0tail`)
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('late-nul.txt')
-    await expect(fs.readText(target)).resolves.toContain('\0tail')
-    remote.streamChunks = [bytes('a'.repeat(8192)), bytes([0, 116])]
-    let streamed = ''
-    for await (const chunk of await fs.streamText(target)) streamed += chunk
-    expect(streamed).toBe(`${'a'.repeat(8192)}\0t`)
-    await expectCode(fs.editText(target, { oldString: 'tail', newString: 'end', replaceAll: false }), 'FS_NOT_TEXT')
-  })
-
-  it('maps binary, invalid UTF-8, missing, and non-regular read failures', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/binary', [0, 1])
-    remote.file('/workspace/invalid', [0xff])
-    remote.dir('/workspace/directory')
-    const { fs } = await setup(remote)
-    await expectCode(fs.readText(await fs.resolve('binary')), 'FS_NOT_TEXT')
-    await expectCode(fs.readText(await fs.resolve('invalid')), 'FS_NOT_TEXT')
-    await expectCode(fs.readText(await fs.resolve('missing')), 'FS_NOT_FOUND')
-    await expectCode(fs.readText(await fs.resolve('directory')), 'FS_NOT_REGULAR_FILE')
-
-    remote.streamChunks = [bytes([0xff])]
-    const invalid = await fs.streamText(await fs.resolve('invalid'))
-    await expect((async () => { for await (const _chunk of invalid) void _chunk })()).rejects.toMatchObject({ code: 'FS_NOT_TEXT' })
-    remote.streamChunks = [bytes([0])]
-    const binary = await fs.streamText(await fs.resolve('binary'))
-    await expect((async () => { for await (const _chunk of binary) void _chunk })()).rejects.toMatchObject({ code: 'FS_NOT_TEXT' })
-
-    remote.streamChunks = [bytes([0xe2])]
-    const incomplete = await fs.streamText(await fs.resolve('invalid'))
-    await expect((async () => { for await (const _chunk of incomplete) void _chunk })()).rejects.toMatchObject({ code: 'FS_NOT_TEXT' })
-
-    const raced = await fs.resolve('invalid')
-    remote.nextReadError = new FileNotFoundError('gone after stat')
-    await expectCode(fs.streamText(raced), 'FS_NOT_FOUND')
-  })
-
-  it('readBytes returns raw content, enforces the byte cap, and maps failures', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/img.bin', [0x89, 0, 0xff, 0x47])
-    remote.dir('/workspace/directory')
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('img.bin')
-    expect(Array.from(await fs.readBytes(target, undefined, 4))).toEqual([0x89, 0, 0xff, 0x47])
-    expect(remote.reads).toEqual([{ path: '/workspace/img.bin', format: 'stream' }])
-    remote.reads.length = 0
-    await expectCode(fs.readBytes(target, undefined, 3), 'FS_TOO_LARGE')
-    expect(remote.reads).toEqual([])
-    await expectCode(fs.readBytes(await fs.resolve('missing'), undefined, 4), 'FS_NOT_FOUND')
-    await expectCode(fs.readBytes(await fs.resolve('directory'), undefined, 4), 'FS_NOT_REGULAR_FILE')
-
-    const live = new AbortController()
-    expect((await fs.readBytes(target, live.signal, 4)).byteLength).toBe(4)
-    remote.nextReadError = new DOMException('aborted', 'AbortError')
-    await expectCode(fs.readBytes(target, undefined, 4), 'FS_ABORTED')
-  })
-
-  it('readBytes bounds a post-stat grower mid-stream and reads an empty file through the SDK quirk', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/grow.bin', [1, 1, 1, 1])
-    remote.file('/workspace/empty.bin', '')
-    const { fs } = await setup(remote)
-
-    remote.streamChunks = [bytes([1, 1, 1]), bytes([1, 2, 2])]
-    remote.streamKeepOpen = true
-    await expectCode(fs.readBytes(await fs.resolve('grow.bin'), undefined, 4), 'FS_TOO_LARGE')
-    expect(remote.streamCancel).toHaveBeenCalledOnce()
-
-    remote.streamChunks = undefined
-    remote.streamKeepOpen = false
-    expect((await fs.readBytes(await fs.resolve('empty.bin'), undefined, 4)).byteLength).toBe(0)
-  })
-
-  it('readByteRange skips to the offset, keeps the window, and cancels the stream there', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/ramp.bin', [1, 2, 3, 4, 5, 6, 7, 8, 9])
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('ramp.bin')
-    remote.streamChunks = [bytes([1, 2, 3]), bytes([4, 5, 6]), bytes([7, 8, 9])]
-    remote.streamKeepOpen = true
-    expect(Array.from(await fs.readByteRange(target, { offset: 4, length: 3 }))).toEqual([5, 6, 7])
-    expect(remote.reads).toEqual([{ path: '/workspace/ramp.bin', format: 'stream' }])
-    expect(remote.streamCancel).toHaveBeenCalledOnce()
-  })
-
-  it('readByteRange shortens at the end, empties past it, and skips the read for length 0', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/ramp.bin', [1, 2, 3, 4, 5, 6, 7, 8, 9])
-    remote.dir('/workspace/directory')
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('ramp.bin')
-    remote.streamChunks = [bytes([1, 2, 3]), bytes([4, 5, 6]), bytes([7, 8, 9])]
-    expect(Array.from(await fs.readByteRange(target, { offset: 7, length: 10 }))).toEqual([8, 9])
-    expect(remote.streamCancel).not.toHaveBeenCalled()
-    expect((await fs.readByteRange(target, { offset: 9, length: 2 })).byteLength).toBe(0)
-    remote.reads.length = 0
-    expect((await fs.readByteRange(target, { offset: 0, length: 0 })).byteLength).toBe(0)
-    expect(remote.reads).toEqual([])
-    await expectCode(fs.readByteRange(await fs.resolve('missing'), { offset: 0, length: 1 }), 'FS_NOT_FOUND')
-    await expectCode(fs.readByteRange(await fs.resolve('directory'), { offset: 0, length: 1 }), 'FS_NOT_REGULAR_FILE')
-  })
-
-  it('readByteRange maps a failing open, an abort mid-stream, and tolerates a failing cancel', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/ramp.bin', [1, 2, 3, 4])
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('ramp.bin')
-    remote.nextReadError = new DOMException('aborted', 'AbortError')
-    await expectCode(fs.readByteRange(target, { offset: 0, length: 2 }), 'FS_ABORTED')
-
-    // The window wants more than the one chunk delivered; the abort fails the open stream.
-    remote.streamChunks = [bytes([1])]
-    remote.streamKeepOpen = true
-    const controller = new AbortController()
-    const pending = fs.readByteRange(target, { offset: 0, length: 4 }, controller.signal)
-    await new Promise<void>((resolve) => { setTimeout(resolve, 0) })
-    controller.abort()
-    await expectCode(pending, 'FS_ABORTED')
-
-    remote.streamChunks = [bytes([1, 2, 3, 4])]
-    remote.streamCancel.mockRejectedValueOnce(new Error('cancel failed'))
-    expect(Array.from(await fs.readByteRange(target, { offset: 1, length: 2 }))).toEqual([2, 3])
-  })
-
-  it('honors aborts before and during remote reads', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/a', 'a')
-    const { fs } = await setup(remote)
-    await expectCode(fs.resolve('a', { signal: AbortSignal.abort() }), 'FS_ABORTED')
-    await expectCode(fs.lstat('a', undefined, AbortSignal.abort()), 'FS_ABORTED')
-    await expectCode(fs.stat(await fs.resolve('a'), AbortSignal.abort()), 'FS_ABORTED')
-    remote.nextReadError = new DOMException('aborted', 'AbortError')
-    await expectCode(fs.readText(await fs.resolve('a')), 'FS_ABORTED')
-  })
-
-  it('rejects empty paths and directory-listing type errors', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/file', 'x')
-    const { fs } = await setup(remote)
-    await expectCode(fs.resolve('   '), 'FS_NOT_FOUND')
-    await expectCode(fs.lstat(''), 'FS_NOT_FOUND')
-    await expectCode(fs.listDir(await fs.resolve('missing')), 'FS_NOT_FOUND')
-    await expectCode(fs.listDir(await fs.resolve('/workspace/file')), 'FS_NOT_DIRECTORY')
-    remote.nextListError = new Error('listing transport failed')
-    await expectCode(fs.listDir(await fs.resolve('/workspace')), 'FS_IO_ERROR')
-  })
-})
-
-describe('E2BFileSystem atomic writes and edits', () => {
-  it('creates owner-only files and returns metadata after the committed move', async () => {
-    const { fs, remote } = await setup()
-    const target = await fs.resolve('new.txt')
-    const outcome = await fs.writeText(target, 'one\r\ntwo\rthree', { kind: 'createIfAbsent' })
-    expect(outcome).toMatchObject({ operation: 'create', before: null, after: 'one\ntwo\rthree' })
-    expect(remote.nodes.get('/workspace/new.txt')?.mode).toBe(0o600)
-    expect(remote.nodes.get('/workspace/new.txt')?.metadata?.['dsh-version']).toBeDefined()
-    expect(remote.writeParentModes).toEqual([0o700])
-    expect(remote.links).toHaveLength(1)
-    const stagingDirectory = posix.dirname(remote.writes[0]!.path)
-    expect(posix.dirname(stagingDirectory)).toBe('/workspace')
-    expect(remote.removals).toContain(stagingDirectory)
-    await expect(fs.stat(target)).resolves.toMatchObject({ version: outcome.version, size: 14 })
-  })
-
-  it('preserves replacement mode, normalizes only CRLF for diffs, and changes version on external writes', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/file.txt', 'old\r\nline\rlone', 0o640)
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('file.txt')
-    const before = (await fs.stat(target))!.version
-    const outcome = await fs.writeText(target, 'new', { kind: 'replaceIfVersion', version: before })
-    expect(outcome).toMatchObject({ operation: 'update', before: 'old\nline\rlone', after: 'new' })
-    expect(remote.nodes.get('/workspace/file.txt')?.mode).toBe(0o640)
-    const committed = outcome.version
-    remote.mutate('/workspace/file.txt', 'external')
-    expect((await fs.stat(target))!.version).not.toBe(committed)
-  })
-
-  it('returns null as the overwrite diff basis for binary or invalid prior content', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/file.txt', [0xff])
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('file.txt')
-    await expect(fs.writeText(target, 'valid')).resolves.toMatchObject({ before: null, after: 'valid' })
-  })
-
-  it('fails an overwrite when reading its text diff basis fails for another reason', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/file.txt', 'prior')
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('file.txt')
-    remote.nextReadError = new Error('read transport failed')
-    await expectCode(fs.writeText(target, 'replacement'), 'FS_IO_ERROR')
-    expect(new TextDecoder().decode(remote.nodes.get('/workspace/file.txt')?.data)).toBe('prior')
-  })
-
-  it('enforces create and version intents before publication', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/file.txt', 'v1')
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('file.txt')
-    const version = (await fs.stat(target))!.version
-    await expectCode(fs.writeText(target, 'blind', { kind: 'createIfAbsent' }), 'FS_NOT_OBSERVED')
-    remote.mutate('/workspace/file.txt', 'v2')
-    await expectCode(fs.writeText(target, 'stale', { kind: 'replaceIfVersion', version }), 'FS_STALE_VERSION')
-    await expectCode(fs.writeText(await fs.resolve('missing'), 'stale', { kind: 'replaceIfVersion', version }), 'FS_STALE_VERSION')
-    remote.dir('/workspace/dir')
-    await expectCode(fs.writeText(await fs.resolve('dir'), 'x'), 'FS_NOT_REGULAR_FILE')
-  })
-
-  it('preserves a competitor created after the guarded-create probe', async () => {
-    const remote = new FakeRemote()
-    remote.competitorBeforeLink = { path: '/workspace/race.txt', kind: 'file', data: 'competitor' }
-    const { fs } = await setup(remote)
-
-    await expectCode(
-      fs.writeText(await fs.resolve('race.txt'), 'ours', { kind: 'createIfAbsent' }),
-      'FS_NOT_OBSERVED',
-    )
-    expect(new TextDecoder().decode(remote.nodes.get('/workspace/race.txt')?.data)).toBe('competitor')
-    expect(remote.links).toHaveLength(0)
-    expect(remote.removals).toHaveLength(1)
-  })
-
-  it('preserves a competing directory during guarded-create publication', async () => {
-    const remote = new FakeRemote()
-    remote.competitorBeforeLink = { path: '/workspace/race-dir', kind: 'directory' }
-    const { fs } = await setup(remote)
-
-    await expectCode(
-      fs.writeText(await fs.resolve('race-dir'), 'ours', { kind: 'createIfAbsent' }),
-      'FS_NOT_OBSERVED',
-    )
-    expect(remote.nodes.get('/workspace/race-dir')?.type).toBe(FileType.DIR)
-    expect(remote.nodes.has('/workspace/race-dir/content')).toBe(false)
-    expect(remote.links).toHaveLength(0)
-    expect(remote.removals).toHaveLength(1)
-  })
-
-  it('rejects an invalid guarded-create publication response before claiming success', async () => {
-    const remote = new FakeRemote()
-    remote.guardedLinkOutput = 'unexpected'
-    const { fs } = await setup(remote)
-
-    await expectCode(
-      fs.writeText(await fs.resolve('invalid.txt'), 'ours', { kind: 'createIfAbsent' }),
-      'FS_IO_ERROR',
-    )
-    expect(remote.nodes.has('/workspace/invalid.txt')).toBe(false)
-    expect(remote.removals).toHaveLength(1)
-  })
-
-  it('does not turn an abort observed after a successful move into a failed write', async () => {
-    const remote = new FakeRemote()
-    const controller = new AbortController()
-    remote.abortAfterRename = controller
-    const { fs } = await setup(remote)
-    await expect(fs.writeText(await fs.resolve('committed'), 'yes', undefined, controller.signal))
-      .resolves.toMatchObject({ operation: 'create' })
-    expect(controller.signal.aborted).toBe(true)
-  })
-
-  it('does not turn an abort observed after a guarded create into a failed write', async () => {
-    const remote = new FakeRemote()
-    const controller = new AbortController()
-    remote.abortAfterRename = controller
-    const { fs } = await setup(remote)
-    await expect(fs.writeText(
-      await fs.resolve('committed-create'),
-      'yes',
-      { kind: 'createIfAbsent' },
-      controller.signal,
-    )).resolves.toMatchObject({ operation: 'create' })
-    expect(controller.signal.aborted).toBe(true)
-  })
-
-  it('does not turn post-commit staging cleanup failure into a failed write', async () => {
-    const remote = new FakeRemote()
-    remote.nextRemoveError = new Error('empty staging cleanup failed')
-    const { fs } = await setup(remote)
-    await expect(fs.writeText(await fs.resolve('committed'), 'yes'))
-      .resolves.toMatchObject({ operation: 'create' })
-    expect(new TextDecoder().decode(remote.nodes.get('/workspace/committed')?.data)).toBe('yes')
-  })
-
-  it('returns committed rename metadata without a fallible post-commit lookup', async () => {
-    const remote = new FakeRemote()
-    const getInfo = vi.spyOn(remote.sandbox.files, 'getInfo')
-    const { fs } = await setup(remote)
-
-    await expect(fs.writeText(await fs.resolve('committed'), 'yes'))
-      .resolves.toMatchObject({ operation: 'create' })
-    expect(getInfo).toHaveBeenCalledTimes(1)
-    expect(remote.renames).toHaveLength(1)
-  })
-
-  it('cleans staging files and maps command, permission, and abort failures', async () => {
-    const remote = new FakeRemote()
-    const { fs } = await setup(remote)
-    const commandTarget = await fs.resolve('command')
-    remote.nextCommandError = commandError(1, 'chmod failed')
-    await expectCode(fs.writeText(commandTarget, 'x'), 'FS_IO_ERROR')
-    expect(remote.removals).toHaveLength(1)
-
-    remote.nextRenameError = new Error('permission denied')
-    await expectCode(fs.writeText(await fs.resolve('permission'), 'x'), 'FS_PERMISSION_DENIED')
-    remote.nextRemoveError = new Error('cleanup also failed')
-    remote.nextRenameError = new DOMException('aborted', 'AbortError')
-    await expectCode(fs.writeText(await fs.resolve('abort'), 'x'), 'FS_ABORTED')
-
-    const removalsBeforeCollision = remote.removals.length
-    remote.nextMakeDirResult = false
-    await expectCode(fs.writeText(await fs.resolve('collision'), 'x'), 'FS_IO_ERROR')
-    expect(remote.removals).toHaveLength(removalsBeforeCollision)
-  })
-
-  it('applies literal edits atomically and restores the detected CRLF style', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/file.txt', 'one\r\ntwo\r\nthree\n')
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('file.txt')
-    const version = (await fs.stat(target))!.version
-    const outcome = await fs.editText(
-      target,
-      { oldString: 'two\r\n', newString: 'TWO\r\n', replaceAll: false },
-      { version },
-    )
-    expect(outcome).toMatchObject({ before: 'one\ntwo\nthree\n', after: 'one\nTWO\nthree\n' })
-    expect(new TextDecoder().decode(remote.nodes.get('/workspace/file.txt')?.data)).toBe('one\r\nTWO\r\nthree\r\n')
-  })
-
-  it('reports stale and literal-match failures with stable codes', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/file.txt', 'a a')
-    remote.dir('/workspace/dir')
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('file.txt')
-    await expectCode(fs.editText(target, { oldString: '', newString: 'x', replaceAll: false }), 'FS_EDIT_NOT_FOUND')
-    await expectCode(fs.editText(target, { oldString: 'z', newString: 'x', replaceAll: false }), 'FS_EDIT_NOT_FOUND')
-    await expectCode(fs.editText(target, { oldString: 'a', newString: 'x', replaceAll: false }), 'FS_AMBIGUOUS_EDIT')
-    await expect(fs.editText(target, { oldString: 'a', newString: 'x', replaceAll: true }))
-      .resolves.toMatchObject({ after: 'x x' })
-    await expectCode(fs.editText(target, { oldString: 'x', newString: 'y', replaceAll: false }, { version: FsVersion('stale') }), 'FS_STALE_VERSION')
-    await expectCode(fs.editText(await fs.resolve('missing'), { oldString: 'x', newString: 'y', replaceAll: false }), 'FS_STALE_VERSION')
-    await expectCode(fs.editText(await fs.resolve('dir'), { oldString: 'x', newString: 'y', replaceAll: false }), 'FS_NOT_REGULAR_FILE')
-  })
-
-  it('serializes guarded mutations so only one stale version can win', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/file.txt', 'base')
-    const { fs } = await setup(remote)
-    const target = await fs.resolve('file.txt')
-    const version = (await fs.stat(target))!.version
-    const results = await Promise.allSettled([
-      fs.writeText(target, 'one', { kind: 'replaceIfVersion', version }),
-      fs.editText(target, { oldString: 'base', newString: 'two', replaceAll: false }, { version }),
-    ])
-    expect(results.filter(result => result.status === 'fulfilled')).toHaveLength(1)
-    expect(results.filter(result => result.status === 'rejected')).toHaveLength(1)
-  })
-})
-
-describe('E2B filesystem adapter integration edges', () => {
-  it('maps canonicalization, permission, and generic provider failures', async () => {
-    const remote = new FakeRemote()
-    const { fs } = await setup(remote)
-    remote.nextCommandError = commandError(1, 'not a directory')
-    await expectCode(fs.resolve('bad'), 'FS_IO_ERROR')
-    remote.nextCommandError = commandError(1)
-    await expectCode(fs.resolve('bad-again'), 'FS_IO_ERROR')
-    remote.nextCommandError = new Error('canonical transport failed')
-    await expectCode(fs.resolve('bad-transport'), 'FS_IO_ERROR')
-    remote.file('/workspace/a', 'a')
-    const target = await fs.resolve('a')
-    remote.nextInfoError = new Error('metadata transport failed')
-    await expectCode(fs.stat(target), 'FS_IO_ERROR')
-    remote.nextReadError = new Error('operation not permitted')
-    await expectCode(fs.readText(target), 'FS_PERMISSION_DENIED')
-    remote.nextReadError = 'transport vanished'
-    await expectCode(fs.readText(target), 'FS_IO_ERROR')
-  })
-
-  it('uses listing metadata directly and canonicalizes only symbolic links', async () => {
-    const remote = new FakeRemote()
-    remote.file('/workspace/a', 'a')
-    remote.file('/workspace/target', 'target')
-    remote.file('/workspace/gone', 'gone')
-    remote.symlink('/workspace/link', '/workspace/target')
-    remote.symlink('/workspace/vanished-link', '/workspace/gone')
-    remote.disappearOnInfo.add('/workspace/gone')
-    const { fs } = await setup(remote)
-    const directory = await fs.resolve('/workspace')
-    const commandsBefore = remote.commands.length
-    const getInfo = vi.spyOn(remote.sandbox.files, 'getInfo')
-
-    const listed = await fs.listDir(directory)
-
-    expect(listed.find(entry => entry.name === 'a')).toMatchObject({
-      type: 'file', target: { targetKey: '/workspace/a' }, size: 1,
-    })
-    expect(listed.find(entry => entry.name === 'link')).toMatchObject({
-      type: 'file', target: { targetKey: '/workspace/target' }, size: 6,
-    })
-    expect(listed.find(entry => entry.name === 'vanished-link')).toEqual({
-      name: 'vanished-link',
-      type: 'other',
-      target: { targetKey: '/workspace/gone', displayPath: '/workspace/vanished-link' },
-    })
-    expect(remote.commands.slice(commandsBefore)).toHaveLength(2)
-    expect(getInfo).toHaveBeenCalledTimes(3)
-  })
-})

+ 0 - 22
packages/e2b/fs-e2b/tsconfig.json

@@ -1,22 +0,0 @@
-{
-  "extends": "../../../tsconfig.base.json",
-  "compilerOptions": {
-    "rootDir": "src",
-    "outDir": "lib/types"
-  },
-  "include": ["src"],
-  "references": [
-    {
-      "path": "../../../vendor/cosmokit"
-    },
-    {
-      "path": "../../../vendor/cordis"
-    },
-    {
-      "path": "../e2b"
-    },
-    {
-      "path": "../../fs/fs"
-    }
-  ]
-}

+ 0 - 6
packages/e2b/subprocess-e2b/README.i18n.yaml

@@ -1,6 +0,0 @@
-# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
-# 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: 35d20ace111442becfe496bc0ac5b152fbae5222
-README.zh.md: f2c244fd50c578064ee4c331ef3017cae3185a67

+ 0 - 169
packages/e2b/subprocess-e2b/README.md

@@ -1,169 +0,0 @@
----
-description: "Shell commands and terminals inside the shared remote sandbox: what the agent can run there, how output is handled, and what to expect — for deployments and maintainers of the E2B family."
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-subprocess-e2b
-
-English | [中文](README.zh.md)
-
-## Summary
-
-`dsh-subprocess-e2b` runs the agent's shell commands and interactive terminals inside an E2B remote sandbox instead of the host. Existing command, terminal, and language-server workflows continue without E2B-specific tools. Host environment variables and secrets are excluded; only explicitly requested environment entries enter the sandbox. Use it with `dsh-e2b` and `dsh-fs-e2b` so commands, terminals, and files share one sandbox. Remote execution adds latency because each command requires asynchronous setup.
-
-## Table of Contents
-
-- [Use this package](#use-this-package)
-- [Understand the implementation](#understand-the-implementation)
-- [Further Exploration](#further-exploration)
-- [Model Experience](#model-experience)
-- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
-- [Dev Note](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## Use this package
-
-Use this package when the agent's shell commands and terminals should run inside the remote sandbox rather than on your machine. It is the command half of the E2B family: commands, terminals, and files share one remote world.
-
-### When to choose it
-
-Choose it when a composition already uses the E2B sandbox and you want commands and terminals to run there. Choose the local subprocess package for host execution.
-
-### Configuration
-
-The only setting is how often the package checks a running command's status; the default suits most deployments, and raising it reduces remote requests at the cost of slightly slower exit detection.
-
-| Field | Default | Meaning |
-|---|---|---|
-| `pollMs` | `20` | How often the package checks a running command's status, in milliseconds |
-
-The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-subprocess-e2b) is the exhaustive source for every accepted field and its JSDoc.
-
-### Running commands
-
-The agent can run a command in the sandbox with a working directory and environment, choose how its output is delivered (streamed live, captured up to a size cap, or routed to the app's own output), and stop it if it hangs — a stop first asks the command to exit politely, then force-kills it after a short grace, so a stuck command cannot leak. Very large output can be saved to a file in the sandbox so the agent can read it later. The command's exit code is reported normally; if the sandbox vanishes while a command runs, the command is treated as ended rather than erroring.
-
-### Using terminals
-
-The agent can open an interactive terminal in the sandbox, send input, read output, and signal programs running in it — prompts, interactive tools, and full-screen programs behave as they do locally. Terminal features like scrollback and readiness detection are provided by the terminal tooling, which works unchanged.
-
-### Keeping the environment clean
-
-Commands run with a clean, sandbox-native environment: host variables and values that look like credentials are not passed in implicitly, and only entries the agent explicitly requests are set. This keeps secrets out of the sandbox.
-
-### If the sandbox disappears
-
-The sandbox is ephemeral: if it is deleted while commands or terminals are running — through expiry, shutdown, or removal elsewhere — the affected commands are treated as ended cleanly. Do not rely on work surviving the sandbox.
-
-The default sandbox image ships with the runtime and utilities command work needs: `node`, `bash`, `setsid`, `ps`, `awk`, `tr`, `env`, `base64`, `chmod`, `tee`, `head`, `rm`, `kill`, `id`, and `getent`.
-
------
-
-<a id="understand-the-implementation"></a>
-## Understand the implementation
-
-<details>
-<summary>Implementation internals — click to expand</summary>
-
-This section explains the design decisions behind the provider and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
-
-### Design philosophy
-
-- **Provider-private remote identity.** The synchronous seam never blocks on the network. Private wrapper files asynchronously publish a process-group identity for stdin, observation, termination, and quiescence checks, together with the direct exit code and spill validity; that identity is not the requested target PID.
-- **One teardown ladder.** Termination, rollback, and disposal share one process-group signal path — `SIGTERM`, then `SIGKILL` plus the SDK kill fallback — and treat proven quiescence as final.
-- **Environment is explicit.** Nothing from the host and nothing credential-shaped enters the sandbox implicitly; every ambient value is scrubbed and every `spec.env` entry is an explicit opt-in.
-
-### Source map
-
-| File | Role |
-|---|---|
-| [`src/index.ts`](src/index.ts) | Plugin entry: `E2BSubprocessRuntime`, `Config`, spawn and spawnTerminal, disposal |
-| [`src/process.ts`](src/process.ts) | `E2BSubprocessHandle`: remote wrapper, publication, termination, output projection |
-| [`src/terminal.ts`](src/terminal.ts) | `E2BTerminalHandle`: PTY allocation, session teardown |
-| [`src/environment.ts`](src/environment.ts) | Remote environment probe, scrubbing, serialization |
-| [`src/output.ts`](src/output.ts) | Base64 decoder and bounded output readers |
-| [`src/remote.ts`](src/remote.ts) | Shared control-shell helpers: option shaping, poll ticks, group signalling |
-| — | No runtime invariant companion is published; live remote handles are private state owned by teardown logic, and the E2B command event stream is the sole outcome authority. |
-
-### Remote wrapper
-
-The bootstrap resolves its own tools from the sandbox PATH, refuses any missing or non-executable path, execs through `env -i` and `setsid --wait`, publishes the process-group id and exit code to private files beneath `ctx.e2b.runtimeRoot/processes`, and redirects stdout and stderr through base64 encoders that emit a reserved completion frame; `tee` and `head -c` bound optional spill files.
-
-### Private process identity and publication
-
-The synchronous seam returns a handle immediately while the command starts asynchronously. The wrapper publishes a private process-group ID for stdin, observation, termination, and quiescence checks, but that ID is not the requested target PID. A startup signal aborts environment and private-state preparation before allocation; once allocation begins, cancellation waits for a provisional SDK handle it can clean.
-
-### Environment boundary
-
-One trusted control-shell probe resolves the sandbox user's login home from its passwd entry and transports the sandbox environment as base64 ASCII for one strict UTF-8 decode; the wrapper then removes ambient `DSH_*` and credential-shaped (`*KEY*`, `*SECRET*`, `*TOKEN*`) names and restores every valid `spec.env` entry as an explicit caller opt-in. Empty names, `=`, and NUL framing violations reject before launch; subsequent command and PTY login shells receive a fresh randomized root-level `HOME` plus empty overrides for every scrubbed ambient name before user profiles can run. Private environment files are removed after consumption.
-
-### Output handling
-
-The remote wrapper branches raw bytes into optional bounded spill files and frames each live chunk as newline-delimited base64 ASCII; the host restores bytes across arbitrary SDK callback boundaries. Pipe mode writes to host Node streams, inherit mode to the harness process streams, and collect mode retains a bounded host tail with offset reads. For collect or inherit output, an incomplete SDK stream is disconnected after `graceMs` with its partial spill withheld; natural raw-pipe completion awaits lossless transport and preserves backpressure. Batch and streaming stdin use the SDK handle.
-
-### Termination ladder
-
-Termination and rollback share one tolerant signal path (`signalRemoteGroups`), escalate `SIGTERM` to `SIGKILL` on grace expiry, use the SDK kill as a fallback, and prove quiescence with a bounded process-table probe before reporting success; zombie-only groups count as empty, and a `SandboxNotFoundError` is treated as quiescence.
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## Further Exploration
-
-Read these pages when the package-level contract is not enough. They move from the family composition to the subprocess seam surface and the consumers that render it.
-
-- [E2B provider family map](../README.md) — the sandbox owner and the three-package composition.
-- [Subprocess subsystem](../../../docs/subsystems/subprocess.md) — the subprocess seam contract and the generated Cordis surface.
-- [Subprocess seam package](../../subprocess/subprocess/README.md) — the abstract contract this provider implements.
-- [Bash executor](../../shell/bash-local/README.md) — the consumer that renders spawned commands to the model.
-- [PTY terminal backend](../../terminal/terminal-bash/README.md) — the consumer that renders terminal sessions.
-- [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-subprocess-e2b) — every accepted config field and its source declaration.
-
------
-
-<a id="model-experience"></a>
-## Model Experience
-
-Indirectly, through consumer seams such as the bash executor family, which render remote output, exit facts, background deltas, and spill paths.
-
-#### KV Cache effect
-
-No direct invalidation: the consumer seams own any request-prefix changes; this backend's transport never reaches a request.
-
-## Known Limitations and Deferred Work
-
-<a id="known-limitations-and-deferred-work"></a>
-
-
-These limits define when the provider is a poor fit or needs special operational care. They are current package constraints, not a task backlog.
-
-- **The SDK still retains complete command output in host memory** — E2B `CommandHandle.stdout` and `.stderr` accumulate the base64 transport even when this adapter exposes bounded raw-byte tails, so the subprocess seam's normal host-memory bound is not achieved and transport retention is larger than the source stream.
-- **Private state lives for the sandbox lifetime** — process directories and valid spill files remain under `.dsh-e2b` until the owner deletes the sandbox; this POC supplies no in-sandbox sweep.
-- **Control state shares the sandbox user's UID** — E2B runs every command as the same default user, so `0700`/`0600` modes cannot isolate `.dsh-e2b` control files from concurrently running sandbox processes; real isolation needs an E2B per-command user or an out-of-band control channel.
-- **Numeric process identities are not reuse-fenced** — E2B exposes numeric PID/PGID input, signalling, and cleanup operations but no atomic identity-bound alternative; replacement is deferred until E2B adds an identity primitive or a failure demonstrates a narrower protocol.
-- **The initial environment probe inherits sandbox defaults** — E2B merges command overrides with default environment entries, so the probe cannot blank unknown credential-shaped names before enumerating them; this POC therefore does not support secrets in sandbox-default environment variables.
-- **E2B exposes no signal fact** — an adapter-requested `SIGTERM` or `SIGKILL` is reported only when no wrapper-published direct exit code wins; every unrequested SDK exit remains an exit code, including values equal to `128 + signal`.
-- **Exact terminal stdin-wait inspection is unavailable** — E2B exposes the foreground process group but not the syscall evidence needed to prove it is waiting on fd 0, so the generic PTY backend falls back to controlled prompt markers and bounded silence.
-- **Linux utility and E2B transport semantics are assumed** — there is no Windows, escaped-session recovery, or network-partition fidelity layer.
-
-<a id="dev-note"></a>
-### Dev Note
-
-<details>
-<summary>Working context for maintainers — click to expand</summary>
-
-This Dev Note is working context for maintainers: open questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above and the package code.
-
-#### Open: numeric process identities
-
-E2B exposes numeric PID/PGID input, signalling, and cleanup operations without an atomic identity-bound alternative. The adapter minimizes host round trips and defers a replacement until E2B adds an identity primitive or a failure demonstrates a narrower protocol (TODO(e2b-pgid-identity)).
-
-#### Open: replacement environments and status observation
-
-The initial environment probe inherits sandbox defaults because E2B merges command overrides, and collect/inherit command status needs control-plane polling because E2B cannot observe direct-command exit independently of descendant-held output. Both close only with new E2B primitives (TODO(e2b-replace-environment), TODO(e2b-status-watch)).
-
-</details>

+ 0 - 169
packages/e2b/subprocess-e2b/README.zh.md

@@ -1,169 +0,0 @@
----
-description: "共享远程沙箱内的 shell 命令与终端:agent(智能体)可以在那里运行什么、输出如何处理,以及可以期待什么——面向 E2B 家族的部署方与维护者。"
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-subprocess-e2b
-
-[English](README.md) | 中文
-
-## 概述
-
-`dsh-subprocess-e2b` 让 agent 的 shell 命令与交互式终端在 E2B 远程沙箱而非宿主中运行。现有的命令、终端与语言服务器工作流无需 E2B 专用工具即可继续使用。宿主环境变量与密钥不会传入沙箱;只有显式请求的环境条目会进入沙箱。请与 `dsh-e2b`、`dsh-fs-e2b` 一起使用,让命令、终端与文件共享同一个沙箱。远程执行会增加延迟,因为每条命令都需要异步初始化。
-
-## 目录
-
-- [使用本包](#use-this-package)
-- [理解实现](#understand-the-implementation)
-- [进一步探索](#further-exploration)
-- [模型体验](#model-experience)
-- [已知限制与延期工作](#known-limitations-and-deferred-work)
-- [开发备注](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## 使用本包
-
-当 agent 的 shell 命令与终端应在远程沙箱内而非你的机器上运行时,使用本包。它负责 E2B 家族中的命令执行部分:命令、终端与文件共享同一个远程执行环境。
-
-### 何时选择
-
-当组合已经使用 E2B 沙箱且希望命令与终端在其中运行时,选择本包。宿主执行请选择本地子进程包。
-
-### 配置
-
-唯一设置是包检查运行中命令状态的频率;默认值适合大多数部署,调大它可以减少远程请求,代价是退出检测略慢。
-
-| 字段 | 默认值 | 含义 |
-|---|---|---|
-| `pollMs` | `20` | 包检查运行中命令状态的频率(毫秒) |
-
-生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-subprocess-e2b)是每个受支持字段及其 JSDoc 的穷尽式真源。
-
-### 运行命令
-
-agent 可以在沙箱中按指定的工作目录与环境运行命令,选择输出的交付方式(实时流式、在大小上限内捕获,或路由到应用自身的输出),并在命令卡住时停止它——停止会先礼貌地请求命令退出,短暂宽限期后再强制终止,因此卡住的命令不会残留。非常大的输出可以保存到沙箱中的文件里,供 agent 稍后读取。命令的退出码会正常报告;如果命令运行期间沙箱消失,该命令会被视为已结束而不是报错。
-
-### 使用终端
-
-agent 可以在沙箱中打开交互式终端、发送输入、读取输出,并向其中运行的程序发送信号——提示符、交互式工具与全屏程序的行为与本地完全一致。scrollback 与就绪检测等终端功能由终端工具提供,无需改动即可工作。
-
-### 保持环境干净
-
-命令在干净、沙箱原生的环境中运行:宿主变量与形似凭据的值不会被隐式传入,只有 agent 显式请求的条目才会被设置。这使密钥不会进入沙箱。
-
-### 如果沙箱消失
-
-沙箱是短暂的:如果命令或终端运行期间沙箱被删除——无论是到期、关闭还是被别处移除——受影响的命令会被视为干净地结束。不要指望任何工作在沙箱删除后继续存在。
-
-默认沙箱镜像自带命令工作所需的运行时与工具:`node`、`bash`、`setsid`、`ps`、`awk`、`tr`、`env`、`base64`、`chmod`、`tee`、`head`、`rm`、`kill`、`id` 与 `getent`。
-
------
-
-<a id="understand-the-implementation"></a>
-## 理解实现
-
-<details>
-<summary>实现细节——点击展开</summary>
-
-本节解释提供方背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
-
-### 设计理念
-
-- **提供方私有的远程身份。** 同步 seam 从不阻塞在网络请求上。包装层的私有文件会异步发布进程组身份,供 stdin、观察、终止与完全停稳检查使用,同时发布直接退出码与 spill 有效性;该身份不是请求目标的 PID。
-- **单一终止阶梯。** 终止、回滚与资源释放共享同一条进程组信号路径——先 `SIGTERM`,再 `SIGKILL` 加 SDK kill 回退——并把已证明的完全停稳视为最终状态。
-- **环境必须显式。** 宿主内容与形似凭据的内容都不会隐式进入沙箱;每个环境值都会被清理,每个 `spec.env` 条目都是显式选择。
-
-### 源码地图
-
-| 文件 | 职责 |
-|---|---|
-| [`src/index.ts`](src/index.ts) | 插件入口:`E2BSubprocessRuntime`、`Config`、spawn 与 spawnTerminal、资源释放 |
-| [`src/process.ts`](src/process.ts) | `E2BSubprocessHandle`:远程包装层、发布、终止、输出投影 |
-| [`src/terminal.ts`](src/terminal.ts) | `E2BTerminalHandle`:PTY 分配、会话拆除 |
-| [`src/environment.ts`](src/environment.ts) | 远程环境探测、清理、序列化 |
-| [`src/output.ts`](src/output.ts) | base64 解码器与有界输出读取器 |
-| [`src/remote.ts`](src/remote.ts) | 共享控制 shell 辅助:选项构造、轮询 tick、进程组信号 |
-| — | 不发布运行时不变式伴生入口;存活的远程句柄是拆除逻辑持有的私有状态,E2B 命令事件流是判定结果的唯一权威来源。 |
-
-### 远程包装层
-
-引导脚本会从沙箱 PATH 解析自身所需的工具,拒绝任何缺失或不可执行的路径,通过 `env -i` 与 `setsid --wait` 执行 exec,把进程组 ID 与退出码发布到 `ctx.e2b.runtimeRoot/processes` 下的私有文件,并把 stdout 与 stderr 重定向到带保留完成帧的 base64 编码器;`tee` 与 `head -c` 约束可选 spill 文件的大小。
-
-### 私有进程身份与发布
-
-同步 seam 会立即返回句柄,同时命令异步启动。包装层会发布私有进程组 ID,供 stdin、观察、终止与完全停稳检查使用,但该 ID 不是请求目标的 PID。启动信号会在分配前中止环境与私有状态准备;分配开始后,取消会等待可清理的临时 SDK 句柄。
-
-### 环境边界
-
-一次受信任的控制 shell 探测会从 passwd 条目解析沙箱用户的登录主目录,以 base64 ASCII 传输沙箱环境,再进行一次严格 UTF-8 解码;随后包装层移除环境中的 `DSH_*` 与形似凭据的名称(`*KEY*`、`*SECRET*`、`*TOKEN*`),并把每个有效的 `spec.env` 条目恢复为调用方显式选择。空名称、`=` 与违反 NUL 分帧规则的条目会在启动前被拒绝;在用户 profile 脚本运行前,此后的命令与 PTY 登录 shell 会获得位于根目录下、全新随机生成的 `HOME`,并为每个被清理的环境变量名设置空值覆盖。私有环境文件在使用后会被删除。
-
-### 输出处理
-
-远程包装层先把原始字节分流到可选的有界 spill 文件,再把每个实时分片编码为换行分隔的 base64 ASCII 帧;宿主会跨任意 SDK 回调边界增量恢复字节。pipe 模式把字节写入宿主 Node 流,inherit 模式写入 harness 进程流,collect 模式保留有界的宿主尾部并支持偏移读取。对于 collect 或 inherit 输出,超过 `graceMs` 后适配器会断开未完成的 SDK 流并扣留其不完整的 spill;原始 pipe 自然完成时则会等待无损传输并保留背压。批量与流式 stdin 都使用 SDK 句柄。
-
-### 终止阶梯
-
-终止与回滚共享同一条容错信号路径(`signalRemoteGroups`),在宽限期满时从 `SIGTERM` 升级到 `SIGKILL`,以 SDK kill 作为回退,并在报告成功前用有界进程表探测证明完全停稳;仅含僵尸进程的进程组视为空,`SandboxNotFoundError` 视为完全停稳。
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## 进一步探索
-
-当包级约定不够用时阅读以下页面。它们从家族组合逐步进入子进程 seam 表面,以及渲染它的消费方。
-
-- [E2B 提供方家族地图](../README.zh.md)——沙箱所有者与三包组合。
-- [子进程子系统](../../../docs/subsystems/subprocess.zh.md)——子进程 seam 约定与生成的 Cordis 表面。
-- [子进程 seam 包](../../subprocess/subprocess/README.zh.md)——本提供方实现的抽象约定。
-- [Bash 执行器](../../shell/bash-local/README.zh.md)——向模型渲染所启动命令的消费方。
-- [PTY 终端后端](../../terminal/terminal-bash/README.zh.md)——渲染终端会话的消费方。
-- [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-subprocess-e2b)——每个受支持配置字段及其源声明。
-
------
-
-<a id="model-experience"></a>
-## 模型体验
-
-通过消费方 seam 间接影响模型,例如 bash 执行器家族;它们渲染远程输出、退出事实、后台增量与 spill 路径。
-
-#### KV Cache 影响
-
-不会直接失效:请求前缀变更由消费方 seam 负责;本后端的传输永远不会进入请求。
-
-## 已知限制与延期工作
-
-<a id="known-limitations-and-deferred-work"></a>
-
-
-这些限制说明本提供方何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。
-
-- **SDK 仍会在宿主内存中保留完整命令输出**:即使本适配器公开的是有界原始字节尾部,E2B `CommandHandle.stdout` 与 `.stderr` 仍会累积 base64 传输内容,因此无法达到子进程 seam 通常提供的宿主内存边界,而且传输保留量大于源数据流。
-- **私有状态随沙箱生命周期存在**:进程目录与有效的 spill 文件会留在 `.dsh-e2b` 下,直到所有者删除沙箱;本 POC 不提供沙箱内清理。
-- **控制状态与沙箱用户同 UID**:E2B 以同一默认用户运行每条命令,因此 `0700`/`0600` 权限无法把 `.dsh-e2b` 控制文件与并发运行的沙箱进程隔离开;真正的隔离需要 E2B 提供按命令用户或带外控制通道。
-- **数值进程身份没有复用围栏**:E2B 公开基于数值 PID/PGID 的输入、信号发送与清理操作,却没有与身份原子绑定的替代方案;在 E2B 新增身份原语,或实际故障证明需要更窄的协议之前,替代方案继续延后。
-- **初始环境探测会继承沙箱默认值**:E2B 会把命令覆盖与默认环境条目合并,因此探测无法在枚举未知且形似凭据的名称之前将它们置空;因此,该 POC 不支持把 secret 放入沙箱默认环境变量。
-- **E2B 不公开信号事实**:适配器请求的 `SIGTERM` 或 `SIGKILL` 只有在包装层发布的直接退出码没有胜出时才报告为信号;其他未请求的 SDK 退出始终保留为退出码,包括等于 `128 + signal` 的值。
-- **无法精确检查终端 stdin 等待状态**:E2B 会公开前台进程组,但不提供证明其正在等待 fd 0 所需的 syscall 证据,因此通用 PTY 后端会回退到受控提示符标记与有界静默机制。
-- **依赖 Linux 工具与 E2B 传输语义**:没有 Windows、逃逸会话恢复或网络分区的保真层。
-
-<a id="dev-note"></a>
-### 开发备注
-
-<details>
-<summary>维护者的工作上下文——点击展开</summary>
-
-本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文和包代码为准。
-
-#### 开放:数值进程身份
-
-E2B 公开基于数值 PID/PGID 的输入、信号发送与清理操作,却没有与身份原子绑定的替代方案。适配器会尽量减少宿主往返,并在 E2B 新增身份原语或实际故障证明需要更窄的协议之前,继续延后替代方案(TODO(e2b-pgid-identity))。
-
-#### 开放:替换环境与状态观察
-
-由于 E2B 会合并命令覆盖,初始环境探测会继承沙箱默认值;又因为 E2B 无法独立于后代持有的输出观察直接命令的退出,collect/inherit 命令状态需要控制面轮询。两者都只能靠 E2B 的新原语来弥合(TODO(e2b-replace-environment)、TODO(e2b-status-watch))。
-
-</details>

+ 0 - 44
packages/e2b/subprocess-e2b/package.json

@@ -1,44 +0,0 @@
-{
-  "name": "@deepseek-ai/dsh-subprocess-e2b",
-  "description": "E2B subprocess implementation for DeepSeek Harness",
-  "version": "0.1.5-rc.2",
-  "publishConfig": {
-    "access": "public"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
-    "directory": "packages/e2b/subprocess-e2b"
-  },
-  "type": "module",
-  "main": "lib/index.js",
-  "types": "lib/types/index.d.ts",
-  "exports": {
-    ".": {
-      "types": "./lib/types/index.d.ts",
-      "default": "./lib/index.js"
-    },
-    "./src/*": "./src/*",
-    "./package.json": "./package.json"
-  },
-  "files": [
-    "lib/index.js",
-    "lib/types/**/*.d.ts"
-  ],
-  "license": "MIT",
-  "peerDependencies": {
-    "@deepseek-ai/dsh-e2b": "workspace:^",
-    "@deepseek-ai/dsh-subprocess": "workspace:^",
-    "@deepseek-ai/dsh-timeout": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
-  },
-  "dependencies": {
-    "@deepseek-ai/schemastery": "workspace:^"
-  },
-  "devDependencies": {
-    "@deepseek-ai/dsh-e2b": "workspace:^",
-    "@deepseek-ai/dsh-subprocess": "workspace:^",
-    "@deepseek-ai/dsh-timeout": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
-  }
-}

+ 0 - 104
packages/e2b/subprocess-e2b/src/environment.ts

@@ -1,104 +0,0 @@
-/** Shared remote-environment scrubbing for E2B process and terminal launchers. */
-
-import { Buffer } from 'node:buffer'
-import { posix } from 'node:path'
-import { e2bControlEnvs } from '@deepseek-ai/dsh-e2b'
-import type { Sandbox } from '@deepseek-ai/dsh-e2b'
-import { SENSITIVE_ENV_PATTERN } from '@deepseek-ai/dsh-subprocess'
-
-const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
-
-function remoteEnvironmentEntries(raw: string): Array<readonly [string, string]> {
-  const entries: Array<readonly [string, string]> = []
-  for (const entry of raw.split('\0')) {
-    if (entry.length === 0) continue
-    const separator = entry.indexOf('=')
-    if (separator <= 0) continue
-    entries.push([entry.slice(0, separator), entry.slice(separator + 1)])
-  }
-  return entries
-}
-
-/**
- * Read the remote environment through ASCII base64 so SDK callback chunking cannot corrupt UTF-8.
- * @param sandbox - shared E2B execution world.
- * @param signal - optional cancellation for the control-plane request.
- * @returns the complete NUL-delimited UTF-8 environment.
- */
-export async function readRemoteEnvironment(sandbox: Sandbox, signal?: AbortSignal): Promise<string> {
-  // 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; 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')
-  if (lines.length !== 2 || !lines.every(line => BASE64.test(line))) {
-    throw new Error('subprocess-e2b: remote environment transport returned invalid base64')
-  }
-  const [encodedHome, encodedEnvironment] = lines as [string, string]
-  let home: string
-  let raw: string
-  try {
-    const decoder = new TextDecoder('utf-8', { fatal: true })
-    home = decoder.decode(Buffer.from(encodedHome, 'base64'))
-    raw = decoder.decode(Buffer.from(encodedEnvironment, 'base64'))
-  } catch (error: unknown) {
-    throw new Error('subprocess-e2b: remote environment is not valid UTF-8', { cause: error })
-  }
-  if (!posix.isAbsolute(home) || home.includes('\0')) {
-    throw new Error(`subprocess-e2b: remote login home is invalid: ${JSON.stringify(home)}`)
-  }
-  const environment = new Map(remoteEnvironmentEntries(raw))
-  environment.set('HOME', home)
-  return [...environment].map(([name, value]) => `${name}=${value}\0`).join('')
-}
-
-/**
- * Parse an E2B NUL-delimited environment while removing harness-private and credential-shaped names.
- * @param raw - The complete NUL-delimited remote environment.
- * @returns Mutable retained entries for the caller to overlay and serialize.
- */
-export function scrubRemoteEnvironment(raw: string): Map<string, string> {
-  const environment = new Map<string, string>()
-  for (const [name, value] of remoteEnvironmentEntries(raw)) {
-    if (name.startsWith('DSH_') || SENSITIVE_ENV_PATTERN.test(name)) continue
-    environment.set(name, value)
-  }
-  return environment
-}
-
-/**
- * Isolate E2B's fixed login-shell bootstrap from user profiles and ambient credentials.
- * @param raw - The complete NUL-delimited remote environment.
- * @returns Explicit E2B command or PTY overrides for bootstrap-shell startup.
- */
-export function bootstrapEnvironment(raw: string): Record<string, string> {
-  const environment: Record<string, string> = { TERM: 'dumb' }
-  for (const [name] of remoteEnvironmentEntries(raw)) {
-    if (name.startsWith('DSH_') || SENSITIVE_ENV_PATTERN.test(name)) environment[name] = ''
-  }
-  return environment
-}
-
-/**
- * Overlay explicit entries and serialize one validated E2B environment.
- * @param raw - The complete NUL-delimited remote environment.
- * @param explicit - Deliberate caller overrides applied after ambient scrubbing; an `undefined` tombstone removes an ambient entry.
- * @returns NUL-delimited `name=value` entries accepted by `env -i`.
- */
-export function serializeRemoteEnvironment(
-  raw: string,
-  explicit: Readonly<NodeJS.ProcessEnv> | undefined,
-): string {
-  const environment = scrubRemoteEnvironment(raw)
-  for (const [name, value] of Object.entries(explicit ?? {})) {
-    if (name.length === 0 || name.includes('=') || name.includes('\0') || value?.includes('\0') === true) {
-      throw new Error('subprocess-e2b: environment entries require non-empty NUL-free names without = and NUL-free values')
-    }
-    // An explicit undefined is the seam's tombstone: remove the ambient entry.
-    if (value === undefined) environment.delete(name)
-    else environment.set(name, value)
-  }
-  return [...environment].map(([name, value]) => `${name}=${value}\0`).join('')
-}

+ 0 - 231
packages/e2b/subprocess-e2b/src/index.ts

@@ -1,231 +0,0 @@
-/**
- * E2B Service Provider for the subprocess capability seam. Each handle starts through the
- * shared sandbox and retains command output/status paths in that remote world.
- * @module @deepseek-ai/dsh-subprocess-e2b
- */
-
-import { randomUUID } from 'node:crypto'
-import { posix } from 'node:path'
-import { inspect } from 'node:util'
-import { Context } from '@deepseek-ai/cordis'
-import z from '@deepseek-ai/schemastery'
-import { SubprocessRuntime } from '@deepseek-ai/dsh-subprocess'
-import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
-import type {
-  SubprocessHandle,
-  SubprocessSpawnSpec,
-  SubprocessTerminalHandle,
-  SubprocessTerminalSpawnSpec,
-} from '@deepseek-ai/dsh-subprocess'
-import { e2bControlEnvs, quoteE2BShellArg } from '@deepseek-ai/dsh-e2b'
-import { E2BSubprocessHandle } from './process.ts'
-import { asError, signalOpts } from './remote.ts'
-import { spawnE2BTerminal } from './terminal.ts'
-
-/** Configuration for the E2B subprocess adapter. */
-export interface Config {
-  /** Remote status/liveness poll cadence in milliseconds; each tick is one control-plane request. */
-  pollMs?: number
-}
-
-interface SchemaResolvedConfig extends Config {
-  pollMs: number
-}
-
-interface TerminalSetup {
-  done: Promise<void>
-  controller: AbortController
-}
-
-/**
- * Enforce the seam's documented grace bound (positive, finite, one Node timer),
- * matching subprocess-local's spawn-time check; an unbounded grace would make
- * the remote force-escalation deadline unreachable.
- * @param graceMs - The spec's cleanup grace in milliseconds.
- */
-function requireRepresentableGrace(graceMs: number): void {
-  if (!Number.isFinite(graceMs) || graceMs <= 0 || graceMs > MAX_TIMER_DELAY_MS) {
-    throw new Error(`subprocess graceMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`)
-  }
-}
-
-function validateNoNullByte(subject: string, value: string): void {
-  if (!value.includes('\0')) return
-  const error = new TypeError(`${subject} must be a string without null bytes. Received ${inspect(value)}`)
-  Object.assign(error, { code: 'ERR_INVALID_ARG_VALUE' })
-  throw error
-}
-
-/** E2B command manager registered as `ctx.subprocess`. */
-export class E2BSubprocessRuntime extends SubprocessRuntime {
-  static inject = ['e2b']
-
-  static Config: z<Config> = z.object({
-    pollMs: z.number().default(20),
-  })
-
-  private readonly live = new Set<E2BSubprocessHandle>()
-  private readonly terminals = new Set<SubprocessTerminalHandle>()
-  private readonly terminalSetups = new Set<TerminalSetup>()
-  private readonly pollMs: number
-  private disposing = false
-
-  /** Create the E2B subprocess service and bind its disposal policy. */
-  constructor(ctx: Context, config: Config) {
-    super(ctx)
-    // Schemastery fills pollMs before construction; the type does not encode that step.
-    const { pollMs } = config as SchemaResolvedConfig
-    if (!Number.isSafeInteger(pollMs) || pollMs <= 0) {
-      throw new Error('subprocess-e2b: pollMs must be a positive safe integer')
-    }
-    this.pollMs = pollMs
-    ctx.effect(() => async () => {
-      this.disposing = true
-      for (const setup of this.terminalSetups) {
-        setup.controller.abort(new Error('subprocess-e2b: service disposed during terminal setup'))
-      }
-      await Promise.all([...this.terminalSetups].map(setup => setup.done))
-      const handles = [...this.live]
-      const terminals = [...this.terminals]
-      const pending: Promise<unknown>[] = []
-      for (const handle of handles) {
-        handle.terminate()
-        pending.push(handle.waitForExit().then(async () => {
-          await handle.done.catch(() => undefined)
-          this.live.delete(handle)
-        }))
-      }
-      for (const terminal of terminals) {
-        pending.push(terminal.terminate().then(() => { this.terminals.delete(terminal) }))
-      }
-      const outcomes = await Promise.allSettled(pending)
-      const failures = outcomes.flatMap<unknown>(outcome => outcome.status === 'rejected'
-        ? [outcome.reason as unknown]
-        : [])
-      if (failures.length === 1) throw asError(failures[0])
-      if (failures.length > 1) throw new AggregateError(failures, 'subprocess-e2b: teardown failed')
-    }, 'e2b subprocess teardown')
-  }
-
-  /** @inheritdoc */
-  async resolveExecutable(
-    command: string,
-    env?: Readonly<Record<string, string>>,
-    signal?: AbortSignal,
-  ): Promise<string> {
-    if (command.length === 0) throw new Error('subprocess-e2b: executable name must be non-empty')
-    signal?.throwIfAborted()
-    const sandbox = await this.ctx.e2b.getSandbox()
-    if (posix.isAbsolute(command)) {
-      await sandbox.commands.run(
-        `test -f ${quoteE2BShellArg(command)} -a -x ${quoteE2BShellArg(command)}`,
-        { envs: e2bControlEnvs(), ...signalOpts(signal) },
-      )
-      signal?.throwIfAborted()
-      return command
-    }
-    if (command.includes('/')) {
-      throw new Error(
-        `subprocess-e2b: command ${JSON.stringify(command)} is a relative path; use an absolute path or a bare PATH name`,
-      )
-    }
-    const path = env?.PATH
-    const prefix = path === undefined ? '' : `PATH=${quoteE2BShellArg(path)} `
-    const result = await sandbox.commands.run(
-      `${prefix}command -v -- ${quoteE2BShellArg(command)}`,
-      { 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`)
-    }
-    // A relative result comes from a relative PATH entry; the lookup ran with the shared cwd.
-    return posix.resolve(this.ctx.e2b.cwd, executable)
-  }
-
-  /** @inheritdoc */
-  spawn(spec: SubprocessSpawnSpec): SubprocessHandle {
-    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]')
-    }
-    requireRepresentableGrace(spec.graceMs)
-    if (spec.signal?.aborted === true) {
-      let reason = 'aborted'
-      try {
-        reason = String(spec.signal.reason ?? reason)
-      } catch {
-        // Arbitrary caller-owned reasons cannot escape the stable Error boundary.
-      }
-      throw new Error(`aborted before spawn: ${reason}`)
-    }
-    spec.argv.forEach((value, index) => {
-      validateNoNullByte(index === 0 ? "The argument 'file'" : `The argument 'args[${String(index - 1)}]'`, value)
-    })
-    validateNoNullByte("The property 'options.cwd'", spec.cwd)
-    for (const [key, value] of Object.entries(spec.env ?? {})) {
-      if (value === undefined) continue
-      validateNoNullByte(`The property 'options.env['${key}']'`, key)
-      validateNoNullByte(`The property 'options.env['${key}']'`, value)
-    }
-    const stateDir = posix.join(this.ctx.e2b.runtimeRoot, 'processes', randomUUID())
-    const handle = new E2BSubprocessHandle(this.ctx.e2b, spec, stateDir, this.pollMs)
-    this.live.add(handle)
-    const release = async (): Promise<void> => {
-      await handle.waitForExit()
-      this.live.delete(handle)
-    }
-    void handle.done.then(release, release).catch((_automaticReleaseFailure: unknown) => {
-      // Retain the handle so service disposal can retry its cleanup transaction.
-    })
-    return handle
-  }
-
-  /** @inheritdoc */
-  async spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise<SubprocessTerminalHandle> {
-    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')
-    }
-    requireRepresentableGrace(spec.graceMs)
-    spec.signal?.throwIfAborted()
-    const stateDir = posix.join(this.ctx.e2b.runtimeRoot, 'terminals', randomUUID())
-    const done = Promise.withResolvers<void>()
-    const setup: TerminalSetup = { done: done.promise, controller: new AbortController() }
-    const setupSignal = spec.signal === undefined
-      ? setup.controller.signal
-      : AbortSignal.any([spec.signal, setup.controller.signal])
-    this.terminalSetups.add(setup)
-    try {
-      const terminal = await spawnE2BTerminal(
-        this.ctx.e2b,
-        { ...spec, signal: setupSignal },
-        stateDir,
-        this.pollMs,
-      )
-      this.terminals.add(terminal)
-      // 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')
-      }
-      const release = async (): Promise<void> => {
-        await terminal.terminate()
-        this.terminals.delete(terminal)
-      }
-      void terminal.done.then(release, release).catch((_automaticReleaseFailure: unknown) => {
-        // Retain the terminal so service disposal can retry its cleanup transaction.
-      })
-      return terminal
-    } finally {
-      this.terminalSetups.delete(setup)
-      done.resolve()
-    }
-  }
-}
-
-export default E2BSubprocessRuntime

+ 0 - 131
packages/e2b/subprocess-e2b/src/output.ts

@@ -1,131 +0,0 @@
-/** Bounded host-side projection of a complete output file retained in E2B. */
-
-import { Buffer } from 'node:buffer'
-import type { SubprocessOutputRead, SubprocessOutputReader } from '@deepseek-ai/dsh-subprocess'
-
-const BASE64_TEXT = /^[A-Za-z0-9+/]+={0,2}$/u
-
-/** Reserved non-base64 frame proving that one remote encoder reached clean EOF. */
-export const E2B_OUTPUT_COMPLETE_FRAME = '!dsh-e2b-output-complete!'
-
-/** Incrementally decode newline-delimited base64 frames emitted by one remote encoder. */
-export class E2BBase64Decoder {
-  private pending = ''
-  private complete = false
-
-  /**
-   * Decode every complete newline-delimited frame in one arbitrarily split SDK callback.
-   * @param text - ASCII base64 frames from E2B's decoded callback.
-   * @returns the complete raw bytes made available by this callback.
-   */
-  push(text: string): Buffer {
-    if (text.length === 0) return Buffer.alloc(0)
-    this.pending += text
-    const decoded: Buffer[] = []
-    for (;;) {
-      const boundary = this.pending.indexOf('\n')
-      if (boundary < 0) break
-      const frame = this.pending.slice(0, boundary)
-      this.pending = this.pending.slice(boundary + 1)
-      if (frame === E2B_OUTPUT_COMPLETE_FRAME) {
-        if (this.complete) throw new Error('subprocess-e2b: duplicate output transport completion')
-        this.complete = true
-        continue
-      }
-      if (this.complete) throw new Error('subprocess-e2b: output transport continued after completion')
-      if (!BASE64_TEXT.test(frame)) {
-        throw new Error('subprocess-e2b: invalid base64 output transport')
-      }
-      const bytes = Buffer.from(frame, 'base64')
-      if (bytes.toString('base64') !== frame) {
-        throw new Error('subprocess-e2b: invalid base64 output transport')
-      }
-      decoded.push(bytes)
-    }
-    return Buffer.concat(decoded)
-  }
-
-  /**
-   * Validate clean encoder completion, or discard an interrupted trailing frame after requested termination.
-   * @param requireComplete - Whether natural completion requires the reserved EOF frame.
-   */
-  finish(requireComplete = true): void {
-    if (!requireComplete) {
-      this.pending = ''
-      return
-    }
-    if (this.pending.length > 0) {
-      throw new Error('subprocess-e2b: truncated base64 output transport')
-    }
-    if (!this.complete) throw new Error('subprocess-e2b: incomplete output transport')
-  }
-}
-
-/** Offset reader used for one collect-mode E2B stream. */
-export class E2BOutputReader implements SubprocessOutputReader {
-  private chunks: Buffer[] = []
-  private retainedBytes = 0
-  private totalBytes = 0
-  private spillValid = true
-
-  /**
-   * Create a bounded reader over one remote spill path.
-   * @param maxBytes - In-memory tail cap.
-   * @param maxSpillBytes - Maximum complete remote file size the caller accepts.
-   * @param spillPath - Remote full-output path.
-   */
-  constructor(
-    private readonly maxBytes: number,
-    private readonly maxSpillBytes: number | undefined,
-    private readonly spillPath: string,
-  ) {}
-
-  /** Total bytes observed from the SDK stream. */
-  get size(): number {
-    return this.totalBytes
-  }
-
-  /** Stop advertising a remote spill whose writer did not reach clean EOF. */
-  invalidateSpill(): void {
-    this.spillValid = false
-  }
-
-  /**
-   * Append one byte-faithful decoded transport event.
-   * @param bytes - Raw command bytes recovered from the ASCII SDK transport.
-   */
-  push(bytes: Uint8Array): void {
-    if (bytes.length === 0) return
-    const chunk = Buffer.from(bytes)
-    this.totalBytes += chunk.length
-    this.chunks.push(chunk)
-    this.retainedBytes += chunk.length
-    while (this.retainedBytes > this.maxBytes) {
-      const head = this.chunks[0] as Buffer
-      const excess = this.retainedBytes - this.maxBytes
-      if (head.length <= excess) {
-        this.chunks.shift()
-        this.retainedBytes -= head.length
-      } else {
-        this.chunks[0] = head.subarray(excess)
-        this.retainedBytes -= excess
-      }
-    }
-  }
-
-  /** @inheritdoc */
-  readFrom(fromByte: number): SubprocessOutputRead {
-    const retained = Buffer.concat(this.chunks, this.retainedBytes)
-    const firstRetained = this.totalBytes - this.retainedBytes
-    const lossy = fromByte < firstRetained
-    const start = lossy ? 0 : Math.min(retained.length, Math.max(0, fromByte - firstRetained))
-    return {
-      text: retained.subarray(start).toString('utf8'),
-      nextOffset: this.totalBytes,
-      lossy,
-      ...(lossy && this.spillValid && this.maxSpillBytes !== undefined && this.totalBytes <= this.maxSpillBytes
-        ? { spillPath: this.spillPath }
-        : {}),
-    }
-  }
-}

+ 0 - 693
packages/e2b/subprocess-e2b/src/process.ts

@@ -1,693 +0,0 @@
-/** One asynchronously-started E2B command projected onto the subprocess seam. */
-
-import { Buffer } from 'node:buffer'
-import { PassThrough, Writable } from 'node:stream'
-import { posix } from 'node:path'
-import {
-  CommandExitError,
-  e2bControlEnvs,
-  FileNotFoundError,
-  SandboxNotFoundError,
-  quoteE2BShellArg,
-} from '@deepseek-ai/dsh-e2b'
-import type { CommandHandle, CommandResult, Sandbox } from '@deepseek-ai/dsh-e2b'
-import type {
-  SubprocessCollect,
-  SubprocessHandle,
-  SubprocessOutcome,
-  SubprocessOutputMode,
-  SubprocessSpawnSpec,
-} from '@deepseek-ai/dsh-subprocess'
-import type E2BRuntime from '@deepseek-ai/dsh-e2b'
-import { bootstrapEnvironment, readRemoteEnvironment, serializeRemoteEnvironment } from './environment.ts'
-import { E2BBase64Decoder, E2B_OUTPUT_COMPLETE_FRAME, E2BOutputReader } from './output.ts'
-import { asError, commandOpts, signalRemoteGroups, waitTick } from './remote.ts'
-
-const OUTPUT_ENCODER_SOURCE = [
-  '(async () => {',
-  '  for await (const chunk of process.stdin) {',
-  "    if (!process.stdout.write(chunk.toString('base64') + '\\n')) {",
-  "      await new Promise(resolve => process.stdout.once('drain', resolve))",
-  '    }',
-  '  }',
-  `  if (!process.stdout.write(${JSON.stringify(E2B_OUTPUT_COMPLETE_FRAME)} + '\\n')) {`,
-  "    await new Promise(resolve => process.stdout.once('drain', resolve))",
-  '  }',
-  '})().catch(() => { process.exitCode = 1 })',
-].join('\n')
-
-function isCollect(mode: SubprocessOutputMode): mode is SubprocessCollect {
-  return mode !== 'pipe' && mode !== 'inherit'
-}
-
-function hasSpill(mode: SubprocessOutputMode): mode is SubprocessCollect & { spill: { maxBytes: number } } {
-  return isCollect(mode) && mode.spill !== undefined
-}
-
-function isValidProcessId(value: number): boolean {
-  return Number.isSafeInteger(value) && value > 0
-}
-
-class DeferredStdin extends Writable {
-  constructor(private readonly ready: Promise<CommandHandle>) {
-    super({ decodeStrings: false })
-  }
-
-  override _write(chunk: string | Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
-    void this.ready.then(handle => handle.sendStdin(chunk)).then(
-      () => { callback() },
-      (error: unknown) => { callback(asError(error)) },
-    )
-  }
-
-  override _final(callback: (error?: Error | null) => void): void {
-    void this.ready.then(handle => handle.closeStdin()).then(
-      () => { callback() },
-      (error: unknown) => { callback(asError(error)) },
-    )
-  }
-}
-
-interface RemotePaths {
-  pid: string
-  status: string
-  environment: string
-  stdout: string
-  stderr: string
-}
-
-type CommandSettlement =
-  | { kind: 'result'; result: CommandResult }
-  | { kind: 'error'; error: unknown }
-
-function withinMs(settlement: Promise<CommandSettlement>, timeoutMs: number): Promise<CommandSettlement | undefined> {
-  return new Promise<CommandSettlement | undefined>((resolve) => {
-    const timer = setTimeout(() => { resolve(undefined) }, timeoutMs)
-    void settlement.then((value) => {
-      clearTimeout(timer)
-      resolve(value)
-    })
-  })
-}
-
-function commandText(spec: SubprocessSpawnSpec, paths: RemotePaths): string {
-  const encoder = `"$dsh_e2b_env_bin" -i "$dsh_e2b_node" -e ${quoteE2BShellArg(OUTPUT_ENCODER_SOURCE)}`
-  const stdoutRedirect = hasSpill(spec.stdio.stdout)
-    ? `> >("$dsh_e2b_tee" --output-error=warn-nopipe >("$dsh_e2b_head" -c ${spec.stdio.stdout.spill.maxBytes} > ${quoteE2BShellArg(paths.stdout)}) | ${encoder} 2>/dev/null)`
-    : `> >(${encoder} 2>/dev/null)`
-  const stderrRedirect = hasSpill(spec.stdio.stderr)
-    ? `2> >("$dsh_e2b_tee" --output-error=warn-nopipe >("$dsh_e2b_head" -c ${spec.stdio.stderr.spill.maxBytes} > ${quoteE2BShellArg(paths.stderr)}) | ${encoder} >&2 2>/dev/null)`
-    : `2> >(${encoder} >&2 2>/dev/null)`
-  const inner = [
-    'set +e',
-    'dsh_e2b_env_bin=$1',
-    'dsh_e2b_node=$2',
-    'dsh_e2b_ps=$3',
-    'dsh_e2b_tr=$4',
-    'dsh_e2b_tee=$5',
-    'dsh_e2b_head=$6',
-    'dsh_e2b_rm=$7',
-    'shift 7',
-    'dsh_e2b_pgid="$("$dsh_e2b_ps" -o pgid= -p "$$" | "$dsh_e2b_tr" -d " ")"',
-    `printf '%s\\n' "$dsh_e2b_pgid" > ${quoteE2BShellArg(paths.pid)}`,
-    `mapfile -d '' -t dsh_e2b_env < ${quoteE2BShellArg(paths.environment)}`,
-    `"$dsh_e2b_rm" -f -- ${quoteE2BShellArg(paths.environment)}`,
-    `"$dsh_e2b_env_bin" -i -- "\${dsh_e2b_env[@]}" "$@" ${stdoutRedirect} ${stderrRedirect}`.trimEnd(),
-    'dsh_e2b_status=$?',
-    `printf '%s\\n' "$dsh_e2b_status" > ${quoteE2BShellArg(paths.status)}`,
-    'wait',
-    'exit "$dsh_e2b_status"',
-  ].join('\n')
-  const argv = spec.argv.map(quoteE2BShellArg).join(' ')
-  const bootstrap = [
-    `mapfile -d '' -t dsh_e2b_env < ${quoteE2BShellArg(paths.environment)}`,
-    'dsh_e2b_env_bin="$(command -v env)"',
-    'dsh_e2b_setsid="$(command -v setsid)"',
-    'dsh_e2b_bash="$(command -v bash)"',
-    'dsh_e2b_node="$(command -v node)"',
-    'dsh_e2b_ps="$(command -v ps)"',
-    'dsh_e2b_tr="$(command -v tr)"',
-    'dsh_e2b_tee="$(command -v tee)"',
-    'dsh_e2b_head="$(command -v head)"',
-    'dsh_e2b_rm="$(command -v rm)"',
-    'for dsh_e2b_tool in "$dsh_e2b_env_bin" "$dsh_e2b_setsid" "$dsh_e2b_bash" "$dsh_e2b_node" "$dsh_e2b_ps" "$dsh_e2b_tr" "$dsh_e2b_tee" "$dsh_e2b_head" "$dsh_e2b_rm"; do',
-    '  [[ "$dsh_e2b_tool" == /* && -x "$dsh_e2b_tool" ]] || exit 125',
-    'done',
-    `exec "$dsh_e2b_env_bin" -i -- "\${dsh_e2b_env[@]}" "$dsh_e2b_setsid" --wait -- "$dsh_e2b_bash" -c ${quoteE2BShellArg(inner)} dsh-e2b "$dsh_e2b_env_bin" "$dsh_e2b_node" "$dsh_e2b_ps" "$dsh_e2b_tr" "$dsh_e2b_tee" "$dsh_e2b_head" "$dsh_e2b_rm" ${argv}`,
-  ].join('\n')
-  return bootstrap
-}
-
-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) => {
-    const onAbort = (): void => { cleanup(); resolve(WAIT_ABORTED) }
-    const cleanup = (): void => { signal.removeEventListener('abort', onAbort) }
-    signal.addEventListener('abort', onAbort, { once: true })
-    if (signal.aborted) {
-      onAbort()
-      return
-    }
-    void promise.then((value) => { cleanup(); resolve(value) })
-  })
-}
-
-/** E2B-backed subprocess handle with private remote process-group tracking. */
-export class E2BSubprocessHandle implements SubprocessHandle {
-  readonly stdin: Writable | undefined
-  readonly stdout: PassThrough | undefined
-  readonly stderr: PassThrough | undefined
-  readonly collected: SubprocessHandle['collected']
-  readonly done: Promise<SubprocessOutcome>
-
-  private readonly commandState = Promise.withResolvers<CommandHandle | undefined>()
-  private readonly readyState = Promise.withResolvers<CommandHandle>()
-  private readonly stdoutDecoder = new E2BBase64Decoder()
-  private readonly stderrDecoder = new E2BBase64Decoder()
-  private readonly terminationController = new AbortController()
-  /** Releases output waits that survive the command outcome, so blocked SDK callbacks settle. */
-  private readonly outputReleased = new AbortController()
-  private readonly stdoutReader: E2BOutputReader | undefined
-  private readonly stderrReader: E2BOutputReader | undefined
-  private readonly paths: RemotePaths
-  private controlEnvs: Record<string, string> = {}
-  private remoteProcessGroupId: number | undefined
-  private outputTransportError: Error | undefined
-  private outputDrainExpired = false
-  private stateDirectoryCreated = false
-  private quiescenceProven = false
-  private terminationAttempt: Promise<void> | undefined
-  private terminationFailure: Error | undefined
-  private terminationSignal: NodeJS.Signals | null = null
-
-  /**
-   * Begin an E2B command without blocking the synchronous subprocess spawn call.
-   * @param runtime - Shared E2B sandbox owner.
-   * @param spec - Fully resolved subprocess request.
-   * @param stateDir - Remote directory retaining process identity, status, and valid spills.
-   * @param pollMs - Remote status/liveness poll cadence.
-   */
-  constructor(
-    private readonly runtime: E2BRuntime,
-    private readonly spec: SubprocessSpawnSpec,
-    readonly stateDir: string,
-    private readonly pollMs: number,
-  ) {
-    this.paths = {
-      pid: posix.join(stateDir, 'pid'),
-      status: posix.join(stateDir, 'exit-code'),
-      environment: posix.join(stateDir, 'environment'),
-      stdout: posix.join(stateDir, 'stdout.log'),
-      stderr: posix.join(stateDir, 'stderr.log'),
-    }
-    const outMode = spec.stdio.stdout
-    const errMode = spec.stdio.stderr
-    this.stdout = outMode === 'pipe' ? new PassThrough() : undefined
-    this.stderr = errMode === 'pipe' ? new PassThrough() : undefined
-    this.stdoutReader = isCollect(outMode)
-      ? new E2BOutputReader(outMode.maxBytes, outMode.spill?.maxBytes, this.paths.stdout)
-      : undefined
-    this.stderrReader = isCollect(errMode)
-      ? new E2BOutputReader(errMode.maxBytes, errMode.spill?.maxBytes, this.paths.stderr)
-      : undefined
-    this.collected = {
-      ...(this.stdoutReader !== undefined ? { stdout: this.stdoutReader } : {}),
-      ...(this.stderrReader !== undefined ? { stderr: this.stderrReader } : {}),
-    }
-    this.stdin = spec.stdio.stdin === 'pipe' ? new DeferredStdin(this.readyState.promise) : undefined
-    void this.readyState.promise.catch(() => {})
-    spec.signal?.addEventListener('abort', this.onAbort, { once: true })
-    this.done = this.run()
-    void this.done.catch(() => {})
-    if (spec.signal?.aborted === true) this.terminate()
-  }
-
-  /** @inheritdoc */
-  terminate(): void {
-    if (this.quiescenceProven || this.terminationAttempt !== undefined) return
-    this.terminationController.abort(new Error('subprocess-e2b: command terminated'))
-    this.stdout?.destroy()
-    this.stderr?.destroy()
-    this.terminationFailure = undefined
-    const attempt = this.terminateRemote()
-    this.terminationAttempt = attempt
-    void attempt.then(
-      () => { this.terminationAttempt = undefined },
-      (error: unknown) => {
-        if (!this.quiescenceProven) this.terminationFailure = asError(error)
-        this.terminationAttempt = undefined
-      },
-    )
-  }
-
-  /** @inheritdoc */
-  async waitForExit(signal?: AbortSignal): Promise<boolean> {
-    if (this.quiescenceProven) return true
-    let handle: CommandHandle | undefined
-    if (this.terminationController.signal.aborted) {
-      const observed = await waitWithSignal(this.commandState.promise, signal)
-      if (observed === WAIT_ABORTED) return false
-      handle = observed
-      if (handle === undefined) {
-        this.markQuiescent()
-        return true
-      }
-      if (this.remoteProcessGroupId === undefined) {
-        const attempt = this.terminationAttempt
-        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 {
-      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()
-    let sandbox: Sandbox
-    try {
-      sandbox = await this.runtime.getSandbox()
-    } catch (error: unknown) {
-      if (signal?.aborted === true) return false
-      if (error instanceof SandboxNotFoundError) {
-        this.markQuiescent()
-        return true
-      }
-      throw error
-    }
-    const processGroupId = this.remoteProcessGroupId ?? handle.pid
-    while (await this.groupAlive(sandbox, processGroupId, signal)) {
-      this.throwTerminationFailure()
-      if (!await waitTick(this.pollMs, signal)) return false
-    }
-    this.throwTerminationFailure()
-    if (signal?.aborted === true) return false
-    this.markQuiescent()
-    return true
-  }
-
-  private readonly onAbort = (): void => { this.terminate() }
-
-  private markQuiescent(): void {
-    this.quiescenceProven = true
-    this.terminationFailure = undefined
-  }
-
-  private async run(): Promise<SubprocessOutcome> {
-    let sandbox: Sandbox | undefined
-    let preparing = true
-    try {
-      sandbox = await this.runtime.getSandbox()
-      await this.prepareState(sandbox)
-      preparing = false
-      const handle = await sandbox.commands.run(
-        commandText(this.spec, this.paths),
-        {
-          background: true,
-          cwd: this.spec.cwd,
-          envs: e2bControlEnvs(this.controlEnvs),
-          stdin: this.spec.stdio.stdin !== 'ignore',
-          timeoutMs: 0,
-          onStdout: async (data) => { await this.dispatchOutput('stdout', data) },
-          onStderr: async (data) => { await this.dispatchOutput('stderr', data) },
-        },
-      )
-      const completion = handle.wait()
-      void completion.catch(() => {})
-      if (!isValidProcessId(handle.pid)) {
-        const invalidPid = new Error(`subprocess-e2b: E2B returned invalid command pid ${handle.pid}`)
-        try {
-          await handle.kill()
-          this.markQuiescent()
-        } catch (cleanupError: unknown) {
-          this.terminationFailure = asError(cleanupError)
-          this.commandState.resolve(handle)
-          throw new AggregateError(
-            [invalidPid, cleanupError],
-            'subprocess-e2b: invalid command pid rollback did not reach quiescence',
-          )
-        }
-        throw invalidPid
-      }
-      this.commandState.resolve(handle)
-      try {
-        this.remoteProcessGroupId = await this.waitForProcessGroupId(sandbox, completion)
-      } catch (error: unknown) {
-        try {
-          await this.rollbackUnpublishedGroup(sandbox, handle)
-        } catch (cleanupError: unknown) {
-          throw new AggregateError(
-            [error, cleanupError],
-            'subprocess-e2b: process-group publication failed and rollback did not reach quiescence',
-          )
-        }
-        throw error
-      }
-      this.readyState.resolve(handle)
-      await this.writeBatchStdin(handle)
-      const outcome = await this.waitForCommand(sandbox, handle, completion)
-      if (this.outputTransportError !== undefined) throw this.outputTransportError
-      const requireCompleteOutput = this.terminationSignal === null && !this.outputDrainExpired
-      this.stdoutDecoder.finish(requireCompleteOutput)
-      this.stderrDecoder.finish(requireCompleteOutput)
-      await this.finalizeSpills(sandbox)
-      return outcome
-    } catch (error: unknown) {
-      const canceledPreparation = preparing && this.terminationController.signal.aborted
-      let failure = await this.rollbackPublishedFailure(error)
-      if (sandbox !== undefined && this.stateDirectoryCreated) {
-        try {
-          await this.removeFailedState(sandbox)
-        } catch (cleanupError: unknown) {
-          failure = new AggregateError(
-            [failure, cleanupError],
-            'subprocess-e2b: command failed and private state cleanup failed',
-          )
-        }
-      }
-      this.commandState.resolve(undefined)
-      this.readyState.reject(failure)
-      if (canceledPreparation && failure === error) return { exitCode: null, signal: 'SIGTERM' }
-      throw failure
-    } finally {
-      this.spec.signal?.removeEventListener('abort', this.onAbort)
-      this.stdout?.end()
-      this.stderr?.end()
-    }
-  }
-
-  private async prepareState(sandbox: Sandbox): Promise<void> {
-    const signal = this.terminationController.signal
-    const ambient = await readRemoteEnvironment(sandbox, signal)
-    this.controlEnvs = bootstrapEnvironment(ambient)
-    // Own the directory before the request: a cancellation racing a committed
-    // creation must still enter cleanup (removal tolerates an absent path).
-    this.stateDirectoryCreated = true
-    await sandbox.files.makeDir(this.stateDir, { signal })
-    await sandbox.commands.run(
-      `chmod 700 -- ${quoteE2BShellArg(this.stateDir)}`,
-      commandOpts(this.controlEnvs, signal),
-    )
-    const files = [
-      { path: this.paths.pid, data: '' },
-      { path: this.paths.status, data: '' },
-      { path: this.paths.environment, data: serializeRemoteEnvironment(ambient, this.spec.env) },
-      ...(hasSpill(this.spec.stdio.stdout) ? [{ path: this.paths.stdout, data: '' }] : []),
-      ...(hasSpill(this.spec.stdio.stderr) ? [{ path: this.paths.stderr, data: '' }] : []),
-    ]
-    await sandbox.files.write(files, { signal })
-    await sandbox.commands.run(
-      `chmod 600 -- ${files.map(file => quoteE2BShellArg(file.path)).join(' ')}`,
-      commandOpts(this.controlEnvs, signal),
-    )
-    signal.throwIfAborted()
-  }
-
-  private async writeBatchStdin(handle: CommandHandle): Promise<void> {
-    if (typeof this.spec.stdio.stdin !== 'object') return
-    try {
-      await handle.sendStdin(this.spec.stdio.stdin.data)
-      await handle.closeStdin()
-    } catch (_processClosedItsInput) {
-      // Like the local adapter, batch stdin is best-effort; exit and output remain authoritative.
-    }
-  }
-
-  private async dispatchOutput(stream: 'stdout' | 'stderr', data: string): Promise<void> {
-    let bytes: Buffer
-    try {
-      bytes = stream === 'stdout' ? this.stdoutDecoder.push(data) : this.stderrDecoder.push(data)
-    } catch (error: unknown) {
-      this.outputTransportError ??= asError(error)
-      const target = stream === 'stdout' ? this.stdout : this.stderr
-      target?.destroy(this.outputTransportError)
-      return
-    }
-    try {
-      if (stream === 'stdout') {
-        this.stdoutReader?.push(bytes)
-        await this.writeOutput(this.stdout, this.spec.stdio.stdout === 'inherit' ? process.stdout : undefined, bytes)
-        return
-      }
-      this.stderrReader?.push(bytes)
-      await this.writeOutput(this.stderr, this.spec.stdio.stderr === 'inherit' ? process.stderr : undefined, bytes)
-    } catch (error: unknown) {
-      const target = stream === 'stdout' ? this.stdout : this.stderr
-      target?.destroy(asError(error))
-    }
-  }
-
-  private async writeOutput(pipe: PassThrough | undefined, inherited: NodeJS.WriteStream | undefined, data: Uint8Array): Promise<void> {
-    const target = pipe ?? inherited
-    if (target === undefined || data.length === 0 || this.terminationController.signal.aborted) return
-    if (target.destroyed) throw new Error('subprocess output stream is closed')
-    if (target.write(data)) return
-    await new Promise<void>((resolve, reject) => {
-      const onDrain = (): void => { cleanup(); resolve() }
-      const onClose = (): void => { cleanup(); resolve() }
-      const onRelease = (): void => { cleanup(); resolve() }
-      const onError = (error: Error): void => { cleanup(); reject(error) }
-      const cleanup = (): void => {
-        target.removeListener('drain', onDrain)
-        target.removeListener('close', onClose)
-        target.removeListener('error', onError)
-        this.terminationController.signal.removeEventListener('abort', onRelease)
-        this.outputReleased.signal.removeEventListener('abort', onRelease)
-      }
-      target.once('drain', onDrain)
-      target.once('close', onClose)
-      target.once('error', onError)
-      this.terminationController.signal.addEventListener('abort', onRelease, { once: true })
-      this.outputReleased.signal.addEventListener('abort', onRelease, { once: true })
-      if (this.terminationController.signal.aborted || this.outputReleased.signal.aborted) onRelease()
-    })
-  }
-
-  private async waitForProcessGroupId(sandbox: Sandbox, completion: Promise<CommandResult>): Promise<number> {
-    const commandSettled = completion.then(
-      () => true,
-      () => 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) {
-        const pid = Number(value)
-        if (!/^[1-9][0-9]*$/.test(value) || !Number.isSafeInteger(pid)) {
-          throw new Error(`subprocess-e2b: remote wrapper published invalid process-group id ${JSON.stringify(value)}`)
-        }
-        // A same-UID sandbox process can rewrite this file; refuse ids whose
-        // negative form addresses every process (`kill -- -1`) or init's group.
-        if (pid <= 1) {
-          throw new Error(`subprocess-e2b: unsafe published process-group id ${pid}`)
-        }
-        return pid
-      }
-      const settled = await Promise.race([commandSettled, waitTick(this.pollMs).then(() => false)])
-      if (settled) throw new Error('subprocess-e2b: remote command exited before publishing its process-group id')
-    }
-  }
-
-  private async waitForCommand(
-    sandbox: Sandbox,
-    handle: CommandHandle,
-    completion: Promise<CommandResult>,
-  ): Promise<SubprocessOutcome> {
-    const settlement = completion.then<CommandSettlement, CommandSettlement>(
-      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) {
-        const exitCode = Number(rawStatus)
-        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 (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()
-        // Release inherited-output waits so a callback blocked on host
-        // backpressure cannot keep the disconnected SDK settlement pending.
-        this.outputReleased.abort(new Error('subprocess-e2b: output drain grace expired'))
-        await handle.disconnect()
-        return { exitCode, signal: null }
-      }
-      if (completed !== undefined) return this.commandOutcome(completed)
-      // TODO(e2b-status-watch): Replace collect/inherit control-plane polling
-      // when E2B can observe direct-command exit independently of descendant-held output.
-      completed = await Promise.race([settlement, waitTick(this.pollMs).then(() => undefined)])
-    }
-  }
-
-  private commandOutcome(settlement: CommandSettlement, publishedExitCode?: number): SubprocessOutcome {
-    if (settlement.kind === 'result') {
-      return { exitCode: publishedExitCode ?? settlement.result.exitCode, signal: null }
-    }
-    if (settlement.error instanceof CommandExitError) {
-      if (publishedExitCode !== undefined) return { exitCode: publishedExitCode, signal: null }
-      return this.terminationSignal === null
-        ? { exitCode: settlement.error.exitCode, signal: null }
-        : { exitCode: null, signal: this.terminationSignal }
-    }
-    throw settlement.error
-  }
-
-  private async rollbackPublishedFailure(error: unknown): Promise<unknown> {
-    if (this.remoteProcessGroupId === undefined || this.quiescenceProven) return error
-    this.terminate()
-    try {
-      await this.waitForExit()
-      return error
-    } catch (cleanupError: unknown) {
-      return new AggregateError(
-        [asError(error), asError(cleanupError)],
-        'subprocess-e2b: command monitoring failed and process-group rollback did not reach quiescence',
-      )
-    }
-  }
-
-  private async rollbackUnpublishedGroup(sandbox: Sandbox, handle: CommandHandle): Promise<void> {
-    // The bootstrap ends in an exec chain through the scrubbed environment and
-    // `setsid`, so E2B's command PID is the provisional group id even before the
-    // private publication file can be trusted. Kill that group before the SDK-PID
-    // fallback, then prove no group member survived before rejecting startup.
-    await this.forceKillGroup(sandbox, handle, handle.pid)
-    this.markQuiescent()
-  }
-
-  private async terminateRemote(): Promise<void> {
-    try {
-      await this.terminateRemoteInSandbox()
-    } catch (error: unknown) {
-      if (error instanceof SandboxNotFoundError) {
-        this.markQuiescent()
-        return
-      }
-      throw error
-    }
-  }
-
-  private async terminateRemoteInSandbox(): Promise<void> {
-    const handle = await this.commandState.promise
-    if (handle === undefined) {
-      this.markQuiescent()
-      return
-    }
-    if (!isValidProcessId(handle.pid) && this.remoteProcessGroupId === undefined) {
-      await handle.kill()
-      this.markQuiescent()
-      return
-    }
-    const sandbox = await this.runtime.getSandbox()
-    const processGroupId = this.remoteProcessGroupId ?? handle.pid
-    await this.terminateGroup(sandbox, handle, processGroupId)
-  }
-
-  private async terminateGroup(sandbox: Sandbox, handle: CommandHandle, processGroupId: number): Promise<void> {
-    this.terminationSignal = 'SIGTERM'
-    try {
-      await signalRemoteGroups(sandbox, this.controlEnvs, [processGroupId], 'TERM')
-      if (await this.waitForGroupExit(sandbox, processGroupId)) {
-        this.markQuiescent()
-        return
-      }
-    } catch (_gracefulTerminationFailure) {
-      // Failed TERM delivery or observation cannot prove exit; force cleanup still owns the group.
-    }
-    this.terminationSignal = 'SIGKILL'
-    await this.forceKillGroup(sandbox, handle, processGroupId)
-    this.markQuiescent()
-  }
-
-  private async forceKillGroup(sandbox: Sandbox, handle: CommandHandle, processGroupId: number): Promise<void> {
-    try {
-      await signalRemoteGroups(sandbox, this.controlEnvs, [processGroupId], 'KILL')
-    } catch (_processGroupKillFailure) {
-      // SDK kill and the final liveness probe remain independent cleanup paths.
-    }
-    try {
-      await handle.kill()
-    } catch (_sdkKillFailure) {
-      // The final liveness probe, not either transport's self-report, proves cleanup.
-    }
-    if (await this.waitForGroupExit(sandbox, processGroupId)) return
-    throw new Error(`subprocess-e2b: remote process group ${processGroupId} remained live after force termination`)
-  }
-
-  private async waitForGroupExit(sandbox: Sandbox, processGroupId: number): Promise<boolean> {
-    const deadline = Date.now() + this.spec.graceMs
-    while (await this.groupAlive(sandbox, processGroupId)) {
-      if (Date.now() >= deadline) return false
-      await waitTick(this.pollMs)
-    }
-    return true
-  }
-
-  private throwTerminationFailure(): void {
-    if (this.terminationFailure !== undefined) throw this.terminationFailure
-  }
-
-  private async groupAlive(sandbox: Sandbox, pid: number, signal?: AbortSignal): Promise<boolean> {
-    const result = await sandbox.commands.run(
-      `set -o pipefail; ps -eo pgid=,stat= | awk '$1 == ${pid} && $2 !~ /^[ZXx]/ { live=1 } END { if (live) print "live" }'`,
-      commandOpts(this.controlEnvs, signal),
-    ).catch((error: unknown) => {
-      if (signal?.aborted === true) return undefined
-      if (error instanceof SandboxNotFoundError) return { exitCode: 0, stdout: '', stderr: '' }
-      throw error
-    })
-    return result?.stdout.trim() === 'live'
-  }
-
-  private async finalizeSpills(sandbox: Sandbox): Promise<void> {
-    const removals: Promise<void>[] = []
-    const collect = (mode: SubprocessOutputMode, reader: E2BOutputReader | undefined, path: string): void => {
-      if (!hasSpill(mode)) return
-      // A spill mode is a collect mode, so construction always created its reader.
-      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; owner teardown bounds private residue.
-        }))
-      }
-    }
-    collect(this.spec.stdio.stdout, this.stdoutReader, this.paths.stdout)
-    collect(this.spec.stdio.stderr, this.stderrReader, this.paths.stderr)
-    await Promise.all(removals)
-  }
-
-  private async removeFailedState(sandbox: Sandbox): Promise<void> {
-    const failures: Error[] = []
-    for (const path of [this.paths.environment, this.stateDir]) {
-      try {
-        await sandbox.files.remove(path)
-      } catch (error: unknown) {
-        if (!(error instanceof FileNotFoundError)) failures.push(asError(error))
-      }
-    }
-    if (failures.length > 0) {
-      throw new AggregateError(failures, 'subprocess-e2b: failed to remove private command state')
-    }
-  }
-}

+ 0 - 97
packages/e2b/subprocess-e2b/src/remote.ts

@@ -1,97 +0,0 @@
-/**
- * Shared remote-control helpers for the E2B subprocess adapter: SDK option
- * shaping, poll ticks, and the one tolerant process-group signal used by both
- * the ordinary-process and terminal teardown ladders.
- */
-
-import { CommandExitError, e2bControlEnvs, SandboxNotFoundError } from '@deepseek-ai/dsh-e2b'
-import type { Sandbox } from '@deepseek-ai/dsh-e2b'
-
-/**
- * Normalize an unknown rejection into an Error.
- * @param error - Any thrown or rejected value.
- * @returns The value itself when already an Error, else a stringified wrapper.
- */
-export function asError(error: unknown): Error {
-  return error instanceof Error ? error : new Error(String(error))
-}
-
-/**
- * Shape the optional-signal SDK options object.
- * @param signal - Optional cancellation for one SDK request.
- * @returns An options fragment that omits an undefined signal.
- */
-export function signalOpts(signal: AbortSignal | undefined): { signal?: AbortSignal } {
-  return signal === undefined ? {} : { signal }
-}
-
-/**
- * Shape control-shell command options with the isolated HOME override.
- * @param envs - Explicit environment entries for the control command.
- * @param signal - Optional cancellation for the SDK request.
- * @returns Options for `sandbox.commands.run` control invocations.
- */
-export function commandOpts(
-  envs: Record<string, string>,
-  signal?: AbortSignal,
-): { envs: Record<string, string>; signal?: AbortSignal } {
-  return { envs: e2bControlEnvs(envs), ...signalOpts(signal) }
-}
-
-/**
- * Resolve after one duration.
- * @param ms - Milliseconds to wait.
- * @returns Settles after the timeout.
- */
-export function delay(ms: number): Promise<void> {
-  return new Promise(resolve => setTimeout(resolve, ms))
-}
-
-/**
- * Wait one poll interval or until the signal aborts.
- * @param pollMs - Poll cadence in milliseconds.
- * @param signal - Optional abort that ends the wait early.
- * @returns `true` after a full tick, `false` when aborted first.
- */
-export function waitTick(pollMs: number, signal?: AbortSignal): Promise<boolean> {
-  if (signal?.aborted === true) return Promise.resolve(false)
-  return new Promise<boolean>((resolve) => {
-    const timer = setTimeout(() => {
-      signal?.removeEventListener('abort', onAbort)
-      resolve(true)
-    }, pollMs)
-    const onAbort = (): void => {
-      clearTimeout(timer)
-      resolve(false)
-    }
-    signal?.addEventListener('abort', onAbort, { once: true })
-  })
-}
-
-/**
- * Signal remote process groups, tolerating the shared teardown outcomes: a
- * nonzero `kill` (groups already gone) and a disappeared sandbox. Both the
- * pgid-keyed process ladder and the sid-keyed terminal ladder deliver signals
- * through this single tolerance so they cannot drift apart.
- * @param sandbox - Live SDK handle.
- * @param envs - Control-shell environment entries.
- * @param groups - Positive process-group ids to signal.
- * @param signal - `TERM` or `KILL`.
- */
-export async function signalRemoteGroups(
-  sandbox: Sandbox,
-  envs: Record<string, string>,
-  groups: readonly number[],
-  signal: 'TERM' | 'KILL',
-): Promise<void> {
-  // TODO(e2b-pgid-identity): Prefer an atomic identity-bound group signal if E2B adds one;
-  // a userspace identity precheck cannot close the numeric-PGID reuse race.
-  try {
-    await sandbox.commands.run(
-      `kill -${signal} -- ${groups.map(group => `-${group}`).join(' ')}`,
-      commandOpts(envs),
-    )
-  } catch (error: unknown) {
-    if (!(error instanceof CommandExitError) && !(error instanceof SandboxNotFoundError)) throw error
-  }
-}

+ 0 - 567
packages/e2b/subprocess-e2b/src/terminal.ts

@@ -1,567 +0,0 @@
-/** E2B PTY allocation and process-session ownership for the subprocess seam. */
-
-import { Buffer } from 'node:buffer'
-import { randomUUID } from 'node:crypto'
-import { PassThrough } from 'node:stream'
-import { posix } from 'node:path'
-import {
-  CommandExitError,
-  e2bControlEnvs,
-  FileNotFoundError,
-  SandboxNotFoundError,
-  quoteE2BShellArg,
-} from '@deepseek-ai/dsh-e2b'
-import type { CommandHandle, CommandResult, Sandbox } from '@deepseek-ai/dsh-e2b'
-import type {
-  SubprocessOutcome,
-  SubprocessTerminalForeground,
-  SubprocessTerminalHandle,
-  SubprocessTerminalSignal,
-  SubprocessTerminalSpawnSpec,
-} from '@deepseek-ai/dsh-subprocess'
-import type E2BRuntime from '@deepseek-ai/dsh-e2b'
-import {
-  bootstrapEnvironment,
-  readRemoteEnvironment,
-  serializeRemoteEnvironment,
-} from './environment.ts'
-import { asError, commandOpts, delay, signalOpts, signalRemoteGroups } from './remote.ts'
-
-const TERMINAL_RUNNER_SOURCE = [
-  '#!/bin/bash',
-  'set -euo pipefail',
-  'dsh_state=$1',
-  'mapfile -d \'\' -t dsh_env < "$dsh_state/environment"',
-  'mapfile -d \'\' -t dsh_argv < "$dsh_state/argv"',
-  'dsh_output_marker=$(<"$dsh_state/output-marker")',
-  'rm -f -- "$dsh_state/environment" "$dsh_state/argv" "$dsh_state/output-marker" "$dsh_state/runner.bash"',
-  'if (( ${#dsh_argv[@]} == 0 )); then',
-  "  printf 'terminal runner received empty argv\\n' >&2",
-  '  exit 125',
-  'fi',
-  'printf \'%s\' "$dsh_output_marker"',
-  'exec env -i -- "${dsh_env[@]}" "${dsh_argv[@]}"',
-  '',
-].join('\n')
-
-interface TerminalPaths {
-  runner: string
-  environment: string
-  argv: string
-  outputMarker: string
-}
-
-class BootstrapOutputFilter {
-  readonly ready: Promise<void>
-
-  private readonly readyState = Promise.withResolvers<void>()
-  private pending = Buffer.alloc(0)
-  private published = false
-
-  constructor(
-    private readonly marker: Buffer,
-    private readonly output: PassThrough,
-  ) {
-    this.ready = this.readyState.promise
-  }
-
-  push(data: Uint8Array): void {
-    if (this.published) {
-      this.write(data)
-      return
-    }
-    const combined = Buffer.concat([this.pending, Buffer.from(data)])
-    const markerOffset = combined.indexOf(this.marker)
-    if (markerOffset < 0) {
-      const retained = Math.min(combined.length, this.marker.length - 1)
-      this.pending = Buffer.from(combined.subarray(combined.length - retained))
-      return
-    }
-    this.published = true
-    this.pending = Buffer.alloc(0)
-    this.readyState.resolve()
-    this.write(combined.subarray(markerOffset + this.marker.length))
-  }
-
-  private write(data: Uint8Array): void {
-    if (data.length > 0 && !this.output.destroyed) this.output.write(data)
-  }
-}
-
-async function waitForBootstrapOutput(
-  ready: Promise<void>,
-  completion: Promise<CommandResult>,
-  signal?: AbortSignal,
-): Promise<void> {
-  signal?.throwIfAborted()
-  await new Promise<void>((resolve, reject) => {
-    let settled = false
-    let removeAbort: (() => void) | undefined
-    const finish = (complete: () => void): void => {
-      if (settled) return
-      settled = true
-      removeAbort?.()
-      complete()
-    }
-    const onExit = (): void => {
-      finish(() => { reject(new Error('subprocess-e2b: terminal exited before publishing its output boundary')) })
-    }
-    if (signal !== undefined) {
-      const onAbort = (): void => {
-        finish(() => { reject(asError(signal.reason)) })
-      }
-      signal.addEventListener('abort', onAbort, { once: true })
-      removeAbort = () => { signal.removeEventListener('abort', onAbort) }
-    }
-    void ready.then(() => { finish(resolve) })
-    void completion.then(onExit, onExit)
-  })
-}
-
-function parsePositiveId(value: string, message: string): number {
-  const raw = value.trim()
-  const id = Number(raw)
-  if (!/^[1-9][0-9]*$/.test(raw) || !Number.isSafeInteger(id)) throw new Error(message)
-  return id
-}
-
-function serializeValues(values: readonly string[], kind: string): string {
-  for (const value of values) {
-    if (value.includes('\0')) throw new Error(`subprocess-e2b: terminal ${kind} must not contain NUL bytes`)
-  }
-  return values.map(value => `${value}\0`).join('')
-}
-
-async function terminalSessionId(
-  sandbox: Sandbox,
-  pid: number,
-  envs: Record<string, string>,
-  signal?: AbortSignal,
-): Promise<number> {
-  const result = await sandbox.commands.run(`ps -o sid= -p ${pid}`, commandOpts(envs, signal))
-  signal?.throwIfAborted()
-  return parsePositiveId(result.stdout, `subprocess-e2b: cannot resolve process session for terminal ${pid}`)
-}
-
-async function sessionProcessGroups(
-  sandbox: Sandbox,
-  sessionId: number,
-  envs: Record<string, string>,
-): Promise<number[]> {
-  let result: CommandResult
-  try {
-    result = await sandbox.commands.run(
-      `set -o pipefail; ps -eo sid=,pgid=,stat= | awk '$1 == ${sessionId} && $3 !~ /^[ZXx]/ { print $2 }'`,
-      commandOpts(envs),
-    )
-  } catch (error: unknown) {
-    if (error instanceof SandboxNotFoundError) return []
-    throw error
-  }
-  const groups = new Set<number>()
-  for (const raw of result.stdout.trim().split(/\s+/)) {
-    if (raw.length === 0) continue
-    const group = parsePositiveId(
-      raw,
-      `subprocess-e2b: invalid process group ${JSON.stringify(raw)} in terminal session ${sessionId}`,
-    )
-    if (group <= 1) {
-      throw new Error(`subprocess-e2b: unsafe process group ${group} in terminal session ${sessionId}`)
-    }
-    groups.add(group)
-  }
-  return [...groups]
-}
-
-async function awaitSessionEmpty(
-  sandbox: Sandbox,
-  sessionId: number,
-  envs: Record<string, string>,
-  graceMs: number,
-  pollMs: number,
-  kill = false,
-): Promise<number[]> {
-  const deadline = Date.now() + graceMs
-  for (;;) {
-    const groups = await sessionProcessGroups(sandbox, sessionId, envs)
-    if (groups.length === 0) return groups
-    if (kill) {
-      await signalRemoteGroups(sandbox, envs, groups, 'KILL')
-      if (Date.now() >= deadline) return await sessionProcessGroups(sandbox, sessionId, envs)
-    } else if (Date.now() >= deadline) {
-      return groups
-    }
-    await delay(Math.min(pollMs, Math.max(1, deadline - Date.now())))
-  }
-}
-
-async function rollbackUnpublishedTerminal(
-  sandbox: Sandbox,
-  handle: CommandHandle,
-  completion: Promise<CommandResult>,
-  envs: Record<string, string>,
-  graceMs: number,
-  pollMs: number,
-): Promise<void> {
-  let topLevelExited = false
-  void completion.then(
-    () => { topLevelExited = true },
-    () => { topLevelExited = true },
-  )
-  const validPid = Number.isSafeInteger(handle.pid) && handle.pid > 1
-  const attemptFailures: Error[] = []
-  let sessionId: number | undefined
-  if (validPid) {
-    sessionId = handle.pid
-    try {
-      sessionId = await terminalSessionId(sandbox, handle.pid, envs)
-    } catch (_sessionLookupFailure) {
-      // E2B's PTY leader is also the provisional POSIX session leader, so its
-      // PID remains usable after the setup lookup itself fails or is canceled.
-    }
-    try {
-      let groups = await sessionProcessGroups(sandbox, sessionId, envs)
-      if (groups.length > 0) {
-        await signalRemoteGroups(sandbox, envs, groups, 'TERM')
-        groups = await awaitSessionEmpty(sandbox, sessionId, envs, graceMs, pollMs)
-      }
-      if (groups.length > 0) {
-        await awaitSessionEmpty(sandbox, sessionId, envs, graceMs, pollMs, true)
-      }
-    } catch (error: unknown) {
-      attemptFailures.push(asError(error))
-    }
-  }
-  // Completion can settle while any awaited provider cleanup above is running.
-  // oxlint-disable-next-line typescript/no-unnecessary-condition -- Provider cleanup yields to completion.
-  if (!topLevelExited) {
-    try {
-      await handle.kill()
-    } catch (error: unknown) {
-      if (error instanceof SandboxNotFoundError) return
-      attemptFailures.push(asError(error))
-    }
-    await Promise.race([completion.catch(() => undefined), delay(graceMs)])
-  }
-  const proofFailures: Error[] = []
-  if (sessionId !== undefined) {
-    try {
-      const groups = await awaitSessionEmpty(sandbox, sessionId, envs, graceMs, pollMs, true)
-      if (groups.length > 0) {
-        proofFailures.push(new Error(
-          `subprocess-e2b: terminal setup rollback failed; surviving process groups: ${groups.join(', ')}`,
-        ))
-      }
-    } catch (error: unknown) {
-      proofFailures.push(asError(error))
-    }
-  }
-  // The bounded completion race above updates this callback-owned state.
-  // 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}`))
-  }
-  if (proofFailures.length > 0) {
-    throw new AggregateError(
-      [...attemptFailures, ...proofFailures],
-      'subprocess-e2b: terminal setup rollback did not reach quiescence',
-    )
-  }
-  try {
-    await handle.disconnect()
-  } catch (error: unknown) {
-    if (!(error instanceof SandboxNotFoundError)) throw error
-  }
-}
-
-/** One E2B PTY and all process groups in its remote process session. */
-export class E2BTerminalHandle implements SubprocessTerminalHandle {
-  readonly pid: number
-  readonly done: Promise<SubprocessOutcome>
-
-  private topLevelExited = false
-  private cleanup: Promise<void> | undefined
-  private readonly operationController = new AbortController()
-  private readonly operations = new Set<Promise<unknown>>()
-  private terminationSignal: NodeJS.Signals | null = null
-
-  constructor(
-    private readonly sandbox: Sandbox,
-    private readonly handle: CommandHandle,
-    readonly output: PassThrough,
-    private readonly completion: Promise<CommandResult>,
-    private readonly sessionId: number,
-    private readonly controlEnvs: Record<string, string>,
-    private readonly stateDir: string,
-    private readonly graceMs: number,
-    private readonly pollMs: number,
-  ) {
-    this.pid = handle.pid
-    this.done = this.waitForCommand()
-  }
-
-  // TODO(e2b-pgid-identity): Replace retained numeric PTY/session ids when E2B
-  // exposes identity-bound input, foreground-signal, and cleanup operations.
-  /** @inheritdoc */
-  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 */
-  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.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, signal),
-      )
-      return {
-        processGroupId: parsePositiveId(
-          result.stdout,
-          `subprocess-e2b: cannot resolve foreground process group for terminal ${this.pid}`,
-        ),
-        // E2B exposes process-table commands but not the /proc memory access
-        // needed to prove a specific syscall is waiting on fd 0.
-        inputWaiting: false,
-      }
-    } catch (error: unknown) {
-      if (error instanceof CommandExitError && (error.exitCode === 1 || this.topLevelExited)) return undefined
-      throw error
-    }
-  }
-
-  private trackOperation<T>(operation: (signal: AbortSignal) => Promise<T>): Promise<T> {
-    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(
-      () => { this.operations.delete(pending) },
-      () => { this.operations.delete(pending) },
-    )
-    return pending
-  }
-
-  private async closeAfterOperations(): Promise<void> {
-    await Promise.allSettled(this.operations)
-    await this.closeOnce()
-  }
-
-  private async waitForCommand(): Promise<SubprocessOutcome> {
-    try {
-      const result = await this.completion
-      return { exitCode: result.exitCode, signal: null }
-    } catch (error: unknown) {
-      if (error instanceof CommandExitError) {
-        return this.terminationSignal === null
-          ? { exitCode: error.exitCode, signal: null }
-          : { exitCode: null, signal: this.terminationSignal }
-      }
-      this.output.destroy(error instanceof Error ? error : new Error(String(error)))
-      throw error
-    } finally {
-      this.topLevelExited = true
-      if (!this.output.destroyed) this.output.end()
-    }
-  }
-
-  private async closeOnce(): Promise<void> {
-    let groups = await sessionProcessGroups(this.sandbox, this.sessionId, this.controlEnvs)
-    if (groups.length > 0) {
-      this.terminationSignal = 'SIGTERM'
-      await signalRemoteGroups(this.sandbox, this.controlEnvs, groups, 'TERM')
-      groups = await awaitSessionEmpty(this.sandbox, this.sessionId, this.controlEnvs, this.graceMs, this.pollMs)
-    }
-    if (groups.length === 0 && !this.topLevelExited) {
-      await Promise.race([this.done.catch(() => undefined), delay(this.graceMs)])
-    }
-    if (groups.length > 0 || !this.topLevelExited) {
-      this.terminationSignal = 'SIGKILL'
-      if (!this.topLevelExited) {
-        try {
-          await this.handle.kill()
-        } catch (error: unknown) {
-          if (error instanceof SandboxNotFoundError) return
-          throw error
-        }
-      }
-      groups = await awaitSessionEmpty(this.sandbox, this.sessionId, this.controlEnvs, this.graceMs, this.pollMs, true)
-      if (!this.topLevelExited) await Promise.race([this.done.catch(() => undefined), delay(this.graceMs)])
-    }
-    if (groups.length > 0) {
-      throw new Error(`subprocess-e2b: terminal cleanup failed; surviving process groups: ${groups.join(', ')}`)
-    }
-    if (!this.topLevelExited) {
-      throw new Error(`subprocess-e2b: terminal cleanup failed; surviving pid: ${this.pid}`)
-    }
-    try {
-      await this.handle.disconnect()
-    } catch (error: unknown) {
-      if (!(error instanceof SandboxNotFoundError)) throw error
-    }
-    try {
-      await this.sandbox.files.remove(this.stateDir)
-    } catch (_adapterPrivateStateRemovalFailure) {
-      // The terminal is quiescent; owner teardown bounds private residue.
-    }
-  }
-}
-
-/**
- * Allocate an E2B PTY, replace its bootstrap shell with the requested argv,
- * and return only after the private runner has published readiness.
- * @param runtime - Shared E2B sandbox owner.
- * @param spec - Fully specified terminal-process request.
- * @param stateDir - Private remote directory for one startup transaction.
- * @param pollMs - Remote session liveness poll cadence.
- * @returns The live subprocess terminal handle.
- */
-export async function spawnE2BTerminal(
-  runtime: E2BRuntime,
-  spec: SubprocessTerminalSpawnSpec,
-  stateDir: string,
-  pollMs: number,
-): Promise<E2BTerminalHandle> {
-  const sandbox = await runtime.getSandbox()
-  spec.signal?.throwIfAborted()
-  const paths: TerminalPaths = {
-    runner: posix.join(stateDir, 'runner.bash'),
-    environment: posix.join(stateDir, 'environment'),
-    argv: posix.join(stateDir, 'argv'),
-    outputMarker: posix.join(stateDir, 'output-marker'),
-  }
-  const outputMarker = Buffer.from(`dsh-e2b-bootstrap:${randomUUID()}`)
-  const output = new PassThrough()
-  const outputFilter = new BootstrapOutputFilter(outputMarker, output)
-  let handle: CommandHandle | undefined
-  let completion: Promise<CommandResult> | undefined
-  let stateDirectoryCreated = false
-  let controlEnvs: Record<string, string> = {}
-  try {
-    const ambient = await readRemoteEnvironment(sandbox, spec.signal)
-    controlEnvs = bootstrapEnvironment(ambient)
-    const environment = serializeRemoteEnvironment(ambient, spec.env)
-    const argv = serializeValues(spec.argv, 'argv')
-    stateDirectoryCreated = true
-    await sandbox.files.makeDir(stateDir, signalOpts(spec.signal))
-    await sandbox.commands.run(
-      `chmod 700 -- ${quoteE2BShellArg(stateDir)}`,
-      commandOpts(controlEnvs, spec.signal),
-    )
-    await sandbox.files.write([
-      { path: paths.runner, data: TERMINAL_RUNNER_SOURCE },
-      { path: paths.environment, data: environment },
-      { path: paths.argv, data: argv },
-      { path: paths.outputMarker, data: outputMarker.toString('utf8') },
-    ], signalOpts(spec.signal))
-    await sandbox.commands.run(
-      `chmod 600 -- ${quoteE2BShellArg(paths.runner)} ${quoteE2BShellArg(paths.environment)} ${quoteE2BShellArg(paths.argv)} ${quoteE2BShellArg(paths.outputMarker)}`,
-      commandOpts(controlEnvs, spec.signal),
-    )
-    handle = await sandbox.pty.create({
-      rows: spec.rows,
-      cols: spec.cols,
-      cwd: spec.cwd,
-      envs: e2bControlEnvs(controlEnvs),
-      timeoutMs: 0,
-      onData: (data) => { outputFilter.push(data) },
-    })
-    completion = handle.wait()
-    void completion.catch(() => {})
-    spec.signal?.throwIfAborted()
-    if (!Number.isSafeInteger(handle.pid) || handle.pid <= 0) {
-      throw new Error(`subprocess-e2b: E2B returned invalid terminal pid ${handle.pid}`)
-    }
-    const command = `exec /bin/bash ${quoteE2BShellArg(paths.runner)} ${quoteE2BShellArg(stateDir)}\r`
-    await sandbox.pty.sendInput(handle.pid, Buffer.from(command), signalOpts(spec.signal))
-    await waitForBootstrapOutput(outputFilter.ready, completion, spec.signal)
-    const sessionId = await terminalSessionId(sandbox, handle.pid, controlEnvs, spec.signal)
-    return new E2BTerminalHandle(
-      sandbox,
-      handle,
-      output,
-      completion,
-      sessionId,
-      controlEnvs,
-      stateDir,
-      spec.graceMs,
-      pollMs,
-    )
-  } catch (error: unknown) {
-    output.destroy()
-    let terminalQuiescent = handle === undefined
-    let stateRemoved = !stateDirectoryCreated
-    const cleanup = async (): Promise<void> => {
-      const failures: Error[] = []
-      if (!terminalQuiescent && handle !== undefined) {
-        try {
-          if (completion === undefined) await handle.kill()
-          else await rollbackUnpublishedTerminal(sandbox, handle, completion, controlEnvs, spec.graceMs, pollMs)
-          terminalQuiescent = true
-        } catch (cleanupError: unknown) {
-          if (cleanupError instanceof SandboxNotFoundError) terminalQuiescent = true
-          else failures.push(asError(cleanupError))
-        }
-      }
-      if (!stateRemoved) {
-        try {
-          await sandbox.files.remove(stateDir)
-          stateRemoved = true
-        } catch (stateError: unknown) {
-          if (stateError instanceof FileNotFoundError || stateError instanceof SandboxNotFoundError) stateRemoved = true
-          else failures.push(asError(stateError))
-        }
-      }
-      if (failures.length > 0) {
-        throw new AggregateError(failures, 'subprocess-e2b: terminal setup cleanup did not complete')
-      }
-    }
-    try {
-      await cleanup()
-    } catch (cleanupError: unknown) {
-      // TODO(e2b-terminal-setup-rollback): Retain retry state only if a real
-      // double failure must be recovered before sandbox disposal or timeout.
-      throw new AggregateError([asError(error), asError(cleanupError)], asError(error).message)
-    }
-    throw error
-  }
-}

+ 0 - 1810
packages/e2b/subprocess-e2b/tests/subprocess.spec.ts

@@ -1,1810 +0,0 @@
-import { once } from 'node:events'
-import { Context } from '@deepseek-ai/cordis'
-import {
-  CommandExitError,
-  FileNotFoundError,
-  SandboxNotFoundError,
-  type CommandHandle,
-  type CommandResult,
-  type Sandbox,
-} from '@deepseek-ai/dsh-e2b'
-import type E2BRuntime from '@deepseek-ai/dsh-e2b'
-import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
-import E2BSubprocessRuntime from '@deepseek-ai/dsh-subprocess-e2b'
-import { E2BBase64Decoder, E2B_OUTPUT_COMPLETE_FRAME, E2BOutputReader } from '../src/output.ts'
-import { E2BSubprocessHandle } from '../src/process.ts'
-import { describe, expect, it, vi } from 'vitest'
-
-function commandError(exitCode: number): CommandExitError {
-  return new CommandExitError({ exitCode, stdout: '', stderr: '', error: `exit ${exitCode}` })
-}
-
-interface StartOptions {
-  background: true
-  cwd: string
-  stdin: boolean
-  timeoutMs: number
-  signal?: AbortSignal
-  envs?: Record<string, string>
-  onStdout?: (data: string) => void | Promise<void>
-  onStderr?: (data: string) => void | Promise<void>
-}
-
-class FakeCommandHandle {
-  pid = 4242
-  readonly sent: Array<string | Uint8Array> = []
-  closes = 0
-  kills = 0
-  disconnects = 0
-  killError: unknown
-  killResult = true
-  disconnectError: unknown
-  private readonly result = Promise.withResolvers<CommandResult>()
-  private settled = false
-
-  constructor(private readonly onKill: () => void = () => {}) {}
-
-  wait(): Promise<CommandResult> {
-    return this.result.promise
-  }
-
-  async sendStdin(data: string | Uint8Array): Promise<void> {
-    this.sent.push(data)
-  }
-
-  async closeStdin(): Promise<void> {
-    this.closes += 1
-  }
-
-  async kill(): Promise<boolean> {
-    this.kills += 1
-    if (this.killError !== undefined) throw this.killError
-    this.onKill()
-    return this.killResult
-  }
-
-  async disconnect(): Promise<void> {
-    this.disconnects += 1
-    if (this.disconnectError !== undefined) throw this.disconnectError
-  }
-
-  succeed(exitCode = 0): void {
-    if (this.settled) return
-    this.settled = true
-    this.result.resolve({ exitCode, stdout: '', stderr: '' })
-  }
-
-  fail(exitCode: number): void {
-    if (this.settled) return
-    this.settled = true
-    this.result.reject(commandError(exitCode))
-  }
-
-  crash(error: unknown): void {
-    if (this.settled) return
-    this.settled = true
-    this.result.reject(error)
-  }
-}
-
-class FakeSandbox {
-  readonly handle: FakeCommandHandle
-  readonly commandsSeen: string[] = []
-  readonly writtenFiles: string[][] = []
-  readonly writtenFileData = new Map<string, string>()
-  readonly removed: string[] = []
-  readonly directories: string[] = []
-  startOptions: StartOptions | undefined
-  backgroundError: unknown
-  envError: unknown
-  statusError: unknown
-  nextRemoveError: unknown
-  probeError: unknown
-  signalError: unknown
-  readonly signalErrors: unknown[] = []
-  trapsTerm = false
-  delaysKill = false
-  delaysKillCompletion = false
-  sdkKillStops = true
-  alive = true
-  zombieOnly = false
-  ambient = 'PATH=/ambient/bin\0KEEP=safe\0UNICODE=你好\0NPM_TOKEN=secret\0DSH_STALE=old\0BROKEN\0=bad\0'
-  environmentHome = '/home/user'
-  environmentWire: string | undefined
-  environmentRequest: ((signal: AbortSignal | undefined) => Promise<void>) | undefined
-  processGroupId = '4242\n'
-  exitStatus = ''
-  statusReads = 0
-  readonly processGroupReads: string[] = []
-  afterStatusRead: (() => void) | undefined
-  beforeProbe: (() => void) | undefined
-  afterProbe: (() => void) | undefined
-  private startGate: Promise<void> | undefined
-  private openStart: (() => void) | undefined
-  private processGroupReadGate: Promise<void> | undefined
-  private openProcessGroupRead: (() => void) | undefined
-  private signalGate: Promise<void> | undefined
-  private openSignal: (() => void) | undefined
-
-  constructor() {
-    this.handle = new FakeCommandHandle(() => {
-      if (this.sdkKillStops) {
-        this.alive = false
-        this.handle.fail(137)
-      }
-    })
-  }
-
-  deferStart(): void {
-    const gate = Promise.withResolvers<undefined>()
-    this.startGate = gate.promise
-    this.openStart = () => { gate.resolve(undefined) }
-  }
-
-  releaseStart(): void {
-    this.openStart?.()
-  }
-
-  deferProcessGroupRead(): void {
-    const gate = Promise.withResolvers<undefined>()
-    this.processGroupReadGate = gate.promise
-    this.openProcessGroupRead = () => { gate.resolve(undefined) }
-  }
-
-  releaseProcessGroupRead(): void {
-    this.openProcessGroupRead?.()
-  }
-
-  deferSignals(): void {
-    const gate = Promise.withResolvers<undefined>()
-    this.signalGate = gate.promise
-    this.openSignal = () => { gate.resolve(undefined) }
-  }
-
-  releaseSignals(): void {
-    this.openSignal?.()
-  }
-
-  finish(exitCode = 0): void {
-    this.alive = false
-    void this.completeOutput().then(
-      () => {
-        if (exitCode === 0) this.handle.succeed(0)
-        else this.handle.fail(exitCode)
-      },
-      (error: unknown) => { this.handle.crash(error) },
-    )
-  }
-
-  async completeOutput(): Promise<void> {
-    await Promise.all([
-      this.stdoutWire(`${E2B_OUTPUT_COMPLETE_FRAME}\n`),
-      this.stderrWire(`${E2B_OUTPUT_COMPLETE_FRAME}\n`),
-    ])
-  }
-
-  async stdout(data: string): Promise<void> {
-    await this.stdoutWire(data.length === 0 ? '' : `${Buffer.from(data).toString('base64')}\n`)
-  }
-
-  async stderr(data: string): Promise<void> {
-    await this.stderrWire(data.length === 0 ? '' : `${Buffer.from(data).toString('base64')}\n`)
-  }
-
-  async stdoutWire(data: string): Promise<void> {
-    await this.startOptions?.onStdout?.(data)
-  }
-
-  async stderrWire(data: string): Promise<void> {
-    await this.startOptions?.onStderr?.(data)
-  }
-
-  readonly sandbox = {
-    sandboxId: 'fake',
-    files: {
-      makeDir: async (path: string): Promise<boolean> => {
-        this.directories.push(path)
-        return true
-      },
-      write: async (files: Array<{ path: string; data: string }>): Promise<object[]> => {
-        this.writtenFiles.push(files.map(file => file.path))
-        for (const file of files) this.writtenFileData.set(file.path, file.data)
-        return files.map(() => ({}))
-      },
-      read: async (path: string): Promise<string> => {
-        if (!path.endsWith('/exit-code')) {
-          await this.processGroupReadGate
-          return this.processGroupReads.shift() ?? this.processGroupId
-        }
-        if (this.statusError !== undefined) {
-          const error = this.statusError
-          this.statusError = undefined
-          throw error
-        }
-        this.statusReads += 1
-        this.afterStatusRead?.()
-        return this.exitStatus
-      },
-      remove: async (path: string): Promise<void> => {
-        this.removed.push(path)
-        if (this.nextRemoveError !== undefined) {
-          const error = this.nextRemoveError
-          this.nextRemoveError = undefined
-          throw error
-        }
-      },
-    },
-    commands: {
-      run: async (command: string, options?: StartOptions | { signal?: AbortSignal }): Promise<CommandHandle | CommandResult> => {
-        this.commandsSeen.push(command)
-        if (command.includes('env -0 | base64')) {
-          await this.environmentRequest?.(options?.signal)
-          if (this.envError !== undefined) throw this.envError
-          return {
-            exitCode: 0,
-            stdout: this.environmentWire ?? [this.environmentHome, this.ambient]
-              .map(value => Buffer.from(value).toString('base64'))
-              .join('\n'),
-            stderr: '',
-          }
-        }
-        if (command.startsWith('set -o pipefail; ps -eo pgid=,stat=')) {
-          this.beforeProbe?.()
-          if (options?.signal?.aborted === true) throw new DOMException('aborted', 'AbortError')
-          if (this.probeError !== undefined) {
-            const error = this.probeError
-            this.probeError = undefined
-            throw error
-          }
-          const stdout = this.alive && !this.zombieOnly ? 'live\n' : ''
-          this.afterProbe?.()
-          return { exitCode: 0, stdout, stderr: '' }
-        }
-        if (command.startsWith('kill -TERM ')) {
-          await this.signalGate
-          const error = this.signalErrors.shift() ?? this.signalError
-          if (error !== undefined) {
-            if (this.signalErrors.length === 0) this.signalError = undefined
-            throw error
-          }
-          if (!this.trapsTerm) {
-            this.alive = false
-            this.handle.fail(143)
-          }
-          return { exitCode: 0, stdout: '', stderr: '' }
-        }
-        if (command.startsWith('kill -KILL ')) {
-          await this.signalGate
-          const error = this.signalErrors.shift() ?? this.signalError
-          if (error !== undefined) {
-            if (this.signalErrors.length === 0) this.signalError = undefined
-            throw error
-          }
-          if (!this.delaysKill) this.alive = false
-          if (!this.delaysKillCompletion) this.handle.fail(137)
-          return { exitCode: 0, stdout: '', stderr: '' }
-        }
-        if ((options as StartOptions | undefined)?.background === true) {
-          this.startOptions = options as StartOptions
-          await this.startGate
-          if (this.backgroundError !== undefined) throw this.backgroundError
-          return this.handle as unknown as CommandHandle
-        }
-        return { exitCode: 0, stdout: '', stderr: '' }
-      },
-    },
-  } as unknown as Sandbox
-}
-
-function spec(overrides: Partial<SubprocessSpawnSpec> = {}): SubprocessSpawnSpec {
-  return {
-    argv: ['bash', '-c', 'printf ok'],
-    cwd: '/workspace',
-    stdio: {
-      stdin: 'ignore',
-      stdout: { maxBytes: 4, spill: { maxBytes: 16 } },
-      stderr: { maxBytes: 4 },
-    },
-    graceMs: 5,
-    ...overrides,
-  }
-}
-
-function runtime(fake: FakeSandbox, getSandbox: () => Promise<Sandbox> = async () => fake.sandbox): E2BRuntime {
-  return {
-    cwd: '/workspace',
-    runtimeRoot: '/workspace/.dsh-e2b',
-    getSandbox,
-  } as unknown as E2BRuntime
-}
-
-async function flush(): Promise<void> {
-  await new Promise(resolve => setTimeout(resolve, 0))
-}
-
-/** Construct the handle under test with the config default the service would pass. */
-function testHandle(
-  runtime: ConstructorParameters<typeof E2BSubprocessHandle>[0],
-  spec: ConstructorParameters<typeof E2BSubprocessHandle>[1],
-  stateDir: string,
-  pollMs = 20,
-): E2BSubprocessHandle {
-  return new E2BSubprocessHandle(runtime, spec, stateDir, pollMs)
-}
-
-describe('E2BOutputReader', () => {
-  it('decodes base64 across arbitrary callback boundaries and rejects malformed framing', () => {
-    const decoder = new E2BBase64Decoder()
-    expect(decoder.push('')).toEqual(Buffer.alloc(0))
-    expect(decoder.push('5')).toEqual(Buffer.alloc(0))
-    expect(decoder.push('L2')).toEqual(Buffer.alloc(0))
-    expect(decoder.push('g\n').toString()).toBe('你')
-    expect(decoder.push('YQ==\nYg==\n').toString()).toBe('ab')
-    expect(decoder.push(`${Buffer.from([0, 255]).toString('base64')}\n`)).toEqual(Buffer.from([0, 255]))
-    expect(decoder.push(`${E2B_OUTPUT_COMPLETE_FRAME}\n`)).toEqual(Buffer.alloc(0))
-    decoder.finish()
-
-    expect(() => new E2BBase64Decoder().push('%\n')).toThrow('invalid base64')
-    expect(() => new E2BBase64Decoder().push('AB==\n')).toThrow('invalid base64')
-    expect(() => decoder.push(`${E2B_OUTPUT_COMPLETE_FRAME}\n`)).toThrow('duplicate output transport completion')
-    expect(() => decoder.push('YQ==\n')).toThrow('continued after completion')
-    const truncated = new E2BBase64Decoder()
-    truncated.push('YQ')
-    expect(() => { truncated.finish() }).toThrow('truncated base64')
-    expect(() => { new E2BBase64Decoder().finish() }).toThrow('incomplete output transport')
-    const interrupted = new E2BBase64Decoder()
-    interrupted.push('YQ')
-    expect(() => { interrupted.finish(false) }).not.toThrow()
-  })
-
-  it('keeps a byte-exact tail with independent whole-stream cursors', () => {
-    const reader = new E2BOutputReader(4, 10, '/remote/spill')
-    reader.push(Buffer.alloc(0))
-    reader.push(Buffer.from('ab'))
-    reader.push(Buffer.from('cdef'))
-    expect(reader.size).toBe(6)
-    expect(reader.readFrom(0)).toEqual({ text: 'cdef', nextOffset: 6, lossy: true, spillPath: '/remote/spill' })
-    expect(reader.readFrom(2)).toEqual({ text: 'cdef', nextOffset: 6, lossy: false })
-    expect(reader.readFrom(5)).toEqual({ text: 'f', nextOffset: 6, lossy: false })
-    expect(reader.readFrom(99)).toEqual({ text: '', nextOffset: 6, lossy: false })
-    reader.invalidateSpill()
-    expect(reader.readFrom(0)).toEqual({ text: 'cdef', nextOffset: 6, lossy: true })
-  })
-
-  it('drops whole head chunks and withholds absent or over-cap spills', () => {
-    const withoutSpill = new E2BOutputReader(2, undefined, '/unused')
-    withoutSpill.push(Buffer.from('ab'))
-    withoutSpill.push(Buffer.from('cd'))
-    expect(withoutSpill.readFrom(0)).toEqual({ text: 'cd', nextOffset: 4, lossy: true })
-    const overCap = new E2BOutputReader(2, 3, '/too-small')
-    overCap.push(Buffer.from('abcd'))
-    expect(overCap.readFrom(0)).toEqual({ text: 'cd', nextOffset: 4, lossy: true })
-  })
-})
-
-describe('E2BSubprocessHandle', () => {
-  it('starts asynchronously, keeps secrets out of the command, and supports deferred piped stdin/output', async () => {
-    const fake = new FakeSandbox()
-    fake.processGroupId = '4343\n'
-    fake.deferStart()
-    const handle = testHandle(runtime(fake), spec({
-      argv: ['tool', 'argument with spaces'],
-      stdio: { stdin: 'pipe', stdout: 'pipe', stderr: { maxBytes: 8, spill: { maxBytes: 32 } } },
-      env: {
-        PATH: '/bin',
-        'FOO-BAR': 'hyphen-value',
-        '--split-string': 'literal-value',
-        DEEPSEEK_API_KEY: 'explicit-secret',
-        DSH_MODE: 'test',
-        // The seam's tombstone: an explicit undefined removes the ambient entry.
-        KEEP: undefined,
-      },
-    }), '/workspace/.dsh-e2b/processes/one')
-    handle.stdin!.write('hello')
-    handle.stdin!.end()
-    fake.releaseStart()
-    await flush()
-    expect(fake.handle.sent.map(value => String(value))).toEqual(['hello'])
-    expect(fake.handle.closes).toBe(1)
-    const controlEnvs = fake.startOptions?.envs
-    expect(controlEnvs?.HOME).toMatch(/^\/\.dsh-e2b-control-/)
-    expect(controlEnvs).toEqual({
-      TERM: 'dumb',
-      NPM_TOKEN: '',
-      DSH_STALE: '',
-      HOME: controlEnvs?.HOME,
-    })
-    const command = fake.commandsSeen.find(value => value.includes('exec "$dsh_e2b_env_bin" -i'))!
-    expect(command).toContain('"$dsh_e2b_setsid" --wait -- "$dsh_e2b_bash" -c')
-    expect(command).not.toContain('DEEPSEEK_API_KEY')
-    expect(command).not.toContain('DSH_MODE')
-    expect(command).not.toContain('FOO-BAR')
-    expect(command).not.toContain('explicit-secret')
-    expect(command).not.toContain('hyphen-value')
-    expect(command).not.toContain('${!dsh_e2b_name}')
-    const environmentProbe = fake.commandsSeen.find(value => value.includes('env -0 | base64'))
-    expect(environmentProbe).toContain('getent passwd "$(id -u)"')
-    expect(environmentProbe).toContain('test -n "$dsh_e2b_home" -a -d "$dsh_e2b_home"')
-    expect(environmentProbe).not.toContain('"$PWD"')
-    expect(command).toContain('mapfile -d')
-    expect(command).toContain('dsh_e2b_node="$(command -v node)"')
-    expect(command).toContain('"$dsh_e2b_env_bin" -i "$dsh_e2b_node" -e')
-    expect(command).toContain('"$dsh_e2b_env_bin" -i -- "${dsh_e2b_env[@]}" "$@"')
-    expect(command).toContain('exec "$dsh_e2b_env_bin" -i -- "${dsh_e2b_env[@]}"')
-    expect(command).toContain('>&2 2>/dev/null')
-    expect(command).not.toContain('2>/dev/null >&2')
-    expect(command).toContain('base64')
-    expect(fake.writtenFiles[0]).toEqual([
-      '/workspace/.dsh-e2b/processes/one/pid',
-      '/workspace/.dsh-e2b/processes/one/exit-code',
-      '/workspace/.dsh-e2b/processes/one/environment',
-      '/workspace/.dsh-e2b/processes/one/stderr.log',
-    ])
-    expect(fake.writtenFileData.get('/workspace/.dsh-e2b/processes/one/environment')).toBe(
-      'PATH=/bin\0UNICODE=你好\0HOME=/home/user\0FOO-BAR=hyphen-value\0--split-string=literal-value\0DEEPSEEK_API_KEY=explicit-secret\0DSH_MODE=test\0',
-    )
-
-    let piped = ''
-    handle.stdout!.on('data', (chunk) => { piped += String(chunk) })
-    await fake.stdout('pipe-data')
-    await fake.stderr('err')
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    expect(piped).toBe('pipe-data')
-    expect(handle.collected.stderr!.readFrom(0)).toMatchObject({ text: 'err', lossy: false })
-    expect(fake.removed).toContain('/workspace/.dsh-e2b/processes/one/stderr.log')
-    await expect(handle.waitForExit()).resolves.toBe(true)
-  })
-
-  it('rejects an unrepresentable graceMs before any remote work', () => {
-    const ctx = new Context()
-    const service = Object.create(E2BSubprocessRuntime.prototype) as E2BSubprocessRuntime
-    Reflect.set(service, 'disposing', false)
-    Reflect.set(service, 'ctx', ctx)
-    for (const graceMs of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
-      expect(() => service.spawn(spec({ graceMs }))).toThrow('graceMs must be a positive finite number')
-      void expect(service.spawnTerminal({
-        argv: ['bash'], cwd: '/w', rows: 24, cols: 80, graceMs,
-      })).rejects.toThrow('graceMs must be a positive finite number')
-    }
-  })
-
-  it('rejects malformed environment entries before command start', async () => {
-    for (const env of [{ 'BAD=NAME': 'x' }, { BAD: 'x\0INJECTED=1' }]) {
-      const fake = new FakeSandbox()
-      const handle = testHandle(runtime(fake), spec({ env }), '/runtime/invalid-environment')
-      await expect(handle.done).rejects.toThrow('environment entries')
-      expect(fake.startOptions).toBeUndefined()
-      expect(fake.removed).toContain('/runtime/invalid-environment')
-    }
-  })
-
-  it('preserves UTF-8 bytes when the ASCII transport is split across callbacks', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({
-      stdio: { stdin: 'ignore', stdout: 'pipe', stderr: { maxBytes: 4 } },
-    }), '/runtime/split-utf8')
-    await flush()
-    const chunks: Buffer[] = []
-    handle.stdout!.on('data', (chunk: Buffer) => { chunks.push(chunk) })
-    for (const character of `${Buffer.from('A你好B').toString('base64')}\n`) {
-      await fake.stdoutWire(character)
-    }
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    expect(Buffer.concat(chunks).toString('utf8')).toBe('A你好B')
-  })
-
-  it('rejects malformed output transport without confusing it with a consumer sink failure', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/malformed-output')
-    await flush()
-    await fake.stdoutWire('%\n')
-    fake.finish()
-    await expect(handle.done).rejects.toThrow('invalid base64 output transport')
-
-    const stderrFake = new FakeSandbox()
-    const stderrHandle = testHandle(runtime(stderrFake), spec(), '/runtime/malformed-stderr')
-    await flush()
-    await stderrFake.stderrWire('%\n')
-    stderrFake.finish()
-    await expect(stderrHandle.done).rejects.toThrow('invalid base64 output transport')
-  })
-
-  it('rejects a naturally completed command whose encoder omits its completion frame', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/incomplete-output')
-    await flush()
-    fake.alive = false
-    fake.handle.succeed(0)
-    await expect(handle.done).rejects.toThrow('incomplete output transport')
-  })
-
-  it('bounds descendant-held output draining and withholds the incomplete spill', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({ graceMs: 5 }), '/runtime/drain-bound')
-    await flush()
-    await fake.stdout('leader-output')
-    fake.exitStatus = '0\n'
-
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    expect(fake.handle.disconnects).toBe(1)
-    expect(handle.collected.stdout?.readFrom(0)).toEqual({
-      text: 'tput',
-      nextOffset: 13,
-      lossy: true,
-    })
-    expect(fake.removed).toContain('/runtime/drain-bound/stdout.log')
-
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-  })
-
-  it('releases an inherited-output callback blocked on host backpressure at drain expiry', async () => {
-    const fake = new FakeSandbox()
-    const written: string[] = []
-    const stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(((chunk: Uint8Array) => {
-      written.push(Buffer.from(chunk).toString())
-      return false
-    }) as typeof process.stdout.write)
-    try {
-      const handle = testHandle(runtime(fake), spec({
-        graceMs: 5,
-        stdio: { stdin: 'ignore', stdout: 'inherit', stderr: { maxBytes: 4 } },
-      }), '/runtime/inherit-backpressure')
-      await flush()
-      let callbackSettled = false
-      const blocked = fake.stdout('blocked bytes').then(() => { callbackSettled = true })
-      await flush()
-      expect(callbackSettled).toBe(false)
-      fake.exitStatus = '0\n'
-
-      await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-      await blocked
-      expect(callbackSettled).toBe(true)
-      expect(written.join('')).toBe('blocked bytes')
-      expect(fake.handle.disconnects).toBe(1)
-
-      handle.terminate()
-      await expect(handle.waitForExit()).resolves.toBe(true)
-    } finally {
-      stdoutWrite.mockRestore()
-    }
-  })
-
-  it('waits for lossless raw-pipe output after the direct status is published', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({
-      graceMs: 1,
-      stdio: { stdin: 'ignore', stdout: 'pipe', stderr: { maxBytes: 4 } },
-    }), '/runtime/pipe-drain')
-    let output = ''
-    handle.stdout!.on('data', (chunk) => { output += String(chunk) })
-    await flush()
-    fake.exitStatus = '0\n'
-
-    let settled = false
-    void handle.done.then(() => { settled = true })
-    await new Promise(resolve => setTimeout(resolve, 50))
-    expect(settled).toBe(false)
-    expect(fake.handle.disconnects).toBe(0)
-    expect(fake.statusReads).toBe(0)
-
-    await fake.stdout('complete protocol frame')
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    expect(output).toBe('complete protocol frame')
-    expect(fake.statusReads).toBe(1)
-  })
-
-  it('accepts clean encoder completion inside the output-drain grace', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({ graceMs: 100 }), '/runtime/drain-complete')
-    await flush()
-    fake.exitStatus = '0\n'
-    fake.afterStatusRead = () => {
-      fake.afterStatusRead = undefined
-      setTimeout(() => { fake.finish() }, 0)
-    }
-
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    expect(fake.handle.disconnects).toBe(0)
-  })
-
-  it('preserves a published exit code when requested termination outlives output draining', async () => {
-    const fake = new FakeSandbox()
-    fake.trapsTerm = true
-    fake.delaysKill = true
-    fake.delaysKillCompletion = true
-    fake.sdkKillStops = false
-    const handle = testHandle(runtime(fake), spec({ graceMs: 5 }), '/runtime/drain-signal')
-    await flush()
-
-    handle.terminate()
-    await vi.waitFor(() => { expect(fake.commandsSeen).toContain('kill -KILL -- -4242') })
-    fake.exitStatus = '0\n'
-
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    expect(fake.handle.disconnects).toBe(1)
-    fake.alive = false
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-  })
-
-  it('preserves a published nonzero exit code when termination settles the SDK inside the drain grace', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({ graceMs: 100 }), '/runtime/drain-signal-settled')
-    await flush()
-    fake.exitStatus = '7\n'
-    fake.afterStatusRead = () => {
-      fake.afterStatusRead = undefined
-      handle.terminate()
-    }
-
-    await expect(handle.done).resolves.toEqual({ exitCode: 7, signal: null })
-    await expect(handle.waitForExit()).resolves.toBe(true)
-  })
-
-  it('rejects an invalid direct-command exit status', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/invalid-status')
-    await flush()
-    fake.exitStatus = '999\n'
-    await expect(handle.done).rejects.toThrow('invalid exit code')
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-  })
-
-  it('rolls back a published process group before rejecting a monitoring failure', async () => {
-    const fake = new FakeSandbox()
-    fake.statusError = new Error('status transport failed')
-    const handle = testHandle(runtime(fake), spec(), '/runtime/status-failure')
-
-    await expect(handle.done).rejects.toThrow('status transport failed')
-    expect(fake.commandsSeen).toContain('kill -TERM -- -4242')
-    expect(fake.alive).toBe(false)
-    await expect(handle.waitForExit()).resolves.toBe(true)
-
-    const failed = new FakeSandbox()
-    failed.statusError = new Error('status transport failed')
-    failed.signalErrors.push(new Error('TERM transport failed'), new Error('KILL transport failed'))
-    failed.handle.killError = new Error('SDK kill failed')
-    const retained = testHandle(runtime(failed), spec({ graceMs: 1 }), '/runtime/status-cleanup-failure')
-
-    await expect(retained.done).rejects.toThrow(
-      'command monitoring failed and process-group rollback did not reach quiescence',
-    )
-    expect(failed.alive).toBe(true)
-    failed.handle.killError = undefined
-    retained.terminate()
-    await expect(retained.waitForExit()).resolves.toBe(true)
-
-    // A state-cleanup failure on top preserves the rollback failure instead of
-    // re-aggregating only the original monitoring error.
-    const triple = new FakeSandbox()
-    triple.statusError = new Error('status transport failed')
-    triple.signalErrors.push(new Error('TERM transport failed'), new Error('KILL transport failed'))
-    triple.handle.killError = new Error('SDK kill failed')
-    triple.nextRemoveError = new Error('state cleanup failed')
-    const tripleHandle = testHandle(runtime(triple), spec({ graceMs: 1 }), '/runtime/triple-failure')
-    const failure = await tripleHandle.done.catch((error: unknown) => error as AggregateError)
-    expect(failure).toBeInstanceOf(AggregateError)
-    expect((failure as AggregateError).message).toContain('private state cleanup failed')
-    const nested = (failure as AggregateError).errors[0] as AggregateError
-    expect(nested.message).toContain('rollback did not reach quiescence')
-    triple.handle.killError = undefined
-    tripleHandle.terminate()
-    await expect(tripleHandle.waitForExit()).resolves.toBe(true)
-  })
-
-  it('surfaces deferred piped-stdin write and close failures as stream errors', async () => {
-    const writeFake = new FakeSandbox()
-    writeFake.deferStart()
-    vi.spyOn(writeFake.handle, 'sendStdin').mockRejectedValueOnce('stdin rejected')
-    const writeHandle = testHandle(runtime(writeFake), spec({
-      stdio: { stdin: 'pipe', stdout: { maxBytes: 4 }, stderr: { maxBytes: 4 } },
-    }), '/runtime/stdin-write-error')
-    const writeError = once(writeHandle.stdin!, 'error')
-    writeHandle.stdin!.write('input')
-    writeFake.releaseStart()
-    await expect(writeError).resolves.toMatchObject([{ message: 'stdin rejected' }])
-    writeFake.finish()
-    await writeHandle.done
-
-    const closeFake = new FakeSandbox()
-    vi.spyOn(closeFake.handle, 'closeStdin').mockRejectedValueOnce(new Error('close rejected'))
-    const closeHandle = testHandle(runtime(closeFake), spec({
-      stdio: { stdin: 'pipe', stdout: { maxBytes: 4 }, stderr: { maxBytes: 4 } },
-    }), '/runtime/stdin-close-error')
-    await flush()
-    const closeError = once(closeHandle.stdin!, 'error')
-    closeHandle.stdin!.end()
-    await expect(closeError).resolves.toMatchObject([{ message: 'close rejected' }])
-    closeFake.finish()
-    await closeHandle.done
-  })
-
-  it('collects bounded tails, retains valid spills, and maps natural nonzero exits', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({
-      stdio: {
-        stdin: { data: 'batch' },
-        stdout: { maxBytes: 4, spill: { maxBytes: 16 } },
-        stderr: { maxBytes: 3 },
-      },
-    }), '/runtime/two')
-    await flush()
-    await fake.stdout('abcdef')
-    await fake.stderr('12345')
-    fake.finish(7)
-    await expect(handle.done).resolves.toEqual({ exitCode: 7, signal: null })
-    expect(fake.handle.sent).toEqual(['batch'])
-    expect(fake.handle.closes).toBe(1)
-    expect(handle.collected.stdout!.readFrom(0)).toEqual({
-      text: 'cdef',
-      nextOffset: 6,
-      lossy: true,
-      spillPath: '/runtime/two/stdout.log',
-    })
-    expect(handle.collected.stderr!.readFrom(0)).toEqual({ text: '345', nextOffset: 5, lossy: true })
-    expect(fake.removed).not.toContain('/runtime/two/stdout.log')
-  })
-
-  it('removes a spill once the complete stream exceeds its cap', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({
-      stdio: { stdin: 'ignore', stdout: { maxBytes: 2, spill: { maxBytes: 3 } }, stderr: 'inherit' },
-    }), '/runtime/oversize')
-    await flush()
-    await fake.stdout('abcd')
-    await fake.stderr('')
-    fake.finish()
-    await handle.done
-    expect(handle.collected.stdout!.readFrom(0)).toEqual({ text: 'cd', nextOffset: 4, lossy: true })
-    expect(fake.removed).toContain('/runtime/oversize/stdout.log')
-    const command = fake.commandsSeen.find(value => value.includes('dsh_e2b_tee='))!
-    expect(command).toContain('"$dsh_e2b_head" -c 3')
-    expect(command).toContain('/runtime/oversize/stdout.log')
-    expect(command).toContain('"$dsh_e2b_tee" --output-error=warn-nopipe')
-    expect(command).not.toContain('tee -a')
-  })
-
-  it('contains remote spill-removal failures and routes empty inherited output', async () => {
-    const fake = new FakeSandbox()
-    fake.nextRemoveError = new Error('already removed')
-    const handle = testHandle(runtime(fake), spec({
-      stdio: { stdin: 'ignore', stdout: 'inherit', stderr: { maxBytes: 4, spill: { maxBytes: 8 } } },
-    }), '/runtime/remove-error')
-    await flush()
-    await fake.stdout('')
-    await fake.stderr('')
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    expect(fake.removed).toContain('/runtime/remove-error/stderr.log')
-  })
-
-  it('terminates a process group with TERM and reports the signal outcome', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/term')
-    await flush()
-    handle.terminate()
-    handle.terminate()
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    expect(fake.commandsSeen).toContain('kill -TERM -- -4242')
-    expect(fake.commandsSeen).not.toContain('kill -KILL -- -4242')
-    const signals = fake.commandsSeen.filter(command => command.startsWith('kill -')).length
-    fake.alive = true
-    handle.terminate()
-    await flush()
-    expect(fake.alive).toBe(true)
-    expect(fake.commandsSeen.filter(command => command.startsWith('kill -'))).toHaveLength(signals)
-  })
-
-  it('makes termination a permanent no-op after natural quiescence is observed', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/natural-quiescence')
-    await flush()
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    await expect(handle.waitForExit()).resolves.toBe(true)
-
-    const signals = fake.commandsSeen.filter(command => command.startsWith('kill -')).length
-    fake.alive = true
-    handle.terminate()
-    await flush()
-    expect(fake.alive).toBe(true)
-    expect(fake.commandsSeen.filter(command => command.startsWith('kill -'))).toHaveLength(signals)
-  })
-
-  it('treats a zombie-only process group as quiescent', async () => {
-    const fake = new FakeSandbox()
-    fake.zombieOnly = true
-    const handle = testHandle(runtime(fake), spec(), '/runtime/zombie-quiescence')
-    await flush()
-
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    expect(fake.commandsSeen).toContain(
-      'set -o pipefail; ps -eo pgid=,stat= | awk \'$1 == 4242 && $2 !~ /^[ZXx]/ { live=1 } END { if (live) print "live" }\'',
-    )
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-  })
-
-  it('keeps proven quiescence after a concurrent termination transport fails', async () => {
-    const fake = new FakeSandbox()
-    fake.signalErrors.push(new Error('TERM transport failed'), new Error('KILL transport failed'))
-    fake.handle.killError = new Error('SDK kill failed')
-    fake.deferSignals()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/quiescent-race')
-    await flush()
-
-    handle.terminate()
-    await vi.waitFor(() => { expect(fake.commandsSeen).toContain('kill -TERM -- -4242') })
-    fake.alive = false
-    await expect(handle.waitForExit()).resolves.toBe(true)
-
-    fake.probeError = new Error('post-quiescence probe failed')
-    fake.releaseSignals()
-    await vi.waitFor(() => { expect(fake.handle.kills).toBe(1) })
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    const signals = fake.commandsSeen.filter(command => command.startsWith('kill -')).length
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    expect(fake.commandsSeen.filter(command => command.startsWith('kill -'))).toHaveLength(signals)
-  })
-
-  it('escalates a TERM-trapping process group to KILL and uses the SDK kill as fallback', async () => {
-    const fake = new FakeSandbox()
-    fake.trapsTerm = true
-    fake.handle.killError = new Error('already gone')
-    const handle = testHandle(runtime(fake), spec({ graceMs: 1 }), '/runtime/kill')
-    await flush()
-    handle.terminate()
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    expect(fake.commandsSeen).toContain('kill -KILL -- -4242')
-    expect(fake.handle.kills).toBe(1)
-  })
-
-  it('keeps force cleanup retryable until quiescence is proven', async () => {
-    const fake = new FakeSandbox()
-    fake.trapsTerm = true
-    fake.delaysKill = true
-    fake.delaysKillCompletion = true
-    fake.sdkKillStops = false
-    const handle = testHandle(runtime(fake), spec({ graceMs: 1 }), '/runtime/termination-fence')
-    await flush()
-    handle.terminate()
-    await vi.waitFor(() => { expect(fake.handle.kills).toBe(1) })
-    await expect(handle.waitForExit()).rejects.toThrow('remained live after force termination')
-    expect(fake.alive).toBe(true)
-
-    fake.delaysKill = false
-    fake.delaysKillCompletion = false
-    fake.sdkKillStops = true
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-  })
-
-  it('honors termination requested before asynchronous startup finishes', async () => {
-    const fake = new FakeSandbox()
-    fake.deferStart()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/deferred-kill')
-    handle.terminate()
-    fake.releaseStart()
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-  })
-
-  it('aborts a stalled preparation request before reporting startup quiescence', async () => {
-    const fake = new FakeSandbox()
-    let preparationSignal: AbortSignal | undefined
-    fake.environmentRequest = async (signal) => {
-      preparationSignal = signal
-      await new Promise<never>((_resolve, reject) => {
-        const rejectAbort = (): void => {
-          const reason: unknown = signal?.reason
-          reject(reason instanceof Error ? reason : new Error(String(reason)))
-        }
-        if (signal?.aborted === true) {
-          rejectAbort()
-          return
-        }
-        signal?.addEventListener('abort', rejectAbort, { once: true })
-      })
-    }
-    const handle = testHandle(runtime(fake), spec(), '/runtime/stalled-preparation')
-    await vi.waitFor(() => { expect(preparationSignal).toBeDefined() })
-
-    handle.terminate()
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    expect(preparationSignal?.aborted).toBe(true)
-    expect(fake.startOptions).toBeUndefined()
-  })
-
-  it('kills through the provisional SDK handle before process-group publication', async () => {
-    const fake = new FakeSandbox()
-    fake.deferProcessGroupRead()
-    fake.signalErrors.push(commandError(1), commandError(1))
-    const handle = testHandle(runtime(fake), spec(), '/runtime/pre-publication-kill')
-    await vi.waitFor(() => { expect(fake.startOptions).toBeDefined() })
-
-    handle.terminate()
-    await vi.waitFor(() => { expect(fake.handle.kills).toBe(1) })
-    expect(fake.alive).toBe(false)
-    await expect(handle.waitForExit()).resolves.toBe(true)
-
-    fake.releaseProcessGroupRead()
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-  })
-
-  it('does not treat an unsuccessful SDK fallback as provisional group quiescence', async () => {
-    const fake = new FakeSandbox()
-    fake.deferProcessGroupRead()
-    fake.trapsTerm = true
-    fake.delaysKill = true
-    fake.delaysKillCompletion = true
-    fake.sdkKillStops = false
-    fake.handle.killResult = false
-    const handle = testHandle(runtime(fake), spec({ graceMs: 1 }), '/runtime/provisional-sdk-false')
-    await vi.waitFor(() => { expect(fake.startOptions).toBeDefined() })
-
-    handle.terminate()
-    await vi.waitFor(() => { expect(fake.handle.kills).toBe(1) })
-    await expect(handle.waitForExit()).rejects.toThrow('remained live after force termination')
-    fake.alive = false
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    fake.releaseProcessGroupRead()
-    fake.finish()
-    await handle.done
-  })
-
-  it('bounds a quiescence observer while provisional termination is awaiting the controller', async () => {
-    const fake = new FakeSandbox()
-    fake.deferProcessGroupRead()
-    const reconnect = Promise.withResolvers<Sandbox>()
-    let calls = 0
-    const delayedRuntime = runtime(fake, async () => {
-      calls += 1
-      return calls === 1 ? fake.sandbox : await reconnect.promise
-    })
-    const handle = testHandle(delayedRuntime, spec(), '/runtime/pre-publication-observer')
-    await vi.waitFor(() => { expect(fake.startOptions).toBeDefined() })
-    handle.terminate()
-
-    const controller = new AbortController()
-    const waiting = handle.waitForExit(controller.signal)
-    await flush()
-    controller.abort()
-    await expect(waiting).resolves.toBe(false)
-
-    reconnect.resolve(fake.sandbox)
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    fake.releaseProcessGroupRead()
-    await handle.done
-  })
-
-  it('proves a provisional group exit when the SDK kill fallback fails', async () => {
-    const fake = new FakeSandbox()
-    fake.deferProcessGroupRead()
-    fake.trapsTerm = true
-    fake.handle.killError = new Error('SDK kill unavailable')
-    const handle = testHandle(runtime(fake), spec({ graceMs: 1 }), '/runtime/pre-publication-group-kill')
-    await vi.waitFor(() => { expect(fake.startOptions).toBeDefined() })
-
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    fake.releaseProcessGroupRead()
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-  })
-
-  it('reports failed provisional group and SDK force transports', async () => {
-    const fake = new FakeSandbox()
-    fake.deferProcessGroupRead()
-    fake.signalErrors.push(new Error('TERM transport failed'), new Error('KILL transport failed'))
-    fake.handle.killError = new Error('SDK kill failed')
-    const handle = testHandle(runtime(fake), spec({ graceMs: 1 }), '/runtime/pre-publication-failure')
-    await vi.waitFor(() => { expect(fake.startOptions).toBeDefined() })
-
-    handle.terminate()
-    await expect(handle.waitForExit()).rejects.toThrow('remained live after force termination')
-
-    fake.handle.killError = undefined
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    fake.releaseProcessGroupRead()
-    await handle.done
-
-    const absentGroup = new FakeSandbox()
-    absentGroup.deferProcessGroupRead()
-    absentGroup.signalErrors.push(commandError(1), commandError(1))
-    absentGroup.handle.killError = new Error('SDK kill failed without a provisional group')
-    const absentHandle = testHandle(
-      runtime(absentGroup),
-      spec({ graceMs: 1 }),
-      '/runtime/pre-publication-absent-group',
-    )
-    await vi.waitFor(() => { expect(absentGroup.startOptions).toBeDefined() })
-    absentHandle.terminate()
-    await expect(absentHandle.waitForExit()).rejects.toThrow('remained live after force termination')
-    absentGroup.handle.killError = undefined
-    absentHandle.terminate()
-    await expect(absentHandle.waitForExit()).resolves.toBe(true)
-    absentGroup.releaseProcessGroupRead()
-    await absentHandle.done
-
-    const optimisticSdk = new FakeSandbox()
-    optimisticSdk.deferProcessGroupRead()
-    optimisticSdk.signalErrors.push(commandError(1), commandError(1))
-    optimisticSdk.sdkKillStops = false
-    const optimisticHandle = testHandle(
-      runtime(optimisticSdk),
-      spec({ graceMs: 1 }),
-      '/runtime/pre-publication-optimistic-sdk',
-    )
-    await vi.waitFor(() => { expect(optimisticSdk.startOptions).toBeDefined() })
-    optimisticHandle.terminate()
-    await expect(optimisticHandle.waitForExit()).rejects.toThrow('remained live after force termination')
-    optimisticHandle.terminate()
-    await expect(optimisticHandle.waitForExit()).resolves.toBe(true)
-    optimisticSdk.releaseProcessGroupRead()
-    await optimisticHandle.done
-  })
-
-  it('honors an already-aborted signal when constructing the asynchronous handle directly', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({ signal: AbortSignal.abort('stop') }), '/runtime/pre-aborted')
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-  })
-
-  it('reacts to a signal that aborts after the remote command has started', async () => {
-    const fake = new FakeSandbox()
-    const controller = new AbortController()
-    const handle = testHandle(runtime(fake), spec({ signal: controller.signal }), '/runtime/live-abort')
-    await flush()
-    controller.abort('stop')
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-  })
-
-  it('can terminate a surviving process group after the command leader settles', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/surviving-group')
-    await flush()
-    await fake.completeOutput()
-    fake.handle.succeed(0)
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    expect(fake.alive).toBe(true)
-
-    handle.terminate()
-    await flush()
-    const signaled = fake.commandsSeen.includes('kill -TERM -- -4242')
-    if (!signaled) fake.finish()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    expect(signaled).toBe(true)
-  })
-
-  it('bounds waitForExit while startup or a live group is pending', async () => {
-    const fake = new FakeSandbox()
-    fake.deferStart()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/wait')
-    const beforeStart = new AbortController()
-    const pending = handle.waitForExit(beforeStart.signal)
-    beforeStart.abort()
-    await expect(pending).resolves.toBe(false)
-    await expect(handle.waitForExit(AbortSignal.abort())).resolves.toBe(false)
-    fake.releaseStart()
-    await flush()
-    const live = new AbortController()
-    const liveWait = handle.waitForExit(live.signal)
-    live.abort()
-    await expect(liveWait).resolves.toBe(false)
-    fake.finish()
-    await handle.done
-
-    const terminatingFake = new FakeSandbox()
-    terminatingFake.deferStart()
-    const terminating = testHandle(runtime(terminatingFake), spec(), '/runtime/wait-termination-start')
-    terminating.terminate()
-    const beforeHandle = new AbortController()
-    const handlePending = terminating.waitForExit(beforeHandle.signal)
-    beforeHandle.abort()
-    await expect(handlePending).resolves.toBe(false)
-    terminatingFake.releaseStart()
-    await terminating.done
-  })
-
-  it('bounds both sides of the liveness-poll abort race', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/poll-abort')
-    await flush()
-
-    const beforeTick = new AbortController()
-    fake.afterProbe = () => { beforeTick.abort(); fake.afterProbe = undefined }
-    await expect(handle.waitForExit(beforeTick.signal)).resolves.toBe(false)
-
-    const duringTick = new AbortController()
-    fake.afterProbe = () => {
-      fake.afterProbe = undefined
-      setTimeout(() => { duringTick.abort() }, 0)
-    }
-    await expect(handle.waitForExit(duringTick.signal)).resolves.toBe(false)
-
-    const duringProbe = new AbortController()
-    fake.beforeProbe = () => { duringProbe.abort(); fake.beforeProbe = undefined }
-    await expect(handle.waitForExit(duringProbe.signal)).resolves.toBe(false)
-
-    let racedAbort = false
-    const raceSignal = {
-      get aborted() { return racedAbort },
-      addEventListener: () => { racedAbort = true },
-      removeEventListener: () => {},
-    } as unknown as AbortSignal
-    await expect(handle.waitForExit(raceSignal)).resolves.toBe(false)
-    fake.finish()
-    await handle.done
-  })
-
-  it('observes a live group across one successful bounded poll', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/poll-success')
-    await flush()
-    setTimeout(() => { fake.finish() }, 1)
-    await expect(handle.waitForExit(new AbortController().signal)).resolves.toBe(true)
-    await handle.done
-  })
-
-  it('treats startup failure as no live tree and contains readiness rejection', async () => {
-    const fake = new FakeSandbox()
-    fake.backgroundError = new Error('start failed')
-    const handle = testHandle(runtime(fake), spec(), '/runtime/fail')
-    await expect(handle.done).rejects.toThrow('start failed')
-    expect(fake.removed).toContain('/runtime/fail/environment')
-    expect(fake.removed).toContain('/runtime/fail')
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    handle.terminate()
-
-    const unavailableHandle = testHandle(
-      runtime(new FakeSandbox(), async () => { throw new Error('sandbox unavailable') }),
-      spec(),
-      '/runtime/unavailable-start',
-    )
-    await expect(unavailableHandle.done).rejects.toThrow('sandbox unavailable')
-    await expect(unavailableHandle.waitForExit()).resolves.toBe(true)
-
-    const envFailure = new FakeSandbox()
-    envFailure.envError = new Error('ambient lookup failed')
-    const envHandle = testHandle(runtime(envFailure), spec(), '/runtime/env-failure')
-    await expect(envHandle.done).rejects.toThrow('ambient lookup failed')
-    expect(envFailure.removed).toEqual([])
-
-    const expectEnvironmentFailure = async (name: string, wire: string, message: string): Promise<void> => {
-      const fake = new FakeSandbox()
-      fake.environmentWire = wire
-      const failed = testHandle(runtime(fake), spec(), `/runtime/${name}`)
-      await expect(failed.done).rejects.toThrow(message)
-    }
-    const encodedEnvironment = Buffer.from('PATH=/bin\0').toString('base64')
-    const encodedHome = Buffer.from('/home/user').toString('base64')
-    await expectEnvironmentFailure('malformed-frame', '%', 'invalid base64')
-    await expectEnvironmentFailure('malformed-base64', `${encodedHome}\n%`, 'invalid base64')
-    await expectEnvironmentFailure(
-      'invalid-utf8-home',
-      `${Buffer.from([0xff]).toString('base64')}\n${encodedEnvironment}`,
-      'not valid UTF-8',
-    )
-    await expectEnvironmentFailure(
-      'invalid-utf8-environment',
-      `${encodedHome}\n${Buffer.from([0xff]).toString('base64')}`,
-      'not valid UTF-8',
-    )
-    await expectEnvironmentFailure(
-      'relative-home',
-      `${Buffer.from('home/user').toString('base64')}\n${encodedEnvironment}`,
-      'remote login home is invalid',
-    )
-    await expectEnvironmentFailure(
-      'nul-home',
-      `${Buffer.from('/home/user\0tail').toString('base64')}\n${encodedEnvironment}`,
-      'remote login home is invalid',
-    )
-
-    const cleanupFailure = new FakeSandbox()
-    cleanupFailure.backgroundError = new Error('start failed before credential consumption')
-    cleanupFailure.nextRemoveError = new Error('credential cleanup failed')
-    const cleanupHandle = testHandle(runtime(cleanupFailure), spec(), '/runtime/cleanup-failure')
-    await expect(cleanupHandle.done).rejects.toThrow('command failed and private state cleanup failed')
-
-    const absentState = new FakeSandbox()
-    absentState.backgroundError = new Error('start failed after external cleanup')
-    absentState.nextRemoveError = new FileNotFoundError('already removed')
-    const absentHandle = testHandle(runtime(absentState), spec(), '/runtime/absent-state')
-    await expect(absentHandle.done).rejects.toThrow('start failed after external cleanup')
-  })
-
-  it('bounds a readiness rejection with a still-live caller signal', async () => {
-    const fake = new FakeSandbox()
-    fake.deferStart()
-    fake.backgroundError = new Error('start failed')
-    const handle = testHandle(runtime(fake), spec(), '/runtime/fail-with-signal')
-    const waiting = handle.waitForExit(new AbortController().signal)
-    fake.releaseStart()
-    await expect(handle.done).rejects.toThrow('start failed')
-    await expect(waiting).resolves.toBe(true)
-  })
-
-  it('propagates an unavailable sandbox unless the caller aborts the wait', async () => {
-    const fake = new FakeSandbox()
-    let calls = 0
-    const unavailable = runtime(fake, async () => {
-      calls += 1
-      if (calls === 1) return fake.sandbox
-      throw new Error('connection unavailable')
-    })
-    const handle = testHandle(unavailable, spec(), '/runtime/unavailable')
-    await flush()
-    await expect(handle.waitForExit()).rejects.toThrow('connection unavailable')
-    fake.finish()
-    await handle.done
-  })
-
-  it('returns false when the caller aborts while reconnecting for liveness', async () => {
-    const fake = new FakeSandbox()
-    const reconnect = Promise.withResolvers<Sandbox>()
-    let calls = 0
-    const unavailable = runtime(fake, async () => {
-      calls += 1
-      return calls === 1 ? fake.sandbox : await reconnect.promise
-    })
-    const handle = testHandle(unavailable, spec(), '/runtime/reconnect-abort')
-    await flush()
-    const controller = new AbortController()
-    const waiting = handle.waitForExit(controller.signal)
-    await flush()
-    controller.abort()
-    reconnect.reject(new Error('connection unavailable'))
-    await expect(waiting).resolves.toBe(false)
-    fake.finish()
-    await handle.done
-  })
-
-  it('returns false when a liveness request itself is aborted and surfaces other probe failures', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/probe')
-    await flush()
-    const controller = new AbortController()
-    controller.abort()
-    await expect(handle.waitForExit(controller.signal)).resolves.toBe(false)
-    fake.probeError = new Error('probe failed')
-    await expect(handle.waitForExit()).rejects.toThrow('probe failed')
-    fake.finish()
-    await handle.done
-  })
-
-  it('treats a timeout-killed sandbox as quiescent during liveness probing', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec(), '/runtime/expired-sandbox')
-    await flush()
-    fake.finish()
-    await handle.done
-    fake.probeError = new SandboxNotFoundError('sandbox expired')
-
-    await expect(handle.waitForExit()).resolves.toBe(true)
-  })
-
-  it('treats a missing sandbox handle as quiescent during liveness acquisition', async () => {
-    const fake = new FakeSandbox()
-    let calls = 0
-    const handle = testHandle(runtime(fake, async () => {
-      calls += 1
-      if (calls === 1) return fake.sandbox
-      throw new SandboxNotFoundError('sandbox expired')
-    }), spec(), '/runtime/expired-acquisition')
-    await flush()
-
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    await fake.completeOutput()
-    fake.alive = false
-    fake.handle.succeed(0)
-    await handle.done
-  })
-
-  it('treats sandbox loss during termination as quiescent', async () => {
-    const fake = new FakeSandbox()
-    let calls = 0
-    const handle = testHandle(runtime(fake, async () => {
-      calls += 1
-      if (calls === 1) return fake.sandbox
-      throw new SandboxNotFoundError('sandbox expired')
-    }), spec(), '/runtime/expired-termination')
-    await flush()
-    await fake.completeOutput()
-
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    fake.alive = false
-    fake.handle.succeed(0)
-    await handle.done
-  })
-
-  it('makes batch stdin close failures best-effort', async () => {
-    const fake = new FakeSandbox()
-    vi.spyOn(fake.handle, 'sendStdin').mockRejectedValueOnce(new Error('closed'))
-    const handle = testHandle(runtime(fake), spec({
-      stdio: { stdin: { data: 'ignored' }, stdout: { maxBytes: 4 }, stderr: { maxBytes: 4 } },
-    }), '/runtime/stdin-closed')
-    await flush()
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-  })
-
-  it('rejects malformed SDK process ids and non-command settlement failures', async () => {
-    const invalidPid = new FakeSandbox()
-    invalidPid.handle.pid = 0
-    const invalid = testHandle(runtime(invalidPid), spec(), '/runtime/invalid-pid')
-    await expect(invalid.done).rejects.toThrow(/invalid command pid 0/)
-    expect(invalidPid.handle.kills).toBe(1)
-    expect(invalidPid.removed).toContain('/runtime/invalid-pid/environment')
-    await expect(invalid.waitForExit()).resolves.toBe(true)
-
-    const failedRollback = new FakeSandbox()
-    failedRollback.handle.pid = 0
-    failedRollback.handle.killError = new Error('invalid handle kill failed')
-    const retained = testHandle(runtime(failedRollback), spec(), '/runtime/invalid-pid-retained')
-    await expect(retained.done).rejects.toThrow('invalid command pid rollback did not reach quiescence')
-    await expect(retained.waitForExit()).rejects.toThrow('invalid handle kill failed')
-    failedRollback.handle.killError = undefined
-    retained.terminate()
-    await expect(retained.waitForExit()).resolves.toBe(true)
-
-    const crashedFake = new FakeSandbox()
-    const crashed = testHandle(runtime(crashedFake), spec(), '/runtime/crashed')
-    await flush()
-    crashedFake.alive = false
-    crashedFake.handle.crash(new Error('command transport failed'))
-    await expect(crashed.done).rejects.toThrow('command transport failed')
-  })
-
-  it('rejects invalid or absent process-group publication', async () => {
-    const invalidGroup = new FakeSandbox()
-    invalidGroup.processGroupId = 'not-a-pid\n'
-    invalidGroup.delaysKill = true
-    invalidGroup.sdkKillStops = false
-    invalidGroup.afterProbe = () => { invalidGroup.alive = false }
-    const invalid = testHandle(runtime(invalidGroup), spec(), '/runtime/invalid-group')
-    await expect(invalid.done).rejects.toThrow(/invalid process-group id/)
-    expect(invalidGroup.handle.kills).toBe(1)
-    expect(invalidGroup.commandsSeen).toContain('kill -KILL -- -4242')
-    await expect(invalid.waitForExit()).resolves.toBe(true)
-
-    // A rewritten pid file must not aim the kill at every process (`-- -1`).
-    const unsafeGroup = new FakeSandbox()
-    unsafeGroup.processGroupId = '1\n'
-    unsafeGroup.delaysKill = true
-    unsafeGroup.sdkKillStops = false
-    unsafeGroup.afterProbe = () => { unsafeGroup.alive = false }
-    const unsafe = testHandle(runtime(unsafeGroup), spec(), '/runtime/unsafe-group')
-    await expect(unsafe.done).rejects.toThrow(/unsafe published process-group id 1/)
-    expect(unsafeGroup.commandsSeen).not.toContain('kill -KILL -- -1')
-    await expect(unsafe.waitForExit()).resolves.toBe(true)
-
-    const absentGroup = new FakeSandbox()
-    absentGroup.processGroupId = ''
-    const absent = testHandle(runtime(absentGroup), spec(), '/runtime/absent-group')
-    await flush()
-    absentGroup.finish()
-    await expect(absent.done).rejects.toThrow(/exited before publishing/)
-    expect(absentGroup.handle.kills).toBe(1)
-    expect(absentGroup.commandsSeen).toContain('kill -KILL -- -4242')
-    await expect(absent.waitForExit()).resolves.toBe(true)
-  })
-
-  it('keeps polling while a running command has not published its process group yet', async () => {
-    const fake = new FakeSandbox()
-    fake.processGroupReads.push('', '4242\n')
-    const handle = testHandle(runtime(fake), spec(), '/runtime/delayed-group-publication', 1)
-
-    await vi.waitFor(() => { expect(fake.processGroupReads).toEqual([]) })
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-    await expect(handle.waitForExit()).resolves.toBe(true)
-  })
-
-  it('preserves publication failure and reports cleanup that cannot be verified', async () => {
-    const fake = new FakeSandbox()
-    fake.processGroupId = 'not-a-pid\n'
-    fake.signalError = new Error('rollback signal failed')
-    fake.handle.killError = new Error('SDK kill failed')
-    const handle = testHandle(runtime(fake), spec(), '/runtime/failed-rollback')
-
-    let failure: unknown
-    try {
-      await handle.done
-    } catch (error: unknown) {
-      failure = error
-    }
-    expect(failure).toBeInstanceOf(AggregateError)
-    if (!(failure instanceof AggregateError)) throw new Error('expected AggregateError')
-    expect(failure.message).toBe('subprocess-e2b: process-group publication failed and rollback did not reach quiescence')
-    const failures = Array.from(failure.errors as Iterable<unknown>)
-    expect(failures).toHaveLength(2)
-    expect(failures[0]).toBeInstanceOf(Error)
-    expect(failures[1]).toBeInstanceOf(Error)
-    if (!(failures[0] instanceof Error) || !(failures[1] instanceof Error)) throw new Error('expected nested errors')
-    expect(failures[0].message).toContain('invalid process-group id')
-    expect(failures[1].message).toContain('remained live after force termination')
-    expect(fake.handle.kills).toBe(1)
-    const bounded = new AbortController()
-    const waiting = handle.waitForExit(bounded.signal)
-    bounded.abort()
-    await expect(waiting).resolves.toBe(false)
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    expect(fake.commandsSeen).toContain('kill -TERM -- -4242')
-
-    const naturallyGone = new FakeSandbox()
-    naturallyGone.processGroupId = 'not-a-pid\n'
-    naturallyGone.signalError = new Error('rollback signal failed')
-    naturallyGone.handle.killError = new Error('SDK kill failed')
-    const observed = testHandle(runtime(naturallyGone), spec(), '/runtime/failed-rollback-observed')
-    await expect(observed.done).rejects.toThrow('process-group publication failed')
-    naturallyGone.alive = false
-    await expect(observed.waitForExit()).resolves.toBe(true)
-  })
-
-  it('handles output backpressure and contains a stderr sink failure', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({
-      stdio: { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' },
-    }), '/runtime/backpressure')
-    await flush()
-
-    handle.stdout!.on('error', () => {})
-    const stdoutWrite = vi.spyOn(handle.stdout!, 'write').mockReturnValueOnce(false)
-    const stdoutPending = fake.stdout('blocked')
-    queueMicrotask(() => { handle.stdout!.emit('drain') })
-    await stdoutPending
-    stdoutWrite.mockRestore()
-
-    handle.stderr!.on('error', () => {})
-    const stderrWrite = vi.spyOn(handle.stderr!, 'write').mockReturnValueOnce(false)
-    const stderrPending = fake.stderr('broken')
-    queueMicrotask(() => { handle.stderr!.emit('error', new Error('sink failed')) })
-    await stderrPending
-    stderrWrite.mockRestore()
-
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-  })
-
-  it('settles output backpressure when the consumer closes the pipe', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({
-      stdio: { stdin: 'ignore', stdout: 'pipe', stderr: { maxBytes: 4 } },
-    }), '/runtime/backpressure-close')
-    await flush()
-
-    const stdoutWrite = vi.spyOn(handle.stdout!, 'write').mockReturnValueOnce(false)
-    const pending = fake.stdout('discarded')
-    queueMicrotask(() => { handle.stdout!.destroy() })
-    await pending
-    stdoutWrite.mockRestore()
-
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-  })
-
-  it('breaks output backpressure when termination owns the command', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({
-      stdio: { stdin: 'ignore', stdout: 'pipe', stderr: { maxBytes: 4 } },
-    }), '/runtime/backpressure-termination')
-    await flush()
-
-    const stdoutWrite = vi.spyOn(handle.stdout!, 'write').mockReturnValueOnce(false)
-    let released = false
-    const pending = fake.stdout('blocked').then(() => { released = true })
-    await Promise.resolve()
-    handle.terminate()
-    await flush()
-    const releasedByTermination = released
-    if (!released) handle.stdout!.emit('drain')
-    await pending
-    stdoutWrite.mockRestore()
-    await handle.done
-
-    expect(releasedByTermination).toBe(true)
-  })
-
-  it('settles backpressure when a synchronous pipe write starts termination', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({
-      stdio: { stdin: 'ignore', stdout: 'pipe', stderr: { maxBytes: 4 } },
-    }), '/runtime/backpressure-synchronous-termination')
-    await flush()
-
-    const stdoutWrite = vi.spyOn(handle.stdout!, 'write').mockImplementationOnce(() => {
-      handle.terminate()
-      return false
-    })
-    await expect(fake.stdout('blocked')).resolves.toBeUndefined()
-    stdoutWrite.mockRestore()
-    await handle.done
-  })
-
-  it('contains a pipe callback failure instead of rejecting command settlement', async () => {
-    const fake = new FakeSandbox()
-    const handle = testHandle(runtime(fake), spec({
-      stdio: { stdin: 'ignore', stdout: 'pipe', stderr: { maxBytes: 4 } },
-    }), '/runtime/pipe-error')
-    await flush()
-    const emitted = once(handle.stdout!, 'error')
-    handle.stdout!.destroy(new Error('consumer failed'))
-    await emitted
-    await fake.stdout('late output')
-    fake.finish()
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-  })
-
-  it('contains an already-gone group signal and escalates after a TERM transport failure', async () => {
-    const gone = new FakeSandbox()
-    gone.trapsTerm = true
-    gone.signalError = commandError(1)
-    const goneHandle = testHandle(runtime(gone), spec({ graceMs: 1 }), '/runtime/gone-signal')
-    await flush()
-    goneHandle.terminate()
-    await expect(goneHandle.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-
-    const failed = new FakeSandbox()
-    failed.signalError = new Error('signal transport failed')
-    const failedHandle = testHandle(runtime(failed), spec(), '/runtime/failed-signal')
-    await flush()
-    failedHandle.terminate()
-    await expect(failedHandle.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-    expect(failed.commandsSeen).toContain('kill -KILL -- -4242')
-  })
-
-  it('allows termination retry after both force transports fail', async () => {
-    const fake = new FakeSandbox()
-    fake.signalErrors.push(new Error('TERM transport failed'), new Error('KILL transport failed'))
-    fake.handle.killError = new Error('SDK kill failed')
-    const handle = testHandle(runtime(fake), spec({ graceMs: 1 }), '/runtime/retry-signal')
-    await flush()
-
-    handle.terminate()
-    await expect(handle.waitForExit()).rejects.toThrow('remained live after force termination')
-    fake.handle.killError = undefined
-    handle.terminate()
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-    expect(fake.commandsSeen.filter(command => command.startsWith('kill -TERM '))).toHaveLength(2)
-
-    const missingGroup = new FakeSandbox()
-    missingGroup.trapsTerm = true
-    missingGroup.signalErrors.push(undefined, commandError(1))
-    missingGroup.handle.killError = new Error('SDK kill failed after group exit race')
-    const raced = testHandle(runtime(missingGroup), spec({ graceMs: 1 }), '/runtime/group-exit-race')
-    await flush()
-    raced.terminate()
-    await expect(raced.waitForExit()).rejects.toThrow('remained live after force termination')
-    missingGroup.handle.killError = undefined
-    raced.terminate()
-    await expect(raced.waitForExit()).resolves.toBe(true)
-  })
-
-  it('rejects an optimistic SDK kill while descendants survive a failed group KILL', async () => {
-    const fake = new FakeSandbox()
-    fake.trapsTerm = true
-    fake.sdkKillStops = false
-    fake.signalErrors.push(undefined, new Error('KILL transport failed'))
-    const handle = testHandle(runtime(fake), spec({ graceMs: 1 }), '/runtime/optimistic-sdk-kill')
-    await flush()
-
-    handle.terminate()
-    await expect(handle.waitForExit()).rejects.toThrow('remained live after force termination')
-    expect(fake.alive).toBe(true)
-
-    fake.sdkKillStops = true
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-  })
-})
-
-describe('E2BSubprocessRuntime', () => {
-  async function service(
-    fake = new FakeSandbox(),
-    providedRuntime: E2BRuntime = runtime(fake),
-  ): Promise<{ ctx: Context; fiber: Awaited<ReturnType<Context['plugin']>> }> {
-    const ctx = new Context()
-    ctx.provide('e2b', providedRuntime)
-    const fiber = await ctx.plugin(E2BSubprocessRuntime)
-    return { ctx, fiber }
-  }
-
-  it('registers handles and disposal terminates and joins live remote groups regardless of sandbox policy', async () => {
-    const fake = new FakeSandbox()
-    fake.trapsTerm = true
-    const { ctx, fiber } = await service(fake)
-    const handle = ctx.subprocess.spawn(spec({ graceMs: 1 }))
-    await flush()
-    await fiber.dispose()
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-    expect(fake.alive).toBe(false)
-  })
-
-  it('awaits SDK settlement after the remote process group becomes quiescent', async () => {
-    const fake = new FakeSandbox()
-    fake.trapsTerm = true
-    const { ctx, fiber } = await service(fake)
-    const handle = ctx.subprocess.spawn(spec())
-    await flush()
-    fake.alive = false
-
-    let disposed = false
-    const disposing = fiber.dispose().then(() => { disposed = true })
-    await flush()
-    expect(disposed).toBe(false)
-
-    fake.finish()
-    await disposing
-    await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
-  })
-
-  it('reports a failed termination transaction from disposal instead of waiting on done', async () => {
-    const fake = new FakeSandbox()
-    fake.signalErrors.push(new Error('TERM transport failed'), new Error('KILL transport failed'))
-    fake.handle.killError = new Error('SDK kill failed')
-    const { ctx, fiber } = await service(fake)
-    const handle = ctx.subprocess.spawn(spec({ graceMs: 1 }))
-    await flush()
-
-    await expect(fiber.dispose()).resolves.toBeUndefined()
-    await expect(handle.waitForExit()).rejects.toThrow('remained live after force termination')
-
-    fake.handle.killError = undefined
-    handle.terminate()
-    await expect(handle.waitForExit()).resolves.toBe(true)
-    await expect(handle.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-  })
-
-  it('aggregates sibling cleanup failures instead of reporting only the first', async () => {
-    const { ctx, fiber } = await service()
-    const disposalErrors: unknown[] = []
-    ctx.logger.error = ((error: unknown) => { disposalErrors.push(error) }) as typeof ctx.logger.error
-    const first = {
-      terminate: vi.fn(),
-      waitForExit: vi.fn(async () => { throw new Error('first cleanup failed') }),
-      done: Promise.resolve({ exitCode: 0, signal: null }),
-    } as unknown as E2BSubprocessHandle
-    const second = {
-      terminate: vi.fn(),
-      waitForExit: vi.fn(async () => { throw new Error('second cleanup failed') }),
-      done: Promise.resolve({ exitCode: 0, signal: null }),
-    } as unknown as E2BSubprocessHandle
-    const live = (ctx.subprocess as unknown as { live: Set<E2BSubprocessHandle> }).live
-    live.add(first)
-    live.add(second)
-
-    await fiber.dispose()
-    const failure = disposalErrors[0]
-    expect(failure).toBeInstanceOf(AggregateError)
-    if (!(failure instanceof AggregateError)) throw new Error('expected AggregateError')
-    expect(failure.errors.map(error => (error as Error).message).sort()).toEqual([
-      'first cleanup failed',
-      'second cleanup failed',
-    ])
-  })
-
-  it('waits for every owned cleanup before reporting a disposal failure', async () => {
-    const { ctx, fiber } = await service()
-    const failed = {
-      terminate: vi.fn(),
-      waitForExit: vi.fn(async () => { throw new Error('cleanup failed') }),
-      done: Promise.resolve({ exitCode: 0, signal: null }),
-    } as unknown as E2BSubprocessHandle
-    let finishCleanup!: () => void
-    const cleanup = new Promise<boolean>((resolve) => {
-      finishCleanup = () => { resolve(true) }
-    })
-    const draining = {
-      terminate: vi.fn(),
-      waitForExit: vi.fn(() => cleanup),
-      done: Promise.resolve({ exitCode: 0, signal: null }),
-    } as unknown as E2BSubprocessHandle
-    const live = (ctx.subprocess as unknown as { live: Set<E2BSubprocessHandle> }).live
-    live.add(failed)
-    live.add(draining)
-
-    let disposed = false
-    const disposing = fiber.dispose().then(() => { disposed = true })
-    await flush()
-    expect(disposed).toBe(false)
-    finishCleanup()
-    await disposing
-    expect(live).toEqual(new Set([failed]))
-  })
-
-  it('releases naturally settled handles before later service disposal', async () => {
-    const fake = new FakeSandbox()
-    const { ctx, fiber } = await service(fake)
-    const handle = ctx.subprocess.spawn(spec())
-    await flush()
-    fake.finish()
-    await handle.done
-    await flush()
-    const signalsBefore = fake.commandsSeen.filter(command => command.startsWith('kill -')).length
-    await fiber.dispose()
-    expect(fake.commandsSeen.filter(command => command.startsWith('kill -')).length).toBe(signalsBefore)
-  })
-
-  it('contains a release liveness failure and retries quiescence during disposal', async () => {
-    const fake = new FakeSandbox()
-    let calls = 0
-    const reconnecting = runtime(fake, async () => {
-      calls += 1
-      if (calls === 2) throw new Error('transient liveness failure')
-      return fake.sandbox
-    })
-    const { ctx, fiber } = await service(fake, reconnecting)
-    const handle = ctx.subprocess.spawn(spec())
-    await flush()
-    fake.finish()
-    await handle.done
-    await flush()
-    await fiber.dispose()
-    expect(calls).toBeGreaterThanOrEqual(3)
-  })
-
-  it('contains spawn rejection while disposal is joining the pending handle', async () => {
-    const fake = new FakeSandbox()
-    fake.deferStart()
-    fake.backgroundError = new Error('start failed during disposal')
-    const { ctx, fiber } = await service(fake)
-    const subprocess = ctx.subprocess
-    const handle = subprocess.spawn(spec())
-    await vi.waitFor(() => { expect(fake.startOptions).toBeDefined() })
-    const disposing = fiber.dispose()
-    await flush()
-    expect(() => subprocess.spawn(spec())).toThrow('service is disposing')
-    fake.releaseStart()
-    await expect(disposing).resolves.toBeUndefined()
-    await expect(handle.done).rejects.toThrow('start failed during disposal')
-  })
-
-  it('validates synchronous spawn preconditions before handle or remote work', async () => {
-    const fake = new FakeSandbox()
-    const getSandbox = vi.fn(async () => fake.sandbox)
-    const { ctx } = await service(fake, runtime(fake, getSandbox))
-    const live = (ctx.subprocess as unknown as { live: Set<E2BSubprocessHandle> }).live
-    expect(() => ctx.subprocess.spawn(spec({ argv: [] }))).toThrow(/non-empty program/)
-    expect(() => ctx.subprocess.spawn(spec({ signal: AbortSignal.abort('stop') })))
-      .toThrow(new Error('aborted before spawn: stop'))
-    expect(() => ctx.subprocess.spawn(spec({ signal: AbortSignal.abort(null) })))
-      .toThrow(new Error('aborted before spawn: aborted'))
-    const throwingReason = { toString: () => { throw new Error('caller reason escaped') } }
-    expect(() => ctx.subprocess.spawn(spec({ signal: AbortSignal.abort(throwingReason) })))
-      .toThrow(new Error('aborted before spawn: aborted'))
-
-    for (const invalid of [
-      spec({ argv: ['bash\0'] }),
-      spec({ argv: ['bash', 'bad\0arg'] }),
-      spec({ cwd: 'bad\0cwd' }),
-      spec({ env: { REMOVED: undefined, 'BAD\0KEY': 'value' } }),
-      spec({ env: { BAD: 'bad\0value' } }),
-    ]) {
-      let thrown: unknown
-      try {
-        ctx.subprocess.spawn(invalid)
-      } catch (error) {
-        thrown = error
-      }
-      expect(thrown).toMatchObject({ name: 'TypeError', code: 'ERR_INVALID_ARG_VALUE' })
-    }
-    expect(getSandbox).not.toHaveBeenCalled()
-    expect(live).toEqual(new Set())
-    expect(fake.directories).toEqual([])
-  })
-})

+ 0 - 942
packages/e2b/subprocess-e2b/tests/terminal.spec.ts

@@ -1,942 +0,0 @@
-import { Buffer } from 'node:buffer'
-import { once } from 'node:events'
-import { Context } from '@deepseek-ai/cordis'
-import { describe, expect, it, vi } from 'vitest'
-import {
-  CommandExitError,
-  FileNotFoundError,
-  SandboxNotFoundError,
-  type CommandHandle,
-  type CommandResult,
-  type Sandbox,
-} from '@deepseek-ai/dsh-e2b'
-import type E2BRuntime from '@deepseek-ai/dsh-e2b'
-import type { SubprocessTerminalSpawnSpec } from '@deepseek-ai/dsh-subprocess'
-import E2BSubprocessRuntime from '@deepseek-ai/dsh-subprocess-e2b'
-import { spawnE2BTerminal } from '../src/terminal.ts'
-
-function commandError(exitCode: number): CommandExitError {
-  return new CommandExitError({ exitCode, stdout: '', stderr: '', error: `exit ${exitCode}` })
-}
-
-interface CommandOptions {
-  signal?: AbortSignal
-  cwd?: string
-  envs?: Record<string, string>
-}
-
-class FakeTerminalCommandHandle {
-  pid = 123
-  disconnects = 0
-  sdkKills = 0
-  disconnectError: unknown
-  sdkKillError: unknown
-  waitError: unknown
-  settleOnSdkKill = true
-  private readonly result = Promise.withResolvers<CommandResult>()
-  private settled = false
-
-  wait(): Promise<CommandResult> {
-    if (this.waitError !== undefined) throw this.waitError
-    return this.result.promise
-  }
-
-  async disconnect(): Promise<void> {
-    this.disconnects += 1
-    if (this.disconnectError !== undefined) throw this.disconnectError
-  }
-
-  async kill(): Promise<boolean> {
-    this.sdkKills += 1
-    if (this.sdkKillError !== undefined) {
-      const error = this.sdkKillError
-      if (this.settleOnSdkKill) this.fail(137)
-      throw error
-    }
-    if (this.settleOnSdkKill) this.fail(137)
-    return true
-  }
-
-  succeed(exitCode = 0): void {
-    if (this.settled) return
-    this.settled = true
-    this.result.resolve({ exitCode, stdout: '', stderr: '' })
-  }
-
-  fail(exitCode: number): void {
-    if (this.settled) return
-    this.settled = true
-    this.result.reject(commandError(exitCode))
-  }
-
-  crash(error: unknown): void {
-    if (this.settled) return
-    this.settled = true
-    this.result.reject(error)
-  }
-
-  asHandle(): CommandHandle {
-    return this as unknown as CommandHandle
-  }
-}
-
-class FakeTerminalSandbox {
-  readonly handle = new FakeTerminalCommandHandle()
-  readonly commands: string[] = []
-  readonly commandOptions: CommandOptions[] = []
-  readonly inputs: Array<{ pid: number; data: Buffer }> = []
-  readonly removed: string[] = []
-  readonly directories: string[] = []
-  readonly writes = new Map<string, string>()
-  createOptions: Parameters<Sandbox['pty']['create']>[0] | undefined
-  ambient = 'KEEP=visible\0UNICODE=你好\0NPM_TOKEN=secret\0DSH_STALE=old\0BROKEN\0=bad\0'
-  sessionId = '123\n'
-  foreground = '456\n'
-  groups = [123]
-  zombieGroups: number[] = []
-  createError: unknown
-  writeError: unknown
-  sendError: unknown
-  commandFailure: unknown
-  makeDirRequest: ((signal: AbortSignal | undefined) => Promise<void>) | undefined
-  sendInputRequest: ((signal: AbortSignal | undefined) => Promise<void>) | undefined
-  foregroundRequest: ((signal: AbortSignal | undefined) => Promise<void>) | undefined
-  signalRequest: ((signal: AbortSignal | undefined) => Promise<void>) | undefined
-  sessionGroupsFailure: unknown
-  foregroundFailure: unknown
-  termFailure: unknown
-  removeError: unknown
-  clearOnTerm = true
-  clearOnKill = true
-  resolvedExecutable = '/usr/bin/node\n'
-  requestedOutput = 'requested-shell$ '
-  emitOutputMarker = true
-  afterSessionLookup: (() => void) | undefined
-  private createGate: Promise<undefined> | undefined
-  private releaseCreateGate: (() => void) | undefined
-
-  deferCreate(): void {
-    const gate = Promise.withResolvers<undefined>()
-    this.createGate = gate.promise
-    this.releaseCreateGate = () => { gate.resolve(undefined) }
-  }
-
-  releaseCreate(): void {
-    this.releaseCreateGate?.()
-  }
-
-  readonly sandbox = {
-    files: {
-      makeDir: async (path: string, options?: CommandOptions): Promise<boolean> => {
-        this.directories.push(path)
-        await this.makeDirRequest?.(options?.signal)
-        options?.signal?.throwIfAborted()
-        return true
-      },
-      write: async (files: Array<{ path: string; data: string }>): Promise<object[]> => {
-        for (const file of files) this.writes.set(file.path, file.data)
-        if (this.writeError !== undefined) throw this.writeError
-        return files.map(() => ({}))
-      },
-      remove: async (path: string): Promise<void> => {
-        this.removed.push(path)
-        if (this.removeError !== undefined) throw this.removeError
-      },
-    },
-    commands: {
-      run: async (command: string, options?: CommandOptions): Promise<CommandResult> => {
-        this.commands.push(command)
-        if (options !== undefined) this.commandOptions.push(options)
-        options?.signal?.throwIfAborted()
-        if (this.commandFailure !== undefined) {
-          const error = this.commandFailure
-          this.commandFailure = undefined
-          throw error
-        }
-        if (command.includes('env -0 | base64')) {
-          return {
-            exitCode: 0,
-            stdout: ['/home/user', this.ambient].map(value => Buffer.from(value).toString('base64')).join('\n'),
-            stderr: '',
-          }
-        }
-        if (command.includes('command -v -- ')) {
-          return { exitCode: 0, stdout: this.resolvedExecutable, stderr: '' }
-        }
-        if (command.startsWith('ps -o sid=')) {
-          this.afterSessionLookup?.()
-          return { exitCode: 0, stdout: this.sessionId, stderr: '' }
-        }
-        if (command.startsWith('ps -o tpgid=')) {
-          await this.foregroundRequest?.(options?.signal)
-          options?.signal?.throwIfAborted()
-          if (this.foregroundFailure !== undefined) throw this.foregroundFailure
-          return { exitCode: 0, stdout: this.foreground, stderr: '' }
-        }
-        if (command.startsWith('set -o pipefail; ps -eo sid=')) {
-          if (this.sessionGroupsFailure !== undefined) throw this.sessionGroupsFailure
-          const groups = command.includes('stat=') && command.includes('$3 !~ /^[ZXx]/')
-            ? this.groups
-            : [...this.groups, ...this.zombieGroups]
-          return { exitCode: 0, stdout: groups.map(group => `${group}\n`).join(''), stderr: '' }
-        }
-        if (command.startsWith('kill -TERM -- ')) {
-          if (this.termFailure !== undefined) throw this.termFailure
-          if (this.clearOnTerm) {
-            this.groups = []
-            this.handle.fail(143)
-          }
-        }
-        if (command.startsWith('kill -INT -- ')) {
-          await this.signalRequest?.(options?.signal)
-          options?.signal?.throwIfAborted()
-        }
-        if (command.startsWith('kill -KILL -- ') && this.clearOnKill) this.groups = []
-        return { exitCode: 0, stdout: '', stderr: '' }
-      },
-    },
-    pty: {
-      create: async (options: Parameters<Sandbox['pty']['create']>[0]): Promise<CommandHandle> => {
-        this.createOptions = options
-        if (this.createError !== undefined) throw this.createError
-        await this.createGate
-        options.signal?.throwIfAborted()
-        await options.onData(Buffer.from('buffered banner\n'))
-        return this.handle.asHandle()
-      },
-      sendInput: async (pid: number, data: Uint8Array, options?: { signal?: AbortSignal }): Promise<void> => {
-        options?.signal?.throwIfAborted()
-        await this.sendInputRequest?.(options?.signal)
-        options?.signal?.throwIfAborted()
-        this.inputs.push({ pid, data: Buffer.from(data) })
-        if (this.sendError !== undefined) throw this.sendError
-        if (this.emitOutputMarker && Buffer.from(data).includes(Buffer.from('runner.bash'))) {
-          const marker = [...this.writes].find(([path]) => path.endsWith('/output-marker'))?.[1]
-          const onData = this.createOptions?.onData
-          if (marker !== undefined && onData !== undefined) {
-            await onData(Buffer.from(Buffer.from(data).toString().replace(/\r$/, '\r\n')))
-            const split = Math.floor(marker.length / 2)
-            await onData(Buffer.from(marker.slice(0, split)))
-            await onData(Buffer.from(marker.slice(split)))
-            await onData(Buffer.from(this.requestedOutput))
-          }
-        }
-      },
-    },
-  } as unknown as Sandbox
-}
-
-function runtime(fake: FakeTerminalSandbox): E2BRuntime {
-  return {
-    cwd: '/workspace',
-    runtimeRoot: '/workspace/.dsh-e2b',
-    getSandbox: async () => fake.sandbox,
-  } as unknown as E2BRuntime
-}
-
-function spec(overrides: Partial<SubprocessTerminalSpawnSpec> = {}): SubprocessTerminalSpawnSpec {
-  return {
-    argv: ['/bin/bash', '--noprofile', '--norc'],
-    cwd: '/workspace',
-    rows: 24,
-    cols: 80,
-    graceMs: 5,
-    env: { TERM: 'dumb', DSH_SESSION_ID: 'owner', TOKEN_EXPLICIT: 'kept' },
-    ...overrides,
-  }
-}
-
-function holdRequestUntilAbort(started: PromiseWithResolvers<AbortSignal>) {
-  return async (signal: AbortSignal | undefined): Promise<void> => {
-    if (signal === undefined) throw new Error('expected an operation signal')
-    signal.throwIfAborted()
-    started.resolve(signal)
-    await new Promise<void>((_resolve, reject) => {
-      signal.addEventListener('abort', () => {
-        reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)))
-      }, { once: true })
-    })
-  }
-}
-
-/** Spawn the terminal under test with the config default the service would pass. */
-function testSpawn(
-  runtime: Parameters<typeof spawnE2BTerminal>[0],
-  spec: Parameters<typeof spawnE2BTerminal>[1],
-  stateDir: string,
-  pollMs = 20,
-): ReturnType<typeof spawnE2BTerminal> {
-  return spawnE2BTerminal(runtime, spec, stateDir, pollMs)
-}
-
-describe('E2B terminal allocation', () => {
-  it('hides bootstrap-shell bytes and preserves requested-shell bytes across the output boundary', async () => {
-    const fake = new FakeTerminalSandbox()
-    const terminal = await testSpawn(runtime(fake), spec(), '/runtime/terminal-one')
-    let output = ''
-    terminal.output.on('data', (chunk) => { output += String(chunk) })
-    await new Promise(resolve => setTimeout(resolve, 0))
-
-    expect(output).toBe('requested-shell$ ')
-    expect(output).not.toContain('buffered banner')
-    expect(output).not.toContain('runner.bash')
-    expect(fake.createOptions).toMatchObject({ rows: 24, cols: 80, cwd: '/workspace', timeoutMs: 0 })
-    const controlEnvs = fake.createOptions?.envs
-    expect(controlEnvs?.HOME).toMatch(/^\/\.dsh-e2b-control-/)
-    expect(controlEnvs).toEqual({
-      TERM: 'dumb',
-      NPM_TOKEN: '',
-      DSH_STALE: '',
-      HOME: controlEnvs?.HOME,
-    })
-    expect(fake.inputs[0]?.data.toString()).toContain("exec /bin/bash '/runtime/terminal-one/runner.bash'")
-    expect(fake.writes.get('/runtime/terminal-one/environment')).toContain('KEEP=visible\0')
-    expect(fake.writes.get('/runtime/terminal-one/environment')).toContain('UNICODE=你好\0')
-    expect(fake.writes.get('/runtime/terminal-one/environment')).toContain('TOKEN_EXPLICIT=kept\0')
-    expect(fake.writes.get('/runtime/terminal-one/environment')).not.toContain('secret')
-    expect(fake.writes.get('/runtime/terminal-one/environment')).not.toContain('DSH_STALE')
-    expect(fake.writes.get('/runtime/terminal-one/argv')).toBe('/bin/bash\0--noprofile\0--norc\0')
-    const marker = fake.writes.get('/runtime/terminal-one/output-marker') ?? ''
-    expect(marker).toMatch(/^dsh-e2b-bootstrap:/)
-    expect(fake.inputs[0]?.data.toString()).not.toContain(marker)
-    const runner = fake.writes.get('/runtime/terminal-one/runner.bash') ?? ''
-    expect(runner).toContain('if (( ${#dsh_argv[@]} == 0 )); then')
-    expect(runner).toContain('printf \'%s\' "$dsh_output_marker"')
-    expect(runner).toContain('exec env -i -- "${dsh_env[@]}" "${dsh_argv[@]}"')
-    expect(runner).not.toContain('\u007f')
-    terminal.output.destroy()
-    await fake.createOptions?.onData(Buffer.from('late bootstrap callback'))
-    expect(output).toBe('requested-shell$ ')
-
-    await terminal.write('echo ok\r')
-    expect(fake.inputs.at(-1)?.data.toString()).toBe('echo ok\r')
-    await expect(terminal.inspectForeground()).resolves.toEqual({ processGroupId: 456, inputWaiting: false })
-    await expect(terminal.signalForeground('SIGINT')).resolves.toBe(456)
-    expect(fake.commands).toContain('kill -INT -- -456')
-
-    const terminated = terminal.terminate()
-    await expect(terminal.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-    await terminated
-    expect(fake.handle.disconnects).toBe(1)
-    expect(fake.removed).toContain('/runtime/terminal-one')
-  })
-
-  it('inherits only safe ambient values and limits the allocation signal to setup', async () => {
-    const fake = new FakeTerminalSandbox()
-    const controller = new AbortController()
-    const terminal = await testSpawn(
-      runtime(fake),
-      spec({ env: undefined, signal: controller.signal }),
-      '/runtime/abort-live',
-    )
-    const environment = fake.writes.get('/runtime/abort-live/environment') ?? ''
-    expect(environment).toContain('KEEP=visible\0')
-    expect(environment).not.toContain('secret')
-    expect(environment).not.toContain('DSH_STALE')
-
-    controller.abort(new Error('stop'))
-    await terminal.write('still live\r')
-    expect(fake.inputs.at(-1)?.data.toString()).toBe('still live\r')
-    await terminal.terminate()
-    await expect(terminal.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-  })
-
-  it('publishes the PTY handle before honoring allocation cancellation', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.deferCreate()
-    const controller = new AbortController()
-    const spawning = testSpawn(
-      runtime(fake),
-      spec({ signal: controller.signal }),
-      '/runtime/allocation-cancel',
-    )
-    await vi.waitFor(() => { expect(fake.createOptions).toBeDefined() })
-
-    controller.abort(new Error('allocation cancelled'))
-    fake.releaseCreate()
-    await expect(spawning).rejects.toThrow('allocation cancelled')
-    expect(fake.createOptions?.signal).toBeUndefined()
-    expect(fake.groups).toEqual([])
-    expect(fake.handle.disconnects).toBe(1)
-  })
-
-  it('rejects malformed environment and argv values before PTY allocation', async () => {
-    const invalidName = new FakeTerminalSandbox()
-    await expect(testSpawn(runtime(invalidName), spec({ env: { 'BAD=NAME': 'x' } }), '/runtime/name'))
-      .rejects.toThrow('environment entries')
-    expect(invalidName.createOptions).toBeUndefined()
-
-    const invalidValue = new FakeTerminalSandbox()
-    await expect(testSpawn(runtime(invalidValue), spec({ env: { BAD: 'x\0y' } }), '/runtime/value'))
-      .rejects.toThrow('environment entries')
-
-    const invalidArg = new FakeTerminalSandbox()
-    await expect(testSpawn(runtime(invalidArg), spec({ argv: ['/bin/bash', 'x\0y'] }), '/runtime/argv'))
-      .rejects.toThrow('argv must not contain NUL')
-  })
-
-  it('cleans malformed handles, bootstrap failures, and readiness failures', async () => {
-    const failedState = new FakeTerminalSandbox()
-    failedState.writeError = new Error('state write failed')
-    await expect(testSpawn(runtime(failedState), spec(), '/runtime/state-write'))
-      .rejects.toThrow('state write failed')
-    expect(failedState.writes.get('/runtime/state-write/environment')).toContain('KEEP=visible\0')
-    expect(failedState.removed).toContain('/runtime/state-write')
-    expect(failedState.createOptions).toBeUndefined()
-
-    const stateAlreadyGone = new FakeTerminalSandbox()
-    stateAlreadyGone.writeError = new Error('state write failed after external cleanup')
-    stateAlreadyGone.removeError = new FileNotFoundError('state already gone')
-    await expect(testSpawn(runtime(stateAlreadyGone), spec(), '/runtime/state-gone'))
-      .rejects.toThrow('state write failed after external cleanup')
-
-    const invalidPid = new FakeTerminalSandbox()
-    invalidPid.handle.pid = 0
-    await expect(testSpawn(runtime(invalidPid), spec(), '/runtime/invalid-pid'))
-      .rejects.toThrow('invalid terminal pid 0')
-    expect(invalidPid.handle.sdkKills).toBe(1)
-    expect(invalidPid.removed).toContain('/runtime/invalid-pid')
-
-    const failedInput = new FakeTerminalSandbox()
-    failedInput.sendError = new Error('bootstrap failed')
-    await expect(testSpawn(runtime(failedInput), spec(), '/runtime/input'))
-      .rejects.toThrow('bootstrap failed')
-    expect(failedInput.commands).toContain('kill -TERM -- -123')
-    expect(failedInput.groups).toEqual([])
-
-    const invalidSession = new FakeTerminalSandbox()
-    invalidSession.sessionId = 'not-a-session\n'
-    invalidSession.clearOnTerm = false
-    await expect(testSpawn(runtime(invalidSession), spec(), '/runtime/session'))
-      .rejects.toThrow('cannot resolve process session')
-    expect(invalidSession.commands).toContain('kill -TERM -- -123')
-    expect(invalidSession.commands).toContain('kill -KILL -- -123')
-    expect(invalidSession.groups).toEqual([])
-    expect(invalidSession.handle.sdkKills).toBe(1)
-    const lateData = invalidSession.createOptions?.onData
-    if (lateData === undefined) throw new Error('missing captured terminal callback')
-    expect(lateData(Buffer.from('late bytes'))).toBeUndefined()
-
-    const termFailed = new FakeTerminalSandbox()
-    termFailed.sendError = new Error('bootstrap failed')
-    termFailed.termFailure = new Error('TERM transport failed')
-    await expect(testSpawn(runtime(termFailed), spec(), '/runtime/term-failed'))
-      .rejects.toThrow('bootstrap failed')
-    expect(termFailed.commands).toContain('kill -KILL -- -123')
-    expect(termFailed.handle.sdkKills).toBe(1)
-
-    const uninspectable = new FakeTerminalSandbox()
-    uninspectable.sendError = new Error('bootstrap failed')
-    uninspectable.sessionGroupsFailure = 'session enumeration failed'
-    uninspectable.handle.sdkKillError = new Error('PTY kill failed')
-    let uninspectableFailure: unknown
-    try {
-      await testSpawn(runtime(uninspectable), spec(), '/runtime/uninspectable')
-    } catch (error: unknown) {
-      uninspectableFailure = error
-    }
-    expect(uninspectableFailure).toBeInstanceOf(AggregateError)
-    expect(uninspectable.handle.sdkKills).toBe(1)
-
-    const survivingGroups = new FakeTerminalSandbox()
-    survivingGroups.sendError = new Error('bootstrap failed')
-    survivingGroups.clearOnTerm = false
-    survivingGroups.clearOnKill = false
-    await expect(testSpawn(runtime(survivingGroups), spec({ graceMs: 1 }), '/runtime/surviving-groups'))
-      .rejects.toThrow('bootstrap failed')
-
-    const survivingPid = new FakeTerminalSandbox()
-    survivingPid.sendError = new Error('bootstrap failed')
-    survivingPid.groups = []
-    survivingPid.handle.settleOnSdkKill = false
-    await expect(testSpawn(runtime(survivingPid), spec({ graceMs: 1 }), '/runtime/surviving-pid'))
-      .rejects.toThrow('bootstrap failed')
-
-    const waitFailed = new FakeTerminalSandbox()
-    waitFailed.handle.waitError = new Error('wait failed')
-    waitFailed.handle.settleOnSdkKill = false
-    waitFailed.handle.sdkKillError = new Error('kill failed')
-    await expect(testSpawn(runtime(waitFailed), spec(), '/runtime/wait-failed'))
-      .rejects.toThrow('wait failed')
-    expect(waitFailed.handle.sdkKills).toBe(1)
-
-    const cleanupFailed = new FakeTerminalSandbox()
-    cleanupFailed.handle.pid = 0
-    cleanupFailed.handle.sdkKillError = new Error('kill transport failed')
-    cleanupFailed.removeError = new Error('remove transport failed')
-    await expect(testSpawn(runtime(cleanupFailed), spec(), '/runtime/cleanup-failed'))
-      .rejects.toThrow('invalid terminal pid 0')
-
-    const expiredDuringRollback = new FakeTerminalSandbox()
-    expiredDuringRollback.sendError = new Error('bootstrap failed before timeout')
-    expiredDuringRollback.groups = []
-    expiredDuringRollback.handle.settleOnSdkKill = false
-    expiredDuringRollback.handle.sdkKillError = new SandboxNotFoundError('sandbox expired')
-    expiredDuringRollback.removeError = new SandboxNotFoundError('sandbox expired')
-    await expect(testSpawn(runtime(expiredDuringRollback), spec(), '/runtime/expired-rollback'))
-      .rejects.toThrow('bootstrap failed before timeout')
-    expect(expiredDuringRollback.handle.sdkKills).toBe(1)
-
-    const expiredBeforeSdkRollback = new FakeTerminalSandbox()
-    expiredBeforeSdkRollback.handle.waitError = new Error('wait failed after timeout')
-    expiredBeforeSdkRollback.handle.sdkKillError = new SandboxNotFoundError('sandbox expired')
-    expiredBeforeSdkRollback.handle.settleOnSdkKill = false
-    await expect(testSpawn(runtime(expiredBeforeSdkRollback), spec(), '/runtime/expired-sdk-rollback'))
-      .rejects.toThrow('wait failed after timeout')
-
-    const missingDuringDisconnect = new FakeTerminalSandbox()
-    missingDuringDisconnect.sendError = new Error('bootstrap failed before disconnect')
-    missingDuringDisconnect.handle.disconnectError = new SandboxNotFoundError('sandbox expired')
-    await expect(testSpawn(runtime(missingDuringDisconnect), spec(), '/runtime/missing-disconnect'))
-      .rejects.toThrow('bootstrap failed before disconnect')
-
-    const failedDisconnect = new FakeTerminalSandbox()
-    failedDisconnect.sendError = new Error('bootstrap failed with disconnect failure')
-    failedDisconnect.handle.disconnectError = new Error('disconnect transport failed')
-    await expect(testSpawn(runtime(failedDisconnect), spec(), '/runtime/failed-disconnect'))
-      .rejects.toThrow('bootstrap failed with disconnect failure')
-  })
-
-  it('propagates setup cancellation and provider failures', async () => {
-    const aborted = new FakeTerminalSandbox()
-    await expect(testSpawn(runtime(aborted), spec({ signal: AbortSignal.abort(new Error('stop')) }), '/runtime/abort'))
-      .rejects.toThrow('stop')
-
-    const createFailed = new FakeTerminalSandbox()
-    createFailed.createError = new Error('create failed')
-    await expect(testSpawn(runtime(createFailed), spec(), '/runtime/create'))
-      .rejects.toThrow('create failed')
-
-  })
-
-  it('bounds a missing bootstrap-output boundary by process exit or cancellation', async () => {
-    const exited = new FakeTerminalSandbox()
-    exited.emitOutputMarker = false
-    const exiting = testSpawn(runtime(exited), spec(), '/runtime/missing-output-boundary')
-    await vi.waitFor(() => { expect(exited.inputs).toHaveLength(1) })
-    exited.handle.succeed(0)
-    await expect(exiting).rejects.toThrow('terminal exited before publishing its output boundary')
-
-    const cancelled = new FakeTerminalSandbox()
-    cancelled.emitOutputMarker = false
-    const controller = new AbortController()
-    const cancelling = testSpawn(
-      runtime(cancelled),
-      spec({ signal: controller.signal }),
-      '/runtime/cancel-output-boundary',
-    )
-    await vi.waitFor(() => { expect(cancelled.inputs).toHaveLength(1) })
-    await new Promise(resolve => setTimeout(resolve, 0))
-    controller.abort(new Error('cancel output boundary'))
-    await expect(cancelling).rejects.toThrow('cancel output boundary')
-  })
-})
-
-describe('E2B terminal lifecycle', () => {
-  it('aborts and joins in-flight terminal operations before cleanup', async () => {
-    const fake = new FakeTerminalSandbox()
-    const terminal = await testSpawn(runtime(fake), spec(), '/runtime/in-flight-operations')
-    const writeStarted = Promise.withResolvers<AbortSignal>()
-    const inspectStarted = Promise.withResolvers<AbortSignal>()
-    const signalStarted = Promise.withResolvers<AbortSignal>()
-    fake.sendInputRequest = holdRequestUntilAbort(writeStarted)
-    let foregroundRequests = 0
-    fake.foregroundRequest = async (signal) => {
-      foregroundRequests += 1
-      if (foregroundRequests === 1) await holdRequestUntilAbort(inspectStarted)(signal)
-    }
-    let signalCompleted = false
-    fake.signalRequest = async (operationSignal) => {
-      await holdRequestUntilAbort(signalStarted)(operationSignal)
-      signalCompleted = true
-    }
-    const write = terminal.write('late input')
-    const inspect = terminal.inspectForeground()
-    await Promise.all([writeStarted.promise, inspectStarted.promise])
-    const signal = terminal.signalForeground('SIGINT')
-    await signalStarted.promise
-
-    const terminating = terminal.terminate()
-    await expect(write).rejects.toThrow('terminal is terminating')
-    await expect(inspect).rejects.toThrow('terminal is terminating')
-    await expect(signal).rejects.toThrow('terminal is terminating')
-    await terminating
-    expect(signalCompleted).toBe(false)
-    expect(fake.inputs).toHaveLength(1)
-    const commandCount = fake.commands.length
-    await expect(terminal.write('after termination')).rejects.toThrow('terminal is terminating')
-    await expect(terminal.inspectForeground()).rejects.toThrow('terminal is terminating')
-    await expect(terminal.signalForeground('SIGINT')).rejects.toThrow('terminal is terminating')
-    expect(fake.commands).toHaveLength(commandCount)
-  })
-
-  it('maps ordinary exits, closes output, and reports an absent foreground after exit', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.groups = []
-    const terminal = await testSpawn(runtime(fake), spec(), '/runtime/natural')
-    terminal.output.resume()
-    const ended = once(terminal.output, 'end')
-    fake.handle.succeed(7)
-    await expect(terminal.done).resolves.toEqual({ exitCode: 7, signal: null })
-    await ended
-    await expect(terminal.write('late')).rejects.toThrow('exited')
-    fake.foregroundFailure = commandError(1)
-    await expect(terminal.inspectForeground()).resolves.toBeUndefined()
-    await expect(terminal.signalForeground('SIGINT')).rejects.toThrow('cannot resolve foreground process group')
-    await terminal.terminate()
-  })
-
-  it.each([
-    [7, { exitCode: 7, signal: null }],
-    [143, { exitCode: 143, signal: null }],
-    [255, { exitCode: 255, signal: null }],
-  ] as const)('classifies an unrequested command exit %i', async (exitCode, expected) => {
-    const fake = new FakeTerminalSandbox()
-    fake.groups = []
-    const terminal = await testSpawn(runtime(fake), spec(), `/runtime/exit-${exitCode}`)
-    fake.handle.fail(exitCode)
-    await expect(terminal.done).resolves.toEqual(expected)
-    await terminal.terminate()
-  })
-
-  it('treats a terminal session containing only zombies as quiescent', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.groups = []
-    fake.zombieGroups = [123]
-    const terminal = await testSpawn(runtime(fake), spec(), '/runtime/zombie-session')
-
-    fake.handle.succeed(0)
-    await expect(terminal.done).resolves.toEqual({ exitCode: 0, signal: null })
-    await terminal.terminate()
-    expect(fake.commands).toContain(
-      "set -o pipefail; ps -eo sid=,pgid=,stat= | awk '$1 == 123 && $3 !~ /^[ZXx]/ { print $2 }'",
-    )
-  })
-
-  it('treats a timeout-killed sandbox as quiescent during terminal cleanup', async () => {
-    const fake = new FakeTerminalSandbox()
-    const terminal = await testSpawn(runtime(fake), spec(), '/runtime/expired-sandbox')
-    fake.sessionGroupsFailure = new SandboxNotFoundError('sandbox expired')
-    fake.handle.succeed(0)
-
-    await expect(terminal.done).resolves.toEqual({ exitCode: 0, signal: null })
-    await terminal.terminate()
-  })
-
-  it('treats sandbox disappearance during PTY kill as quiescent', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.groups = []
-    fake.handle.settleOnSdkKill = false
-    fake.handle.sdkKillError = new SandboxNotFoundError('sandbox expired')
-    const terminal = await testSpawn(runtime(fake), spec({ graceMs: 1 }), '/runtime/expired-pty-kill')
-
-    await terminal.terminate()
-    expect(fake.handle.sdkKills).toBe(1)
-  })
-
-  it('propagates a non-missing PTY kill failure', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.groups = []
-    fake.handle.settleOnSdkKill = false
-    fake.handle.sdkKillError = new Error('PTY kill transport failed')
-    const terminal = await testSpawn(runtime(fake), spec({ graceMs: 1 }), '/runtime/failed-pty-kill')
-
-    await expect(terminal.terminate()).rejects.toThrow('PTY kill transport failed')
-    fake.handle.sdkKillError = undefined
-    fake.handle.succeed(0)
-    await terminal.done
-    await terminal.terminate()
-  })
-
-  it.each([
-    ['accepts sandbox loss', new SandboxNotFoundError('sandbox expired'), true],
-    ['propagates another failure', new Error('disconnect failed'), false],
-  ] as const)('%s while disconnecting a settled terminal', async (_label, failure, accepted) => {
-    const fake = new FakeTerminalSandbox()
-    const terminal = await testSpawn(runtime(fake), spec(), `/runtime/disconnect-${accepted}`)
-    fake.handle.disconnectError = failure
-    fake.groups = []
-    fake.handle.succeed(0)
-
-    if (accepted) await expect(terminal.terminate()).resolves.toBeUndefined()
-    else await expect(terminal.terminate()).rejects.toThrow('disconnect failed')
-  })
-
-  it('rejects killing the terminal shell and propagates live foreground failures', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.foreground = '123\n'
-    const terminal = await testSpawn(runtime(fake), spec(), '/runtime/signal')
-    await expect(terminal.signalForeground('SIGKILL')).rejects.toThrow('refusing to SIGKILL')
-    fake.foreground = 'invalid\n'
-    await expect(terminal.inspectForeground()).rejects.toThrow('cannot resolve foreground')
-    fake.foregroundFailure = commandError(1)
-    await expect(terminal.inspectForeground()).resolves.toBeUndefined()
-    fake.foregroundFailure = commandError(2)
-    await expect(terminal.inspectForeground()).rejects.toBeInstanceOf(CommandExitError)
-    fake.clearOnTerm = true
-    await terminal.terminate()
-  })
-
-  it('sends KILL before checking an expired force-cleanup deadline', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.groups = [123, 456]
-    fake.clearOnTerm = false
-    const terminal = await testSpawn(runtime(fake), spec({ graceMs: 0 }), '/runtime/escalate')
-    const terminating = terminal.terminate()
-    await expect(terminal.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-    await terminating
-    expect(fake.commands).toContain('kill -TERM -- -123 -456')
-    expect(fake.commands).toContain('kill -KILL -- -123 -456')
-  })
-
-  it('surfaces cleanup failures and allows a later retry', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.groups = [1]
-    const terminal = await testSpawn(runtime(fake), spec({ graceMs: 1 }), '/runtime/retry')
-    await expect(terminal.terminate()).rejects.toThrow('unsafe process group 1')
-
-    fake.groups = []
-    fake.handle.succeed(0)
-    await terminal.done
-    await terminal.terminate()
-  })
-
-  it('propagates a process-group signalling transport failure before retry', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.termFailure = new Error('signal transport failed')
-    const terminal = await testSpawn(runtime(fake), spec({ graceMs: 1 }), '/runtime/signal-failure')
-    await expect(terminal.terminate()).rejects.toThrow('signal transport failed')
-
-    fake.groups = []
-    fake.handle.succeed(0)
-    await terminal.done
-    await terminal.terminate()
-
-    const alreadyExited = new FakeTerminalSandbox()
-    alreadyExited.termFailure = commandError(1)
-    const tolerant = await testSpawn(runtime(alreadyExited), spec({ graceMs: 1 }), '/runtime/group-exited')
-    const tolerantTermination = tolerant.terminate()
-    await expect(tolerant.done).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
-    await tolerantTermination
-  })
-
-  it('keeps command rejection authoritative while cleanup is already waiting', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.groups = []
-    fake.removeError = new Error('private state already gone')
-    const terminal = await testSpawn(runtime(fake), spec(), '/runtime/reject-during-cleanup')
-    terminal.output.on('error', () => {})
-    const cleanup = terminal.terminate()
-    await Promise.resolve()
-    fake.handle.crash(new Error('command transport failed'))
-    await expect(terminal.done).rejects.toThrow('command transport failed')
-    await cleanup
-  })
-
-  it('keeps a late command rejection authoritative after PTY kill', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.groups = []
-    fake.handle.settleOnSdkKill = false
-    const terminal = await testSpawn(runtime(fake), spec({ graceMs: 1 }), '/runtime/reject-after-kill')
-    terminal.output.on('error', () => {})
-    const cleanup = terminal.terminate()
-    while (fake.handle.sdkKills === 0) await new Promise(resolve => setTimeout(resolve, 0))
-    await Promise.resolve()
-    fake.handle.crash(new Error('late command transport failed'))
-    await expect(terminal.done).rejects.toThrow('late command transport failed')
-    await cleanup
-  })
-
-  it('reports surviving groups, a surviving top-level pid, and transport failure', async () => {
-    const survivor = new FakeTerminalSandbox()
-    survivor.clearOnTerm = false
-    survivor.clearOnKill = false
-    const terminal = await testSpawn(runtime(survivor), spec({ graceMs: 1 }), '/runtime/survivor')
-    await expect(terminal.terminate()).rejects.toThrow('surviving process groups: 123')
-
-    const livePid = new FakeTerminalSandbox()
-    livePid.groups = []
-    livePid.handle.settleOnSdkKill = false
-    const live = await testSpawn(runtime(livePid), spec({ graceMs: 1 }), '/runtime/live-pid')
-    await expect(live.terminate()).rejects.toThrow('surviving pid: 123')
-    livePid.handle.succeed(0)
-    await live.done
-
-    const crashed = new FakeTerminalSandbox()
-    crashed.groups = []
-    const failed = await testSpawn(runtime(crashed), spec(), '/runtime/crashed')
-    const outputError = once(failed.output, 'error')
-    crashed.handle.crash('transport gone')
-    await expect(failed.done).rejects.toEqual('transport gone')
-    await expect(outputError).resolves.toMatchObject([{ message: 'transport gone' }])
-    await failed.terminate()
-  })
-})
-
-describe('E2B subprocess terminal service', () => {
-  async function service(fake = new FakeTerminalSandbox()): Promise<{
-    ctx: Context
-    fiber: Awaited<ReturnType<Context['plugin']>>
-    fake: FakeTerminalSandbox
-  }> {
-    const ctx = new Context()
-    ctx.provide('e2b', runtime(fake))
-    const fiber = await ctx.plugin(E2BSubprocessRuntime)
-    return { ctx, fiber, fake }
-  }
-
-  it('resolves remote executables', async () => {
-    const { ctx, fake } = await service()
-    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')
-    fake.resolvedExecutable = 'tools/bin/node\n'
-    await expect(ctx.subprocess.resolveExecutable('node', { PATH: 'tools/bin' }))
-      .resolves.toBe('/workspace/tools/bin/node')
-    const commandOptions = fake.commandOptions.at(-1)
-    expect(commandOptions).toMatchObject({ cwd: '/workspace' })
-    expect(commandOptions?.envs?.HOME).toMatch(/^\/\.dsh-e2b-control-/)
-    expect(commandOptions?.envs).toEqual({ HOME: commandOptions?.envs?.HOME })
-    expect((ctx.e2b)).toBeDefined()
-  })
-
-  it('rejects invalid executable lookup inputs and results', async () => {
-    const { ctx, fake } = await service()
-    await expect(ctx.subprocess.resolveExecutable('')).rejects.toThrow('non-empty')
-    await expect(ctx.subprocess.resolveExecutable('./bin/server')).rejects.toThrow('is a relative path')
-    await expect(ctx.subprocess.resolveExecutable('node_modules/.bin/server')).rejects.toThrow('is a relative path')
-    await expect(ctx.subprocess.resolveExecutable('node', undefined, AbortSignal.abort(new Error('stop'))))
-      .rejects.toThrow('stop')
-    fake.resolvedExecutable = 'node\n'
-    await expect(ctx.subprocess.resolveExecutable('node')).rejects.toThrow('did not resolve')
-    fake.resolvedExecutable = '/one\n/two\n'
-    await expect(ctx.subprocess.resolveExecutable('node')).rejects.toThrow('did not resolve')
-  })
-
-  it('rejects a non-positive poll cadence at load', async () => {
-    const ctx = new Context()
-    ctx.provide('e2b', runtime(new FakeTerminalSandbox()))
-    await expect(ctx.plugin(E2BSubprocessRuntime, { pollMs: 0 }))
-      .rejects.toThrow('pollMs must be a positive safe integer')
-    const explicit = await ctx.plugin(E2BSubprocessRuntime, { pollMs: 5 })
-    await explicit.dispose()
-  })
-
-  it('owns live terminals through service disposal', async () => {
-    const { ctx, fiber, fake } = await service()
-    const terminal = await ctx.subprocess.spawnTerminal(spec({ signal: new AbortController().signal }))
-    await fiber.dispose()
-    await expect(terminal.done).resolves.toEqual({ exitCode: null, signal: 'SIGTERM' })
-    expect(fake.handle.disconnects).toBe(1)
-  })
-
-  it('joins and rejects terminal setup that completes during service disposal', async () => {
-    const fake = new FakeTerminalSandbox()
-    const { ctx, fiber } = await service(fake)
-    let disposing: Promise<void> | undefined
-    fake.afterSessionLookup = () => {
-      fake.afterSessionLookup = undefined
-      queueMicrotask(() => {
-        queueMicrotask(() => { disposing = fiber.dispose() })
-      })
-    }
-    const subprocess = ctx.subprocess
-    const spawning = ctx.subprocess.spawnTerminal(spec())
-    const rejected = expect(spawning).rejects.toThrow('service disposed during terminal setup')
-    await vi.waitFor(() => { expect(disposing).toBeDefined() })
-    await expect(subprocess.spawnTerminal(spec())).rejects.toThrow('service is disposing')
-
-    await rejected
-    await disposing
-    expect(fake.groups).toEqual([])
-    expect(fake.handle.disconnects).toBe(1)
-    expect(fake.removed.some(path => path.includes('/terminals/'))).toBe(true)
-  })
-
-  it('aborts and rolls back terminal setup that cannot publish its output boundary during disposal', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.emitOutputMarker = false
-    const { ctx, fiber } = await service(fake)
-    const spawning = ctx.subprocess.spawnTerminal(spec())
-    const rejected = expect(spawning).rejects.toThrow('service disposed during terminal setup')
-    await vi.waitFor(() => { expect(fake.inputs).toHaveLength(1) })
-
-    await fiber.dispose()
-    await rejected
-    expect(fake.groups).toEqual([])
-    expect(fake.handle.disconnects).toBe(1)
-  })
-
-  it('owns and cancels terminal state-directory creation during disposal', async () => {
-    const fake = new FakeTerminalSandbox()
-    fake.makeDirRequest = async (signal) => {
-      await new Promise<never>((_resolve, reject) => {
-        const onAbort = (): void => {
-          const reason: unknown = signal?.reason
-          reject(reason instanceof Error ? reason : new Error(String(reason)))
-        }
-        signal?.addEventListener('abort', onAbort, { once: true })
-        if (signal?.aborted === true) onAbort()
-      })
-    }
-    const { ctx, fiber } = await service(fake)
-    const spawning = ctx.subprocess.spawnTerminal(spec())
-    const rejected = expect(spawning).rejects.toThrow('service disposed during terminal setup')
-    await vi.waitFor(() => { expect(fake.directories.some(path => path.includes('/terminals/'))).toBe(true) })
-
-    await fiber.dispose()
-    await rejected
-    expect(fake.removed.some(path => path.includes('/terminals/'))).toBe(true)
-    expect(fake.createOptions).toBeUndefined()
-  })
-
-  it('releases naturally settled terminals and validates terminal requests', async () => {
-    const { ctx, fiber, fake } = await service()
-    for (const request of [
-      spec({ argv: [] }),
-      spec({ signal: AbortSignal.abort(new Error('cancelled')) }),
-    ]) {
-      await expect(ctx.subprocess.spawnTerminal(request)).rejects.toThrow()
-    }
-
-    fake.groups = []
-    const terminal = await ctx.subprocess.spawnTerminal(spec())
-    fake.handle.succeed(0)
-    await terminal.done
-    await terminal.terminate()
-    const signals = fake.commands.filter(command => command.startsWith('kill -')).length
-    await fiber.dispose()
-    expect(fake.commands.filter(command => command.startsWith('kill -'))).toHaveLength(signals)
-  })
-
-  it('contains a failed automatic terminal release until service disposal retries it', async () => {
-    const { fiber, fake } = await service()
-    fake.clearOnTerm = false
-    fake.clearOnKill = false
-    const terminal = await (fiber.ctx).subprocess.spawnTerminal(spec({ graceMs: 1 }))
-    fake.handle.succeed(0)
-    await terminal.done
-    await new Promise(resolve => setTimeout(resolve, 10))
-    expect(fake.commands).toContain('kill -KILL -- -123')
-
-    fake.groups = []
-    await fiber.dispose()
-    await expect(terminal.terminate()).resolves.toBeUndefined()
-  })
-
-  it('contains an immediate automatic terminal release rejection before disposal retries it', async () => {
-    const { fiber, fake } = await service()
-    fake.groups = []
-    const terminal = await (fiber.ctx).subprocess.spawnTerminal(spec())
-    const terminate = vi.spyOn(terminal, 'terminate')
-      .mockRejectedValueOnce(new Error('automatic release failed'))
-    fake.handle.succeed(0)
-    await terminal.done
-    await vi.waitFor(() => { expect(terminate).toHaveBeenCalledTimes(1) })
-    await new Promise(resolve => setTimeout(resolve, 0))
-
-    await fiber.dispose()
-    expect(terminate).toHaveBeenCalledTimes(2)
-    expect(fake.handle.disconnects).toBe(1)
-  })
-})

+ 0 - 30
packages/e2b/subprocess-e2b/tsconfig.json

@@ -1,30 +0,0 @@
-{
-  "extends": "../../../tsconfig.base.json",
-  "compilerOptions": {
-    "rootDir": "src",
-    "outDir": "lib/types"
-  },
-  "include": [
-    "src"
-  ],
-  "references": [
-    {
-      "path": "../../../vendor/cordis"
-    },
-    {
-      "path": "../../../vendor/cosmokit"
-    },
-    {
-      "path": "../../../vendor/schemastery"
-    },
-    {
-      "path": "../../subprocess/subprocess"
-    },
-    {
-      "path": "../../util/timeout"
-    },
-    {
-      "path": "../e2b"
-    }
-  ]
-}

+ 0 - 24
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -870,30 +870,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
       },
     ],
   },
-  {
-    key: 'e2b',
-    summary: 'Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal.',
-    description: 'Creates one lazily consumable E2B SDK handle and deletes the sandbox at timeout or disposal. Creation begins at plugin construction; adapters await getSandbox before their first operation.',
-    methods: [
-      {
-        signature: 'readonly cwd: string',
-        description: 'Validated remote working directory shared by provider adapters.',
-        parameters: [],
-      },
-      {
-        signature: 'readonly runtimeRoot: string',
-        description: 'Remote directory reserved for adapter-owned process and terminal state.',
-        parameters: [],
-      },
-      {
-        signature: 'async getSandbox(): Promise<Sandbox>',
-        description: 'Return the shared live SDK handle.',
-        parameters: [],
-        returns: 'the created sandbox after the configured cwd exists.',
-        throws: ['when E2B rejects creation or the service is disposing.'],
-      },
-    ],
-  },
   {
     key: 'fileReferences',
     summary: 'Host capability for cancellable file-reference discovery.',

+ 2 - 2
packages/fs/README.i18n.yaml

@@ -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/fs/README.md
-README.md: dd3e6f1fab3a8439dd8e2ff15f854e2872549eef
-README.zh.md: e4139d05c5bbd08c14bce80c0d2f363e92eeaaf4
+README.md: 6a1a6208ae7ec6794d1ce4c945f01178556228b8
+README.zh.md: 847001264e603f0a584e6d702dd18f23b8fa281b

+ 2 - 3
packages/fs/README.md

@@ -22,14 +22,13 @@ The `fs/` group gives agents durable, policy-governed access to files: the `ctx.
 <a id="packages"></a>
 ## Packages
 
-Eight packages plus the remote sibling `fs-e2b` play the filesystem roles; the subsystem reference owns the exhaustive contracts and the error taxonomy.
+Eight packages play the filesystem roles; the subsystem reference owns the exhaustive contracts and the error taxonomy.
 
 | Package | Role | ctx key |
 |---|---|---|
 | [`fs/`](fs/README.md) | `ctx.fs` service contract: execution-world paths, bounded text I/O, and atomic mutations with an optional version guard | `ctx.fs` |
 | [`fs-local/`](fs-local/README.md) | Host-filesystem backend: reads, writes, and edits real files on the local machine | registers on `ctx.fs` |
 | [`fs-sandbox/`](fs-sandbox/README.md) | Sandbox-enforcing backend: fences writes and edits by the per-call sandbox mode while reads pass through | registers on `ctx.fs` |
-| [`e2b/fs-e2b`](../e2b/fs-e2b/README.md) | E2B-backed backend: file state lives in the remote execution world shared with the E2B subprocess provider | registers on `ctx.fs` |
 | [`fs-observation-policy/`](fs-observation-policy/README.md) | Read-before-edit policy: records observed presence or absence and guards write/edit through the `fs/*` events | `fs/*` listeners |
 | [`tool-fs/`](tool-fs/README.md) | Model-facing `read`, `read_image`, `write`, and `edit` tools plus their executor | registers on `ctx.tools` |
 | [`tool-fs-search/`](tool-fs-search/README.md) | Model-facing `glob` and `grep` discovery tools backed by the packaged ripgrep binary | registers on `ctx.tools` |
@@ -47,7 +46,7 @@ Start with the subsystem reference for the shared vocabulary and error taxonomy,
 
 - [Filesystem subsystem](../../docs/subsystems/filesystem.md) — targets, outcomes, guards, policy events, and the error taxonomy.
 - [Cross-family fs sandbox decision](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.md) — the shared sandbox mode fence over the filesystem seam.
-- [Portable execution world consumers decision](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md) — why the E2B backend shares the remote execution world.
+- [Portable execution world consumers decision](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md) — why filesystem and subprocess providers share one execution world.
 
 <a id="dev-note"></a>
 ## Dev Note

+ 2 - 3
packages/fs/README.zh.md

@@ -22,14 +22,13 @@ kind: "package-group"
 <a id="packages"></a>
 ## 包
 
-八个包加上远程同级 `fs-e2b` 承担文件系统角色;子系统参考文档完整收录各项约定与错误分类体系。
+八个包承担文件系统角色;子系统参考文档完整收录各项约定与错误分类体系。
 
 | 包 | 职责 | ctx 键 |
 |---|---|---|
 | [`fs/`](fs/README.zh.md) | `ctx.fs` 服务约定:执行世界路径、有界文本 I/O,以及带可选版本防护的原子变更 | `ctx.fs` |
 | [`fs-local/`](fs-local/README.zh.md) | 宿主文件系统后端:读取、写入并编辑本机上的真实文件 | 注册到 `ctx.fs` |
 | [`fs-sandbox/`](fs-sandbox/README.zh.md) | 沙箱强制后端:按每次调用的沙箱模式约束写入与编辑,读取直接通过 | 注册到 `ctx.fs` |
-| [`e2b/fs-e2b`](../e2b/fs-e2b/README.zh.md) | 以 E2B 为后端:文件状态位于与 E2B 子进程提供方共享的远程执行世界 | 注册到 `ctx.fs` |
 | [`fs-observation-policy/`](fs-observation-policy/README.zh.md) | 编辑前读取策略:记录观测到的存在或缺失,并通过 `fs/*` 事件防护写入/编辑 | `fs/*` 监听器 |
 | [`tool-fs/`](tool-fs/README.zh.md) | 面向模型的 `read`、`read_image`、`write` 与 `edit` 工具及其执行器 | 注册到 `ctx.tools` |
 | [`tool-fs-search/`](tool-fs-search/README.zh.md) | 由打包 ripgrep 二进制支持的面向模型 `glob` 与 `grep` 发现工具 | 注册到 `ctx.tools` |
@@ -47,7 +46,7 @@ kind: "package-group"
 
 - [文件系统子系统](../../docs/subsystems/filesystem.zh.md)——目标、结果、防护、策略事件与错误分类体系。
 - [跨能力族 fs 沙箱决策](../../.agents/notes/implemented/feature/2026-07-14-cross-family-fs-sandbox.zh.md)——文件系统 seam 上共享的沙箱模式围栏。
-- [可移植执行世界消费方决策](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md)——E2B 后端为何共享远程执行世界。
+- [可移植执行世界消费方决策](../../.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md)——文件系统与子进程提供方为何共享一个执行世界。
 
 <a id="dev-note"></a>
 ## 开发备注

+ 2 - 2
packages/fs/fs-local/README.i18n.yaml

@@ -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/fs/fs-local/README.md
-README.md: d8f1afaa59e8313c553ceb1ea4e94cec17eaca10
-README.zh.md: 7daa9143a4b7455446b2238889524089a8172ea1
+README.md: 6b53b8bea81a31d047154232f4fe03899b6909ec
+README.zh.md: 56c834c7e024bf28909d28c1ebfacbb99dc83518

+ 2 - 2
packages/fs/fs-local/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Use `dsh-fs-local` to read, list, atomically write, and edit files on the host filesystem. Relative paths resolve from a configurable base directory, while absolute paths and parent traversal remain unrestricted. Paths and symlinks that reach the same file share one identity. Writes preserve file permissions, and optional version guards reject stale overwrites. Choose this package for direct host access; use `fs-sandbox` for confined mutations or `fs-e2b` for files in a remote execution world.
+Use `dsh-fs-local` to read, list, atomically write, and edit files on the host filesystem. Relative paths resolve from a configurable base directory, while absolute paths and parent traversal remain unrestricted. Paths and symlinks that reach the same file share one identity. Writes preserve file permissions, and optional version guards reject stale overwrites. Choose this package for direct host access; use `fs-sandbox` for confined mutations.
 
 ## Table of Contents
 
@@ -29,7 +29,7 @@ Mount this backend when a composition needs `ctx.fs` backed by the real host fil
 
 ### When to choose it
 
-Choose `fs-local` for ordinary host-file access in a single process. Choose [`fs-sandbox`](../fs-sandbox/README.md) when a session's writes and edits must be confined to its workspace and temp roots — it extends this backend and adds only the mode fence. Choose [`fs-e2b`](../../e2b/fs-e2b/README.md) when files must live in a remote execution world shared with subprocesses. `config.cwd` is a resolution default, not a containment boundary: absolute paths and `..` escape it.
+Choose `fs-local` for ordinary host-file access in a single process. Choose [`fs-sandbox`](../fs-sandbox/README.md) when a session's writes and edits must be confined to its workspace and temp roots — it extends this backend and adds only the mode fence. `config.cwd` is a resolution default, not a containment boundary: absolute paths and `..` escape it.
 
 ### Minimal configuration
 

+ 2 - 2
packages/fs/fs-local/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-使用 `dsh-fs-local` 可在宿主文件系统上读取、列出、原子写入和编辑文件。相对路径从可配置的基准目录解析,而绝对路径和父目录遍历不受限制。到达同一文件的路径和符号链接共享一个身份。写入保留文件权限,可选版本防护会拒绝陈旧覆盖。直接访问宿主文件时选择本包;需要约束变更时使用 `fs-sandbox`,文件位于远程执行世界时使用 `fs-e2b`
+使用 `dsh-fs-local` 可在宿主文件系统上读取、列出、原子写入和编辑文件。相对路径从可配置的基准目录解析,而绝对路径和父目录遍历不受限制。到达同一文件的路径和符号链接共享一个身份。写入保留文件权限,可选版本防护会拒绝陈旧覆盖。直接访问宿主文件时选择本包;需要约束变更时使用 `fs-sandbox`。
 
 ## 目录
 
@@ -29,7 +29,7 @@ kind: "package-reference"
 
 ### 何时选择
 
-在单个进程中进行普通宿主文件访问时,请选择 `fs-local`。会话的写入与编辑必须限制在工作区与临时根目录内时,选择 [`fs-sandbox`](../fs-sandbox/README.zh.md)——它扩展此后端,只增加模式围栏。文件必须位于与子进程共享的远程执行世界时,选择 [`fs-e2b`](../../e2b/fs-e2b/README.zh.md)。`config.cwd` 只是解析默认值,不是约束边界:绝对路径与 `..` 都可以逃逸它。
+在单个进程中进行普通宿主文件访问时,请选择 `fs-local`。会话的写入与编辑必须限制在工作区与临时根目录内时,选择 [`fs-sandbox`](../fs-sandbox/README.zh.md)——它扩展此后端,只增加模式围栏。`config.cwd` 只是解析默认值,不是约束边界:绝对路径与 `..` 都可以逃逸它。
 
 ### 最小配置
 

+ 2 - 2
packages/fs/fs/README.i18n.yaml

@@ -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/fs/fs/README.md
-README.md: baad80c13fa5cbdfebc4e1a367d0726f728cffdb
-README.zh.md: a6598c9b532ebe0349c5ad9af9c56eab75f4c756
+README.md: e36e400d53cc581eb19733c9b3ccd890f34932a2
+README.zh.md: 3efc2106a9200ee84ab1a859a19fdb5b1631f056

+ 2 - 2
packages/fs/fs/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-Use `dsh-fs` when an application needs consistent filesystem operations across host, confined, or remote execution environments. It lets consumers resolve stable file identities, map shared host files where supported, perform bounded text and byte reads, list directories, and apply atomic text writes and literal edits. Version guards are optional, so a backend works without policy enforcement; callers can supply a guard to reject a mutation after the file changes. Choose `fs-local`, `fs-sandbox`, or `fs-e2b` for the required execution environment. Model-facing filesystem tools are provided separately by `dsh-tool-fs`.
+Use `dsh-fs` when an application needs consistent filesystem operations across host, confined, or remote execution environments. It lets consumers resolve stable file identities, map shared host files where supported, perform bounded text and byte reads, list directories, and apply atomic text writes and literal edits. Version guards are optional, so a backend works without policy enforcement; callers can supply a guard to reject a mutation after the file changes. Choose `fs-local` or `fs-sandbox` for host execution. Model-facing filesystem tools are provided separately by `dsh-tool-fs`.
 
 ## Table of Contents
 
@@ -29,7 +29,7 @@ You rarely load `dsh-fs` directly: you mount a backend that registers as `ctx.fs
 
 ### Choosing and mounting a backend
 
-Pick [`fs-local`](../fs-local/README.md) for ordinary host files, [`fs-sandbox`](../fs-sandbox/README.md) when a session's mutations must be confined to its workspace and temp roots, and [`fs-e2b`](../../e2b/fs-e2b/README.md) when file state must live in a remote execution world. Mounting any backend populates `ctx.fs`; swapping backends changes nothing for the policy plugin, the tools, or the tool schemas. A composition that mounts no backend has no `ctx.fs` at all, and the tools fail at registration.
+Pick [`fs-local`](../fs-local/README.md) for ordinary host files or [`fs-sandbox`](../fs-sandbox/README.md) when a session's mutations must be confined to its workspace and temp roots. Mounting any backend populates `ctx.fs`; swapping backends changes nothing for the policy plugin, the tools, or the tool schemas. A composition that mounts no backend has no `ctx.fs` at all, and the tools fail at registration.
 
 ### What the service lets you do
 

+ 2 - 2
packages/fs/fs/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-应用需要在宿主、受限或远程执行环境中使用一致的文件系统操作时,选择 `dsh-fs`。消费方可以解析稳定的文件身份、在受支持时映射共享宿主文件、执行有界的文本与字节读取、列出目录,并原子地写入文本及执行字面量编辑。版本防护是可选的,因此后端无需策略强制也能工作;调用方可以提供防护,在文件变化后拒绝变更。根据所需执行环境选择 `fs-local`、`fs-sandbox` 或 `fs-e2b`。面向模型的文件系统工具由 `dsh-tool-fs` 单独提供。
+应用需要在宿主、受限或远程执行环境中使用一致的文件系统操作时,选择 `dsh-fs`。消费方可以解析稳定的文件身份、在受支持时映射共享宿主文件、执行有界的文本与字节读取、列出目录,并原子地写入文本及执行字面量编辑。版本防护是可选的,因此后端无需策略强制也能工作;调用方可以提供防护,在文件变化后拒绝变更。在宿主执行时选择 `fs-local` 或 `fs-sandbox`。面向模型的文件系统工具由 `dsh-tool-fs` 单独提供。
 
 ## 目录
 
@@ -29,7 +29,7 @@ kind: "package-reference"
 
 ### 选择并挂载后端
 
-普通宿主文件选择 [`fs-local`](../fs-local/README.zh.md),会话变更必须限制在工作区与临时根目录内时选择 [`fs-sandbox`](../fs-sandbox/README.zh.md),文件状态必须位于远程执行世界时选择 [`fs-e2b`](../../e2b/fs-e2b/README.zh.md)。挂载任一后端都会填充 `ctx.fs`;更换后端不会改变策略插件、工具或工具 schema。未挂载任何后端的组合就没有 `ctx.fs`,工具会在注册时失败。
+普通宿主文件选择 [`fs-local`](../fs-local/README.zh.md),会话变更必须限制在工作区与临时根目录内时选择 [`fs-sandbox`](../fs-sandbox/README.zh.md)。挂载任一后端都会填充 `ctx.fs`;更换后端不会改变策略插件、工具或工具 schema。未挂载任何后端的组合就没有 `ctx.fs`,工具会在注册时失败。
 
 ### 服务能做什么
 

+ 2 - 2
packages/subprocess/subprocess/README.i18n.yaml

@@ -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/subprocess/subprocess/README.md
-README.md: 561e1e3c86b77e1358747280b66c95d4d86480e5
-README.zh.md: 904e9e6d5f3343a4e5ffbe42fb90d24fa5c750af
+README.md: d5ef57b9068864f9fbba6794e4176948c8173239
+README.zh.md: 21be9a3071ef857945157daa2fafd6c1e2889f75

+ 0 - 1
packages/subprocess/subprocess/README.md

@@ -117,7 +117,6 @@ Read these pages when the package-level contract is not enough. They move from t
 
 - [Subprocess subsystem](../../../docs/subsystems/subprocess.md) — spawn specs, output readers, outcomes, and the `DSH_*` environment in full.
 - [dsh-subprocess-local](../subprocess-local/README.md) — the local host provider that implements this contract.
-- [dsh-subprocess-e2b](../../e2b/subprocess-e2b/README.md) — the remote E2B provider for the same seam.
 - [dsh-bash-local](../../shell/bash-local/README.md) — the largest consumer: bash commands over this service.
 - [Subprocess seam Agent Note](../../../.agents/notes/archived/architecture/2026-07-26-subprocess-seam.md) — why the process half became its own seam and what moved with it.
 

+ 0 - 1
packages/subprocess/subprocess/README.zh.md

@@ -117,7 +117,6 @@ spawn 会立即返回活动句柄,而不公开目标身份。`done` 独立报
 
 - [子进程子系统](../../../docs/subsystems/subprocess.zh.md)——spawn spec、输出读取器、结果与完整的 `DSH_*` 环境。
 - [dsh-subprocess-local](../subprocess-local/README.zh.md)——实现本约定的本地宿主提供方。
-- [dsh-subprocess-e2b](../../e2b/subprocess-e2b/README.zh.md)——同一 seam 的远程 E2B 提供方。
 - [dsh-bash-local](../../shell/bash-local/README.zh.md)——最大的消费方:经由本服务运行 bash 命令。
 - [subprocess seam Agent Note](../../../.agents/notes/archived/architecture/2026-07-26-subprocess-seam.md)——进程部分为何成为独立的 seam,以及随之迁移的内容。
 

+ 2 - 2
packages/util/http-proxy/README.i18n.yaml

@@ -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/util/http-proxy/README.md
-README.md: 87b5d7710643a62860cd9480f2ceeb2de7645761
-README.zh.md: cc48152783bf3240ebb2f99fe98854055f36ef1c
+README.md: 66c0b026ee8b26f23fd39a25fd7e4adc44077dbe
+README.zh.md: c3ee9fc5815b754027c54648d527c2f34fdc96c0

+ 2 - 4
packages/util/http-proxy/README.md

@@ -29,7 +29,7 @@ Nothing to mount, and nothing to configure. The `dsh` launcher resolves and inst
 
 ### Writing a new outbound call
 
-Plain `fetch()` is proxied, and so is any SDK that reaches `globalThis.fetch` — the MCP HTTP transport and the pi-ai provider stack both do. An SDK that builds its own transport does **not**, and two of the ones this repository ships turned out to: the OTLP exporter posts through `node:http`, and the E2B SDK constructs its own undici dispatcher. Assume nothing about an SDK; check it.
+Plain `fetch()` is proxied, and so is any SDK that reaches `globalThis.fetch` — the MCP HTTP transport and the pi-ai provider stack both do. Verify each SDK's actual transport; exceptions belong under [Known Limitations and Deferred Work](#known-limitations-and-deferred-work).
 
 | You are writing | Use |
 |---|---|
@@ -41,11 +41,9 @@ Plain `fetch()` is proxied, and so is any SDK that reaches `globalThis.fetch` 
 
 `proxyRouteFor` answers with the transport that answer assumed, not just the answer: its proxied arm carries the dispatcher already routing by this policy. A caller that read the policy and then built its own transport could have an unmount land between the two and send the request somewhere its branch never cleared.
 
-An SDK that builds its own transport reaches none of this, and two of the ones this repository ships do. E2B takes a proxy URL of its own and is handed `route.proxy`. The OTLP telemetry exporter posts through `node:http`, and is deliberately left direct — see the limitation below.
-
 Constructing `new Agent(...)` and passing it as `dispatcher` overrides the global one and silently bypasses the proxy. `verify-no-bare-dispatcher` rejects that outside this package. One call site legitimately owns its transport — `web-fetch-http` pins a request to addresses it validated, which is per-request state a process-wide dispatcher cannot hold — and says so with a `proxy-exempt:` comment on the line.
 
-That gate cannot see inside an SDK, so every outbound call site in the repository also carries an `egress.spec.ts` that drives its real code path through a fake proxy and asserts the proxy saw the request — or, for telemetry, that it did not. A new call site adds one. It is the only thing that catches an SDK changing transports underneath us, in either direction: it is how the OTLP and E2B gaps were found, and it is what would catch an upgrade that started routing telemetry silently.
+That gate cannot see inside an SDK, so each outbound call site carries an `egress.spec.ts` that drives its actual transport through a fake proxy and checks the observed route. Every new outbound call site must include that transport test. Telemetry asserts its direct-route exception. These tests detect dependency changes that alter routing without changing the call site.
 
 ### What the policy reads
 

+ 2 - 4
packages/util/http-proxy/README.zh.md

@@ -29,7 +29,7 @@ kind: "package-reference"
 
 ### 编写新的出站调用
 
-普通 `fetch()` 已经走代理,任何最终落到 `globalThis.fetch` 的 SDK 也一样——MCP HTTP 传输与 pi-ai 提供方栈都是如此。自建传输的 SDK 则不会走代理,而本仓库随附的 SDK 中已有两个如此。不要对任何 SDK 想当然,去查
+普通 `fetch()` 会走代理,任何使用 `globalThis.fetch` 的 SDK 也一样——MCP HTTP 传输与 pi-ai 提供方栈都使用它。应验证每个 SDK 的实际传输;例外见[已知限制与延后工作](#known-limitations-and-deferred-work)
 
 | 你要写的东西 | 使用 |
 |---|---|
@@ -41,11 +41,9 @@ kind: "package-reference"
 
 `proxyRouteFor` 给出的不只是答案,还有该答案所假定的传输:走代理的那一支携带着此刻正按该策略路由的 dispatcher。若调用方先读策略、再自建传输,卸载就可能落在两次读取之间,把请求发往其分支从未放行的去处。
 
-自建传输的 SDK 接触不到上述任何一条,而本仓库随附的 SDK 里有两个如此。E2B 接受自有代理 URL,现在接收 `route.proxy`。OTLP 遥测导出器通过 `node:http` 投递,被有意保留为直连——见下方限制一节。
-
 构造 `new Agent(...)` 再作为 `dispatcher` 传入会覆盖全局 dispatcher,从而静默绕开代理。`verify-no-bare-dispatcher` 会在本包之外拒绝该写法。有一处调用点确实自有传输——`web-fetch-http` 会把请求钉在它已校验过的地址上,而这是进程级 dispatcher 无法承载的单次请求状态——它在该行用 `proxy-exempt:` 注释说明。
 
-该门禁看不进 SDK 内部,因此仓库中每一个出网点都另有一份 `egress.spec.ts`:它驱动该点的真实代码路径穿过一个假代理,并断言代理确实收到了请求——遥测那份则断言代理什么也没收到。新增出网点就补一份。它是唯一能双向发现 SDK 在我们脚下更换传输的手段:OTLP 与 E2B 这两个漏洞正是这样被发现的,而某次升级若开始静默地把遥测送去代理,也由它拦下
+该门禁看不进 SDK 内部,因此每个出网点都配有 `egress.spec.ts`,通过假代理驱动实际传输并检查观察到的路由。每个新的出网点必须包含该传输测试。遥测断言其直连例外。这些测试可发现调用点不变但依赖变更改变路由的情况
 
 ### 策略读取哪些值
 

+ 0 - 214
pnpm-lock.yaml

@@ -5297,98 +5297,6 @@ importers:
         specifier: workspace:^
         version: link:../../util/launch-environment
 
-  packages/e2b/e2b:
-    dependencies:
-      '@deepseek-ai/schemastery':
-        specifier: link:../../../vendor/schemastery
-        version: link:../../../vendor/schemastery
-      e2b:
-        specifier: 2.29.1
-        version: 2.29.1
-    devDependencies:
-      '@deepseek-ai/cordis':
-        specifier: workspace:^
-        version: link:../../../vendor/cordis
-      '@deepseek-ai/dsh-agent':
-        specifier: workspace:^
-        version: link:../../core/agent
-      '@deepseek-ai/dsh-agent-loop-testkit':
-        specifier: workspace:^
-        version: link:../../test-support/agent-loop-testkit
-      '@deepseek-ai/dsh-app-boot':
-        specifier: workspace:^
-        version: link:../../boot/app-boot
-      '@deepseek-ai/dsh-bash-local':
-        specifier: workspace:^
-        version: link:../../shell/bash-local
-      '@deepseek-ai/dsh-fs-e2b':
-        specifier: workspace:^
-        version: link:../fs-e2b
-      '@deepseek-ai/dsh-http-proxy':
-        specifier: workspace:^
-        version: link:../../util/http-proxy
-      '@deepseek-ai/dsh-launch-environment':
-        specifier: workspace:^
-        version: link:../../util/launch-environment
-      '@deepseek-ai/dsh-loader-smoke':
-        specifier: workspace:^
-        version: link:../../test-support/loader-smoke
-      '@deepseek-ai/dsh-lsp':
-        specifier: workspace:^
-        version: link:../../lsp/lsp
-      '@deepseek-ai/dsh-lsp-stdio':
-        specifier: workspace:^
-        version: link:../../lsp/lsp-stdio
-      '@deepseek-ai/dsh-sandbox-policy':
-        specifier: workspace:^
-        version: link:../../sandbox/sandbox-policy
-      '@deepseek-ai/dsh-session':
-        specifier: workspace:^
-        version: link:../../core/session
-      '@deepseek-ai/dsh-session-projection':
-        specifier: workspace:^
-        version: link:../../session/session-projection
-      '@deepseek-ai/dsh-subprocess-e2b':
-        specifier: workspace:^
-        version: link:../subprocess-e2b
-      '@deepseek-ai/dsh-terminal':
-        specifier: workspace:^
-        version: link:../../terminal/terminal
-      '@deepseek-ai/dsh-terminal-bash':
-        specifier: workspace:^
-        version: link:../../terminal/terminal-bash
-
-  packages/e2b/fs-e2b:
-    devDependencies:
-      '@deepseek-ai/cordis':
-        specifier: workspace:^
-        version: link:../../../vendor/cordis
-      '@deepseek-ai/dsh-e2b':
-        specifier: workspace:^
-        version: link:../e2b
-      '@deepseek-ai/dsh-fs':
-        specifier: workspace:^
-        version: link:../../fs/fs
-
-  packages/e2b/subprocess-e2b:
-    dependencies:
-      '@deepseek-ai/schemastery':
-        specifier: link:../../../vendor/schemastery
-        version: link:../../../vendor/schemastery
-    devDependencies:
-      '@deepseek-ai/cordis':
-        specifier: workspace:^
-        version: link:../../../vendor/cordis
-      '@deepseek-ai/dsh-e2b':
-        specifier: workspace:^
-        version: link:../e2b
-      '@deepseek-ai/dsh-subprocess':
-        specifier: workspace:^
-        version: link:../../subprocess/subprocess
-      '@deepseek-ai/dsh-timeout':
-        specifier: workspace:^
-        version: link:../../util/timeout
-
   packages/experimental/agent-team:
     dependencies:
       '@deepseek-ai/dsh-brand':
@@ -11917,23 +11825,9 @@ packages:
     resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==}
     hasBin: true
 
-  '@bufbuild/protobuf@2.13.0':
-    resolution: {integrity: sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==}
-
   '@chevrotain/types@11.1.2':
     resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
 
-  '@connectrpc/connect-web@2.0.0-rc.3':
-    resolution: {integrity: sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw==}
-    peerDependencies:
-      '@bufbuild/protobuf': ^2.2.0
-      '@connectrpc/connect': 2.0.0-rc.3
-
-  '@connectrpc/connect@2.0.0-rc.3':
-    resolution: {integrity: sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ==}
-    peerDependencies:
-      '@bufbuild/protobuf': ^2.2.0
-
   '@csstools/color-helpers@6.1.0':
     resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==}
     engines: {node: '>=20.19.0'}
@@ -12766,10 +12660,6 @@ packages:
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
 
-  '@isaacs/cliui@9.0.0':
-    resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
-    engines: {node: '>=18'}
-
   '@isaacs/fs-minipass@4.0.1':
     resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
     engines: {node: '>=18.0.0'}
@@ -14864,10 +14754,6 @@ packages:
     resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
     engines: {node: '>=10'}
 
-  chalk@5.6.2:
-    resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
-    engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
-
   character-entities-html4@2.1.0:
     resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
 
@@ -14955,9 +14841,6 @@ packages:
     resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==}
     engines: {node: '>=0.10.0'}
 
-  compare-versions@6.1.1:
-    resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
-
   compressible@2.0.18:
     resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
     engines: {node: '>= 0.6'}
@@ -15294,9 +15177,6 @@ packages:
   dmg-builder@26.15.3:
     resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==}
 
-  dockerfile-ast@0.7.1:
-    resolution: {integrity: sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw==}
-
   dom-accessibility-api@0.5.16:
     resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==}
 
@@ -15327,10 +15207,6 @@ packages:
   duplexer2@0.1.4:
     resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
 
-  e2b@2.29.1:
-    resolution: {integrity: sha512-n4aGNwRKTj2oct7BrOWfR4T+xGO834vbsrzfSlWUNJrhz615Lp+ad9hc8KtRaaHULKr/W/14Z6v4c7fqk3y0pg==}
-    engines: {node: '>=20.18.1'}
-
   eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
 
@@ -15785,12 +15661,6 @@ packages:
     deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
     hasBin: true
 
-  glob@11.1.0:
-    resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
-    engines: {node: 20 || >=22}
-    deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
-    hasBin: true
-
   glob@7.2.3:
     resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
     deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
@@ -16083,10 +15953,6 @@ packages:
   jackspeak@3.4.3:
     resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
 
-  jackspeak@4.2.3:
-    resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==}
-    engines: {node: 20 || >=22}
-
   jake@10.9.4:
     resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==}
     engines: {node: '>=10'}
@@ -16862,12 +16728,6 @@ packages:
       zod:
         optional: true
 
-  openapi-fetch@0.14.1:
-    resolution: {integrity: sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==}
-
-  openapi-typescript-helpers@0.0.15:
-    resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
-
   opener@1.5.2:
     resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
     hasBin: true
@@ -16965,10 +16825,6 @@ packages:
     resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
     engines: {node: '>=16 || 14 >=14.18'}
 
-  path-scurry@2.0.2:
-    resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
-    engines: {node: 18 || 20 || >=22}
-
   path-to-regexp@8.4.2:
     resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
 
@@ -17004,9 +16860,6 @@ packages:
     resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==}
     engines: {node: '>=16.0.0'}
 
-  platform@1.3.6:
-    resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
-
   playwright-core@1.61.1:
     resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
     engines: {node: '>=18'}
@@ -18055,9 +17908,6 @@ packages:
   vscode-languageserver-protocol@3.18.2:
     resolution: {integrity: sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==}
 
-  vscode-languageserver-textdocument@1.0.12:
-    resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==}
-
   vscode-languageserver-types@3.18.0:
     resolution: {integrity: sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==}
 
@@ -18887,19 +18737,8 @@ snapshots:
     dependencies:
       css-tree: 3.2.1
 
-  '@bufbuild/protobuf@2.13.0': {}
-
   '@chevrotain/types@11.1.2': {}
 
-  '@connectrpc/connect-web@2.0.0-rc.3(@bufbuild/protobuf@2.13.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.13.0))':
-    dependencies:
-      '@bufbuild/protobuf': 2.13.0
-      '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.13.0)
-
-  '@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.13.0)':
-    dependencies:
-      '@bufbuild/protobuf': 2.13.0
-
   '@csstools/color-helpers@6.1.0': {}
 
   '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
@@ -19524,8 +19363,6 @@ snapshots:
       wrap-ansi: 8.1.0
       wrap-ansi-cjs: wrap-ansi@7.0.0
 
-  '@isaacs/cliui@9.0.0': {}
-
   '@isaacs/fs-minipass@4.0.1':
     dependencies:
       minipass: 7.1.3
@@ -21513,8 +21350,6 @@ snapshots:
       ansi-styles: 4.3.0
       supports-color: 7.2.0
 
-  chalk@5.6.2: {}
-
   character-entities-html4@2.1.0: {}
 
   character-entities-legacy@3.0.0: {}
@@ -21581,8 +21416,6 @@ snapshots:
 
   compare-version@0.1.2: {}
 
-  compare-versions@6.1.1: {}
-
   compressible@2.0.18:
     dependencies:
       mime-db: 1.54.0
@@ -21936,11 +21769,6 @@ snapshots:
       - electron-builder-squirrel-windows
       - supports-color
 
-  dockerfile-ast@0.7.1:
-    dependencies:
-      vscode-languageserver-textdocument: 1.0.12
-      vscode-languageserver-types: 3.18.0
-
   dom-accessibility-api@0.5.16: {}
 
   dompurify@3.4.11:
@@ -21967,20 +21795,6 @@ snapshots:
     dependencies:
       readable-stream: 2.3.8
 
-  e2b@2.29.1:
-    dependencies:
-      '@bufbuild/protobuf': 2.13.0
-      '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.13.0)
-      '@connectrpc/connect-web': 2.0.0-rc.3(@bufbuild/protobuf@2.13.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.13.0))
-      chalk: 5.6.2
-      compare-versions: 6.1.1
-      dockerfile-ast: 0.7.1
-      glob: 11.1.0
-      openapi-fetch: 0.14.1
-      platform: 1.3.6
-      tar: 7.5.22
-      undici: 7.28.0
-
   eastasianwidth@0.2.0: {}
 
   ecdsa-sig-formatter@1.0.11:
@@ -22616,15 +22430,6 @@ snapshots:
       package-json-from-dist: 1.0.1
       path-scurry: 1.11.1
 
-  glob@11.1.0:
-    dependencies:
-      foreground-child: 3.3.1
-      jackspeak: 4.2.3
-      minimatch: 10.2.5
-      minipass: 7.1.3
-      package-json-from-dist: 1.0.1
-      path-scurry: 2.0.2
-
   glob@7.2.3:
     dependencies:
       fs.realpath: 1.0.0
@@ -22929,10 +22734,6 @@ snapshots:
     optionalDependencies:
       '@pkgjs/parseargs': 0.11.0
 
-  jackspeak@4.2.3:
-    dependencies:
-      '@isaacs/cliui': 9.0.0
-
   jake@10.9.4:
     dependencies:
       async: 3.2.6
@@ -23855,12 +23656,6 @@ snapshots:
       ws: 8.21.0
       zod: 4.4.3
 
-  openapi-fetch@0.14.1:
-    dependencies:
-      openapi-typescript-helpers: 0.0.15
-
-  openapi-typescript-helpers@0.0.15: {}
-
   opener@1.5.2: {}
 
   optionator@0.9.4:
@@ -23977,11 +23772,6 @@ snapshots:
       lru-cache: 10.4.3
       minipass: 7.1.3
 
-  path-scurry@2.0.2:
-    dependencies:
-      lru-cache: 11.5.1
-      minipass: 7.1.3
-
   path-to-regexp@8.4.2: {}
 
   pathe@2.0.3: {}
@@ -24011,8 +23801,6 @@ snapshots:
       pvutils: 1.2.0
       tslib: 2.8.1
 
-  platform@1.3.6: {}
-
   playwright-core@1.61.1: {}
 
   playwright@1.61.1:
@@ -25222,8 +25010,6 @@ snapshots:
       vscode-jsonrpc: 9.0.1
       vscode-languageserver-types: 3.18.0
 
-  vscode-languageserver-textdocument@1.0.12: {}
-
   vscode-languageserver-types@3.18.0: {}
 
   vue@3.5.39(typescript@6.0.3):

+ 0 - 27
scripts/ci-workflow.spec.ts

@@ -603,33 +603,6 @@ describe('DeepSeek e2e workflow', () => {
   })
 })
 
-describe('E2B e2e workflow', () => {
-  it('is manual-only and fails loud before running the focused live suite', () => {
-    const workflow = loadWorkflow('.github/workflows/e2b-e2e.yml')
-    expect(workflow.on).toEqual({ workflow_dispatch: null })
-    if (!isRecord(workflow.jobs) || !isRecord(workflow.jobs.e2b) || !Array.isArray(workflow.jobs.e2b.steps)) {
-      throw new TypeError('E2B e2e workflow must define the e2b job steps')
-    }
-
-    const steps = workflow.jobs.e2b.steps.filter(isRecord)
-    const preflight = steps.find(step => step.name === 'Preflight (require E2B API key)')
-    const e2b = steps.find(step => step.name === 'E2B tests (live sandbox)')
-
-    expect(preflight).toMatchObject({
-      env: { E2B_API_KEY: '${{ secrets.E2B_API_KEY_EXTERNAL }}' },
-    })
-    expect(preflight?.run).toContain('E2B_API_KEY_EXTERNAL repository secret')
-    expect(e2b).toMatchObject({
-      env: {
-        E2B_API_KEY: '${{ secrets.E2B_API_KEY_EXTERNAL }}',
-        DSH_E2E_MAX_WORKERS: '1',
-        DSH_EXAMPLE_MODE: 'lib',
-      },
-    })
-    expect(e2b?.run).toContain('packages/e2b/e2b/tests/composition.e2e.ts')
-  })
-})
-
 describe('Python release workflows', () => {
   it('keeps complete wheel validation separate from protected public publication', () => {
     const workflow = loadWorkflow('.github/workflows/python-release.yml')

+ 0 - 1
scripts/client-build-environment.client.spec.ts

@@ -28,7 +28,6 @@ const roots: string[] = []
 const dshBuildWorkflows = [
   'build-exe-for-python-sdk.yml',
   'ci.yml',
-  'e2b-e2e.yml',
   'e2e.yml',
   'release.yml',
   'release-publish.yml',

+ 0 - 2
scripts/gen-cordis-catalog.ts

@@ -74,7 +74,6 @@ export const SERVICE_PAGE: Record<string, string> = {
   directoryPicker: 'workspace.md',
   deepseekLlmApiExtensions: 'llm-streaming.md',
   dynamicCordisRunner: 'extensions.md',
-  e2b: 'subprocess.md',
   fileUploads: 'attachment.md',
   fileReferences: 'session-reference.md',
   fs: 'filesystem.md',
@@ -757,7 +756,6 @@ export const TYPE_LINK_EXEMPTIONS: Readonly<Record<string, string>> = {
   PromptAssembly: 'assembly result is owned by packages/core/system-prompt/README.md',
   RequestRunId: 'dynamic-package payload contract is owned by packages/extensions/cordis-host-runner/src/types.ts',
   RpcReceipt: 'carrier-layer receipt is owned by packages/client/connection/src/rpc.ts',
-  Sandbox: 'external E2B SDK handle is owned by packages/e2b/e2b/README.md',
   SessionForkSource: 'service-local fork input is owned by packages/core/session/src/index.ts',
   SubagentRunEndInfo: 'event payload contract is owned by packages/subagent/subagent/src/types.ts',
   SubagentRunInfo: 'event payload contract is owned by packages/subagent/subagent/src/types.ts',

+ 2 - 11
scripts/gen-doc-graphs.ts

@@ -72,7 +72,6 @@ const GROUP_ORDER = [
   'bash',
   'pty',
   'sandbox',
-  'e2b',
   'fs',
   'skill',
   'compact',
@@ -462,20 +461,12 @@ const SERVICE_ROLES: ServiceRole[] = [
     mode: 'core',
     note: 'Folds revisioned objective state from the session log and keeps live continuation activation process-local.',
   },
-  {
-    key: 'e2b',
-    pkg: 'e2b',
-    title: 'E2B sandbox lifecycle owner',
-    mode: 'core',
-    consumers: ['fs-e2b', 'subprocess-e2b'],
-    note: 'Owns one shared E2B SDK handle, remote working directory, and final sandbox disposition so both fundamental E2B providers inhabit the same Linux runtime.',
-  },
   {
     key: 'subprocess',
     pkg: 'subprocess',
     title: 'Subprocess seam',
     mode: 'seam',
-    implementations: ['subprocess-local', 'subprocess-e2b'],
+    implementations: ['subprocess-local'],
     consumers: ['bash-local', 'bash-sandbox', 'terminal-bash', 'lsp-stdio', 'subagent-acp', 'subagent-codex', 'subagent-claude-code'],
     note: 'The bash executors, the PTY shell backend, the LSP host, and the out-of-process ACP, Codex, and Claude Code subagent backends spawn through ctx.subprocess; the service owns process coordinates, tree/session lifetime, stdio dispositions, terminal mechanics, and kill escalation.',
   },
@@ -554,7 +545,7 @@ const SERVICE_ROLES: ServiceRole[] = [
     pkg: 'fs',
     title: 'Filesystem provider seam',
     mode: 'seam',
-    implementations: ['fs-local', 'fs-sandbox', 'fs-e2b'],
+    implementations: ['fs-local', 'fs-sandbox'],
     consumers: ['tool-fs'],
     companions: ['fs-observation-policy'],
     note: 'tool-fs executes read/write/edit through ctx.fs; fs-sandbox fences mutations by the shared sandbox mode; fs-observation-policy contributes observed-state checks through the fs/* event gate.',

+ 0 - 3
scripts/verify-package-readme-model-experience.ts

@@ -65,7 +65,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
   'packages/preset/agent-presets': { kind: 'indirect', reason: 'The mount installs a preset\'s own plugins, which own every model-facing registration it makes visible.' },
   'packages/typert/registry': { kind: 'none', reason: 'Runtime type registry; consumers (cordis_inspect, wire faces, gates) own any model-visible projection of registry contents.' },
   'packages/typert/loader': { kind: 'none', reason: 'Loader integration only registers generated artifacts; consumers own any model-visible projection.' },
-  'packages/e2b/e2b': { kind: 'none', reason: 'The shared remote-runtime owner registers no model context; provider adapters and consumers own rendered effects.' },
   'packages/util/http-proxy': { kind: 'none', reason: 'Transport policy only: it changes how bytes reach the network and registers no prompt, schema, or result text.' },
   'packages/client/hmr': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
   'packages/client/modules': { kind: 'none', reason: 'Browser-side module-loading kernel machinery; registers nothing model-facing.' },
@@ -123,7 +122,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
   'packages/client/web': { kind: 'none', reason: 'Browser-side UI plugin layer; registers nothing model-facing.' },
   'packages/context/file-reference': { kind: 'indirect', reason: 'The discovery seam and grammar delegate model guidance to the composed provider.' },
   'packages/fs/fs': { kind: 'indirect', reason: 'The service interface delegates model rendering to dsh-tool-fs.' },
-  'packages/e2b/fs-e2b': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-fs.' },
   'packages/fs/fs-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-fs.' },
   'packages/hooks/hook-protocol': { kind: 'indirect', reason: 'Only the hook bridge plugins render decoded hook output to a model.' },
   'packages/host/directory-picker': { kind: 'none', reason: 'The GUI-host picking seam registers nothing model-facing.' },
@@ -143,7 +141,6 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
   'packages/lsp/lsp': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-lsp.' },
   'packages/lsp/lsp-stdio': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-lsp.' },
   'packages/subprocess/subprocess': { kind: 'indirect', reason: 'The seam delegates all model rendering to consumer seams such as the bash executor family.' },
-  'packages/e2b/subprocess-e2b': { kind: 'indirect', reason: 'The remote spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
   'packages/subprocess/subprocess-local': { kind: 'indirect', reason: 'The spawn backend delegates model rendering to consumer seams such as the bash executor family.' },
   'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' },
   'packages/sandbox/sandbox-windows-acl': { kind: 'indirect', reason: 'The provider backend delegates model rendering to the shell/pwsh sandbox executors and their tools.' },

+ 0 - 3
tsconfig.base.json

@@ -292,12 +292,10 @@
       "@deepseek-ai/dsh-credentials-local": ["./packages/credentials/credentials-local/src"],
       "@deepseek-ai/dsh-deepseek-llm-api-extensions": ["./packages/llm/deepseek-llm-api-extensions/src"],
       "@deepseek-ai/dsh-deque": ["./packages/util/deque/src"],
-      "@deepseek-ai/dsh-e2b": ["./packages/e2b/e2b/src"],
       "@deepseek-ai/dsh-file-reference": ["./packages/context/file-reference/src"],
       "@deepseek-ai/dsh-file-reference-local": ["./packages/context/file-reference-local/src"],
       "@deepseek-ai/dsh-fs": ["./packages/fs/fs/src"],
       "@deepseek-ai/dsh-fs/invariant": ["./packages/fs/fs/src/invariant.ts"],
-      "@deepseek-ai/dsh-fs-e2b": ["./packages/e2b/fs-e2b/src"],
       "@deepseek-ai/dsh-fs-local": ["./packages/fs/fs-local/src"],
       "@deepseek-ai/dsh-fs-observation-policy": ["./packages/fs/fs-observation-policy/src"],
       "@deepseek-ai/dsh-fs-sandbox": ["./packages/fs/fs-sandbox/src"],
@@ -403,7 +401,6 @@
       "@deepseek-ai/dsh-subagent-in-process-driver": ["./packages/subagent/subagent-in-process-driver/src"],
       "@deepseek-ai/dsh-subagent-spawn-in-process": ["./packages/subagent/subagent-spawn-in-process/src"],
       "@deepseek-ai/dsh-subprocess": ["./packages/subprocess/subprocess/src"],
-      "@deepseek-ai/dsh-subprocess-e2b": ["./packages/e2b/subprocess-e2b/src"],
       "@deepseek-ai/dsh-subprocess-local": ["./packages/subprocess/subprocess-local/src"],
       "@deepseek-ai/dsh-system-prompt": ["./packages/core/system-prompt/src"],
       "@deepseek-ai/dsh-system-prompt/invariant": ["./packages/core/system-prompt/src/invariant.ts"],

+ 0 - 3
tsconfig.host.json

@@ -244,8 +244,6 @@
     { "path": "./packages/subprocess/subprocess" },
     { "path": "./packages/subprocess/subprocess-local" },
     { "path": "./packages/subprocess/win32-process" },
-    { "path": "./packages/e2b/e2b" },
-    { "path": "./packages/e2b/subprocess-e2b" },
     { "path": "./packages/shell/shell" },
     { "path": "./packages/terminal/terminal" },
     { "path": "./packages/terminal/terminal-bash" },
@@ -271,7 +269,6 @@
     { "path": "./packages/shell/tool-bash" },
     { "path": "./packages/fs/fs" },
     { "path": "./packages/fs/fs-local" },
-    { "path": "./packages/e2b/fs-e2b" },
     { "path": "./packages/fs/fs-observation-policy" },
     { "path": "./packages/fs/fs-sandbox" },
     { "path": "./packages/fs/tool-fs" },