pi-landstrip 0.11.8 → 0.11.9

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 +2 -1
  2. package/index.ts +135 -100
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -34,10 +34,11 @@ Use Pi settings to toggle sandboxing:
34
34
  ```
35
35
 
36
36
  Project Pi settings override global Pi settings.
37
+ The `/sandbox` UI updates the project setting when present, otherwise the global setting.
37
38
 
38
39
  ## Usage
39
40
 
40
- Use `/sandbox` inside Pi to show the active config.
41
+ Use `/sandbox` inside Pi to show the active config and toggle the Pi setting.
41
42
 
42
43
  ## License
43
44
 
package/index.ts CHANGED
@@ -63,11 +63,14 @@ interface SandboxConfig {
63
63
  }
64
64
 
65
65
  interface PiLandstripSettings {
66
+ [key: string]: unknown;
66
67
  landstrip?: {
67
68
  enabled?: boolean;
68
69
  };
69
70
  }
70
71
 
72
+ type PiSettingsScope = 'global' | 'project';
73
+
71
74
  interface LandstripPolicy {
72
75
  network: {
73
76
  allowNetwork: boolean;
@@ -190,14 +193,53 @@ function loadConfig(cwd: string): SandboxConfig {
190
193
  return deepMerge(deepMerge(DEFAULT_CONFIG, globalConfig), projectConfig);
191
194
  }
192
195
 
193
- function isSandboxEnabledInPiSettings(cwd: string): boolean {
196
+ function getPiLandstripSettings(cwd: string): {
197
+ globalSettings: PiLandstripSettings;
198
+ projectSettings: PiLandstripSettings;
199
+ } {
194
200
  const settings = SettingsManager.create(cwd);
195
- const globalSettings = settings.getGlobalSettings() as PiLandstripSettings;
196
- const projectSettings = settings.getProjectSettings() as PiLandstripSettings;
201
+ return {
202
+ globalSettings: settings.getGlobalSettings() as PiLandstripSettings,
203
+ projectSettings: settings.getProjectSettings() as PiLandstripSettings,
204
+ };
205
+ }
197
206
 
207
+ function isSandboxEnabledInPiSettings(cwd: string): boolean {
208
+ const { globalSettings, projectSettings } = getPiLandstripSettings(cwd);
198
209
  return projectSettings.landstrip?.enabled ?? globalSettings.landstrip?.enabled ?? true;
199
210
  }
200
211
 
212
+ function getPiSettingsWriteScope(cwd: string): PiSettingsScope {
213
+ const { projectSettings } = getPiLandstripSettings(cwd);
214
+ return projectSettings.landstrip?.enabled === undefined ? 'global' : 'project';
215
+ }
216
+
217
+ function getPiSettingsPath(cwd: string, scope: PiSettingsScope): string {
218
+ if (scope === 'project') return join(cwd, '.pi', 'settings.json');
219
+ return join(getAgentDir(), 'settings.json');
220
+ }
221
+
222
+ function readPiSettingsFile(settingsPath: string): PiLandstripSettings {
223
+ if (!existsSync(settingsPath)) return {};
224
+ return JSON.parse(readFileSync(settingsPath, 'utf-8')) as PiLandstripSettings;
225
+ }
226
+
227
+ async function setPiSettingsSandboxEnabled(
228
+ cwd: string,
229
+ enabled: boolean,
230
+ ): Promise<PiSettingsScope> {
231
+ const scope = getPiSettingsWriteScope(cwd);
232
+ const settingsPath = getPiSettingsPath(cwd, scope);
233
+ await withFileMutationQueue(settingsPath, async () => {
234
+ const settings = readPiSettingsFile(settingsPath);
235
+ settings.landstrip = { ...settings.landstrip, enabled };
236
+ mkdirSync(dirname(settingsPath), { recursive: true });
237
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
238
+ });
239
+
240
+ return scope;
241
+ }
242
+
201
243
  function mergeArray(base: string[], override?: string[]): string[] {
202
244
  if (!override) return base;
203
245
  return [...new Set([...base, ...override])];
@@ -1610,10 +1652,7 @@ export function createLandstripIntegration(
1610
1652
  maybePi.registerCommand?.('sandbox', {
1611
1653
  description: 'Show sandbox configuration',
1612
1654
  handler: async (_args, ctx) => {
1613
- if (!sandboxEnabled) {
1614
- notify(ctx, 'Sandbox is disabled', 'info');
1615
- return;
1616
- }
1655
+ let piSettingsEnabled = isSandboxEnabledInPiSettings(ctx.cwd);
1617
1656
 
1618
1657
  const config = loadConfig(ctx.cwd);
1619
1658
  const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
@@ -1627,6 +1666,14 @@ export function createLandstripIntegration(
1627
1666
  const text = (s: string) => theme.fg('text', s);
1628
1667
  const borderFg = (s: string) => theme.fg('border', s);
1629
1668
 
1669
+ function sandboxStatus(): { color: 'success' | 'warning'; label: string } {
1670
+ if (noSandboxFlag) return { color: 'warning', label: 'Disabled (--no-sandbox)' };
1671
+ if (!piSettingsEnabled) return { color: 'warning', label: 'Disabled (Pi setting)' };
1672
+ if (!config.enabled) return { color: 'warning', label: 'Disabled (config)' };
1673
+ if (!sandboxEnabled || !sandboxReady) return { color: 'warning', label: 'Inactive' };
1674
+ return { color: 'success', label: 'Active' };
1675
+ }
1676
+
1630
1677
  function boolVal(v: boolean): string {
1631
1678
  return v ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
1632
1679
  }
@@ -1639,120 +1686,108 @@ export function createLandstripIntegration(
1639
1686
 
1640
1687
  return {
1641
1688
  render(width: number): string[] {
1642
- const innerW = width - 4;
1689
+ const innerW = Math.max(1, width - 4);
1643
1690
  const border = borderFg('│');
1644
- const row = (c: string) => makeRow(c, innerW, border);
1691
+ const row = (content: string) => makeRow(content, innerW, border);
1645
1692
  const lines: string[] = [];
1693
+ const status = sandboxStatus();
1694
+ const toggleValue = piSettingsEnabled
1695
+ ? theme.fg('success', 'enabled')
1696
+ : theme.fg('warning', 'disabled');
1697
+
1698
+ function topBorder(titleText: string): string {
1699
+ const title = accent(` ${titleText} `);
1700
+ const fill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
1701
+ return `${borderFg('╭─')}${title}${fill}${borderFg('─╮')}`;
1702
+ }
1646
1703
 
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('─╮')}`);
1704
+ function section(titleText: string, detail?: string): void {
1705
+ lines.push(row(''));
1706
+ lines.push(row(`${accent(titleText)}${detail ? dim(` · ${detail}`) : ''}`));
1707
+ }
1651
1708
 
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
- );
1709
+ function item(label: string, value: string): void {
1710
+ lines.push(row(` ${dim('')} ${muted(label.padEnd(13))} ${value}`));
1711
+ }
1660
1712
 
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)}`));
1713
+ function listValue(values: string[], maxWidth: number): string {
1714
+ const value = values.join(', ') || 'none';
1715
+ return text(truncateToWidth(value, Math.max(10, maxWidth)));
1716
+ }
1665
1717
 
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)';
1718
+ lines.push(topBorder('Sandbox'));
1719
+
1720
+ const statusDot = theme.fg(status.color, '');
1721
+ const pathSnippet = text(truncateToWidth(binaryPath(), Math.max(20, innerW - 28)));
1683
1722
  lines.push(
1684
1723
  row(
1685
- ` ${dim('')} ${muted('Denied:')} ${text(truncateToWidth(denyStr, Math.max(10, innerW - 14)))}`,
1724
+ `${statusDot} ${text(status.label)} ${dim('·')} Pi setting ${toggleValue} ${dim('·')} ${muted('landstrip')} ${pathSnippet}`,
1686
1725
  ),
1687
1726
  );
1688
- if (sessionAllowedDomains.length > 0) {
1689
- lines.push(
1690
- row(
1691
- ` ${dim('')} ${muted('Session:')} ${theme.fg('accent', sessionAllowedDomains.join(', '))}`,
1692
- ),
1693
- );
1727
+
1728
+ section('Config');
1729
+ item('project', text(projectPath));
1730
+ item('global', text(globalPath));
1731
+
1732
+ const netMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
1733
+ section('Network', netMode);
1734
+ item('allow network', boolVal(config.network.allowNetwork));
1735
+ item('allowed', listValue(config.network.allowedDomains, innerW - 17));
1736
+ item('denied', listValue(config.network.deniedDomains, innerW - 17));
1737
+ if (sessionAllowedDomains.length > 0)
1738
+ item('session', theme.fg('accent', sessionAllowedDomains.join(', ')));
1739
+
1740
+ section('Filesystem');
1741
+ item('deny read', listValue(config.filesystem.denyRead, innerW - 17));
1742
+ item('allow read', listValue(config.filesystem.allowRead, innerW - 17));
1743
+ item('allow write', listValue(config.filesystem.allowWrite, innerW - 17));
1744
+ item('deny write', listValue(config.filesystem.denyWrite, innerW - 17));
1745
+
1746
+ if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
1747
+ section('Session grants');
1748
+ if (sessionAllowedReadPaths.length > 0)
1749
+ item('read', theme.fg('accent', sessionAllowedReadPaths.join(', ')));
1750
+ if (sessionAllowedWritePaths.length > 0)
1751
+ item('write', theme.fg('accent', sessionAllowedWritePaths.join(', ')));
1694
1752
  }
1695
1753
 
1696
- // Filesystem section
1697
1754
  lines.push(row(''));
1698
- lines.push(row(`${'─'.repeat(innerW)}`));
1699
- lines.push(row(` ${accent('Filesystem')}`));
1700
- const denyReadStr = config.filesystem.denyRead.join(', ') || '(none)';
1701
- lines.push(
1702
- row(
1703
- ` ${dim('•')} ${muted('Deny read:')} ${text(truncateToWidth(denyReadStr, Math.max(10, innerW - 16)))}`,
1704
- ),
1705
- );
1706
- const allowReadStr = config.filesystem.allowRead.join(', ') || '(none)';
1707
- 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
1755
  lines.push(
1714
- row(
1715
- ` ${dim('•')} ${muted('Allow write:')} ${text(truncateToWidth(allowWriteStr, Math.max(10, innerW - 18)))}`,
1716
- ),
1756
+ row(`${dim('t')} ${muted('toggle Pi setting')} ${dim('esc')} ${muted('close')}`),
1717
1757
  );
1718
- const denyWriteStr = config.filesystem.denyWrite.join(', ') || '(none)';
1719
1758
  lines.push(
1720
- row(
1721
- ` ${dim('•')} ${muted('Deny write:')} ${text(truncateToWidth(denyWriteStr, Math.max(10, innerW - 17)))}`,
1722
- ),
1759
+ `${borderFg('╰')}${borderFg('─'.repeat(Math.max(0, width - 2)))}${borderFg('╯')}`,
1723
1760
  );
1724
1761
 
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
1762
  return lines;
1752
1763
  },
1753
1764
 
1754
- handleInput(): void {
1755
- done(undefined);
1765
+ handleInput(data: string): void {
1766
+ if (data !== 't' && data !== 'T') {
1767
+ done(undefined);
1768
+ return;
1769
+ }
1770
+
1771
+ void (async () => {
1772
+ const enabled = !piSettingsEnabled;
1773
+ const scope = await setPiSettingsSandboxEnabled(ctx.cwd, enabled);
1774
+ piSettingsEnabled = isSandboxEnabledInPiSettings(ctx.cwd);
1775
+
1776
+ if (!enabled) {
1777
+ disableSandbox(ctx);
1778
+ notify(ctx, `Sandbox disabled via ${scope} Pi settings`, 'info');
1779
+ } else if (noSandboxFlag) {
1780
+ notify(ctx, 'Sandbox remains disabled via --no-sandbox', 'warning');
1781
+ } else if (!config.enabled) {
1782
+ notify(ctx, 'Sandbox remains disabled via config', 'info');
1783
+ } else if (enableSandbox(ctx)) {
1784
+ notify(ctx, `Sandbox enabled via ${scope} Pi settings`, 'info');
1785
+ }
1786
+
1787
+ tui.requestRender();
1788
+ })().catch((error: unknown) => {
1789
+ notify(ctx, `Could not update Pi settings: ${error}`, 'error');
1790
+ });
1756
1791
  },
1757
1792
 
1758
1793
  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.9",
4
4
  "description": "Landlock-based sandboxing for pi with interactive permission prompts",
5
5
  "keywords": [
6
6
  "landstrip",