opencode-landstrip 0.16.20 → 0.16.22

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/README.md CHANGED
@@ -54,6 +54,10 @@ The plugin can be later on disabled as follows:
54
54
  }
55
55
  ```
56
56
 
57
+ The `/sandbox` command shows the current configuration and toggles the sandbox
58
+ on or off. The toggle persists `enabled` to the project config when it already
59
+ sets it, otherwise to the global config.
60
+
57
61
  ## Behavior
58
62
 
59
63
  When OpenCode asks for a sandboxed permission, the plugin emits a host
package/index.ts CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  formatLandstripTraps,
19
19
  getConfigPaths,
20
20
  isRecord,
21
- isSandboxDisabled,
22
21
  landstripBinaryPath,
23
22
  loadConfig,
24
23
  normalizeOptions,
@@ -27,7 +26,6 @@ import {
27
26
  permissionType,
28
27
  sandboxSummary,
29
28
  sessionScopeFor,
30
- setSandboxDisabled,
31
29
  } from './shared.js';
32
30
 
33
31
  interface LandstripPolicy {
@@ -840,10 +838,6 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
840
838
  return [...config.filesystem.allowWrite, ...sessionAllowedWritePaths];
841
839
  }
842
840
  let enabledNotified = false;
843
- let sandboxDisabled = false;
844
- // A previous session may have left a disable flag for this directory; start
845
- // enabled so /sandbox-disable stays scoped to the current session.
846
- setSandboxDisabled(directory, false);
847
841
  let configuredShell: string | undefined;
848
842
  let landstripCheck: { ok: true; version: string } | { ok: false; reason: string } | undefined;
849
843
 
@@ -897,9 +891,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
897
891
 
898
892
  function buildSandboxSummary(config: SandboxConfig): string {
899
893
  const { globalPath, projectPath } = getConfigPaths(directory);
900
- const statusText =
901
- sandboxDisabled || isSandboxDisabled(directory) ? 'disabled for this session' : undefined;
902
- const report = sandboxSummary(config, globalPath, projectPath, statusText);
894
+ const report = sandboxSummary(config, globalPath, projectPath);
903
895
  return ['# Sandbox Configuration', '', report].join('\n');
904
896
  }
905
897
 
@@ -1003,10 +995,6 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
1003
995
  }
1004
996
 
1005
997
  async function activeConfig(): Promise<SandboxConfig | null> {
1006
- // The flag file lets the TUI plugin pause the sandbox cross-process; the
1007
- // in-memory bool covers the server's own command path.
1008
- if (sandboxDisabled || isSandboxDisabled(directory)) return null;
1009
-
1010
998
  const config = loadConfig(directory, optionOverrides);
1011
999
  if (!config.enabled) {
1012
1000
  await notifyOnce(
@@ -1392,72 +1380,6 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
1392
1380
  return;
1393
1381
  }
1394
1382
 
1395
- if (command === 'sandbox-disable') {
1396
- if (sandboxDisabled || isSandboxDisabled(directory)) {
1397
- pushCommandText(
1398
- input,
1399
- output,
1400
- 'Sandbox is already disabled. Use /sandbox-enable to re-enable.',
1401
- );
1402
- return;
1403
- }
1404
- sandboxDisabled = true;
1405
- setSandboxDisabled(directory, true);
1406
- pushCommandText(
1407
- input,
1408
- output,
1409
- 'Sandbox disabled for this session. Use /sandbox-enable to re-enable.',
1410
- );
1411
- await client.tui
1412
- ?.showToast?.({
1413
- body: {
1414
- title: 'Sandbox',
1415
- message: 'Sandbox disabled for this session. Use /sandbox-enable to re-enable.',
1416
- variant: 'warning',
1417
- },
1418
- })
1419
- ?.catch?.(() => undefined);
1420
- return;
1421
- }
1422
-
1423
- if (command === 'sandbox-enable') {
1424
- if (!sandboxDisabled && !isSandboxDisabled(directory)) {
1425
- pushCommandText(
1426
- input,
1427
- output,
1428
- 'Sandbox is already enabled. Use /sandbox-disable to pause.',
1429
- );
1430
- return;
1431
- }
1432
- sandboxDisabled = false;
1433
- setSandboxDisabled(directory, false);
1434
- const config = await activeConfig();
1435
- if (!config) {
1436
- pushCommandText(
1437
- input,
1438
- output,
1439
- 'Sandbox re-enabled but no sandbox.json5 found — no rules active.\nCreate sandbox.json5 to enforce sandboxing.',
1440
- );
1441
- await client.tui
1442
- ?.showToast?.({
1443
- body: {
1444
- title: 'Sandbox',
1445
- message: 'Sandbox re-enabled but no sandbox.json5 found — no rules active.',
1446
- variant: 'warning',
1447
- },
1448
- })
1449
- ?.catch?.(() => undefined);
1450
- } else {
1451
- pushCommandText(input, output, 'Sandbox re-enabled.');
1452
- await client.tui
1453
- ?.showToast?.({
1454
- body: { title: 'Sandbox', message: 'Sandbox re-enabled.', variant: 'success' },
1455
- })
1456
- ?.catch?.(() => undefined);
1457
- }
1458
- return;
1459
- }
1460
-
1461
1383
  // Check domain and filesystem in user shell commands (commands starting with !)
1462
1384
  if (input.command.startsWith('!')) {
1463
1385
  const shellCommand = input.command.slice(1).trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-landstrip",
3
- "version": "0.16.20",
3
+ "version": "0.16.22",
4
4
  "description": "Landlock-based sandboxing for opencode with landstrip",
5
5
  "keywords": [
6
6
  "landlock",
@@ -49,7 +49,7 @@
49
49
  "ci:test": "npm test"
50
50
  },
51
51
  "dependencies": {
52
- "@landstrip/landstrip": "^0.16.15"
52
+ "@landstrip/landstrip": "^0.16.16"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@opencode-ai/plugin": "^1.17.7",
package/shared.ts CHANGED
@@ -542,24 +542,19 @@ export function discoveryFilePath(baseDirectory: string): string {
542
542
  return join(discoveryDir(), `port-${directoryHash(baseDirectory)}.json`);
543
543
  }
544
544
 
545
- // /sandbox-disable pauses the sandbox per-directory. The flag lives beside the
546
- // discovery file so the TUI plugin (which runs the command) and the server
547
- // plugin (which gates wrapping) agree even though they are separate processes.
548
- export function disableFlagPath(baseDirectory: string): string {
549
- return join(discoveryDir(), `disabled-${directoryHash(baseDirectory)}`);
550
- }
551
-
552
- export function setSandboxDisabled(baseDirectory: string, disabled: boolean): void {
553
- if (disabled) {
554
- mkdirSync(discoveryDir(), { recursive: true, mode: 0o700 });
555
- writeFileSync(disableFlagPath(baseDirectory), `${process.pid}\n`);
556
- } else {
557
- rmSync(disableFlagPath(baseDirectory), { force: true });
558
- }
559
- }
560
-
561
- export function isSandboxDisabled(baseDirectory: string): boolean {
562
- return existsSync(disableFlagPath(baseDirectory));
545
+ // /sandbox toggles the persisted `enabled` flag. Write it where the setting
546
+ // already lives the project config if it sets `enabled`, otherwise the global
547
+ // config and return the scope written so the UI can report it.
548
+ export function setSandboxConfigEnabled(
549
+ baseDirectory: string,
550
+ enabled: boolean,
551
+ ): 'project' | 'global' {
552
+ const { globalPath, projectPath } = getConfigPaths(baseDirectory);
553
+ const projectConfig = readConfigFile(projectPath);
554
+ const useProject = projectConfig !== null && projectConfig.enabled !== undefined;
555
+ const target = useProject ? projectPath : globalPath;
556
+ writeConfigFile(target, { enabled });
557
+ return useProject ? 'project' : 'global';
563
558
  }
564
559
 
565
560
  export function writeDiscoveryPort(baseDirectory: string, port: number): void {
package/tui.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  sessionAllows,
16
16
  sandboxSummary,
17
17
  sessionScopeFor,
18
- setSandboxDisabled,
18
+ setSandboxConfigEnabled,
19
19
  updateForPermission,
20
20
  writeConfigFile,
21
21
  writeDiscoveryPort,
@@ -447,74 +447,49 @@ const tui: TuiPlugin = async (api, options, meta) => {
447
447
  });
448
448
  }
449
449
 
450
+ // /sandbox shows the config and toggles the persisted `enabled` flag. The
451
+ // server reads sandbox.json on every tool call, so the toggle takes effect on
452
+ // the next command without any cross-process signalling.
450
453
  const showSandbox = () => {
451
454
  const directory = api.state.path.directory || process.cwd();
452
455
  const config = loadConfig(directory, optionOverrides);
453
456
  const { globalPath, projectPath } = getConfigPaths(directory);
457
+ const next = !config.enabled;
454
458
  const message =
455
- sandboxSummary(config, globalPath, projectPath) + '\n\nPress esc or enter to close';
459
+ sandboxSummary(config, globalPath, projectPath) +
460
+ `\n\n${next ? 'Enable' : 'Disable'} the sandbox? (enter = ${next ? 'enable' : 'disable'}, esc = close)`;
456
461
 
457
- // No `onConfirm`/`onClose` that call `clear()`: the host already pops the
458
- // dialog on enter/esc/click, and its `clear()` re-invokes every entry's
459
- // `onClose`, so a `clear()` in there recurses forever and freezes the TUI.
462
+ // No `clear()` in onConfirm: the host pops the dialog itself, and its
463
+ // `clear()` re-invokes onClose, which would recurse and freeze the TUI.
460
464
  api.ui.dialog.replace(() =>
461
- api.ui.DialogAlert({
462
- title: 'Sandbox Configuration',
465
+ api.ui.DialogConfirm({
466
+ title: 'Sandbox',
463
467
  message,
468
+ onConfirm: () => {
469
+ const scope = setSandboxConfigEnabled(directory, next);
470
+ api.ui.toast({
471
+ title: 'Sandbox',
472
+ message: `Sandbox ${next ? 'enabled' : 'disabled'} (${scope} config)`,
473
+ variant: next ? 'success' : 'warning',
474
+ });
475
+ },
464
476
  }),
465
477
  );
466
478
  };
467
479
 
468
- // Toggle the per-directory disable flag directly. The server plugin gates
469
- // wrapping on this flag, so disabling works without depending on a command
470
- // round-trip reaching the server's command hook.
471
- const setSandbox = (disabled: boolean): boolean => {
472
- setSandboxDisabled(api.state.path.directory || process.cwd(), disabled);
473
- api.ui.toast({
474
- title: 'Sandbox',
475
- message: disabled
476
- ? 'Sandbox disabled for this session. Use /sandbox-enable to re-enable.'
477
- : 'Sandbox re-enabled.',
478
- variant: disabled ? 'warning' : 'success',
479
- });
480
- return true;
481
- };
482
-
483
480
  api.keymap.registerLayer({
484
481
  commands: [
485
482
  {
486
483
  namespace: 'palette',
487
484
  name: 'sandbox',
488
485
  title: 'Sandbox',
489
- desc: 'Show sandbox configuration',
486
+ desc: 'Show config and toggle the sandbox',
490
487
  category: 'Sandbox',
491
488
  suggested: true,
492
489
  slash: { name: 'sandbox' },
493
490
  slashName: 'sandbox',
494
491
  run: showSandbox,
495
492
  },
496
- {
497
- namespace: 'palette',
498
- name: 'sandbox-disable',
499
- title: 'Disable sandbox',
500
- desc: 'Disable sandbox for this session',
501
- category: 'Sandbox',
502
- suggested: true,
503
- slash: { name: 'sandbox-disable' },
504
- slashName: 'sandbox-disable',
505
- run: () => setSandbox(true),
506
- },
507
- {
508
- namespace: 'palette',
509
- name: 'sandbox-enable',
510
- title: 'Enable sandbox',
511
- desc: 'Re-enable sandbox for this session',
512
- category: 'Sandbox',
513
- suggested: true,
514
- slash: { name: 'sandbox-enable' },
515
- slashName: 'sandbox-enable',
516
- run: () => setSandbox(false),
517
- },
518
493
  ],
519
494
  });
520
495
 
@@ -522,30 +497,12 @@ const tui: TuiPlugin = async (api, options, meta) => {
522
497
  {
523
498
  title: 'Sandbox',
524
499
  value: 'sandbox',
525
- description: 'Show sandbox configuration',
500
+ description: 'Show config and toggle the sandbox',
526
501
  category: 'Sandbox',
527
502
  suggested: true,
528
503
  slash: { name: 'sandbox' },
529
504
  onSelect: showSandbox,
530
505
  },
531
- {
532
- title: 'Disable sandbox',
533
- value: 'sandbox-disable',
534
- description: 'Disable sandbox for this session',
535
- category: 'Sandbox',
536
- suggested: true,
537
- slash: { name: 'sandbox-disable' },
538
- onSelect: () => setSandbox(true),
539
- },
540
- {
541
- title: 'Enable sandbox',
542
- value: 'sandbox-enable',
543
- description: 'Re-enable sandbox for this session',
544
- category: 'Sandbox',
545
- suggested: true,
546
- slash: { name: 'sandbox-enable' },
547
- onSelect: () => setSandbox(false),
548
- },
549
506
  ]);
550
507
 
551
508
  // Persistent status badge in the prompt area. It needs the host's Solid