@erichll/pi-sandbox 0.7.0 → 0.8.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 CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 - 2026-08-18
4
+
5
+ - **Trusted network domain policies.** Persistent `network.allowedDomains` and
6
+ `network.deniedDomains` arrays in the trusted global configuration
7
+ (`~/.pi/agent/extensions/pi-sandbox/config.json`, with legacy fallback) now
8
+ authorize matching connections statically, without the one-shot reviewer.
9
+ Entries use Sandbox Runtime-compatible patterns: exact domains,
10
+ strict-subdomain wildcards such as `*.example.com`, and an optional `:port`;
11
+ a bare `*` (optionally with a port) is accepted only in `deniedDomains`.
12
+ Values are trimmed and deduplicated, and malformed or unknown configuration
13
+ fails closed. Precedence is deterministic — deny, then allow, then the
14
+ existing one-shot reviewer — and empty arrays preserve the previous
15
+ behavior (no persistent network authorization, with every eligible public
16
+ connection reviewed once).
17
+ - **Policy parity across execution surfaces.** The same copied network policy
18
+ is applied to main-agent Bash, built-in subagents, and outer-isolated
19
+ `pi-subagents` workers. External workers receive policy only through their
20
+ capability-bound supervisor registration; missing, malformed, or extra-field
21
+ transport becomes deny-all without disabling dynamic review for valid
22
+ policies.
23
+ - **Exfiltration-risk documentation.** The README now warns that domain
24
+ allowlists are not a complete data-loss-prevention boundary, that
25
+ multi-tenant or user-uploadable destinations (for example `github.com`) can
26
+ themselves be exfiltration channels, and that pi-sandbox never infers or
27
+ silently adds domains.
28
+ - Pin `@anthropic-ai/sandbox-runtime` to `0.0.73` (fixed version; its
29
+ network-policy evaluation semantics are reused by the static allow/deny
30
+ matching).
31
+
3
32
  ## 0.7.0 - 2026-08-17
4
33
 
5
34
  - **Builtin `subagent` model fail-fast.** An explicit `model` on `start`/
package/README.md CHANGED
@@ -12,9 +12,10 @@ also run complete process-backed subagent trees inside independent sandboxes.
12
12
  writes outside the workspace fail closed. Runtime filesystem denials are not
13
13
  converted into dynamic grants because Sandbox Runtime does not expose a
14
14
  trustworthy filesystem ask callback.
15
- - Network access is denied by default. Each unmatched destination is sent to
16
- `pi-auto-review` as a concrete hostname and port. An approval applies only to
17
- that connection.
15
+ - Network policy is evaluated as static deny, then static allow, then dynamic
16
+ review. Each unmatched public destination is sent to `pi-auto-review` as a
17
+ canonical hostname and concrete port. An approval applies only to that
18
+ connection.
18
19
  - Every Bash command and persistent subagent session owns a separate broker
19
20
  process and Sandbox Runtime manager. Concurrent workers therefore keep
20
21
  independent policy, proxy, approval, and cleanup lifecycles.
@@ -118,6 +119,53 @@ such as `return runs.run('main', { agent, task })`. The compatibility gate's
118
119
  The configuration parser rejects malformed JSON, unknown fields, and unknown
119
120
  providers instead of silently weakening isolation.
120
121
 
