@erichll/pi-sandbox 0.5.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1 - 2026-08-09
4
+
5
+ - Upgrade `@anthropic-ai/sandbox-runtime` from `0.0.67` to `0.0.71` (path
6
+ normalization, proxy abort and query-string redaction hardening, per-command
7
+ violation attribution, network domain `:port` suffixes and per-entry deny
8
+ reasons, compacted macOS Seatbelt profiles). No breaking contract changes.
9
+ - Give every sandboxed command (main-agent Bash and builtin subagents) a
10
+ **private, writable temp directory**: each invocation creates an isolated
11
+ `pi-sandbox-tmp-*` subdirectory of the host temp tree, adds it to the
12
+ sandbox's read/write allowlists, and points the command's `TMPDIR`, `TMP`,
13
+ and `TEMP` at it. The shared host temp root stays read-only to the sandbox;
14
+ the directory is removed when the command exits. If the host temp directory
15
+ is unavailable (e.g. a read-only `/tmp`), the command degrades to the
16
+ previous denied-temp behaviour instead of failing. This mirrors the outer
17
+ external-worker isolation and unifies the broker temp-dir env under
18
+ `PI_SANDBOX_TMPDIR`.
19
+ - Add an injectable `createTempDir` test seam plus a broker probe fixture and
20
+ runner coverage for the private-temp success and degrade paths.
21
+
22
+ - Retarget the development compatibility target from `pi-subagents 0.42.1` to
23
+ `pi-subagents 0.44.0` and update the coexistence assertions, capability
24
+ boundary, and compatibility gate. The 0.44.0 line lands internal
25
+ workflow/mission/schedule fixes that do not change pi-sandbox's contract
26
+ (tool-ownership detection, `PI_SUBAGENT_PI_BINARY` worker bootstrap).
27
+ - Adapt the compatibility gate's "direct child" probe to the `workflowScript`
28
+ single-run form: pi-subagents 0.43.0 removed public top-level `agent`/`task`
29
+ direct execution, so the external provider's direct single-child launch is
30
+ now `subagent({ workflowScript: "return runs.run('main', { agent, task })" })`.
31
+ This unblocks the `subagents.externalWorkerIsolation: "enforce"` gate, which
32
+ previously timed out when the isolated worker could not reconcile the removed
33
+ direct signature with the newer `workflowScript`-only contract.
34
+
3
35
  ## 0.5.0 - 2026-08-08
4
36
 
5
37
  - Pin the development compatibility target to `pi-subagents 0.42.1` and add
package/README.md CHANGED
@@ -18,6 +18,14 @@ also run complete process-backed subagent trees inside independent sandboxes.
18
18
  - Every Bash command and persistent subagent session owns a separate broker
19
19
  process and Sandbox Runtime manager. Concurrent workers therefore keep
20
20
  independent policy, proxy, approval, and cleanup lifecycles.
21
+ - Each sandboxed command is given a **private, writable temp directory**: a
22
+ fresh `pi-sandbox-tmp-*` subdirectory of the host temp tree that is added to
23
+ the sandbox's read/write allowlists, with the command's `TMPDIR`, `TMP`, and
24
+ `TEMP` pointed at it. Only that isolated directory is writable — the shared
25
+ host temp root (e.g. `/tmp` itself) stays read-only to the sandbox — and it
26
+ is removed when the command exits. If the host temp directory is unavailable
27
+ (e.g. a read-only `/tmp`) the command degrades to the previous denied-temp
28
+ behaviour rather than failing.
21
29
  - Project and global Pi security configuration, the trusted extensions tree,
22
30
  the installed package, Git hooks, and other Sandbox Runtime mandatory paths
23
31
  remain write-protected.
@@ -74,12 +82,12 @@ Supported modes:
74
82
  itself is not wrapped while `externalWorkerIsolation` is `off`.
75
83
  - `off`: protect Bash only.
76
84
 
77
- ### `pi-subagents` 0.42.1 capability boundary
85
+ ### `pi-subagents` 0.44.0 capability boundary
78
86
 
79
87
  `pi-sandbox` verifies the following combination in its test suite and release
80
88
  gate:
81
89
 
82
- | Capability | `builtin` | `pi-subagents` 0.42.1 |
90
+ | Capability | `builtin` | `pi-subagents` 0.44.0 |
83
91
  | --- | --- | --- |
84
92
  | Outer worker sandbox | Yes | Opt-in (`externalWorkerIsolation: "enforce"`) |
