pi-landstrip 0.14.1 → 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.
- package/index.ts +105 -47
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -454,53 +454,66 @@ function extractBlockedWritePath(output: string, cwd: string): string | null {
|
|
|
454
454
|
return extractNativeWriteDeniedPath(output, cwd);
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const head = value.slice(0, length);
|
|
462
|
-
return head.every((item) => typeof item === 'string') ? (head as string[]) : null;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// landstrip emits each trap as a serde externally-tagged enum: a single-key
|
|
466
|
-
// object whose key is the variant name (`Filesystem`, `Network`, `Launch`,
|
|
467
|
-
// `Usage`, `Internal`) and whose value is the variant payload.
|
|
468
|
-
function parseLandstripTrap(obj: Record<string, unknown>): LandstripTrap | null {
|
|
469
|
-
const fs = stringTuple(obj.Filesystem, 3);
|
|
470
|
-
if (fs) {
|
|
471
|
-
const [operation, file, mechanism] = fs;
|
|
472
|
-
if (operation === 'read' || operation === 'write') {
|
|
473
|
-
return { kind: 'filesystem', operation, file, mechanism };
|
|
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);
|
|
474
461
|
}
|
|
475
|
-
return null;
|
|
476
462
|
}
|
|
477
463
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const [operation, target, mechanism] = net;
|
|
481
|
-
return { kind: 'network', operation, target, mechanism };
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const launch = stringTuple(obj.Launch, 2);
|
|
485
|
-
if (launch) {
|
|
486
|
-
const [program, source] = launch;
|
|
487
|
-
return { kind: 'launch', program, source };
|
|
488
|
-
}
|
|
464
|
+
return extractNativeDeniedPath(output, cwd);
|
|
465
|
+
}
|
|
489
466
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
467
|
+
function asString(value: unknown): string | null {
|
|
468
|
+
return typeof value === 'string' ? value : null;
|
|
469
|
+
}
|
|
493
470
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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.
|
|
474
|
+
function parseLandstripTrap(obj: Record<string, unknown>): LandstripTrap | null {
|
|
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;
|
|
483
|
+
}
|
|
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;
|
|
499
499
|
}
|
|
500
|
-
|
|
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;
|
|
501
516
|
}
|
|
502
|
-
|
|
503
|
-
return null;
|
|
504
517
|
}
|
|
505
518
|
|
|
506
519
|
function parseLandstripTraps(output: string): LandstripTrap[] {
|
|
@@ -1223,7 +1236,7 @@ export function createLandstripIntegration(
|
|
|
1223
1236
|
const choice = await promptReadBlock(
|
|
1224
1237
|
ctx,
|
|
1225
1238
|
blockedPath,
|
|
1226
|
-
isDeniedByDenyRead ? '
|
|
1239
|
+
isDeniedByDenyRead ? 'granting allowRead will override it' : undefined,
|
|
1227
1240
|
);
|
|
1228
1241
|
if (choice !== 'abort') await applyReadChoice(choice, blockedPath, cwd);
|
|
1229
1242
|
} else if (!isWriteAllowed) {
|
|
@@ -1310,17 +1323,54 @@ export function createLandstripIntegration(
|
|
|
1310
1323
|
return run();
|
|
1311
1324
|
};
|
|
1312
1325
|
|
|
1326
|
+
const retryWithReadAccess = async (
|
|
1327
|
+
blockedPath: string,
|
|
1328
|
+
): Promise<AgentToolResult<BashToolDetails | undefined> | null> => {
|
|
1329
|
+
if (!ctx.hasUI) return null;
|
|
1330
|
+
|
|
1331
|
+
if (!matchesPattern(blockedPath, getEffectiveAllowRead(ctx.cwd))) {
|
|
1332
|
+
const config = loadConfig(ctx.cwd);
|
|
1333
|
+
const choice = await promptReadBlock(
|
|
1334
|
+
ctx,
|
|
1335
|
+
blockedPath,
|
|
1336
|
+
matchesPattern(blockedPath, config.filesystem.denyRead)
|
|
1337
|
+
? 'granting allowRead will override it'
|
|
1338
|
+
: undefined,
|
|
1339
|
+
);
|
|
1340
|
+
if (choice === 'abort') return null;
|
|
1341
|
+
await applyReadChoice(choice, blockedPath, ctx.cwd);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
onUpdate?.({
|
|
1345
|
+
content: [
|
|
1346
|
+
{ type: 'text', text: `\n--- Read access granted for "${blockedPath}", retrying ---\n` },
|
|
1347
|
+
],
|
|
1348
|
+
details: {},
|
|
1349
|
+
});
|
|
1350
|
+
landstripErrorOutput = '';
|
|
1351
|
+
stderrOutput = '';
|
|
1352
|
+
return run();
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1313
1355
|
let result: AgentToolResult<BashToolDetails | undefined>;
|
|
1314
1356
|
try {
|
|
1315
1357
|
result = await run();
|
|
1316
1358
|
} catch (error) {
|
|
1317
1359
|
const errorText = error instanceof Error ? error.message : String(error);
|
|
1318
1360
|
const fallbackOutput = `${stderrOutput}\n${errorText}`;
|
|
1319
|
-
const
|
|
1361
|
+
const blockedWritePath =
|
|
1320
1362
|
extractBlockedWritePath(landstripErrorOutput, ctx.cwd) ??
|
|
1321
1363
|
extractBlockedWritePath(fallbackOutput, ctx.cwd);
|
|
1322
|
-
if (
|
|
1323
|
-
const retryResult = await retryWithWriteAccess(
|
|
1364
|
+
if (blockedWritePath) {
|
|
1365
|
+
const retryResult = await retryWithWriteAccess(blockedWritePath);
|
|
1366
|
+
if (retryResult) return retryResult;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const blockedReadPath =
|
|
1370
|
+
extractBlockedReadPath(landstripErrorOutput, ctx.cwd) ??
|
|
1371
|
+
extractBlockedReadPath(fallbackOutput, ctx.cwd);
|
|
1372
|
+
if (blockedReadPath) {
|
|
1373
|
+
const retryResult = await retryWithReadAccess(blockedReadPath);
|
|
1324
1374
|
if (retryResult) return retryResult;
|
|
1325
1375
|
}
|
|
1326
1376
|
|
|
@@ -1335,12 +1385,20 @@ export function createLandstripIntegration(
|
|
|
1335
1385
|
const message = formatLandstripTraps(landstripErrors);
|
|
1336
1386
|
result.content.unshift({ type: 'text', text: `\n${message}\n` });
|
|
1337
1387
|
}
|
|
1338
|
-
const
|
|
1388
|
+
const blockedWritePath =
|
|
1339
1389
|
extractBlockedWritePath(landstripErrorOutput, ctx.cwd) ??
|
|
1340
1390
|
extractBlockedWritePath(stderrOutput, ctx.cwd);
|
|
1341
|
-
if (
|
|
1391
|
+
if (blockedWritePath) {
|
|
1392
|
+
const retryResult = await retryWithWriteAccess(blockedWritePath);
|
|
1393
|
+
if (retryResult) return retryResult;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
const blockedReadPath =
|
|
1397
|
+
extractBlockedReadPath(landstripErrorOutput, ctx.cwd) ??
|
|
1398
|
+
extractBlockedReadPath(stderrOutput, ctx.cwd);
|
|
1399
|
+
if (!blockedReadPath) return result;
|
|
1342
1400
|
|
|
1343
|
-
const retryResult = await
|
|
1401
|
+
const retryResult = await retryWithReadAccess(blockedReadPath);
|
|
1344
1402
|
return retryResult ?? result;
|
|
1345
1403
|
}
|
|
1346
1404
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
34
|
+
"@landstrip/landstrip": "^0.15.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|