opencode-landstrip 0.1.1 → 0.2.0

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 -11
  2. package/index.ts +74 -62
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -3,17 +3,6 @@
3
3
  Landlock-based sandboxing for [opencode](https://opencode.ai/) using
4
4
  [`landstrip`](https://github.com/jarkkojs/landstrip).
5
5
 
6
- ## Prerequisites
7
-
8
- Install `landstrip` and make sure it is on the `PATH` used to launch opencode:
9
-
10
- ```bash
11
- cargo install landstrip
12
- ```
13
-
14
- `landstrip` supports Linux, macOS, and Windows. On other platforms this plugin
15
- loads but leaves sandboxing disabled.
16
-
17
6
  ## Install
18
7
 
19
8
  Add the plugin to `opencode.json`:
@@ -25,6 +14,11 @@ Add the plugin to `opencode.json`:
25
14
  }
26
15
  ```
27
16
 
17
+ This installs `opencode-landstrip` and its `@jarkkojs/landstrip` dependency, which
18
+ includes platform-specific native binaries for Linux, macOS, and Windows.
19
+
20
+ On unsupported platforms the plugin loads but leaves sandboxing disabled.
21
+
28
22
  ## Configure
29
23
 
30
24
  Create `.opencode/sandbox.json` in a project or
package/index.ts CHANGED
@@ -3,6 +3,8 @@
3
3
 
4
4
  import type { Hooks, Plugin, PluginInput, PluginOptions } from '@opencode-ai/plugin';
5
5
 
6
+ import { binaryPath } from '@jarkkojs/landstrip';
7
+
6
8
  import { spawnSync } from 'node:child_process';
7
9
  import {
8
10
  existsSync,
@@ -32,16 +34,10 @@ interface SandboxNetworkConfig {
32
34
  deniedDomains: string[];
33
35
  }
34
36
 
35
- interface LandstripConfig {
36
- command: string;
37
- debug: boolean;
38
- }
39
-
40
37
  interface SandboxConfig {
41
38
  enabled: boolean;
42
39
  network: SandboxNetworkConfig;
43
40
  filesystem: SandboxFilesystemConfig;
44
- landstrip: LandstripConfig;
45
41
  }
46
42
 
47
43
  interface LandstripPolicy {
@@ -54,11 +50,19 @@ interface LandstripPolicy {
54
50
  filesystem: SandboxFilesystemConfig;
55
51
  }
56
52
 
53
+ interface LandstripErrorResponse {
54
+ category: 'policy' | 'tool' | 'platform' | 'system';
55
+ file?: string;
56
+ program?: string;
57
+ target?: 'filesystem' | 'network' | 'platform';
58
+ kind?: 'launch' | 'encoding';
59
+ message: string;
60
+ }
61
+
57
62
  interface SandboxConfigOverrides {
58
63
  enabled?: boolean;
59
64
  network?: Partial<SandboxNetworkConfig>;
60
65
  filesystem?: Partial<SandboxFilesystemConfig>;
61
- landstrip?: Partial<LandstripConfig>;
62
66
  }
63
67
 
64
68
  interface BashSandboxState {
@@ -71,7 +75,7 @@ interface BashSandboxState {
71
75
 
72
76
  type ToastVariant = 'info' | 'success' | 'warning' | 'error';
73
77
 
74
- const LANDSTRIP_VERSION = [0, 8, 3] as const;
78
+ const LANDSTRIP_VERSION = [0, 9, 2] as const;
75
79
  const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
76
80
 
77
81
  const DEFAULT_CONFIG: SandboxConfig = {
@@ -103,10 +107,6 @@ const DEFAULT_CONFIG: SandboxConfig = {
103
107
  allowWrite: ['.', '/tmp'],
104
108
  denyWrite: ['.env', '.env.*', '*.pem', '*.key'],
105
109
  },
106
- landstrip: {
107
- command: 'landstrip',
108
- debug: false,
109
- },
110
110
  };
111
111
 
112
112
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -158,15 +158,6 @@ function normalizeFilesystemConfig(value: unknown): Partial<SandboxFilesystemCon
158
158
  return config;
159
159
  }
160
160
 
161
- function normalizeLandstripConfig(value: unknown): Partial<LandstripConfig> | undefined {
162
- if (!isRecord(value)) return undefined;
163
-
164
- const config: Partial<LandstripConfig> = {};
165
- if (typeof value.command === 'string') config.command = value.command;
166
- if (typeof value.debug === 'boolean') config.debug = value.debug;
167
- return config;
168
- }
169
-
170
161
  function normalizeConfig(value: unknown): SandboxConfigOverrides {
171
162
  if (!isRecord(value)) return {};
172
163
 
@@ -179,9 +170,6 @@ function normalizeConfig(value: unknown): SandboxConfigOverrides {
179
170
  const filesystem = normalizeFilesystemConfig(value.filesystem);
180
171
  if (filesystem) config.filesystem = filesystem;
181
172
 
182
- const landstrip = normalizeLandstripConfig(value.landstrip);
183
- if (landstrip) config.landstrip = landstrip;
184
-
185
173
  return config;
186
174
  }
187
175
 
@@ -201,10 +189,6 @@ function deepMerge(base: SandboxConfig, overrides: SandboxConfigOverrides): Sand
201
189
  ...base.filesystem,
202
190
  ...overrides.filesystem,
203
191
  },
204
- landstrip: {
205
- ...base.landstrip,
206
- ...overrides.landstrip,
207
- },
208
192
  };
209
193
  }
210
194
 
@@ -364,8 +348,8 @@ function firstBlockedDomain(
364
348
  return null;
365
349
  }
366
350
 
367
- function landstripVersion(command: string): string | null {
368
- const result = spawnSync(command, ['--version'], { encoding: 'utf-8' });
351
+ function landstripVersion(): string | null {
352
+ const result = spawnSync(binaryPath(), ['--version'], { encoding: 'utf-8' });
369
353
  if (result.status !== 0) return null;
370
354
  return result.stdout.trim();
371
355
  }
@@ -388,6 +372,52 @@ function hasMinimumVersion(version: string, minimum: readonly [number, number, n
388
372
  return true;
389
373
  }
390
374
 
375
+ function parseLandstripErrors(output: string): LandstripErrorResponse[] {
376
+ const errors: LandstripErrorResponse[] = [];
377
+
378
+ for (const line of output.split('\n')) {
379
+ try {
380
+ const parsed = JSON.parse(line);
381
+
382
+ if (
383
+ typeof parsed === 'object' &&
384
+ parsed !== null &&
385
+ typeof parsed.category === 'string' &&
386
+ ['policy', 'tool', 'platform', 'system'].includes(parsed.category) &&
387
+ typeof parsed.message === 'string' &&
388
+ parsed.message.length > 0
389
+ ) {
390
+ errors.push(parsed as LandstripErrorResponse);
391
+ }
392
+ } catch {
393
+ // ignore non-JSON lines
394
+ }
395
+ }
396
+
397
+ return errors;
398
+ }
399
+
400
+ function formatLandstripErrors(errors: LandstripErrorResponse[]): string {
401
+ return errors
402
+ .map((err) => {
403
+ const parts: string[] = [`landstrip: ${err.category}`];
404
+
405
+ if (err.target) {
406
+ parts.push(`(${err.target})`);
407
+ }
408
+ if (err.program) {
409
+ parts.push(` ${err.program}`);
410
+ }
411
+ if (err.kind) {
412
+ parts.push(`:${err.kind}`);
413
+ }
414
+ parts.push(`: ${err.message}`);
415
+
416
+ return parts.join('');
417
+ })
418
+ .join('\n');
419
+ }
420
+
391
421
  function splitHostPort(target: string, defaultPort: number): { host: string; port: number } | null {
392
422
  const bracketMatch = target.match(/^\[([^\]]+)\](?::(\d+))?$/);
393
423
  if (bracketMatch) {
@@ -606,26 +636,15 @@ function shellArgs(shell: string, command: string): string[] {
606
636
  return [shell, '-lc', command];
607
637
  }
608
638
 
609
- function buildWrappedCommand(
610
- config: SandboxConfig,
611
- policyPath: string,
612
- shell: string,
613
- command: string,
614
- ): string {
615
- const args = [
616
- config.landstrip.command,
617
- ...(config.landstrip.debug ? ['--debug'] : []),
618
- '-p',
619
- policyPath,
620
- ...shellArgs(shell, command),
621
- ];
639
+ function buildWrappedCommand(policyPath: string, shell: string, command: string): string {
640
+ const args = ['-p', policyPath, ...shellArgs(shell, command)];
622
641
 
623
- return args.map(shellQuote).join(' ');
642
+ return [binaryPath(), ...args].map(shellQuote).join(' ');
624
643
  }
625
644
 
626
- function isGeneratedWrappedCommand(config: SandboxConfig, command: string): boolean {
645
+ function isGeneratedWrappedCommand(command: string): boolean {
627
646
  return (
628
- command.startsWith(`${shellQuote(config.landstrip.command)} `) &&
647
+ command.startsWith(`${shellQuote(binaryPath())} `) &&
629
648
  command.includes(` ${shellQuote('-p')} `) &&
630
649
  command.includes('opencode-landstrip-')
631
650
  );
@@ -710,10 +729,7 @@ export default (async ({ client, directory }: PluginInput, options?: PluginOptio
710
729
  const notified = new Set<string>();
711
730
  let enabledNotified = false;
712
731
  let configuredShell: string | undefined;
713
- let landstripCheck:
714
- | { command: string; ok: true; version: string }
715
- | { command: string; ok: false; reason: string }
716
- | undefined;
732
+ let landstripCheck: { ok: true; version: string } | { ok: false; reason: string } | undefined;
717
733
 
718
734
  async function notifyOnce(key: string, message: string, variant: ToastVariant): Promise<void> {
719
735
  if (notified.has(key)) return;
@@ -738,38 +754,35 @@ export default (async ({ client, directory }: PluginInput, options?: PluginOptio
738
754
  .catch(() => undefined);
739
755
  }
740
756
 
741
- function checkLandstrip(config: SandboxConfig): typeof landstripCheck {
742
- if (landstripCheck?.command === config.landstrip.command) return landstripCheck;
757
+ function checkLandstrip(): typeof landstripCheck {
758
+ if (landstripCheck) return landstripCheck;
743
759
 
744
760
  if (!SUPPORTED_PLATFORMS.has(process.platform)) {
745
761
  landstripCheck = {
746
- command: config.landstrip.command,
747
762
  ok: false,
748
763
  reason: `landstrip sandboxing is not supported on ${process.platform}`,
749
764
  };
750
765
  return landstripCheck;
751
766
  }
752
767
 
753
- const version = landstripVersion(config.landstrip.command);
768
+ const version = landstripVersion();
754
769
  if (!version) {
755
770
  landstripCheck = {
756
- command: config.landstrip.command,
757
771
  ok: false,
758
- reason: `landstrip was not found. Install it with: cargo install landstrip`,
772
+ reason: `landstrip was not found. Reinstall with: npm install @jarkkojs/landstrip`,
759
773
  };
760
774
  return landstripCheck;
761
775
  }
762
776
 
763
777
  if (!hasMinimumVersion(version, LANDSTRIP_VERSION)) {
764
778
  landstripCheck = {
765
- command: config.landstrip.command,
766
779
  ok: false,
767
- reason: `landstrip 0.8.3 or newer is required; found: ${version}`,
780
+ reason: `landstrip 0.9.2 or newer is required; found: ${version}`,
768
781
  };
769
782
  return landstripCheck;
770
783
  }
771
784
 
772
- landstripCheck = { command: config.landstrip.command, ok: true, version };
785
+ landstripCheck = { ok: true, version };
773
786
  return landstripCheck;
774
787
  }
775
788
 
@@ -777,7 +790,7 @@ export default (async ({ client, directory }: PluginInput, options?: PluginOptio
777
790
  const config = loadConfig(directory, optionOverrides);
778
791
  if (!config.enabled) return null;
779
792
 
780
- const check = checkLandstrip(config);
793
+ const check = checkLandstrip();
781
794
  if (!check?.ok) {
782
795
  await notifyOnce(
783
796
  `disabled:${check?.reason ?? 'unknown'}`,
@@ -837,7 +850,7 @@ export default (async ({ client, directory }: PluginInput, options?: PluginOptio
837
850
  await cleanupBash(callID);
838
851
  }
839
852
 
840
- if (isGeneratedWrappedCommand(config, args.command)) {
853
+ if (isGeneratedWrappedCommand(args.command)) {
841
854
  if (typeof args.description === 'string')
842
855
  args.description = landstripDescription(args.description);
843
856
  return;
@@ -867,7 +880,6 @@ export default (async ({ client, directory }: PluginInput, options?: PluginOptio
867
880
 
868
881
  const originalCommand = args.command;
869
882
  const wrappedCommand = buildWrappedCommand(
870
- config,
871
883
  policy.path,
872
884
  configuredShell ?? process.env.SHELL ?? '/bin/sh',
873
885
  originalCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-landstrip",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Landlock-based sandboxing for opencode with landstrip",
5
5
  "keywords": [
6
6
  "landlock",
@@ -41,6 +41,9 @@
41
41
  "ci:check": "npm run check",
42
42
  "ci:test": "npm test"
43
43
  },
44
+ "dependencies": {
45
+ "@jarkkojs/landstrip": "^0.9.2"
46
+ },
44
47
  "devDependencies": {
45
48
  "@opencode-ai/plugin": "^1.16.2",
46
49
  "@types/node": "^24.0.0",