@erichll/pi-sandbox 0.15.3 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.0 - 2026-09-05
4
+
5
+ - Depend on the coordinated `@erichll/pi-auto-review 0.16.0` release.
6
+ - Replace the obsolete `externalWorkerIsolation` integration with the required
7
+ `native-background-tools` protection mode for `pi-subagents 0.65.0`.
8
+ - Require a validated canonical native-agent whitelist, explicit asynchronous
9
+ launches, ambient child extensions, disabled nested delegation, and
10
+ `scheduledRuns.enabled=false`.
11
+ - Register the upstream capability ceiling with only `bash`, `read`, `grep`,
12
+ `find`, and `ls`; child writes therefore pass through sandboxed Bash while
13
+ `write`, `edit`, MCP, and extension tools are unavailable.
14
+ - Reject external runners, all workflow forms, schedules, resume, and agent or
15
+ workflow management mutations. Protected mode supports only explicit async
16
+ direct-agent launches because 0.65.0 workflow children disable ambient
17
+ extensions.
18
+ - Remove the obsolete external launcher, supervisor, network-policy transport,
19
+ and FleetView bridge. This mode is a tool boundary, not OS process isolation;
20
+ use the default `builtin` provider when whole-worker isolation is required.
21
+
3
22
  ## 0.15.3 - 2026-09-03
4
23
 
5
24
  - Reviewer decisions on sandbox network boundaries inherit the fenced-JSON
