pi-landstrip 0.11.8 → 0.11.10

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 (3) hide show
  1. package/README.md +5 -6
  2. package/index.ts +109 -120
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,21 +23,20 @@ Project config takes precedence.
23
23
 
24
24
  See [`sandbox.json`](./sandbox.json) for a starter config.
25
25
 
26
- Use Pi settings to toggle sandboxing:
26
+ Use sandbox config to toggle sandboxing:
27
27
 
28
28
  ```json
29
29
  {
30
- "landstrip": {
31
- "enabled": false
32
- }
30
+ "enabled": false
33
31
  }
34
32
  ```
35
33
 
36
- Project Pi settings override global Pi settings.
34
+ Project config overrides global config.
35
+ The `/sandbox` UI updates the project config when present, otherwise the global config.
37
36
 
38
37
  ## Usage
39
38
 
40
- Use `/sandbox` inside Pi to show the active config.
39
+ Use `/sandbox` inside Pi to show the active config and toggle sandboxing.
41
40
 
42
41
  ## License
43
42
 
package/index.ts CHANGED
@@ -62,11 +62,7 @@ interface SandboxConfig {
62
62
  filesystem: SandboxFilesystemConfig;
63
63
  }
64
64
 
65
- interface PiLandstripSettings {
66
- landstrip?: {
67
- enabled?: boolean;
68
- };
69
- }
65
+ type SandboxConfigScope = 'global' | 'project';
70
66
 
