@erichll/pi-sandbox 0.4.3 → 0.6.0
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 +29 -0
- package/README.md +63 -2
- package/package.json +3 -3
- package/src/config.ts +24 -2
- package/src/external-supervisor.ts +159 -0
- package/src/external-worker-launcher.mjs +259 -0
- package/src/index.ts +113 -2
- package/src/srt-broker.mjs +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0 - 2026-08-09
|
|
4
|
+
|
|
5
|
+
- Retarget the development compatibility target from `pi-subagents 0.42.1` to
|
|
6
|
+
`pi-subagents 0.44.0` and update the coexistence assertions, capability
|
|
7
|
+
boundary, and compatibility gate. The 0.44.0 line lands internal
|
|
8
|
+
workflow/mission/schedule fixes that do not change pi-sandbox's contract
|
|
9
|
+
(tool-ownership detection, `PI_SUBAGENT_PI_BINARY` worker bootstrap).
|
|
10
|
+
- Adapt the compatibility gate's "direct child" probe to the `workflowScript`
|
|
11
|
+
single-run form: pi-subagents 0.43.0 removed public top-level `agent`/`task`
|
|
12
|
+
direct execution, so the external provider's direct single-child launch is
|
|
13
|
+
now `subagent({ workflowScript: "return runs.run('main', { agent, task })" })`.
|
|
14
|
+
This unblocks the `subagents.externalWorkerIsolation: "enforce"` gate, which
|
|
15
|
+
previously timed out when the isolated worker could not reconcile the removed
|
|
16
|
+
direct signature with the newer `workflowScript`-only contract.
|
|
17
|
+
|
|
18
|
+
## 0.5.0 - 2026-08-08
|
|
19
|
+
|
|
20
|
+
- Pin the development compatibility target to `pi-subagents 0.42.1` and add
|
|
21
|
+
coexistence assertions for its package identity and tool ownership.
|
|
22
|
+
- Add `npm run gate:pi-subagents`, an opt-in real-Pi compatibility gate. It
|
|
23
|
+
uses an isolated agent directory and reports `SKIP` unless a dedicated gate
|
|
24
|
+
model and credentials are supplied. The gate now waits for returned child
|
|
25
|
+
results and uses configurable provider-latency timeouts.
|
|
26
|
+
- Add the opt-in `subagents.externalWorkerIsolation: "enforce"` bootstrap for
|
|
27
|
+
external `pi-subagents` workers. It uses a tarball-shipped launcher and
|
|
28
|
+
fails closed if its outer Sandbox Runtime broker cannot start, protects
|
|
29
|
+
first-time security configuration creation, and cleans up only unchanged
|
|
30
|
+
temporary bind placeholders.
|
|
31
|
+
|
|
3
32
|
## 0.4.3 - 2026-08-06
|
|
4
33
|
|
|
5
34
|
- Move the `pi-auto-review` dependency to `0.3.3`, which allows multi-segment
|
package/README.md
CHANGED
|
@@ -59,7 +59,8 @@ new installs.
|
|
|
59
59
|
```json
|
|
60
60
|
{
|
|
61
61
|
"subagents": {
|
|
62
|
-
"provider": "builtin"
|
|
62
|
+
"provider": "builtin",
|
|
63
|
+
"externalWorkerIsolation": "off"
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
```
|
|
@@ -70,12 +71,59 @@ Supported modes:
|
|
|
70
71
|
each complete worker process tree.
|
|
71
72
|
- `pi-subagents`: let the external extension own orchestration. Main-agent and
|
|
72
73
|
inherited Bash execution remains sandboxed, but the external worker process
|
|
73
|
-
itself is not wrapped
|
|
74
|
+
itself is not wrapped while `externalWorkerIsolation` is `off`.
|
|
74
75
|
- `off`: protect Bash only.
|
|
75
76
|
|
|
77
|
+
### `pi-subagents` 0.44.0 capability boundary
|
|
78
|
+
|
|
79
|
+
`pi-sandbox` verifies the following combination in its test suite and release
|
|
80
|
+
gate:
|
|
81
|
+
|
|
82
|
+
| Capability | `builtin` | `pi-subagents` 0.44.0 |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| Outer worker sandbox | Yes | Opt-in (`externalWorkerIsolation: "enforce"`) |
|
|
85
|
+
| Bash sandbox | Yes | Yes |
|
|
86
|
+
| Persistent follow-up | Yes | Yes |
|
|
87
|
+
| `workflowScript`, missions, schedules | No | Yes |
|
|
88
|
+
| permission-system parent forwarding | Not required | Verified by compatibility gate |
|
|
89
|
+
|
|
90
|
+
For the external provider, `off` leaves the worker process outside the outer
|
|
91
|
+
Sandbox Runtime boundary. Do not treat inherited Bash sandboxing as complete
|
|
92
|
+
worker isolation unless the trusted global configuration explicitly enables
|
|
93
|
+
`enforce`.
|
|
94
|
+
|
|
95
|
+
Note on the external provider's public execution surface: pi-subagents 0.43.0
|
|
96
|
+
removed top-level `agent`/`task` direct execution. Every public `subagent`
|
|
97
|
+
launch — including a minimal single child — is expressed as a `workflowScript`
|
|
98
|
+
such as `return runs.run('main', { agent, task })`. The compatibility gate's
|
|
99
|
+
"direct single child" probe uses that single-run form.
|
|
100
|
+
|
|
76
101
|
The configuration parser rejects malformed JSON, unknown fields, and unknown
|
|
77
102
|
providers instead of silently weakening isolation.
|
|
78
103
|
|
|
104
|
+
To opt in to outer worker isolation for the external provider, use:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"subagents": {
|
|
109
|
+
"provider": "pi-subagents",
|
|
110
|
+
"externalWorkerIsolation": "enforce"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`enforce` installs a session-scoped `PI_SUBAGENT_PI_BINARY` wrapper which
|
|
116
|
+
starts the real Pi worker under a dedicated Sandbox Runtime broker. Bootstrap
|
|
117
|
+
failure is terminal for that worker; it never falls back to host execution.
|
|
118
|
+
The wrapper is preserved for nested child launches. This is opt-in while the
|
|
119
|
+
permission-forwarding supervisor and managed-worktree gate continue to mature.
|
|
120
|
+
|
|
121
|
+
Managed worktrees receive their own writable checkout plus the smallest known
|
|
122
|
+
Git metadata paths as read-only access so `git status` works. The main
|
|
123
|
+
repository `.git` is never made writable by this policy; Git mutation from an
|
|
124
|
+
outer-sandboxed external worker is therefore intentionally unsupported for
|
|
125
|
+
now.
|
|
126
|
+
|
|
79
127
|
## Optional host IPC fallback
|
|
80
128
|
|
|
81
129
|
Some host services expose only a Unix socket that the OS sandbox cannot use.
|
|
@@ -157,8 +205,21 @@ From the monorepo root:
|
|
|
157
205
|
npm install
|
|
158
206
|
npm run check
|
|
159
207
|
npm test
|
|
208
|
+
npm run gate:pi-subagents
|
|
160
209
|
```
|
|
161
210
|
|
|
211
|
+
`gate:pi-subagents` is a release/manual integration check, not a default CI
|
|
212
|
+
test. Set `PI_SUBAGENTS_GATE_MODEL` to a configured test model and provide its
|
|
213
|
+
normal Pi credential environment. The command creates a dedicated temporary
|
|
214
|
+
agent directory, never reads or updates your production Pi config, and prints
|
|
215
|
+
`SKIP` when those prerequisites are absent.
|
|
216
|
+
|
|
217
|
+
It allows 10 minutes for the direct-child phase and 15 minutes for the
|
|
218
|
+
multi-stage workflow, and exits only after the parent reports returned child
|
|
219
|
+
results. Override those bounds with positive millisecond values in
|
|
220
|
+
`PI_SUBAGENTS_GATE_DIRECT_TIMEOUT_MS` and
|
|
221
|
+
`PI_SUBAGENTS_GATE_WORKFLOW_TIMEOUT_MS` when needed for a dedicated provider.
|
|
222
|
+
|
|
162
223
|
The test suite covers real Linux Sandbox Runtime enforcement when its native
|
|
163
224
|
dependencies are installed, plus deterministic broker, network approval,
|
|
164
225
|
process cleanup, concurrency, and cross-platform orchestration tests.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erichll/pi-sandbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@anthropic-ai/sandbox-runtime": "0.0.67",
|
|
54
|
-
"@erichll/pi-auto-review": "0.3.
|
|
54
|
+
"@erichll/pi-auto-review": "0.3.4"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@earendil-works/pi-coding-agent": ">=0.82.1",
|
|
@@ -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.
|
|
63
|
+
"pi-subagents": "0.44.0",
|
|
64
64
|
"typescript": "^6.0.3"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/config.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type HostIPCConfig = {
|
|
|
22
22
|
export type PiSandboxConfig = {
|
|
23
23
|
subagents: {
|
|
24
24
|
provider: SubagentProvider;
|
|
25
|
+
externalWorkerIsolation: "off" | "enforce";
|
|
25
26
|
};
|
|
26
27
|
filesystem: {
|
|
27
28
|
additionalAllowRead: readonly string[];
|
|
@@ -39,6 +40,7 @@ export const DEFAULT_PI_SANDBOX_CONFIG: Readonly<PiSandboxConfig> = Object.freez
|
|
|
39
40
|
{
|
|
40
41
|
subagents: Object.freeze({
|
|
41
42
|
provider: "builtin",
|
|
43
|
+
externalWorkerIsolation: "off",
|
|
42
44
|
}),
|
|
43
45
|
filesystem: Object.freeze({
|
|
44
46
|
additionalAllowRead: Object.freeze([]),
|
|
@@ -96,7 +98,11 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
|
|
|
96
98
|
);
|
|
97
99
|
}
|
|
98
100
|
const subagents = value.subagents ?? {};
|
|
99
|
-
rejectUnknownKeys(
|
|
101
|
+
rejectUnknownKeys(
|
|
102
|
+
subagents,
|
|
103
|
+
["provider", "externalWorkerIsolation"],
|
|
104
|
+
"subagents",
|
|
105
|
+
);
|
|
100
106
|
|
|
101
107
|
const provider =
|
|
102
108
|
subagents.provider ?? DEFAULT_PI_SANDBOX_CONFIG.subagents.provider;
|
|
@@ -108,6 +114,17 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
|
|
|
108
114
|
`invalid pi-sandbox configuration: subagents.provider must be one of ${SUBAGENT_PROVIDERS.join(", ")}`,
|
|
109
115
|
);
|
|
110
116
|
}
|
|
117
|
+
const externalWorkerIsolation =
|
|
118
|
+
subagents.externalWorkerIsolation ??
|
|
119
|
+
DEFAULT_PI_SANDBOX_CONFIG.subagents.externalWorkerIsolation;
|
|
120
|
+
if (
|
|
121
|
+
externalWorkerIsolation !== "off" &&
|
|
122
|
+
externalWorkerIsolation !== "enforce"
|
|
123
|
+
) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"invalid pi-sandbox configuration: subagents.externalWorkerIsolation must be off or enforce",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
111
128
|
|
|
112
129
|
if (value.filesystem !== undefined && !isRecord(value.filesystem)) {
|
|
113
130
|
throw new Error(
|
|
@@ -182,6 +199,7 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
|
|
|
182
199
|
return {
|
|
183
200
|
subagents: {
|
|
184
201
|
provider: provider as SubagentProvider,
|
|
202
|
+
externalWorkerIsolation,
|
|
185
203
|
},
|
|
186
204
|
filesystem: {
|
|
187
205
|
additionalAllowRead: [...new Set(additionalAllowRead)],
|
|
@@ -198,7 +216,11 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
|
|
|
198
216
|
|
|
199
217
|
function defaultPiSandboxConfig(): PiSandboxConfig {
|
|
200
218
|
return {
|
|
201
|
-
subagents: {
|
|
219
|
+
subagents: {
|
|
220
|
+
provider: DEFAULT_PI_SANDBOX_CONFIG.subagents.provider,
|
|
221
|
+
externalWorkerIsolation:
|
|
222
|
+
DEFAULT_PI_SANDBOX_CONFIG.subagents.externalWorkerIsolation,
|
|
223
|
+
},
|
|
202
224
|
filesystem: {
|
|
203
225
|
additionalAllowRead: [
|
|
204
226
|
...DEFAULT_PI_SANDBOX_CONFIG.filesystem.additionalAllowRead,
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer, type Server } from "node:net";
|
|
3
|
+
import { chmod, mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import type { NetworkEndpoint, TrapApprovalContext } from "./approval.ts";
|
|
7
|
+
import { approveDomainEndpoint } from "./approval.ts";
|
|
8
|
+
|
|
9
|
+
const MAX_FRAME_BYTES = 16 * 1024;
|
|
10
|
+
const PROTOCOL_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
type Request = {
|
|
13
|
+
version?: unknown;
|
|
14
|
+
capability?: unknown;
|
|
15
|
+
id?: unknown;
|
|
16
|
+
type?: unknown;
|
|
17
|
+
workerId?: unknown;
|
|
18
|
+
cwd?: unknown;
|
|
19
|
+
hostname?: unknown;
|
|
20
|
+
port?: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ExternalWorkerSupervisor = {
|
|
24
|
+
socketPath: string;
|
|
25
|
+
capability: string;
|
|
26
|
+
workers(): ReadonlyArray<{
|
|
27
|
+
id: string;
|
|
28
|
+
cwd: string;
|
|
29
|
+
state: "active" | "exited";
|
|
30
|
+
startedAt: number;
|
|
31
|
+
requests: number;
|
|
32
|
+
}>;
|
|
33
|
+
close(): Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function createExternalWorkerSupervisor(
|
|
37
|
+
approvalContext: (worker: { id: string; cwd: string }) => TrapApprovalContext,
|
|
38
|
+
): Promise<ExternalWorkerSupervisor> {
|
|
39
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-sandbox-external-"));
|
|
40
|
+
await chmod(directory, 0o700);
|
|
41
|
+
const socketPath = join(directory, "supervisor.sock");
|
|
42
|
+
const capability = randomBytes(32).toString("hex");
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
const workers = new Map<string, {
|
|
45
|
+
cwd: string;
|
|
46
|
+
state: "active" | "exited";
|
|
47
|
+
startedAt: number;
|
|
48
|
+
requests: number;
|
|
49
|
+
}>();
|
|
50
|
+
const server: Server = createServer((socket) => {
|
|
51
|
+
let buffer = "";
|
|
52
|
+
socket.setEncoding("utf8");
|
|
53
|
+
socket.on("data", (chunk: string) => {
|
|
54
|
+
buffer += chunk;
|
|
55
|
+
if (Buffer.byteLength(buffer, "utf8") > MAX_FRAME_BYTES) {
|
|
56
|
+
socket.end('{"action":"deny"}\n');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let newline = buffer.indexOf("\n");
|
|
60
|
+
while (newline >= 0) {
|
|
61
|
+
const line = buffer.slice(0, newline);
|
|
62
|
+
buffer = buffer.slice(newline + 1);
|
|
63
|
+
void respond(line);
|
|
64
|
+
newline = buffer.indexOf("\n");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
const respond = async (line: string): Promise<void> => {
|
|
68
|
+
let request: Request | undefined;
|
|
69
|
+
try { request = JSON.parse(line) as Request; } catch { /* denied below */ }
|
|
70
|
+
if (!request) {
|
|
71
|
+
socket.write('{"action":"deny"}\n');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const baseValid =
|
|
75
|
+
request.version === PROTOCOL_VERSION &&
|
|
76
|
+
request.capability === capability &&
|
|
77
|
+
typeof request.id === "string" && request.id.length > 0 && request.id.length <= 128 &&
|
|
78
|
+
!seen.has(request.id);
|
|
79
|
+
if (!baseValid) {
|
|
80
|
+
socket.write('{"action":"deny"}\n');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const registered =
|
|
84
|
+
request.type === "register" &&
|
|
85
|
+
typeof request.workerId === "string" && request.workerId.length > 0 && request.workerId.length <= 128 &&
|
|
86
|
+
typeof request.cwd === "string" && request.cwd.startsWith("/") && request.cwd.length <= 4_096 &&
|
|
87
|
+
!workers.has(request.workerId);
|
|
88
|
+
if (registered) {
|
|
89
|
+
seen.add(request.id as string);
|
|
90
|
+
workers.set(request.workerId as string, {
|
|
91
|
+
cwd: request.cwd as string,
|
|
92
|
+
state: "active",
|
|
93
|
+
startedAt: Date.now(),
|
|
94
|
+
requests: 0,
|
|
95
|
+
});
|
|
96
|
+
socket.write(`${JSON.stringify({ id: request.id, action: "allow" })}\n`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const knownWorker =
|
|
100
|
+
typeof request.workerId === "string" && typeof request.cwd === "string"
|
|
101
|
+
? workers.get(request.workerId)
|
|
102
|
+
: undefined;
|
|
103
|
+
const unregister = request.type === "unregister" &&
|
|
104
|
+
knownWorker?.state === "active" && knownWorker.cwd === request.cwd;
|
|
105
|
+
if (unregister) {
|
|
106
|
+
seen.add(request.id as string);
|
|
107
|
+
knownWorker.state = "exited";
|
|
108
|
+
socket.write(`${JSON.stringify({ id: request.id, action: "allow" })}\n`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const workerCwd =
|
|
112
|
+
knownWorker?.state === "active" ? knownWorker.cwd : undefined;
|
|
113
|
+
const valid = request.type === "network" &&
|
|
114
|
+
workerCwd === request.cwd &&
|
|
115
|
+
typeof request.hostname === "string" && request.hostname.length <= 253 &&
|
|
116
|
+
Number.isInteger(request.port) && Number(request.port) > 0 && Number(request.port) <= 65535 &&
|
|
117
|
+
Boolean(workerCwd);
|
|
118
|
+
if (!valid) {
|
|
119
|
+
socket.write('{"action":"deny"}\n');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const approvedRequest = request as Required<Request>;
|
|
123
|
+
seen.add(approvedRequest.id as string);
|
|
124
|
+
knownWorker!.requests++;
|
|
125
|
+
let action: "allow" | "deny" = "deny";
|
|
126
|
+
try {
|
|
127
|
+
const endpoint: NetworkEndpoint = {
|
|
128
|
+
hostname: approvedRequest.hostname as string,
|
|
129
|
+
port: approvedRequest.port as number,
|
|
130
|
+
protocol: "tcp",
|
|
131
|
+
};
|
|
132
|
+
action = (await approveDomainEndpoint(endpoint, approvalContext({
|
|
133
|
+
id: approvedRequest.workerId as string,
|
|
134
|
+
cwd: workerCwd as string,
|
|
135
|
+
}))).action;
|
|
136
|
+
} catch {
|
|
137
|
+
action = "deny";
|
|
138
|
+
}
|
|
139
|
+
socket.write(`${JSON.stringify({ id: approvedRequest.id, action })}\n`);
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
await new Promise<void>((resolve, reject) => {
|
|
143
|
+
server.once("error", reject);
|
|
144
|
+
server.listen(socketPath, () => resolve());
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
socketPath,
|
|
148
|
+
capability,
|
|
149
|
+
workers() {
|
|
150
|
+
return [...workers.entries()].map(([id, worker]) => ({ id, ...worker }));
|
|
151
|
+
},
|
|
152
|
+
async close() {
|
|
153
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
154
|
+
await rm(directory, { recursive: true, force: true });
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const EXTERNAL_SUPERVISOR_PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Public PI_SUBAGENT_PI_BINARY wrapper. It deliberately has no host fallback:
|
|
3
|
+
// failure to initialize the broker prevents the worker from starting.
|
|
4
|
+
import { fork } from "node:child_process";
|
|
5
|
+
import { createConnection } from "node:net";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir, tmpdir } from "node:os";
|
|
9
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const realPi = process.env.PI_SANDBOX_EXTERNAL_REAL_PI_BINARY;
|
|
13
|
+
const workerId = randomUUID();
|
|
14
|
+
|
|
15
|
+
function worktreeGitReadPaths(cwd) {
|
|
16
|
+
const dotGit = join(cwd, ".git");
|
|
17
|
+
try {
|
|
18
|
+
const pointer = readFileSync(dotGit, "utf8").match(/^gitdir:\s*(.+)\s*$/m)?.[1];
|
|
19
|
+
if (!pointer) return [];
|
|
20
|
+
const gitDir = resolve(cwd, pointer);
|
|
21
|
+
const paths = [gitDir];
|
|
22
|
+
try {
|
|
23
|
+
const common = readFileSync(join(gitDir, "commondir"), "utf8").trim();
|
|
24
|
+
if (common) paths.push(resolve(gitDir, common));
|
|
25
|
+
} catch { /* a direct worktree gitdir has no commondir */ }
|
|
26
|
+
return paths;
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function supervisorRequest(payload) {
|
|
33
|
+
const socketPath = process.env.PI_SANDBOX_EXTERNAL_SUPERVISOR_SOCKET;
|
|
34
|
+
const capability = process.env.PI_SANDBOX_EXTERNAL_SUPERVISOR_CAPABILITY;
|
|
35
|
+
if (!socketPath || !capability) return Promise.resolve("deny");
|
|
36
|
+
return new Promise((resolveAction) => {
|
|
37
|
+
const socket = createConnection(socketPath);
|
|
38
|
+
let buffer = "";
|
|
39
|
+
const finish = (action) => { socket.destroy(); resolveAction(action === "allow" ? "allow" : "deny"); };
|
|
40
|
+
socket.setEncoding("utf8");
|
|
41
|
+
socket.setTimeout(30_000, () => finish("deny"));
|
|
42
|
+
socket.once("error", () => finish("deny"));
|
|
43
|
+
socket.on("data", (chunk) => {
|
|
44
|
+
buffer += chunk;
|
|
45
|
+
const newline = buffer.indexOf("\n");
|
|
46
|
+
if (newline < 0 || Buffer.byteLength(buffer, "utf8") > 16 * 1024) return;
|
|
47
|
+
try { finish(JSON.parse(buffer.slice(0, newline)).action); } catch { finish("deny"); }
|
|
48
|
+
});
|
|
49
|
+
socket.once("connect", () => socket.write(`${JSON.stringify({
|
|
50
|
+
version: 1, capability, id: randomUUID(), ...payload,
|
|
51
|
+
})}\n`));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function registerWorker() {
|
|
55
|
+
return supervisorRequest({ type: "register", workerId, cwd: process.cwd() });
|
|
56
|
+
}
|
|
57
|
+
function unregisterWorker() {
|
|
58
|
+
return supervisorRequest({ type: "unregister", workerId, cwd: process.cwd() });
|
|
59
|
+
}
|
|
60
|
+
function askSupervisor(hostname, port) {
|
|
61
|
+
return supervisorRequest({ type: "network", workerId, cwd: process.cwd(), hostname, port });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createMandatoryDenyPlaceholders(cwd, agentDir) {
|
|
65
|
+
// Sandbox Runtime protects these names unconditionally. Under an outer
|
|
66
|
+
// bubblewrap it cannot create a missing bind target after the root becomes
|
|
67
|
+
// read-only, so create only absent regular-file placeholders before broker
|
|
68
|
+
// bootstrap and remove exactly those placeholders at wrapper exit.
|
|
69
|
+
const created = [];
|
|
70
|
+
const remember = (path, kind, content) => {
|
|
71
|
+
const stat = lstatSync(path);
|
|
72
|
+
created.push({ path, kind, content, dev: stat.dev, ino: stat.ino });
|
|
73
|
+
};
|
|
74
|
+
for (const name of [
|
|
75
|
+
".gitconfig", ".gitmodules", ".bashrc", ".bash_profile", ".zshrc",
|
|
76
|
+
".zprofile", ".profile", ".ripgreprc", ".mcp.json",
|
|
77
|
+
]) {
|
|
78
|
+
const path = join(cwd, name);
|
|
79
|
+
if (existsSync(path)) continue;
|
|
80
|
+
try {
|
|
81
|
+
writeFileSync(path, "", { flag: "wx", mode: 0o600 });
|
|
82
|
+
remember(path, "file", "");
|
|
83
|
+
} catch {
|
|
84
|
+
// Sandbox bootstrap will fail closed if a mandatory target is unusable.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const directory of [".vscode", ".idea", ".claude/commands", ".claude/agents"]) {
|
|
88
|
+
let path = cwd;
|
|
89
|
+
for (const part of directory.split("/")) {
|
|
90
|
+
path = join(path, part);
|
|
91
|
+
if (existsSync(path)) continue;
|
|
92
|
+
try {
|
|
93
|
+
mkdirSync(path, { mode: 0o700 });
|
|
94
|
+
remember(path, "directory");
|
|
95
|
+
} catch {
|
|
96
|
+
// Sandbox bootstrap will fail closed if a mandatory target is unusable.
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Keep the project-state directory available so denyWrite can bind missing
|
|
102
|
+
// security files at their exact paths instead of masking the first missing
|
|
103
|
+
// `.pi` component and preventing ordinary project-state creation.
|
|
104
|
+
const projectState = join(cwd, ".pi");
|
|
105
|
+
if (!existsSync(projectState)) {
|
|
106
|
+
try {
|
|
107
|
+
mkdirSync(projectState, { mode: 0o700 });
|
|
108
|
+
remember(projectState, "directory");
|
|
109
|
+
} catch {
|
|
110
|
+
// Sandbox bootstrap will fail closed if the directory remains unusable.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// The agent directory sits below denyRead(home). Bubblewrap cannot create a
|
|
114
|
+
// missing nested bind target there after the home mask becomes read-only, so
|
|
115
|
+
// seed valid empty JSON files and remove only unchanged placeholders later.
|
|
116
|
+
for (const name of ["settings.json", "permissions.json", "sandbox.json"]) {
|
|
117
|
+
const path = join(agentDir, name);
|
|
118
|
+
if (existsSync(path)) continue;
|
|
119
|
+
try {
|
|
120
|
+
writeFileSync(path, "{}\n", { flag: "wx", mode: 0o600 });
|
|
121
|
+
remember(path, "file", "{}\n");
|
|
122
|
+
} catch {
|
|
123
|
+
// Sandbox bootstrap will fail closed if a protected target is unusable.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return () => {
|
|
127
|
+
for (const item of created.reverse()) {
|
|
128
|
+
try {
|
|
129
|
+
const current = lstatSync(item.path);
|
|
130
|
+
if (current.dev !== item.dev || current.ino !== item.ino) continue;
|
|
131
|
+
if (item.kind === "directory") rmdirSync(item.path);
|
|
132
|
+
else if (readFileSync(item.path, "utf8") === item.content) unlinkSync(item.path);
|
|
133
|
+
} catch { /* remove only unchanged empty placeholders */ }
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (!realPi) {
|
|
138
|
+
process.stderr.write("pi-sandbox: external worker wrapper has no verified Pi binary\n");
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
} else {
|
|
141
|
+
const cwd = process.cwd();
|
|
142
|
+
const workerTempDir = mkdtempSync(join(tmpdir(), "pi-sandbox-external-worker-"));
|
|
143
|
+
process.once("exit", () => {
|
|
144
|
+
try { rmSync(workerTempDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
|
|
145
|
+
});
|
|
146
|
+
const home = resolve(homedir());
|
|
147
|
+
const agentDir = resolve(process.env.PI_CODING_AGENT_DIR || join(home, ".pi", "agent"));
|
|
148
|
+
const removeMandatoryDenyPlaceholders = createMandatoryDenyPlaceholders(cwd, agentDir);
|
|
149
|
+
process.once("exit", removeMandatoryDenyPlaceholders);
|
|
150
|
+
const sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR;
|
|
151
|
+
const packageRoot = dirname(fileURLToPath(import.meta.url));
|
|
152
|
+
const nodeRoot = dirname(dirname(process.execPath));
|
|
153
|
+
const runtimeRoot = dirname(dirname(fileURLToPath(import.meta.resolve("@anthropic-ai/sandbox-runtime"))));
|
|
154
|
+
const extraRead = (process.env.PI_SANDBOX_EXTERNAL_ALLOW_READ || "")
|
|
155
|
+
.split(":")
|
|
156
|
+
.filter(Boolean);
|
|
157
|
+
const existingGitConfig = [
|
|
158
|
+
join(home, ".gitconfig"),
|
|
159
|
+
join(home, ".config", "git", "config"),
|
|
160
|
+
].filter(existsSync);
|
|
161
|
+
// On Linux Sandbox Runtime masks the first missing component of a deny path.
|
|
162
|
+
// Denying a not-yet-created `.pi/settings.json` would therefore mask `.pi`
|
|
163
|
+
// itself and prevent Pi from creating its ordinary project state directory.
|
|
164
|
+
// Existing security files remain outer-sandbox protected; creation attempts
|
|
165
|
+
// are still gated by permission-system and pi-auto-review.
|
|
166
|
+
const workspaceSecurityFiles = [
|
|
167
|
+
join(cwd, ".pi", "settings.json"),
|
|
168
|
+
join(cwd, ".pi", "sandbox.json"),
|
|
169
|
+
join(cwd, ".pi", "pi-auto-review.json"),
|
|
170
|
+
];
|
|
171
|
+
const agentSecurityPaths = [
|
|
172
|
+
join(agentDir, "settings.json"),
|
|
173
|
+
join(agentDir, "permissions.json"),
|
|
174
|
+
join(agentDir, "sandbox.json"),
|
|
175
|
+
join(agentDir, "extensions"),
|
|
176
|
+
];
|
|
177
|
+
const runtimeConfig = {
|
|
178
|
+
filesystem: {
|
|
179
|
+
denyRead: home === parse(home).root ? [] : [home],
|
|
180
|
+
allowRead: [cwd, nodeRoot, runtimeRoot, ...existingGitConfig, "/dev/null", ...worktreeGitReadPaths(cwd), ...extraRead],
|
|
181
|
+
// Child sessions and permission-forwarding request/response files live
|
|
182
|
+
// here. Keep this narrower than the agent directory, whose security
|
|
183
|
+
// settings and extensions remain read-only.
|
|
184
|
+
allowWrite: [
|
|
185
|
+
cwd,
|
|
186
|
+
"/dev/null",
|
|
187
|
+
workerTempDir,
|
|
188
|
+
...(sessionDir?.startsWith("/") ? [sessionDir] : []),
|
|
189
|
+
// Pi creates directory-style settings locks and provider model caches
|
|
190
|
+
// beside its global settings. Security-sensitive files and extensions
|
|
191
|
+
// below remain explicit denyWrite entries.
|
|
192
|
+
agentDir,
|
|
193
|
+
],
|
|
194
|
+
denyWrite: [
|
|
195
|
+
...workspaceSecurityFiles,
|
|
196
|
+
...agentSecurityPaths,
|
|
197
|
+
packageRoot,
|
|
198
|
+
dirname(process.execPath),
|
|
199
|
+
],
|
|
200
|
+
allowGitConfig: true,
|
|
201
|
+
},
|
|
202
|
+
network: {
|
|
203
|
+
allowedDomains: [], deniedDomains: [], allowLocalBinding: false,
|
|
204
|
+
allowAllUnixSockets: false, allowUnixSockets: [],
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const registration = await registerWorker();
|
|
208
|
+
if (registration !== "allow") {
|
|
209
|
+
process.stderr.write("pi-sandbox: external worker registration was denied\n");
|
|
210
|
+
process.exitCode = 1;
|
|
211
|
+
process.exit();
|
|
212
|
+
}
|
|
213
|
+
const broker = fork(new URL("./srt-broker.mjs", import.meta.url), [], {
|
|
214
|
+
cwd,
|
|
215
|
+
detached: true,
|
|
216
|
+
env: {
|
|
217
|
+
...process.env,
|
|
218
|
+
PI_SANDBOX_EXTERNAL_WORKER_TMPDIR: workerTempDir,
|
|
219
|
+
CLAUDE_CODE_TMPDIR: workerTempDir,
|
|
220
|
+
TMPDIR: workerTempDir,
|
|
221
|
+
TMP: workerTempDir,
|
|
222
|
+
TEMP: workerTempDir,
|
|
223
|
+
},
|
|
224
|
+
stdio: ["pipe", "pipe", "pipe", "ipc"],
|
|
225
|
+
});
|
|
226
|
+
broker.stdout.pipe(process.stdout);
|
|
227
|
+
broker.stderr.pipe(process.stderr);
|
|
228
|
+
process.stdin.pipe(broker.stdin);
|
|
229
|
+
broker.on("message", (message) => {
|
|
230
|
+
if (!message || typeof message !== "object" || message.type !== "network-request") return;
|
|
231
|
+
void askSupervisor(message.hostname, message.port).then((action) => {
|
|
232
|
+
if (broker.connected) broker.send({ type: "network-response", id: message.id, action });
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
let prefix = [];
|
|
236
|
+
try { prefix = JSON.parse(process.env.PI_SANDBOX_EXTERNAL_REAL_PI_PREFIX || "[]"); } catch { prefix = []; }
|
|
237
|
+
if (!Array.isArray(prefix) || prefix.some((item) => typeof item !== "string")) {
|
|
238
|
+
process.stderr.write("pi-sandbox: external worker wrapper received invalid Pi prefix\n");
|
|
239
|
+
broker.kill("SIGKILL");
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
} else {
|
|
242
|
+
broker.send({ type: "init", invocation: [realPi, ...prefix, ...process.argv.slice(2)], runtimeConfig });
|
|
243
|
+
}
|
|
244
|
+
const stop = (signal) => {
|
|
245
|
+
if (broker.pid) {
|
|
246
|
+
try { process.kill(-broker.pid, signal); } catch { broker.kill(signal); }
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
process.on("SIGINT", () => stop("SIGINT"));
|
|
250
|
+
process.on("SIGTERM", () => stop("SIGTERM"));
|
|
251
|
+
broker.once("exit", async (code, signal) => {
|
|
252
|
+
await unregisterWorker();
|
|
253
|
+
process.exitCode = code ?? (signal ? 1 : 1);
|
|
254
|
+
});
|
|
255
|
+
broker.once("error", (error) => {
|
|
256
|
+
process.stderr.write(`pi-sandbox: external worker broker failed: ${error.message}\n`);
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
});
|
|
259
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ExtensionAPI,
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import {
|
|
7
8
|
createBashToolDefinition,
|
|
8
9
|
createLocalBashOperations,
|
|
@@ -31,6 +32,10 @@ import type {
|
|
|
31
32
|
ProcessBackedSubagentSession,
|
|
32
33
|
} from "./subagent.ts";
|
|
33
34
|
import { Type } from "typebox";
|
|
35
|
+
import {
|
|
36
|
+
createExternalWorkerSupervisor,
|
|
37
|
+
type ExternalWorkerSupervisor,
|
|
38
|
+
} from "./external-supervisor.ts";
|
|
34
39
|
|
|
35
40
|
const EXTENSION_NAME = "pi-sandbox";
|
|
36
41
|
const registrations = new WeakMap<ExtensionAPI, Promise<void>>();
|
|
@@ -84,6 +89,50 @@ function externalSubagentDiagnostic(pi: ExtensionAPI): {
|
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
const EXTERNAL_WORKER_LAUNCHER = fileURLToPath(
|
|
93
|
+
new URL("./external-worker-launcher.mjs", import.meta.url),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
type ExternalWorkerIsolation = {
|
|
97
|
+
restore(): void;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function enableExternalWorkerIsolation(
|
|
101
|
+
supervisor: ExternalWorkerSupervisor,
|
|
102
|
+
): ExternalWorkerIsolation {
|
|
103
|
+
if (process.platform !== "linux" && process.platform !== "darwin") {
|
|
104
|
+
throw new Error(`external worker isolation is unavailable on ${process.platform}`);
|
|
105
|
+
}
|
|
106
|
+
if (process.env.PI_SUBAGENT_PI_BINARY) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
"external worker isolation refuses to replace an existing PI_SUBAGENT_PI_BINARY wrapper",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const entry = process.argv[1];
|
|
112
|
+
if (!entry) {
|
|
113
|
+
throw new Error("external worker isolation cannot determine the real Pi CLI entrypoint");
|
|
114
|
+
}
|
|
115
|
+
const injected = {
|
|
116
|
+
PI_SUBAGENT_PI_BINARY: EXTERNAL_WORKER_LAUNCHER,
|
|
117
|
+
PI_SANDBOX_EXTERNAL_REAL_PI_BINARY: process.execPath,
|
|
118
|
+
PI_SANDBOX_EXTERNAL_REAL_PI_PREFIX: JSON.stringify([entry]),
|
|
119
|
+
PI_SANDBOX_EXTERNAL_ALLOW_READ: [
|
|
120
|
+
process.cwd(),
|
|
121
|
+
process.env.PI_CODING_AGENT_DIR,
|
|
122
|
+
].filter((value): value is string => Boolean(value)).join(":"),
|
|
123
|
+
PI_SANDBOX_EXTERNAL_SUPERVISOR_SOCKET: supervisor.socketPath,
|
|
124
|
+
PI_SANDBOX_EXTERNAL_SUPERVISOR_CAPABILITY: supervisor.capability,
|
|
125
|
+
};
|
|
126
|
+
Object.assign(process.env, injected);
|
|
127
|
+
return {
|
|
128
|
+
restore() {
|
|
129
|
+
for (const [key, value] of Object.entries(injected)) {
|
|
130
|
+
if (process.env[key] === value) delete process.env[key];
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
87
136
|
function humanApproval(ctx: ExtensionContext): HumanApproval {
|
|
88
137
|
return async (request, reason, signal) => {
|
|
89
138
|
if (!ctx.hasUI) return "deny";
|
|
@@ -204,6 +253,7 @@ async function performRegistration(
|
|
|
204
253
|
const config = loadPiSandboxConfig();
|
|
205
254
|
const subagentProvider =
|
|
206
255
|
options.subagentProvider ?? config.subagents.provider;
|
|
256
|
+
const externalWorkerIsolation = config.subagents.externalWorkerIsolation;
|
|
207
257
|
const additionalAllowRead = config.filesystem.additionalAllowRead;
|
|
208
258
|
const hostIPC = options.hostIPC ?? config.hostIPC;
|
|
209
259
|
const subagents =
|
|
@@ -216,6 +266,9 @@ async function performRegistration(
|
|
|
216
266
|
}))
|
|
217
267
|
: undefined;
|
|
218
268
|
const cwd = process.cwd();
|
|
269
|
+
let isolation: ExternalWorkerIsolation | undefined;
|
|
270
|
+
let supervisor: ExternalWorkerSupervisor | undefined;
|
|
271
|
+
let externalIsolationFailure: string | undefined;
|
|
219
272
|
const localBash = createBashToolDefinition(cwd);
|
|
220
273
|
|
|
221
274
|
pi.registerTool({
|
|
@@ -476,11 +529,65 @@ async function performRegistration(
|
|
|
476
529
|
currentTurn = event.turnIndex;
|
|
477
530
|
});
|
|
478
531
|
|
|
479
|
-
pi
|
|
532
|
+
// pi-subagents owns the public tool, so a failed wrapper bootstrap cannot
|
|
533
|
+
// be fixed by replacing that tool. Block at Pi's pre-execution boundary:
|
|
534
|
+
// enforce must never degrade into a host worker launch.
|
|
535
|
+
pi.on("tool_call", (event) => {
|
|
536
|
+
if (
|
|
537
|
+
event.toolName === "subagent" &&
|
|
538
|
+
subagentProvider === "pi-subagents" &&
|
|
539
|
+
externalWorkerIsolation === "enforce" &&
|
|
540
|
+
!isolation
|
|
541
|
+
) {
|
|
542
|
+
return {
|
|
543
|
+
block: true,
|
|
544
|
+
reason: `${EXTENSION_NAME}: external worker isolation is unavailable; refusing host worker launch${externalIsolationFailure ? `: ${externalIsolationFailure}` : ""}`,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
return undefined;
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
551
|
+
if (
|
|
552
|
+
subagentProvider === "pi-subagents" &&
|
|
553
|
+
externalWorkerIsolation === "enforce"
|
|
554
|
+
) {
|
|
555
|
+
try {
|
|
556
|
+
isolation?.restore();
|
|
557
|
+
await supervisor?.close();
|
|
558
|
+
supervisor = await createExternalWorkerSupervisor((worker) => ({
|
|
559
|
+
broker: getBoundaryBroker(),
|
|
560
|
+
command: "external pi-subagents worker network request",
|
|
561
|
+
cwd: worker.cwd,
|
|
562
|
+
sessionId: sessionId(ctx),
|
|
563
|
+
scopeKey: `${sessionId(ctx)}:turn:${currentTurn}`,
|
|
564
|
+
agentName: `pi-subagents-worker:${worker.id}`,
|
|
565
|
+
humanApproval: humanApproval(ctx),
|
|
566
|
+
}));
|
|
567
|
+
isolation = enableExternalWorkerIsolation(supervisor);
|
|
568
|
+
externalIsolationFailure = undefined;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
const message = `${EXTENSION_NAME}: external worker isolation unavailable; child launches will fail closed: ${error instanceof Error ? error.message : String(error)}`;
|
|
571
|
+
externalIsolationFailure = message;
|
|
572
|
+
isolation?.restore();
|
|
573
|
+
isolation = undefined;
|
|
574
|
+
await supervisor?.close();
|
|
575
|
+
supervisor = undefined;
|
|
576
|
+
console.error(message);
|
|
577
|
+
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
480
580
|
if (ctx.hasUI) {
|
|
481
581
|
if (subagentProvider === "pi-subagents") {
|
|
482
582
|
const diagnostic = externalSubagentDiagnostic(pi);
|
|
483
|
-
ctx.ui.notify(
|
|
583
|
+
ctx.ui.notify(
|
|
584
|
+
externalWorkerIsolation === "enforce" && isolation
|
|
585
|
+
? "pi-subagents orchestration active; external worker process trees require outer Sandbox Runtime isolation."
|
|
586
|
+
: diagnostic.message,
|
|
587
|
+
externalWorkerIsolation === "enforce" && isolation
|
|
588
|
+
? "info"
|
|
589
|
+
: diagnostic.level,
|
|
590
|
+
);
|
|
484
591
|
} else if (subagentProvider === "builtin") {
|
|
485
592
|
ctx.ui.notify(
|
|
486
593
|
"pi-sandbox subagent provider: builtin; worker process trees use the outer Sandbox Runtime sandbox",
|
|
@@ -513,6 +620,10 @@ async function performRegistration(
|
|
|
513
620
|
});
|
|
514
621
|
|
|
515
622
|
pi.on("session_shutdown", async () => {
|
|
623
|
+
isolation?.restore();
|
|
624
|
+
isolation = undefined;
|
|
625
|
+
await supervisor?.close();
|
|
626
|
+
supervisor = undefined;
|
|
516
627
|
await subagents?.shutdown();
|
|
517
628
|
});
|
|
518
629
|
}
|
package/src/srt-broker.mjs
CHANGED
|
@@ -88,9 +88,12 @@ async function main() {
|
|
|
88
88
|
undefined,
|
|
89
89
|
process.cwd(),
|
|
90
90
|
);
|
|
91
|
+
const workerTempDir = process.env.PI_SANDBOX_EXTERNAL_WORKER_TMPDIR;
|
|
91
92
|
target = spawn(wrapped.argv[0], wrapped.argv.slice(1), {
|
|
92
93
|
cwd: process.cwd(),
|
|
93
|
-
env:
|
|
94
|
+
env: workerTempDir
|
|
95
|
+
? { ...wrapped.env, TMPDIR: workerTempDir, TMP: workerTempDir, TEMP: workerTempDir }
|
|
96
|
+
: wrapped.env,
|
|
94
97
|
stdio: ["pipe", "pipe", "pipe"],
|
|
95
98
|
});
|
|
96
99
|
process.stdin.pipe(target.stdin);
|