@erichll/pi-sandbox 0.3.1 → 0.4.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 ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0 - 2026-07-29
4
+
5
+ - Add an optional, approval-gated Host-IPC Bash backend for commands that need
6
+ access to host Unix sockets.
7
+ - Support trusted preflight command prefixes without treating a prefix as
8
+ authorization; every complete command still requires an exact one-shot
9
+ approval.
10
+ - Support one non-recursive host retry after a narrowly detected Unix-socket
11
+ `Operation not permitted` sandbox failure.
12
+ - Preserve the first attempt's output, apply only the original timeout's
13
+ remaining duration, and never retry successful, timed-out, or aborted
14
+ commands.
15
+ - Refuse Host-IPC forwarding for built-in subagents.
16
+ - Add deterministic execution and approval coverage plus a Linux tmux socket
17
+ smoke test.
18
+
19
+ ## 0.3.1 - 2026-07-29
20
+
21
+ - Add trusted global filesystem read-path extensions while keeping default
22
+ write and configuration protections intact.
23
+
24
+ ## 0.3.0 - 2026-07-28
25
+
26
+ - Initial public release.
package/README.md CHANGED
@@ -20,6 +20,9 @@ also run complete process-backed subagent trees inside independent sandboxes.
20
20
  independent policy, proxy, approval, and cleanup lifecycles.
21
21
  - Project and global Pi security configuration, the installed package, Git
22
22
  hooks, and other Sandbox Runtime mandatory paths remain write-protected.
23
+ - Optional host IPC execution is disabled by default. When enabled, every
24
+ complete host command still requires a one-shot `pi-auto-review` or human
25
+ approval and runs outside the OS sandbox.
23
26
 
24
27
  Windows is not supported by this Pi adapter.
25
28
 
@@ -62,6 +65,39 @@ Supported modes:
62
65
  The configuration parser rejects malformed JSON, unknown fields, and unknown
63
66
  providers instead of silently weakening isolation.
64
67
 
68
+ ## Optional host IPC fallback
69
+
70
+ Some host services expose only a Unix socket that the OS sandbox cannot use.
71
+ The trusted global configuration can enable an approval-driven local Bash
72
+ backend for those commands:
73
+
74
+ ```json
75
+ {
76
+ "hostIPC": {
77
+ "mode": "ask",
78
+ "preflightCommandPrefixes": ["tmux", "/usr/bin/tmux"],
79
+ "retryOnUnixSocketError": true
80
+ }
81
+ }
82
+ ```
83
+
84
+ `mode` is `off` by default and only accepts `off` or `ask`. Prefixes are
85
+ trimmed, deduplicated, and must be non-empty. A prefix matches only at the
86
+ start of the trimmed command and must end at whitespace or the end of the
87
+ command. Prefixes select commands for review; they never authorize execution.
88
+ The reviewer and one-shot grant are bound to the full command and working
89
+ directory.
90
+
91
+ With `retryOnUnixSocketError` enabled, commands not selected for preflight
92
+ still run in Sandbox Runtime first. A single host retry is considered only
93
+ after a nonzero exit whose stderr identifies both `Operation not permitted`
94
+ and a socket/connect/IPC operation. The original output is retained, and the
95
+ approval warns that the first attempt may already have had partial side
96
+ effects. Successful, timed-out, or aborted commands are never retried.
97
+
98
+ Host forwarding is intentionally unavailable inside built-in subagents in
99
+ this version.
100
+
65
101
  ## Additional trusted read paths
66
102
 
67
103
  The trusted global configuration may append absolute paths to the sandbox's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erichll/pi-sandbox",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "files": [
31
31
  "src",
32
+ "CHANGELOG.md",
32
33
  "README.md",
33
34
  "NOTICE"
34
35
  ],
@@ -50,7 +51,7 @@
50
51
  },
51
52
  "dependencies": {
52
53
  "@anthropic-ai/sandbox-runtime": "0.0.67",
53
- "@erichll/pi-auto-review": "0.3.0"
54
+ "@erichll/pi-auto-review": "0.3.1"
54
55
  },
55
56
  "peerDependencies": {
56
57
  "@earendil-works/pi-coding-agent": ">=0.82.1",
package/src/approval.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  } from "@erichll/pi-auto-review/broker";
5
5
  import { sandboxTrapToBoundaryRequest } from "@erichll/pi-auto-review/sandbox";
6
6
  import type { SandboxApprovalTrap } from "./traps.ts";
