pi-landstrip 0.12.0 → 0.14.0

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ![pi-landstrip screenshot](screenshot.png)
4
4
 
5
5
  Landlock-based sandboxing for [pi](https://pi.dev/) using
6
- [`landstrip`](https://github.com/jarkkojs/landstrip).
6
+ [`landstrip`](https://github.com/landstrip/landstrip).
7
7
 
8
8
  ## Install
9
9
 
@@ -11,7 +11,7 @@ Landlock-based sandboxing for [pi](https://pi.dev/) using
11
11
  pi install npm:pi-landstrip
12
12
  ```
13
13
 
14
- This installs `pi-landstrip` and its `@jarkkojs/landstrip` dependency, which
14
+ This installs `pi-landstrip` and its `@landstrip/landstrip` dependency, which
15
15
  includes platform-specific native binaries for Linux, macOS, and Windows.
16
16
 
17
17
  On unsupported platforms the extension loads but leaves sandboxing disabled.
@@ -43,5 +43,5 @@ Use `/sandbox` inside Pi to show the active config and toggle sandboxing.
43
43
  `pi-landstrip` is licensed under `MIT`. See [LICENSE](LICENSE) for more
44
44
  information.
45
45
 
46
- The bundled `@jarkkojs/landstrip` package is licensed under
46
+ The bundled `@landstrip/landstrip` package is licensed under
47
47
  `Apache-2.0 AND LGPL-2.1-or-later`.
package/index.ts CHANGED
@@ -12,7 +12,7 @@ import type {
12
12
  ExtensionContext,
13
13
  } from '@earendil-works/pi-coding-agent';
14
14
 
15
- import { binaryPath } from '@jarkkojs/landstrip';
15
+ import { binaryPath } from '@landstrip/landstrip';
16
16
 
17
17
  import { spawn, spawnSync } from 'node:child_process';
18
18
  import {
@@ -76,42 +76,22 @@ interface LandstripPolicy {
76
76
  filesystem: SandboxFilesystemConfig;
77
77
  }
78
78
 
79
- type LandstripErrorReason = 'Other' | 'AccessDenied' | 'LaunchFailed' | 'SetupFailed' | 'Usage';
80
79
  type LandstripOperation = 'read' | 'write';
81
- type LandstripErrorType = 'filesystem' | 'network' | 'platform' | 'launch' | 'encoding';
82
-
83
- interface LandstripErrorResponse {
84
- reason: LandstripErrorReason;
85
- file?: string;
86
- operation?: LandstripOperation;
87
- program?: string;
88
- type?: LandstripErrorType;
89
- source?: string;
90
- mechanism?: string;
91
- }
80
+
81
+ type LandstripTrap =
82
+ | { kind: 'filesystem'; operation: LandstripOperation; file: string; mechanism: string }
83
+ | { kind: 'network'; operation: string; target: string; mechanism: string }
84
+ | { kind: 'launch'; program: string; source: string }
85
+ | { kind: 'usage'; message: string }
86
+ | { kind: 'internal'; detail: Record<string, string> };
92
87
 
93
88
  interface LandstripBashCallbacks {
94
89
  onStderr?: (data: Buffer) => void;
95
90
  onErrorFd?: (data: Buffer) => void;
96
91
  }
97
92
 
98
- const LANDSTRIP_VERSION = [0, 12, 2] as const;
93
+ const LANDSTRIP_VERSION = [0, 14, 2] as const;
99
94
  const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
100
- const LANDSTRIP_ERROR_REASONS = new Set<LandstripErrorReason>([
101
- 'Other',
102
- 'AccessDenied',
103
- 'LaunchFailed',
104
- 'SetupFailed',
105
- 'Usage',
106
- ]);
107
- const LANDSTRIP_OPERATIONS = new Set<LandstripOperation>(['read', 'write']);
108
- const LANDSTRIP_ERROR_TYPES = new Set<LandstripErrorType>([
109
- 'filesystem',
110
- 'network',
111
- 'platform',
112
- 'launch',
113
- 'encoding',
114
- ]);
115
95
  const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
116
96
 
117
97
  const DEFAULT_CONFIG: SandboxConfig = {
@@ -405,20 +385,10 @@ function normalizePathMatch(value: string, cwd: string): string | null {
405
385
  return isPathLike(value) ? normalizeBlockedPath(value, cwd) : null;
406
386
  }
407
387
 
408
- function isFilesystemAccessDenied(error: LandstripErrorResponse): boolean {
409
- return error.reason === 'AccessDenied' && error.type === 'filesystem';
410
- }
411
-
412
- function isLandstripErrorReason(value: string): value is LandstripErrorReason {
413
- return LANDSTRIP_ERROR_REASONS.has(value as LandstripErrorReason);
414
- }
415
-
416
- function isLandstripOperation(value: string): value is LandstripOperation {
417
- return LANDSTRIP_OPERATIONS.has(value as LandstripOperation);
418
- }
388
+ type LandstripFilesystemTrap = Extract<LandstripTrap, { kind: 'filesystem' }>;
419
389
 
420
- function isLandstripErrorType(value: string): value is LandstripErrorType {
421
- return LANDSTRIP_ERROR_TYPES.has(value as LandstripErrorType);
390
+ function isFilesystemTrap(trap: LandstripTrap): trap is LandstripFilesystemTrap {
391
+ return trap.kind === 'filesystem';
422
392
  }
423
393
 
424
394
  function extractCandidatePaths(command: string): string[] {
@@ -435,9 +405,9 @@ function extractCandidatePaths(command: string): string[] {
435
405
  }
436
406
 
437
407
  function extractBlockedPath(output: string, cwd: string, command?: string): string | null {
438
- const landstripErrors = parseLandstripErrors(output).filter(isFilesystemAccessDenied);
439
- for (const error of landstripErrors) {
440
- if (error.file) return normalizeBlockedPath(error.file, cwd);
408
+ const landstripErrors = parseLandstripTraps(output).filter(isFilesystemTrap);
409
+ if (landstripErrors.length > 0) {
410
+ return normalizeBlockedPath(landstripErrors[0].file, cwd);
441
411
  }
442
412
 
443
413
  // If landstrip reported an error but without a file field, try to
@@ -503,8 +473,8 @@ function extractNativeWriteDeniedPath(output: string, cwd: string): string | nul
503
473
  }
504
474
 
505
475
  function extractBlockedWritePath(output: string, cwd: string): string | null {
506
- for (const error of parseLandstripErrors(output).filter(isFilesystemAccessDenied)) {
507
- if (error.file && error.operation === 'write') {
476
+ for (const error of parseLandstripTraps(output).filter(isFilesystemTrap)) {
477
+ if (error.operation === 'write') {
508
478
  return normalizeBlockedPath(error.file, cwd);
509
479
  }
510
480
  }
@@ -512,67 +482,93 @@ function extractBlockedWritePath(output: string, cwd: string): string | null {
512
482
  return extractNativeWriteDeniedPath(output, cwd);
513
483
  }
514
484
 
515
- function parseLandstripErrors(output: string): LandstripErrorResponse[] {
516
- const errors: LandstripErrorResponse[] = [];
485
+ // Returns the first `length` elements of a string tuple, or null if `value` is
486
+ // not an array of at least that many strings.
487
+ function stringTuple(value: unknown, length: number): string[] | null {
488
+ if (!Array.isArray(value) || value.length < length) return null;
489
+ const head = value.slice(0, length);
490
+ return head.every((item) => typeof item === 'string') ? (head as string[]) : null;
491
+ }
517
492
 
518
- for (const line of output.trim().split('\n')) {
519
- if (line.length === 0) continue;
493
+ // landstrip emits each trap as a serde externally-tagged enum: a single-key
494
+ // object whose key is the variant name (`Filesystem`, `Network`, `Launch`,
495
+ // `Usage`, `Internal`) and whose value is the variant payload.
496
+ function parseLandstripTrap(obj: Record<string, unknown>): LandstripTrap | null {
497
+ const fs = stringTuple(obj.Filesystem, 3);
498
+ if (fs) {
499
+ const [operation, file, mechanism] = fs;
500
+ if (operation === 'read' || operation === 'write') {
501
+ return { kind: 'filesystem', operation, file, mechanism };
502
+ }
503
+ return null;
504
+ }
520
505
 
521
- try {
522
- const parsed: unknown = JSON.parse(line);
523
- if (typeof parsed !== 'object' || parsed === null) continue;
524
- const obj = parsed as Record<string, unknown>;
506
+ const net = stringTuple(obj.Network, 3);
507
+ if (net) {
508
+ const [operation, target, mechanism] = net;
509
+ return { kind: 'network', operation, target, mechanism };
510
+ }
525
511
 
526
- const reason = obj.reason;
527
- if (typeof reason !== 'string' || !isLandstripErrorReason(reason)) continue;
512
+ const launch = stringTuple(obj.Launch, 2);
513
+ if (launch) {
514
+ const [program, source] = launch;
515
+ return { kind: 'launch', program, source };
516
+ }
528
517
 
529
- const error: LandstripErrorResponse = { reason };
518
+ if (typeof obj.Usage === 'string') {
519
+ return { kind: 'usage', message: obj.Usage };
520
+ }
530
521
 
531
- if (typeof obj.source === 'string') error.source = obj.source;
532
- if (typeof obj.file === 'string') error.file = obj.file;
533
- if (typeof obj.program === 'string') error.program = obj.program;
522
+ const internal = obj.Internal;
523
+ if (typeof internal === 'object' && internal !== null && !Array.isArray(internal)) {
524
+ const detail: Record<string, string> = {};
525
+ for (const [key, value] of Object.entries(internal)) {
526
+ if (typeof value === 'string') detail[key] = value;
527
+ }
528
+ return { kind: 'internal', detail };
529
+ }
534
530
 
535
- if (typeof obj.operation === 'string' && isLandstripOperation(obj.operation)) {
536
- error.operation = obj.operation;
537
- }
531
+ return null;
532
+ }
538
533
 
539
- if (typeof obj.type === 'string' && isLandstripErrorType(obj.type)) {
540
- error.type = obj.type;
541
- }
534
+ function parseLandstripTraps(output: string): LandstripTrap[] {
535
+ const traps: LandstripTrap[] = [];
542
536
 
543
- if (typeof obj.mechanism === 'string') error.mechanism = obj.mechanism;
537
+ for (const line of output.trim().split('\n')) {
538
+ if (line.length === 0) continue;
544
539
 
545
- errors.push(error);
540
+ try {
541
+ const parsed: unknown = JSON.parse(line);
542
+ if (typeof parsed !== 'object' || parsed === null) continue;
543
+ const trap = parseLandstripTrap(parsed as Record<string, unknown>);
544
+ if (trap) traps.push(trap);
546
545
  } catch {
547
546
  // Ignore non-JSON lines (e.g. stderr from child processes)
548
547
  }
549
548
  }
550
549
 
551
- return errors;
550
+ return traps;
552
551
  }
553
552
 
554
- function formatLandstripErrors(errors: LandstripErrorResponse[]): string {
555
- return errors
556
- .map((err) => {
557
- const parts: string[] = [`landstrip: ${err.reason}`];
558
-
559
- if (err.file) {
560
- parts.push(` (${err.file})`);
561
- }
562
- if (err.program) {
563
- parts.push(` ${err.program}`);
564
- }
565
- if (err.type) {
566
- parts.push(`:${err.type}`);
567
- }
568
- if (err.operation) {
569
- parts.push(`:${err.operation}`);
570
- }
571
- if (err.source) {
572
- parts.push(`: ${err.source}`);
553
+ function formatLandstripTraps(traps: LandstripTrap[]): string {
554
+ return traps
555
+ .map((trap) => {
556
+ switch (trap.kind) {
557
+ case 'filesystem':
558
+ return `landstrip: filesystem ${trap.operation} denied: ${trap.file} (${trap.mechanism})`;
559
+ case 'network':
560
+ return `landstrip: network ${trap.operation} denied: ${trap.target} (${trap.mechanism})`;
561
+ case 'launch':
562
+ return `landstrip: launch failed: ${trap.program}: ${trap.source}`;
563
+ case 'usage':
564
+ return `landstrip: usage error: ${trap.message}`;
565
+ case 'internal': {
566
+ const detail = Object.entries(trap.detail)
567
+ .map(([key, value]) => `${key}=${value}`)
568
+ .join(', ');
569
+ return detail ? `landstrip: internal error: ${detail}` : 'landstrip: internal error';
570
+ }
573
571
  }
574
-
575
- return parts.join('');
576
572
  })
577
573
  .join('\n');
578
574
  }
@@ -1249,9 +1245,9 @@ export function createLandstripIntegration(
1249
1245
  if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
1250
1246
  }
1251
1247
  } else if (!blockedPath && ctx.hasUI) {
1252
- const landstripErrors = parseLandstripErrors(errorOutput);
1248
+ const landstripErrors = parseLandstripTraps(errorOutput);
1253
1249
  if (landstripErrors.length > 0) {
1254
- const formatted = formatLandstripErrors(landstripErrors);
1250
+ const formatted = formatLandstripTraps(landstripErrors);
1255
1251
  notify(ctx, `Sandbox blocked an operation: ${formatted}`, 'warning');
1256
1252
  }
1257
1253
  }
@@ -1342,15 +1338,15 @@ export function createLandstripIntegration(
1342
1338
  if (retryResult) return retryResult;
1343
1339
  }
1344
1340
 
1345
- const landstripErrors = parseLandstripErrors(landstripErrorOutput || errorText);
1341
+ const landstripErrors = parseLandstripTraps(landstripErrorOutput || errorText);
1346
1342
  if (landstripErrors.length > 0) {
1347
- throw new Error(formatLandstripErrors(landstripErrors));
1343
+ throw new Error(formatLandstripTraps(landstripErrors));
1348
1344
  }
1349
1345
  throw error;
1350
1346
  }
1351
- const landstripErrors = parseLandstripErrors(landstripErrorOutput);
1347
+ const landstripErrors = parseLandstripTraps(landstripErrorOutput);
1352
1348
  if (landstripErrors.length > 0) {
1353
- const message = formatLandstripErrors(landstripErrors);
1349
+ const message = formatLandstripTraps(landstripErrors);
1354
1350
  result.content.unshift({ type: 'text', text: `\n${message}\n` });
1355
1351
  }
1356
1352
  const blockedPath =
@@ -1428,7 +1424,7 @@ export function createLandstripIntegration(
1428
1424
  sandboxReady = false;
1429
1425
  notify(
1430
1426
  ctx,
1431
- `landstrip was not found. Reinstall with: npm install @jarkkojs/landstrip`,
1427
+ `landstrip was not found. Reinstall with: npm install @landstrip/landstrip`,
1432
1428
  'error',
1433
1429
  );
1434
1430
  return false;
package/landstrip.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- declare module '@jarkkojs/landstrip' {
1
+ declare module '@landstrip/landstrip' {
2
2
  function binaryPath(): string;
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Landlock-based sandboxing for pi with interactive permission prompts",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -11,7 +11,7 @@
11
11
  "license": "MIT",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git+https://github.com/jarkkojs/pi-landstrip.git"
14
+ "url": "git+https://github.com/landstrip/pi-landstrip.git"
15
15
  },
16
16
  "files": [
17
17
  "index.ts",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@earendil-works/pi-tui": "^0.78.0",
34
- "@jarkkojs/landstrip": "^0.12.2"
34
+ "@landstrip/landstrip": "^0.14.5"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.78.0",