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 +3 -3
- package/index.ts +96 -91
- package/landstrip.d.ts +1 -1
- package/package.json +3 -3
- package/shared.ts +7 -7
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/
|
|
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 `@
|
|
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 `@
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
|
61
|
+
const LANDSTRIP_VERSION = [0, 14, 0] as const;
|
|
64
62
|
const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
|
|
65
|
-
const
|
|
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
|
|
86
|
-
return
|
|
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
|
|
216
|
+
// Landstrip structured trap format carrying a denied path
|
|
246
217
|
const landstripErrors = parseLandstripErrors(output);
|
|
247
|
-
for (const
|
|
248
|
-
if (
|
|
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
|
|
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
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
|
|
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
|
-
|
|
423
|
-
|
|
408
|
+
return { kind, fields };
|
|
409
|
+
}
|
|
410
|
+
default:
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
424
414
|
|
|
425
|
-
|
|
415
|
+
function parseLandstripErrors(output: string): LandstripTrap[] {
|
|
416
|
+
const traps: LandstripTrap[] = [];
|
|
426
417
|
|
|
427
|
-
|
|
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
|
|
433
|
+
return traps;
|
|
432
434
|
}
|
|
433
435
|
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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 @
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.
|
|
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/
|
|
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
|
-
"@
|
|
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 '@
|
|
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
|
-
'@
|
|
58
|
-
'@
|
|
59
|
-
'@
|
|
60
|
-
'@
|
|
61
|
-
'@
|
|
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 @
|
|
230
|
+
`Refusing to use landstrip binary outside official @landstrip/landstrip packages: ${filePath}`,
|
|
231
231
|
);
|
|
232
232
|
}
|
|
233
233
|
|