pi-better-background-tasks 0.6.1 → 0.6.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-background-tasks",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Pi extension for durable background shell tasks, watchers, logs, and status inspection.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -228,10 +228,7 @@ const CREDENTIAL_LOCATIONS = [
228
228
  ".docker/config.json", ".npmrc", ".netrc", ".git-credentials", ".pi/agent/auth.json",
229
229
  ] as const;
230
230
 
231
- /**
232
- * Credential stores added by the broad-write profile. They are never readable
233
- * or writable there, whatever the Stored credentials row says.
234
- */
231
+ /** Credential stores added to the Stored credentials row by the broad-write profile. */
235
232
  const BROAD_CREDENTIAL_LOCATIONS = [
236
233
  "Library/Keychains", ".claude/.credentials.json", ".claude.json", ".gnupg",
237
234
  ".codex/auth.json", ".cargo/credentials.toml", ".cargo/credentials", ".pgpass", ".config/rclone",
@@ -386,10 +383,7 @@ function compile(
386
383
  return {
387
384
  writableRoot, denyWrite, home: policy.home,
388
385
  ...(policy.permissions && {
389
- // The broad-write profile's credential deny list is not optional.
390
- permissions: broadMode
391
- ? { ...policy.permissions, storedCredentials: "off" as const }
392
- : { ...policy.permissions },
386
+ permissions: { ...policy.permissions },
393
387
  credentialPaths: broadMode ? broadCredentialPaths(policy.home, seams) : credentialFilePaths(policy.home, seams),
394
388
  compatibilityWrite,
395
389
  runtimeWrite,
@@ -598,7 +592,7 @@ export function evaluateReadAccess(
598
592
  const permissions = policy.permissions;
599
593
  if (!permissions) return { allowed: true, path };
600
594
  if (policy.broad) {
601
- const denied = isCredential(path, policy) ||
595
+ const denied = (isCredential(path, policy) && permissions.storedCredentials === "off") ||
602
596
  (permissions.projectFiles === "off" && contains(policy.writableRoot, path));
603
597
  return denied ? { allowed: false, path, reason: "read-denied" } : { allowed: true, path };
604
598
  }
@@ -652,7 +646,8 @@ function evaluateBroadWrite(
652
646
  for (const denied of [...policy.denyWrite, ...broad.codePaths]) {
653
647
  if (contains(denied, path)) return { allowed: false, path, reason: "write-denied", deniedBy: denied };
654
648
  }
655
- if (isCredential(path, policy)) return { allowed: false, path, reason: "permission-denied" };
649
+ if (isCredential(path, policy)) return canWrite(policy.permissions!.storedCredentials)
650
+ ? { allowed: true, path } : { allowed: false, path, reason: "permission-denied" };
656
651
  if (policy.runtimeWrite?.some((root) => contains(root, path))) return { allowed: true, path };
657
652
  if (contains(policy.writableRoot, path)) {
658
653
  return canWrite(policy.permissions!.projectFiles)
@@ -808,7 +803,8 @@ export function resetRecoverySnapshotClock(): void {
808
803
  }
809
804
 
810
805
  function broadProtectedPaths(policy: CompiledSandboxWritePolicy, broad: CompiledBroadWrite): string[] {
811
- return [...policy.denyWrite, ...(policy.credentialPaths ?? []), ...broad.codePaths,
806
+ return [...policy.denyWrite,
807
+ ...(policy.permissions?.storedCredentials === "read-write" ? [] : policy.credentialPaths ?? []), ...broad.codePaths,
812
808
  ...(!canWrite(policy.permissions?.projectFiles) ? [policy.writableRoot] : [])];
813
809
  }
814
810
 
@@ -927,7 +923,7 @@ function buildBroadProfile(policy: CompiledSandboxWritePolicy, seams: SandboxSea
927
923
  `(allow file-write-unlink (regex #"^${home}/(.+/)?[.]worktrees/."))`,
928
924
  `(allow file-write-unlink (regex #"^${home}/(.+/)?[^/]+-worktrees/."))`,
929
925
  );
930
- // The fixed deny list overrides every grant above. A rule naming
926
+ // Write-denied controls and credentials override every broad grant above. A rule naming
931
927
  // `file-write-unlink` outranks a `file-write*` wildcard whatever their
932
928
  // order (verified on the real kernel), so each protected path also gets an
933
929
  // explicit unlink denial; otherwise the removal grants above would reopen it.
@@ -936,8 +932,8 @@ function buildBroadProfile(policy: CompiledSandboxWritePolicy, seams: SandboxSea
936
932
  if (!canWrite(permissions.projectFiles)) denyWrite(project);
937
933
  for (const path of broad.codePaths) denyWrite(path);
938
934
  for (const path of policy.credentialPaths ?? []) {
939
- rules.push(`(deny file-read* (subpath ${sbpl(path)}))`);
940
- denyWrite(path);
935
+ if (permissions.storedCredentials === "off") rules.push(`(deny file-read* (subpath ${sbpl(path)}))`);
936
+ if (permissions.storedCredentials !== "read-write") denyWrite(path);
941
937
  }
942
938
  for (const path of policy.denyWrite) denyWrite(path);
943
939
  // Renaming an ancestor would move a protected subtree to an unprotected path,
@@ -1350,9 +1346,11 @@ function maskSources(profilePath: string): { directory: string; file: string } {
1350
1346
  * the root is read-only; temp, the workspace, hidden home entries and worktree
1351
1347
  * folders are writable (removal included); ordinary top-level home folders and
1352
1348
  * home itself stay read-only, so sibling repositories cannot even be edited in
1353
- * place there. Credentials are masked by empty mode-000 mounts; code that runs
1354
- * later and caller-denied paths are read-only binds. Every writable bind and
1355
- * protected leaf is a mount point, which Linux refuses to rename. A protected
1349
+ * place there. Off credentials are masked by empty mode-000 mounts, Read
1350
+ * credentials are read-only binds, and Read / write credentials follow the
1351
+ * surrounding broad-write rules. Code that runs later and caller-denied paths
1352
+ * are read-only binds. Every writable bind and protected leaf is a mount point,
1353
+ * which Linux refuses to rename. A protected
1356
1354
  * symlink inside a writable directory (a stow link in `~/.config`) makes that
1357
1355
  * directory read-only with each existing entry bound writable again, so only
1358
1356
  * new top-level entries there are refused.
@@ -1373,14 +1371,15 @@ function buildLinuxBroadCommand(
1373
1371
  throw new Error("Linux bubblewrap cannot separate removal from writing for Project files; use Write & delete or Read.");
1374
1372
  }
1375
1373
  const credentials = policy.credentialPaths ?? [];
1374
+ const protectedCredentials = permissions.storedCredentials === "read-write" ? [] : credentials;
1376
1375
  const writeDenied = [...broad.codePaths, ...policy.denyWrite];
1377
1376
  if (permissions.projectFiles === "read-write" &&
1378
- [...credentials, ...writeDenied].some((path) => path !== project && contains(path, project))) {
1377
+ [...protectedCredentials, ...writeDenied].some((path) => path !== project && contains(path, project))) {
1379
1378
  throw new Error("Linux bubblewrap cannot make a project writable inside a protected directory.");
1380
1379
  }
1381
1380
  const list = seams.listDirectory ?? systemListDirectory;
1382
1381
  const exists = (path: string) => (seams.pathExists ?? existsSync)(path);
1383
- const protectedPaths = [...credentials, ...writeDenied];
1382
+ const protectedPaths = [...protectedCredentials, ...writeDenied];
1384
1383
  const insideProtected = (path: string) => protectedPaths.some((guard) => contains(guard, path));
1385
1384
 
1386
1385
  const writable = new Set<string>();
@@ -1498,7 +1497,10 @@ function buildLinuxBroadCommand(
1498
1497
  readOnly.add(path);
1499
1498
  }
1500
1499
  };
1501
- for (const path of credentials) guard(path, true);
1500
+ for (const path of credentials) {
1501
+ if (permissions.storedCredentials === "off") guard(path, true);
1502
+ else if (permissions.storedCredentials === "read") guard(path, false);
1503
+ }
1502
1504
  for (const path of writeDenied) guard(path, false);
1503
1505
  if (permissions.projectFiles === "read") readOnly.add(project);
1504
1506