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.
- package/index.ts +52 -6
- 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,
|
|
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
|
|
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.
|
|
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.
|
|
34
|
+
"@jarkkojs/landstrip": "^0.10.3"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|