7
+ import type { HostIPCTrigger } from "./host-ipc.ts";
7
8
  import { randomUUID } from "node:crypto";
8
9
  export type NetworkEndpoint = {
9
10
  hostname: string;
@@ -56,6 +57,33 @@ export async function approveDomainEndpoint(
56
57
  return approveBoundaryRequest(request, context);
57
58
  }
58
59
 
60
+ export async function approveHostIPCExecution(
61
+ trigger: HostIPCTrigger,
62
+ context: TrapApprovalContext,
63
+ ): Promise<TrapApprovalResult> {
64
+ const request: BoundaryRequest = {
65
+ id: `sandbox-runtime-host-ipc-${randomUUID()}`,
66
+ source: "sandbox-runtime",
67
+ surface: "host-ipc",
68
+ operation: "execute-host",
69
+ cwd: context.cwd,
70
+ command: context.command,
71
+ matchedPolicy: {
72
+ decision: "ask",
73
+ rule:
74
+ trigger.reason === "preflight-prefix"
75
+ ? `preflight-prefix:${trigger.rule}`
76
+ : "unix-socket-eperm",
77
+ },
78
+ toolInputPreview:
79
+ trigger.reason === "unix-socket-eperm"
80
+ ? "The sandboxed attempt failed and may already have had partial side effects."
81
+ : "A configured prefix requested approval before sandbox execution.",
82
+ agentName: context.agentName,
83
+ };
84
+ return approveBoundaryRequest(request, context);
85
+ }
86
+
59
87
  async function askHuman(
60
88
  request: BoundaryRequest,
61
89
  context: TrapApprovalContext,
package/src/config.ts CHANGED
@@ -10,6 +10,15 @@ export const SUBAGENT_PROVIDERS = [
10
10
 
11
11
  export type SubagentProvider = (typeof SUBAGENT_PROVIDERS)[number];
12
12
 
13
+ export const HOST_IPC_MODES = ["off", "ask"] as const;
14
+ export type HostIPCMode = (typeof HOST_IPC_MODES)[number];
15
+
16
+ export type HostIPCConfig = {
17
+ mode: HostIPCMode;
18
+ preflightCommandPrefixes: readonly string[];
19
+ retryOnUnixSocketError: boolean;
20
+ };
21
+
13
22
  export type PiSandboxConfig = {
14
23
  subagents: {
15
24
  provider: SubagentProvider;
@@ -17,6 +26,7 @@ export type PiSandboxConfig = {
17
26
  filesystem: {
18
27
  additionalAllowRead: readonly string[];
19
28
  };
29
+ hostIPC: HostIPCConfig;
20
30
  };
21
31
 
22
32
  export type LoadPiSandboxConfigOptions = {
@@ -31,6 +41,11 @@ export const DEFAULT_PI_SANDBOX_CONFIG: Readonly<PiSandboxConfig> = Object.freez
31
41
  filesystem: Object.freeze({
32
42
  additionalAllowRead: Object.freeze([]),
33
43
  }),
44
+ hostIPC: Object.freeze({
45
+ mode: "off",
46
+ preflightCommandPrefixes: Object.freeze([]),
47
+ retryOnUnixSocketError: false,
48
+ }),
34
49
  },
35
50
  );
36
51
 
@@ -59,7 +74,7 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
59
74
  if (!isRecord(value)) {
60
75
  throw new Error("invalid pi-sandbox configuration: root must be an object");
61
76
  }
62
- rejectUnknownKeys(value, ["subagents", "filesystem"], "root");
77
+ rejectUnknownKeys(value, ["subagents", "filesystem", "hostIPC"], "root");
63
78
 
64
79
  if (value.subagents !== undefined && !isRecord(value.subagents)) {
65
80
  throw new Error(
@@ -108,6 +123,48 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
108
123
  );
109
124
  }
110
125
 
126
+ if (value.hostIPC !== undefined && !isRecord(value.hostIPC)) {
127
+ throw new Error(
128
+ "invalid pi-sandbox configuration: hostIPC must be an object",
129
+ );
130
+ }
131
+ const hostIPC = value.hostIPC ?? {};
132
+ rejectUnknownKeys(
133
+ hostIPC,
134
+ ["mode", "preflightCommandPrefixes", "retryOnUnixSocketError"],
135
+ "hostIPC",
136
+ );
137
+ const hostIPCMode = hostIPC.mode ?? DEFAULT_PI_SANDBOX_CONFIG.hostIPC.mode;
138
+ if (
139
+ typeof hostIPCMode !== "string" ||
140
+ !HOST_IPC_MODES.includes(hostIPCMode as HostIPCMode)
141
+ ) {
142
+ throw new Error(
143
+ `invalid pi-sandbox configuration: hostIPC.mode must be one of ${HOST_IPC_MODES.join(", ")}`,
144
+ );
145
+ }
146
+ const preflightCommandPrefixes =
147
+ hostIPC.preflightCommandPrefixes ??
148
+ DEFAULT_PI_SANDBOX_CONFIG.hostIPC.preflightCommandPrefixes;
149
+ if (
150
+ !Array.isArray(preflightCommandPrefixes) ||
151
+ preflightCommandPrefixes.some(
152
+ (prefix) => typeof prefix !== "string" || prefix.trim() === "",
153
+ )
154
+ ) {
155
+ throw new Error(
156
+ "invalid pi-sandbox configuration: hostIPC.preflightCommandPrefixes must be an array of non-empty strings",
157
+ );
158
+ }
159
+ const retryOnUnixSocketError =
160
+ hostIPC.retryOnUnixSocketError ??
161
+ DEFAULT_PI_SANDBOX_CONFIG.hostIPC.retryOnUnixSocketError;
162
+ if (typeof retryOnUnixSocketError !== "boolean") {
163
+ throw new Error(
164
+ "invalid pi-sandbox configuration: hostIPC.retryOnUnixSocketError must be a boolean",
165
+ );
166
+ }
167
+
111
168
  return {
112
169
  subagents: {
113
170
  provider: provider as SubagentProvider,
@@ -115,6 +172,13 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
115
172
  filesystem: {
116
173
  additionalAllowRead: [...new Set(additionalAllowRead)],
117
174
  },
175
+ hostIPC: {
176
+ mode: hostIPCMode as HostIPCMode,
177
+ preflightCommandPrefixes: [
178
+ ...new Set(preflightCommandPrefixes.map((prefix) => prefix.trim())),
179
+ ],
180
+ retryOnUnixSocketError,
181
+ },
118
182
  };
119
183
  }
120
184
 
@@ -139,6 +203,14 @@ export function loadPiSandboxConfig(
139
203
  ...DEFAULT_PI_SANDBOX_CONFIG.filesystem.additionalAllowRead,
140
204
  ],
141
205
  },
206
+ hostIPC: {
207
+ mode: DEFAULT_PI_SANDBOX_CONFIG.hostIPC.mode,
208
+ preflightCommandPrefixes: [
209
+ ...DEFAULT_PI_SANDBOX_CONFIG.hostIPC.preflightCommandPrefixes,
210
+ ],
211
+ retryOnUnixSocketError:
212
+ DEFAULT_PI_SANDBOX_CONFIG.hostIPC.retryOnUnixSocketError,
213
+ },
142
214
  };
143
215
  }
144
216
  throw new Error(`failed to read pi-sandbox configuration at ${path}`, {
@@ -0,0 +1,160 @@
1
+ import type { HostIPCConfig } from "./config.ts";
2
+ import type { TrapApprovalResult } from "./approval.ts";
3
+
4
+ const MAX_STDERR_BYTES = 64 * 1024;
5
+ const SUBAGENT_DEPTH_ENV = "PI_SANDBOX_SUBAGENT_DEPTH";
6
+
7
+ export type HostIPCTrigger =
8
+ | {
9
+ reason: "preflight-prefix";
10
+ rule: string;
11
+ }
12
+ | {
13
+ reason: "unix-socket-eperm";
14
+ rule: "unix-socket-eperm";
15
+ };
16
+
17
+ type CommandResult = { exitCode: number | null };
18
+
19
+ export type HostIPCCommandOptions = {
20
+ command: string;
21
+ cwd: string;
22
+ env?: NodeJS.ProcessEnv;
23
+ signal?: AbortSignal;
24
+ timeout?: number;
25
+ onData(data: Buffer): void;
26
+ config: HostIPCConfig;
27
+ approve(trigger: HostIPCTrigger): Promise<TrapApprovalResult>;
28
+ runSandbox(onStderr: (data: Buffer) => void): Promise<CommandResult>;
29
+ runHost(timeout: number | undefined): Promise<CommandResult>;
30
+ now?: () => number;
31
+ };
32
+
33
+ export function matchingPreflightPrefix(
34
+ command: string,
35
+ prefixes: readonly string[],
36
+ ): string | undefined {
37
+ const trimmed = command.trim();
38
+ return prefixes.find(
39
+ (prefix) =>
40
+ trimmed.startsWith(prefix) &&
41
+ (trimmed.length === prefix.length ||
42
+ /\s/.test(trimmed.charAt(prefix.length))),
43
+ );
44
+ }
45
+
46
+ export function isUnixSocketPermissionError(stderr: Buffer | string): boolean {
47
+ const text = typeof stderr === "string" ? stderr : stderr.toString("utf8");
48
+ return (
49
+ /operation not permitted/i.test(text) &&
50
+ /socket|connect(?:ing)?|ipc/i.test(text)
51
+ );
52
+ }
53
+
54
+ function appendBounded(current: Buffer, chunk: Buffer): Buffer {
55
+ if (chunk.length >= MAX_STDERR_BYTES) {
56
+ return chunk.subarray(chunk.length - MAX_STDERR_BYTES);
57
+ }
58
+ const combined = Buffer.concat([current, chunk]);
59
+ return combined.length <= MAX_STDERR_BYTES
60
+ ? combined
61
+ : combined.subarray(combined.length - MAX_STDERR_BYTES);
62
+ }
63
+
64
+ function isBuiltinSubagent(env: NodeJS.ProcessEnv | undefined): boolean {
65
+ return Number(env?.[SUBAGENT_DEPTH_ENV] ?? process.env[SUBAGENT_DEPTH_ENV]) > 0;
66
+ }
67
+
68
+ function remainingTimeout(
69
+ timeout: number | undefined,
70
+ startedAt: number,
71
+ now: () => number,
72
+ ): number | undefined {
73
+ if (timeout === undefined) return undefined;
74
+ const remaining = timeout - (now() - startedAt) / 1_000;
75
+ if (remaining <= 0) throw new Error(`timeout:${timeout}`);
76
+ return remaining;
77
+ }
78
+
79
+ function refusal(options: HostIPCCommandOptions, reason?: string): CommandResult {
80
+ options.onData(
81
+ Buffer.from(
82
+ `pi-sandbox: host-IPC execution denied${reason ? `: ${reason}` : ""}\n`,
83
+ "utf8",
84
+ ),
85
+ );
86
+ return { exitCode: 1 };
87
+ }
88
+
89
+ export async function runCommandWithHostIPC(
90
+ options: HostIPCCommandOptions,
91
+ ): Promise<CommandResult> {
92
+ const now = options.now ?? Date.now;
93
+ const startedAt = now();
94
+ const enabled = options.config.mode === "ask";
95
+ const prefix = enabled
96
+ ? matchingPreflightPrefix(
97
+ options.command,
98
+ options.config.preflightCommandPrefixes,
99
+ )
100
+ : undefined;
101
+
102
+ if (prefix !== undefined) {
103
+ if (isBuiltinSubagent(options.env)) {
104
+ return refusal(
105
+ options,
106
+ "builtin subagents do not support host forwarding",
107
+ );
108
+ }
109
+ const approval = await options.approve({
110
+ reason: "preflight-prefix",
111
+ rule: prefix,
112
+ });
113
+ if (approval.action === "deny") {
114
+ return refusal(options, approval.reason);
115
+ }
116
+ return options.runHost(
117
+ remainingTimeout(options.timeout, startedAt, now),
118
+ );
119
+ }
120
+
121
+ let stderr: Buffer<ArrayBufferLike> = Buffer.alloc(0);
122
+ const sandboxResult = await options.runSandbox((data) => {
123
+ stderr = appendBounded(stderr, data);
124
+ });
125
+ if (
126
+ !enabled ||
127
+ !options.config.retryOnUnixSocketError ||
128
+ sandboxResult.exitCode === 0 ||
129
+ sandboxResult.exitCode === null ||
130
+ options.signal?.aborted ||
131
+ !isUnixSocketPermissionError(stderr)
132
+ ) {
133
+ return sandboxResult;
134
+ }
135
+ if (isBuiltinSubagent(options.env)) {
136
+ options.onData(
137
+ Buffer.from(
138
+ "pi-sandbox: host-IPC retry refused: builtin subagents do not support host forwarding\n",
139
+ "utf8",
140
+ ),
141
+ );
142
+ return sandboxResult;
143
+ }
144
+
145
+ const approval = await options.approve({
146
+ reason: "unix-socket-eperm",
147
+ rule: "unix-socket-eperm",
148
+ });
149
+ if (approval.action === "deny") return sandboxResult;
150
+
151
+ options.onData(
152
+ Buffer.from(
153
+ "\n--- pi-sandbox: approved host-IPC retry (outside OS sandbox) ---\n",
154
+ "utf8",
155
+ ),
156
+ );
157
+ return options.runHost(
158
+ remainingTimeout(options.timeout, startedAt, now),
159
+ );
160
+ }
package/src/index.ts CHANGED
@@ -5,18 +5,22 @@ import type {
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import {
7
7
  createBashToolDefinition,
8
+ createLocalBashOperations,
8
9
  SettingsManager,
9
10
  } from "@earendil-works/pi-coding-agent";
10
11
  import { getBoundaryBroker } from "@erichll/pi-auto-review/broker";
11
12
  import {
12
13
  approveDomainEndpoint,
14
+ approveHostIPCExecution,
13
15
  approveSandboxTrap,
14
16
  type HumanApproval,
15
17
  } from "./approval.ts";
16
18
  import {
17
19
  loadPiSandboxConfig,
20
+ type HostIPCConfig,
18
21
  type SubagentProvider,
19
22
  } from "./config.ts";
23
+ import { runCommandWithHostIPC } from "./host-ipc.ts";
20
24
  import {
21
25
  runSandboxedCommand,
22
26
  type SandboxCommandOptions,
@@ -36,6 +40,8 @@ export type PiSandboxExtensionOptions = {
36
40
  subagentManager?: ProcessBackedSubagentManager;
37
41
  createSubagentManager?: () => ProcessBackedSubagentManager;
38
42
  sandbox?: Pick<SandboxCommandOptions, "broker" | "platform">;
43
+ /** Test/embedding override. Normal extension loading uses trusted global config. */
44
+ hostIPC?: HostIPCConfig;
39
45
  };
40
46
 
41
47
  function sessionId(ctx: ExtensionContext): string {
@@ -82,13 +88,21 @@ function humanApproval(ctx: ExtensionContext): HumanApproval {
82
88
  return async (request, reason, signal) => {
83
89
  if (!ctx.hasUI) return "deny";
84
90
  const target =
85
- request.resolvedPath ??
86
- request.path ??
87
- request.destination ??
88
- request.operation;
91
+ request.surface === "host-ipc"
92
+ ? request.command ?? request.operation
93
+ : request.resolvedPath ??
94
+ request.path ??
95
+ request.destination ??
96
+ request.operation;
89
97
  const suffix = reason ? `\n${reason}` : "";
98
+ const hostWarning =
99
+ request.surface === "host-ipc"
100
+ ? request.matchedPolicy?.rule === "unix-socket-eperm"
101
+ ? "\nWarning: the first sandboxed attempt may already have had partial side effects. The retry runs on the host outside the OS sandbox."
102
+ : "\nWarning: this command will run on the host outside the OS sandbox."
103
+ : "";
90
104
  const selected = await ctx.ui.select(
91
- `Sandbox approval required: ${request.operation} ${target}${suffix}`,
105
+ `Sandbox approval required: ${request.operation} ${target}${hostWarning}${suffix}`,
92
106
  ["Allow this exact operation once", "Deny"],
93
107
  { signal },
94
108
  );
@@ -102,53 +116,81 @@ function sandboxOperations(
102
116
  ctx: ExtensionContext,
103
117
  turnIndex: () => number,
104
118
  additionalAllowRead: readonly string[],
119
+ hostIPC: HostIPCConfig,
105
120
  sandbox?: PiSandboxExtensionOptions["sandbox"],
106
121
  ): BashOperations {
107
122
  return {
108
123
  exec(command, cwd, options) {
109
124
  const currentSessionId = sessionId(ctx);
110
- return runSandboxedCommand({
125
+ const shellPath = SettingsManager.create(cwd).getShellPath();
126
+ const approvalContext = {
127
+ broker: getBoundaryBroker(),
128
+ command,
129
+ cwd,
130
+ sessionId: currentSessionId,
131
+ scopeKey: `${currentSessionId}:turn:${turnIndex()}`,
132
+ signal: options.signal,
133
+ humanApproval: humanApproval(ctx),
134
+ };
135
+ const local = createLocalBashOperations({ shellPath });
136
+ return runCommandWithHostIPC({
111
137
  command,
112
138
  cwd,
113
139
  env: options.env,
114
140
  signal: options.signal,
115
141
  timeout: options.timeout,
116
- ...sandbox,
117
142
  onData: options.onData,
118
- shellPath: SettingsManager.create(cwd).getShellPath(),
119
- policy: createDefaultPolicy(cwd, { additionalAllowRead }),
120
- review: async (trap) => {
121
- const result = await approveSandboxTrap(trap, {
122
- broker: getBoundaryBroker(),
123
- command,
124
- cwd,
125
- sessionId: currentSessionId,
126
- scopeKey: `${currentSessionId}:turn:${turnIndex()}`,
127
- signal: options.signal,
128
- humanApproval: humanApproval(ctx),
129
- });
130
- if (result.action === "deny" && result.reason) {
131
- if (ctx.hasUI) {
132
- ctx.ui.notify(`Sandbox denied: ${result.reason}`, "warning");
133
- }
143
+ config: hostIPC,
144
+ approve: async (trigger) => {
145
+ const result = await approveHostIPCExecution(
146
+ trigger,
147
+ approvalContext,
148
+ );
149
+ if (result.action === "deny" && result.reason && ctx.hasUI) {
150
+ ctx.ui.notify(`Host-IPC denied: ${result.reason}`, "warning");
134
151
  }
135
- return result.action;
152
+ return result;
136
153
  },
137
- reviewDomain: async (endpoint) => {
138
- const result = await approveDomainEndpoint(endpoint, {
139
- broker: getBoundaryBroker(),
154
+ runHost: (timeout) =>
155
+ local.exec(command, cwd, {
156
+ ...options,
157
+ timeout,
158
+ }),
159
+ runSandbox: (onStderr) =>
160
+ runSandboxedCommand({
140
161
  command,
141
162
  cwd,
142
- sessionId: currentSessionId,
143
- scopeKey: `${currentSessionId}:turn:${turnIndex()}`,
163
+ env: options.env,
144
164
  signal: options.signal,
145
- humanApproval: humanApproval(ctx),
146
- });
147
- if (result.action === "deny" && result.reason && ctx.hasUI) {
148
- ctx.ui.notify(`Domain proxy denied: ${result.reason}`, "warning");
149
- }
150
- return result.action;
151
- },
165
+ timeout: options.timeout,
166
+ ...sandbox,
167
+ onData: options.onData,
168
+ onStderr,
169
+ shellPath,
170
+ policy: createDefaultPolicy(cwd, { additionalAllowRead }),
171
+ review: async (trap) => {
172
+ const result = await approveSandboxTrap(trap, approvalContext);
173
+ if (result.action === "deny" && result.reason) {
174
+ if (ctx.hasUI) {
175
+ ctx.ui.notify(`Sandbox denied: ${result.reason}`, "warning");
176
+ }
177
+ }
178
+ return result.action;
179
+ },
180
+ reviewDomain: async (endpoint) => {
181
+ const result = await approveDomainEndpoint(
182
+ endpoint,
183
+ approvalContext,
184
+ );
185
+ if (result.action === "deny" && result.reason && ctx.hasUI) {
186
+ ctx.ui.notify(
187
+ `Domain proxy denied: ${result.reason}`,
188
+ "warning",
189
+ );
190
+ }
191
+ return result.action;
192
+ },
193
+ }),
152
194
  });
153
195
  },
154
196
  };
@@ -163,6 +205,7 @@ async function performRegistration(
163
205
  const subagentProvider =
164
206
  options.subagentProvider ?? config.subagents.provider;
165
207
  const additionalAllowRead = config.filesystem.additionalAllowRead;
208
+ const hostIPC = options.hostIPC ?? config.hostIPC;
166
209
  const subagents =
167
210
  subagentProvider === "builtin"
168
211
  ? (options.subagentManager ??
@@ -185,6 +228,7 @@ async function performRegistration(
185
228
  ctx,
186
229
  () => currentTurn,
187
230
  additionalAllowRead,
231
+ hostIPC,
188
232
  options.sandbox,
189
233
  ),
190
234
  });
@@ -423,6 +467,7 @@ async function performRegistration(
423
467
  ctx,
424
468
  () => currentTurn,
425
469
  additionalAllowRead,
470
+ hostIPC,
426
471
  options.sandbox,
427
472
  ),
428
473
  }));