pi-landstrip 0.14.0 → 0.14.2

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 +82 -41
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -90,7 +90,7 @@ interface LandstripBashCallbacks {
90
90
  onErrorFd?: (data: Buffer) => void;
91
91
  }
92
92
 
93
- const LANDSTRIP_VERSION = [0, 14, 2] as const;
93
+ const LANDSTRIP_VERSION = [0, 14, 5] as const;
94
94
  const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
95
95
  const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
96
96
 
@@ -198,7 +198,7 @@ function deepMerge(base: SandboxConfig, overrides: Partial<SandboxConfig>): Sand
198
198
 
199
199
  function getConfigPaths(cwd: string): { globalPath: string; projectPath: string } {
200
200
  return {
201
- globalPath: join(homedir(), '.pi', 'agent', 'sandbox.json'),
201
+ globalPath: join(getAgentDir(), 'sandbox.json'),
202
202
  projectPath: join(cwd, '.pi', 'sandbox.json'),
203
203
  };
204
204
  }
@@ -391,40 +391,12 @@ function isFilesystemTrap(trap: LandstripTrap): trap is LandstripFilesystemTrap
391
391
  return trap.kind === 'filesystem';
392
392
  }
393
393
 
394
- function extractCandidatePaths(command: string): string[] {
395
- const paths: string[] = [];
396
- // Split on whitespace, preserving quoted strings minimally
397
- const tokens = command.match(/[^\s"']+|"[^"]*"|'[^']*'/g) ?? [];
398
- for (const token of tokens) {
399
- const clean = token.replace(/^["']|["']$/g, '').replace(/[,;]$/, '');
400
- if (isPathLike(clean)) {
401
- paths.push(clean);
402
- }
403
- }
404
- return paths;
405
- }
406
-
407
- function extractBlockedPath(output: string, cwd: string, command?: string): string | null {
394
+ function extractBlockedPath(output: string, cwd: string): string | null {
408
395
  const landstripErrors = parseLandstripTraps(output).filter(isFilesystemTrap);
409
396
  if (landstripErrors.length > 0) {
410
397
  return normalizeBlockedPath(landstripErrors[0].file, cwd);
411
398
  }
412
399
 
413
- // If landstrip reported an error but without a file field, try to
414
- // extract the blocked path from the command itself.
415
- if (landstripErrors.length > 0 && command) {
416
- const config = loadConfig(cwd);
417
- for (const candidate of extractCandidatePaths(command)) {
418
- const resolved = normalizeBlockedPath(candidate, cwd);
419
- if (
420
- matchesPattern(resolved, config.filesystem.denyRead) ||
421
- !matchesPattern(resolved, config.filesystem.allowRead)
422
- ) {
423
- return resolved;
424
- }
425
- }
426
- }
427
-
428
400
  return extractNativeDeniedPath(output, cwd);
429
401
  }
430
402
 
@@ -482,6 +454,16 @@ function extractBlockedWritePath(output: string, cwd: string): string | null {
482
454
  return extractNativeWriteDeniedPath(output, cwd);
483
455
  }
484
456
 
457
+ function extractBlockedReadPath(output: string, cwd: string): string | null {
458
+ for (const error of parseLandstripTraps(output).filter(isFilesystemTrap)) {
459
+ if (error.operation === 'read') {
460
+ return normalizeBlockedPath(error.file, cwd);
461
+ }
462
+ }
463
+
464
+ return extractNativeDeniedPath(output, cwd);
465
+ }
466
+
485
467
  // Returns the first `length` elements of a string tuple, or null if `value` is
486
468
  // not an array of at least that many strings.
487
469
  function stringTuple(value: unknown, length: number): string[] | null {
@@ -982,8 +964,8 @@ export function createLandstripIntegration(
982
964
  },
983
965
  filesystem: {
984
966
  denyRead: config.filesystem.denyRead,
985
- allowRead: [...config.filesystem.allowRead, ...sessionAllowedReadPaths],
986
- allowWrite: [...config.filesystem.allowWrite, ...sessionAllowedWritePaths],
967
+ allowRead: getEffectiveAllowRead(cwd),
968
+ allowWrite: getEffectiveAllowWrite(cwd),
987
969
  denyWrite: config.filesystem.denyWrite,
988
970
  },
989
971
  };
@@ -1019,10 +1001,17 @@ export function createLandstripIntegration(
1019
1001
  return;
1020
1002
  }
1021
1003
 
1004
+ let settled = false;
1022
1005
  const upstream = connectNet(endpoint.port, endpoint.host, () => {
1006
+ settled = true;
1023
1007
  client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
1024
1008
  pipeSockets(client, upstream, rest);
1025
1009
  });
1010
+ upstream.once('error', () => {
1011
+ if (settled) return;
1012
+ settled = true;
1013
+ denyProxyRequest(client, '502 Bad Gateway');
1014
+ });
1026
1015
  }
1027
1016
 
1028
1017
  async function handleHttp(client: Socket, headerText: string, rest: Buffer): Promise<void> {
@@ -1061,10 +1050,17 @@ export function createLandstripIntegration(
1061
1050
  const rewrittenHeader = lines
1062
1051
  .filter((line) => !line.toLowerCase().startsWith('proxy-connection:'))
1063
1052
  .join('\r\n');
1053
+ let settled = false;
1064
1054
  const upstream = connectNet(port, url.hostname, () => {
1055
+ settled = true;
1065
1056
  upstream.write(`${rewrittenHeader}\r\n\r\n`);
1066
1057
  pipeSockets(client, upstream, rest);
1067
1058
  });
1059
+ upstream.once('error', () => {
1060
+ if (settled) return;
1061
+ settled = true;
1062
+ denyProxyRequest(client, '502 Bad Gateway');
1063
+ });
1068
1064
  }
1069
1065
 
1070
1066
  function handleClient(client: Socket): void {
@@ -1215,8 +1211,8 @@ export function createLandstripIntegration(
1215
1211
  const errorOutput = errorFdAcc || stderrAcc;
1216
1212
 
1217
1213
  const blockedPath =
1218
- extractBlockedPath(errorOutput, cwd, command) ??
1219
- (errorFdAcc ? extractBlockedPath(stderrAcc, cwd, command) : null);
1214
+ extractBlockedPath(errorOutput, cwd) ??
1215
+ (errorFdAcc ? extractBlockedPath(stderrAcc, cwd) : null);
1220
1216
  const blockedWritePath =
1221
1217
  extractBlockedWritePath(errorOutput, cwd) ??
1222
1218
  (errorFdAcc ? extractBlockedWritePath(stderrAcc, cwd) : null);
@@ -1324,17 +1320,54 @@ export function createLandstripIntegration(
1324
1320
  return run();
1325
1321
  };
1326
1322
 
1323
+ const retryWithReadAccess = async (
1324
+ blockedPath: string,
1325
+ ): Promise<AgentToolResult<BashToolDetails | undefined> | null> => {
1326
+ if (!ctx.hasUI) return null;
1327
+
1328
+ if (!matchesPattern(blockedPath, getEffectiveAllowRead(ctx.cwd))) {
1329
+ const config = loadConfig(ctx.cwd);
1330
+ const choice = await promptReadBlock(
1331
+ ctx,
1332
+ blockedPath,
1333
+ matchesPattern(blockedPath, config.filesystem.denyRead)
1334
+ ? 'denyRead overrides allowRead'
1335
+ : undefined,
1336
+ );
1337
+ if (choice === 'abort') return null;
1338
+ await applyReadChoice(choice, blockedPath, ctx.cwd);
1339
+ }
1340
+
1341
+ onUpdate?.({
1342
+ content: [
1343
+ { type: 'text', text: `\n--- Read access granted for "${blockedPath}", retrying ---\n` },
1344
+ ],
1345
+ details: {},
1346
+ });
1347
+ landstripErrorOutput = '';
1348
+ stderrOutput = '';
1349
+ return run();
1350
+ };
1351
+
1327
1352
  let result: AgentToolResult<BashToolDetails | undefined>;
1328
1353
  try {
1329
1354
  result = await run();
1330
1355
  } catch (error) {
1331
1356
  const errorText = error instanceof Error ? error.message : String(error);
1332
1357
  const fallbackOutput = `${stderrOutput}\n${errorText}`;
1333
- const blockedPath =
1358
+ const blockedWritePath =
1334
1359
  extractBlockedWritePath(landstripErrorOutput, ctx.cwd) ??
1335
1360
  extractBlockedWritePath(fallbackOutput, ctx.cwd);
1336
- if (blockedPath) {
1337
- const retryResult = await retryWithWriteAccess(blockedPath);
1361
+ if (blockedWritePath) {
1362
+ const retryResult = await retryWithWriteAccess(blockedWritePath);
1363
+ if (retryResult) return retryResult;
1364
+ }
1365
+
1366
+ const blockedReadPath =
1367
+ extractBlockedReadPath(landstripErrorOutput, ctx.cwd) ??
1368
+ extractBlockedReadPath(fallbackOutput, ctx.cwd);
1369
+ if (blockedReadPath) {
1370
+ const retryResult = await retryWithReadAccess(blockedReadPath);
1338
1371
  if (retryResult) return retryResult;
1339
1372
  }
1340
1373
 
@@ -1349,12 +1382,20 @@ export function createLandstripIntegration(
1349
1382
  const message = formatLandstripTraps(landstripErrors);
1350
1383
  result.content.unshift({ type: 'text', text: `\n${message}\n` });
1351
1384
  }
1352
- const blockedPath =
1385
+ const blockedWritePath =
1353
1386
  extractBlockedWritePath(landstripErrorOutput, ctx.cwd) ??
1354
1387
  extractBlockedWritePath(stderrOutput, ctx.cwd);
1355
- if (!blockedPath) return result;
1388
+ if (blockedWritePath) {
1389
+ const retryResult = await retryWithWriteAccess(blockedWritePath);
1390
+ if (retryResult) return retryResult;
1391
+ }
1392
+
1393
+ const blockedReadPath =
1394
+ extractBlockedReadPath(landstripErrorOutput, ctx.cwd) ??
1395
+ extractBlockedReadPath(stderrOutput, ctx.cwd);
1396
+ if (!blockedReadPath) return result;
1356
1397
 
1357
- const retryResult = await retryWithWriteAccess(blockedPath);
1398
+ const retryResult = await retryWithReadAccess(blockedReadPath);
1358
1399
  return retryResult ?? result;
1359
1400
  }
1360
1401
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Landlock-based sandboxing for pi with interactive permission prompts",
5
5
  "keywords": [
6
6
  "landstrip",