pi-landstrip 0.11.4 → 0.11.6
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 +91 -54
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
getShellConfig,
|
|
37
37
|
isToolCallEventType,
|
|
38
38
|
SettingsManager,
|
|
39
|
+
withFileMutationQueue,
|
|
39
40
|
} from '@earendil-works/pi-coding-agent';
|
|
40
41
|
import { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
|
|
41
42
|
|
|
@@ -128,6 +129,7 @@ const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
128
129
|
};
|
|
129
130
|
|
|
130
131
|
type PermissionChoice = 'abort' | 'session' | 'project' | 'global';
|
|
132
|
+
type NotificationLevel = Parameters<ExtensionContext['ui']['notify']>[1];
|
|
131
133
|
|
|
132
134
|
interface PromptOption {
|
|
133
135
|
label: string;
|
|
@@ -231,41 +233,47 @@ function writeConfigFile(configPath: string, config: Partial<SandboxConfig>): vo
|
|
|
231
233
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
232
234
|
}
|
|
233
235
|
|
|
234
|
-
function addDomainToConfig(configPath: string, domain: string): void {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
236
|
+
async function addDomainToConfig(configPath: string, domain: string): Promise<void> {
|
|
237
|
+
await withFileMutationQueue(configPath, async () => {
|
|
238
|
+
const config = readOrEmptyConfig(configPath);
|
|
239
|
+
const existing = config.network?.allowedDomains ?? [];
|
|
240
|
+
if (existing.includes(domain)) return;
|
|
241
|
+
|
|
242
|
+
config.network = {
|
|
243
|
+
...config.network,
|
|
244
|
+
allowedDomains: [...existing, domain],
|
|
245
|
+
deniedDomains: config.network?.deniedDomains ?? [],
|
|
246
|
+
} as SandboxNetworkConfig;
|
|
247
|
+
writeConfigFile(configPath, config);
|
|
248
|
+
});
|
|
245
249
|
}
|
|
246
250
|
|
|
247
|
-
function addReadPathToConfig(configPath: string, pathToAdd: string): void {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
251
|
+
async function addReadPathToConfig(configPath: string, pathToAdd: string): Promise<void> {
|
|
252
|
+
await withFileMutationQueue(configPath, async () => {
|
|
253
|
+
const config = readOrEmptyConfig(configPath);
|
|
254
|
+
const existing = config.filesystem?.allowRead ?? [];
|
|
255
|
+
if (existing.includes(pathToAdd)) return;
|
|
256
|
+
|
|
257
|
+
config.filesystem = {
|
|
258
|
+
...config.filesystem,
|
|
259
|
+
allowRead: [...existing, pathToAdd],
|
|
260
|
+
} as SandboxFilesystemConfig;
|
|
261
|
+
writeConfigFile(configPath, config);
|
|
262
|
+
});
|
|
257
263
|
}
|
|
258
264
|
|
|
259
|
-
function addWritePathToConfig(configPath: string, pathToAdd: string): void {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
async function addWritePathToConfig(configPath: string, pathToAdd: string): Promise<void> {
|
|
266
|
+
await withFileMutationQueue(configPath, async () => {
|
|
267
|
+
const config = readOrEmptyConfig(configPath);
|
|
268
|
+
const existing = config.filesystem?.allowWrite ?? [];
|
|
269
|
+
if (existing.includes(pathToAdd)) return;
|
|
270
|
+
|
|
271
|
+
config.filesystem = {
|
|
272
|
+
...config.filesystem,
|
|
273
|
+
allowWrite: [...existing, pathToAdd],
|
|
274
|
+
} as SandboxFilesystemConfig;
|
|
275
|
+
writeConfigFile(configPath, config);
|
|
276
|
+
});
|
|
269
277
|
}
|
|
270
278
|
|
|
271
279
|
function extractDomainsFromCommand(command: string): string[] {
|
|
@@ -547,6 +555,21 @@ function formatLandstripErrors(errors: LandstripErrorResponse[]): string {
|
|
|
547
555
|
.join('\n');
|
|
548
556
|
}
|
|
549
557
|
|
|
558
|
+
function notify(ctx: ExtensionContext, message: string, level: NotificationLevel): void {
|
|
559
|
+
if (!ctx.hasUI) return;
|
|
560
|
+
ctx.ui.notify(message, level);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function hasTuiStatus(ctx: ExtensionContext): boolean {
|
|
564
|
+
const { mode } = ctx as ExtensionContext & { mode?: string };
|
|
565
|
+
return mode === undefined ? ctx.hasUI : mode === 'tui';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function setTuiStatus(ctx: ExtensionContext, key: string, value: string | undefined): void {
|
|
569
|
+
if (!hasTuiStatus(ctx)) return;
|
|
570
|
+
ctx.ui.setStatus(key, value);
|
|
571
|
+
}
|
|
572
|
+
|
|
550
573
|
async function showPermissionPrompt(
|
|
551
574
|
ctx: ExtensionContext,
|
|
552
575
|
title: string,
|
|
@@ -857,6 +880,12 @@ export function createLandstripIntegration(
|
|
|
857
880
|
const sessionAllowedReadPaths: string[] = [];
|
|
858
881
|
const sessionAllowedWritePaths: string[] = [];
|
|
859
882
|
|
|
883
|
+
function resetSessionAllowances(): void {
|
|
884
|
+
sessionAllowedDomains.length = 0;
|
|
885
|
+
sessionAllowedReadPaths.length = 0;
|
|
886
|
+
sessionAllowedWritePaths.length = 0;
|
|
887
|
+
}
|
|
888
|
+
|
|
860
889
|
function getEffectiveAllowedDomains(cwd: string): string[] {
|
|
861
890
|
const config = loadConfig(cwd);
|
|
862
891
|
return [...config.network.allowedDomains, ...sessionAllowedDomains];
|
|
@@ -879,8 +908,8 @@ export function createLandstripIntegration(
|
|
|
879
908
|
): Promise<void> {
|
|
880
909
|
const { globalPath, projectPath } = getConfigPaths(cwd);
|
|
881
910
|
if (!sessionAllowedDomains.includes(domain)) sessionAllowedDomains.push(domain);
|
|
882
|
-
if (choice === 'project') addDomainToConfig(projectPath, domain);
|
|
883
|
-
if (choice === 'global') addDomainToConfig(globalPath, domain);
|
|
911
|
+
if (choice === 'project') await addDomainToConfig(projectPath, domain);
|
|
912
|
+
if (choice === 'global') await addDomainToConfig(globalPath, domain);
|
|
884
913
|
}
|
|
885
914
|
|
|
886
915
|
async function applyReadChoice(
|
|
@@ -890,8 +919,8 @@ export function createLandstripIntegration(
|
|
|
890
919
|
): Promise<void> {
|
|
891
920
|
const { globalPath, projectPath } = getConfigPaths(cwd);
|
|
892
921
|
if (!sessionAllowedReadPaths.includes(filePath)) sessionAllowedReadPaths.push(filePath);
|
|
893
|
-
if (choice === 'project') addReadPathToConfig(projectPath, filePath);
|
|
894
|
-
if (choice === 'global') addReadPathToConfig(globalPath, filePath);
|
|
922
|
+
if (choice === 'project') await addReadPathToConfig(projectPath, filePath);
|
|
923
|
+
if (choice === 'global') await addReadPathToConfig(globalPath, filePath);
|
|
895
924
|
}
|
|
896
925
|
|
|
897
926
|
async function applyWriteChoice(
|
|
@@ -901,8 +930,8 @@ export function createLandstripIntegration(
|
|
|
901
930
|
): Promise<void> {
|
|
902
931
|
const { globalPath, projectPath } = getConfigPaths(cwd);
|
|
903
932
|
if (!sessionAllowedWritePaths.includes(filePath)) sessionAllowedWritePaths.push(filePath);
|
|
904
|
-
if (choice === 'project') addWritePathToConfig(projectPath, filePath);
|
|
905
|
-
if (choice === 'global') addWritePathToConfig(globalPath, filePath);
|
|
933
|
+
if (choice === 'project') await addWritePathToConfig(projectPath, filePath);
|
|
934
|
+
if (choice === 'global') await addWritePathToConfig(globalPath, filePath);
|
|
906
935
|
}
|
|
907
936
|
|
|
908
937
|
async function ensureDomainAllowed(
|
|
@@ -1201,7 +1230,7 @@ export function createLandstripIntegration(
|
|
|
1201
1230
|
const landstripErrors = parseLandstripErrors(errorOutput);
|
|
1202
1231
|
if (landstripErrors.length > 0) {
|
|
1203
1232
|
const formatted = formatLandstripErrors(landstripErrors);
|
|
1204
|
-
|
|
1233
|
+
notify(ctx, `Sandbox blocked an operation: ${formatted}`, 'warning');
|
|
1205
1234
|
}
|
|
1206
1235
|
}
|
|
1207
1236
|
|
|
@@ -1242,7 +1271,8 @@ export function createLandstripIntegration(
|
|
|
1242
1271
|
let config = loadConfig(ctx.cwd);
|
|
1243
1272
|
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
1244
1273
|
if (matchesPattern(blockedPath, config.filesystem.denyWrite)) {
|
|
1245
|
-
|
|
1274
|
+
notify(
|
|
1275
|
+
ctx,
|
|
1246
1276
|
`"${blockedPath}" is blocked by denyWrite. Check:\n ${projectPath}\n ${globalPath}`,
|
|
1247
1277
|
'warning',
|
|
1248
1278
|
);
|
|
@@ -1257,7 +1287,8 @@ export function createLandstripIntegration(
|
|
|
1257
1287
|
|
|
1258
1288
|
config = loadConfig(ctx.cwd);
|
|
1259
1289
|
if (matchesPattern(blockedPath, config.filesystem.denyWrite)) {
|
|
1260
|
-
|
|
1290
|
+
notify(
|
|
1291
|
+
ctx,
|
|
1261
1292
|
`"${blockedPath}" was added to allowWrite, but denyWrite still blocks it. Check:\n ${projectPath}\n ${globalPath}`,
|
|
1262
1293
|
'warning',
|
|
1263
1294
|
);
|
|
@@ -1322,17 +1353,19 @@ export function createLandstripIntegration(
|
|
|
1322
1353
|
|
|
1323
1354
|
function warnIfAllDomainsAllowed(ctx: ExtensionContext, config: SandboxConfig): void {
|
|
1324
1355
|
if (config.network.allowNetwork) {
|
|
1325
|
-
|
|
1356
|
+
notify(ctx, 'Network sandbox is disabled because network.allowNetwork is true.', 'warning');
|
|
1326
1357
|
return;
|
|
1327
1358
|
}
|
|
1328
1359
|
if (!allowsAllDomains(config.network.allowedDomains)) return;
|
|
1329
|
-
|
|
1360
|
+
notify(
|
|
1361
|
+
ctx,
|
|
1330
1362
|
'Network sandbox allows all domains because network.allowedDomains contains "*".',
|
|
1331
1363
|
'warning',
|
|
1332
1364
|
);
|
|
1333
1365
|
}
|
|
1334
1366
|
|
|
1335
1367
|
function enableStatus(ctx: ExtensionContext, config: SandboxConfig): void {
|
|
1368
|
+
if (!hasTuiStatus(ctx)) return;
|
|
1336
1369
|
const theme = ctx.ui.theme;
|
|
1337
1370
|
const dot = theme.fg('success', '●');
|
|
1338
1371
|
const label = theme.fg('text', 'Sandbox');
|
|
@@ -1354,7 +1387,7 @@ export function createLandstripIntegration(
|
|
|
1354
1387
|
const net = theme.fg(networkColor, networkLabel);
|
|
1355
1388
|
const write = theme.fg('accent', `${config.filesystem.allowWrite.length} write paths`);
|
|
1356
1389
|
|
|
1357
|
-
ctx
|
|
1390
|
+
setTuiStatus(ctx, 'sandbox', `${dot} ${label} ${sep} ${net} ${sep} ${write}`);
|
|
1358
1391
|
}
|
|
1359
1392
|
|
|
1360
1393
|
function enableSandbox(ctx: ExtensionContext): boolean {
|
|
@@ -1363,7 +1396,7 @@ export function createLandstripIntegration(
|
|
|
1363
1396
|
if (!SUPPORTED_PLATFORMS.has(process.platform)) {
|
|
1364
1397
|
sandboxEnabled = false;
|
|
1365
1398
|
sandboxReady = false;
|
|
1366
|
-
|
|
1399
|
+
notify(ctx, `landstrip sandboxing is not supported on ${process.platform}`, 'warning');
|
|
1367
1400
|
return false;
|
|
1368
1401
|
}
|
|
1369
1402
|
|
|
@@ -1371,7 +1404,8 @@ export function createLandstripIntegration(
|
|
|
1371
1404
|
if (!version) {
|
|
1372
1405
|
sandboxEnabled = false;
|
|
1373
1406
|
sandboxReady = false;
|
|
1374
|
-
|
|
1407
|
+
notify(
|
|
1408
|
+
ctx,
|
|
1375
1409
|
`landstrip was not found. Reinstall with: npm install @jarkkojs/landstrip`,
|
|
1376
1410
|
'error',
|
|
1377
1411
|
);
|
|
@@ -1381,7 +1415,8 @@ export function createLandstripIntegration(
|
|
|
1381
1415
|
if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
|
|
1382
1416
|
sandboxEnabled = false;
|
|
1383
1417
|
sandboxReady = false;
|
|
1384
|
-
|
|
1418
|
+
notify(
|
|
1419
|
+
ctx,
|
|
1385
1420
|
`landstrip ${REQUIRED_LANDSTRIP_VERSION} or newer is required; found: ${version}`,
|
|
1386
1421
|
'error',
|
|
1387
1422
|
);
|
|
@@ -1508,12 +1543,13 @@ export function createLandstripIntegration(
|
|
|
1508
1543
|
});
|
|
1509
1544
|
|
|
1510
1545
|
pi.on('session_start', async (_event, ctx) => {
|
|
1546
|
+
resetSessionAllowances();
|
|
1511
1547
|
const noSandbox = maybePi.getFlag?.('no-sandbox') as boolean;
|
|
1512
1548
|
|
|
1513
1549
|
if (noSandbox) {
|
|
1514
1550
|
sandboxEnabled = false;
|
|
1515
1551
|
sandboxReady = false;
|
|
1516
|
-
|
|
1552
|
+
notify(ctx, 'Sandbox disabled via --no-sandbox', 'warning');
|
|
1517
1553
|
return;
|
|
1518
1554
|
}
|
|
1519
1555
|
|
|
@@ -1521,7 +1557,7 @@ export function createLandstripIntegration(
|
|
|
1521
1557
|
if (!config.enabled) {
|
|
1522
1558
|
sandboxEnabled = false;
|
|
1523
1559
|
sandboxReady = false;
|
|
1524
|
-
|
|
1560
|
+
notify(ctx, 'Sandbox disabled via config', 'info');
|
|
1525
1561
|
return;
|
|
1526
1562
|
}
|
|
1527
1563
|
|
|
@@ -1532,11 +1568,11 @@ export function createLandstripIntegration(
|
|
|
1532
1568
|
description: 'Enable the landstrip sandbox for this session',
|
|
1533
1569
|
handler: async (_args, ctx) => {
|
|
1534
1570
|
if (sandboxEnabled) {
|
|
1535
|
-
|
|
1571
|
+
notify(ctx, 'Sandbox is already enabled', 'info');
|
|
1536
1572
|
return;
|
|
1537
1573
|
}
|
|
1538
1574
|
|
|
1539
|
-
if (enableSandbox(ctx))
|
|
1575
|
+
if (enableSandbox(ctx)) notify(ctx, 'Sandbox enabled', 'info');
|
|
1540
1576
|
},
|
|
1541
1577
|
});
|
|
1542
1578
|
|
|
@@ -1544,14 +1580,14 @@ export function createLandstripIntegration(
|
|
|
1544
1580
|
description: 'Disable the landstrip sandbox for this session',
|
|
1545
1581
|
handler: async (_args, ctx) => {
|
|
1546
1582
|
if (!sandboxEnabled) {
|
|
1547
|
-
|
|
1583
|
+
notify(ctx, 'Sandbox is already disabled', 'info');
|
|
1548
1584
|
return;
|
|
1549
1585
|
}
|
|
1550
1586
|
|
|
1551
1587
|
sandboxEnabled = false;
|
|
1552
1588
|
sandboxReady = false;
|
|
1553
|
-
ctx
|
|
1554
|
-
|
|
1589
|
+
setTuiStatus(ctx, 'sandbox', undefined);
|
|
1590
|
+
notify(ctx, 'Sandbox disabled', 'info');
|
|
1555
1591
|
},
|
|
1556
1592
|
});
|
|
1557
1593
|
|
|
@@ -1559,13 +1595,14 @@ export function createLandstripIntegration(
|
|
|
1559
1595
|
description: 'Show sandbox configuration',
|
|
1560
1596
|
handler: async (_args, ctx) => {
|
|
1561
1597
|
if (!sandboxEnabled) {
|
|
1562
|
-
|
|
1598
|
+
notify(ctx, 'Sandbox is disabled', 'info');
|
|
1563
1599
|
return;
|
|
1564
1600
|
}
|
|
1565
1601
|
|
|
1566
1602
|
const config = loadConfig(ctx.cwd);
|
|
1567
1603
|
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
1568
1604
|
|
|
1605
|
+
if (!ctx.hasUI) return;
|
|
1569
1606
|
await ctx.ui.custom(
|
|
1570
1607
|
(tui, theme, _kb, done) => {
|
|
1571
1608
|
const dim = (s: string) => theme.fg('dim', s);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-landstrip",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
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.10"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "^0.78.0",
|