pi-landstrip 0.16.14 → 0.16.16
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 +41 -62
- package/package.json +11 -8
package/index.ts
CHANGED
|
@@ -105,8 +105,6 @@ interface LandstripBashCallbacks {
|
|
|
105
105
|
promptOnBlock?: boolean;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
const LANDSTRIP_VERSION = [0, 16, 4] as const;
|
|
109
|
-
const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
|
|
110
108
|
const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
|
|
111
109
|
|
|
112
110
|
const packageDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -316,16 +314,21 @@ function allowsAllDomains(allowedDomains: string[]): boolean {
|
|
|
316
314
|
return allowedDomains.includes('*');
|
|
317
315
|
}
|
|
318
316
|
|
|
319
|
-
function shouldPromptForWrite(path: string, allowWrite: string[]): boolean {
|
|
320
|
-
return allowWrite.length === 0 || !matchesPattern(path, allowWrite);
|
|
317
|
+
export function shouldPromptForWrite(path: string, allowWrite: string[], cwd: string): boolean {
|
|
318
|
+
return allowWrite.length === 0 || !matchesPattern(path, allowWrite, cwd);
|
|
321
319
|
}
|
|
322
320
|
|
|
323
|
-
|
|
324
|
-
|
|
321
|
+
// Relative entries (notably ".") resolve against `cwd` — the command's working
|
|
322
|
+
// directory that landstrip itself uses as its policy base — not the extension
|
|
323
|
+
// process's own cwd. Resolving against process.cwd() would let the broker's
|
|
324
|
+
// allow/deny decision diverge from landstrip's whenever the agent operates
|
|
325
|
+
// outside the directory pi was launched from.
|
|
326
|
+
function expandPath(filePath: string, cwd: string): string {
|
|
327
|
+
return resolve(cwd, filePath.replace(/^~(?=$|\/)/, homedir()));
|
|
325
328
|
}
|
|
326
329
|
|
|
327
|
-
function canonicalizePath(filePath: string): string {
|
|
328
|
-
const abs = expandPath(filePath);
|
|
330
|
+
function canonicalizePath(filePath: string, cwd: string): string {
|
|
331
|
+
const abs = expandPath(filePath, cwd);
|
|
329
332
|
|
|
330
333
|
try {
|
|
331
334
|
return realpathSync.native(abs);
|
|
@@ -348,11 +351,13 @@ function canonicalizePath(filePath: string): string {
|
|
|
348
351
|
}
|
|
349
352
|
}
|
|
350
353
|
|
|
351
|
-
function matchesPattern(filePath: string, patterns: string[]): boolean {
|
|
352
|
-
const abs = canonicalizePath(filePath);
|
|
354
|
+
export function matchesPattern(filePath: string, patterns: string[], cwd: string): boolean {
|
|
355
|
+
const abs = canonicalizePath(filePath, cwd);
|
|
353
356
|
|
|
354
357
|
return patterns.some((pattern) => {
|
|
355
|
-
const absPattern = pattern.includes('*')
|
|
358
|
+
const absPattern = pattern.includes('*')
|
|
359
|
+
? expandPath(pattern, cwd)
|
|
360
|
+
: canonicalizePath(pattern, cwd);
|
|
356
361
|
|
|
357
362
|
if (pattern.includes('*')) {
|
|
358
363
|
const escaped = absPattern
|
|
@@ -367,7 +372,7 @@ function matchesPattern(filePath: string, patterns: string[]): boolean {
|
|
|
367
372
|
}
|
|
368
373
|
|
|
369
374
|
function normalizeBlockedPath(path: string, cwd: string): string {
|
|
370
|
-
return canonicalizePath(isAbsolute(path) ? path : join(cwd, path));
|
|
375
|
+
return canonicalizePath(isAbsolute(path) ? path : join(cwd, path), cwd);
|
|
371
376
|
}
|
|
372
377
|
|
|
373
378
|
function isPathLike(value: string): boolean {
|
|
@@ -767,28 +772,14 @@ function promptWriteBlock(ctx: ExtensionContext, filePath: string): Promise<Perm
|
|
|
767
772
|
);
|
|
768
773
|
}
|
|
769
774
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const match = version.match(/\b(\d+)\.(\d+)\.(\d+)\b/);
|
|
778
|
-
if (!match) return null;
|
|
779
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function hasMinimumVersion(version: string, minimum: readonly [number, number, number]): boolean {
|
|
783
|
-
const parsed = parseVersion(version);
|
|
784
|
-
if (!parsed) return false;
|
|
785
|
-
|
|
786
|
-
for (let i = 0; i < minimum.length; i++) {
|
|
787
|
-
if (parsed[i] > minimum[i]) return true;
|
|
788
|
-
if (parsed[i] < minimum[i]) return false;
|
|
775
|
+
// The binary is bundled and version-locked to @landstrip/landstrip via npm, so
|
|
776
|
+
// compatibility is settled at install time; only confirm it is runnable here.
|
|
777
|
+
function landstripAvailable(): boolean {
|
|
778
|
+
try {
|
|
779
|
+
return spawnSync(binaryPath(), ['--version']).status === 0;
|
|
780
|
+
} catch {
|
|
781
|
+
return false;
|
|
789
782
|
}
|
|
790
|
-
|
|
791
|
-
return true;
|
|
792
783
|
}
|
|
793
784
|
|
|
794
785
|
function proxyEnv(env: NodeJS.ProcessEnv | undefined, port: number): NodeJS.ProcessEnv {
|
|
@@ -1264,10 +1255,10 @@ export function createLandstripIntegration(
|
|
|
1264
1255
|
const config = loadConfig(cwd);
|
|
1265
1256
|
const isAllowed = (cfg: SandboxConfig): boolean =>
|
|
1266
1257
|
operation === 'read'
|
|
1267
|
-
? !matchesPattern(path, cfg.filesystem.denyRead) &&
|
|
1268
|
-
matchesPattern(path, getEffectiveAllowRead(cfg))
|
|
1269
|
-
: !matchesPattern(path, cfg.filesystem.denyWrite) &&
|
|
1270
|
-
!shouldPromptForWrite(path, getEffectiveAllowWrite(cfg));
|
|
1258
|
+
? !matchesPattern(path, cfg.filesystem.denyRead, cwd) &&
|
|
1259
|
+
matchesPattern(path, getEffectiveAllowRead(cfg), cwd)
|
|
1260
|
+
: !matchesPattern(path, cfg.filesystem.denyWrite, cwd) &&
|
|
1261
|
+
!shouldPromptForWrite(path, getEffectiveAllowWrite(cfg), cwd);
|
|
1271
1262
|
|
|
1272
1263
|
if (isAllowed(config)) {
|
|
1273
1264
|
respondQuery(queryId, 'allow');
|
|
@@ -1280,7 +1271,7 @@ export function createLandstripIntegration(
|
|
|
1280
1271
|
return;
|
|
1281
1272
|
}
|
|
1282
1273
|
// denyWrite is a hard block: never prompt to override it.
|
|
1283
|
-
if (operation === 'write' && matchesPattern(path, config.filesystem.denyWrite)) {
|
|
1274
|
+
if (operation === 'write' && matchesPattern(path, config.filesystem.denyWrite, cwd)) {
|
|
1284
1275
|
respondQuery(queryId, 'deny');
|
|
1285
1276
|
return;
|
|
1286
1277
|
}
|
|
@@ -1298,7 +1289,7 @@ export function createLandstripIntegration(
|
|
|
1298
1289
|
? await promptReadBlock(
|
|
1299
1290
|
ctx,
|
|
1300
1291
|
path,
|
|
1301
|
-
matchesPattern(path, cfg.filesystem.denyRead)
|
|
1292
|
+
matchesPattern(path, cfg.filesystem.denyRead, cwd)
|
|
1302
1293
|
? 'granting allowRead will override it'
|
|
1303
1294
|
: undefined,
|
|
1304
1295
|
)
|
|
@@ -1416,7 +1407,7 @@ export function createLandstripIntegration(
|
|
|
1416
1407
|
|
|
1417
1408
|
let config = loadConfig(ctx.cwd);
|
|
1418
1409
|
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
1419
|
-
if (matchesPattern(blockedPath, config.filesystem.denyWrite)) {
|
|
1410
|
+
if (matchesPattern(blockedPath, config.filesystem.denyWrite, ctx.cwd)) {
|
|
1420
1411
|
notify(
|
|
1421
1412
|
ctx,
|
|
1422
1413
|
`"${blockedPath}" is blocked by denyWrite. Check:\n ${projectPath}\n ${globalPath}`,
|
|
@@ -1425,14 +1416,14 @@ export function createLandstripIntegration(
|
|
|
1425
1416
|
return null;
|
|
1426
1417
|
}
|
|
1427
1418
|
|
|
1428
|
-
if (shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config))) {
|
|
1419
|
+
if (shouldPromptForWrite(blockedPath, getEffectiveAllowWrite(config), ctx.cwd)) {
|
|
1429
1420
|
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1430
1421
|
if (choice === 'abort') return null;
|
|
1431
1422
|
await applyWriteChoice(choice, blockedPath, ctx.cwd);
|
|
1432
1423
|
}
|
|
1433
1424
|
|
|
1434
1425
|
config = loadConfig(ctx.cwd);
|
|
1435
|
-
if (matchesPattern(blockedPath, config.filesystem.denyWrite)) {
|
|
1426
|
+
if (matchesPattern(blockedPath, config.filesystem.denyWrite, ctx.cwd)) {
|
|
1436
1427
|
notify(
|
|
1437
1428
|
ctx,
|
|
1438
1429
|
`"${blockedPath}" was added to allowWrite, but denyWrite still blocks it. Check:\n ${projectPath}\n ${globalPath}`,
|
|
@@ -1458,11 +1449,11 @@ export function createLandstripIntegration(
|
|
|
1458
1449
|
if (!ctx.hasUI) return null;
|
|
1459
1450
|
|
|
1460
1451
|
const config = loadConfig(ctx.cwd);
|
|
1461
|
-
if (!matchesPattern(blockedPath, getEffectiveAllowRead(config))) {
|
|
1452
|
+
if (!matchesPattern(blockedPath, getEffectiveAllowRead(config), ctx.cwd)) {
|
|
1462
1453
|
const choice = await promptReadBlock(
|
|
1463
1454
|
ctx,
|
|
1464
1455
|
blockedPath,
|
|
1465
|
-
matchesPattern(blockedPath, config.filesystem.denyRead)
|
|
1456
|
+
matchesPattern(blockedPath, config.filesystem.denyRead, ctx.cwd)
|
|
1466
1457
|
? 'granting allowRead will override it'
|
|
1467
1458
|
: undefined,
|
|
1468
1459
|
);
|
|
@@ -1591,8 +1582,7 @@ export function createLandstripIntegration(
|
|
|
1591
1582
|
return false;
|
|
1592
1583
|
}
|
|
1593
1584
|
|
|
1594
|
-
|
|
1595
|
-
if (!version) {
|
|
1585
|
+
if (!landstripAvailable()) {
|
|
1596
1586
|
sandboxEnabled = false;
|
|
1597
1587
|
sandboxReady = false;
|
|
1598
1588
|
notify(
|
|
@@ -1603,17 +1593,6 @@ export function createLandstripIntegration(
|
|
|
1603
1593
|
return false;
|
|
1604
1594
|
}
|
|
1605
1595
|
|
|
1606
|
-
if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
|
|
1607
|
-
sandboxEnabled = false;
|
|
1608
|
-
sandboxReady = false;
|
|
1609
|
-
notify(
|
|
1610
|
-
ctx,
|
|
1611
|
-
`landstrip ${REQUIRED_LANDSTRIP_VERSION} or newer is required; found: ${version}`,
|
|
1612
|
-
'error',
|
|
1613
|
-
);
|
|
1614
|
-
return false;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
1596
|
sandboxEnabled = true;
|
|
1618
1597
|
sandboxReady = true;
|
|
1619
1598
|
warnIfAllDomainsAllowed(ctx, config);
|
|
@@ -1716,8 +1695,8 @@ export function createLandstripIntegration(
|
|
|
1716
1695
|
}
|
|
1717
1696
|
|
|
1718
1697
|
if (isToolCallEventType('read', event)) {
|
|
1719
|
-
const filePath = canonicalizePath(event.input.path);
|
|
1720
|
-
if (!matchesPattern(filePath, getEffectiveAllowRead(config))) {
|
|
1698
|
+
const filePath = canonicalizePath(event.input.path, ctx.cwd);
|
|
1699
|
+
if (!matchesPattern(filePath, getEffectiveAllowRead(config), ctx.cwd)) {
|
|
1721
1700
|
const choice = await promptReadBlock(ctx, filePath);
|
|
1722
1701
|
if (choice === 'abort') {
|
|
1723
1702
|
return {
|
|
@@ -1730,9 +1709,9 @@ export function createLandstripIntegration(
|
|
|
1730
1709
|
}
|
|
1731
1710
|
|
|
1732
1711
|
if (isToolCallEventType('write', event) || isToolCallEventType('edit', event)) {
|
|
1733
|
-
const filePath = canonicalizePath((event.input as { path: string }).path);
|
|
1712
|
+
const filePath = canonicalizePath((event.input as { path: string }).path, ctx.cwd);
|
|
1734
1713
|
|
|
1735
|
-
if (matchesPattern(filePath, config.filesystem.denyWrite)) {
|
|
1714
|
+
if (matchesPattern(filePath, config.filesystem.denyWrite, ctx.cwd)) {
|
|
1736
1715
|
return {
|
|
1737
1716
|
block: true,
|
|
1738
1717
|
reason:
|
|
@@ -1741,7 +1720,7 @@ export function createLandstripIntegration(
|
|
|
1741
1720
|
};
|
|
1742
1721
|
}
|
|
1743
1722
|
|
|
1744
|
-
if (shouldPromptForWrite(filePath, getEffectiveAllowWrite(config))) {
|
|
1723
|
+
if (shouldPromptForWrite(filePath, getEffectiveAllowWrite(config), ctx.cwd)) {
|
|
1745
1724
|
const choice = await promptWriteBlock(ctx, filePath);
|
|
1746
1725
|
if (choice === 'abort') {
|
|
1747
1726
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.16",
|
|
4
4
|
"description": "Landlock-based sandboxing for pi with interactive permission prompts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landstrip",
|
|
@@ -21,24 +21,27 @@
|
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
23
|
"scripts": {
|
|
24
|
-
"fmt": "oxfmt index.ts package.json tsconfig.json sandbox.json .oxfmtrc.json README.md",
|
|
25
|
-
"lint": "oxlint index.ts",
|
|
24
|
+
"fmt": "oxfmt index.ts index.test.ts package.json tsconfig.json sandbox.json .oxfmtrc.json README.md",
|
|
25
|
+
"lint": "oxlint index.ts index.test.ts",
|
|
26
26
|
"check": "tsc --noEmit",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"all": "npm run fmt && npm run lint && npm run check && npm run test",
|
|
29
|
+
"ci:fmt": "oxfmt --check index.ts index.test.ts package.json tsconfig.json sandbox.json .oxfmtrc.json README.md",
|
|
29
30
|
"ci:lint": "npm run lint",
|
|
30
|
-
"ci:check": "npm run check"
|
|
31
|
+
"ci:check": "npm run check",
|
|
32
|
+
"ci:test": "npm run test"
|
|
31
33
|
},
|
|
32
34
|
"dependencies": {
|
|
33
35
|
"@earendil-works/pi-tui": "^0.78.0",
|
|
34
|
-
"@landstrip/landstrip": "^0.16.
|
|
36
|
+
"@landstrip/landstrip": "^0.16.16"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@earendil-works/pi-coding-agent": "^0.74.2",
|
|
38
40
|
"@types/node": "^24.0.0",
|
|
39
41
|
"oxfmt": "^0.53.0",
|
|
40
42
|
"oxlint": "^1.68.0",
|
|
41
|
-
"typescript": "^5.0.0"
|
|
43
|
+
"typescript": "^5.0.0",
|
|
44
|
+
"vitest": "^4.1.9"
|
|
42
45
|
},
|
|
43
46
|
"peerDependencies": {
|
|
44
47
|
"@earendil-works/pi-coding-agent": "^0.74.2"
|