@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erichll/pi-sandbox",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Sandbox Runtime-backed Linux and macOS sandbox with pi-auto-review approval",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
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 === undefined) {
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
- rejectUnknownKeys(value.subagents, ["provider"], "subagents");
69
+ const subagents = value.subagents ?? {};
70
+ rejectUnknownKeys(subagents, ["provider"], "subagents");
69
71
 
70
72
  const provider =
71
- value.subagents.provider ?? DEFAULT_PI_SANDBOX_CONFIG.subagents.provider;
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 ?? loadPiSandboxConfig().subagents.provider;
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(ctx, () => currentTurn, options.sandbox),
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(ctx, () => currentTurn, options.sandbox),
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 function createDefaultPolicy(cwd: string): SandboxPolicy {
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: [