opencode-landstrip 0.16.19 → 0.16.21
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 +4 -0
- package/index.ts +1 -79
- package/package.json +2 -2
- package/shared.ts +13 -18
- package/tui.ts +21 -64
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
|
|
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.
|
|
3
|
+
"version": "0.16.21",
|
|
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.
|
|
52
|
+
"@landstrip/landstrip": "^0.16.15"
|
|
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
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
export function
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
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) +
|
|
459
|
+
sandboxSummary(config, globalPath, projectPath) +
|
|
460
|
+
`\n\n${next ? 'Enable' : 'Disable'} the sandbox? (enter = ${next ? 'enable' : 'disable'}, esc = close)`;
|
|
456
461
|
|
|
457
|
-
// No `
|
|
458
|
-
//
|
|
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.
|
|
462
|
-
title: 'Sandbox
|
|
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
|
|
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
|
|
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
|