85
93
  | Bash sandbox | Yes | Yes |
@@ -92,6 +100,12 @@ Sandbox Runtime boundary. Do not treat inherited Bash sandboxing as complete
92
100
  worker isolation unless the trusted global configuration explicitly enables
93
101
  `enforce`.
94
102
 
103
+ Note on the external provider's public execution surface: pi-subagents 0.43.0
104
+ removed top-level `agent`/`task` direct execution. Every public `subagent`
105
+ launch — including a minimal single child — is expressed as a `workflowScript`
106
+ such as `return runs.run('main', { agent, task })`. The compatibility gate's
107
+ "direct single child" probe uses that single-run form.
108
+
95
109
  The configuration parser rejects malformed JSON, unknown fields, and unknown
96
110
  providers instead of silently weakening isolation.
97
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erichll/pi-sandbox",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
4
4
  "description": "Sandbox Runtime-backed Linux and macOS sandbox with pi-auto-review approval",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -50,7 +50,7 @@
50
50
  "test": "node --experimental-strip-types --test test/*.test.ts"
51
51
  },
52
52
  "dependencies": {
53
- "@anthropic-ai/sandbox-runtime": "0.0.67",
53
+ "@anthropic-ai/sandbox-runtime": "0.0.71",
54
54
  "@erichll/pi-auto-review": "0.3.4"
55
55
  },
56
56
  "peerDependencies": {
@@ -60,7 +60,7 @@
60
60
  "devDependencies": {
61
61
  "@earendil-works/pi-coding-agent": "0.83.0",
62
62
  "@types/node": "^22.15.3",
63
- "pi-subagents": "0.42.1",
63
+ "pi-subagents": "0.44.0",
64
64
  "typescript": "^6.0.3"
65
65
  }
66
66
  }