122
+ ## Network domain policy
123
+
124
+ Persistent domain authorization is accepted only from the trusted global
125
+ `~/.pi/agent/extensions/pi-sandbox/config.json` (or its legacy trusted fallback
126
+ when the new file is absent). Project configuration cannot override it.
127
+
128
+ ```json
129
+ {
130
+ "network": {
131
+ "allowedDomains": [
132
+ "github.com",
133
+ "*.github.com",
134
+ "registry.npmjs.org:443"
135
+ ],
136
+ "deniedDomains": [
137
+ "uploads.github.com",
138
+ "*:22"
139
+ ]
140
+ }
141
+ }
142
+ ```
143
+
144
+ Entries support exact domains, strict-subdomain wildcards such as
145
+ `*.example.com`, and an optional `:port` restriction. A bare `*` (optionally
146
+ with a port) is accepted only in `deniedDomains`. Values are trimmed and
147
+ deduplicated, and malformed or unknown configuration fails closed.
148
+
149
+ The precedence is deterministic:
150
+
151
+ 1. A matching `deniedDomains` entry rejects the connection.
152
+ 2. Otherwise, a matching `allowedDomains` entry permits it without review.
153
+ 3. Otherwise, the canonical public hostname and port enter the existing
154
+ one-shot auto-review or human approval flow.
155
+
156
+ Both arrays default to empty. That preserves the prior behavior: no persistent
157
+ network authorization, with every eligible public connection reviewed once.
158
+ The same trusted arrays are copied into main-agent Bash, built-in subagents,
159
+ and external `pi-subagents` workers when outer isolation is enforced. Missing
160
+ or malformed external-worker policy transport hard-denies network access.
161
+
162
+ Domain allowlists are not a complete data-loss-prevention boundary. A
163
+ multi-tenant or user-uploadable destination such as `github.com` can itself be
164
+ an exfiltration channel; grant only the narrow domains and ports you intend.
165
+ Real package-manager traffic may require additional hosts—for example, PyPI
166
+ downloads commonly use `files.pythonhosted.org`. `pi-sandbox` never infers or
167
+ silently adds those domains.
168
+
121
169
  To opt in to outer worker isolation for the external provider, use:
122
170
 
123
171
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erichll/pi-sandbox",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",
@@ -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.71",
53
+ "@anthropic-ai/sandbox-runtime": "0.0.73",
54
54
  "@erichll/pi-auto-review": "0.3.5"
55
55
  },