package/README.md CHANGED
@@ -9,7 +9,7 @@ also run complete process-backed subagent trees inside independent sandboxes.
9
9
  - [Security model](#security-model)
10
10
  - [Linux requirements](#linux-requirements)
11
11
  - [Subagent provider](#subagent-provider)
12
- - [`pi-subagents` 0.64.0 capability boundary](#pi-subagents-0640-capability-boundary)
12
+ - [`pi-subagents` 0.65.0 native-background tool boundary](#pi-subagents-0650-native-background-tool-boundary)
13
13
  - [Network domain policy](#network-domain-policy)
14
14
  - [Optional host IPC fallback](#optional-host-ipc-fallback)
15
15
  - [Additional trusted read paths](#additional-trusted-read-paths)
@@ -81,8 +81,7 @@ new installs.
81
81
  ```json
82
82
  {
83
83
  "subagents": {
84
- "provider": "builtin",
85
- "externalWorkerIsolation": "off"
84
+ "provider": "builtin"
86
85
  }
87
86
  }
88
87
  ```
@@ -91,58 +90,57 @@ Supported modes:
91
90
 
92
91
  - `builtin` (default): register the process-backed `subagent` tool and sandbox
93
92
  each complete worker process tree.
94
- - `pi-subagents`: let the external extension own orchestration. Main-agent and
95
- inherited Bash execution remains sandboxed, but the external worker process
96
- itself is not wrapped while `externalWorkerIsolation` is `off`.
93
+ - `pi-subagents`: let `pi-subagents 0.65.0` own orchestration under the required
94
+ native-background tool boundary described below.
97
95
  - `off`: protect Bash only.
98
96
 
99
- ### `pi-subagents` 0.64.0 capability boundary
97
+ ### `pi-subagents` 0.65.0 native-background tool boundary
98
+
99
+ This provider is intentionally locked to exactly `pi-subagents 0.65.0` and
100
+ fails closed if its public ceiling API or internal discovery layout drifts.
101
+ Configure both the protection mode and a non-empty canonical whitelist:
100
102
 
101
- `pi-sandbox` verifies the following combination in its test suite and release
102
- gate. This version boundary is pinned exactly (see the package
103
- `devDependencies`) and additionally exercised against the latest published
104
- `pi-subagents` on a nightly CI schedule. The `PI_SUBAGENT_PI_BINARY` seam
105
- (`externalWorkerIsolation: "enforce"`) is re-checked deterministically by the
106
- `gate:external-isolation` probe on the pinned and latest dependency. The
107
- published peer range remains `>=0.50.0`; this documents the current tested
108
- development baseline, not a runtime minimum.
103
+ ```json
104
+ {
105
+ "subagents": {
106
+ "provider": "pi-subagents",
107
+ "protection": "native-background-tools",
108
+ "allowedNativeAgents": ["worker", "reviewer", "scout"]
109
+ }
110
+ }
111
+ ```
109
112
 
110
- | Capability | `builtin` | `pi-subagents` 0.64.0 |
113
+ The `pi-subagents` config at
114
+ `~/.pi/agent/extensions/subagent/config.json` must also contain
115
+ `"scheduledRuns": { "enabled": false }`. Every launch must explicitly set
116
+ `async: true`. Only direct single-agent launches are supported. In 0.65.0,
117
+ public `workflowScript` / `workflowScriptPath` children disable ambient
118
+ extensions, so both are rejected along with named workflows, schedules,
119
+ configuration mutations, resume, nested subagents, and external runners.
120
+
121
+ Every child is constrained by the upstream capability ceiling to `bash`,
122
+ `read`, `grep`, `find`, and `ls`. The child loads ambient extensions and must
123
+ acknowledge `@erichll:pi-sandbox`; writes can therefore occur only through the
124
+ sandboxed Bash tool. `write`, `edit`, MCP, and other extension tools are not
125
+ available.
126
+
127
+ | Capability | `builtin` | `pi-subagents` 0.65.0 protected mode |
111
128
  | --- | --- | --- |
112
- | Outer worker sandbox | Yes | Opt-in (`externalWorkerIsolation: "enforce"`) |
129
+ | Outer worker sandbox | Yes | No |
113
130
  | Bash sandbox | Yes | Yes |
114
131
  | Persistent follow-up | Yes | Yes |
115
- | `workflowScript`, missions, schedules | No | Yes |
116
- | Named workflow resources | No | Yes |
117
- | permission-system parent forwarding | Not required | Verified by compatibility gate |
132
+ | Public async `workflowScript` | No | No (ambient extensions disabled upstream) |
133
+ | Schedules / named workflow resources | No | No |
134
+ | Child tools | Configured parent tools | Fixed five-tool ceiling |
118
135
 
119
- Moving the tested baseline requires re-running `npm run gate:external-isolation`
120
- and, before a release, the model-backed `npm run gate:pi-subagents`. See
136
+ Before a release, run the deterministic and model-backed portions of
137
+ `npm run gate:pi-subagents`. See
121
138
  `docs/compat-notes.md` for the recorded compatibility seams and how each is
122
139
  verified on upgrade.
123
140
 
124
- The 0.64.0 baseline also exercises tasks passed as an `@task.md` argument,
125
- standard Git worktree metadata (`.git` pointer plus `commondir`), and the
126
- read-only `watchdog_diff` tool through the real outer-sandbox launcher. Its
127
- watchdog `action: "block"` launch rule is checked before worker spawn planning,
128
- and the model-backed gate verifies that a blocked child creates no transcript
129
- before running the normal isolated-worker probes. Worktrunk itself is not a
130
- dependency; provider selection remains upstream behavior.
131
-
132
- For the external provider, `off` leaves the worker process outside the outer
133
- Sandbox Runtime boundary. Do not treat inherited Bash sandboxing as complete
134
- worker isolation unless the trusted global configuration explicitly enables
135
- `enforce`.
136
-
137
- Note on the external provider's public execution surface: pi-subagents 0.43.0
138
- removed the old static multi-agent controls; `workflowScript` remains the
139
- public multi-agent orchestration surface. Current releases also accept a direct
140
- single-child `{ agent, task }` launch. The compatibility gate deliberately uses
141
- `return runs.run('main', { agent, task })` so the single-child probe continues
142
- to exercise the workflow executor and its isolation seam.
143
-
144
141
  The configuration parser rejects malformed JSON, unknown fields, and unknown
145
- providers instead of silently weakening isolation.
142
+ providers instead of silently weakening isolation. All legacy
143
+ `externalWorkerIsolation` configurations produce a migration error.
146
144
 
147
145
  ## Network domain policy
148
146
 
@@ -180,9 +178,8 @@ The precedence is deterministic:
180
178
 
181
179
  Both arrays default to empty. That preserves the prior behavior: no persistent
182
180
  network authorization, with every eligible public connection reviewed once.
183
- The same trusted arrays are copied into main-agent Bash, built-in subagents,
184
- and external `pi-subagents` workers when outer isolation is enforced. Missing
185
- or malformed external-worker policy transport hard-denies network access.
181
+ The same trusted arrays govern main-agent Bash, built-in subagents, and the
182
+ sandboxed Bash tool loaded by protected native `pi-subagents` children.
186
183
 
187
184
  Domain allowlists are not a complete data-loss-prevention boundary. A
188
185
  multi-tenant or user-uploadable destination such as `github.com` can itself be
@@ -191,28 +188,9 @@ Real package-manager traffic may require additional hosts—for example, PyPI
191
188
  downloads commonly use `files.pythonhosted.org`. `pi-sandbox` never infers or
192
189
  silently adds those domains.
193
190
 
194
- To opt in to outer worker isolation for the external provider, use:
195
-
196
- ```json
197
- {
198
- "subagents": {
199
- "provider": "pi-subagents",
200
- "externalWorkerIsolation": "enforce"
201
- }
202
- }
203
- ```
204
-
205
- `enforce` installs a session-scoped `PI_SUBAGENT_PI_BINARY` wrapper which
206
- starts the real Pi worker under a dedicated Sandbox Runtime broker. Bootstrap
207
- failure is terminal for that worker; it never falls back to host execution.
208
- The wrapper is preserved for nested child launches. This is opt-in while the
209
- permission-forwarding supervisor and managed-worktree gate continue to mature.
210
-
211
- Managed worktrees receive their own writable checkout plus the smallest known
212
- Git metadata paths as read-only access so `git status` works. The main
213
- repository `.git` is never made writable by this policy; Git mutation from an
214
- outer-sandboxed external worker is therefore intentionally unsupported for
215
- now.
191
+ Native background runner initialization itself is outside the OS sandbox.
192
+ Only the child's Bash modification channel is sandboxed. Select `builtin` when
193
+ the entire worker process tree must be contained by Sandbox Runtime.
216
194
 
217
195
  ## Optional host IPC fallback
218
196
 
@@ -298,17 +276,11 @@ npm test
298
276
  npm run gate:pi-subagents
299
277
  ```
300
278
 
301
- `gate:pi-subagents` is a release/manual integration check, not a default CI
302
- test. Set `PI_SUBAGENTS_GATE_MODEL` to a configured test model and provide its
303
- normal Pi credential environment. The command creates a dedicated temporary
304
- agent directory, never reads or updates your production Pi config, and prints
305
- `SKIP` when those prerequisites are absent.
306
-
307
- It allows 10 minutes for the direct-child phase and 15 minutes for the
308
- multi-stage workflow, and exits only after the parent reports returned child
309
- results. Override those bounds with positive millisecond values in
310
- `PI_SUBAGENTS_GATE_DIRECT_TIMEOUT_MS` and
311
- `PI_SUBAGENTS_GATE_WORKFLOW_TIMEOUT_MS` when needed for a dedicated provider.
279
+ `gate:pi-subagents` verifies the pinned package layout, discovery behavior,
280
+ capability ceiling, and protected launch policy in an isolated temporary agent
281
+ directory. It never reads or updates production Pi configuration. Model-backed
282
+ acceptance requires `PI_SUBAGENTS_GATE_MODEL` and an already-exported matching
283
+ credential; missing prerequisites are reported as `SKIP`.
312
284
 
313
285
  The test suite covers real Linux Sandbox Runtime enforcement when its native
314
286
  dependencies are installed, plus deterministic broker, network approval,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erichll/pi-sandbox",
3
- "version": "0.15.3",
3
+ "version": "0.16.0",
4
4
  "description": "Linux (bubblewrap) and macOS (Seatbelt) process sandbox for Pi backed by Anthropic's @anthropic-ai/sandbox-runtime. Fail-closed static filesystem policy plus per-connection network approval from pi-auto-review; sandboxes main-agent Bash and built-in subagent process trees.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -51,11 +51,12 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@anthropic-ai/sandbox-runtime": "0.0.75",
54
- "@erichll/pi-auto-review": "0.15.3"
54
+ "@erichll/pi-auto-review": "0.16.0",
55
+ "jiti": "2.7.0"
55
56
  },
56
57
  "peerDependencies": {
57
- "@earendil-works/pi-coding-agent": "^0.84.1",
58
- "pi-subagents": ">=0.50.0",
58
+ "@earendil-works/pi-coding-agent": "^0.85.0",
59
+ "pi-subagents": "0.65.0",
59
60
  "typebox": ">=1.0.0"
60
61
  },
61
62
  "peerDependenciesMeta": {
@@ -64,9 +65,10 @@
64
65
  }
65
66
  },
66
67
  "devDependencies": {
67
- "@earendil-works/pi-coding-agent": "^0.84.1",
68
+ "@earendil-works/pi-coding-agent": "^0.85.0",
69
+ "@earendil-works/pi-server": "^0.85.0",
68
70
  "@types/node": "^26.2.0",
69
- "pi-subagents": "0.64.0",
71
+ "pi-subagents": "0.65.0",
70
72
  "typescript": "^6.0.3"
71
73
  }
72
74
  }
package/src/config.ts CHANGED
@@ -28,7 +28,8 @@ export type NetworkConfig = {
28
28
  export type PiSandboxConfig = {
29
29
  subagents: {
30
30
  provider: SubagentProvider;
31
- externalWorkerIsolation: "off" | "enforce";
31
+ protection?: "native-background-tools";
32
+ allowedNativeAgents?: readonly string[];
32
33
  };
33
34
  filesystem: {
34
35
  additionalAllowRead: readonly string[];
@@ -47,7 +48,6 @@ export const DEFAULT_PI_SANDBOX_CONFIG: Readonly<PiSandboxConfig> = Object.freez
47
48
  {
48
49
  subagents: Object.freeze({
49
50
  provider: "builtin",
50
- externalWorkerIsolation: "off",
51
51
  }),
52
52
  filesystem: Object.freeze({
53
53
  additionalAllowRead: Object.freeze([]),
@@ -138,9 +138,14 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
138
138
  );
139
139
  }
140
140
  const subagents = value.subagents ?? {};
141
+ if (Object.hasOwn(subagents, "externalWorkerIsolation")) {
142
+ throw new Error(
143
+ "invalid pi-sandbox configuration: subagents.externalWorkerIsolation was removed in 0.16.0; migrate pi-subagents to protection 'native-background-tools' with allowedNativeAgents, or use provider 'builtin' for OS-level worker isolation",
144
+ );
145
+ }
141
146
  rejectUnknownKeys(
142
147
  subagents,
143
- ["provider", "externalWorkerIsolation"],
148
+ ["provider", "protection", "allowedNativeAgents"],
144
149
  "subagents",
145
150
  );
146
151
 
@@ -154,17 +159,39 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
154
159
  `invalid pi-sandbox configuration: subagents.provider must be one of ${SUBAGENT_PROVIDERS.join(", ")}`,
155
160
  );
156
161
  }
157
- const externalWorkerIsolation =
158
- subagents.externalWorkerIsolation ??
159
- DEFAULT_PI_SANDBOX_CONFIG.subagents.externalWorkerIsolation;
160
- if (
161
- externalWorkerIsolation !== "off" &&
162
- externalWorkerIsolation !== "enforce"
163
- ) {
162
+ const protection = subagents.protection;
163
+ const allowedNativeAgents = subagents.allowedNativeAgents;
164
+ if (provider === "pi-subagents" && protection !== "native-background-tools") {
164
165
  throw new Error(
165
- "invalid pi-sandbox configuration: subagents.externalWorkerIsolation must be off or enforce",
166
+ "invalid pi-sandbox configuration: provider 'pi-subagents' requires subagents.protection 'native-background-tools'; migrate legacy configurations explicitly",
166
167
  );
167
168
  }
169
+ if (provider !== "pi-subagents" && (protection !== undefined || allowedNativeAgents !== undefined)) {
170
+ throw new Error(
171
+ "invalid pi-sandbox configuration: subagents.protection and allowedNativeAgents are only valid with provider 'pi-subagents'",
172
+ );
173
+ }
174
+ if (provider === "pi-subagents") {
175
+ if (!Array.isArray(allowedNativeAgents) || allowedNativeAgents.length === 0) {
176
+ throw new Error(
177
+ "invalid pi-sandbox configuration: subagents.allowedNativeAgents must be a non-empty array of canonical agent names",
178
+ );
179
+ }
180
+ if (allowedNativeAgents.some((name) =>
181
+ typeof name !== "string" ||
182
+ name !== name.trim() ||
183
+ !/^[A-Za-z0-9_.:-]+$/u.test(name)
184
+ )) {
185
+ throw new Error(
186
+ "invalid pi-sandbox configuration: subagents.allowedNativeAgents must contain only canonical agent names",
187
+ );
188
+ }
189
+ if (new Set(allowedNativeAgents).size !== allowedNativeAgents.length) {
190
+ throw new Error(
191
+ "invalid pi-sandbox configuration: subagents.allowedNativeAgents must not contain duplicates",
192
+ );
193
+ }
194
+ }
168
195
 
169
196
  if (value.filesystem !== undefined && !isRecord(value.filesystem)) {
170
197
  throw new Error(
@@ -291,7 +318,12 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
291
318
  return {
292
319
  subagents: {
293
320
  provider: provider as SubagentProvider,
294
- externalWorkerIsolation,
321
+ ...(provider === "pi-subagents"
322
+ ? {
323
+ protection: "native-background-tools" as const,
324
+ allowedNativeAgents: [...(allowedNativeAgents as string[])],
325
+ }
326
+ : {}),
295
327
  },
296
328
  filesystem: {
297
329
  additionalAllowRead: [...new Set(additionalAllowRead)],
@@ -314,8 +346,6 @@ function defaultPiSandboxConfig(): PiSandboxConfig {
314
346
  return {
315
347
  subagents: {
316
348
  provider: DEFAULT_PI_SANDBOX_CONFIG.subagents.provider,
317
- externalWorkerIsolation:
318
- DEFAULT_PI_SANDBOX_CONFIG.subagents.externalWorkerIsolation,
319
349
  },
320
350
  filesystem: {
321
351
  additionalAllowRead: [
package/src/index.ts CHANGED
@@ -3,7 +3,6 @@ import type {
3
3
  ExtensionAPI,
4
4
  ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { fileURLToPath } from "node:url";
7
6
  import {
8
7
  createBashToolDefinition,
9
8
  createLocalBashOperations,
@@ -35,14 +34,12 @@ import type {
35
34
  import { validateSubagentModel } from "./subagent.ts";
36
35
  import { Type } from "typebox";
37
36
  import {
38
- createExternalWorkerSupervisor,
39
- type ExternalWorkerSupervisor,
40
- } from "./external-supervisor.ts";
41
- import {
42
- createExternalRunsView,
43
- externalRunsViewEnabledWhen,
44
- type ExternalRunsView,
45
- } from "./external-runs-view.ts";
37
+ loadPiSubagentsNativeRuntime,
38
+ nativeSubagentCallBlockReason,
39
+ PI_SANDBOX_ACKNOWLEDGEMENT,
40
+ terminalChildrenHaveSandboxAcknowledgement,
41
+ type CapabilityCeilingHandle,
42
+ } from "./pi-subagents-native.ts";
46
43
 
47
44
  const EXTENSION_NAME = "pi-sandbox";
48
45
  const registrations = new WeakMap<ExtensionAPI, Promise<void>>();
@@ -54,12 +51,19 @@ export type PiSandboxExtensionOptions = {
54
51
  sandbox?: Pick<SandboxCommandOptions, "broker" | "platform">;
55
52
  /** Test/embedding override. Normal extension loading uses trusted global config. */
56
53
  hostIPC?: HostIPCConfig;
54
+ /** Test/embedding override. Normal extension loading uses trusted global config. */
55
+ allowedNativeAgents?: readonly string[];
57
56
  };
58
57
 
59
58
  function sessionId(ctx: ExtensionContext): string {
60
59
  return ctx.sessionManager.getSessionId();
61
60
  }
62
61
 
62
+ function piSubagentsSessionId(ctx: ExtensionContext): string {
63
+ const getSessionFile = (ctx.sessionManager as { getSessionFile?: () => string | null | undefined }).getSessionFile;
64
+ return getSessionFile?.call(ctx.sessionManager) ?? ctx.sessionManager.getSessionId();
65
+ }
66
+
63
67
  function isPiSubagentsSource(
64
68
  sourceInfo: ReturnType<ExtensionAPI["getAllTools"]>[number]["sourceInfo"],
65
69
  ): boolean {
@@ -91,55 +95,11 @@ function externalSubagentDiagnostic(pi: ExtensionAPI): {
91
95
  }
92
96
  return {
93
97
  message:
94
- "pi-subagents orchestration active; pi-sandbox protects Bash execution. External workers are not yet wrapped in an outer Sandbox Runtime sandbox.",
98
+ "pi-subagents protected native-background mode active; child writes are restricted to sandboxed Bash. This is a tool boundary, not outer worker process isolation.",
95
99
  level: "info",
96
100
  };
97
101
  }
98
102
 
99
- const EXTERNAL_WORKER_LAUNCHER = fileURLToPath(
100
- new URL("./external-worker-launcher.mjs", import.meta.url),
101
- );
102
-
103
- type ExternalWorkerIsolation = {
104
- restore(): void;
105
- };
106
-
107
- function enableExternalWorkerIsolation(
108
- supervisor: ExternalWorkerSupervisor,
109
- ): ExternalWorkerIsolation {
110
- if (process.platform !== "linux" && process.platform !== "darwin") {
111
- throw new Error(`external worker isolation is unavailable on ${process.platform}`);
112
- }
113
- if (process.env.PI_SUBAGENT_PI_BINARY) {
114
- throw new Error(
115
- "external worker isolation refuses to replace an existing PI_SUBAGENT_PI_BINARY wrapper",
116
- );
117
- }
118
- const entry = process.argv[1];
119
- if (!entry) {
120
- throw new Error("external worker isolation cannot determine the real Pi CLI entrypoint");
121
- }
122
- const injected = {
123
- PI_SUBAGENT_PI_BINARY: EXTERNAL_WORKER_LAUNCHER,
124
- PI_SANDBOX_EXTERNAL_REAL_PI_BINARY: process.execPath,
125
- PI_SANDBOX_EXTERNAL_REAL_PI_PREFIX: JSON.stringify([entry]),
126
- PI_SANDBOX_EXTERNAL_ALLOW_READ: [
127
- process.cwd(),
128
- process.env.PI_CODING_AGENT_DIR,
129
- ].filter((value): value is string => Boolean(value)).join(":"),
130
- PI_SANDBOX_EXTERNAL_SUPERVISOR_SOCKET: supervisor.socketPath,
131
- PI_SANDBOX_EXTERNAL_SUPERVISOR_CAPABILITY: supervisor.capability,
132
- };
133
- Object.assign(process.env, injected);
134
- return {
135
- restore() {
136
- for (const [key, value] of Object.entries(injected)) {
137
- if (process.env[key] === value) delete process.env[key];
138
- }
139
- },
140
- };
141
- }
142
-
143
103
  function humanApproval(ctx: ExtensionContext): HumanApproval {
144
104
  return async (request, reason, signal) => {
145
105
  if (!ctx.hasUI) return "deny";
@@ -261,11 +221,19 @@ async function performRegistration(
261
221
  const config = loadPiSandboxConfig();
262
222
  const subagentProvider =
263
223
  options.subagentProvider ?? config.subagents.provider;
264
- const externalWorkerIsolation = config.subagents.externalWorkerIsolation;
265
- const externalRunsViewEnabled = externalRunsViewEnabledWhen(
266
- subagentProvider,
267
- externalWorkerIsolation,
268
- );
224
+ const isNativeSubagentChild = process.env.PI_SUBAGENT_CHILD === "1";
225
+ const protectExternalOrchestration =
226
+ subagentProvider === "pi-subagents" && !isNativeSubagentChild;
227
+ const allowedNativeAgents = options.allowedNativeAgents ??
228
+ config.subagents.allowedNativeAgents;
229
+ if (protectExternalOrchestration && !allowedNativeAgents?.length) {
230
+ throw new Error(
231
+ `${EXTENSION_NAME}: pi-subagents protected mode requires allowedNativeAgents`,
232
+ );
233
+ }
234
+ const nativeRuntime = protectExternalOrchestration
235
+ ? await loadPiSubagentsNativeRuntime(allowedNativeAgents!)
236
+ : undefined;
269
237
  const additionalAllowRead = config.filesystem.additionalAllowRead;
270
238
  const network = config.network;
271
239
  const hostIPC = options.hostIPC ?? config.hostIPC;
@@ -279,10 +247,8 @@ async function performRegistration(
279
247
  }))
280
248
  : undefined;
281
249
  const cwd = process.cwd();
282
- let isolation: ExternalWorkerIsolation | undefined;
283
- let supervisor: ExternalWorkerSupervisor | undefined;
284
- let fleetView: ExternalRunsView | undefined;
285
- let externalIsolationFailure: string | undefined;
250
+ let capabilityCeiling: CapabilityCeilingHandle | undefined;
251
+ let validatedNativeAgents: string[] = [];
286
252
  const localBash = createBashToolDefinition(cwd);
287
253
 
288
254
  pi.registerTool({
@@ -560,93 +526,91 @@ async function performRegistration(
560
526
  currentTurn = event.turnIndex;
561
527
  });
562
528
 
563
- // pi-subagents owns the public tool, so a failed wrapper bootstrap cannot
564
- // be fixed by replacing that tool. Block at Pi's pre-execution boundary:
565
- // enforce must never degrade into a host worker launch.
566
- pi.on("tool_call", (event) => {
529
+ pi.on("tool_call", (event, ctx) => {
530
+ if (event.toolName === "subagent" && protectExternalOrchestration) {
531
+ const reason = nativeSubagentCallBlockReason(event.input);
532
+ if (reason) {
533
+ return { block: true, reason: `${EXTENSION_NAME}: ${reason}` };
534
+ }
535
+ if (event.input.action === undefined) {
536
+ try {
537
+ validatedNativeAgents = nativeRuntime!.validateAllowedAgents(
538
+ ctx.cwd,
539
+ ctx.model?.provider,
540
+ );
541
+ capabilityCeiling?.dispose();
542
+ capabilityCeiling = nativeRuntime!.registerCeiling(
543
+ piSubagentsSessionId(ctx),
544
+ validatedNativeAgents,
545
+ );
546
+ } catch (error) {
547
+ return {
548
+ block: true,
549
+ reason: `${EXTENSION_NAME}: protected launch validation failed: ${error instanceof Error ? error.message : String(error)}`,
550
+ };
551
+ }
552
+ }
553
+ }
554
+ return undefined;
555
+ });
556
+
557
+ pi.on("tool_result", (event) => {
567
558
  if (
559
+ protectExternalOrchestration &&
568
560
  event.toolName === "subagent" &&
569
- subagentProvider === "pi-subagents" &&
570
- externalWorkerIsolation === "enforce" &&
571
- !isolation
561
+ (event.input.action === "status" || event.input.action === "debug.run") &&
562
+ !event.isError &&
563
+ !terminalChildrenHaveSandboxAcknowledgement(event.details)
572
564
  ) {
573
565
  return {
574
- block: true,
575
- reason: `${EXTENSION_NAME}: external worker isolation is unavailable; refusing host worker launch${externalIsolationFailure ? `: ${externalIsolationFailure}` : ""}`,
566
+ content: [{
567
+ type: "text",
568
+ text: `${EXTENSION_NAME}: child result did not acknowledge ${PI_SANDBOX_ACKNOWLEDGEMENT}; protected execution proof is missing`,
569
+ }],
570
+ isError: true,
576
571
  };
577
572
  }
578
573
  return undefined;
579
574
  });
580
575
 
581
576
  pi.on("session_start", async (_event, ctx) => {
582
- // A new session may replace a prior supervisor ownership; drop the old
583
- // session's FleetView registrations before reconstructing.
584
- fleetView?.close();
585
- fleetView = undefined;
586
- if (
587
- subagentProvider === "pi-subagents" &&
588
- externalWorkerIsolation === "enforce"
589
- ) {
590
- try {
591
- isolation?.restore();
592
- await supervisor?.close();
593
- supervisor = undefined;
594
- if (externalRunsViewEnabled) {
595
- fleetView = await createExternalRunsView(sessionId(ctx));
596
- }
597
- supervisor = await createExternalWorkerSupervisor(
598
- (worker) => ({
599
- broker: getBoundaryBroker(),
600
- command: "external pi-subagents worker network request",
601
- cwd: worker.cwd,
602
- sessionId: sessionId(ctx),
603
- scopeKey: `${sessionId(ctx)}:turn:${currentTurn}`,
604
- agentName: `pi-subagents-worker:${worker.id}`,
605
- humanApproval: humanApproval(ctx),
606
- }),
607
- {
608
- // Observability-only: never influences allow/deny, and a failure
609
- // here must not stop the supervisor from answering approval RPCs
610
- // or make the worker launch on the host.
611
- registered: (worker) => fleetView?.registered(worker),
612
- unregistered: (worker) => fleetView?.unregistered(worker.id),
613
- },
614
- network,
615
- );
616
- isolation = enableExternalWorkerIsolation(supervisor);
617
- externalIsolationFailure = undefined;
618
- } catch (error) {
619
- const message = `${EXTENSION_NAME}: external worker isolation unavailable; child launches will fail closed: ${error instanceof Error ? error.message : String(error)}`;
620
- externalIsolationFailure = message;
621
- isolation?.restore();
622
- isolation = undefined;
623
- await supervisor?.close();
624
- supervisor = undefined;
625
- fleetView?.close();
626
- fleetView = undefined;
627
- console.error(message);
628
- if (ctx.hasUI) ctx.ui.notify(message, "warning");
577
+ if (isNativeSubagentChild) {
578
+ pi.events?.emit("subagent:acknowledge-extension", {
579
+ id: PI_SANDBOX_ACKNOWLEDGEMENT,
580
+ });
581
+ }
582
+ capabilityCeiling?.dispose();
583
+ capabilityCeiling = undefined;
584
+ if (protectExternalOrchestration) {
585
+ validatedNativeAgents = nativeRuntime!.validateAllowedAgents(
586
+ ctx.cwd,
587
+ ctx.model?.provider,
588
+ );
589
+ capabilityCeiling = nativeRuntime!.registerCeiling(
590
+ piSubagentsSessionId(ctx),
591
+ validatedNativeAgents,
592
+ );
593
+ const ownership = externalSubagentDiagnostic(pi);
594
+ if (ownership.level === "warning") {
595
+ capabilityCeiling.dispose();
596
+ capabilityCeiling = undefined;
597
+ throw new Error(`${EXTENSION_NAME}: ${ownership.message}`);
629
598
  }
630
599
  }
631
600
  if (ctx.hasUI) {
632
- if (subagentProvider === "pi-subagents") {
601
+ if (protectExternalOrchestration) {
633
602
  const diagnostic = externalSubagentDiagnostic(pi);
634
- ctx.ui.notify(
635
- externalWorkerIsolation === "enforce" && isolation
636
- ? "pi-subagents orchestration active; external worker process trees require outer Sandbox Runtime isolation."
637
- : diagnostic.message,
638
- externalWorkerIsolation === "enforce" && isolation
639
- ? "info"
640
- : diagnostic.level,
641
- );
642
- } else if (subagentProvider === "builtin") {
603
+ ctx.ui.notify(diagnostic.message, diagnostic.level);
604
+ } else if (subagentProvider === "builtin" && !isNativeSubagentChild) {
643
605
  ctx.ui.notify(
644
606
  "pi-sandbox subagent provider: builtin; worker process trees use the outer Sandbox Runtime sandbox",
645
607
  "info",
646
608
  );
647
609
  } else {
648
610
  ctx.ui.notify(
649
- "pi-sandbox subagent provider: off; pi-sandbox protects Bash execution only",
611
+ isNativeSubagentChild
612
+ ? "pi-sandbox active in native subagent child; writes are restricted to sandboxed Bash"
613
+ : "pi-sandbox subagent provider: off; pi-sandbox protects Bash execution only",
650
614
  "info",
651
615
  );
652
616
  }
@@ -671,12 +635,8 @@ async function performRegistration(
671
635
  });
672
636
 
673
637
  pi.on("session_shutdown", async () => {
674
- isolation?.restore();
675
- isolation = undefined;
676
- fleetView?.close();
677
- fleetView = undefined;
678
- await supervisor?.close();
679
- supervisor = undefined;
638
+ capabilityCeiling?.dispose();
639
+ capabilityCeiling = undefined;
680
640
  await subagents?.shutdown();
681
641
  });
682
642
  }