71
67
  interface LandstripPolicy {
72
68
  network: {
@@ -190,14 +186,6 @@ function loadConfig(cwd: string): SandboxConfig {
190
186
  return deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig);
191
187
  }
192
188
 
193
- function isSandboxEnabledInPiSettings(cwd: string): boolean {
194
- const settings = SettingsManager.create(cwd);
195
- const globalSettings = settings.getGlobalSettings() as PiLandstripSettings;
196
- const projectSettings = settings.getProjectSettings() as PiLandstripSettings;
197
-
198
- return projectSettings.landstrip?.enabled ?? globalSettings.landstrip?.enabled ?? true;
199
- }
200
-
201
189
  function mergeArray(base: string[], override?: string[]): string[] {
202
190
  if (!override) return base;
203
191
  return [...new Set([...base, ...override])];
@@ -247,6 +235,25 @@ function writeConfigFile(configPath: string, config: Partial<SandboxConfig>): vo
247
235
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
248
236
  }
249
237
 
238
+ function getSandboxConfigWriteTarget(cwd: string): { scope: SandboxConfigScope; path: string } {
239
+ const { globalPath, projectPath } = getConfigPaths(cwd);
240
+ const projectConfig = readOrEmptyConfig(projectPath);
241
+
242
+ if (projectConfig.enabled !== undefined) return { scope: 'project', path: projectPath };
243
+ return { scope: 'global', path: globalPath };
244
+ }
245
+
246
+ async function setSandboxConfigEnabled(cwd: string, enabled: boolean): Promise<SandboxConfigScope> {
247
+ const { scope, path } = getSandboxConfigWriteTarget(cwd);
248
+ await withFileMutationQueue(path, async () => {
249
+ const config = readOrEmptyConfig(path);
250
+ config.enabled = enabled;
251
+ writeConfigFile(path, config);
252
+ });
253
+
254
+ return scope;
255
+ }
256
+
250
257
  async function addDomainToConfig(configPath: string, domain: string): Promise<void> {
251
258
  await withFileMutationQueue(configPath, async () => {
252
259
  const config = readOrEmptyConfig(configPath);
@@ -1457,11 +1464,6 @@ export function createLandstripIntegration(
1457
1464
  return false;
1458
1465
  }
1459
1466
 
1460
- if (!isSandboxEnabledInPiSettings(ctx.cwd)) {
1461
- disableSandbox(ctx);
1462
- return false;
1463
- }
1464
-
1465
1467
  const config = loadConfig(ctx.cwd);
1466
1468
  if (!config.enabled) {
1467
1469
  disableSandbox(ctx);
@@ -1592,12 +1594,6 @@ export function createLandstripIntegration(
1592
1594
  return;
1593
1595
  }
1594
1596
 
1595
- if (!isSandboxEnabledInPiSettings(ctx.cwd)) {
1596
- disableSandbox(ctx);
1597
- notify(ctx, 'Sandbox disabled via pi settings', 'info');
1598
- return;
1599
- }
1600
-
1601
1597
  const config = loadConfig(ctx.cwd);
1602
1598
  if (!config.enabled) {
1603
1599
  disableSandbox(ctx);
@@ -1610,12 +1606,8 @@ export function createLandstripIntegration(
1610
1606
  maybePi.registerCommand?.('sandbox', {
1611
1607
  description: 'Show sandbox configuration',
1612
1608
  handler: async (_args, ctx) => {
1613
- if (!sandboxEnabled) {
1614
- notify(ctx, 'Sandbox is disabled', 'info');
1615
- return;
1616
- }
1609
+ let config = loadConfig(ctx.cwd);
1617
1610
 
1618
- const config = loadConfig(ctx.cwd);
1619
1611
  const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
1620
1612
 
1621
1613
  if (!ctx.hasUI) return;
@@ -1627,6 +1619,13 @@ export function createLandstripIntegration(
1627
1619
  const text = (s: string) => theme.fg('text', s);
1628
1620
  const borderFg = (s: string) => theme.fg('border', s);
1629
1621
 
1622
+ function sandboxStatus(): { color: 'success' | 'warning'; label: string } {
1623
+ if (noSandboxFlag) return { color: 'warning', label: 'Disabled (--no-sandbox)' };
1624
+ if (!config.enabled) return { color: 'warning', label: 'Disabled' };
1625
+ if (!sandboxEnabled || !sandboxReady) return { color: 'warning', label: 'Inactive' };
1626
+ return { color: 'success', label: 'Active' };
1627
+ }
1628
+
1630
1629
  function boolVal(v: boolean): string {
1631
1630
  return v ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
1632
1631
  }
@@ -1639,120 +1638,110 @@ export function createLandstripIntegration(
1639
1638
 
1640
1639
  return {
1641
1640
  render(width: number): string[] {
1642
- const innerW = width - 4;
1641
+ const innerW = Math.max(1, width - 4);
1643
1642
  const border = borderFg('│');
1644
- const row = (c: string) => makeRow(c, innerW, border);
1643
+ const row = (content: string) => makeRow(content, innerW, border);
1645
1644
  const lines: string[] = [];
1645
+ const status = sandboxStatus();
1646
+ const toggleValue = config.enabled
1647
+ ? theme.fg('success', 'enabled')
1648
+ : theme.fg('warning', 'disabled');
1649
+
1650
+ function topBorder(titleText: string): string {
1651
+ const title = accent(` ${titleText} `);
1652
+ const fill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
1653
+ return `${borderFg('╭─')}${title}${fill}${borderFg('─╮')}`;
1654
+ }
1646
1655
 
1647
- // Top border
1648
- const title = accent(' Sandbox Configuration ');
1649
- const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
1650
- lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
1656
+ function section(titleText: string, detail?: string): void {
1657
+ lines.push(row(''));
1658
+ lines.push(row(`${accent(titleText)}${detail ? dim(` · ${detail}`) : ''}`));
1659
+ }
1651
1660
 
1652
- // Status
1653
- const statusDot = theme.fg('success', '●');
1654
- const pathSnippet = text(truncateToWidth(binaryPath(), Math.max(20, innerW - 27)));
1655
- lines.push(
1656
- row(
1657
- ` ${statusDot} ${text('Active')} ${dim('·')} ${muted('landstrip:')} ${pathSnippet}`,
1658
- ),
1659
- );
1661
+ function item(label: string, value: string): void {
1662
+ lines.push(row(` ${dim('')} ${muted(label.padEnd(13))} ${value}`));
1663
+ }
1660
1664
 
1661
- // Config files
1662
- lines.push(row(` ${dim('Config files:')}`));
1663
- lines.push(row(` ${dim('project')} ${text(projectPath)}`));
1664
- lines.push(row(` ${dim('global')} ${text(globalPath)}`));
1665
+ function listValue(values: string[], maxWidth: number): string {
1666
+ const value = values.join(', ') || 'none';
1667
+ return text(truncateToWidth(value, Math.max(10, maxWidth)));
1668
+ }
1665
1669
 
1666
- // Network section
1667
- lines.push(row(''));
1668
- lines.push(row(`${''.repeat(innerW)}`));
1669
- const netMode = config.network.allowNetwork ? ' (unrestricted)' : ' (proxied)';
1670
- lines.push(row(` ${accent('Network')}${dim(netMode)}`));
1671
- lines.push(
1672
- row(
1673
- ` ${dim('•')} ${muted('Allow network:')} ${boolVal(config.network.allowNetwork)}`,
1674
- ),
1675
- );
1676
- const domainsStr = config.network.allowedDomains.join(', ') || '(none)';
1677
- lines.push(
1678
- row(
1679
- ` ${dim('•')} ${muted('Allowed:')} ${text(truncateToWidth(domainsStr, Math.max(10, innerW - 15)))}`,
1680
- ),
1681
- );
1682
- const denyStr = config.network.deniedDomains.join(', ') || '(none)';
1670
+ lines.push(topBorder('Sandbox'));
1671
+
1672
+ const statusDot = theme.fg(status.color, '');
1673
+ const pathSnippet = text(truncateToWidth(binaryPath(), Math.max(20, innerW - 28)));
1683
1674
  lines.push(
1684
1675
  row(
1685
- ` ${dim('')} ${muted('Denied:')} ${text(truncateToWidth(denyStr, Math.max(10, innerW - 14)))}`,
1676
+ `${statusDot} ${text(status.label)} ${dim('·')} persisted ${toggleValue} ${dim('·')} ${muted('landstrip')} ${pathSnippet}`,
1686
1677
  ),
1687
1678
  );
1688
- if (sessionAllowedDomains.length > 0) {
1689
- lines.push(
1690
- row(
1691
- ` ${dim('')} ${muted('Session:')} ${theme.fg('accent', sessionAllowedDomains.join(', '))}`,
1692
- ),
1693
- );
1679
+
1680
+ section('Config');
1681
+ item('project', text(projectPath));
1682
+ item('global', text(globalPath));
1683
+
1684
+ const netMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
1685
+ section('Network', netMode);
1686
+ item('allow network', boolVal(config.network.allowNetwork));
1687
+ item('allowed', listValue(config.network.allowedDomains, innerW - 17));
1688
+ item('denied', listValue(config.network.deniedDomains, innerW - 17));
1689
+ if (sessionAllowedDomains.length > 0)
1690
+ item('session', theme.fg('accent', sessionAllowedDomains.join(', ')));
1691
+
1692
+ section('Filesystem');
1693
+ item('deny read', listValue(config.filesystem.denyRead, innerW - 17));
1694
+ item('allow read', listValue(config.filesystem.allowRead, innerW - 17));
1695
+ item('allow write', listValue(config.filesystem.allowWrite, innerW - 17));
1696
+ item('deny write', listValue(config.filesystem.denyWrite, innerW - 17));
1697
+
1698
+ if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
1699
+ section('Session grants');
1700
+ if (sessionAllowedReadPaths.length > 0)
1701
+ item('read', theme.fg('accent', sessionAllowedReadPaths.join(', ')));
1702
+ if (sessionAllowedWritePaths.length > 0)
1703
+ item('write', theme.fg('accent', sessionAllowedWritePaths.join(', ')));
1694
1704
  }
1695
1705
 
1696
- // Filesystem section
1697
1706
  lines.push(row(''));
1698
- lines.push(row(`${'─'.repeat(innerW)}`));
1699
- lines.push(row(` ${accent('Filesystem')}`));
1700
- const denyReadStr = config.filesystem.denyRead.join(', ') || '(none)';
1701
1707
  lines.push(
1702
1708
  row(
1703
- ` ${dim('')} ${muted('Deny read:')} ${text(truncateToWidth(denyReadStr, Math.max(10, innerW - 16)))}`,
1709
+ `${dim('t')} ${muted('toggle persisted setting')} ${dim('esc')} ${muted('close')}`,
1704
1710
  ),
1705
1711
  );
1706
- const allowReadStr = config.filesystem.allowRead.join(', ') || '(none)';
1707
1712
  lines.push(
1708
- row(
1709
- ` ${dim('•')} ${muted('Allow read:')} ${text(truncateToWidth(allowReadStr, Math.max(10, innerW - 17)))}`,
1710
- ),
1711
- );
1712
- const allowWriteStr = config.filesystem.allowWrite.join(', ') || '(none)';
1713
- lines.push(
1714
- row(
1715
- ` ${dim('•')} ${muted('Allow write:')} ${text(truncateToWidth(allowWriteStr, Math.max(10, innerW - 18)))}`,
1716
- ),
1717
- );
1718
- const denyWriteStr = config.filesystem.denyWrite.join(', ') || '(none)';
1719
- lines.push(
1720
- row(
1721
- ` ${dim('•')} ${muted('Deny write:')} ${text(truncateToWidth(denyWriteStr, Math.max(10, innerW - 17)))}`,
1722
- ),
1713
+ `${borderFg('╰')}${borderFg('─'.repeat(Math.max(0, width - 2)))}${borderFg('╯')}`,
1723
1714
  );
1724
1715
 
1725
- // Session allowances
1726
- if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
1727
- lines.push(row(''));
1728
- if (sessionAllowedReadPaths.length > 0) {
1729
- lines.push(
1730
- row(
1731
- ` ${dim('•')} ${muted('Session read:')} ${theme.fg('accent', sessionAllowedReadPaths.join(', '))}`,
1732
- ),
1733
- );
1734
- }
1735
- if (sessionAllowedWritePaths.length > 0) {
1736
- lines.push(
1737
- row(
1738
- ` ${dim('•')} ${muted('Session write:')} ${theme.fg('accent', sessionAllowedWritePaths.join(', '))}`,
1739
- ),
1740
- );
1741
- }
1742
- }
1743
-
1744
- // Footer
1745
- lines.push(row(''));
1746
- lines.push(row(` ${dim('esc')} ${muted('or any key to close')}`));
1747
-
1748
- // Bottom border
1749
- lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
1750
-
1751
1716
  return lines;
1752
1717
  },
1753
1718
 
1754
- handleInput(): void {
1755
- done(undefined);
1719
+ handleInput(data: string): void {
1720
+ if (data !== 't' && data !== 'T') {
1721
+ done(undefined);
1722
+ return;
1723
+ }
1724
+
1725
+ void (async () => {
1726
+ const enabled = !config.enabled;
1727
+ const scope = await setSandboxConfigEnabled(ctx.cwd, enabled);
1728
+ config = loadConfig(ctx.cwd);
1729
+
1730
+ if (!enabled) {
1731
+ disableSandbox(ctx);
1732
+ notify(ctx, `Sandbox disabled in ${scope} config`, 'info');
1733
+ } else if (noSandboxFlag) {
1734
+ notify(ctx, 'Sandbox remains disabled via --no-sandbox', 'warning');
1735
+ } else if (!config.enabled) {
1736
+ notify(ctx, 'Sandbox remains disabled via config', 'info');
1737
+ } else if (enableSandbox(ctx)) {
1738
+ notify(ctx, `Sandbox enabled in ${scope} config`, 'info');
1739
+ }
1740
+
1741
+ tui.requestRender();
1742
+ })().catch((error: unknown) => {
1743
+ notify(ctx, `Could not update config: ${error}`, 'error');
1744
+ });
1756
1745
  },
1757
1746
 
1758
1747
  invalidate(): void {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.11.8",
3
+ "version": "0.11.10",
4
4
  "description": "Landlock-based sandboxing for pi with interactive permission prompts",
5
5
  "keywords": [
6
6
  "landstrip",