pi-better-subagents 0.13.1 → 0.14.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/docs/usage.md CHANGED
@@ -279,7 +279,8 @@ it does not depend on any other extension being installed.
279
279
  denials retain precedence. See the permission details below.
280
280
  - **Tool allowlist.** Confined children admit only verified read/write/edit/bash
281
281
  implementations, the guarded `apply_patch`, and the trusted tools ticked in
282
- `/sandbox`. Other requested tools are reported as unavailable, with the reason.
282
+ `/sandbox`. The fixed `process_list` adapter is available with Process access
283
+ set to Read. Other requested tools are reported as unavailable, with the reason.
283
284
  - **No runaway recursion.** Confined children cannot spawn nested agents.
284
285
  `allow_nested:true` loads nested-agent support only for unconfined children.
285
286
 
@@ -291,9 +292,10 @@ use Project files = Write & delete and Outside project = **Write**: tasks write
291
292
  across home and temp (tool caches such as `~/.gradle` and `~/.npm` just work), but
292
293
  outside the workspace they can only remove or rename files in temp, hidden home
293
294
  entries, and worktree folders (`.worktrees/`, `*-worktrees/`). Credential files
294
- default to readable but not writable, so npm, git over SSH, and authenticated
295
- CLIs can reuse their normal user configuration. This exposes known file-based
296
- credentials to confined subagents; set Stored credentials to Off to hide them.
295
+ default to Write & delete, so authenticated CLIs can maintain their credential
296
+ databases and token state. Tasks can also modify or delete these credentials,
297
+ subject to stricter protected-path rules. Set Stored credentials to Read to
298
+ deny writes or Off to hide them. Explicit saved profiles keep their values.
297
299
  Shell startup files, `~/.pi`, `~/.claude`,
298
300
  `~/.agents`, and harness state cannot be written, removed, or renamed. Rename-based
299
301
  saves and git commits in a sibling repository fail under Write; set Outside
@@ -312,6 +314,15 @@ credential-file rules; there are no writable lock exceptions. Runtime code,
312
314
  configuration, and control files remain protected from task writes.
313
315
 
314
316
  Commands Off still permits file tools according to their file permissions.
317
+ Process access defaults to Off. Set it to Read for the fixed `process_list`
318
+ tool, which returns current-user PIDs and names only, even with Commands Off.
319
+ It accepts a literal name filter and a bounded limit, not shell commands or
320
+ process-control actions. It does not expose arguments or environments, and
321
+ does not start an emulator or allocate test resources. The setting governs
322
+ the fixed adapter only, not every OS process operation available through bash
323
+ or trusted tools. New launches capture changes; running children retain their
324
+ launch-time setting. Raw macOS `/bin/ps` remains subject to setuid execution
325
+ restrictions. See [ADR 0011](../../../docs/adr/0011-fixed-read-only-process-inventory.md).
315
326
  Network Off blocks task network access while Pi's provider transport remains
316
327
  available. The confined file worker has an 8 MiB file limit and rejects larger
317
328
  files explicitly; use confined commands for larger files when commands are On.
package/index.ts CHANGED
@@ -1761,14 +1761,19 @@ export default function (pi: ExtensionAPI) {
1761
1761
  if (p.tools === undefined) names.push(...permissionPlan.tools.trusted.map((entry) => entry.name));
1762
1762
  allow = [...new Set(names)].join(",");
1763
1763
  }
1764
+ if (sandboxEnabled && p.tools === undefined && permissionPlan.permissions?.processAccess === "read") {
1765
+ allow = [...new Set([...allow.split(","), "process_list"])].join(",");
1766
+ }
1764
1767
  const taskTools = sandboxEnabled ? planTaskTools({
1765
1768
  requested: allow.split(",").filter((name) => name && !requestedExcludes.has(name)),
1766
1769
  settings: permissionPlan.tools, network: permissionPlan.permissions?.network ?? true,
1770
+ processAccess: permissionPlan.permissions?.processAccess ?? "off",
1767
1771
  builtins: TASK_BUILTINS, registered: pi.getAllTools?.() ?? [],
1768
1772
  toolExtensions: cfg.toolExtensions, resolvePath: resolveExtensionPath,
1769
1773
  }) : undefined;
