@erichll/pi-sandbox 0.20.1 → 0.21.1
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 +33 -0
- package/README.md +25 -14
- package/package.json +2 -2
- package/src/config.ts +353 -182
- package/src/index.ts +2 -2
- package/src/pi-subagents-native.ts +90 -1
- package/src/policy.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.1 - 2026-09-26
|
|
4
|
+
|
|
5
|
+
- Alias `@earendil-works/pi-tui` for the `jiti` instance that loads the
|
|
6
|
+
`pi-subagents` internals. `pi-subagents` imports that host peer from its
|
|
7
|
+
internal modules but does not ship it, and Pi's own extension loader aliases
|
|
8
|
+
it to the copy inside the running Pi package. The protected-mode loader
|
|
9
|
+
created its own `jiti` instance without that alias, so installs where the peer
|
|
10
|
+
is not hoisted next to the extension (for example the global
|
|
11
|
+
`~/.pi/agent/npm` tree) failed to load as soon as `subagents.provider` was
|
|
12
|
+
`pi-subagents` with `pi-subagents compatibility failure: Cannot find module
|
|
13
|
+
'@earendil-works/pi-tui'`. The alias is computed host-first from the running Pi
|
|
14
|
+
package (`process.argv[1]`, `PI_PACKAGE_DIR`) and falls back to plain
|
|
15
|
+
resolution from the extension tree; an unresolvable host peer still fails
|
|
16
|
+
closed with that explicit error.
|
|
17
|
+
- Add regression coverage for host alias resolution, including a `jiti` load in
|
|
18
|
+
a tree where `@earendil-works/pi-tui` is unreachable without the alias.
|
|
19
|
+
|
|
20
|
+
## 0.21.0 - 2026-09-25
|
|
21
|
+
|
|
22
|
+
- Support project-level configuration at `.pi/extensions/pi-sandbox/config.json`.
|
|
23
|
+
When present, project configuration is merged with global configuration:
|
|
24
|
+
array permissions (`additionalAllowRead`, `allowedDomains`, `deniedDomains`,
|
|
25
|
+
`preflightCommandPrefixes`) form deduplicated unions, and scalar settings
|
|
26
|
+
(`subagents.provider`, `hostIPC.mode`, `hostIPC.retryOnUnixSocketError`)
|
|
27
|
+
allow project-level overrides.
|
|
28
|
+
- Harden project-level configuration: added `.pi/extensions/pi-sandbox/config.json`
|
|
29
|
+
to default sandbox `filesystem.denyWrite` policy. Project configuration is
|
|
30
|
+
strictly read-only for sandboxed execution processes.
|
|
31
|
+
- Remove legacy configuration compatibility: removed fallback loading for
|
|
32
|
+
`~/.pi/agent/pi-sandbox.json` and dropped deprecated `externalWorkerIsolation`
|
|
33
|
+
migration parsing.
|
|
34
|
+
- Pair with `@erichll/pi-auto-review` `^0.21.0`.
|
|
35
|
+
|
|
3
36
|
## 0.20.1 - 2026-09-24
|
|
4
37
|
|
|
5
38
|
- Move `@anthropic-ai/sandbox-runtime` from `devDependencies` to runtime
|
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
|
{
|
|
@@ -99,7 +94,10 @@ Supported modes:
|
|
|
99
94
|
This provider supports the `pi-subagents >=0.66.0` line (validated through
|
|
100
95
|
0.71.0) and fails closed if its public ceiling API, module layout, or internal
|
|
101
96
|
discovery layout drifts. The peer dependency is a floor with no upper pin; the
|
|
102
|
-
loader's explicit export and layout checks are the real gate.
|
|
97
|
+
loader's explicit export and layout checks are the real gate. The loader also
|
|
98
|
+
aliases `@earendil-works/pi-tui`, a host peer that the `pi-subagents` internals
|
|
99
|
+
import but do not ship, to the copy inside the running Pi package, so installs
|
|
100
|
+
where it is not hoisted next to the extension still load.
|
|
103
101
|
Configure both the protection mode and a non-empty canonical whitelist:
|
|
104
102
|
|
|
105
103
|
```json
|
|
@@ -141,14 +139,13 @@ Before a release, run the deterministic and model-backed portions of
|
|
|
141
139
|
verified on upgrade.
|
|
142
140
|
|
|
143
141
|
The configuration parser rejects malformed JSON, unknown fields, and unknown
|
|
144
|
-
providers instead of silently weakening isolation.
|
|
145
|
-
`externalWorkerIsolation` configurations produce a migration error.
|
|
142
|
+
providers instead of silently weakening isolation.
|
|
146
143
|
|
|
147
144
|
## Network domain policy
|
|
148
145
|
|
|
149
|
-
|
|
150
|
-
`~/.pi/agent/extensions/pi-sandbox/config.json`
|
|
151
|
-
|
|
146
|
+
Domain authorization can be configured globally at
|
|
147
|
+
`~/.pi/agent/extensions/pi-sandbox/config.json` or project-locally at
|
|
148
|
+
`.pi/extensions/pi-sandbox/config.json` (merged with union semantics):
|
|
152
149
|
|
|
153
150
|
```json
|
|
154
151
|
{
|
|
@@ -255,9 +252,23 @@ Persistent built-in sessions support `start`, `follow_up`, `wait`, `status`,
|
|
|
255
252
|
`stop`, and nested `handoff` operations. At most four sessions are live at
|
|
256
253
|
once, and nesting depth is capped at three.
|
|
257
254
|
|
|
258
|
-
##
|
|
255
|
+
## Project-level configuration and read-only protection
|
|
256
|
+
|
|
257
|
+
In addition to user-level configuration at `~/.pi/agent/extensions/pi-sandbox/config.json`,
|
|
258
|
+
`pi-sandbox` supports project-level configuration placed at:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
.pi/extensions/pi-sandbox/config.json
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
When present, project configuration is merged with global configuration:
|
|
265
|
+
- Array policies (`additionalAllowRead`, `allowedDomains`, `deniedDomains`, `preflightCommandPrefixes`) form a deduplicated union.
|
|
266
|
+
- Scalar settings (`provider`, `mode`, `retryOnUnixSocketError`) allow project-level overrides.
|
|
259
267
|
|
|
260
|
-
|
|
268
|
+
**Read-only Security Guarantee**:
|
|
269
|
+
Project-level configuration is explicitly protected in the sandbox policy (`denyWrite`),
|
|
270
|
+
meaning sandboxed tasks can read `.pi/extensions/pi-sandbox/config.json` but can never
|
|
271
|
+
tamper with or overwrite it from within the sandbox.
|
|
261
272
|
|
|
262
273
|
```bash
|
|
263
274
|
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.1",
|
|
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,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@anthropic-ai/sandbox-runtime": "^0.0.77",
|
|
54
|
-
"@erichll/pi-auto-review": "^0.
|
|
54
|
+
"@erichll/pi-auto-review": "^0.21.0",
|
|
55
55
|
"jiti": "2.7.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
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);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
5
|
import { createJiti } from "jiti";
|
|
@@ -24,6 +25,10 @@ export const PI_SUBAGENTS_COMPAT_RANGE = ">=0.66.0";
|
|
|
24
25
|
export const NATIVE_CHILD_TOOLS = ["bash", "read", "grep", "find", "ls"] as const;
|
|
25
26
|
export const PI_SANDBOX_ACKNOWLEDGEMENT = "@erichll:pi-sandbox";
|
|
26
27
|
|
|
28
|
+
const HOST_PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
29
|
+
/** Host peer that pi-subagents' internal modules import but that is not installed next to it. */
|
|
30
|
+
const HOST_PI_TUI_PACKAGE = "@earendil-works/pi-tui";
|
|
31
|
+
|
|
27
32
|
/**
|
|
28
33
|
* Module extension of the installed pi-subagents layout. Source checkouts (and
|
|
29
34
|
* the 0.66.0-0.69.0 npm line) ship `.ts` modules that need a TypeScript loader;
|
|
@@ -142,6 +147,89 @@ export function piSubagentsInternalModulePath(
|
|
|
142
147
|
return `${relativePath.replace(/\.(?:ts|js)$/u, "")}${extension}`;
|
|
143
148
|
}
|
|
144
149
|
|
|
150
|
+
async function readPackageName(dir: string): Promise<string | undefined> {
|
|
151
|
+
try {
|
|
152
|
+
const manifest = JSON.parse(
|
|
153
|
+
await readFile(join(dir, "package.json"), "utf8"),
|
|
154
|
+
) as { name?: unknown };
|
|
155
|
+
return typeof manifest?.name === "string" ? manifest.name : undefined;
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Walk up from a Pi entry script (or a Pi package directory) to the running
|
|
163
|
+
* `@earendil-works/pi-coding-agent` root. Returns undefined for launchers whose
|
|
164
|
+
* entry is not a real file (compiled binaries, embedded SDK hosts).
|
|
165
|
+
*/
|
|
166
|
+
export async function findPiPackageRootFromEntry(
|
|
167
|
+
entryPath: string,
|
|
168
|
+
): Promise<string | undefined> {
|
|
169
|
+
let dir = entryPath;
|
|
170
|
+
for (;;) {
|
|
171
|
+
if ((await readPackageName(dir)) === HOST_PI_PACKAGE) return dir;
|
|
172
|
+
const parent = dirname(dir);
|
|
173
|
+
if (parent === dir) return undefined;
|
|
174
|
+
dir = parent;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolveFrom(base: string, specifier: string): string | undefined {
|
|
179
|
+
try {
|
|
180
|
+
return createRequire(base).resolve(specifier);
|
|
181
|
+
} catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Alias map for the jiti instance that loads pi-subagents internals.
|
|
188
|
+
*
|
|
189
|
+
* pi-subagents imports `@earendil-works/pi-tui` from its internal modules but,
|
|
190
|
+
* as a peer dependency, does not ship it. Pi's own extension loader aliases that
|
|
191
|
+
* specifier to the copy inside the running Pi package (`getAliases()` in the
|
|
192
|
+
* host loader), so extensions loaded by Pi resolve it; our jiti instance is a
|
|
193
|
+
* separate resolver and would otherwise fall back to plain Node resolution,
|
|
194
|
+
* which fails for installs where pi-tui is neither hoisted next to the
|
|
195
|
+
* extension nor installed beside `@earendil-works/pi-subagents`.
|
|
196
|
+
*
|
|
197
|
+
* The alias is computed host-first so the loaded modules share the host's
|
|
198
|
+
* pi-tui instance, and degrades to an empty map when no copy is reachable: the
|
|
199
|
+
* hard failure then stays the explicit compatibility error raised on load.
|
|
200
|
+
*/
|
|
201
|
+
export async function resolvePiSubagentsHostAliases(
|
|
202
|
+
options: { entry?: string; moduleUrl?: string; env?: Record<string, string | undefined> } = {},
|
|
203
|
+
): Promise<Record<string, string>> {
|
|
204
|
+
const bases: string[] = [];
|
|
205
|
+
const entry = options.entry ?? process.argv[1];
|
|
206
|
+
if (entry) {
|
|
207
|
+
const realEntry = await realpath(entry).catch(() => undefined);
|
|
208
|
+
const root = realEntry
|
|
209
|
+
? await findPiPackageRootFromEntry(realEntry)
|
|
210
|
+
: undefined;
|
|
211
|
+
if (root) bases.push(join(root, "package.json"));
|
|
212
|
+
}
|
|
213
|
+
const packageDir = (options.env ?? process.env).PI_PACKAGE_DIR?.trim();
|
|
214
|
+
if (packageDir && (await readPackageName(packageDir)) === HOST_PI_PACKAGE) {
|
|
215
|
+
bases.push(join(packageDir, "package.json"));
|
|
216
|
+
}
|
|
217
|
+
// Extension-local resolution: the development checkout and installs that
|
|
218
|
+
// provide pi-tui beside the extension or beside pi-subagents.
|
|
219
|
+
bases.push(options.moduleUrl ?? import.meta.url);
|
|
220
|
+
|
|
221
|
+
const seen = new Set<string>();
|
|
222
|
+
for (const base of bases) {
|
|
223
|
+
if (seen.has(base)) continue;
|
|
224
|
+
seen.add(base);
|
|
225
|
+
const resolved = resolveFrom(base, HOST_PI_TUI_PACKAGE);
|
|
226
|
+
if (!resolved) continue;
|
|
227
|
+
const target = await realpath(resolved).catch(() => resolved);
|
|
228
|
+
return { [HOST_PI_TUI_PACKAGE]: target };
|
|
229
|
+
}
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
|
|
145
233
|
/** Load the public ceiling API and the discovery/config internals.
|
|
146
234
|
* This intentionally fails closed on package version, export, or layout drift.
|
|
147
235
|
*/
|
|
@@ -163,6 +251,7 @@ export async function loadPiSubagentsNativeRuntime(
|
|
|
163
251
|
const jiti = createJiti(import.meta.url, {
|
|
164
252
|
interopDefault: false,
|
|
165
253
|
fsCache: false,
|
|
254
|
+
alias: await resolvePiSubagentsHostAliases(),
|
|
166
255
|
});
|
|
167
256
|
const internal = (relative: string): string =>
|
|
168
257
|
pathToFileURL(join(root, piSubagentsInternalModulePath(relative, extension))).href;
|
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).
|