opencode-landstrip 0.3.13 → 0.14.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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # opencode-landstrip
2
2
 
3
3
  Landlock-based sandboxing for [opencode](https://opencode.ai/) using
4
- [`landstrip`](https://github.com/jarkkojs/landstrip).
4
+ [`landstrip`](https://github.com/landstrip/landstrip).
5
5
 
6
6
  ## Install
7
7
 
@@ -26,7 +26,7 @@ manually:
26
26
 
27
27
  `opencode plugin install opencode-landstrip` configures both entrypoints.
28
28
 
29
- This installs `opencode-landstrip` and its `@jarkkojs/landstrip` dependency, which
29
+ This installs `opencode-landstrip` and its `@landstrip/landstrip` dependency, which
30
30
  includes platform-specific native binaries for Linux, macOS, and Windows.
31
31
 
32
32
  Requires OpenCode `1.17.7` or newer.
@@ -82,5 +82,5 @@ Set `enabled` to `false` in `sandbox.json`, or pass plugin options:
82
82
  `opencode-landstrip` is licensed under `MIT`. See [LICENSE](LICENSE) for more
83
83
  information.
84
84
 
85
- The bundled `@jarkkojs/landstrip` package is licensed separately as
85
+ The bundled `@landstrip/landstrip` package is licensed separately as
86
86
  `Apache-2.0 AND LGPL-2.1-or-later`.
package/index.ts CHANGED
@@ -32,14 +32,12 @@ interface LandstripPolicy {
32
32
  filesystem: SandboxFilesystemConfig;
33
33
  }
34
34
 
35
- interface LandstripErrorResponse {
36
- reason: 'Other' | 'AccessDenied' | 'LaunchFailed' | 'SetupFailed' | 'Usage';
37
- file?: string;
38
- operation?: 'read' | 'write';
39
- program?: string;
40
- type?: 'filesystem' | 'network' | 'platform' | 'launch' | 'encoding';
41
- source?: string;
42
- }
35
+ type LandstripTrap =
36
+ | { kind: 'Filesystem'; operation: 'read' | 'write'; path: string; mechanism: string }
37
+ | { kind: 'Network'; operation: string; target: string; mechanism: string }
38
+ | { kind: 'Launch'; program: string; message: string }
39
+ | { kind: 'Usage'; message: string }
40
+ | { kind: 'Internal'; fields: Record<string, string> };
43
41
 
44
42
  interface BashSandboxState {
45
43
  originalCommand: string;
@@ -60,40 +58,13 @@ interface SandboxPermissionDecision {
60
58
 
61
59
  type ToastVariant = 'info' | 'success' | 'warning' | 'error';
62
60
 
63
- const LANDSTRIP_VERSION = [0, 11, 9] as const;
61
+ const LANDSTRIP_VERSION = [0, 14, 0] as const;
64
62
  const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
65
- const LANDSTRIP_ERROR_REASONS = new Set<LandstripErrorResponse['reason']>([
66
- 'Other',
67
- 'AccessDenied',
68
- 'LaunchFailed',
69
- 'SetupFailed',
70
- 'Usage',
71
- ]);
72
- const LANDSTRIP_OPERATIONS = new Set<NonNullable<LandstripErrorResponse['operation']>>([
73
- 'read',
74
- 'write',
75
- ]);
76
- const LANDSTRIP_ERROR_TYPES = new Set<NonNullable<LandstripErrorResponse['type']>>([
77
- 'filesystem',
78
- 'network',
79
- 'platform',
80
- 'launch',
81
- 'encoding',
82
- ]);
63
+ const LANDSTRIP_OPERATIONS = new Set<'read' | 'write'>(['read', 'write']);
83
64
  const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
84
65
 
85
- function isLandstripErrorReason(value: string): value is LandstripErrorResponse['reason'] {
86
- return LANDSTRIP_ERROR_REASONS.has(value as LandstripErrorResponse['reason']);
87
- }
88
-
89
- function isLandstripOperation(
90
- value: string,
91
- ): value is NonNullable<LandstripErrorResponse['operation']> {
92
- return LANDSTRIP_OPERATIONS.has(value as NonNullable<LandstripErrorResponse['operation']>);
93
- }
94
-
95
- function isLandstripErrorType(value: string): value is NonNullable<LandstripErrorResponse['type']> {
96
- return LANDSTRIP_ERROR_TYPES.has(value as NonNullable<LandstripErrorResponse['type']>);
66
+ function isLandstripOperation(value: unknown): value is 'read' | 'write' {
67
+ return typeof value === 'string' && LANDSTRIP_OPERATIONS.has(value as 'read' | 'write');
97
68
  }
98
69
 
99
70
  function expandPath(filePath: string, baseDirectory: string): string {
@@ -242,13 +213,16 @@ function extractBlockedPath(
242
213
  );
243
214
  if (match) return normalizeBlockedPath(match[1], baseDirectory);
244
215
 
245
- // Landstrip structured error format with file field
216
+ // Landstrip structured trap format carrying a denied path
246
217
  const landstripErrors = parseLandstripErrors(output);
247
- for (const error of landstripErrors) {
248
- if (error.file) return normalizeBlockedPath(error.file, baseDirectory);
218
+ for (const trap of landstripErrors) {
219
+ if (trap.kind === 'Filesystem') return normalizeBlockedPath(trap.path, baseDirectory);
220
+ if (trap.kind === 'Internal' && trap.fields.file) {
221
+ return normalizeBlockedPath(trap.fields.file, baseDirectory);
222
+ }
249
223
  }
250
224
 
251
- // If landstrip reported an error but without a file field, try to
225
+ // If landstrip reported a trap but without a path, try to
252
226
  // extract the blocked path from the command itself
253
227
  if (landstripErrors.length > 0 && command) {
254
228
  for (const candidate of extractCandidatePaths(command)) {
@@ -396,63 +370,94 @@ function hasMinimumVersion(version: string, minimum: readonly [number, number, n
396
370
  return true;
397
371
  }
398
372
 
399
- function parseLandstripErrors(output: string): LandstripErrorResponse[] {
400
- const errors: LandstripErrorResponse[] = [];
401
-
402
- for (const block of output.trim().split(/\n\n+/)) {
403
- const fields: Record<string, string> = {};
404
-
405
- for (const line of block.split('\n')) {
406
- const colonIndex = line.indexOf(':');
407
- if (colonIndex === -1) continue;
408
- const key = line.slice(0, colonIndex).trim();
409
- const value = line.slice(colonIndex + 1).trim();
410
- if (key.length > 0 && value.length > 0) fields[key] = value;
373
+ function decodeLandstripTrap(value: unknown): LandstripTrap | null {
374
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
375
+ const entries = Object.entries(value as Record<string, unknown>);
376
+ if (entries.length !== 1) return null;
377
+ const [kind, payload] = entries[0];
378
+
379
+ switch (kind) {
380
+ case 'Filesystem': {
381
+ if (!Array.isArray(payload)) return null;
382
+ const [operation, path, mechanism] = payload;
383
+ if (!isLandstripOperation(operation) || typeof path !== 'string') return null;
384
+ return { kind, operation, path, mechanism: typeof mechanism === 'string' ? mechanism : '' };
411
385
  }
412
-
413
- if (fields.reason && isLandstripErrorReason(fields.reason)) {
414
- const error: LandstripErrorResponse = {
415
- reason: fields.reason,
416
- };
417
-
418
- if (fields.file) error.file = fields.file;
419
- if (fields.operation && isLandstripOperation(fields.operation)) {
420
- error.operation = fields.operation;
386
+ case 'Network': {
387
+ if (!Array.isArray(payload)) return null;
388
+ const [operation, target, mechanism] = payload;
389
+ if (typeof operation !== 'string' || typeof target !== 'string') return null;
390
+ return { kind, operation, target, mechanism: typeof mechanism === 'string' ? mechanism : '' };
391
+ }
392
+ case 'Launch': {
393
+ if (!Array.isArray(payload)) return null;
394
+ const [program, message] = payload;
395
+ if (typeof program !== 'string') return null;
396
+ return { kind, program, message: typeof message === 'string' ? message : '' };
397
+ }
398
+ case 'Usage': {
399
+ if (typeof payload !== 'string') return null;
400
+ return { kind, message: payload };
401
+ }
402
+ case 'Internal': {
403
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) return null;
404
+ const fields: Record<string, string> = {};
405
+ for (const [key, val] of Object.entries(payload as Record<string, unknown>)) {
406
+ fields[key] = typeof val === 'string' ? val : JSON.stringify(val);
421
407
  }
422
- if (fields.program) error.program = fields.program;
423
- if (fields.source) error.source = fields.source;
408
+ return { kind, fields };
409
+ }
410
+ default:
411
+ return null;
412
+ }
413
+ }
424
414
 
425
- if (fields.type && isLandstripErrorType(fields.type)) error.type = fields.type;
415
+ function parseLandstripErrors(output: string): LandstripTrap[] {
416
+ const traps: LandstripTrap[] = [];
426
417
 
427
- errors.push(error);
418
+ for (const line of output.split('\n')) {
419
+ const trimmed = line.trim();
420
+ if (trimmed.length === 0 || trimmed[0] !== '{') continue;
421
+
422
+ let parsed: unknown;
423
+ try {
424
+ parsed = JSON.parse(trimmed);
425
+ } catch {
426
+ continue;
428
427
  }
428
+
429
+ const trap = decodeLandstripTrap(parsed);
430
+ if (trap) traps.push(trap);
429
431
  }
430
432
 
431
- return errors;
433
+ return traps;
432
434
  }
433
435
 
434
- function formatLandstripErrors(errors: LandstripErrorResponse[]): string {
435
- return errors
436
- .map((err) => {
437
- const parts: string[] = [`landstrip: ${err.reason}`];
438
-
439
- if (err.file) {
440
- parts.push(` (${err.file})`);
441
- }
442
- if (err.operation) {
443
- parts.push(` ${err.operation}`);
444
- }
445
- if (err.program) {
446
- parts.push(` ${err.program}`);
447
- }
448
- if (err.type) {
449
- parts.push(`:${err.type}`);
450
- }
451
- if (err.source) parts.push(`: ${err.source}`);
436
+ function formatLandstripTrap(trap: LandstripTrap): string {
437
+ switch (trap.kind) {
438
+ case 'Filesystem':
439
+ return `landstrip: filesystem ${trap.operation} denied (${trap.path})${
440
+ trap.mechanism ? ` [${trap.mechanism}]` : ''
441
+ }`;
442
+ case 'Network':
443
+ return `landstrip: network ${trap.operation} denied (${trap.target})${
444
+ trap.mechanism ? ` [${trap.mechanism}]` : ''
445
+ }`;
446
+ case 'Launch':
447
+ return `landstrip: launch failed (${trap.program})${trap.message ? `: ${trap.message}` : ''}`;
448
+ case 'Usage':
449
+ return `landstrip: usage error: ${trap.message}`;
450
+ case 'Internal': {
451
+ const detail = Object.entries(trap.fields)
452
+ .map(([key, val]) => `${key}: ${val}`)
453
+ .join(', ');
454
+ return `landstrip: internal error${detail ? ` (${detail})` : ''}`;
455
+ }
456
+ }
457
+ }
452
458
 
453
- return parts.join('');
454
- })
455
- .join('\n');
459
+ function formatLandstripErrors(traps: LandstripTrap[]): string {
460
+ return traps.map(formatLandstripTrap).join('\n');
456
461
  }
457
462
 
458
463
  function splitHostPort(target: string, defaultPort: number): { host: string; port: number } | null {
@@ -948,7 +953,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
948
953
  if (!version) {
949
954
  landstripCheck = {
950
955
  ok: false,
951
- reason: `landstrip was not found. Reinstall with: npm install @jarkkojs/landstrip`,
956
+ reason: `landstrip was not found. Reinstall with: npm install @landstrip/landstrip`,
952
957
  };
953
958
  return landstripCheck;
954
959
  }
package/landstrip.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- declare module '@jarkkojs/landstrip' {
1
+ declare module '@landstrip/landstrip' {
2
2
  function binaryPath(): string;
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-landstrip",
3
- "version": "0.3.13",
3
+ "version": "0.14.0",
4
4
  "description": "Landlock-based sandboxing for opencode with landstrip",
5
5
  "keywords": [
6
6
  "landlock",
@@ -11,7 +11,7 @@
11
11
  "license": "MIT",
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git+https://github.com/jarkkojs/opencode-landstrip.git"
14
+ "url": "git+https://github.com/landstrip/opencode-landstrip.git"
15
15
  },
16
16
  "files": [
17
17
  "index.ts",
@@ -49,7 +49,7 @@
49
49
  "ci:test": "npm test"
50
50
  },
51
51
  "dependencies": {
52
- "@jarkkojs/landstrip": "^0.11.11"
52
+ "@landstrip/landstrip": "^0.14.5"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@opencode-ai/plugin": "^1.17.7",
package/shared.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Copyright (C) Jarkko Sakkinen 2026
3
3
 
4
- import { binaryPath } from '@jarkkojs/landstrip';
4
+ import { binaryPath } from '@landstrip/landstrip';
5
5
 
6
6
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
7
7
  import { homedir } from 'node:os';
@@ -54,11 +54,11 @@ export const DEFAULT_CONFIG: SandboxConfig = {
54
54
  };
55
55
 
56
56
  const LANDSTRIP_PACKAGE_NAMES = new Set([
57
- '@jarkkojs/landstrip',
58
- '@jarkkojs/landstrip-darwin-arm64',
59
- '@jarkkojs/landstrip-darwin-x64',
60
- '@jarkkojs/landstrip-linux-x64',
61
- '@jarkkojs/landstrip-win32-x64',
57
+ '@landstrip/landstrip',
58
+ '@landstrip/landstrip-darwin-arm64',
59
+ '@landstrip/landstrip-darwin-x64',
60
+ '@landstrip/landstrip-linux-x64',
61
+ '@landstrip/landstrip-win32-x64',
62
62
  ]);
63
63
 
64
64
  export function isRecord(value: unknown): value is Record<string, unknown> {
@@ -227,7 +227,7 @@ export function landstripBinaryPath(): string {
227
227
  }
228
228
 
229
229
  throw new Error(
230
- `Refusing to use landstrip binary outside official @jarkkojs/landstrip packages: ${filePath}`,
230
+ `Refusing to use landstrip binary outside official @landstrip/landstrip packages: ${filePath}`,
231
231
  );
232
232
  }
233
233