1770
1774
  const resolution = resolveExtensions({
1771
- tools: sandboxEnabled ? allow.split(",").filter((name) => (TASK_BUILTINS as readonly string[]).includes(name)).join(",") : allow,
1775
+ // process_list is installed by the mandatory task guard, not an SDK builtin or extension mapping.
1776
+ tools: sandboxEnabled ? allow.split(",").filter((name) => name !== "process_list" && (TASK_BUILTINS as readonly string[]).includes(name)).join(",") : allow,
1772
1777
  model, clean, allowNested: sandboxEnabled ? false : p.allow_nested, config: cfg,
1773
1778
  });
1774
1779
  const { args: resolvedExtArgs, missing } = extensionArgs(resolution, resolveExtensionPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,6 +12,7 @@ export type PermissionProfile = Readonly<{
12
12
  storedCredentials: CredentialAccess;
13
13
  commands: boolean;
14
14
  network: boolean;
15
+ processAccess?: "off" | "read";
15
16
  }>;
16
17
  export type PermissionSnapshot = Readonly<{
17
18
  permissions?: PermissionProfile;
@@ -38,12 +39,14 @@ function profile(value: unknown): PermissionProfile {
38
39
  const credential = (v: unknown): v is CredentialAccess => v === "off" || v === "read" || v === "read-write";
39
40
  const access = (v: unknown): v is Access => credential(v) || v === "write";
40
41
  if (typeof p.enabled !== "boolean" || typeof p.commands !== "boolean" || typeof p.network !== "boolean" ||
41
- !access(p.projectFiles) || !access(p.outsideProject) || !credential(p.storedCredentials)) {
42
+ !access(p.projectFiles) || !access(p.outsideProject) || !credential(p.storedCredentials) ||
43
+ (p.processAccess !== undefined && p.processAccess !== "off" && p.processAccess !== "read")) {
42
44
  throw new Error("Invalid sandbox permission profile; update permissions in the sandbox UI.");
43
45
  }
44
46
  return Object.freeze({
45
47
  enabled: p.enabled, commands: p.commands, network: p.network,
46
48
  projectFiles: p.projectFiles, outsideProject: p.outsideProject, storedCredentials: p.storedCredentials,
49
+ ...(p.processAccess === undefined ? {} : { processAccess: p.processAccess }),
47
50
  });
48
51
  }
49
52
 
@@ -60,6 +60,8 @@ export type SandboxPermissions = {
60
60
  storedCredentials: "off" | "read" | "read-write";
61
61
  commands: boolean;
62
62
  network: boolean;
63
+ /** Fixed process_list adapter only; omitted legacy profiles keep it Off. */
64
+ processAccess?: "off" | "read";
63
65
  };
64
66
 
65
67
  /** Whether a file access level allows creating and overwriting files. */
@@ -0,0 +1,203 @@
1
+ // Generated from packages/task-sandbox/process-list.ts. Do not edit directly.
2
+ /** Fixed, read-only current-user process inventory. Never accepts command arguments. */
3
+ import { spawn } from "node:child_process";
4
+ import { randomUUID } from "node:crypto";
5
+ import { lstatSync, realpathSync } from "node:fs";
6
+ import { unlink } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ import { maybeBuildSandboxCommand, type SandboxWritePolicy } from "./shared-sandbox-core.ts";
9
+ import type { TaskFileController } from "./shared-task-files.ts";
10
+
11
+ export const PROCESS_LIST_TOOL = "process_list";
12
+
13
+ const EXECUTABLE = "/usr/bin/pgrep";
14
+ const MAX_STDOUT = 1024 * 1024;
15
+ const MAX_STDERR = 2048;
16
+ const TIMEOUT_MS = 5000;
17
+
18
+ const parameters = {
19
+ type: "object",
20
+ additionalProperties: false,
21
+ properties: {
22
+ name: { type: "string", maxLength: 128, description: "Literal case-insensitive process-name substring." },
23
+ limit: { type: "integer", minimum: 1, maximum: 200, default: 100 },
24
+ },
25
+ } as const;
26
+
27
+ type Inventory = { processes: { pid: number; name: string }[]; truncated: boolean; scope: "current-user" };
28
+
29
+ function validateParams(params: unknown): { name: string | undefined; limit: number } {
30
+ if (!params || typeof params !== "object" || Array.isArray(params) ||
31
+ Reflect.ownKeys(params).some((key) => key !== "name" && key !== "limit")) {
32
+ throw new Error("process_list accepts only optional name and limit fields.");
33
+ }
34
+ const { name, limit } = params as { name?: unknown; limit?: unknown };
35
+ if (name !== undefined && (typeof name !== "string" || [...name].length > 128)) {
36
+ throw new Error("process_list name must be a string of at most 128 characters.");
37
+ }
38
+ if (limit !== undefined && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > 200)) {
39
+ throw new Error("process_list limit must be an integer from 1 to 200.");
40
+ }
41
+ return { name: name as string | undefined, limit: limit as number | undefined ?? 100 };
42
+ }
43
+
44
+ function executableIdentity(): string {
45
+ try {
46
+ if (realpathSync.native(EXECUTABLE) !== EXECUTABLE) throw new Error("non-canonical executable path");
47
+ // Root-owned, non-replaceable ancestors prevent task users retargeting the
48
+ // checked path. Normal Linux utilities need no filesystem immutable flag.
49
+ for (const path of ["/", "/usr", "/usr/bin", EXECUTABLE]) {
50
+ const info = lstatSync(path, { bigint: true });
51
+ if (info.uid !== 0n || (info.mode & 0o022n) !== 0n ||
52
+ (path === EXECUTABLE ? !info.isFile() : !info.isDirectory())) {
53
+ throw new Error("path must be root-owned and not group/world writable, without symlinks");
54
+ }
55
+ if (path === EXECUTABLE) {
56
+ if ((info.mode & 0o6000n) !== 0n) throw new Error("setuid/setgid executable is forbidden");
57
+ if ((info.mode & 0o111n) === 0n) throw new Error("executable permission is missing");
58
+ return [info.dev, info.ino, info.uid, info.gid, info.mode, info.size, info.mtimeNs, info.ctimeNs].join(":");
59
+ }
60
+ }
61
+ } catch (error) {
62
+ throw new Error(`process_list unsupported: fixed ${EXECUTABLE} is unavailable or unsafe (${error instanceof Error ? error.message : String(error)}).`);
63
+ }
64
+ throw new Error("process_list unsupported: fixed executable identity unavailable.");
65
+ }
66
+
67
+ function parseInventory(stdout: Buffer, name: string | undefined, limit: number): Inventory {
68
+ const processes: Inventory["processes"] = [];
69
+ const seen = new Set<number>();
70
+ const needle = name?.toLowerCase();
71
+ let matches = 0;
72
+ for (const line of stdout.toString("utf8").split("\n")) {
73
+ if (!line) continue;
74
+ // pgrep -l (without -f/-a) emits PID, one separator, and the comm/name.
75
+ // Keep spaces in names; never interpret the model's filter as a regex.
76
+ const match = /^[ \t]*([0-9]+)[ \t](.+)$/.exec(line);
77
+ const pid = match ? Number(match[1]) : NaN;
78
+ if (!match || !Number.isSafeInteger(pid) || pid <= 0 || seen.has(pid) || /[\x00-\x1f\x7f]/.test(match[2]!)) {
79
+ throw new Error("process_list received malformed pgrep process-name output.");
80
+ }
81
+ seen.add(pid);
82
+ const processName = match[2]!;
83
+ if (needle !== undefined && !processName.toLowerCase().includes(needle)) continue;
84
+ matches++;
85
+ if (processes.length < limit) processes.push({ pid, name: processName });
86
+ }
87
+ return { processes, truncated: matches > limit, scope: "current-user" };
88
+ }
89
+
90
+ function aborted(): Error {
91
+ return Object.assign(new Error("process_list aborted."), { name: "AbortError" });
92
+ }
93
+
94
+ function run(file: string, argv: string[], cwd: string, home: string, signal?: AbortSignal): Promise<Buffer> {
95
+ if (signal?.aborted) return Promise.reject(aborted());
96
+ return new Promise((resolve, reject) => {
97
+ const child = spawn(file, argv, {
98
+ cwd, detached: true, shell: false, stdio: ["ignore", "pipe", "pipe"],
99
+ env: { LC_ALL: "C", HOME: home, PATH: "/usr/bin:/bin" },
100
+ });
101
+ const chunks: Buffer[] = [];
102
+ let size = 0;
103
+ let stderr = Buffer.alloc(0);
104
+ let failure: Error | undefined;
105
+ let closed = false;
106
+ const stop = (error: Error) => {
107
+ if (closed || failure) return;
108
+ failure = error;
109
+ // Only the owned helper's detached group, never an inventory PID.
110
+ if (child.pid !== undefined) {
111
+ try { process.kill(-child.pid, "SIGKILL"); }
112
+ catch { child.kill("SIGKILL"); }
113
+ }
114
+ };
115
+ const onAbort = () => stop(aborted());
116
+ const timer = setTimeout(() => stop(new Error("process_list timed out after 5 seconds.")), TIMEOUT_MS);
117
+ signal?.addEventListener("abort", onAbort, { once: true });
118
+ if (signal?.aborted) onAbort();
119
+ child.stdout.on("data", (chunk: Buffer) => {
120
+ size += chunk.length;
121
+ if (size > MAX_STDOUT) stop(new Error("process_list stdout exceeds 1 MiB inventory limit."));
122
+ else if (!failure) chunks.push(chunk);
123
+ });
124
+ child.stderr.on("data", (chunk: Buffer) => {
125
+ if (stderr.length < MAX_STDERR) stderr = Buffer.concat([stderr, chunk.subarray(0, MAX_STDERR - stderr.length)]);
126
+ });
127
+ child.on("error", (error) => { failure ??= error; });
128
+ child.on("close", (code, exitSignal) => {
129
+ closed = true;
130
+ clearTimeout(timer);
131
+ signal?.removeEventListener("abort", onAbort);
132
+ if (failure) return reject(failure);
133
+ if (code === 1 && size === 0 && stderr.length === 0) return resolve(Buffer.alloc(0));
134
+ if (code !== 0) {
135
+ return reject(new Error(`process_list helper failed (${exitSignal ?? code}): ${stderr.toString("utf8") || "no diagnostic"}`));
136
+ }
137
+ resolve(Buffer.concat(chunks, size));
138
+ });
139
+ });
140
+ }
141
+
142
+ export function createProcessListToolDefinition(cwd: string, controller: TaskFileController, access: () => "off" | "read") {
143
+ let identity: string | undefined;
144
+ const checkAccess = () => {
145
+ if (access() !== "read") throw new Error("Sandbox: Process access is Off. Enable Read in /sandbox.");
146
+ };
147
+ const checkExecutable = () => {
148
+ const current = executableIdentity();
149
+ if (identity !== undefined && current !== identity) throw new Error("process_list unsupported: fixed executable identity changed.");
150
+ identity = current;
151
+ };
152
+ return {
153
+ name: PROCESS_LIST_TOOL,
154
+ label: "Process List",
155
+ description: "Read current-user process IDs and process names only. Optional literal case-insensitive name filter; no arguments, environment, paths, or process control.",
156
+ promptSnippet: "List current-user process IDs and names with an optional literal name filter.",
157
+ parameters,
158
+ async execute(_toolCallId: string, params: unknown, signal?: AbortSignal) {
159
+ checkAccess();
160
+ const { name, limit } = validateParams(params);
161
+ if (signal?.aborted) throw aborted();
162
+ const plan = controller.requireLaunchPlan();
163
+ if ((process.platform !== "darwin" && process.platform !== "linux") || typeof process.getuid !== "function") {
164
+ throw new Error(`process_list unsupported on ${process.platform}.`);
165
+ }
166
+ const uid = process.getuid();
167
+ if (!Number.isSafeInteger(uid) || uid < 0) throw new Error("process_list unsupported: current user ID unavailable.");
168
+ checkExecutable();
169
+ const argv = ["-l", "-u", String(uid), "."];
170
+ let profilePath: string | undefined;
171
+ try {
172
+ let file = EXECUTABLE;
173
+ let fileArgs = argv;
174
+ if (plan.confined) {
175
+ const helperPolicy: SandboxWritePolicy = {
176
+ ...plan.policy,
177
+ permissions: {
178
+ projectFiles: "read-write", outsideProject: "read", storedCredentials: "read",
179
+ ...plan.policy.permissions, commands: true, network: false,
180
+ },
181
+ };
182
+ profilePath = `${plan.profilePath}.${randomUUID()}.process-list.sb`;
183
+ const command = maybeBuildSandboxCommand({
184
+ execPath: EXECUTABLE, execArgs: argv, internalHelperExecutable: true,
185
+ profilePath, policy: helperPolicy,
186
+ }, { sandboxEnabled: true, explicitSandbox: true });
187
+ if (!command) throw new Error("process_list sandbox backend unavailable.");
188
+ file = command.file;
189
+ fileArgs = command.fileArgs;
190
+ }
191
+ checkExecutable();
192
+ checkAccess();
193
+ const stdout = await run(file, fileArgs, cwd, plan.confined ? plan.policy.home : homedir(), signal);
194
+ const result = parseInventory(stdout, name, limit);
195
+ return { content: [{ type: "text" as const, text: JSON.stringify(result) }], details: result };
196
+ } finally {
197
+ if (profilePath) await unlink(profilePath).catch((error: NodeJS.ErrnoException) => {
198
+ if (error.code !== "ENOENT") throw error;
199
+ });
200
+ }
201
+ },
202
+ };
203
+ }
@@ -14,8 +14,9 @@ import { basename, dirname, isAbsolute, join, parse, resolve, sep } from "node:p
14
14
  import { canonicalizePath, compileWritePolicy, isRemovableUnderWrite, maybeBuildSandboxCommand, type SandboxPermissions } from "./shared-sandbox-core.ts";
15
15
  import { createTaskFileOperations, type TaskFileController } from "./shared-task-files.ts";
16
16
  import { APPLY_PATCH_TOOL, createApplyPatchToolDefinition } from "./shared-task-apply-patch.ts";
17
+ import { createProcessListToolDefinition, PROCESS_LIST_TOOL } from "./shared-task-process-list.ts";
17
18
 
18
- export const TASK_BUILTINS = Object.freeze(["read", "write", "edit", "bash"] as const);
19
+ export const TASK_BUILTINS = Object.freeze(["read", "write", "edit", "bash", PROCESS_LIST_TOOL] as const);
19
20
  /** Harness adapters that follow the file rules; each is a task builtin only when the profile enables it. */
20
21
  export const GUARDED_TASK_TOOLS = Object.freeze([APPLY_PATCH_TOOL] as const);
21
22
 
@@ -182,6 +183,8 @@ export function installTaskTools(pi: ExtensionAPI, options: {
182
183
  admitExtensionTool?: (name: string, input: unknown, sourcePath: string | undefined) => boolean;
183
184
  /** Register the guarded apply_patch adapter as a task builtin. */
184
185
  applyPatch?: boolean;
186
+ /** Human-controlled access to the fixed read-only inventory adapter. */
187
+ processAccess?: () => "off" | "read";
185
188
  /** Build the admitted bash definition from the confined operations (default: the SDK bash tool). */
186
189
  bashDefinition?: (cwd: string, operations: BashOperations) => ReturnType<typeof createBashToolDefinition>;
187
190
  }) {
@@ -205,6 +208,7 @@ export function installTaskTools(pi: ExtensionAPI, options: {
205
208
  pi.registerTool(own(createWriteToolDefinition(cwd, { operations: files.write })));
206
209
  pi.registerTool(own(createEditToolDefinition(cwd, { operations: files.edit })));
207
210
  pi.registerTool(own(options.bashDefinition ? options.bashDefinition(cwd, bash) : createBashToolDefinition(cwd, { operations: bash })));
211
+ pi.registerTool(own(createProcessListToolDefinition(cwd, controller, options.processAccess ?? (() => "off"))) as any);
208
212
  if (options.applyPatch) {
209
213
  pi.registerTool(own(createApplyPatchToolDefinition(cwd, {
210
214
  readFile: files.read.readFile, writeFile: files.write.writeFile, mkdir: files.write.mkdir,
@@ -226,6 +230,9 @@ export function installTaskTools(pi: ExtensionAPI, options: {
226
230
  if (event.toolName === "bash" && plan.policy.permissions?.commands === false) {
227
231
  return { block: true, reason: "Sandbox: Run commands & applications is Off." };
228
232
  }
233
+ if (event.toolName === PROCESS_LIST_TOOL && options.processAccess?.() !== "read") {
234
+ return { block: true, reason: "Sandbox: Process access is Off. Enable Read in /sandbox." };
235
+ }
229
236
  return;
230
237
  }
231
238
  if (options.admitExtensionTool?.(event.toolName, event.input, tool?.sourceInfo?.path)) return;
@@ -41,7 +41,7 @@ export function isNetworkTool(name: string): boolean {
41
41
  }
42
42
 
43
43
  /** Names never offered as trusted: builtins and the guarded adapters. */
44
- export const RESERVED_TOOL_NAMES = Object.freeze(["read", "write", "edit", "bash", "grep", "find", "ls", "powershell", "apply_patch"]);
44
+ export const RESERVED_TOOL_NAMES = Object.freeze(["read", "write", "edit", "bash", "grep", "find", "ls", "powershell", "apply_patch", "process_list"]);
45
45
 
46
46
  /** Packages that are this harness: their tools are never offered as trusted tools. */
47
47
  const HARNESS_PACKAGES = /(^|[/:])pi-better-[a-z-]+$/;
package/subagent-tools.ts CHANGED
@@ -30,6 +30,7 @@ export function planTaskTools(options: {
30
30
  requested: readonly string[];
31
31
  settings: SubagentToolSettings;
32
32
  network: boolean;
33
+ processAccess?: "off" | "read";
33
34
  builtins: readonly string[];
34
35
  /** Tools registered in the parent Pi (`pi.getAllTools()`), used to find a ticked tool's package. */
35
36
  registered: readonly ToolSource[];
@@ -38,6 +39,10 @@ export function planTaskTools(options: {
38
39
  }): TaskToolPlan {
39
40
  const plan: TaskToolPlan = { applyPatch: false, trusted: [], refused: [] };
40
41
  for (const name of options.requested) {
42
+ if (name === "process_list" && options.processAccess !== "read") {
43
+ plan.refused.push({ name, reason: "Process access is Off; enable Read in /sandbox" });
44
+ continue;
45
+ }
41
46
  if (options.builtins.includes(name)) continue;
42
47
  if (name === APPLY_PATCH) {
43
48
  if (options.settings.applyPatch) plan.applyPatch = true;
package/task-guard.ts CHANGED
@@ -47,6 +47,7 @@ export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (erro
47
47
  // Structured command intent (#315): validated before the confined command runs.
48
48
  bashDefinition: (cwd, operations) => intentBashDefinition(cwd, operations) as any,
49
49
  applyPatch: policy.applyPatch,
50
+ processAccess: () => policy.permissions.processAccess ?? "off",
50
51
  // The disposition tool performs no file or command I/O; only this inline registration is admitted.
51
52
  // Trusted tools are admitted by name and package (ADR 0009).
52
53
  admitExtensionTool: (name, _input, sourcePath) => (name === DISPOSITION_TOOL && sourcePath === TRUSTED_INLINE_SOURCE) ||
@@ -68,7 +69,8 @@ export default function taskGuard(pi: ExtensionAPI, input: unknown, fatal: (erro
68
69
  currentCwd = ctx.cwd;
69
70
  boundary.register(ctx.cwd);
70
71
  const builtins: readonly string[] = policy.applyPatch ? [...TASK_BUILTINS, ...GUARDED_TASK_TOOLS] : TASK_BUILTINS;
71
- const selected = policy.tools.filter((name) => builtins.includes(name));
72
+ const selected = policy.tools.filter((name) => builtins.includes(name) &&
73
+ (name !== "process_list" || policy.permissions.processAccess === "read"));
72
74
  boundary.assertInstalled(selected);
73
75
  const inventory = pi.getAllTools();
74
76
  const trusted: string[] = [];
package/task-policy.ts CHANGED
@@ -39,7 +39,8 @@ export type TaskExtensionTool = Readonly<{
39
39
  * the fixed deny list). Matches `pi-better-sandbox`'s default Subagents column.
40
40
  */
41
41
  export const DEFAULT_TASK_PERMISSIONS: Readonly<SandboxPermissions> = Object.freeze({
42
- projectFiles: "read-write", outsideProject: "write", storedCredentials: "read", commands: true, network: true,
42
+ projectFiles: "read-write", outsideProject: "write", storedCredentials: "read-write", commands: true, network: true,
43
+ processAccess: "off",
43
44
  });
44
45
 
45
46
  export function parseTaskPolicy(value: unknown): TaskPolicy {
@@ -57,6 +58,7 @@ export function parseTaskPolicy(value: unknown): TaskPolicy {
57
58
  }
58
59
  if (!["off", "read", "read-write"].includes(String(p.storedCredentials))) throw new Error("Invalid task sandbox storedCredentials.");
59
60
  if (typeof p.commands !== "boolean" || typeof p.network !== "boolean") throw new Error("Invalid task sandbox capabilities.");
61
+ if (p.processAccess !== undefined && p.processAccess !== "off" && p.processAccess !== "read") throw new Error("Invalid task sandbox processAccess.");
60
62
  if (!Array.isArray(v.denyWrite) || !v.denyWrite.every((path) => typeof path === "string" && isAbsolute(path))) throw new Error("Invalid task sandbox protected paths.");
61
63
  if (!Array.isArray(v.tools) || !v.tools.every((name) => typeof name === "string" && name.length > 0)) throw new Error("Invalid task sandbox tool selection.");
62
64
  if (v.applyPatch !== undefined && typeof v.applyPatch !== "boolean") throw new Error("Invalid task sandbox apply_patch setting.");
@@ -71,6 +73,7 @@ export function parseTaskPolicy(value: unknown): TaskPolicy {
71
73
  permissions: Object.freeze({
72
74
  projectFiles: p.projectFiles, outsideProject: p.outsideProject, storedCredentials: p.storedCredentials,
73
75
  commands: p.commands, network: p.network,
76
+ processAccess: p.processAccess ?? "off",
74
77
  } as SandboxPermissions),
75
78
  denyWrite: Object.freeze([...v.denyWrite]), tools: Object.freeze([...v.tools]),
76
79
  applyPatch: v.applyPatch === true,