@erichll/pi-sandbox 0.20.0 → 0.21.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 +32 -0
- package/README.md +21 -13
- package/package.json +4 -4
- package/src/config.ts +353 -182
- package/src/index.ts +2 -2
- package/src/policy.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0 - 2026-09-25
|
|
4
|
+
|
|
5
|
+
- Support project-level configuration at `.pi/extensions/pi-sandbox/config.json`.
|
|
6
|
+
When present, project configuration is merged with global configuration:
|
|
7
|
+
array permissions (`additionalAllowRead`, `allowedDomains`, `deniedDomains`,
|
|
8
|
+
`preflightCommandPrefixes`) form deduplicated unions, and scalar settings
|
|
9
|
+
(`subagents.provider`, `hostIPC.mode`, `hostIPC.retryOnUnixSocketError`)
|
|
10
|
+
allow project-level overrides.
|
|
11
|
+
- Harden project-level configuration: added `.pi/extensions/pi-sandbox/config.json`
|
|
12
|
+
to default sandbox `filesystem.denyWrite` policy. Project configuration is
|
|
13
|
+
strictly read-only for sandboxed execution processes.
|
|
14
|
+
- Remove legacy configuration compatibility: removed fallback loading for
|
|
15
|
+
`~/.pi/agent/pi-sandbox.json` and dropped deprecated `externalWorkerIsolation`
|
|
16
|
+
migration parsing.
|
|
17
|
+
- Pair with `@erichll/pi-auto-review` `^0.21.0`.
|
|
18
|
+
|
|
19
|
+
## 0.20.1 - 2026-09-24
|
|
20
|
+
|
|
21
|
+
- Move `@anthropic-ai/sandbox-runtime` from `devDependencies` to runtime
|
|
22
|
+
`dependencies`. The extension imports the package while loading, so published
|
|
23
|
+
installs failed with `Cannot find module '@anthropic-ai/sandbox-runtime'`.
|
|
24
|
+
- Pair this release with `@erichll/pi-auto-review` `^0.20.1`, whose development
|
|
25
|
+
baseline is upgraded to `@gotgenes/pi-permission-system` 33.1.1.
|
|
26
|
+
- Upgrade the development `@types/node` baseline to `^26.6.2`; the resolved
|
|
27
|
+
version was already current under the previous range. `typescript` remains
|
|
28
|
+
6.0.3 because the 7.0.2 native compiler is incompatible with the
|
|
29
|
+
permission-system integration test transform.
|
|
30
|
+
- Type checking and the full suite pass (89 pass, 1 environment-dependent
|
|
31
|
+
skip). The deterministic `gate:pi-subagents` preflight passes against
|
|
32
|
+
`pi-subagents` 0.71.0; the model-backed phase was not run because no matching
|
|
33
|
+
model credential was available in the current environment.
|
|
34
|
+
|
|
3
35
|
## 0.20.0 - 2026-09-23
|
|
4
36
|
|
|
5
37
|
- Require Pi `^0.87.1` (development and peer) for `@earendil-works/
|
package/README.md
CHANGED
|
@@ -71,12 +71,7 @@ macOS uses built-in Seatbelt support but still requires `ripgrep`.
|
|
|
71
71
|
|
|
72
72
|
## Subagent provider
|
|
73
73
|
|
|
74
|
-
The provider is selected
|
|
75
|
-
`~/.pi/agent/extensions/pi-sandbox/config.json`:
|
|
76
|
-
|
|
77
|
-
If that file is missing, `pi-sandbox` still loads the legacy path
|
|
78
|
-
`~/.pi/agent/pi-sandbox.json` when present. Prefer the extension-local path for
|
|
79
|
-
new installs.
|
|
74
|
+
The provider is selected from the configuration (`~/.pi/agent/extensions/pi-sandbox/config.json` globally or `.pi/extensions/pi-sandbox/config.json` project-locally):
|
|
80
75
|
|
|
81
76
|
```json
|
|
82
77
|
{
|
|
@@ -141,14 +136,13 @@ Before a release, run the deterministic and model-backed portions of
|
|
|
141
136
|
verified on upgrade.
|
|
142
137
|
|
|
143
138
|
The configuration parser rejects malformed JSON, unknown fields, and unknown
|
|
144
|
-
providers instead of silently weakening isolation.
|
|
145
|
-
`externalWorkerIsolation` configurations produce a migration error.
|
|
139
|
+
providers instead of silently weakening isolation.
|
|
146
140
|
|
|
147
141
|
## Network domain policy
|
|
148
142
|
|
|
149
|
-
|
|
150
|
-
`~/.pi/agent/extensions/pi-sandbox/config.json`
|
|
151
|
-
|
|
143
|
+
Domain authorization can be configured globally at
|
|
144
|
+
`~/.pi/agent/extensions/pi-sandbox/config.json` or project-locally at
|
|
145
|
+
`.pi/extensions/pi-sandbox/config.json` (merged with union semantics):
|
|
152
146
|
|
|
153
147
|
```json
|
|
154
148
|
{
|
|
@@ -255,9 +249,23 @@ Persistent built-in sessions support `start`, `follow_up`, `wait`, `status`,
|
|
|
255
249
|
`stop`, and nested `handoff` operations. At most four sessions are live at
|
|
256
250
|
once, and nesting depth is capped at three.
|
|
257
251
|
|
|
258
|
-
##
|
|
252
|
+
## Project-level configuration and read-only protection
|
|
253
|
+
|
|
254
|
+
In addition to user-level configuration at `~/.pi/agent/extensions/pi-sandbox/config.json`,
|
|
255
|
+
`pi-sandbox` supports project-level configuration placed at:
|
|
256
|
+
|
|
257
|
+
```
|
|
258
|
+
.pi/extensions/pi-sandbox/config.json
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
When present, project configuration is merged with global configuration:
|
|
262
|
+
- Array policies (`additionalAllowRead`, `allowedDomains`, `deniedDomains`, `preflightCommandPrefixes`) form a deduplicated union.
|
|
263
|
+
- Scalar settings (`provider`, `mode`, `retryOnUnixSocketError`) allow project-level overrides.
|
|
259
264
|
|
|
260
|
-
|
|
265
|
+
**Read-only Security Guarantee**:
|
|
266
|
+
Project-level configuration is explicitly protected in the sandbox policy (`denyWrite`),
|
|
267
|
+
meaning sandboxed tasks can read `.pi/extensions/pi-sandbox/config.json` but can never
|
|
268
|
+
tamper with or overwrite it from within the sandbox.
|
|
261
269
|
|
|
262
270
|
```bash
|
|
263
271
|
pi install npm:@erichll/pi-auto-review
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erichll/pi-sandbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"test": "node --experimental-strip-types --test test/*.test.ts"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@
|
|
53
|
+
"@anthropic-ai/sandbox-runtime": "^0.0.77",
|
|
54
|
+
"@erichll/pi-auto-review": "^0.21.0",
|
|
54
55
|
"jiti": "2.7.0"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
@@ -64,11 +65,10 @@
|
|
|
64
65
|
}
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
|
-
"@anthropic-ai/sandbox-runtime": "^0.0.77",
|
|
68
68
|
"@earendil-works/pi-ai": "^0.87.1",
|
|
69
69
|
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
70
70
|
"@earendil-works/pi-server": "^0.87.1",
|
|
71
|
-
"@types/node": "^26.2
|
|
71
|
+
"@types/node": "^26.6.2",
|
|
72
72
|
"pi-subagents": "^0.71.0",
|
|
73
73
|
"typebox": "^1.3.34",
|
|
74
74
|
"typescript": "^6.0.3"
|
package/src/config.ts
CHANGED
|
@@ -40,8 +40,10 @@ export type PiSandboxConfig = {
|
|
|
40
40
|
|
|
41
41
|
export type LoadPiSandboxConfigOptions = {
|
|
42
42
|
path?: string;
|
|
43
|
-
/** Override home directory when resolving default
|
|
43
|
+
/** Override home directory when resolving default trusted paths. */
|
|
44
44
|
home?: string;
|
|
45
|
+
/** Override workspace directory when resolving project-level config path. */
|
|
46
|
+
cwd?: string;
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
export const DEFAULT_PI_SANDBOX_CONFIG: Readonly<PiSandboxConfig> = Object.freeze(
|
|
@@ -75,9 +77,15 @@ export function getPiSandboxConfigPath(home = homedir()): string {
|
|
|
75
77
|
);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
export const PROJECT_PI_SANDBOX_CONFIG_PATH = join(
|
|
81
|
+
".pi",
|
|
82
|
+
"extensions",
|
|
83
|
+
"pi-sandbox",
|
|
84
|
+
"config.json",
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
export function getProjectPiSandboxConfigPath(cwd = process.cwd()): string {
|
|
88
|
+
return join(cwd, PROJECT_PI_SANDBOX_CONFIG_PATH);
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -122,7 +130,20 @@ function hasValidDomainLabels(
|
|
|
122
130
|
);
|
|
123
131
|
}
|
|
124
132
|
|
|
125
|
-
export
|
|
133
|
+
export type PartialPiSandboxConfig = {
|
|
134
|
+
subagents?: {
|
|
135
|
+
provider?: SubagentProvider;
|
|
136
|
+
protection?: "native-background-tools";
|
|
137
|
+
allowedNativeAgents?: readonly string[];
|
|
138
|
+
};
|
|
139
|
+
filesystem?: {
|
|
140
|
+
additionalAllowRead?: readonly string[];
|
|
141
|
+
};
|
|
142
|
+
network?: Partial<NetworkConfig>;
|
|
143
|
+
hostIPC?: Partial<HostIPCConfig>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export function parsePiSandboxConfigRaw(value: unknown): PartialPiSandboxConfig {
|
|
126
147
|
if (!isRecord(value)) {
|
|
127
148
|
throw new Error("invalid pi-sandbox configuration: root must be an object");
|
|
128
149
|
}
|
|
@@ -132,212 +153,271 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
|
|
|
132
153
|
"root",
|
|
133
154
|
);
|
|
134
155
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
rejectUnknownKeys(
|
|
147
|
-
subagents,
|
|
148
|
-
["provider", "protection", "allowedNativeAgents"],
|
|
149
|
-
"subagents",
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const provider =
|
|
153
|
-
subagents.provider ?? DEFAULT_PI_SANDBOX_CONFIG.subagents.provider;
|
|
154
|
-
if (
|
|
155
|
-
typeof provider !== "string" ||
|
|
156
|
-
!SUBAGENT_PROVIDERS.includes(provider as SubagentProvider)
|
|
157
|
-
) {
|
|
158
|
-
throw new Error(
|
|
159
|
-
`invalid pi-sandbox configuration: subagents.provider must be one of ${SUBAGENT_PROVIDERS.join(", ")}`,
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
const protection = subagents.protection;
|
|
163
|
-
const allowedNativeAgents = subagents.allowedNativeAgents;
|
|
164
|
-
if (provider === "pi-subagents" && protection !== "native-background-tools") {
|
|
165
|
-
throw new Error(
|
|
166
|
-
"invalid pi-sandbox configuration: provider 'pi-subagents' requires subagents.protection 'native-background-tools'; migrate legacy configurations explicitly",
|
|
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'",
|
|
156
|
+
let parsedSubagents: PartialPiSandboxConfig["subagents"] | undefined;
|
|
157
|
+
if (value.subagents !== undefined) {
|
|
158
|
+
if (!isRecord(value.subagents)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"invalid pi-sandbox configuration: subagents must be an object",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
rejectUnknownKeys(
|
|
164
|
+
value.subagents,
|
|
165
|
+
["provider", "protection", "allowedNativeAgents"],
|
|
166
|
+
"subagents",
|
|
172
167
|
);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
const provider = value.subagents.provider;
|
|
169
|
+
if (
|
|
170
|
+
provider !== undefined &&
|
|
171
|
+
(typeof provider !== "string" ||
|
|
172
|
+
!SUBAGENT_PROVIDERS.includes(provider as SubagentProvider))
|
|
173
|
+
) {
|
|
176
174
|
throw new Error(
|
|
177
|
-
|
|
175
|
+
`invalid pi-sandbox configuration: subagents.provider must be one of ${SUBAGENT_PROVIDERS.join(", ")}`,
|
|
178
176
|
);
|
|
179
177
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
!/^[A-Za-z0-9_.:-]+$/u.test(name)
|
|
184
|
-
)) {
|
|
178
|
+
const protection = value.subagents.protection;
|
|
179
|
+
const allowedNativeAgents = value.subagents.allowedNativeAgents;
|
|
180
|
+
if (provider === "pi-subagents" && protection !== "native-background-tools") {
|
|
185
181
|
throw new Error(
|
|
186
|
-
"invalid pi-sandbox configuration: subagents.
|
|
182
|
+
"invalid pi-sandbox configuration: provider 'pi-subagents' requires subagents.protection 'native-background-tools'; migrate legacy configurations explicitly",
|
|
187
183
|
);
|
|
188
184
|
}
|
|
189
|
-
if (
|
|
185
|
+
if (provider !== undefined && provider !== "pi-subagents" && (protection !== undefined || allowedNativeAgents !== undefined)) {
|
|
190
186
|
throw new Error(
|
|
191
|
-
"invalid pi-sandbox configuration: subagents.allowedNativeAgents
|
|
187
|
+
"invalid pi-sandbox configuration: subagents.protection and allowedNativeAgents are only valid with provider 'pi-subagents'",
|
|
192
188
|
);
|
|
193
189
|
}
|
|
190
|
+
if (provider === "pi-subagents") {
|
|
191
|
+
if (!Array.isArray(allowedNativeAgents) || allowedNativeAgents.length === 0) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
"invalid pi-sandbox configuration: subagents.allowedNativeAgents must be a non-empty array of canonical agent names",
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
if (allowedNativeAgents.some((name) =>
|
|
197
|
+
typeof name !== "string" ||
|
|
198
|
+
name !== name.trim() ||
|
|
199
|
+
!/^[A-Za-z0-9_.:-]+$/u.test(name)
|
|
200
|
+
)) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"invalid pi-sandbox configuration: subagents.allowedNativeAgents must contain only canonical agent names",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (new Set(allowedNativeAgents).size !== allowedNativeAgents.length) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"invalid pi-sandbox configuration: subagents.allowedNativeAgents must not contain duplicates",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
parsedSubagents = {
|
|
212
|
+
...(provider !== undefined ? { provider: provider as SubagentProvider } : {}),
|
|
213
|
+
...(provider === "pi-subagents"
|
|
214
|
+
? {
|
|
215
|
+
protection: "native-background-tools" as const,
|
|
216
|
+
allowedNativeAgents: [...(allowedNativeAgents as string[])],
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
219
|
+
};
|
|
194
220
|
}
|
|
195
221
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const additionalAllowRead =
|
|
208
|
-
filesystem.additionalAllowRead ??
|
|
209
|
-
DEFAULT_PI_SANDBOX_CONFIG.filesystem.additionalAllowRead;
|
|
210
|
-
if (
|
|
211
|
-
!Array.isArray(additionalAllowRead) ||
|
|
212
|
-
additionalAllowRead.some(
|
|
213
|
-
(path) =>
|
|
214
|
-
typeof path !== "string" ||
|
|
215
|
-
path.trim() === "" ||
|
|
216
|
-
!isAbsolute(path),
|
|
217
|
-
)
|
|
218
|
-
) {
|
|
219
|
-
throw new Error(
|
|
220
|
-
"invalid pi-sandbox configuration: filesystem.additionalAllowRead must be an array of absolute paths",
|
|
222
|
+
let parsedFilesystem: PartialPiSandboxConfig["filesystem"] | undefined;
|
|
223
|
+
if (value.filesystem !== undefined) {
|
|
224
|
+
if (!isRecord(value.filesystem)) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
"invalid pi-sandbox configuration: filesystem must be an object",
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
rejectUnknownKeys(
|
|
230
|
+
value.filesystem,
|
|
231
|
+
["additionalAllowRead"],
|
|
232
|
+
"filesystem",
|
|
221
233
|
);
|
|
234
|
+
const additionalAllowRead = value.filesystem.additionalAllowRead;
|
|
235
|
+
if (additionalAllowRead !== undefined) {
|
|
236
|
+
if (
|
|
237
|
+
!Array.isArray(additionalAllowRead) ||
|
|
238
|
+
additionalAllowRead.some(
|
|
239
|
+
(path) =>
|
|
240
|
+
typeof path !== "string" ||
|
|
241
|
+
path.trim() === "" ||
|
|
242
|
+
!isAbsolute(path),
|
|
243
|
+
)
|
|
244
|
+
) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
"invalid pi-sandbox configuration: filesystem.additionalAllowRead must be an array of absolute paths",
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
parsedFilesystem = {
|
|
250
|
+
additionalAllowRead: [...new Set(additionalAllowRead)],
|
|
251
|
+
};
|
|
252
|
+
} else {
|
|
253
|
+
parsedFilesystem = {};
|
|
254
|
+
}
|
|
222
255
|
}
|
|
223
256
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
257
|
+
let parsedNetwork: PartialPiSandboxConfig["network"] | undefined;
|
|
258
|
+
if (value.network !== undefined) {
|
|
259
|
+
if (!isRecord(value.network)) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
"invalid pi-sandbox configuration: network must be an object",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
rejectUnknownKeys(
|
|
265
|
+
value.network,
|
|
266
|
+
["allowedDomains", "deniedDomains"],
|
|
267
|
+
"network",
|
|
227
268
|
);
|
|
269
|
+
const normalizeDomainList = (
|
|
270
|
+
key: "allowedDomains" | "deniedDomains",
|
|
271
|
+
): string[] | undefined => {
|
|
272
|
+
const configured = (value.network as Record<string, unknown>)[key];
|
|
273
|
+
if (configured === undefined) return undefined;
|
|
274
|
+
if (
|
|
275
|
+
!Array.isArray(configured) ||
|
|
276
|
+
configured.some(
|
|
277
|
+
(pattern) => typeof pattern !== "string" || pattern.trim() === "",
|
|
278
|
+
)
|
|
279
|
+
) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`invalid pi-sandbox configuration: network.${key} must be an array of non-empty strings`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
const normalized = [
|
|
285
|
+
...new Set(configured.map((pattern) => pattern.trim())),
|
|
286
|
+
];
|
|
287
|
+
const invalidIndex = normalized.findIndex(
|
|
288
|
+
(pattern) => !hasValidDomainLabels(pattern, key === "deniedDomains"),
|
|
289
|
+
);
|
|
290
|
+
if (invalidIndex >= 0) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
`invalid pi-sandbox configuration: network.${key}[${invalidIndex}] contains an invalid domain pattern`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const candidate = {
|
|
296
|
+
allowedDomains: key === "allowedDomains" ? normalized : [],
|
|
297
|
+
deniedDomains: key === "deniedDomains" ? normalized : [],
|
|
298
|
+
};
|
|
299
|
+
const result = NetworkConfigSchema.safeParse(candidate);
|
|
300
|
+
if (!result.success) {
|
|
301
|
+
const issue = result.error.issues[0];
|
|
302
|
+
throw new Error(
|
|
303
|
+
`invalid pi-sandbox configuration: network.${key}${typeof issue?.path[1] === "number" ? `[${issue.path[1]}]` : ""} ${issue?.message ?? "contains an invalid domain pattern"}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return normalized;
|
|
307
|
+
};
|
|
308
|
+
const allowed = normalizeDomainList("allowedDomains");
|
|
309
|
+
const denied = normalizeDomainList("deniedDomains");
|
|
310
|
+
parsedNetwork = {
|
|
311
|
+
...(allowed !== undefined ? { allowedDomains: allowed } : {}),
|
|
312
|
+
...(denied !== undefined ? { deniedDomains: denied } : {}),
|
|
313
|
+
};
|
|
228
314
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"network",
|
|
234
|
-
);
|
|
235
|
-
const normalizeDomainList = (
|
|
236
|
-
key: "allowedDomains" | "deniedDomains",
|
|
237
|
-
): string[] => {
|
|
238
|
-
const configured = network[key] ?? DEFAULT_PI_SANDBOX_CONFIG.network[key];
|
|
239
|
-
if (
|
|
240
|
-
!Array.isArray(configured) ||
|
|
241
|
-
configured.some(
|
|
242
|
-
(pattern) => typeof pattern !== "string" || pattern.trim() === "",
|
|
243
|
-
)
|
|
244
|
-
) {
|
|
315
|
+
|
|
316
|
+
let parsedHostIPC: PartialPiSandboxConfig["hostIPC"] | undefined;
|
|
317
|
+
if (value.hostIPC !== undefined) {
|
|
318
|
+
if (!isRecord(value.hostIPC)) {
|
|
245
319
|
throw new Error(
|
|
246
|
-
|
|
320
|
+
"invalid pi-sandbox configuration: hostIPC must be an object",
|
|
247
321
|
);
|
|
248
322
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
(pattern) => !hasValidDomainLabels(pattern, key === "deniedDomains"),
|
|
323
|
+
rejectUnknownKeys(
|
|
324
|
+
value.hostIPC,
|
|
325
|
+
["mode", "preflightCommandPrefixes", "retryOnUnixSocketError"],
|
|
326
|
+
"hostIPC",
|
|
254
327
|
);
|
|
255
|
-
|
|
328
|
+
const mode = value.hostIPC.mode;
|
|
329
|
+
if (
|
|
330
|
+
mode !== undefined &&
|
|
331
|
+
(typeof mode !== "string" ||
|
|
332
|
+
!HOST_IPC_MODES.includes(mode as HostIPCMode))
|
|
333
|
+
) {
|
|
256
334
|
throw new Error(
|
|
257
|
-
`invalid pi-sandbox configuration:
|
|
335
|
+
`invalid pi-sandbox configuration: hostIPC.mode must be one of ${HOST_IPC_MODES.join(", ")}`,
|
|
258
336
|
);
|
|
259
337
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
338
|
+
const preflight = value.hostIPC.preflightCommandPrefixes;
|
|
339
|
+
if (preflight !== undefined) {
|
|
340
|
+
if (
|
|
341
|
+
!Array.isArray(preflight) ||
|
|
342
|
+
preflight.some(
|
|
343
|
+
(prefix) => typeof prefix !== "string" || prefix.trim() === "",
|
|
344
|
+
)
|
|
345
|
+
) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
"invalid pi-sandbox configuration: hostIPC.preflightCommandPrefixes must be an array of non-empty strings",
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const retry = value.hostIPC.retryOnUnixSocketError;
|
|
352
|
+
if (retry !== undefined && typeof retry !== "boolean") {
|
|
267
353
|
throw new Error(
|
|
268
|
-
|
|
354
|
+
"invalid pi-sandbox configuration: hostIPC.retryOnUnixSocketError must be a boolean",
|
|
269
355
|
);
|
|
270
356
|
}
|
|
271
|
-
|
|
357
|
+
parsedHostIPC = {
|
|
358
|
+
...(mode !== undefined ? { mode: mode as HostIPCMode } : {}),
|
|
359
|
+
...(preflight !== undefined
|
|
360
|
+
? {
|
|
361
|
+
preflightCommandPrefixes: [
|
|
362
|
+
...new Set(preflight.map((prefix) => prefix.trim())),
|
|
363
|
+
],
|
|
364
|
+
}
|
|
365
|
+
: {}),
|
|
366
|
+
...(retry !== undefined ? { retryOnUnixSocketError: retry } : {}),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
...(parsedSubagents !== undefined ? { subagents: parsedSubagents } : {}),
|
|
372
|
+
...(parsedFilesystem !== undefined ? { filesystem: parsedFilesystem } : {}),
|
|
373
|
+
...(parsedNetwork !== undefined ? { network: parsedNetwork } : {}),
|
|
374
|
+
...(parsedHostIPC !== undefined ? { hostIPC: parsedHostIPC } : {}),
|
|
272
375
|
};
|
|
273
|
-
|
|
274
|
-
const deniedDomains = normalizeDomainList("deniedDomains");
|
|
376
|
+
}
|
|
275
377
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const hostIPC = value.hostIPC ?? {};
|
|
282
|
-
rejectUnknownKeys(
|
|
283
|
-
hostIPC,
|
|
284
|
-
["mode", "preflightCommandPrefixes", "retryOnUnixSocketError"],
|
|
285
|
-
"hostIPC",
|
|
286
|
-
);
|
|
287
|
-
const hostIPCMode = hostIPC.mode ?? DEFAULT_PI_SANDBOX_CONFIG.hostIPC.mode;
|
|
288
|
-
if (
|
|
289
|
-
typeof hostIPCMode !== "string" ||
|
|
290
|
-
!HOST_IPC_MODES.includes(hostIPCMode as HostIPCMode)
|
|
291
|
-
) {
|
|
292
|
-
throw new Error(
|
|
293
|
-
`invalid pi-sandbox configuration: hostIPC.mode must be one of ${HOST_IPC_MODES.join(", ")}`,
|
|
294
|
-
);
|
|
295
|
-
}
|
|
296
|
-
const preflightCommandPrefixes =
|
|
297
|
-
hostIPC.preflightCommandPrefixes ??
|
|
298
|
-
DEFAULT_PI_SANDBOX_CONFIG.hostIPC.preflightCommandPrefixes;
|
|
299
|
-
if (
|
|
300
|
-
!Array.isArray(preflightCommandPrefixes) ||
|
|
301
|
-
preflightCommandPrefixes.some(
|
|
302
|
-
(prefix) => typeof prefix !== "string" || prefix.trim() === "",
|
|
303
|
-
)
|
|
304
|
-
) {
|
|
305
|
-
throw new Error(
|
|
306
|
-
"invalid pi-sandbox configuration: hostIPC.preflightCommandPrefixes must be an array of non-empty strings",
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
const retryOnUnixSocketError =
|
|
310
|
-
hostIPC.retryOnUnixSocketError ??
|
|
311
|
-
DEFAULT_PI_SANDBOX_CONFIG.hostIPC.retryOnUnixSocketError;
|
|
312
|
-
if (typeof retryOnUnixSocketError !== "boolean") {
|
|
313
|
-
throw new Error(
|
|
314
|
-
"invalid pi-sandbox configuration: hostIPC.retryOnUnixSocketError must be a boolean",
|
|
315
|
-
);
|
|
316
|
-
}
|
|
378
|
+
export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
|
|
379
|
+
const raw = parsePiSandboxConfigRaw(value);
|
|
380
|
+
const provider =
|
|
381
|
+
raw.subagents?.provider ?? DEFAULT_PI_SANDBOX_CONFIG.subagents.provider;
|
|
382
|
+
const isPiSubagents = provider === "pi-subagents";
|
|
317
383
|
|
|
318
384
|
return {
|
|
319
385
|
subagents: {
|
|
320
|
-
provider
|
|
321
|
-
...(
|
|
386
|
+
provider,
|
|
387
|
+
...(isPiSubagents
|
|
322
388
|
? {
|
|
323
389
|
protection: "native-background-tools" as const,
|
|
324
|
-
allowedNativeAgents: [
|
|
390
|
+
allowedNativeAgents: [
|
|
391
|
+
...(raw.subagents?.allowedNativeAgents ?? []),
|
|
392
|
+
],
|
|
325
393
|
}
|
|
326
394
|
: {}),
|
|
327
395
|
},
|
|
328
396
|
filesystem: {
|
|
329
|
-
additionalAllowRead: [
|
|
397
|
+
additionalAllowRead: [
|
|
398
|
+
...(raw.filesystem?.additionalAllowRead ??
|
|
399
|
+
DEFAULT_PI_SANDBOX_CONFIG.filesystem.additionalAllowRead),
|
|
400
|
+
],
|
|
330
401
|
},
|
|
331
402
|
network: {
|
|
332
|
-
allowedDomains: [
|
|
333
|
-
|
|
403
|
+
allowedDomains: [
|
|
404
|
+
...(raw.network?.allowedDomains ??
|
|
405
|
+
DEFAULT_PI_SANDBOX_CONFIG.network.allowedDomains),
|
|
406
|
+
],
|
|
407
|
+
deniedDomains: [
|
|
408
|
+
...(raw.network?.deniedDomains ??
|
|
409
|
+
DEFAULT_PI_SANDBOX_CONFIG.network.deniedDomains),
|
|
410
|
+
],
|
|
334
411
|
},
|
|
335
412
|
hostIPC: {
|
|
336
|
-
mode:
|
|
413
|
+
mode: raw.hostIPC?.mode ?? DEFAULT_PI_SANDBOX_CONFIG.hostIPC.mode,
|
|
337
414
|
preflightCommandPrefixes: [
|
|
338
|
-
...
|
|
415
|
+
...(raw.hostIPC?.preflightCommandPrefixes ??
|
|
416
|
+
DEFAULT_PI_SANDBOX_CONFIG.hostIPC.preflightCommandPrefixes),
|
|
339
417
|
],
|
|
340
|
-
retryOnUnixSocketError
|
|
418
|
+
retryOnUnixSocketError:
|
|
419
|
+
raw.hostIPC?.retryOnUnixSocketError ??
|
|
420
|
+
DEFAULT_PI_SANDBOX_CONFIG.hostIPC.retryOnUnixSocketError,
|
|
341
421
|
},
|
|
342
422
|
};
|
|
343
423
|
}
|
|
@@ -401,32 +481,123 @@ function parsePiSandboxConfigFile(path: string, source: string): PiSandboxConfig
|
|
|
401
481
|
return parsePiSandboxConfig(value);
|
|
402
482
|
}
|
|
403
483
|
|
|
484
|
+
function parsePiSandboxConfigFileRaw(path: string, source: string): PartialPiSandboxConfig {
|
|
485
|
+
let value: unknown;
|
|
486
|
+
try {
|
|
487
|
+
value = JSON.parse(source);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
throw new Error(`invalid JSON in pi-sandbox configuration at ${path}`, {
|
|
490
|
+
cause: error,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return parsePiSandboxConfigRaw(value);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function mergePiSandboxConfigs(
|
|
497
|
+
trusted: PiSandboxConfig,
|
|
498
|
+
project: PartialPiSandboxConfig,
|
|
499
|
+
): PiSandboxConfig {
|
|
500
|
+
const provider = project.subagents?.provider ?? trusted.subagents.provider;
|
|
501
|
+
const isPiSubagents = provider === "pi-subagents";
|
|
502
|
+
|
|
503
|
+
let allowedNativeAgents: string[] | undefined;
|
|
504
|
+
if (isPiSubagents) {
|
|
505
|
+
const trustedAgents = trusted.subagents.allowedNativeAgents ?? [];
|
|
506
|
+
const projectAgents = project.subagents?.allowedNativeAgents ?? [];
|
|
507
|
+
allowedNativeAgents = [...new Set([...trustedAgents, ...projectAgents])];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return {
|
|
511
|
+
subagents: {
|
|
512
|
+
provider,
|
|
513
|
+
...(isPiSubagents
|
|
514
|
+
? {
|
|
515
|
+
protection: "native-background-tools" as const,
|
|
516
|
+
allowedNativeAgents,
|
|
517
|
+
}
|
|
518
|
+
: {}),
|
|
519
|
+
},
|
|
520
|
+
filesystem: {
|
|
521
|
+
additionalAllowRead: [
|
|
522
|
+
...new Set([
|
|
523
|
+
...trusted.filesystem.additionalAllowRead,
|
|
524
|
+
...(project.filesystem?.additionalAllowRead ?? []),
|
|
525
|
+
]),
|
|
526
|
+
],
|
|
527
|
+
},
|
|
528
|
+
network: {
|
|
529
|
+
allowedDomains: [
|
|
530
|
+
...new Set([
|
|
531
|
+
...trusted.network.allowedDomains,
|
|
532
|
+
...(project.network?.allowedDomains ?? []),
|
|
533
|
+
]),
|
|
534
|
+
],
|
|
535
|
+
deniedDomains: [
|
|
536
|
+
...new Set([
|
|
537
|
+
...trusted.network.deniedDomains,
|
|
538
|
+
...(project.network?.deniedDomains ?? []),
|
|
539
|
+
]),
|
|
540
|
+
],
|
|
541
|
+
},
|
|
542
|
+
hostIPC: {
|
|
543
|
+
mode: project.hostIPC?.mode ?? trusted.hostIPC.mode,
|
|
544
|
+
preflightCommandPrefixes: [
|
|
545
|
+
...new Set([
|
|
546
|
+
...trusted.hostIPC.preflightCommandPrefixes,
|
|
547
|
+
...(project.hostIPC?.preflightCommandPrefixes ?? []),
|
|
548
|
+
]),
|
|
549
|
+
],
|
|
550
|
+
retryOnUnixSocketError:
|
|
551
|
+
project.hostIPC?.retryOnUnixSocketError ??
|
|
552
|
+
trusted.hostIPC.retryOnUnixSocketError,
|
|
553
|
+
},
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
404
557
|
export function loadPiSandboxConfig(
|
|
405
558
|
options: LoadPiSandboxConfigOptions = {},
|
|
406
559
|
): PiSandboxConfig {
|
|
407
|
-
|
|
560
|
+
if (options.path !== undefined) {
|
|
561
|
+
const path = options.path;
|
|
562
|
+
try {
|
|
563
|
+
return parsePiSandboxConfigFile(path, readPiSandboxConfigFile(path));
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (isNotFoundError(error)) {
|
|
566
|
+
return defaultPiSandboxConfig();
|
|
567
|
+
}
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const globalPath = getPiSandboxConfigPath(options.home);
|
|
573
|
+
let globalConfig: PiSandboxConfig | undefined;
|
|
408
574
|
try {
|
|
409
|
-
|
|
575
|
+
globalConfig = parsePiSandboxConfigFile(
|
|
576
|
+
globalPath,
|
|
577
|
+
readPiSandboxConfigFile(globalPath),
|
|
578
|
+
);
|
|
410
579
|
} catch (error) {
|
|
411
580
|
if (!isNotFoundError(error)) {
|
|
412
581
|
throw error;
|
|
413
582
|
}
|
|
414
583
|
}
|
|
415
584
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
throw error;
|
|
427
|
-
}
|
|
585
|
+
const projectPath = getProjectPiSandboxConfigPath(options.cwd);
|
|
586
|
+
let projectRaw: PartialPiSandboxConfig | undefined;
|
|
587
|
+
try {
|
|
588
|
+
projectRaw = parsePiSandboxConfigFileRaw(
|
|
589
|
+
projectPath,
|
|
590
|
+
readPiSandboxConfigFile(projectPath),
|
|
591
|
+
);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
if (!isNotFoundError(error)) {
|
|
594
|
+
throw error;
|
|
428
595
|
}
|
|
429
596
|
}
|
|
430
597
|
|
|
431
|
-
|
|
598
|
+
const base = globalConfig ?? defaultPiSandboxConfig();
|
|
599
|
+
if (projectRaw) {
|
|
600
|
+
return mergePiSandboxConfigs(base, projectRaw);
|
|
601
|
+
}
|
|
602
|
+
return base;
|
|
432
603
|
}
|
package/src/index.ts
CHANGED
|
@@ -218,7 +218,8 @@ async function performRegistration(
|
|
|
218
218
|
options: PiSandboxExtensionOptions,
|
|
219
219
|
): Promise<void> {
|
|
220
220
|
let currentTurn = 0;
|
|
221
|
-
const
|
|
221
|
+
const cwd = process.cwd();
|
|
222
|
+
const config = loadPiSandboxConfig({ cwd });
|
|
222
223
|
const subagentProvider =
|
|
223
224
|
options.subagentProvider ?? config.subagents.provider;
|
|
224
225
|
const isNativeSubagentChild = process.env.PI_SUBAGENT_CHILD === "1";
|
|
@@ -246,7 +247,6 @@ async function performRegistration(
|
|
|
246
247
|
maxDepth: 3,
|
|
247
248
|
}))
|
|
248
249
|
: undefined;
|
|
249
|
-
const cwd = process.cwd();
|
|
250
250
|
let capabilityCeiling: CapabilityCeilingHandle | undefined;
|
|
251
251
|
let validatedNativeAgents: string[] = [];
|
|
252
252
|
const localBash = createBashToolDefinition(cwd);
|
package/src/policy.ts
CHANGED
|
@@ -10,7 +10,10 @@ 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
|
|
13
|
+
import {
|
|
14
|
+
PROJECT_PI_SANDBOX_CONFIG_PATH,
|
|
15
|
+
type NetworkConfig,
|
|
16
|
+
} from "./config.ts";
|
|
14
17
|
|
|
15
18
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
16
19
|
const NODE_INSTALL_ROOT = dirname(dirname(process.execPath));
|
|
@@ -248,6 +251,7 @@ export function createDefaultPolicy(
|
|
|
248
251
|
allowWrite: [workspace, "/dev/null"],
|
|
249
252
|
denyWrite: [
|
|
250
253
|
join(workspace, ".pi", "pi-auto-review.json"),
|
|
254
|
+
join(workspace, PROJECT_PI_SANDBOX_CONFIG_PATH),
|
|
251
255
|
join(home, ".pi", "agent", "logs"),
|
|
252
256
|
// Prevent the sandbox from installing or rewriting trusted extensions
|
|
253
257
|
// (includes ~/.pi/agent/extensions/pi-sandbox/config.json).
|