pi-landstrip 0.16.4 → 0.16.5
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 +110 -32
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1247,9 +1247,114 @@ export function createLandstripIntegration(
|
|
|
1247
1247
|
callbacks.onStderr?.(data);
|
|
1248
1248
|
onData(data);
|
|
1249
1249
|
});
|
|
1250
|
+
let trapBuffer = '';
|
|
1251
|
+
let queryChain: Promise<void> = Promise.resolve();
|
|
1252
|
+
|
|
1253
|
+
const respondQuery = (queryId: number, action: 'allow' | 'deny'): void => {
|
|
1254
|
+
if (trapSocket.destroyed) return;
|
|
1255
|
+
trapSocket.write(JSON.stringify({ query_id: queryId, action }) + '\n');
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
// Surface a denial through the error-fd accumulator so the post-close
|
|
1259
|
+
// notify and the runBashWithOptionalRetry prompt/retry paths still work.
|
|
1260
|
+
const appendErrorLine = (line: string): void => {
|
|
1261
|
+
const infoLine = line + '\n';
|
|
1262
|
+
errorFdAcc += infoLine;
|
|
1263
|
+
callbacks.onErrorFd?.(Buffer.from(infoLine, 'utf8'));
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
// Answer a landstrip query (state:"query"). The broker suspends the
|
|
1267
|
+
// child's syscall until we respond allow/deny on the trap socket.
|
|
1268
|
+
const handleQuery = (
|
|
1269
|
+
queryId: number,
|
|
1270
|
+
operation: LandstripOperation,
|
|
1271
|
+
rawPath: string,
|
|
1272
|
+
rawLine: string,
|
|
1273
|
+
): void => {
|
|
1274
|
+
const path = normalizeBlockedPath(rawPath, cwd);
|
|
1275
|
+
const config = loadConfig(cwd);
|
|
1276
|
+
const isAllowed = (cfg: SandboxConfig): boolean =>
|
|
1277
|
+
operation === 'read'
|
|
1278
|
+
? !matchesPattern(path, cfg.filesystem.denyRead) &&
|
|
1279
|
+
matchesPattern(path, getEffectiveAllowRead(cfg))
|
|
1280
|
+
: !matchesPattern(path, cfg.filesystem.denyWrite) &&
|
|
1281
|
+
!shouldPromptForWrite(path, getEffectiveAllowWrite(cfg));
|
|
1282
|
+
|
|
1283
|
+
if (isAllowed(config)) {
|
|
1284
|
+
respondQuery(queryId, 'allow');
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
// Without an interactive prompt, deny and let the retry path grant.
|
|
1288
|
+
if (!ctx.hasUI || !callbacks.promptOnBlock) {
|
|
1289
|
+
appendErrorLine(rawLine);
|
|
1290
|
+
respondQuery(queryId, 'deny');
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
// denyWrite is a hard block: never prompt to override it.
|
|
1294
|
+
if (operation === 'write' && matchesPattern(path, config.filesystem.denyWrite)) {
|
|
1295
|
+
respondQuery(queryId, 'deny');
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
// Serialize prompts so concurrent queries never overlap on screen and
|
|
1299
|
+
// a path granted by one prompt auto-allows later queries for it.
|
|
1300
|
+
queryChain = queryChain
|
|
1301
|
+
.then(async () => {
|
|
1302
|
+
const cfg = loadConfig(cwd);
|
|
1303
|
+
if (isAllowed(cfg)) {
|
|
1304
|
+
respondQuery(queryId, 'allow');
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const choice =
|
|
1308
|
+
operation === 'read'
|
|
1309
|
+
? await promptReadBlock(
|
|
1310
|
+
ctx,
|
|
1311
|
+
path,
|
|
1312
|
+
matchesPattern(path, cfg.filesystem.denyRead)
|
|
1313
|
+
? 'granting allowRead will override it'
|
|
1314
|
+
: undefined,
|
|
1315
|
+
)
|
|
1316
|
+
: await promptWriteBlock(ctx, path);
|
|
1317
|
+
if (choice === 'abort') {
|
|
1318
|
+
respondQuery(queryId, 'deny');
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
if (operation === 'read') await applyReadChoice(choice, path, cwd);
|
|
1322
|
+
else await applyWriteChoice(choice, path, cwd);
|
|
1323
|
+
respondQuery(queryId, 'allow');
|
|
1324
|
+
})
|
|
1325
|
+
.catch(() => respondQuery(queryId, 'deny'));
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1250
1328
|
trapSocket.on('data', (data: Buffer) => {
|
|
1251
|
-
|
|
1252
|
-
|
|
1329
|
+
trapBuffer += data.toString('utf8');
|
|
1330
|
+
let nl = trapBuffer.indexOf('\n');
|
|
1331
|
+
while (nl !== -1) {
|
|
1332
|
+
const line = trapBuffer.slice(0, nl);
|
|
1333
|
+
trapBuffer = trapBuffer.slice(nl + 1);
|
|
1334
|
+
nl = trapBuffer.indexOf('\n');
|
|
1335
|
+
if (line.length === 0) continue;
|
|
1336
|
+
let obj: Record<string, unknown> | null = null;
|
|
1337
|
+
try {
|
|
1338
|
+
const parsed: unknown = JSON.parse(line);
|
|
1339
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
1340
|
+
obj = parsed as Record<string, unknown>;
|
|
1341
|
+
}
|
|
1342
|
+
} catch {
|
|
1343
|
+
obj = null;
|
|
1344
|
+
}
|
|
1345
|
+
if (
|
|
1346
|
+
obj &&
|
|
1347
|
+
obj.state === 'query' &&
|
|
1348
|
+
typeof obj.query_id === 'number' &&
|
|
1349
|
+
(obj.operation === 'read' || obj.operation === 'write') &&
|
|
1350
|
+
typeof obj.path === 'string'
|
|
1351
|
+
) {
|
|
1352
|
+
handleQuery(obj.query_id, obj.operation, obj.path, line);
|
|
1353
|
+
} else {
|
|
1354
|
+
// Informational trap (network denials, etc.): keep for post-close handling.
|
|
1355
|
+
appendErrorLine(line);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1253
1358
|
});
|
|
1254
1359
|
|
|
1255
1360
|
child.on('error', (error) => {
|
|
@@ -1271,39 +1376,12 @@ export function createLandstripIntegration(
|
|
|
1271
1376
|
|
|
1272
1377
|
const errorOutput = errorFdAcc || stderrAcc;
|
|
1273
1378
|
|
|
1379
|
+
// Filesystem denials are now answered live during execution; only
|
|
1380
|
+
// informational traps (network, etc.) remain to surface here.
|
|
1274
1381
|
const blockedPath =
|
|
1275
1382
|
extractBlockedPath(errorOutput, cwd) ??
|
|
1276
1383
|
(errorFdAcc ? extractBlockedPath(stderrAcc, cwd) : null);
|
|
1277
|
-
|
|
1278
|
-
extractBlockedWritePath(errorOutput, cwd) ??
|
|
1279
|
-
(errorFdAcc ? extractBlockedWritePath(stderrAcc, cwd) : null);
|
|
1280
|
-
if (blockedPath && ctx.hasUI && callbacks.promptOnBlock) {
|
|
1281
|
-
const config = loadConfig(cwd);
|
|
1282
|
-
const isDeniedByDenyRead = matchesPattern(
|
|
1283
|
-
blockedPath,
|
|
1284
|
-
config.filesystem.denyRead,
|
|
1285
|
-
);
|
|
1286
|
-
const isReadAllowed = matchesPattern(blockedPath, getEffectiveAllowRead(config));
|
|
1287
|
-
const isWriteAllowed = !shouldPromptForWrite(
|
|
1288
|
-
blockedPath,
|
|
1289
|
-
getEffectiveAllowWrite(config),
|
|
1290
|
-
);
|
|
1291
|
-
|
|
1292
|
-
if (blockedWritePath === blockedPath && !isWriteAllowed) {
|
|
1293
|
-
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1294
|
-
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1295
|
-
} else if (isDeniedByDenyRead || !isReadAllowed) {
|
|
1296
|
-
const choice = await promptReadBlock(
|
|
1297
|
-
ctx,
|
|
1298
|
-
blockedPath,
|
|
1299
|
-
isDeniedByDenyRead ? 'granting allowRead will override it' : undefined,
|
|
1300
|
-
);
|
|
1301
|
-
if (choice !== 'abort') await applyReadChoice(choice, blockedPath, cwd);
|
|
1302
|
-
} else if (!isWriteAllowed) {
|
|
1303
|
-
const choice = await promptWriteBlock(ctx, blockedPath);
|
|
1304
|
-
if (choice !== 'abort') await applyWriteChoice(choice, blockedPath, cwd);
|
|
1305
|
-
}
|
|
1306
|
-
} else if (!blockedPath && ctx.hasUI) {
|
|
1384
|
+
if (!blockedPath && ctx.hasUI) {
|
|
1307
1385
|
const landstripErrors = parseLandstripTraps(errorOutput);
|
|
1308
1386
|
if (landstripErrors.length > 0) {
|
|
1309
1387
|
const formatted = formatLandstripTraps(landstripErrors);
|