pi-better-background-tasks 0.6.1 → 0.6.3
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 +1 -1
- package/src/shared-navigator.ts +22 -17
- package/src/shared-sandbox-core.ts +22 -20
package/package.json
CHANGED
package/src/shared-navigator.ts
CHANGED
|
@@ -66,6 +66,8 @@ export type BackgroundWorkProvider = {
|
|
|
66
66
|
detail(id: string, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null;
|
|
67
67
|
armCloseLabel(row: BackgroundWorkRow): string;
|
|
68
68
|
close(id: string): BackgroundWorkCloseOutcome;
|
|
69
|
+
/** Return true when this detail handled the key. The navigator refreshes the sheet. */
|
|
70
|
+
handleDetailInput?(id: string, data: string): boolean;
|
|
69
71
|
onVisibleChanged?(notify: () => void): () => void;
|
|
70
72
|
};
|
|
71
73
|
|
|
@@ -559,7 +561,7 @@ function rowSummary(row: InternalRow): string {
|
|
|
559
561
|
const parts: string[] = [];
|
|
560
562
|
if (row.statusTone !== "running" && row.kind !== "main agent") parts.push(singleLine(row.status));
|
|
561
563
|
if (row.model) parts.push(row.effort ? `${singleLine(row.model)} ${singleLine(row.effort)}` : singleLine(row.model));
|
|
562
|
-
if (row.tool) parts.push(
|
|
564
|
+
if (row.tool) parts.push(singleLine(row.tool));
|
|
563
565
|
if (row.tokens) parts.push(singleLine(row.tokens));
|
|
564
566
|
else if (row.primary) parts.push(singleLine(row.primary));
|
|
565
567
|
if (parts.length) return parts.join(" · ");
|
|
@@ -703,12 +705,7 @@ function handleMainListInput(data: string, deps: HostDeps): boolean {
|
|
|
703
705
|
return true;
|
|
704
706
|
}
|
|
705
707
|
if (deps.matchKey(data, "enter")) {
|
|
706
|
-
|
|
707
|
-
if (selected?.parentRow) {
|
|
708
|
-
unfocusMainList();
|
|
709
|
-
dismissOverlay();
|
|
710
|
-
}
|
|
711
|
-
else openNavigator();
|
|
708
|
+
openNavigator();
|
|
712
709
|
return true;
|
|
713
710
|
}
|
|
714
711
|
if (data === "x" || data === "X" || deps.matchKey(data, "x") || deps.matchKey(data, "X")) {
|
|
@@ -851,7 +848,8 @@ function createOverlayComponent(
|
|
|
851
848
|
let logTailRows: number = DEFAULT_LOG_TAIL_ROWS;
|
|
852
849
|
const expandedSections = new Set<string>();
|
|
853
850
|
let detailId: string = initialDetailId;
|
|
854
|
-
|
|
851
|
+
const initialRow = initialRows.find((row) => row.navigatorId === detailId);
|
|
852
|
+
let detail: BackgroundWorkDetail | null = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? (initialRow ? fallbackDetail(initialRow) : null);
|
|
855
853
|
applyDefaultExpandedSections(detail, expandedSections);
|
|
856
854
|
{
|
|
857
855
|
const idx = overlayState.rows.findIndex((row) => row.navigatorId === detailId);
|
|
@@ -891,8 +889,7 @@ function createOverlayComponent(
|
|
|
891
889
|
}
|
|
892
890
|
|
|
893
891
|
function activateSelectedRow(): void {
|
|
894
|
-
|
|
895
|
-
else openDetail();
|
|
892
|
+
openDetail();
|
|
896
893
|
}
|
|
897
894
|
|
|
898
895
|
function clearCloseArm(): void {
|
|
@@ -922,10 +919,6 @@ function createOverlayComponent(
|
|
|
922
919
|
function openDetail(): void {
|
|
923
920
|
const row = selectedRow();
|
|
924
921
|
if (!row) return;
|
|
925
|
-
if (row.parentRow) {
|
|
926
|
-
close();
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
922
|
clearCloseArm();
|
|
930
923
|
detailId = row.navigatorId;
|
|
931
924
|
expandedSections.clear();
|
|
@@ -1073,6 +1066,15 @@ function createOverlayComponent(
|
|
|
1073
1066
|
requestRender();
|
|
1074
1067
|
}
|
|
1075
1068
|
}
|
|
1069
|
+
else if (data === "m" || data === "M" || data === "\x13" || deps.matchKey(data, "ctrl+s")) {
|
|
1070
|
+
const row = selectedRow();
|
|
1071
|
+
const provider = row ? state().providers.get(row.providerId) : undefined;
|
|
1072
|
+
const key = data === "m" || data === "M" ? "m" : "ctrl+s";
|
|
1073
|
+
if (!row || !provider?.handleDetailInput?.(row.id, key)) return;
|
|
1074
|
+
detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
|
|
1075
|
+
refreshMainListWidget();
|
|
1076
|
+
requestRender();
|
|
1077
|
+
}
|
|
1076
1078
|
else if (data === "l" || data === "L") {
|
|
1077
1079
|
logTailRows = cycleLogTailRows(logTailRows);
|
|
1078
1080
|
if (detailId) detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
|
|
@@ -1091,8 +1093,7 @@ function createOverlayComponent(
|
|
|
1091
1093
|
if (idx < 0) return;
|
|
1092
1094
|
overlayState.selected = idx;
|
|
1093
1095
|
state().mainListSelectedId = navigatorId;
|
|
1094
|
-
|
|
1095
|
-
else openDetail();
|
|
1096
|
+
openDetail();
|
|
1096
1097
|
},
|
|
1097
1098
|
/** Close the overlay (done), e.g. when the navigator is disposed. */
|
|
1098
1099
|
dismiss() { close(); },
|
|
@@ -1150,7 +1151,11 @@ function buildTranscriptDetailLines(
|
|
|
1150
1151
|
...detail.metadata.map((item) => ` ${item.label.padEnd(8, " ").slice(0, 8)} ${item.value}`),
|
|
1151
1152
|
];
|
|
1152
1153
|
const transcriptHeader = ["", dim(section(`transcript · latest ${tailRows} rows`, width), fg)];
|
|
1153
|
-
if (detail.transcriptDiagnostic)
|
|
1154
|
+
if (detail.transcriptDiagnostic) {
|
|
1155
|
+
for (const diagnosticRow of detail.transcriptDiagnostic.split(/\r\n|\r|\n/)) {
|
|
1156
|
+
transcriptHeader.push(` ${dim(diagnosticRow, fg)}`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1154
1159
|
// Metadata always renders; the transcript tail takes only the rows left over (the tail size is a cap, not a guarantee).
|
|
1155
1160
|
lines.push(...optionalMetadata);
|
|
1156
1161
|
if (options.maxRows !== undefined) {
|
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
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.
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
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
|
-
[...
|
|
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 = [...
|
|
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)
|
|
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
|
|