pi-landstrip 0.11.1 → 0.11.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.
- package/index.ts +94 -45
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -72,15 +72,36 @@ interface LandstripPolicy {
|
|
|
72
72
|
filesystem: SandboxFilesystemConfig;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
type LandstripErrorReason = 'Other' | 'AccessDenied' | 'LaunchFailed' | 'SetupFailed' | 'Usage';
|
|
76
|
+
type LandstripOperation = 'read' | 'write';
|
|
77
|
+
type LandstripErrorType = 'filesystem' | 'network' | 'platform' | 'launch' | 'encoding';
|
|
78
|
+
|
|
75
79
|
interface LandstripErrorResponse {
|
|
76
|
-
reason:
|
|
80
|
+
reason: LandstripErrorReason;
|
|
77
81
|
file?: string;
|
|
82
|
+
operation?: LandstripOperation;
|
|
78
83
|
program?: string;
|
|
79
|
-
type?:
|
|
80
|
-
source
|
|
84
|
+
type?: LandstripErrorType;
|
|
85
|
+
source?: string;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
const LANDSTRIP_VERSION = [0, 11,
|
|
88
|
+
const LANDSTRIP_VERSION = [0, 11, 6] as const;
|
|
89
|
+
const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
|
|
90
|
+
const LANDSTRIP_ERROR_REASONS = new Set<LandstripErrorReason>([
|
|
91
|
+
'Other',
|
|
92
|
+
'AccessDenied',
|
|
93
|
+
'LaunchFailed',
|
|
94
|
+
'SetupFailed',
|
|
95
|
+
'Usage',
|
|
96
|
+
]);
|
|
97
|
+
const LANDSTRIP_OPERATIONS = new Set<LandstripOperation>(['read', 'write']);
|
|
98
|
+
const LANDSTRIP_ERROR_TYPES = new Set<LandstripErrorType>([
|
|
99
|
+
'filesystem',
|
|
100
|
+
'network',
|
|
101
|
+
'platform',
|
|
102
|
+
'launch',
|
|
103
|
+
'encoding',
|
|
104
|
+
]);
|
|
84
105
|
const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
|
|
85
106
|
|
|
86
107
|
const DEFAULT_CONFIG: SandboxConfig = {
|
|
@@ -348,6 +369,22 @@ function normalizePathMatch(value: string, cwd: string): string | null {
|
|
|
348
369
|
return isPathLike(value) ? normalizeBlockedPath(value, cwd) : null;
|
|
349
370
|
}
|
|
350
371
|
|
|
372
|
+
function isFilesystemAccessDenied(error: LandstripErrorResponse): boolean {
|
|
373
|
+
return error.reason === 'AccessDenied' && error.type === 'filesystem';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isLandstripErrorReason(value: string): value is LandstripErrorReason {
|
|
377
|
+
return LANDSTRIP_ERROR_REASONS.has(value as LandstripErrorReason);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isLandstripOperation(value: string): value is LandstripOperation {
|
|
381
|
+
return LANDSTRIP_OPERATIONS.has(value as LandstripOperation);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isLandstripErrorType(value: string): value is LandstripErrorType {
|
|
385
|
+
return LANDSTRIP_ERROR_TYPES.has(value as LandstripErrorType);
|
|
386
|
+
}
|
|
387
|
+
|
|
351
388
|
function extractCandidatePaths(command: string): string[] {
|
|
352
389
|
const paths: string[] = [];
|
|
353
390
|
// Split on whitespace, preserving quoted strings minimally
|
|
@@ -362,6 +399,26 @@ function extractCandidatePaths(command: string): string[] {
|
|
|
362
399
|
}
|
|
363
400
|
|
|
364
401
|
function extractBlockedPath(output: string, cwd: string, command?: string): string | null {
|
|
402
|
+
const landstripErrors = parseLandstripErrors(output).filter(isFilesystemAccessDenied);
|
|
403
|
+
for (const error of landstripErrors) {
|
|
404
|
+
if (error.file) return normalizeBlockedPath(error.file, cwd);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// If landstrip reported an error but without a file field, try to
|
|
408
|
+
// extract the blocked path from the command itself.
|
|
409
|
+
if (landstripErrors.length > 0 && command) {
|
|
410
|
+
const config = loadConfig(cwd);
|
|
411
|
+
for (const candidate of extractCandidatePaths(command)) {
|
|
412
|
+
const resolved = normalizeBlockedPath(candidate, cwd);
|
|
413
|
+
if (
|
|
414
|
+
matchesPattern(resolved, config.filesystem.denyRead) ||
|
|
415
|
+
!matchesPattern(resolved, config.filesystem.allowRead)
|
|
416
|
+
) {
|
|
417
|
+
return resolved;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
365
422
|
// bash/sh: line X: /path: Permission denied
|
|
366
423
|
let match = output.match(
|
|
367
424
|
/(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
|
|
@@ -376,38 +433,23 @@ function extractBlockedPath(output: string, cwd: string, command?: string): stri
|
|
|
376
433
|
|
|
377
434
|
// Generic: cmd: /absolute/path: Permission denied or Operation not permitted
|
|
378
435
|
match = output.match(
|
|
379
|
-
/^[a-zA-Z0-9_-]+: (\/[
|
|
436
|
+
/^[a-zA-Z0-9_-]+: (\/[^:\n]+): (?:Operation not permitted|Permission denied)$/m,
|
|
380
437
|
);
|
|
381
438
|
if (match) return normalizeBlockedPath(match[1], cwd);
|
|
382
439
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
for (const error of landstripErrors) {
|
|
386
|
-
if (error.file) return normalizeBlockedPath(error.file, cwd);
|
|
387
|
-
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
388
442
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
for (const candidate of extractCandidatePaths(command)) {
|
|
394
|
-
const resolved = normalizeBlockedPath(candidate, cwd);
|
|
395
|
-
if (
|
|
396
|
-
matchesPattern(resolved, config.filesystem.denyRead) ||
|
|
397
|
-
!matchesPattern(resolved, config.filesystem.allowRead)
|
|
398
|
-
) {
|
|
399
|
-
return resolved;
|
|
400
|
-
}
|
|
443
|
+
function extractBlockedWritePath(output: string, cwd: string): string | null {
|
|
444
|
+
for (const error of parseLandstripErrors(output).filter(isFilesystemAccessDenied)) {
|
|
445
|
+
if (error.file && error.operation === 'write') {
|
|
446
|
+
return normalizeBlockedPath(error.file, cwd);
|
|
401
447
|
}
|
|
402
448
|
}
|
|
403
449
|
|
|
404
450
|
return null;
|
|
405
451
|
}
|
|
406
452
|
|
|
407
|
-
function extractBlockedWritePath(output: string, cwd: string, command?: string): string | null {
|
|
408
|
-
return extractBlockedPath(output, cwd, command);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
453
|
function parseLandstripErrors(output: string): LandstripErrorResponse[] {
|
|
412
454
|
const errors: LandstripErrorResponse[] = [];
|
|
413
455
|
|
|
@@ -424,22 +466,21 @@ function parseLandstripErrors(output: string): LandstripErrorResponse[] {
|
|
|
424
466
|
|
|
425
467
|
if (
|
|
426
468
|
fields.reason &&
|
|
427
|
-
|
|
428
|
-
fields.source
|
|
469
|
+
isLandstripErrorReason(fields.reason) &&
|
|
470
|
+
(fields.source || fields.reason === 'AccessDenied')
|
|
429
471
|
) {
|
|
430
|
-
const error: LandstripErrorResponse = {
|
|
431
|
-
reason: fields.reason as LandstripErrorResponse['reason'],
|
|
432
|
-
source: fields.source,
|
|
433
|
-
};
|
|
472
|
+
const error: LandstripErrorResponse = { reason: fields.reason };
|
|
434
473
|
|
|
474
|
+
if (fields.source) error.source = fields.source;
|
|
435
475
|
if (fields.file) error.file = fields.file;
|
|
436
476
|
if (fields.program) error.program = fields.program;
|
|
437
477
|
|
|
438
|
-
if (
|
|
439
|
-
fields.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
478
|
+
if (fields.operation && isLandstripOperation(fields.operation)) {
|
|
479
|
+
error.operation = fields.operation;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (fields.type && isLandstripErrorType(fields.type)) {
|
|
483
|
+
error.type = fields.type;
|
|
443
484
|
}
|
|
444
485
|
|
|
445
486
|
errors.push(error);
|
|
@@ -463,7 +504,12 @@ function formatLandstripErrors(errors: LandstripErrorResponse[]): string {
|
|
|
463
504
|
if (err.type) {
|
|
464
505
|
parts.push(`:${err.type}`);
|
|
465
506
|
}
|
|
466
|
-
|
|
507
|
+
if (err.operation) {
|
|
508
|
+
parts.push(`:${err.operation}`);
|
|
509
|
+
}
|
|
510
|
+
if (err.source) {
|
|
511
|
+
parts.push(`: ${err.source}`);
|
|
512
|
+
}
|
|
467
513
|
|
|
468
514
|
return parts.join('');
|
|
469
515
|
})
|
|
@@ -1085,6 +1131,7 @@ export function createLandstripIntegration(
|
|
|
1085
1131
|
}
|
|
1086
1132
|
|
|
1087
1133
|
const blockedPath = extractBlockedPath(stderrAcc, cwd, command);
|
|
1134
|
+
const blockedWritePath = extractBlockedWritePath(stderrAcc, cwd);
|
|
1088
1135
|
if (blockedPath && ctx.hasUI) {
|
|
1089
1136
|
const config = loadConfig(cwd);
|
|
1090
1137
|
const isDeniedByDenyRead = matchesPattern(blockedPath, config.filesystem.denyRead);
|
|
@@ -1095,7 +1142,10 @@ export function createLandstripIntegration(
|
|
|
1095
1142
|
matchesPattern,
|
|
1096
1143
|
);
|
|
1097
1144
|
|
|
1098
|
-
if (
|
|
1145
|
+
if (blockedWritePath === blockedPath && !isWriteAllowed) {
|
|
1146
|
+
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1147
|
+
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1148
|
+
} else if (isDeniedByDenyRead || !isReadAllowed) {
|
|
1099
1149
|
const choice = await promptReadBlock(
|
|
1100
1150
|
ctx,
|
|
1101
1151
|
blockedPath,
|
|
@@ -1147,16 +1197,12 @@ export function createLandstripIntegration(
|
|
|
1147
1197
|
}
|
|
1148
1198
|
throw error;
|
|
1149
1199
|
}
|
|
1150
|
-
const outputText = result.content
|
|
1151
|
-
.filter((content) => content.type === 'text')
|
|
1152
|
-
.map((content) => content.text)
|
|
1153
|
-
.join('\n');
|
|
1154
1200
|
const landstripErrors = parseLandstripErrors(landstripStderr);
|
|
1155
1201
|
if (landstripErrors.length > 0) {
|
|
1156
1202
|
const message = formatLandstripErrors(landstripErrors);
|
|
1157
1203
|
result.content.unshift({ type: 'text', text: `\n${message}\n` });
|
|
1158
1204
|
}
|
|
1159
|
-
const blockedPath = extractBlockedWritePath(
|
|
1205
|
+
const blockedPath = extractBlockedWritePath(landstripStderr, ctx.cwd);
|
|
1160
1206
|
|
|
1161
1207
|
if (!blockedPath || !ctx.hasUI) return result;
|
|
1162
1208
|
|
|
@@ -1256,7 +1302,10 @@ export function createLandstripIntegration(
|
|
|
1256
1302
|
if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
|
|
1257
1303
|
sandboxEnabled = false;
|
|
1258
1304
|
sandboxReady = false;
|
|
1259
|
-
ctx.ui.notify(
|
|
1305
|
+
ctx.ui.notify(
|
|
1306
|
+
`landstrip ${REQUIRED_LANDSTRIP_VERSION} or newer is required; found: ${version}`,
|
|
1307
|
+
'error',
|
|
1308
|
+
);
|
|
1260
1309
|
return false;
|
|
1261
1310
|
}
|
|
1262
1311
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
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.11.
|
|
34
|
+
"@jarkkojs/landstrip": "^0.11.6"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|