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.
Files changed (2) hide show
  1. package/index.ts +41 -62
  2. 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
- function expandPath(filePath: string): string {
324
- return resolve(filePath.replace(/^~(?=$|\/)/, homedir()));
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('*') ? expandPath(pattern) : canonicalizePath(pattern);
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
- function landstripVersion(): string | null {
771
- const result = spawnSync(binaryPath(), ['--version'], { encoding: 'utf-8' });
772
- if (result.status !== 0) return null;
773
- return result.stdout.trim();
774
- }
775
-
776
- function parseVersion(version: string): [number, number, number] | null {
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
- const version = landstripVersion();
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.14",
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
- "all": "npm run fmt && npm run lint && npm run check",
28
- "ci:fmt": "oxfmt --check index.ts package.json tsconfig.json sandbox.json .oxfmtrc.json README.md",
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.15"
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"