opencode-landstrip 0.16.17 → 0.16.19
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/index.ts +21 -18
- package/package.json +2 -2
- package/shared.ts +26 -3
- package/tui.ts +17 -6
package/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
formatLandstripTraps,
|
|
19
19
|
getConfigPaths,
|
|
20
20
|
isRecord,
|
|
21
|
+
isSandboxDisabled,
|
|
21
22
|
landstripBinaryPath,
|
|
22
23
|
loadConfig,
|
|
23
24
|
normalizeOptions,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
permissionType,
|
|
27
28
|
sandboxSummary,
|
|
28
29
|
sessionScopeFor,
|
|
30
|
+
setSandboxDisabled,
|
|
29
31
|
} from './shared.js';
|
|
30
32
|
|
|
31
33
|
interface LandstripPolicy {
|
|
@@ -285,19 +287,12 @@ function evaluateReadPermission(
|
|
|
285
287
|
const allowDepth = matchDepth(filePath, effectiveAllowRead, baseDirectory);
|
|
286
288
|
const denyDepth = matchDepth(filePath, config.filesystem.denyRead, baseDirectory);
|
|
287
289
|
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
kind: 'read',
|
|
295
|
-
resource: filePath,
|
|
296
|
-
message: `Sandbox: read access denied for "${filePath}" (denyRead overrides allowRead).`,
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (allowDepth >= 0) {
|
|
290
|
+
// Reads are interactive, so the read tool never hard-denies: a path covered by
|
|
291
|
+
// allowRead at least as specifically as any denyRead is allowed silently;
|
|
292
|
+
// everything else asks for approval (allow once/session/persist or reject)
|
|
293
|
+
// rather than being blocked outright. denyRead still hard-applies to bash
|
|
294
|
+
// through the landstrip binary policy, which has no way to prompt.
|
|
295
|
+
if (allowDepth >= 0 && allowDepth >= denyDepth) {
|
|
301
296
|
return { status: 'allow', kind: 'read', resource: filePath, message: '' };
|
|
302
297
|
}
|
|
303
298
|
|
|
@@ -305,7 +300,7 @@ function evaluateReadPermission(
|
|
|
305
300
|
status: 'ask',
|
|
306
301
|
kind: 'read',
|
|
307
302
|
resource: filePath,
|
|
308
|
-
message: `Sandbox: read access requires approval for "${filePath}"
|
|
303
|
+
message: `Sandbox: read access requires approval for "${filePath}".`,
|
|
309
304
|
};
|
|
310
305
|
}
|
|
311
306
|
|
|
@@ -846,6 +841,9 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
846
841
|
}
|
|
847
842
|
let enabledNotified = false;
|
|
848
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);
|
|
849
847
|
let configuredShell: string | undefined;
|
|
850
848
|
let landstripCheck: { ok: true; version: string } | { ok: false; reason: string } | undefined;
|
|
851
849
|
|
|
@@ -899,7 +897,8 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
899
897
|
|
|
900
898
|
function buildSandboxSummary(config: SandboxConfig): string {
|
|
901
899
|
const { globalPath, projectPath } = getConfigPaths(directory);
|
|
902
|
-
const statusText =
|
|
900
|
+
const statusText =
|
|
901
|
+
sandboxDisabled || isSandboxDisabled(directory) ? 'disabled for this session' : undefined;
|
|
903
902
|
const report = sandboxSummary(config, globalPath, projectPath, statusText);
|
|
904
903
|
return ['# Sandbox Configuration', '', report].join('\n');
|
|
905
904
|
}
|
|
@@ -1004,7 +1003,9 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1004
1003
|
}
|
|
1005
1004
|
|
|
1006
1005
|
async function activeConfig(): Promise<SandboxConfig | null> {
|
|
1007
|
-
|
|
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;
|
|
1008
1009
|
|
|
1009
1010
|
const config = loadConfig(directory, optionOverrides);
|
|
1010
1011
|
if (!config.enabled) {
|
|
@@ -1392,7 +1393,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1392
1393
|
}
|
|
1393
1394
|
|
|
1394
1395
|
if (command === 'sandbox-disable') {
|
|
1395
|
-
if (sandboxDisabled) {
|
|
1396
|
+
if (sandboxDisabled || isSandboxDisabled(directory)) {
|
|
1396
1397
|
pushCommandText(
|
|
1397
1398
|
input,
|
|
1398
1399
|
output,
|
|
@@ -1401,6 +1402,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1401
1402
|
return;
|
|
1402
1403
|
}
|
|
1403
1404
|
sandboxDisabled = true;
|
|
1405
|
+
setSandboxDisabled(directory, true);
|
|
1404
1406
|
pushCommandText(
|
|
1405
1407
|
input,
|
|
1406
1408
|
output,
|
|
@@ -1419,7 +1421,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1419
1421
|
}
|
|
1420
1422
|
|
|
1421
1423
|
if (command === 'sandbox-enable') {
|
|
1422
|
-
if (!sandboxDisabled) {
|
|
1424
|
+
if (!sandboxDisabled && !isSandboxDisabled(directory)) {
|
|
1423
1425
|
pushCommandText(
|
|
1424
1426
|
input,
|
|
1425
1427
|
output,
|
|
@@ -1428,6 +1430,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1428
1430
|
return;
|
|
1429
1431
|
}
|
|
1430
1432
|
sandboxDisabled = false;
|
|
1433
|
+
setSandboxDisabled(directory, false);
|
|
1431
1434
|
const config = await activeConfig();
|
|
1432
1435
|
if (!config) {
|
|
1433
1436
|
pushCommandText(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.19",
|
|
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.14"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@opencode-ai/plugin": "^1.17.7",
|
package/shared.ts
CHANGED
|
@@ -528,15 +528,38 @@ function discoveryDir(): string {
|
|
|
528
528
|
return join(base, 'opencode-landstrip');
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
|
|
531
|
+
function directoryHash(baseDirectory: string): string {
|
|
532
532
|
let key = baseDirectory;
|
|
533
533
|
try {
|
|
534
534
|
key = realpathSync.native(baseDirectory);
|
|
535
535
|
} catch {
|
|
536
536
|
// Directory not resolvable — hash the raw path instead.
|
|
537
537
|
}
|
|
538
|
-
|
|
539
|
-
|
|
538
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function discoveryFilePath(baseDirectory: string): string {
|
|
542
|
+
return join(discoveryDir(), `port-${directoryHash(baseDirectory)}.json`);
|
|
543
|
+
}
|
|
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));
|
|
540
563
|
}
|
|
541
564
|
|
|
542
565
|
export function writeDiscoveryPort(baseDirectory: string, port: number): void {
|
package/tui.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
sessionAllows,
|
|
16
16
|
sandboxSummary,
|
|
17
17
|
sessionScopeFor,
|
|
18
|
+
setSandboxDisabled,
|
|
18
19
|
updateForPermission,
|
|
19
20
|
writeConfigFile,
|
|
20
21
|
writeDiscoveryPort,
|
|
@@ -464,8 +465,18 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
464
465
|
);
|
|
465
466
|
};
|
|
466
467
|
|
|
467
|
-
|
|
468
|
-
|
|
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
|
+
});
|
|
469
480
|
return true;
|
|
470
481
|
};
|
|
471
482
|
|
|
@@ -491,7 +502,7 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
491
502
|
suggested: true,
|
|
492
503
|
slash: { name: 'sandbox-disable' },
|
|
493
504
|
slashName: 'sandbox-disable',
|
|
494
|
-
run: () =>
|
|
505
|
+
run: () => setSandbox(true),
|
|
495
506
|
},
|
|
496
507
|
{
|
|
497
508
|
namespace: 'palette',
|
|
@@ -502,7 +513,7 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
502
513
|
suggested: true,
|
|
503
514
|
slash: { name: 'sandbox-enable' },
|
|
504
515
|
slashName: 'sandbox-enable',
|
|
505
|
-
run: () =>
|
|
516
|
+
run: () => setSandbox(false),
|
|
506
517
|
},
|
|
507
518
|
],
|
|
508
519
|
});
|
|
@@ -524,7 +535,7 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
524
535
|
category: 'Sandbox',
|
|
525
536
|
suggested: true,
|
|
526
537
|
slash: { name: 'sandbox-disable' },
|
|
527
|
-
onSelect: () =>
|
|
538
|
+
onSelect: () => setSandbox(true),
|
|
528
539
|
},
|
|
529
540
|
{
|
|
530
541
|
title: 'Enable sandbox',
|
|
@@ -533,7 +544,7 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
533
544
|
category: 'Sandbox',
|
|
534
545
|
suggested: true,
|
|
535
546
|
slash: { name: 'sandbox-enable' },
|
|
536
|
-
onSelect: () =>
|
|
547
|
+
onSelect: () => setSandbox(false),
|
|
537
548
|
},
|
|
538
549
|
]);
|
|
539
550
|
|