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.
- package/index.ts +82 -41
- 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,
|
|
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(
|
|
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
|
|
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:
|
|
986
|
-
allowWrite:
|
|
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
|
|
1219
|
-
(errorFdAcc ? extractBlockedPath(stderrAcc, cwd
|
|
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
|
|
1358
|
+
const blockedWritePath =
|
|
1334
1359
|
extractBlockedWritePath(landstripErrorOutput, ctx.cwd) ??
|
|
1335
1360
|
extractBlockedWritePath(fallbackOutput, ctx.cwd);
|
|
1336
|
-
if (
|
|
1337
|
-
const retryResult = await retryWithWriteAccess(
|
|
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
|
|
1385
|
+
const blockedWritePath =
|
|
1353
1386
|
extractBlockedWritePath(landstripErrorOutput, ctx.cwd) ??
|
|
1354
1387
|
extractBlockedWritePath(stderrOutput, ctx.cwd);
|
|
1355
|
-
if (
|
|
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
|
|
1398
|
+
const retryResult = await retryWithReadAccess(blockedReadPath);
|
|
1358
1399
|
return retryResult ?? result;
|
|
1359
1400
|
}
|
|
1360
1401
|
|