56
56
  "peerDependencies": {
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { isAbsolute, join } from "node:path";
4
+ import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
4
5
 
5
6
  export const SUBAGENT_PROVIDERS = [
6
7
  "builtin",
@@ -19,6 +20,11 @@ export type HostIPCConfig = {
19
20
  retryOnUnixSocketError: boolean;
20
21
  };
21
22
 
23
+ export type NetworkConfig = {
24
+ readonly allowedDomains: readonly string[];
25
+ readonly deniedDomains: readonly string[];
26
+ };
27
+
22
28
  export type PiSandboxConfig = {
23
29
  subagents: {
24
30
  provider: SubagentProvider;
@@ -27,6 +33,7 @@ export type PiSandboxConfig = {
27
33
  filesystem: {
28
34
  additionalAllowRead: readonly string[];
29
35
  };
36
+ network: NetworkConfig;
30
37
  hostIPC: HostIPCConfig;
31
38
  };
32
39
 
@@ -45,6 +52,10 @@ export const DEFAULT_PI_SANDBOX_CONFIG: Readonly<PiSandboxConfig> = Object.freez
45
52
  filesystem: Object.freeze({
46
53
  additionalAllowRead: Object.freeze([]),
47
54
  }),
55
+ network: Object.freeze({
56
+ allowedDomains: Object.freeze([]),
57
+ deniedDomains: Object.freeze([]),
58
+ }),
48
59
  hostIPC: Object.freeze({
49
60
  mode: "off",
50
61
  preflightCommandPrefixes: Object.freeze([]),
@@ -86,11 +97,40 @@ function rejectUnknownKeys(
86
97
  }
87
98
  }
88
99
 
100
+ function hasValidDomainLabels(
101
+ pattern: string,
102
+ allowDenyAll: boolean,
103
+ ): boolean {
104
+ const portMatch = pattern.match(/:([1-9][0-9]{0,4})$/);
105
+ const hostPattern = portMatch
106
+ ? pattern.slice(0, -portMatch[0].length)
107
+ : pattern;
108
+ if (portMatch && Number(portMatch[1]) > 65_535) return false;
109
+ if (hostPattern === "*") return allowDenyAll;
110
+ const hostname = hostPattern.startsWith("*.")
111
+ ? hostPattern.slice(2)
112
+ : hostPattern;
113
+ return (
114
+ hostname.length <= 253 &&
115
+ hostname.includes(".") &&
116
+ hostname.split(".").every(
117
+ (label) =>
118
+ label.length > 0 &&
119
+ label.length <= 63 &&
120
+ /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label),
121
+ )
122
+ );
123
+ }
124
+
89
125
  export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
90
126
  if (!isRecord(value)) {
91
127
  throw new Error("invalid pi-sandbox configuration: root must be an object");
92
128
  }
93
- rejectUnknownKeys(value, ["subagents", "filesystem", "hostIPC"], "root");
129
+ rejectUnknownKeys(
130
+ value,
131
+ ["subagents", "filesystem", "network", "hostIPC"],
132
+ "root",
133
+ );
94
134
 
95
135
  if (value.subagents !== undefined && !isRecord(value.subagents)) {
96
136
  throw new Error(
@@ -154,6 +194,58 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
154
194
  );
155
195
  }
156
196
 
197
+ if (value.network !== undefined && !isRecord(value.network)) {
198
+ throw new Error(
199
+ "invalid pi-sandbox configuration: network must be an object",
200
+ );
201
+ }
202
+ const network = value.network ?? {};
203
+ rejectUnknownKeys(
204
+ network,
205
+ ["allowedDomains", "deniedDomains"],
206
+ "network",
207
+ );
208
+ const normalizeDomainList = (
209
+ key: "allowedDomains" | "deniedDomains",
210
+ ): string[] => {
211
+ const configured = network[key] ?? DEFAULT_PI_SANDBOX_CONFIG.network[key];
212
+ if (
213
+ !Array.isArray(configured) ||
214
+ configured.some(
215
+ (pattern) => typeof pattern !== "string" || pattern.trim() === "",
216
+ )
217
+ ) {
218
+ throw new Error(
219
+ `invalid pi-sandbox configuration: network.${key} must be an array of non-empty strings`,
220
+ );
221
+ }
222
+ const normalized = [
223
+ ...new Set(configured.map((pattern) => pattern.trim())),
224
+ ];
225
+ const invalidIndex = normalized.findIndex(
226
+ (pattern) => !hasValidDomainLabels(pattern, key === "deniedDomains"),
227
+ );
228
+ if (invalidIndex >= 0) {
229
+ throw new Error(
230
+ `invalid pi-sandbox configuration: network.${key}[${invalidIndex}] contains an invalid domain pattern`,
231
+ );
232
+ }
233
+ const candidate = {
234
+ allowedDomains: key === "allowedDomains" ? normalized : [],
235
+ deniedDomains: key === "deniedDomains" ? normalized : [],
236
+ };
237
+ const result = NetworkConfigSchema.safeParse(candidate);
238
+ if (!result.success) {
239
+ const issue = result.error.issues[0];
240
+ throw new Error(
241
+ `invalid pi-sandbox configuration: network.${key}${typeof issue?.path[1] === "number" ? `[${issue.path[1]}]` : ""} ${issue?.message ?? "contains an invalid domain pattern"}`,
242
+ );
243
+ }
244
+ return normalized;
245
+ };
246
+ const allowedDomains = normalizeDomainList("allowedDomains");
247
+ const deniedDomains = normalizeDomainList("deniedDomains");
248
+
157
249
  if (value.hostIPC !== undefined && !isRecord(value.hostIPC)) {
158
250
  throw new Error(
159
251
  "invalid pi-sandbox configuration: hostIPC must be an object",
@@ -204,6 +296,10 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
204
296
  filesystem: {
205
297
  additionalAllowRead: [...new Set(additionalAllowRead)],
206
298
  },
299
+ network: {
300
+ allowedDomains: [...allowedDomains],
301
+ deniedDomains: [...deniedDomains],
302
+ },
207
303
  hostIPC: {
208
304
  mode: hostIPCMode as HostIPCMode,
209
305
  preflightCommandPrefixes: [
@@ -226,6 +322,10 @@ function defaultPiSandboxConfig(): PiSandboxConfig {
226
322
  ...DEFAULT_PI_SANDBOX_CONFIG.filesystem.additionalAllowRead,
227
323
  ],
228
324
  },
325
+ network: {
326
+ allowedDomains: [...DEFAULT_PI_SANDBOX_CONFIG.network.allowedDomains],
327
+ deniedDomains: [...DEFAULT_PI_SANDBOX_CONFIG.network.deniedDomains],
328
+ },
229
329
  hostIPC: {
230
330
  mode: DEFAULT_PI_SANDBOX_CONFIG.hostIPC.mode,
231
331
  preflightCommandPrefixes: [
@@ -0,0 +1,8 @@
1
+ export type ExternalNetworkPolicy = {
2
+ allowedDomains: string[];
3
+ deniedDomains: string[];
4
+ };
5
+
6
+ export function decodeExternalNetworkPolicy(
7
+ value: unknown,
8
+ ): ExternalNetworkPolicy;
@@ -0,0 +1,27 @@
1
+ import { NetworkConfigSchema } from "@anthropic-ai/sandbox-runtime";
2
+
3
+ /**
4
+ * Decode the supervisor-owned network policy delivered during registration.
5
+ * Any missing, extra, or malformed data becomes a deny-all policy so a broken
6
+ * transport can never widen an external worker's access.
7
+ */
8
+ export function decodeExternalNetworkPolicy(value) {
9
+ if (
10
+ !value ||
11
+ typeof value !== "object" ||
12
+ Array.isArray(value) ||
13
+ Object.keys(value).some(
14
+ (key) => key !== "allowedDomains" && key !== "deniedDomains",
15
+ )
16
+ ) {
17
+ return { allowedDomains: [], deniedDomains: ["*"] };
18
+ }
19
+ const result = NetworkConfigSchema.safeParse(value);
20
+ if (!result.success) {
21
+ return { allowedDomains: [], deniedDomains: ["*"] };
22
+ }
23
+ return {
24
+ allowedDomains: [...result.data.allowedDomains],
25
+ deniedDomains: [...result.data.deniedDomains],
26
+ };
27
+ }
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import type { NetworkEndpoint, TrapApprovalContext } from "./approval.ts";
7
7
  import { approveDomainEndpoint } from "./approval.ts";
8
+ import type { NetworkConfig } from "./config.ts";
8
9
 
9
10
  const MAX_FRAME_BYTES = 16 * 1024;
10
11
  const PROTOCOL_VERSION = 1;
@@ -43,7 +44,12 @@ export type ExternalWorkerSupervisor = {
43
44
  export async function createExternalWorkerSupervisor(
44
45
  approvalContext: (worker: { id: string; cwd: string }) => TrapApprovalContext,
45
46
  lifecycle: ExternalWorkerSupervisorLifecycle = {},
47
+ network: NetworkConfig = { allowedDomains: [], deniedDomains: [] },
46
48
  ): Promise<ExternalWorkerSupervisor> {
49
+ const trustedNetwork = {
50
+ allowedDomains: [...network.allowedDomains],
51
+ deniedDomains: [...network.deniedDomains],
52
+ };
47
53
  const directory = await mkdtemp(join(tmpdir(), "pi-sandbox-external-"));
48
54
  await chmod(directory, 0o700);
49
55
  const socketPath = join(directory, "supervisor.sock");
@@ -101,7 +107,14 @@ export async function createExternalWorkerSupervisor(
101
107
  startedAt: Date.now(),
102
108
  requests: 0,
103
109
  });
104
- socket.write(`${JSON.stringify({ id: request.id, action: "allow" })}\n`);
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`);
105
118
  lifecycle.registered?.({
106
119
  id: request.workerId as string,
107
120
  cwd: request.cwd as string,
@@ -8,6 +8,7 @@ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync,
8
8
  import { homedir, tmpdir } from "node:os";
9
9
  import { dirname, join, parse, resolve } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { decodeExternalNetworkPolicy } from "./external-network-policy.mjs";
11
12
 
12
13
  const realPi = process.env.PI_SANDBOX_EXTERNAL_REAL_PI_BINARY;
13
14
  const workerId = randomUUID();
@@ -32,19 +33,22 @@ function worktreeGitReadPaths(cwd) {
32
33
  function supervisorRequest(payload) {
33
34
  const socketPath = process.env.PI_SANDBOX_EXTERNAL_SUPERVISOR_SOCKET;
34
35
  const capability = process.env.PI_SANDBOX_EXTERNAL_SUPERVISOR_CAPABILITY;
35
- if (!socketPath || !capability) return Promise.resolve("deny");
36
- return new Promise((resolveAction) => {
36
+ if (!socketPath || !capability) return Promise.resolve({ action: "deny" });
37
+ return new Promise((resolveResponse) => {
37
38
  const socket = createConnection(socketPath);
38
39
  let buffer = "";
39
- const finish = (action) => { socket.destroy(); resolveAction(action === "allow" ? "allow" : "deny"); };
40
+ const finish = (response) => {
41
+ socket.destroy();
42
+ resolveResponse(response?.action === "allow" ? response : { action: "deny" });
43
+ };
40
44
  socket.setEncoding("utf8");
41
- socket.setTimeout(30_000, () => finish("deny"));
42
- socket.once("error", () => finish("deny"));
45
+ socket.setTimeout(30_000, () => finish({ action: "deny" }));
46
+ socket.once("error", () => finish({ action: "deny" }));
43
47
  socket.on("data", (chunk) => {
44
48
  buffer += chunk;
45
49
  const newline = buffer.indexOf("\n");
46
50
  if (newline < 0 || Buffer.byteLength(buffer, "utf8") > 16 * 1024) return;
47
- try { finish(JSON.parse(buffer.slice(0, newline)).action); } catch { finish("deny"); }
51
+ try { finish(JSON.parse(buffer.slice(0, newline))); } catch { finish({ action: "deny" }); }
48
52
  });
49
53
  socket.once("connect", () => socket.write(`${JSON.stringify({
50
54
  version: 1, capability, id: randomUUID(), ...payload,
@@ -55,10 +59,10 @@ function registerWorker() {
55
59
  return supervisorRequest({ type: "register", workerId, cwd: process.cwd() });
56
60
  }
57
61
  function unregisterWorker() {
58
- return supervisorRequest({ type: "unregister", workerId, cwd: process.cwd() });
62
+ return supervisorRequest({ type: "unregister", workerId, cwd: process.cwd() }).then((response) => response.action);
59
63
  }
60
64
  function askSupervisor(hostname, port) {
61
- return supervisorRequest({ type: "network", workerId, cwd: process.cwd(), hostname, port });
65
+ return supervisorRequest({ type: "network", workerId, cwd: process.cwd(), hostname, port }).then((response) => response.action);
62
66
  }
63
67
 
64
68
  function createMandatoryDenyPlaceholders(cwd, agentDir) {
@@ -174,6 +178,13 @@ if (!realPi) {
174
178
  join(agentDir, "sandbox.json"),
175
179
  join(agentDir, "extensions"),
176
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);
177
188
  const runtimeConfig = {
178
189
  filesystem: {
179
190
  denyRead: home === parse(home).root ? [] : [home],
@@ -200,16 +211,12 @@ if (!realPi) {
200
211
  allowGitConfig: true,
201
212
  },
202
213
  network: {
203
- allowedDomains: [], deniedDomains: [], allowLocalBinding: false,
214
+ allowedDomains: registeredNetwork.allowedDomains,
215
+ deniedDomains: registeredNetwork.deniedDomains,
204
216
  allowAllUnixSockets: false, allowUnixSockets: [],
217
+ allowLocalBinding: false,
205
218
  },
206
219
  };
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
220
  const broker = fork(new URL("./srt-broker.mjs", import.meta.url), [], {
214
221
  cwd,
215
222
  detached: true,
package/src/index.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  import {
20
20
  loadPiSandboxConfig,
21
21
  type HostIPCConfig,
22
+ type NetworkConfig,
22
23
  type SubagentProvider,
23
24
  } from "./config.ts";
24
25
  import { runCommandWithHostIPC } from "./host-ipc.ts";
@@ -171,6 +172,7 @@ function sandboxOperations(
171
172
  ctx: ExtensionContext,
172
173
  turnIndex: () => number,
173
174
  additionalAllowRead: readonly string[],
175
+ network: NetworkConfig,
174
176
  hostIPC: HostIPCConfig,
175
177
  sandbox?: PiSandboxExtensionOptions["sandbox"],
176
178
  ): BashOperations {
@@ -222,7 +224,7 @@ function sandboxOperations(
222
224
  onData: options.onData,
223
225
  onStderr,
224
226
  shellPath,
225
- policy: createDefaultPolicy(cwd, { additionalAllowRead }),
227
+ policy: createDefaultPolicy(cwd, { additionalAllowRead, network }),
226
228
  review: async (trap) => {
227
229
  const result = await approveSandboxTrap(trap, approvalContext);
228
230
  if (result.action === "deny" && result.reason) {
@@ -265,6 +267,7 @@ async function performRegistration(
265
267
  externalWorkerIsolation,
266
268
  );
267
269
  const additionalAllowRead = config.filesystem.additionalAllowRead;
270
+ const network = config.network;
268
271
  const hostIPC = options.hostIPC ?? config.hostIPC;
269
272
  const subagents =
270
273
  subagentProvider === "builtin"
@@ -292,6 +295,7 @@ async function performRegistration(
292
295
  ctx,
293
296
  () => currentTurn,
294
297
  additionalAllowRead,
298
+ network,
295
299
  hostIPC,
296
300
  options.sandbox,
297
301
  ),
@@ -380,7 +384,7 @@ async function performRegistration(
380
384
  cwd: ctx.cwd,
381
385
  model: params.model,
382
386
  tools: pi.getActiveTools().filter((name) => name !== "subagent"),
383
- policy: createDefaultPolicy(ctx.cwd, { additionalAllowRead }),
387
+ policy: createDefaultPolicy(ctx.cwd, { additionalAllowRead, network }),
384
388
  sandbox: options.sandbox,
385
389
  review: async (trap: Parameters<typeof approveSandboxTrap>[0]) =>
386
390
  (await approveSandboxTrap(trap, approvalContext)).action,
@@ -546,6 +550,7 @@ async function performRegistration(
546
550
  ctx,
547
551
  () => currentTurn,
548
552
  additionalAllowRead,
553
+ network,
549
554
  hostIPC,
550
555
  options.sandbox,
551
556
  ),
@@ -606,6 +611,7 @@ async function performRegistration(
606
611
  registered: (worker) => fleetView?.registered(worker),
607
612
  unregistered: (worker) => fleetView?.unregistered(worker.id),
608
613
  },
614
+ network,
609
615
  );
610
616
  isolation = enableExternalWorkerIsolation(supervisor);
611
617
  externalIsolationFailure = undefined;
@@ -657,8 +663,8 @@ async function performRegistration(
657
663
  if (ctx.hasUI) {
658
664
  ctx.ui.notify(
659
665
  process.platform === "darwin"
660
- ? `${EXTENSION_NAME} enabled: macOS Sandbox Runtime with static filesystem policy and per-connection network review`
661
- : `${EXTENSION_NAME} enabled: Linux Sandbox Runtime with static filesystem policy and per-connection network review`,
666
+ ? `${EXTENSION_NAME} enabled: macOS Sandbox Runtime with static filesystem/domain policy and per-connection network review`
667
+ : `${EXTENSION_NAME} enabled: Linux Sandbox Runtime with static filesystem/domain policy and per-connection network review`,
662
668
  "info",
663
669
  );
664
670
  }
package/src/policy.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import type { SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
13
+ import type { NetworkConfig } from "./config.ts";
13
14
 
14
15
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
15
16
  const NODE_INSTALL_ROOT = dirname(dirname(process.execPath));
@@ -120,6 +121,7 @@ export type SandboxPolicy = {
120
121
 
121
122
  export type CreateDefaultPolicyOptions = {
122
123
  additionalAllowRead?: readonly string[];
124
+ network?: NetworkConfig;
123
125
  };
124
126
 
125
127
  export function isSecretDenyWriteBasename(name: string): boolean {
@@ -271,8 +273,8 @@ export function createDefaultPolicy(
271
273
  ],
272
274
  },
273
275
  network: {
274
- allowedDomains: [],
275
- deniedDomains: [],
276
+ allowedDomains: [...(options.network?.allowedDomains ?? [])],
277
+ deniedDomains: [...(options.network?.deniedDomains ?? [])],
276
278
  allowLocalBinding: false,
277
279
  allowAllUnixSockets: false,
278
280
  allowUnixSockets: [],