@erichll/pi-sandbox 0.6.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 +18 -1
- package/README.md +8 -0
- package/package.json +2 -2
- package/src/external-worker-launcher.mjs +1 -1
- package/src/runner.ts +75 -4
- package/src/srt-broker.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.6.
|
|
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.
|
|
4
21
|
|
|
5
22
|
- Retarget the development compatibility target from `pi-subagents 0.42.1` to
|
|
6
23
|
`pi-subagents 0.44.0` and update the coexistence assertions, capability
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erichll/pi-sandbox",
|
|
3
|
-
"version": "0.6.
|
|
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.
|
|
53
|
+
"@anthropic-ai/sandbox-runtime": "0.0.71",
|
|
54
54
|
"@erichll/pi-auto-review": "0.3.4"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
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(
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
package/src/srt-broker.mjs
CHANGED
|
@@ -88,7 +88,7 @@ async function main() {
|
|
|
88
88
|
undefined,
|
|
89
89
|
process.cwd(),
|
|
90
90
|
);
|
|
91
|
-
const workerTempDir = process.env.
|
|
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
|