@deepseek-ai/dsh-subprocess-e2b 0.1.2-rc.1 → 0.1.3-alpha.2

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 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: 28f1b3410781fd87e40527d70e823bd9111d57e7
6
- README.zh.md: 3d9a35dea74126ae4cc5d381944b2fb5a86f61ee
5
+ README.md: 8222f81e3b077aabf5c4dd4f2c4d58e501b2846a
6
+ README.zh.md: 449ca331f96cb7799535a94c7b4d7583c2447ead
package/README.md CHANGED
@@ -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. Tooling that needs a process id immediately — for example the ACP child backend — cannot use this package.
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
- - **Deferred remote identity.** The synchronous seam never blocks on the network: the handle publishes its real process-group id asynchronously, and the wrapper's private files are the authority for pid, exit code, and spill validity.
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
- ### Process identity and publication
94
+ ### Private process identity and publication
95
95
 
96
- The synchronous seam returns a handle immediately while the command starts asynchronously; `pid` stays `-1` until the wrapper publishes its process-group id and the adapter validates it, and stdin plus ordinary observation wait for that publication. A startup signal aborts environment and private-state preparation before allocation; once allocation begins, cancellation waits for a provisional SDK handle it can clean.
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
@@ -29,7 +29,7 @@ kind: "package-reference"
29
29
 
30
30
  ### 何时选择
31
31
 
32
- 当组合已经使用 E2B 沙箱且希望命令与终端在其中运行时,选择本包。宿主执行请选择本地子进程包。需要立即获得进程 ID 的工具——例如 ACP(Agent Client Protocol)子进程后端——无法使用本包。
32
+ 当组合已经使用 E2B 沙箱且希望命令与终端在其中运行时,选择本包。宿主执行请选择本地子进程包。
33
33
 
34
34
  ### 配置
35
35
 
@@ -71,7 +71,7 @@ agent 可以在沙箱中打开交互式终端、发送输入、读取输出,
71
71
 
72
72
  ### 设计理念
73
73
 
74
- - **延后的远程身份。** 同步 seam 从不阻塞在网络请求上:句柄异步发布真实进程组 ID,包装层的私有文件是 pid、退出码与 spill 有效性的权威来源。
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 会立即返回句柄,同时命令异步启动;`pid` 在包装层发布进程组 ID 且适配器验证通过之前保持 `-1`,stdin 与常规观察都等待该发布。启动信号会在分配前中止环境与私有状态准备;分配开始后,取消会等待可清理的临时 SDK 句柄。
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 deferred remote PID acquisition. */
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
- remotePid = -1;
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.remotePid <= 0) {
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.remotePid > 0 ? this.remotePid : handle.pid;
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.remotePid = await this.waitForProcessGroupId(sandbox, completion);
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.remotePid <= 0 || this.quiescenceProven) return error;
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.remotePid <= 0) {
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.remotePid > 0 ? this.remotePid : handle.pid;
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) throw new Error(`aborted before spawn: ${String(spec.signal.reason)}`);
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);
@@ -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 deferred remote PID acquisition. */
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 remotePid;
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.2-rc.1",
4
+ "version": "0.1.3-alpha.2",
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-subprocess": "^0.1.2-rc.1",
31
- "@deepseek-ai/dsh-timeout": "^0.1.2-rc.1",
32
- "@deepseek-ai/cordis": "^4.0.2",
33
- "@deepseek-ai/dsh-e2b": "^0.1.2-rc.1"
30
+ "@deepseek-ai/dsh-e2b": "^0.1.3-alpha.2",
31
+ "@deepseek-ai/dsh-timeout": "^0.1.3-alpha.2",
32
+ "@deepseek-ai/dsh-subprocess": "^0.1.3-alpha.2",
33
+ "@deepseek-ai/cordis": "^4.0.2"
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.2-rc.1",
40
- "@deepseek-ai/dsh-subprocess": "^0.1.2-rc.1",
41
- "@deepseek-ai/dsh-timeout": "^0.1.2-rc.1",
39
+ "@deepseek-ai/dsh-e2b": "^0.1.3-alpha.2",
40
+ "@deepseek-ai/dsh-timeout": "^0.1.3-alpha.2",
41
+ "@deepseek-ai/dsh-subprocess": "^0.1.3-alpha.2",
42
42
  "@deepseek-ai/cordis": "^4.0.2"
43
43
  }
44
44
  }