@erichll/pi-sandbox 0.15.2 → 0.16.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.
@@ -1,173 +0,0 @@
1
- // Optional FleetView integration for pi-sandbox's isolated external workers.
2
- //
3
- // When the host is running the external `pi-subagents` provider with
4
- // `externalWorkerIsolation: "enforce"`, pi-sandbox supervises the external
5
- // worker process trees itself. This module mirrors those supervised workers
6
- // into pi-subagents' caller-owned external-run registry so they show up in the
7
- // FleetView (pi-subagents >= 0.50.0).
8
- //
9
- // This is a pure observability enhancement: it participates in NO sandbox
10
- // allow/deny decision, and nothing here can downgrade an isolated worker into
11
- // host execution. `pi-subagents/external-runs` is loaded via a DYNAMIC import
12
- // only (never a top-level static import), because pi-subagents is an OPTIONAL
13
- // peer dependency: builtin/off deployments may not have it installed.
14
- //
15
- // Deliberate scope of v1: records are "running" only. No completed/failed/
16
- // endedAt are fabricated because neither the launcher nor the supervisor
17
- // protocol currently reports process exit evidence to this extension (the
18
- // record would therefore be a lie). See docs/compat-notes.md seam 5.
19
-
20
- type ExternalRunState = "running";
21
-
22
- /** Structural view of the specific pi-subagents API we consume. */
23
- type ExternalRunsApi = {
24
- registerExternalRun(input: {
25
- id: string;
26
- sessionId: string;
27
- source: string;
28
- label: string;
29
- state: ExternalRunState;
30
- startedAt: number;
31
- preview?: string;
32
- }): unknown;
33
- unregisterExternalRun(sessionId: string, id: string): boolean;
34
- };
35
-
36
- export type ExternalRunsView = {
37
- /** Whether the pi-subagents runtime is reachable (dynamic import succeeded). */
38
- readonly enabled: boolean;
39
- /** Diagnostic string when the runtime is disabled. */
40
- readonly error?: string;
41
- /** Mirror a supervisor lifecycle.registered event into the registry. */
42
- registered(worker: { id: string; cwd: string; startedAt: number }): void;
43
- /** Mirror a supervisor lifecycle.unregistered event out of the registry. */
44
- unregistered(workerId: string): void;
45
- /** Unregister every run this view registered (session shutdown / replace). */
46
- close(): void;
47
- };
48
-
49
- const VIEW_LABEL = "isolated pi-subagents worker";
50
- const VIEW_SOURCE = "pi-sandbox";
51
-
52
- /**
53
- * Whether the FleetView integration should be enabled for a given provider /
54
- * isolation combination. It is enabled ONLY when the external pi-subagents
55
- * provider owns orchestration AND outer worker isolation is enforced -- the
56
- * sole case where pi-sandbox actually supervises external workers and can
57
- * truthfully vouch for a "running" record it owns.
58
- */
59
- export function externalRunsViewEnabledWhen(
60
- provider: string,
61
- externalWorkerIsolation: string,
62
- ): boolean {
63
- return provider === "pi-subagents" && externalWorkerIsolation === "enforce";
64
- }
65
-
66
- function ident(value: string): { ok: true; value: string } | { ok: false; error: string } {
67
- const trimmed = value.trim();
68
- if (!trimmed || trimmed !== value) {
69
- return { ok: false, error: "must be a non-empty trimmed string" };
70
- }
71
- return { ok: true, value: trimmed };
72
- }
73
-
74
- export function createExternalRunsView(
75
- sessionIdInput: string,
76
- load: () => Promise<unknown> = () => import("pi-subagents/external-runs"),
77
- ): Promise<ExternalRunsView> {
78
- return (async () => {
79
- const sessionId = ident(sessionIdInput);
80
- if (!sessionId.ok) {
81
- return disabledView(`invalid session id: ${sessionId.error}`);
82
- }
83
- let api: ExternalRunsApi | undefined;
84
- let loadError: string | undefined;
85
- try {
86
- const mod = (await load()) as Record<string, unknown>;
87
- const registerExternalRun = mod.registerExternalRun;
88
- const unregisterExternalRun = mod.unregisterExternalRun;
89
- if (typeof registerExternalRun !== "function" || typeof unregisterExternalRun !== "function") {
90
- throw new Error("pi-subagents/external-runs does not expose registerExternalRun/unregisterExternalRun");
91
- }
92
- api = { registerExternalRun, unregisterExternalRun } as unknown as ExternalRunsApi;
93
- } catch (error) {
94
- loadError = `FleetView external-run registration unavailable: ${error instanceof Error ? error.message : String(error)}`;
95
- // Never thrown: disable only the display path. Sandbox fail-closed and
96
- // network approval for the supervisor are untouched.
97
- return disabledView(loadError);
98
- }
99
-
100
- // (sessionId, workerId) pairs this extension registered, so we never
101
- // duplicate-register and never unregister records owned by other sources.
102
- const registered = new Map<string, number>();
103
-
104
- return {
105
- get enabled() {
106
- return Boolean(api);
107
- },
108
- error: loadError,
109
- registered(worker) {
110
- const fullId = ident(worker.id);
111
- const cwd = ident(worker.cwd);
112
- if (!api || !fullId.ok || !cwd.ok) return;
113
- if (registered.has(fullId.value)) return; // avoid duplicate registration
114
- try {
115
- api!.registerExternalRun({
116
- id: fullId.value,
117
- sessionId: sessionId.value,
118
- source: VIEW_SOURCE,
119
- label: VIEW_LABEL,
120
- state: "running",
121
- startedAt:
122
- Number.isSafeInteger(worker.startedAt) && worker.startedAt > 0
123
- ? worker.startedAt
124
- : Date.now(),
125
- preview: cwd.value,
126
- });
127
- registered.set(fullId.value, Date.now());
128
- } catch (error) {
129
- // Registry full / malformed record / unknown fields: log, do not emit.
130
- console.error(
131
- `pi-sandbox: could not register external run for worker '${fullId.value}': ${error instanceof Error ? error.message : String(error)}`,
132
- );
133
- }
134
- },
135
- unregistered(workerId) {
136
- const fullId = ident(workerId);
137
- if (!fullId.ok) return;
138
- if (!registered.has(fullId.value)) return; // we did not register it
139
- registered.delete(fullId.value);
140
- if (api) {
141
- try {
142
- api.unregisterExternalRun(sessionId.value, fullId.value);
143
- } catch {
144
- // Best-effort; nothing else to fall back to.
145
- }
146
- }
147
- },
148
- close() {
149
- for (const workerId of registered.keys()) {
150
- if (api) {
151
- try {
152
- api.unregisterExternalRun(sessionId.value, workerId);
153
- } catch {
154
- // best-effort
155
- }
156
- }
157
- }
158
- registered.clear();
159
- },
160
- };
161
- })();
162
- }
163
-
164
- function disabledView(error: string): ExternalRunsView {
165
- console.error(`pi-sandbox: ${error}`);
166
- return {
167
- enabled: false,
168
- error,
169
- registered() {},
170
- unregistered() {},
171
- close() {},
172
- };
173
- }
@@ -1,189 +0,0 @@
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
- import type { NetworkConfig } from "./config.ts";
9
-
10
- const MAX_FRAME_BYTES = 16 * 1024;
11
- const PROTOCOL_VERSION = 1;
12
-
13
- type Request = {
14
- version?: unknown;
15
- capability?: unknown;
16
- id?: unknown;
17
- type?: unknown;
18
- workerId?: unknown;
19
- cwd?: unknown;
20
- hostname?: unknown;
21
- port?: unknown;
22
- };
23
-
24
- export type ExternalWorkerSupervisorLifecycle = {
25
- /** Called immediately after a worker finishes the register protocol. */
26
- registered?(worker: { id: string; cwd: string; startedAt: number }): void;
27
- /** Called immediately after a worker completes the unregister protocol. */
28
- unregistered?(worker: { id: string; cwd: string }): void;
29
- };
30
-
31
- export type ExternalWorkerSupervisor = {
32
- socketPath: string;
33
- capability: string;
34
- workers(): ReadonlyArray<{
35
- id: string;
36
- cwd: string;
37
- state: "active" | "exited";
38
- startedAt: number;
39
- requests: number;
40
- }>;
41
- close(): Promise<void>;
42
- };
43
-
44
- export async function createExternalWorkerSupervisor(
45
- approvalContext: (worker: { id: string; cwd: string }) => TrapApprovalContext,
46
- lifecycle: ExternalWorkerSupervisorLifecycle = {},
47
- network: NetworkConfig = { allowedDomains: [], deniedDomains: [] },
48
- ): Promise<ExternalWorkerSupervisor> {
49
- const trustedNetwork = {
50
- allowedDomains: [...network.allowedDomains],
51
- deniedDomains: [...network.deniedDomains],
52
- };
53
- const directory = await mkdtemp(join(tmpdir(), "pi-sandbox-external-"));
54
- await chmod(directory, 0o700);
55
- const socketPath = join(directory, "supervisor.sock");
56
- const capability = randomBytes(32).toString("hex");
57
- const seen = new Set<string>();
58
- const workers = new Map<string, {
59
- cwd: string;
60
- state: "active" | "exited";
61
- startedAt: number;
62
- requests: number;
63
- }>();
64
- const server: Server = createServer((socket) => {
65
- let buffer = "";
66
- socket.setEncoding("utf8");
67
- socket.on("data", (chunk: string) => {
68
- buffer += chunk;
69
- if (Buffer.byteLength(buffer, "utf8") > MAX_FRAME_BYTES) {
70
- socket.end('{"action":"deny"}\n');
71
- return;
72
- }
73
- let newline = buffer.indexOf("\n");
74
- while (newline >= 0) {
75
- const line = buffer.slice(0, newline);
76
- buffer = buffer.slice(newline + 1);
77
- void respond(line);
78
- newline = buffer.indexOf("\n");
79
- }
80
- });
81
- const respond = async (line: string): Promise<void> => {
82
- let request: Request | undefined;
83
- try { request = JSON.parse(line) as Request; } catch { /* denied below */ }
84
- if (!request) {
85
- socket.write('{"action":"deny"}\n');
86
- return;
87
- }
88
- const baseValid =
89
- request.version === PROTOCOL_VERSION &&
90
- request.capability === capability &&
91
- typeof request.id === "string" && request.id.length > 0 && request.id.length <= 128 &&
92
- !seen.has(request.id);
93
- if (!baseValid) {
94
- socket.write('{"action":"deny"}\n');
95
- return;
96
- }
97
- const registered =
98
- request.type === "register" &&
99
- typeof request.workerId === "string" && request.workerId.length > 0 && request.workerId.length <= 128 &&
100
- typeof request.cwd === "string" && request.cwd.startsWith("/") && request.cwd.length <= 4_096 &&
101
- !workers.has(request.workerId);
102
- if (registered) {
103
- seen.add(request.id as string);
104
- workers.set(request.workerId as string, {
105
- cwd: request.cwd as string,
106
- state: "active",
107
- startedAt: Date.now(),
108
- requests: 0,
109
- });
110
- socket.write(`${JSON.stringify({
111
- id: request.id,
112
- action: "allow",
113
- network: {
114
- allowedDomains: [...trustedNetwork.allowedDomains],
115
- deniedDomains: [...trustedNetwork.deniedDomains],
116
- },
117
- })}\n`);
118
- lifecycle.registered?.({
119
- id: request.workerId as string,
120
- cwd: request.cwd as string,
121
- startedAt: workers.get(request.workerId as string)!.startedAt,
122
- });
123
- return;
124
- }
125
- const knownWorker =
126
- typeof request.workerId === "string" && typeof request.cwd === "string"
127
- ? workers.get(request.workerId)
128
- : undefined;
129
- const unregister = request.type === "unregister" &&
130
- knownWorker?.state === "active" && knownWorker.cwd === request.cwd;
131
- if (unregister) {
132
- seen.add(request.id as string);
133
- knownWorker.state = "exited";
134
- socket.write(`${JSON.stringify({ id: request.id, action: "allow" })}\n`);
135
- lifecycle.unregistered?.({
136
- id: request.workerId as string,
137
- cwd: request.cwd as string,
138
- });
139
- return;
140
- }
141
- const workerCwd =
142
- knownWorker?.state === "active" ? knownWorker.cwd : undefined;
143
- const valid = request.type === "network" &&
144
- workerCwd === request.cwd &&
145
- typeof request.hostname === "string" && request.hostname.length <= 253 &&
146
- Number.isInteger(request.port) && Number(request.port) > 0 && Number(request.port) <= 65535 &&
147
- Boolean(workerCwd);
148
- if (!valid) {
149
- socket.write('{"action":"deny"}\n');
150
- return;
151
- }
152
- const approvedRequest = request as Required<Request>;
153
- seen.add(approvedRequest.id as string);
154
- knownWorker!.requests++;
155
- let action: "allow" | "deny" = "deny";
156
- try {
157
- const endpoint: NetworkEndpoint = {
158
- hostname: approvedRequest.hostname as string,
159
- port: approvedRequest.port as number,
160
- protocol: "tcp",
161
- };
162
- action = (await approveDomainEndpoint(endpoint, approvalContext({
163
- id: approvedRequest.workerId as string,
164
- cwd: workerCwd as string,
165
- }))).action;
166
- } catch {
167
- action = "deny";
168
- }
169
- socket.write(`${JSON.stringify({ id: approvedRequest.id, action })}\n`);
170
- };
171
- });
172
- await new Promise<void>((resolve, reject) => {
173
- server.once("error", reject);
174
- server.listen(socketPath, () => resolve());
175
- });
176
- return {
177
- socketPath,
178
- capability,
179
- workers() {
180
- return [...workers.entries()].map(([id, worker]) => ({ id, ...worker }));
181
- },
182
- async close() {
183
- await new Promise<void>((resolve) => server.close(() => resolve()));
184
- await rm(directory, { recursive: true, force: true });
185
- },
186
- };
187
- }
188
-
189
- export const EXTERNAL_SUPERVISOR_PROTOCOL_VERSION = PROTOCOL_VERSION;
@@ -1,266 +0,0 @@
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
- import { decodeExternalNetworkPolicy } from "./external-network-policy.mjs";
12
-
13
- const realPi = process.env.PI_SANDBOX_EXTERNAL_REAL_PI_BINARY;
14
- const workerId = randomUUID();
15
-
16
- function worktreeGitReadPaths(cwd) {
17
- const dotGit = join(cwd, ".git");
18
- try {
19
- const pointer = readFileSync(dotGit, "utf8").match(/^gitdir:\s*(.+)\s*$/m)?.[1];
20
- if (!pointer) return [];
21
- const gitDir = resolve(cwd, pointer);
22
- const paths = [gitDir];
23
- try {
24
- const common = readFileSync(join(gitDir, "commondir"), "utf8").trim();
25
- if (common) paths.push(resolve(gitDir, common));
26
- } catch { /* a direct worktree gitdir has no commondir */ }
27
- return paths;
28
- } catch {
29
- return [];
30
- }
31
- }
32
-
33
- function supervisorRequest(payload) {
34
- const socketPath = process.env.PI_SANDBOX_EXTERNAL_SUPERVISOR_SOCKET;
35
- const capability = process.env.PI_SANDBOX_EXTERNAL_SUPERVISOR_CAPABILITY;
36
- if (!socketPath || !capability) return Promise.resolve({ action: "deny" });
37
- return new Promise((resolveResponse) => {
38
- const socket = createConnection(socketPath);
39
- let buffer = "";
40
- const finish = (response) => {
41
- socket.destroy();
42
- resolveResponse(response?.action === "allow" ? response : { action: "deny" });
43
- };
44
- socket.setEncoding("utf8");
45
- socket.setTimeout(30_000, () => finish({ action: "deny" }));
46
- socket.once("error", () => finish({ action: "deny" }));
47
- socket.on("data", (chunk) => {
48
- buffer += chunk;
49
- const newline = buffer.indexOf("\n");
50
- if (newline < 0 || Buffer.byteLength(buffer, "utf8") > 16 * 1024) return;
51
- try { finish(JSON.parse(buffer.slice(0, newline))); } catch { finish({ action: "deny" }); }
52
- });
53
- socket.once("connect", () => socket.write(`${JSON.stringify({
54
- version: 1, capability, id: randomUUID(), ...payload,
55
- })}\n`));
56
- });
57
- }
58
- function registerWorker() {
59
- return supervisorRequest({ type: "register", workerId, cwd: process.cwd() });
60
- }
61
- function unregisterWorker() {
62
- return supervisorRequest({ type: "unregister", workerId, cwd: process.cwd() }).then((response) => response.action);
63
- }
64
- function askSupervisor(hostname, port) {
65
- return supervisorRequest({ type: "network", workerId, cwd: process.cwd(), hostname, port }).then((response) => response.action);
66
- }
67
-
68
- function createMandatoryDenyPlaceholders(cwd, agentDir) {
69
- // Sandbox Runtime protects these names unconditionally. Under an outer
70
- // bubblewrap it cannot create a missing bind target after the root becomes
71
- // read-only, so create only absent regular-file placeholders before broker
72
- // bootstrap and remove exactly those placeholders at wrapper exit.
73
- const created = [];
74
- const remember = (path, kind, content) => {
75
- const stat = lstatSync(path);
76
- created.push({ path, kind, content, dev: stat.dev, ino: stat.ino });
77
- };
78
- for (const name of [
79
- ".gitconfig", ".gitmodules", ".bashrc", ".bash_profile", ".zshrc",
80
- ".zprofile", ".profile", ".ripgreprc", ".mcp.json",
81
- ]) {
82
- const path = join(cwd, name);
83
- if (existsSync(path)) continue;
84
- try {
85
- writeFileSync(path, "", { flag: "wx", mode: 0o600 });
86
- remember(path, "file", "");
87
- } catch {
88
- // Sandbox bootstrap will fail closed if a mandatory target is unusable.
89
- }
90
- }
91
- for (const directory of [".vscode", ".idea", ".claude/commands", ".claude/agents"]) {
92
- let path = cwd;
93
- for (const part of directory.split("/")) {
94
- path = join(path, part);
95
- if (existsSync(path)) continue;
96
- try {
97
- mkdirSync(path, { mode: 0o700 });
98
- remember(path, "directory");
99
- } catch {
100
- // Sandbox bootstrap will fail closed if a mandatory target is unusable.
101
- break;
102
- }
103
- }
104
- }
105
- // Keep the project-state directory available so denyWrite can bind missing
106
- // security files at their exact paths instead of masking the first missing
107
- // `.pi` component and preventing ordinary project-state creation.
108
- const projectState = join(cwd, ".pi");
109
- if (!existsSync(projectState)) {
110
- try {
111
- mkdirSync(projectState, { mode: 0o700 });
112
- remember(projectState, "directory");
113
- } catch {
114
- // Sandbox bootstrap will fail closed if the directory remains unusable.
115
- }
116
- }
117
- // The agent directory sits below denyRead(home). Bubblewrap cannot create a
118
- // missing nested bind target there after the home mask becomes read-only, so
119
- // seed valid empty JSON files and remove only unchanged placeholders later.
120
- for (const name of ["settings.json", "permissions.json", "sandbox.json"]) {
121
- const path = join(agentDir, name);
122
- if (existsSync(path)) continue;
123
- try {
124
- writeFileSync(path, "{}\n", { flag: "wx", mode: 0o600 });
125
- remember(path, "file", "{}\n");
126
- } catch {
127
- // Sandbox bootstrap will fail closed if a protected target is unusable.
128
- }
129
- }
130
- return () => {
131
- for (const item of created.reverse()) {
132
- try {
133
- const current = lstatSync(item.path);
134
- if (current.dev !== item.dev || current.ino !== item.ino) continue;
135
- if (item.kind === "directory") rmdirSync(item.path);
136
- else if (readFileSync(item.path, "utf8") === item.content) unlinkSync(item.path);
137
- } catch { /* remove only unchanged empty placeholders */ }
138
- }
139
- };
140
- }
141
- if (!realPi) {
142
- process.stderr.write("pi-sandbox: external worker wrapper has no verified Pi binary\n");
143
- process.exitCode = 1;
144
- } else {
145
- const cwd = process.cwd();
146
- const workerTempDir = mkdtempSync(join(tmpdir(), "pi-sandbox-external-worker-"));
147
- process.once("exit", () => {
148
- try { rmSync(workerTempDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
149
- });
150
- const home = resolve(homedir());
151
- const agentDir = resolve(process.env.PI_CODING_AGENT_DIR || join(home, ".pi", "agent"));
152
- const removeMandatoryDenyPlaceholders = createMandatoryDenyPlaceholders(cwd, agentDir);
153
- process.once("exit", removeMandatoryDenyPlaceholders);
154
- const sessionDir = process.env.PI_CODING_AGENT_SESSION_DIR;
155
- const packageRoot = dirname(fileURLToPath(import.meta.url));
156
- const nodeRoot = dirname(dirname(process.execPath));
157
- const runtimeRoot = dirname(dirname(fileURLToPath(import.meta.resolve("@anthropic-ai/sandbox-runtime"))));
158
- const extraRead = (process.env.PI_SANDBOX_EXTERNAL_ALLOW_READ || "")
159
- .split(":")
160
- .filter(Boolean);
161
- const existingGitConfig = [
162
- join(home, ".gitconfig"),
163
- join(home, ".config", "git", "config"),
164
- ].filter(existsSync);
165
- // On Linux Sandbox Runtime masks the first missing component of a deny path.
166
- // Denying a not-yet-created `.pi/settings.json` would therefore mask `.pi`
167
- // itself and prevent Pi from creating its ordinary project state directory.
168
- // Existing security files remain outer-sandbox protected; creation attempts
169
- // are still gated by permission-system and pi-auto-review.
170
- const workspaceSecurityFiles = [
171
- join(cwd, ".pi", "settings.json"),
172
- join(cwd, ".pi", "sandbox.json"),
173
- join(cwd, ".pi", "pi-auto-review.json"),
174
- ];
175
- const agentSecurityPaths = [
176
- join(agentDir, "settings.json"),
177
- join(agentDir, "permissions.json"),
178
- join(agentDir, "sandbox.json"),
179
- join(agentDir, "extensions"),
180
- ];
181
- const registration = await registerWorker();
182
- if (registration.action !== "allow") {
183
- process.stderr.write("pi-sandbox: external worker registration was denied\n");
184
- process.exitCode = 1;
185
- process.exit();
186
- }
187
- const registeredNetwork = decodeExternalNetworkPolicy(registration.network);
188
- const runtimeConfig = {
189
- filesystem: {
190
- denyRead: home === parse(home).root ? [] : [home],
191
- allowRead: [cwd, nodeRoot, runtimeRoot, ...existingGitConfig, "/dev/null", ...worktreeGitReadPaths(cwd), ...extraRead],
192
- // Child sessions and permission-forwarding request/response files live
193
- // here. Keep this narrower than the agent directory, whose security
194
- // settings and extensions remain read-only.
195
- allowWrite: [
196
- cwd,
197
- "/dev/null",
198
- workerTempDir,
199
- ...(sessionDir?.startsWith("/") ? [sessionDir] : []),
200
- // Pi creates directory-style settings locks and provider model caches
201
- // beside its global settings. Security-sensitive files and extensions
202
- // below remain explicit denyWrite entries.
203
- agentDir,
204
- ],
205
- denyWrite: [
206
- ...workspaceSecurityFiles,
207
- ...agentSecurityPaths,
208
- packageRoot,
209
- dirname(process.execPath),
210
- ],
211
- allowGitConfig: true,
212
- },
213
- network: {
214
- allowedDomains: registeredNetwork.allowedDomains,
215
- deniedDomains: registeredNetwork.deniedDomains,
216
- allowAllUnixSockets: false, allowUnixSockets: [],
217
- allowLocalBinding: false,
218
- },
219
- };
220
- const broker = fork(new URL("./srt-broker.mjs", import.meta.url), [], {
221
- cwd,
222
- detached: true,
223
- env: {
224
- ...process.env,
225
- PI_SANDBOX_TMPDIR: workerTempDir,
226
- CLAUDE_CODE_TMPDIR: workerTempDir,
227
- TMPDIR: workerTempDir,
228
- TMP: workerTempDir,
229
- TEMP: workerTempDir,
230
- },
231
- stdio: ["pipe", "pipe", "pipe", "ipc"],
232
- });
233
- broker.stdout.pipe(process.stdout);
234
- broker.stderr.pipe(process.stderr);
235
- process.stdin.pipe(broker.stdin);
236
- broker.on("message", (message) => {
237
- if (!message || typeof message !== "object" || message.type !== "network-request") return;
238
- void askSupervisor(message.hostname, message.port).then((action) => {
239
- if (broker.connected) broker.send({ type: "network-response", id: message.id, action });
240
- });
241
- });
242
- let prefix = [];
243
- try { prefix = JSON.parse(process.env.PI_SANDBOX_EXTERNAL_REAL_PI_PREFIX || "[]"); } catch { prefix = []; }
244
- if (!Array.isArray(prefix) || prefix.some((item) => typeof item !== "string")) {
245
- process.stderr.write("pi-sandbox: external worker wrapper received invalid Pi prefix\n");
246
- broker.kill("SIGKILL");
247
- process.exitCode = 1;
248
- } else {
249
- broker.send({ type: "init", invocation: [realPi, ...prefix, ...process.argv.slice(2)], runtimeConfig });
250
- }
251
- const stop = (signal) => {
252
- if (broker.pid) {
253
- try { process.kill(-broker.pid, signal); } catch { broker.kill(signal); }
254
- }
255
- };
256
- process.on("SIGINT", () => stop("SIGINT"));
257
- process.on("SIGTERM", () => stop("SIGTERM"));
258
- broker.once("exit", async (code, signal) => {
259
- await unregisterWorker();
260
- process.exitCode = code ?? (signal ? 1 : 1);
261
- });
262
- broker.once("error", (error) => {
263
- process.stderr.write(`pi-sandbox: external worker broker failed: ${error.message}\n`);
264
- process.exitCode = 1;
265
- });
266
- }