# Agent Note: 基于文件系统与进程管理执行世界的可移植消费方

Status: implemented

[English](2026-07-28-portable-execution-world-consumers.md) | 中文

## 问题

文件系统与进程管理 seam 使文件访问和普通进程访问具备可替换性，但 PTY 和 LSP 仍直接调用宿主 Node API。因此，即使领域行为没有变化，远程执行提供方看起来仍需要独立的 PTY 与 LSP 包。这些包只会成为浅层适配器：每个包都仅为替换文件与进程操作而复制一个现有消费方。

只有文件操作、命令、终端和语言服务器共享同一个沙箱身份时，远程编码世界才有用。若把完整 harness 移入该沙箱，还会把提供方实验与插件加载、凭据、模型传输、会话持久性、监督和部署纠缠在一起。

普通管道无法满足其中一项要求。持久终端需要分配 PTY、检查前台进程组并发送信号，以及清理完整的终端会话。如果假设可以在 `dsh-terminal-bash` 中基于普通 `spawn()` 句柄重建这些操作，最终不是泄漏提供方内部细节，就是削弱其生命周期约定。

## 决策

`ctx.fs` 与 `ctx.subprocess` 共同定义一个执行世界。共同挂载的提供方必须描述相同的路径命名空间、可执行文件、进程和终端会话；上层能力消费这两个接口，而不引用具体提供方。

文件系统接口负责其他能力需要的路径事实，同时不公开其不透明目标身份：规范化进程路径、规范化 `file:` URI 和包含关系。现有完整文本与流式文本操作仍归文件系统负责；协议消费方在消费流时执行各自的保留上限。

进程管理接口负责可执行文件查找与进程原语：以原始或收集模式 spawn 普通进程，以及 `spawnTerminal()`。终端操作是一项深层原语，其句柄负责文本 I/O、前台进程组、信号发送，以及一项须等待的 TERM→KILL 操作；该操作会结算所有在途句柄调用，并使提供方仍可观察到的每个会话成员完全停稳。其信号只取消分配；句柄一经发布，便负责自身生命周期。提示符检测、空闲推断、scrollback、沙箱策略和所有者生命周期仍由 PTY 消费方负责。

通用消费方使用该执行世界：

- `dsh-bash-local` 继续把 Bash 语义映射到普通的 `ctx.subprocess.spawn()`。
- `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 负责可变文件系统、受管命令与 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。

## 验证

聚焦的包测试套件锁定了沙箱生命周期、规范化路径分帧、文件系统元数据与原子版本、进程管理发布／回滚、终端文本 I/O 与会话清理、输出上限、取消、资源释放和不变式注册。一项受凭据门控的 Loader 组合通过源代码导入与构建后导出运行同一套三包提供方组合，其中包括 FS/Bash 可见性、恶意登录 profile、跨字节边界拆分的 UTF-8 输出、进程与终端清理、LSP 查询、宿主工作区隔离，以及最终沙箱删除。

## 考虑过的替代方案

**为每个远程提供方分别保留 PTY 与 LSP 包。** 不予采纳，因为这会在现有 seam 之上重复实现提供方机制。删除检验揭示了这一问题：删除这些适配器不应使领域行为散落到远程提供方中；通用消费方本已负责这些行为。

**为每项能力或工具创建独立沙箱。** 不予采纳，因为文件与进程操作将无法共享身份或状态，从而破坏编码用例，并增加生命周期所有者的数量。

**把终端建模为普通的管道子进程。** 不予采纳，因为管道无法分配控制终端、确定当前前台进程组或证明完整终端会话已清理。一项终端原语比公开特定于执行基底的逃生口更小，也更能如实表达约定。

**把 PTY 就绪判断与会话策略移入进程管理服务。** 不予采纳，因为这些属于持久终端消费方的语义，而非 OS 进程机制。进程管理提供方负责只有其执行基底才能完成的操作；`dsh-terminal-bash` 负责 Harness 终端的语义。

**分别公开终端终止与完全停稳操作，并提供共享生命周期控制器。** 不予采纳，因为每个终端消费方都需要相同的单一清理结果。拆分操作会把提供方簿记、有界观察者和重试语义暴露出来，却没有生产消费方；由提供方提供一个须等待的操作，接口更深。

**在文件系统 seam 中新增稳定的有界读取原语。** 不予采纳，因为只有 LSP 需要完整文档字节上限，而它可以在消费现有文本流时执行该上限。第二项原语会迫使每个提供方实现稳定句柄和不跟随符号链接的机制，远程提供方甚至需要辅助协议，却没有已观察到的并发替换缺陷。

**在远程环境中运行整个 harness。** 不予采纳，因为这是另一种部署模型。让执行能力可移植，并不意味着移动模型调用、会话状态、插件状态或 agent loop。

**把所有提供方操作都放进一个共享所有者包。** 不予采纳，因为沙箱身份与生命周期是所有者唯一的关注点。文件系统与进程管理保留各自独立的约定、测试和消费方，同时避免把所有者变成无边界的能力集合。

**只通过 shell 命令实现远程文件系统操作。** 不予采纳，因为这会丢弃现有文件工具已消费的结构化文件系统身份、错误、流式输出、版本保护和原子变更语义。

**新增通用分布式运行时抽象，或重新连接活跃句柄。** 不予采纳，因为现有能力 seam 已承载经证实的约定，而仅凭远程身份无法重建回调、待处理 promise、权限、协议状态或输出游标。新增一层只会推测 POC 边界之外的持久化与同步问题。

## 后果

远程执行提供方只需实现共享沙箱所有者，以及文件系统与进程管理适配器。Bash、PTY 与 LSP 组合在这些适配器之上，因此对这些能力的修复仍与提供方无关。

基础接口更宽，一对文件系统／进程管理提供方必须在同一个执行世界上保持一致。新增操作仅限当前通用消费方所需的事实与生命周期机制；模型 schema、协议分帧、就绪策略和呈现不会渗入提供方。

本地实现承接 `node-pty` 和平台进程检查，因为它负责本地终端机制。这种代码迁移不会削弱终端拆卸：dispose（资源释放）会在终止顶层 shell 前后清理后代进程，等待前台检查期间保留下来且受精确 PID 身份围栏保护的后代进程，并继续追踪在顶层进程退出后仍存活的 Linux 会话成员。macOS 无法在 POSIX 会话 leader 退出后枚举该会话，因此在两次检查快照之间重新设定父进程的子进程仍是明确的本地提供方限制，而不是把进程机制移回 PTY 消费方的理由。

E2B 组合证明，共享沙箱所有者加上文件系统与进程管理适配器，就足以在保持上层能力与提供方无关的同时，把可变编码世界移出宿主。其 POC 限制仍明确在案：SDK 会把完整命令传输内容保留在宿主内存中；远程启动无法同步发布 PID；无法获得精确的终端 stdin 等待状态与独立信号事实；基于数值 PID/PGID 的操作没有身份围栏；初始环境探测无法向已在运行的同 UID 进程隐藏未知的沙箱默认 secret；适配器产物会一直保留到沙箱删除。这些是提供方限制，不是引入兼容性 shim 或更多 E2B 包的理由。
