DSH / Atlas
2026-09-11implementedarchitecture

Subprocess control pipe

Subprocess control pipe

A managed Node program can write arbitrary bytes to stdout and stderr. A host protocol sharing either stream cannot distinguish those bytes from program diagnostics without restricting ordinary Node behavior. Windows process wrappers also require explicit descriptor inheritance before the child runtime allocates its own descriptors.

English

Problem

A managed Node program can write arbitrary bytes to stdout and stderr. A host protocol sharing either stream cannot distinguish those bytes from program diagnostics without restricting ordinary Node behavior. Windows process wrappers also require explicit descriptor inheritance before the child runtime allocates its own descriptors.

Decision

Ordinary subprocess requests optionally set stdio.control: 'pipe' and receive a raw Duplex as handle.control. The target opens fd 7 through @deepseek-ai/dsh-subprocess/control. The provider owns the environment marker; the child helper consumes it. Consumers own bounded framing, message validation, backpressure, and endpoint closure. Standard output collection and managed-range lifetime retain their existing semantics. The provider tracks open control endpoints independently until they close, including after their managed range exits, and disposal closes remaining endpoints after attempting range teardown.

POSIX launchers preserve fd 7 across exec. Windows ordinary Job and restricted-token launchers place the pipe at slot 7 in the CRT startup descriptor table, preserve standard handles, and leave slots 3–6 closed in the payload. Each wrapper closes its carrier after transferring ownership. Handle inheritance is enabled only around process creation. This channel grants no host capability: the child remains untrusted, and every host tool request requires its usual dispatch and approval checks.

Control pipes use Node's overlapped stdio disposition, which equals pipe on POSIX and creates Windows handles with FILE_FLAG_OVERLAPPED. Reads and writes can then proceed independently, including a child sending its first message before the host sends anything.

Windows managed-range proof observes the runner process exit and its private IPC result independently of caller stream drains. A clean runner exit with a received result confirms its range; a clean exit without a result remains pending only until the IPC channel closes. Paused control output cannot delay this proof or prevent provider disposal from closing the endpoint.

The filesystem and subprocess services remain replaceable together by remote providers. Neither the public handle nor its request exposes a host path, process identifier, execution-world flag, or transport negotiation catalogue. Terminal allocation remains asynchronous and does not gain an extra descriptor.

Alternatives considered

Stdout framing. Native code and ordinary process.stdout.write can emit arbitrary bytes, so protocol integrity would depend on intercepting program output.

Synchronous Windows pipes. A blocking read on an inherited synchronous pipe can prevent a concurrent write on the same handle from progressing. A parent-first echo does not expose this deadlock; child-first readiness and teardown require overlapped handles.

Node IPC. The Windows process supervisor already uses a private IPC channel. Coupling payload requests to that management protocol would expose supervisor operations and complicate remote transport.

A late Windows descriptor replacement. Replacing fd 7 after Node starts can overwrite an internal descriptor. The CRT startup table reserves it before runtime initialization and keeps the child API identical across hosts.

A different descriptor per platform. Per-platform numbers would add bootstrap branching without removing the native startup work. One fixed slot also allows remote providers to preserve the same child API.

Consequences

Native wrappers must preserve and close one extra pipe explicitly. The subprocess service does not interpret control messages or buffer them for callers, so protocol consumers must bound their own retained input and output. The OS sandbox and managed process owner remain responsible for confinement and teardown; a dedicated transport is not a JavaScript security boundary.

中文

问题

受管 Node 程序可以向 stdout 和 stderr 写入任意字节。若宿主协议共用其中任一流,就无法在不限制普通 Node 行为的前提下区分协议字节和程序诊断。Windows 进程包装器还要求在子运行时分配自身描述符前显式设置描述符继承。

决策

普通 subprocess 请求可以设置 stdio.control: 'pipe',并通过 handle.control 收到原始 Duplex。目标通过 @deepseek-ai/dsh-subprocess/control 打开 fd 7。提供方拥有环境标记;子进程辅助函数会消费它。消费方负责有界分帧、消息校验、背压和端点关闭。标准输出收集与受管范围生命周期保留现有语义。提供方独立跟踪打开的控制端点,直到它们关闭,包括受管范围退出之后;销毁时先尝试受管范围拆卸,再关闭剩余端点。

POSIX 启动器在 exec 时保留 fd 7。Windows 普通 Job 与受限令牌启动器把管道放入 CRT 启动描述符表的槽 7,保留标准句柄,并让负载中的槽 3–6 保持关闭。每层包装器在转移所有权后关闭自身承载端。句柄继承仅在进程创建期间启用。该通道不授予任何宿主能力:子进程仍不可信,每次宿主工具请求都需要通常的分发与审批检查。

控制管道使用 Node 的 overlapped stdio 处置方式:它在 POSIX 上等同于 pipe,在 Windows 上创建带有 FILE_FLAG_OVERLAPPED 的句柄。因此读写可以独立进行,包括子进程在宿主发送任何内容前发出第一条消息。

Windows 受管范围证明独立观察 runner 进程退出及其私有 IPC 结果,不依赖调用方排空流。收到结果且 runner 正常退出即可确认范围结束;正常退出但未收到结果时,最多等待到 IPC 通道关闭。暂停的控制输出不会延迟该证明,也不会阻止提供方销毁时关闭端点。

文件系统与 subprocess 服务仍可由远程提供方成对替换。公共句柄和请求均不公开宿主路径、进程标识、执行世界标志或传输协商目录。终端分配保持异步,且不增加额外描述符。

考虑过的替代方案

Stdout 分帧。 原生代码和普通 process.stdout.write 可以输出任意字节,因此协议完整性将依赖于拦截程序输出。

同步 Windows 管道。 继承的同步管道上的阻塞读取可能阻止同一句柄上的并发写入继续执行。宿主先发送的回显无法暴露该死锁;子进程先报告就绪以及拆卸都需要重叠 I/O 句柄。

Node IPC。 Windows 进程监督器已经使用私有 IPC 通道。把负载请求耦合到该管理协议会暴露监督器操作,并使远程传输复杂化。

在 Windows 启动后替换描述符。 Node 启动后替换 fd 7 可能覆盖内部描述符。CRT 启动表在运行时初始化前保留它,并保持各宿主的子进程 API 一致。

各平台使用不同描述符。 不同平台的编号会增加引导分支,却不能省去原生启动工作。固定槽位也让远程提供方可以保留相同的子进程 API。

后果

原生包装器必须显式保留并关闭一条额外管道。subprocess 服务不解释控制消息,也不替调用方缓冲消息,因此协议消费方必须自行限制保留的输入和输出。操作系统沙箱与受管进程拥有者仍负责限制和拆卸;独立传输不是 JavaScript 安全边界。