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.
Files changed (2) hide show
  1. package/index.ts +91 -54
  2. 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
- const config = readOrEmptyConfig(configPath);
236
- const existing = config.network?.allowedDomains ?? [];
237
- if (existing.includes(domain)) return;
238
-
239
- config.network = {
240
- ...config.network,
241
- allowedDomains: [...existing, domain],
242
- deniedDomains: config.network?.deniedDomains ?? [],
243
- } as SandboxNetworkConfig;
244
- writeConfigFile(configPath, config);
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
- const config = readOrEmptyConfig(configPath);
249
- const existing = config.filesystem?.allowRead ?? [];
250
- if (existing.includes(pathToAdd)) return;
251
-
252
- config.filesystem = {
253
- ...config.filesystem,
254
- allowRead: [...existing, pathToAdd],
255
- } as SandboxFilesystemConfig;
256
- writeConfigFile(configPath, config);
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
- const config = readOrEmptyConfig(configPath);
261
- const existing = config.filesystem?.allowWrite ?? [];
262
- if (existing.includes(pathToAdd)) return;
263
-
264
- config.filesystem = {
265
- ...config.filesystem,
266
- allowWrite: [...existing, pathToAdd],
267
- } as SandboxFilesystemConfig;
268
- writeConfigFile(configPath, config);
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
- ctx.ui.notify(`Sandbox blocked an operation: ${formatted}`, 'warning');
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
- ctx.ui.notify(
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
- ctx.ui.notify(
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
- ctx.ui.notify('Network sandbox is disabled because network.allowNetwork is true.', 'warning');
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
- ctx.ui.notify(
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.ui.setStatus('sandbox', `${dot} ${label} ${sep} ${net} ${sep} ${write}`);
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
- ctx.ui.notify(`landstrip sandboxing is not supported on ${process.platform}`, 'warning');
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
- ctx.ui.notify(
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
- ctx.ui.notify(
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
- ctx.ui.notify('Sandbox disabled via --no-sandbox', 'warning');
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
- ctx.ui.notify('Sandbox disabled via config', 'info');
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
- ctx.ui.notify('Sandbox is already enabled', 'info');
1571
+ notify(ctx, 'Sandbox is already enabled', 'info');
1536
1572
  return;
1537
1573
  }
1538
1574
 
1539
- if (enableSandbox(ctx)) ctx.ui.notify('Sandbox enabled', 'info');
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
- ctx.ui.notify('Sandbox is already disabled', 'info');
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.ui.setStatus('sandbox', undefined);
1554
- ctx.ui.notify('Sandbox disabled', 'info');
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
- ctx.ui.notify('Sandbox is disabled', 'info');
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.4",
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.9"
34
+ "@jarkkojs/landstrip": "^0.11.10"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "^0.78.0",