pi-landstrip 0.5.1 → 0.5.3

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 +52 -6
  2. package/package.json +2 -2
package/index.ts CHANGED
@@ -80,7 +80,7 @@ interface LandstripErrorResponse {
80
80
  message: string;
81
81
  }
82
82
 
83
- const LANDSTRIP_VERSION = [0, 10, 2] as const;
83
+ const LANDSTRIP_VERSION = [0, 10, 3] as const;
84
84
  const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
85
85
 
86
86
  const DEFAULT_CONFIG: SandboxConfig = {
@@ -337,7 +337,26 @@ function normalizeBlockedPath(path: string, cwd: string): string {
337
337
  return canonicalizePath(isAbsolute(path) ? path : join(cwd, path));
338
338
  }
339
339
 
340
- function extractBlockedPath(output: string, cwd: string): string | null {
340
+ function extractCandidatePaths(command: string): string[] {
341
+ const paths: string[] = [];
342
+ // Split on whitespace, preserving quoted strings minimally
343
+ const tokens = command.match(/[^\s"']+|"[^"]*"|'[^']*'/g) ?? [];
344
+ for (const token of tokens) {
345
+ const clean = token.replace(/^["']|["']$/g, '').replace(/[,;]$/, '');
346
+ if (
347
+ clean.startsWith('/') ||
348
+ clean.startsWith('~/') ||
349
+ clean === '~' ||
350
+ clean.startsWith('./') ||
351
+ clean.startsWith('../')
352
+ ) {
353
+ paths.push(clean);
354
+ }
355
+ }
356
+ return paths;
357
+ }
358
+
359
+ function extractBlockedPath(output: string, cwd: string, command?: string): string | null {
341
360
  // bash/sh: line X: /path: Permission denied
342
361
  let match = output.match(
343
362
  /(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
@@ -356,11 +375,32 @@ function extractBlockedPath(output: string, cwd: string): string | null {
356
375
  );
357
376
  if (match) return normalizeBlockedPath(match[1], cwd);
358
377
 
378
+ // Landstrip structured error format with file field
379
+ const landstripErrors = parseLandstripErrors(output);
380
+ for (const error of landstripErrors) {
381
+ if (error.file) return normalizeBlockedPath(error.file, cwd);
382
+ }
383
+
384
+ // If landstrip reported an error but without a file field, try to
385
+ // extract the blocked path from the command itself
386
+ if (landstripErrors.length > 0 && command) {
387
+ const config = loadConfig(cwd);
388
+ for (const candidate of extractCandidatePaths(command)) {
389
+ const resolved = canonicalizePath(candidate);
390
+ if (
391
+ matchesPattern(resolved, config.filesystem.denyRead) ||
392
+ !matchesPattern(resolved, config.filesystem.allowRead)
393
+ ) {
394
+ return resolved;
395
+ }
396
+ }
397
+ }
398
+
359
399
  return null;
360
400
  }
361
401
 
362
- function extractBlockedWritePath(output: string, cwd: string): string | null {
363
- return extractBlockedPath(output, cwd);
402
+ function extractBlockedWritePath(output: string, cwd: string, command?: string): string | null {
403
+ return extractBlockedPath(output, cwd, command);
364
404
  }
365
405
 
366
406
  function parseLandstripErrors(output: string): LandstripErrorResponse[] {
@@ -1039,7 +1079,7 @@ export function createLandstripIntegration(
1039
1079
  return;
1040
1080
  }
1041
1081
 
1042
- const blockedPath = extractBlockedPath(stderrAcc, cwd);
1082
+ const blockedPath = extractBlockedPath(stderrAcc, cwd, command);
1043
1083
  if (blockedPath && ctx.hasUI) {
1044
1084
  const config = loadConfig(cwd);
1045
1085
  const isDeniedByDenyRead = matchesPattern(blockedPath, config.filesystem.denyRead);
@@ -1061,6 +1101,12 @@ export function createLandstripIntegration(
1061
1101
  const choice = await promptWriteBlock(ctx, blockedPath);
1062
1102
  if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
1063
1103
  }
1104
+ } else if (!blockedPath && ctx.hasUI) {
1105
+ const landstripErrors = parseLandstripErrors(stderrAcc);
1106
+ if (landstripErrors.length > 0) {
1107
+ const formatted = formatLandstripErrors(landstripErrors);
1108
+ ctx.ui.notify(`Sandbox blocked an operation: ${formatted}`, 'warning');
1109
+ }
1064
1110
  }
1065
1111
 
1066
1112
  resolvePromise({ exitCode: code });
@@ -1105,7 +1151,7 @@ export function createLandstripIntegration(
1105
1151
  const message = formatLandstripErrors(landstripErrors);
1106
1152
  result.content.unshift({ type: 'text', text: `\n${message}\n` });
1107
1153
  }
1108
- const blockedPath = extractBlockedWritePath(outputText, ctx.cwd);
1154
+ const blockedPath = extractBlockedWritePath(outputText, ctx.cwd, params.command);
1109
1155
 
1110
1156
  if (!blockedPath || !ctx.hasUI) return result;
1111
1157
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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
- "@jarkkojs/landstrip": "^0.10.2"
34
+ "@jarkkojs/landstrip": "^0.10.3"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.78.0",