pi-landstrip 0.14.2 → 0.15.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.
Files changed (2) hide show
  1. package/index.ts +46 -43
  2. package/package.json +2 -2
package/index.ts CHANGED
@@ -464,53 +464,56 @@ function extractBlockedReadPath(output: string, cwd: string): string | null {
464
464
  return extractNativeDeniedPath(output, cwd);
465
465
  }
466
466
 
467
- // Returns the first `length` elements of a string tuple, or null if `value` is
468
- // not an array of at least that many strings.
469
- function stringTuple(value: unknown, length: number): string[] | null {
470
- if (!Array.isArray(value) || value.length < length) return null;
471
- const head = value.slice(0, length);
472
- return head.every((item) => typeof item === 'string') ? (head as string[]) : null;
467
+ function asString(value: unknown): string | null {
468
+ return typeof value === 'string' ? value : null;
473
469
  }
474
470
 
475
- // landstrip emits each trap as a serde externally-tagged enum: a single-key
476
- // object whose key is the variant name (`Filesystem`, `Network`, `Launch`,
477
- // `Usage`, `Internal`) and whose value is the variant payload.
471
+ // landstrip emits each trap as a flat JSON record tagged by a `kind`
472
+ // discriminant (`filesystem`, `network`, `launch`, `usage`, `internal`)
473
+ // alongside a stable `code` and variant-specific fields.
478
474
  function parseLandstripTrap(obj: Record<string, unknown>): LandstripTrap | null {
479
- const fs = stringTuple(obj.Filesystem, 3);
480
- if (fs) {
481
- const [operation, file, mechanism] = fs;
482
- if (operation === 'read' || operation === 'write') {
483
- return { kind: 'filesystem', operation, file, mechanism };
475
+ switch (obj.kind) {
476
+ case 'filesystem': {
477
+ const operation = obj.operation;
478
+ const file = asString(obj.path);
479
+ if ((operation === 'read' || operation === 'write') && file !== null) {
480
+ return { kind: 'filesystem', operation, file, mechanism: asString(obj.mechanism) ?? '' };
481
+ }
482
+ return null;
484
483
  }
485
- return null;
486
- }
487
-
488
- const net = stringTuple(obj.Network, 3);
489
- if (net) {
490
- const [operation, target, mechanism] = net;
491
- return { kind: 'network', operation, target, mechanism };
492
- }
493
-
494
- const launch = stringTuple(obj.Launch, 2);
495
- if (launch) {
496
- const [program, source] = launch;
497
- return { kind: 'launch', program, source };
498
- }
499
-
500
- if (typeof obj.Usage === 'string') {
501
- return { kind: 'usage', message: obj.Usage };
502
- }
503
-
504
- const internal = obj.Internal;
505
- if (typeof internal === 'object' && internal !== null && !Array.isArray(internal)) {
506
- const detail: Record<string, string> = {};
507
- for (const [key, value] of Object.entries(internal)) {
508
- if (typeof value === 'string') detail[key] = value;
484
+ case 'network': {
485
+ const operation = asString(obj.operation);
486
+ const target = asString(obj.target);
487
+ if (operation !== null && target !== null) {
488
+ return { kind: 'network', operation, target, mechanism: asString(obj.mechanism) ?? '' };
489
+ }
490
+ return null;
491
+ }
492
+ case 'launch': {
493
+ const program = asString(obj.program);
494
+ const source = asString(obj.message);
495
+ if (program !== null && source !== null) {
496
+ return { kind: 'launch', program, source };
497
+ }
498
+ return null;
509
499
  }
510
- return { kind: 'internal', detail };
500
+ case 'usage': {
501
+ const message = asString(obj.message);
502
+ return message !== null ? { kind: 'usage', message } : null;
503
+ }
504
+ case 'internal': {
505
+ const detail: Record<string, string> = {};
506
+ const raw = obj.detail;
507
+ if (typeof raw === 'object' && raw !== null && !Array.isArray(raw)) {
508
+ for (const [key, value] of Object.entries(raw)) {
509
+ if (typeof value === 'string') detail[key] = value;
510
+ }
511
+ }
512
+ return { kind: 'internal', detail };
513
+ }
514
+ default:
515
+ return null;
511
516
  }
512
-
513
- return null;
514
517
  }
515
518
 
516
519
  function parseLandstripTraps(output: string): LandstripTrap[] {
@@ -1233,7 +1236,7 @@ export function createLandstripIntegration(
1233
1236
  const choice = await promptReadBlock(
1234
1237
  ctx,
1235
1238
  blockedPath,
1236
- isDeniedByDenyRead ? 'denyRead overrides allowRead' : undefined,
1239
+ isDeniedByDenyRead ? 'granting allowRead will override it' : undefined,
1237
1240
  );
1238
1241
  if (choice !== 'abort') await applyReadChoice(choice, blockedPath, cwd);
1239
1242
  } else if (!isWriteAllowed) {
@@ -1331,7 +1334,7 @@ export function createLandstripIntegration(
1331
1334
  ctx,
1332
1335
  blockedPath,
1333
1336
  matchesPattern(blockedPath, config.filesystem.denyRead)
1334
- ? 'denyRead overrides allowRead'
1337
+ ? 'granting allowRead will override it'
1335
1338
  : undefined,
1336
1339
  );
1337
1340
  if (choice === 'abort') return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.14.2",
3
+ "version": "0.15.0",
4
4
  "description": "Landlock-based sandboxing for pi with interactive permission prompts",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@earendil-works/pi-tui": "^0.78.0",
34
- "@landstrip/landstrip": "^0.14.5"
34
+ "@landstrip/landstrip": "^0.15.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.78.0",