@erichll/pi-sandbox 0.3.0 → 0.3.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/README.md +24 -0
- package/package.json +1 -1
- package/src/config.ts +48 -10
- package/src/index.ts +19 -3
- package/src/policy.ts +9 -1
package/README.md
CHANGED
|
@@ -62,6 +62,30 @@ Supported modes:
|
|
|
62
62
|
The configuration parser rejects malformed JSON, unknown fields, and unknown
|
|
63
63
|
providers instead of silently weakening isolation.
|
|
64
64
|
|
|
65
|
+
## Additional trusted read paths
|
|
66
|
+
|
|
67
|
+
The trusted global configuration may append absolute paths to the sandbox's
|
|
68
|
+
default read allowlist. This is useful for executables installed below the
|
|
69
|
+
otherwise-denied home directory:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"subagents": {
|
|
74
|
+
"provider": "builtin"
|
|
75
|
+
},
|
|
76
|
+
"filesystem": {
|
|
77
|
+
"additionalAllowRead": [
|
|
78
|
+
"/home/user/.local/bin/rtk"
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`filesystem.additionalAllowRead` must be an array of absolute paths. These
|
|
85
|
+
paths extend the defaults; they do not replace the workspace, Node.js, or
|
|
86
|
+
Sandbox Runtime read permissions. Keep entries as narrow as possible and
|
|
87
|
+
prefer an exact executable path over allowing an entire bin directory.
|
|
88
|
+
|
|
65
89
|
Persistent built-in sessions support `start`, `follow_up`, `wait`, `status`,
|
|
66
90
|
`stop`, and nested `handoff` operations. At most four sessions are live at
|
|
67
91
|
once, and nesting depth is capped at three.
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
4
|
|
|
5
5
|
export const SUBAGENT_PROVIDERS = [
|
|
6
6
|
"builtin",
|
|
@@ -14,6 +14,9 @@ export type PiSandboxConfig = {
|
|
|
14
14
|
subagents: {
|
|
15
15
|
provider: SubagentProvider;
|
|
16
16
|
};
|
|
17
|
+
filesystem: {
|
|
18
|
+
additionalAllowRead: readonly string[];
|
|
19
|
+
};
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
export type LoadPiSandboxConfigOptions = {
|
|
@@ -25,6 +28,9 @@ export const DEFAULT_PI_SANDBOX_CONFIG: Readonly<PiSandboxConfig> = Object.freez
|
|
|
25
28
|
subagents: Object.freeze({
|
|
26
29
|
provider: "builtin",
|
|
27
30
|
}),
|
|
31
|
+
filesystem: Object.freeze({
|
|
32
|
+
additionalAllowRead: Object.freeze([]),
|
|
33
|
+
}),
|
|
28
34
|
},
|
|
29
35
|
);
|
|
30
36
|
|
|
@@ -53,22 +59,18 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
|
|
|
53
59
|
if (!isRecord(value)) {
|
|
54
60
|
throw new Error("invalid pi-sandbox configuration: root must be an object");
|
|
55
61
|
}
|
|
56
|
-
rejectUnknownKeys(value, ["subagents"], "root");
|
|
62
|
+
rejectUnknownKeys(value, ["subagents", "filesystem"], "root");
|
|
57
63
|
|
|
58
|
-
if (value.subagents
|
|
59
|
-
return {
|
|
60
|
-
subagents: { provider: DEFAULT_PI_SANDBOX_CONFIG.subagents.provider },
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
if (!isRecord(value.subagents)) {
|
|
64
|
+
if (value.subagents !== undefined && !isRecord(value.subagents)) {
|
|
64
65
|
throw new Error(
|
|
65
66
|
"invalid pi-sandbox configuration: subagents must be an object",
|
|
66
67
|
);
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
+
const subagents = value.subagents ?? {};
|
|
70
|
+
rejectUnknownKeys(subagents, ["provider"], "subagents");
|
|
69
71
|
|
|
70
72
|
const provider =
|
|
71
|
-
|
|
73
|
+
subagents.provider ?? DEFAULT_PI_SANDBOX_CONFIG.subagents.provider;
|
|
72
74
|
if (
|
|
73
75
|
typeof provider !== "string" ||
|
|
74
76
|
!SUBAGENT_PROVIDERS.includes(provider as SubagentProvider)
|
|
@@ -78,10 +80,41 @@ export function parsePiSandboxConfig(value: unknown): PiSandboxConfig {
|
|
|
78
80
|
);
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
if (value.filesystem !== undefined && !isRecord(value.filesystem)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"invalid pi-sandbox configuration: filesystem must be an object",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const filesystem = value.filesystem ?? {};
|
|
89
|
+
rejectUnknownKeys(
|
|
90
|
+
filesystem,
|
|
91
|
+
["additionalAllowRead"],
|
|
92
|
+
"filesystem",
|
|
93
|
+
);
|
|
94
|
+
const additionalAllowRead =
|
|
95
|
+
filesystem.additionalAllowRead ??
|
|
96
|
+
DEFAULT_PI_SANDBOX_CONFIG.filesystem.additionalAllowRead;
|
|
97
|
+
if (
|
|
98
|
+
!Array.isArray(additionalAllowRead) ||
|
|
99
|
+
additionalAllowRead.some(
|
|
100
|
+
(path) =>
|
|
101
|
+
typeof path !== "string" ||
|
|
102
|
+
path.trim() === "" ||
|
|
103
|
+
!isAbsolute(path),
|
|
104
|
+
)
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"invalid pi-sandbox configuration: filesystem.additionalAllowRead must be an array of absolute paths",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
return {
|
|
82
112
|
subagents: {
|
|
83
113
|
provider: provider as SubagentProvider,
|
|
84
114
|
},
|
|
115
|
+
filesystem: {
|
|
116
|
+
additionalAllowRead: [...new Set(additionalAllowRead)],
|
|
117
|
+
},
|
|
85
118
|
};
|
|
86
119
|
}
|
|
87
120
|
|
|
@@ -101,6 +134,11 @@ export function loadPiSandboxConfig(
|
|
|
101
134
|
) {
|
|
102
135
|
return {
|
|
103
136
|
subagents: { provider: DEFAULT_PI_SANDBOX_CONFIG.subagents.provider },
|
|
137
|
+
filesystem: {
|
|
138
|
+
additionalAllowRead: [
|
|
139
|
+
...DEFAULT_PI_SANDBOX_CONFIG.filesystem.additionalAllowRead,
|
|
140
|
+
],
|
|
141
|
+
},
|
|
104
142
|
};
|
|
105
143
|
}
|
|
106
144
|
throw new Error(`failed to read pi-sandbox configuration at ${path}`, {
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
runSandboxedCommand,
|
|
22
22
|
type SandboxCommandOptions,
|
|
23
23
|
} from "./runner.ts";
|
|
24
|
+
import { createDefaultPolicy } from "./policy.ts";
|
|
24
25
|
import type {
|
|
25
26
|
ProcessBackedSubagentManager,
|
|
26
27
|
ProcessBackedSubagentSession,
|
|
@@ -100,6 +101,7 @@ function humanApproval(ctx: ExtensionContext): HumanApproval {
|
|
|
100
101
|
function sandboxOperations(
|
|
101
102
|
ctx: ExtensionContext,
|
|
102
103
|
turnIndex: () => number,
|
|
104
|
+
additionalAllowRead: readonly string[],
|
|
103
105
|
sandbox?: PiSandboxExtensionOptions["sandbox"],
|
|
104
106
|
): BashOperations {
|
|
105
107
|
return {
|
|
@@ -114,6 +116,7 @@ function sandboxOperations(
|
|
|
114
116
|
...sandbox,
|
|
115
117
|
onData: options.onData,
|
|
116
118
|
shellPath: SettingsManager.create(cwd).getShellPath(),
|
|
119
|
+
policy: createDefaultPolicy(cwd, { additionalAllowRead }),
|
|
117
120
|
review: async (trap) => {
|
|
118
121
|
const result = await approveSandboxTrap(trap, {
|
|
119
122
|
broker: getBoundaryBroker(),
|
|
@@ -156,8 +159,10 @@ async function performRegistration(
|
|
|
156
159
|
options: PiSandboxExtensionOptions,
|
|
157
160
|
): Promise<void> {
|
|
158
161
|
let currentTurn = 0;
|
|
162
|
+
const config = loadPiSandboxConfig();
|
|
159
163
|
const subagentProvider =
|
|
160
|
-
options.subagentProvider ??
|
|
164
|
+
options.subagentProvider ?? config.subagents.provider;
|
|
165
|
+
const additionalAllowRead = config.filesystem.additionalAllowRead;
|
|
161
166
|
const subagents =
|
|
162
167
|
subagentProvider === "builtin"
|
|
163
168
|
? (options.subagentManager ??
|
|
@@ -176,7 +181,12 @@ async function performRegistration(
|
|
|
176
181
|
async execute(id, params, signal, onUpdate, ctx) {
|
|
177
182
|
if (!ctx) throw new Error(`${EXTENSION_NAME}: missing extension context`);
|
|
178
183
|
const tool = createBashToolDefinition(ctx.cwd, {
|
|
179
|
-
operations: sandboxOperations(
|
|
184
|
+
operations: sandboxOperations(
|
|
185
|
+
ctx,
|
|
186
|
+
() => currentTurn,
|
|
187
|
+
additionalAllowRead,
|
|
188
|
+
options.sandbox,
|
|
189
|
+
),
|
|
180
190
|
});
|
|
181
191
|
return tool.execute(id, params, signal, onUpdate, ctx);
|
|
182
192
|
},
|
|
@@ -262,6 +272,7 @@ async function performRegistration(
|
|
|
262
272
|
cwd: ctx.cwd,
|
|
263
273
|
model: params.model,
|
|
264
274
|
tools: pi.getActiveTools().filter((name) => name !== "subagent"),
|
|
275
|
+
policy: createDefaultPolicy(ctx.cwd, { additionalAllowRead }),
|
|
265
276
|
sandbox: options.sandbox,
|
|
266
277
|
review: async (trap: Parameters<typeof approveSandboxTrap>[0]) =>
|
|
267
278
|
(await approveSandboxTrap(trap, approvalContext)).action,
|
|
@@ -408,7 +419,12 @@ async function performRegistration(
|
|
|
408
419
|
});
|
|
409
420
|
|
|
410
421
|
pi.on("user_bash", (_event, ctx) => ({
|
|
411
|
-
operations: sandboxOperations(
|
|
422
|
+
operations: sandboxOperations(
|
|
423
|
+
ctx,
|
|
424
|
+
() => currentTurn,
|
|
425
|
+
additionalAllowRead,
|
|
426
|
+
options.sandbox,
|
|
427
|
+
),
|
|
412
428
|
}));
|
|
413
429
|
|
|
414
430
|
pi.on("turn_start", (event) => {
|
package/src/policy.ts
CHANGED
|
@@ -35,7 +35,14 @@ export type SandboxPolicy = {
|
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
export
|
|
38
|
+
export type CreateDefaultPolicyOptions = {
|
|
39
|
+
additionalAllowRead?: readonly string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function createDefaultPolicy(
|
|
43
|
+
cwd: string,
|
|
44
|
+
options: CreateDefaultPolicyOptions = {},
|
|
45
|
+
): SandboxPolicy {
|
|
39
46
|
const workspace = resolve(cwd);
|
|
40
47
|
const home = resolve(homedir());
|
|
41
48
|
const denyRead = home === parse(home).root ? [] : [home];
|
|
@@ -53,6 +60,7 @@ export function createDefaultPolicy(cwd: string): SandboxPolicy {
|
|
|
53
60
|
join(home, ".gitconfig"),
|
|
54
61
|
join(home, ".config", "git", "config"),
|
|
55
62
|
"/dev/null",
|
|
63
|
+
...(options.additionalAllowRead ?? []),
|
|
56
64
|
],
|
|
57
65
|
allowWrite: [workspace, "/dev/null"],
|
|
58
66
|
denyWrite: [
|