@@ -215,7 +215,7 @@ if (!realPi) {
215
215
  detached: true,
216
216
  env: {
217
217
  ...process.env,
218
- PI_SANDBOX_EXTERNAL_WORKER_TMPDIR: workerTempDir,
218
+ PI_SANDBOX_TMPDIR: workerTempDir,
219
219
  CLAUDE_CODE_TMPDIR: workerTempDir,
220
220
  TMPDIR: workerTempDir,
221
221
  TMP: workerTempDir,
package/src/runner.ts CHANGED
@@ -3,6 +3,9 @@ import {
3
3
  type ChildProcess,
4
4
  type ForkOptions,
5
5
  } from "node:child_process";
6
+ import { mkdtempSync, rmSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
6
9
  import { fileURLToPath } from "node:url";
7
10
  import type { Writable } from "node:stream";
8
11
  import { getShellConfig } from "@earendil-works/pi-coding-agent";
@@ -38,6 +41,13 @@ export type SandboxCommandOptions = {
38
41
  ): Promise<SandboxApprovalAction>;
39
42
  policy?: SandboxPolicy;
40
43
  shellPath?: string;
44
+ /**
45
+ * Test seam. Production callers leave unset (uses a fresh private temp dir
46
+ * under the host temp dir). Returning undefined simulates a read-only or
47
+ * otherwise unavailable temp directory, in which case the command degrades
48
+ * to the previous (denied-temp) behaviour.
49
+ */
50
+ createTempDir?(): string | undefined;
41
51
  directInvocation?: {
42
52
  command: string;
43
53
  args: string[];
@@ -76,6 +86,45 @@ const BROKER_MODULE = fileURLToPath(
76
86
  new URL("./srt-broker.mjs", import.meta.url),
77
87
  );
78
88
 
89
+ /**
90
+ * Env var the broker honors to point the sandboxed child's temp directories
91
+ * at a private, writable directory (see srt-broker.mjs). Shared with the
92
+ * external worker launcher, which creates the same per-worker temp dir.
93
+ */
94
+ const SANDBOX_TMPDIR_ENV = "PI_SANDBOX_TMPDIR";
95
+ const COMMAND_TMPDIR_PREFIX = "pi-sandbox-tmp-";
96
+
97
+ /**
98
+ * Create a private writable temp directory for one sandboxed command. The
99
+ * writable temp is per-command and isolated (a fresh subdirectory of the host
100
+ * temp dir, never the shared /tmp itself), so tools that honour TMPDIR keep
101
+ * working without opening the host's world-writable temp to the sandbox. When
102
+ * the host temp directory is unavailable (e.g. read-only /tmp) we degrade to
103
+ * the previous behaviour rather than fail the command.
104
+ */
105
+ function createCommandTempDir(): string | undefined {
106
+ try {
107
+ return mkdtempSync(join(tmpdir(), COMMAND_TMPDIR_PREFIX));
108
+ } catch {
109
+ return undefined;
110
+ }
111
+ }
112
+
113
+ /** Add a private temp dir to the sandbox's read/write allowlists (non-mutating). */
114
+ function withWritableTempDir(
115
+ policy: SandboxPolicy,
116
+ tempDir: string,
117
+ ): SandboxPolicy {
118
+ return {
119
+ ...policy,
120
+ filesystem: {
121
+ ...policy.filesystem,
122
+ allowRead: [...policy.filesystem.allowRead, tempDir],
123
+ allowWrite: [...policy.filesystem.allowWrite, tempDir],
124
+ },
125
+ };
126
+ }
127
+
79
128
  function killProcessTree(child: ChildProcess): void {
80
129
  if (child.pid === undefined) return;
81
130
  try {
@@ -108,7 +157,10 @@ function commandInvocation(options: SandboxCommandOptions): {
108
157
  };
109
158
  }
110
159
 
111
- function spawnBroker(options: SandboxCommandOptions): ChildProcess {
160
+ function spawnBroker(
161
+ options: SandboxCommandOptions,
162
+ env: NodeJS.ProcessEnv,
163
+ ): ChildProcess {
112
164
  const broker = options.broker ?? {
113
165
  modulePath: BROKER_MODULE,
114
166
  execArgv: [],
@@ -116,7 +168,7 @@ function spawnBroker(options: SandboxCommandOptions): ChildProcess {
116
168
  const forkOptions: ForkOptions = {
117
169
  cwd: options.cwd,
118
170
  detached: true,
119
- env: options.env ?? process.env,
171
+ env,
120
172
  execArgv: broker.execArgv ?? [],
121
173
  stdio: ["pipe", "pipe", "pipe", "ipc"],
122
174
  };
@@ -133,8 +185,20 @@ export async function runSandboxedCommand(
133
185
  if (options.signal?.aborted) throw new Error("aborted");
134
186
 
135
187
  const invocation = commandInvocation(options);
136
- const policy = options.policy ?? createDefaultPolicy(options.cwd);
137
- const broker = spawnBroker(options);
188
+ let policy = options.policy ?? createDefaultPolicy(options.cwd);
189
+ let brokerEnv = options.env ?? process.env;
190
+ // Give every sandboxed command a private, writable temp dir (see
191
+ // createCommandTempDir). The broker receives SANDBOX_TMPDIR_ENV and redirects
192
+ // the child's TMPDIR/TMP/TEMP there. The test seam, when provided, is
193
+ // authoritative (returning undefined simulates an unavailable temp dir).
194
+ const tempDir = options.createTempDir
195
+ ? options.createTempDir()
196
+ : createCommandTempDir();
197
+ if (tempDir) {
198
+ policy = withWritableTempDir(policy, tempDir);
199
+ brokerEnv = { ...brokerEnv, [SANDBOX_TMPDIR_ENV]: tempDir };
200
+ }
201
+ const broker = spawnBroker(options, brokerEnv);
138
202
  let timeoutHandle: NodeJS.Timeout | undefined;
139
203
  let timedOut = false;
140
204
  let onAbort: (() => void) | undefined;
@@ -192,6 +256,13 @@ export async function runSandboxedCommand(
192
256
  if (broker.exitCode === null && broker.signalCode === null) {
193
257
  killProcessTree(broker);
194
258
  }
259
+ if (tempDir) {
260
+ try {
261
+ rmSync(tempDir, { recursive: true, force: true });
262
+ } catch {
263
+ // Best-effort cleanup; the temp dir is beneath the host temp tree.
264
+ }
265
+ }
195
266
  }
196
267
  }
197
268
 
@@ -88,7 +88,7 @@ async function main() {
88
88
  undefined,
89
89
  process.cwd(),
90
90
  );
91
- const workerTempDir = process.env.PI_SANDBOX_EXTERNAL_WORKER_TMPDIR;
91
+ const workerTempDir = process.env.PI_SANDBOX_TMPDIR;
92
92
  target = spawn(wrapped.argv[0], wrapped.argv.slice(1), {
93
93
  cwd: process.cwd(),
94
94
  env: workerTempDir