pi-landstrip 0.16.22 → 0.16.24
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/index.ts +73 -12
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -375,6 +375,47 @@ function normalizeBlockedPath(path: string, cwd: string): string {
|
|
|
375
375
|
return canonicalizePath(isAbsolute(path) ? path : join(cwd, path), cwd);
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
// Breadth-first filesystem approval: when the user allows a blocked read/write,
|
|
379
|
+
// approve the broadest reasonable ancestor (e.g. `~/.cargo`, not each subcrate
|
|
380
|
+
// file) so a single scan does not spawn one prompt per file. matchesPattern
|
|
381
|
+
// already treats a bare directory entry as covering everything beneath it, so
|
|
382
|
+
// storing the scope is enough for sibling files to auto-allow.
|
|
383
|
+
function pathUnderDirectory(filePath: string, dir: string): boolean {
|
|
384
|
+
if (filePath === dir) return true;
|
|
385
|
+
const sep = dir.endsWith('/') ? '' : '/';
|
|
386
|
+
return filePath.startsWith(dir + sep);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// The broadest ancestor worth approving in one action: the immediate child of
|
|
390
|
+
// `$HOME` (e.g. `~/.cargo`) for paths under the user's home, the project root
|
|
391
|
+
// for paths under it, otherwise the containing directory. When the file sits
|
|
392
|
+
// directly on a boundary (so the only ancestor is `$HOME` itself, which would
|
|
393
|
+
// over-broaden), fall back to the exact file so nothing widens silently.
|
|
394
|
+
export function sessionScopeFor(filePath: string, baseDirectory: string): string {
|
|
395
|
+
const dir = dirname(filePath);
|
|
396
|
+
const home = homedir();
|
|
397
|
+
const boundaries = new Set<string>();
|
|
398
|
+
if (home) boundaries.add(home);
|
|
399
|
+
try {
|
|
400
|
+
const realHome = realpathSync.native(home);
|
|
401
|
+
if (realHome) boundaries.add(realHome);
|
|
402
|
+
} catch {
|
|
403
|
+
// $HOME not resolvable — fall back to the raw value only.
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
for (const boundary of boundaries) {
|
|
407
|
+
if (pathUnderDirectory(dir, boundary)) {
|
|
408
|
+
const rest = dir.slice(boundary.length).replace(/^\/+/, '');
|
|
409
|
+
const first = rest.split('/')[0];
|
|
410
|
+
if (!first) return filePath;
|
|
411
|
+
return boundary.endsWith('/') ? boundary + first : `${boundary}/${first}`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (pathUnderDirectory(dir, baseDirectory)) return baseDirectory;
|
|
416
|
+
return dir;
|
|
417
|
+
}
|
|
418
|
+
|
|
378
419
|
function isPathLike(value: string): boolean {
|
|
379
420
|
const trimmed = value.trim();
|
|
380
421
|
return (
|
|
@@ -933,26 +974,46 @@ export function createLandstripIntegration(
|
|
|
933
974
|
if (choice === 'global') await addDomainToConfig(globalPath, domain);
|
|
934
975
|
}
|
|
935
976
|
|
|
977
|
+
// Breadth-first: approve the broadest reasonable ancestor of the blocked file
|
|
978
|
+
// (see sessionScopeFor) instead of the exact path, so the still-running
|
|
979
|
+
// command stops prompting for sibling files under the same tree. When the
|
|
980
|
+
// scope widens beyond the file, tell the user what was actually granted.
|
|
981
|
+
function noteScope(
|
|
982
|
+
ctx: ExtensionContext,
|
|
983
|
+
verb: string,
|
|
984
|
+
choice: Exclude<PermissionChoice, 'abort'>,
|
|
985
|
+
filePath: string,
|
|
986
|
+
scope: string,
|
|
987
|
+
): void {
|
|
988
|
+
if (scope !== filePath) notify(ctx, `${verb} allowed (${choice}): ${scope}`, 'info');
|
|
989
|
+
}
|
|
990
|
+
|
|
936
991
|
async function applyReadChoice(
|
|
992
|
+
ctx: ExtensionContext,
|
|
937
993
|
choice: Exclude<PermissionChoice, 'abort'>,
|
|
938
994
|
filePath: string,
|
|
939
995
|
cwd: string,
|
|
940
996
|
): Promise<void> {
|
|
941
997
|
const { globalPath, projectPath } = getConfigPaths(cwd);
|
|
942
|
-
|
|
943
|
-
if (
|
|
944
|
-
if (choice === '
|
|
998
|
+
const scope = sessionScopeFor(filePath, cwd);
|
|
999
|
+
if (!sessionAllowedReadPaths.includes(scope)) sessionAllowedReadPaths.push(scope);
|
|
1000
|
+
if (choice === 'project') await addReadPathToConfig(projectPath, scope);
|
|
1001
|
+
if (choice === 'global') await addReadPathToConfig(globalPath, scope);
|
|
1002
|
+
noteScope(ctx, 'Read', choice, filePath, scope);
|
|
945
1003
|
}
|
|
946
1004
|
|
|
947
1005
|
async function applyWriteChoice(
|
|
1006
|
+
ctx: ExtensionContext,
|
|
948
1007
|
choice: Exclude<PermissionChoice, 'abort'>,
|
|
949
1008
|
filePath: string,
|
|
950
1009
|
cwd: string,
|
|
951
1010
|
): Promise<void> {
|
|
952
1011
|
const { globalPath, projectPath } = getConfigPaths(cwd);
|
|
953
|
-
|
|
954
|
-
if (
|
|
955
|
-
if (choice === '
|
|
1012
|
+
const scope = sessionScopeFor(filePath, cwd);
|
|
1013
|
+
if (!sessionAllowedWritePaths.includes(scope)) sessionAllowedWritePaths.push(scope);
|
|
1014
|
+
if (choice === 'project') await addWritePathToConfig(projectPath, scope);
|
|
1015
|
+
if (choice === 'global') await addWritePathToConfig(globalPath, scope);
|
|
1016
|
+
noteScope(ctx, 'Write', choice, filePath, scope);
|
|
956
1017
|
}
|
|
957
1018
|
|
|
958
1019
|
async function ensureDomainAllowed(
|
|
@@ -1325,8 +1386,8 @@ export function createLandstripIntegration(
|
|
|
1325
1386
|
respondQuery(queryId, 'deny');
|
|
1326
1387
|
return;
|
|
1327
1388
|
}
|
|
1328
|
-
if (operation === 'read') await applyReadChoice(choice, path, cwd);
|
|
1329
|
-
else await applyWriteChoice(choice, path, cwd);
|
|
1389
|
+
if (operation === 'read') await applyReadChoice(ctx, choice, path, cwd);
|
|
1390
|
+
else await applyWriteChoice(ctx, choice, path, cwd);
|
|
1330
1391
|
respondQuery(queryId, 'allow');
|
|
1331
1392
|
})
|
|
1332
1393
|
.catch(() => respondQuery(queryId, 'deny'));
|
|
@@ -1447,7 +1508,7 @@ export function createLandstripIntegration(
|
|
|
1447
1508
|
if (shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config), ctx.cwd)) {
|
|
1448
1509
|
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1449
1510
|
if (choice === 'abort') return null;
|
|
1450
|
-
await applyWriteChoice(choice, blockedPath, ctx.cwd);
|
|
1511
|
+
await applyWriteChoice(ctx, choice, blockedPath, ctx.cwd);
|
|
1451
1512
|
}
|
|
1452
1513
|
|
|
1453
1514
|
config = loadConfig(ctx.cwd);
|
|
@@ -1486,7 +1547,7 @@ export function createLandstripIntegration(
|
|
|
1486
1547
|
: undefined,
|
|
1487
1548
|
);
|
|
1488
1549
|
if (choice === 'abort') return null;
|
|
1489
|
-
await applyReadChoice(choice, blockedPath, ctx.cwd);
|
|
1550
|
+
await applyReadChoice(ctx, choice, blockedPath, ctx.cwd);
|
|
1490
1551
|
}
|
|
1491
1552
|
|
|
1492
1553
|
onUpdate?.({
|
|
@@ -1732,7 +1793,7 @@ export function createLandstripIntegration(
|
|
|
1732
1793
|
reason: `Sandbox: read access denied for "${filePath}"`,
|
|
1733
1794
|
};
|
|
1734
1795
|
}
|
|
1735
|
-
await applyReadChoice(choice, filePath, ctx.cwd);
|
|
1796
|
+
await applyReadChoice(ctx, choice, filePath, ctx.cwd);
|
|
1736
1797
|
}
|
|
1737
1798
|
}
|
|
1738
1799
|
|
|
@@ -1756,7 +1817,7 @@ export function createLandstripIntegration(
|
|
|
1756
1817
|
reason: `Sandbox: write access denied for "${filePath}" (not in allowWrite)`,
|
|
1757
1818
|
};
|
|
1758
1819
|
}
|
|
1759
|
-
await applyWriteChoice(choice, filePath, ctx.cwd);
|
|
1820
|
+
await applyWriteChoice(ctx, choice, filePath, ctx.cwd);
|
|
1760
1821
|
}
|
|
1761
1822
|
}
|
|
1762
1823
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.24",
|
|
4
4
|
"description": "Landlock-based sandboxing for pi with interactive permission prompts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@earendil-works/pi-tui": "^0.78.0",
|
|
36
|
-
"@landstrip/landstrip": "^0.16.
|
|
36
|
+
"@landstrip/landstrip": "^0.16.20"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@earendil-works/pi-coding-agent": "^0.74.2",
|