@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 +19 -0
- package/README.md +51 -79
- package/package.json +8 -6
- package/src/config.ts +44 -14
- package/src/index.ts +96 -136
- package/src/pi-subagents-native.ts +250 -0
- package/src/external-network-policy.d.mts +0 -8
- package/src/external-network-policy.mjs +0 -27
- package/src/external-runs-view.ts +0 -173
- package/src/external-supervisor.ts +0 -189
- package/src/external-worker-launcher.mjs +0 -266
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.
|
|
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
|
|
95
|
-
|
|
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.
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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 |
|
|
129
|
+
| Outer worker sandbox | Yes | No |
|
|
113
130
|
| Bash sandbox | Yes | Yes |
|
|
114
131
|
| Persistent follow-up | Yes | Yes |
|
|
115
|
-
| `workflowScript
|
|
116
|
-
|
|
|
117
|
-
|
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
184
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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`
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
58
|
-
"pi-subagents": "
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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", "
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
|
265
|
-
const
|
|
266
|
-
subagentProvider
|
|
267
|
-
|
|
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
|
|
283
|
-
let
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
!
|
|
561
|
+
(event.input.action === "status" || event.input.action === "debug.run") &&
|
|
562
|
+
!event.isError &&
|
|
563
|
+
!terminalChildrenHaveSandboxAcknowledgement(event.details)
|
|
572
564
|
) {
|
|
573
565
|
return {
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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 (
|
|
601
|
+
if (protectExternalOrchestration) {
|
|
633
602
|
const diagnostic = externalSubagentDiagnostic(pi);
|
|
634
|
-
ctx.ui.notify(
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
675
|
-
|
|
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
|
}
|