@deepseek-ai/dsh-subprocess-e2b 0.1.2-rc.1 → 0.1.5-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +5 -6
- package/README.zh.md +5 -6
- package/lib/index.js +31 -13
- package/lib/types/process.d.ts +2 -4
- package/package.json +7 -7
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/e2b/subprocess-e2b/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 1c88e8b8650b93d326086f53311840922656244b
|
|
6
|
+
README.zh.md: 74c43ae98724e652618026c9eb3191998fbf52a9
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
`dsh-subprocess-e2b` runs the agent's shell commands and terminals inside
|
|
12
|
+
`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.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ Use this package when the agent's shell commands and terminals should run inside
|
|
|
29
29
|
|
|
30
30
|
### When to choose it
|
|
31
31
|
|
|
32
|
-
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.
|
|
32
|
+
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.
|
|
33
33
|
|
|
34
34
|
### Configuration
|
|
35
35
|
|
|
@@ -71,7 +71,7 @@ This section explains the design decisions behind the provider and points at the
|
|
|
71
71
|
|
|
72
72
|
### Design philosophy
|
|
73
73
|
|
|
74
|
-
- **
|
|
74
|
+
- **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.
|
|
75
75
|
- **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.
|
|
76
76
|
- **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.
|
|
77
77
|
|
|
@@ -91,9 +91,9 @@ This section explains the design decisions behind the provider and points at the
|
|
|
91
91
|
|
|
92
92
|
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.
|
|
93
93
|
|
|
94
|
-
###
|
|
94
|
+
### Private process identity and publication
|
|
95
95
|
|
|
96
|
-
The synchronous seam returns a handle immediately while the command starts asynchronously
|
|
96
|
+
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.
|
|
97
97
|
|
|
98
98
|
### Environment boundary
|
|
99
99
|
|
|
@@ -142,7 +142,6 @@ No direct invalidation: the consumer seams own any request-prefix changes; this
|
|
|
142
142
|
These limits define when the provider is a poor fit or needs special operational care. They are current package constraints, not a task backlog.
|
|
143
143
|
|
|
144
144
|
- **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.
|
|
145
|
-
- **Synchronous-PID consumers are unsupported** — `pid` remains `-1` during remote startup; consumers that require a positive PID immediately, including the ACP child backend, cannot use this provider unchanged.
|
|
146
145
|
- **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.
|
|
147
146
|
- **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.
|
|
148
147
|
- **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.
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-reference"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
`dsh-subprocess-e2b` 让 agent(智能体)的 shell
|
|
12
|
+
`dsh-subprocess-e2b` 让 agent(智能体)的 shell 命令与交互式终端在 E2B 远程沙箱而非宿主中运行。现有的命令、终端与语言服务器工作流无需 E2B 专用工具即可继续使用。宿主环境变量与密钥不会传入沙箱;只有显式请求的环境条目会进入沙箱。请与 `dsh-e2b`、`dsh-fs-e2b` 一起使用,让命令、终端与文件共享同一个沙箱。远程执行会增加延迟,因为每条命令都需要异步初始化。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ kind: "package-reference"
|
|
|
29
29
|
|
|
30
30
|
### 何时选择
|
|
31
31
|
|
|
32
|
-
当组合已经使用 E2B
|
|
32
|
+
当组合已经使用 E2B 沙箱且希望命令与终端在其中运行时,选择本包。宿主执行请选择本地子进程包。
|
|
33
33
|
|
|
34
34
|
### 配置
|
|
35
35
|
|
|
@@ -71,7 +71,7 @@ agent 可以在沙箱中打开交互式终端、发送输入、读取输出,
|
|
|
71
71
|
|
|
72
72
|
### 设计理念
|
|
73
73
|
|
|
74
|
-
-
|
|
74
|
+
- **提供方私有的远程身份。** 同步 seam 从不阻塞在网络请求上。包装层的私有文件会异步发布进程组身份,供 stdin、观察、终止与完全停稳检查使用,同时发布直接退出码与 spill 有效性;该身份不是请求目标的 PID。
|
|
75
75
|
- **单一终止阶梯。** 终止、回滚与资源释放共享同一条进程组信号路径——先 `SIGTERM`,再 `SIGKILL` 加 SDK kill 回退——并把已证明的完全停稳视为最终状态。
|
|
76
76
|
- **环境必须显式。** 宿主内容与形似凭据的内容都不会隐式进入沙箱;每个环境值都会被清理,每个 `spec.env` 条目都是显式选择。
|
|
77
77
|
|
|
@@ -91,9 +91,9 @@ agent 可以在沙箱中打开交互式终端、发送输入、读取输出,
|
|
|
91
91
|
|
|
92
92
|
引导脚本会从沙箱 PATH 解析自身所需的工具,拒绝任何缺失或不可执行的路径,通过 `env -i` 与 `setsid --wait` 执行 exec,把进程组 ID 与退出码发布到 `ctx.e2b.runtimeRoot/processes` 下的私有文件,并把 stdout 与 stderr 重定向到带保留完成帧的 base64 编码器;`tee` 与 `head -c` 约束可选 spill 文件的大小。
|
|
93
93
|
|
|
94
|
-
###
|
|
94
|
+
### 私有进程身份与发布
|
|
95
95
|
|
|
96
|
-
同步 seam
|
|
96
|
+
同步 seam 会立即返回句柄,同时命令异步启动。包装层会发布私有进程组 ID,供 stdin、观察、终止与完全停稳检查使用,但该 ID 不是请求目标的 PID。启动信号会在分配前中止环境与私有状态准备;分配开始后,取消会等待可清理的临时 SDK 句柄。
|
|
97
97
|
|
|
98
98
|
### 环境边界
|
|
99
99
|
|
|
@@ -142,7 +142,6 @@ agent 可以在沙箱中打开交互式终端、发送输入、读取输出,
|
|
|
142
142
|
这些限制说明本提供方何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。
|
|
143
143
|
|
|
144
144
|
- **SDK 仍会在宿主内存中保留完整命令输出**:即使本适配器公开的是有界原始字节尾部,E2B `CommandHandle.stdout` 与 `.stderr` 仍会累积 base64 传输内容,因此无法达到子进程 seam 通常提供的宿主内存边界,而且传输保留量大于源数据流。
|
|
145
|
-
- **不支持需要同步 PID 的消费方**:远程启动期间 `pid` 保持 `-1`;包括 ACP 子进程后端在内,要求立即获得正 PID 的消费方无法原样使用本提供方。
|
|
146
145
|
- **私有状态随沙箱生命周期存在**:进程目录与有效的 spill 文件会留在 `.dsh-e2b` 下,直到所有者删除沙箱;本 POC 不提供沙箱内清理。
|
|
147
146
|
- **控制状态与沙箱用户同 UID**:E2B 以同一默认用户运行每条命令,因此 `0700`/`0600` 权限无法把 `.dsh-e2b` 控制文件与并发运行的沙箱进程隔离开;真正的隔离需要 E2B 提供按命令用户或带外控制通道。
|
|
148
147
|
- **数值进程身份没有复用围栏**:E2B 公开基于数值 PID/PGID 的输入、信号发送与清理操作,却没有与身份原子绑定的替代方案;在 E2B 新增身份原语,或实际故障证明需要更窄的协议之前,替代方案继续延后。
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { posix } from "node:path";
|
|
3
|
+
import { inspect } from "node:util";
|
|
3
4
|
import z from "@deepseek-ai/schemastery";
|
|
4
5
|
import { SENSITIVE_ENV_PATTERN, SubprocessRuntime } from "@deepseek-ai/dsh-subprocess";
|
|
5
6
|
import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
|
|
@@ -399,7 +400,7 @@ function waitWithSignal(promise, signal) {
|
|
|
399
400
|
});
|
|
400
401
|
});
|
|
401
402
|
}
|
|
402
|
-
/** E2B-backed subprocess handle with
|
|
403
|
+
/** E2B-backed subprocess handle with private remote process-group tracking. */
|
|
403
404
|
var E2BSubprocessHandle = class {
|
|
404
405
|
runtime;
|
|
405
406
|
spec;
|
|
@@ -421,7 +422,7 @@ var E2BSubprocessHandle = class {
|
|
|
421
422
|
stderrReader;
|
|
422
423
|
paths;
|
|
423
424
|
controlEnvs = {};
|
|
424
|
-
|
|
425
|
+
remoteProcessGroupId;
|
|
425
426
|
outputTransportError;
|
|
426
427
|
outputDrainExpired = false;
|
|
427
428
|
stateDirectoryCreated = false;
|
|
@@ -465,10 +466,6 @@ var E2BSubprocessHandle = class {
|
|
|
465
466
|
this.done.catch(() => {});
|
|
466
467
|
if (spec.signal?.aborted === true) this.terminate();
|
|
467
468
|
}
|
|
468
|
-
/** Remote process id after start; `-1` while E2B startup is pending or after it fails. */
|
|
469
|
-
get pid() {
|
|
470
|
-
return this.remotePid;
|
|
471
|
-
}
|
|
472
469
|
/** @inheritdoc */
|
|
473
470
|
terminate() {
|
|
474
471
|
if (this.quiescenceProven || this.terminationAttempt !== void 0) return;
|
|
@@ -497,7 +494,7 @@ var E2BSubprocessHandle = class {
|
|
|
497
494
|
this.markQuiescent();
|
|
498
495
|
return true;
|
|
499
496
|
}
|
|
500
|
-
if (this.
|
|
497
|
+
if (this.remoteProcessGroupId === void 0) {
|
|
501
498
|
const attempt = this.terminationAttempt;
|
|
502
499
|
if (attempt !== void 0 && await waitWithSignal(attempt.catch(() => void 0), signal) === WAIT_ABORTED) return false;
|
|
503
500
|
this.throwTerminationFailure();
|
|
@@ -524,7 +521,7 @@ var E2BSubprocessHandle = class {
|
|
|
524
521
|
}
|
|
525
522
|
throw error;
|
|
526
523
|
}
|
|
527
|
-
const processGroupId = this.
|
|
524
|
+
const processGroupId = this.remoteProcessGroupId ?? handle.pid;
|
|
528
525
|
while (await this.groupAlive(sandbox, processGroupId, signal)) {
|
|
529
526
|
this.throwTerminationFailure();
|
|
530
527
|
if (!await waitTick(this.pollMs, signal)) return false;
|
|
@@ -577,7 +574,7 @@ var E2BSubprocessHandle = class {
|
|
|
577
574
|
}
|
|
578
575
|
this.commandState.resolve(handle);
|
|
579
576
|
try {
|
|
580
|
-
this.
|
|
577
|
+
this.remoteProcessGroupId = await this.waitForProcessGroupId(sandbox, completion);
|
|
581
578
|
} catch (error) {
|
|
582
579
|
try {
|
|
583
580
|
await this.rollbackUnpublishedGroup(sandbox, handle);
|
|
@@ -779,7 +776,7 @@ var E2BSubprocessHandle = class {
|
|
|
779
776
|
throw settlement.error;
|
|
780
777
|
}
|
|
781
778
|
async rollbackPublishedFailure(error) {
|
|
782
|
-
if (this.
|
|
779
|
+
if (this.remoteProcessGroupId === void 0 || this.quiescenceProven) return error;
|
|
783
780
|
this.terminate();
|
|
784
781
|
try {
|
|
785
782
|
await this.waitForExit();
|
|
@@ -809,13 +806,13 @@ var E2BSubprocessHandle = class {
|
|
|
809
806
|
this.markQuiescent();
|
|
810
807
|
return;
|
|
811
808
|
}
|
|
812
|
-
if (!isValidProcessId(handle.pid) && this.
|
|
809
|
+
if (!isValidProcessId(handle.pid) && this.remoteProcessGroupId === void 0) {
|
|
813
810
|
await handle.kill();
|
|
814
811
|
this.markQuiescent();
|
|
815
812
|
return;
|
|
816
813
|
}
|
|
817
814
|
const sandbox = await this.runtime.getSandbox();
|
|
818
|
-
const processGroupId = this.
|
|
815
|
+
const processGroupId = this.remoteProcessGroupId ?? handle.pid;
|
|
819
816
|
await this.terminateGroup(sandbox, handle, processGroupId);
|
|
820
817
|
}
|
|
821
818
|
async terminateGroup(sandbox, handle, processGroupId) {
|
|
@@ -1322,6 +1319,12 @@ async function spawnE2BTerminal(runtime, spec, stateDir, pollMs) {
|
|
|
1322
1319
|
function requireRepresentableGrace(graceMs) {
|
|
1323
1320
|
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}`);
|
|
1324
1321
|
}
|
|
1322
|
+
function validateNoNullByte(subject, value) {
|
|
1323
|
+
if (!value.includes("\0")) return;
|
|
1324
|
+
const error = /* @__PURE__ */ new TypeError(`${subject} must be a string without null bytes. Received ${inspect(value)}`);
|
|
1325
|
+
Object.assign(error, { code: "ERR_INVALID_ARG_VALUE" });
|
|
1326
|
+
throw error;
|
|
1327
|
+
}
|
|
1325
1328
|
/** E2B command manager registered as `ctx.subprocess`. */
|
|
1326
1329
|
var E2BSubprocessRuntime = class extends SubprocessRuntime {
|
|
1327
1330
|
static inject = ["e2b"];
|
|
@@ -1391,7 +1394,22 @@ var E2BSubprocessRuntime = class extends SubprocessRuntime {
|
|
|
1391
1394
|
const program = spec.argv[0];
|
|
1392
1395
|
if (program === void 0 || program.length === 0) throw new Error("invalid argv: expected a non-empty program name at argv[0]");
|
|
1393
1396
|
requireRepresentableGrace(spec.graceMs);
|
|
1394
|
-
if (spec.signal?.aborted === true)
|
|
1397
|
+
if (spec.signal?.aborted === true) {
|
|
1398
|
+
let reason = "aborted";
|
|
1399
|
+
try {
|
|
1400
|
+
reason = String(spec.signal.reason ?? reason);
|
|
1401
|
+
} catch {}
|
|
1402
|
+
throw new Error(`aborted before spawn: ${reason}`);
|
|
1403
|
+
}
|
|
1404
|
+
spec.argv.forEach((value, index) => {
|
|
1405
|
+
validateNoNullByte(index === 0 ? "The argument 'file'" : `The argument 'args[${String(index - 1)}]'`, value);
|
|
1406
|
+
});
|
|
1407
|
+
validateNoNullByte("The property 'options.cwd'", spec.cwd);
|
|
1408
|
+
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
1409
|
+
if (value === void 0) continue;
|
|
1410
|
+
validateNoNullByte(`The property 'options.env['${key}']'`, key);
|
|
1411
|
+
validateNoNullByte(`The property 'options.env['${key}']'`, value);
|
|
1412
|
+
}
|
|
1395
1413
|
const stateDir = posix.join(this.ctx.e2b.runtimeRoot, "processes", randomUUID());
|
|
1396
1414
|
const handle = new E2BSubprocessHandle(this.ctx.e2b, spec, stateDir, this.pollMs);
|
|
1397
1415
|
this.live.add(handle);
|
package/lib/types/process.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { PassThrough, Writable } from 'node:stream';
|
|
3
3
|
import type { SubprocessHandle, SubprocessOutcome, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess';
|
|
4
4
|
import type E2BRuntime from '@deepseek-ai/dsh-e2b';
|
|
5
|
-
/** E2B-backed subprocess handle with
|
|
5
|
+
/** E2B-backed subprocess handle with private remote process-group tracking. */
|
|
6
6
|
export declare class E2BSubprocessHandle implements SubprocessHandle {
|
|
7
7
|
private readonly runtime;
|
|
8
8
|
private readonly spec;
|
|
@@ -24,7 +24,7 @@ export declare class E2BSubprocessHandle implements SubprocessHandle {
|
|
|
24
24
|
private readonly stderrReader;
|
|
25
25
|
private readonly paths;
|
|
26
26
|
private controlEnvs;
|
|
27
|
-
private
|
|
27
|
+
private remoteProcessGroupId;
|
|
28
28
|
private outputTransportError;
|
|
29
29
|
private outputDrainExpired;
|
|
30
30
|
private stateDirectoryCreated;
|
|
@@ -40,8 +40,6 @@ export declare class E2BSubprocessHandle implements SubprocessHandle {
|
|
|
40
40
|
* @param pollMs - Remote status/liveness poll cadence.
|
|
41
41
|
*/
|
|
42
42
|
constructor(runtime: E2BRuntime, spec: SubprocessSpawnSpec, stateDir: string, pollMs: number);
|
|
43
|
-
/** Remote process id after start; `-1` while E2B startup is pending or after it fails. */
|
|
44
|
-
get pid(): number;
|
|
45
43
|
/** @inheritdoc */
|
|
46
44
|
terminate(): void;
|
|
47
45
|
/** @inheritdoc */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-subprocess-e2b",
|
|
3
3
|
"description": "E2B subprocess implementation for DeepSeek Harness",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5-alpha.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,18 +27,18 @@
|
|
|
27
27
|
],
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@deepseek-ai/dsh-
|
|
31
|
-
"@deepseek-ai/dsh-
|
|
30
|
+
"@deepseek-ai/dsh-timeout": "^0.1.5-alpha.1",
|
|
31
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.5-alpha.1",
|
|
32
32
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
33
|
-
"@deepseek-ai/dsh-e2b": "^0.1.
|
|
33
|
+
"@deepseek-ai/dsh-e2b": "^0.1.5-alpha.1"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@deepseek-ai/schemastery": "^3.18.2"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@deepseek-ai/dsh-e2b": "^0.1.
|
|
40
|
-
"@deepseek-ai/dsh-subprocess": "^0.1.
|
|
41
|
-
"@deepseek-ai/dsh-timeout": "^0.1.
|
|
39
|
+
"@deepseek-ai/dsh-e2b": "^0.1.5-alpha.1",
|
|
40
|
+
"@deepseek-ai/dsh-subprocess": "^0.1.5-alpha.1",
|
|
41
|
+
"@deepseek-ai/dsh-timeout": "^0.1.5-alpha.1",
|
|
42
42
|
"@deepseek-ai/cordis": "^4.0.2"
|
|
43
43
|
}
|
|
44
44
|
}
|