opencode-landstrip 0.14.1 → 0.15.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/index.ts +40 -41
- package/package.json +2 -2
- package/tui.ts +17 -12
package/index.ts
CHANGED
|
@@ -33,11 +33,11 @@ interface LandstripPolicy {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
type LandstripTrap =
|
|
36
|
-
| { kind: '
|
|
37
|
-
| { kind: '
|
|
38
|
-
| { kind: '
|
|
39
|
-
| { kind: '
|
|
40
|
-
| { kind: '
|
|
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'; detail: Record<string, string> };
|
|
41
41
|
|
|
42
42
|
interface BashSandboxState {
|
|
43
43
|
originalCommand: string;
|
|
@@ -58,7 +58,7 @@ interface SandboxPermissionDecision {
|
|
|
58
58
|
|
|
59
59
|
type ToastVariant = 'info' | 'success' | 'warning' | 'error';
|
|
60
60
|
|
|
61
|
-
const LANDSTRIP_VERSION = [0,
|
|
61
|
+
const LANDSTRIP_VERSION = [0, 15, 1] as const;
|
|
62
62
|
const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
|
|
63
63
|
const LANDSTRIP_OPERATIONS = new Set<'read' | 'write'>(['read', 'write']);
|
|
64
64
|
const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
|
|
@@ -247,9 +247,9 @@ function extractBlockedPath(
|
|
|
247
247
|
// Landstrip structured trap format carrying a denied path
|
|
248
248
|
const landstripErrors = parseLandstripErrors(output);
|
|
249
249
|
for (const trap of landstripErrors) {
|
|
250
|
-
if (trap.kind === '
|
|
251
|
-
if (trap.kind === '
|
|
252
|
-
return normalizeBlockedPath(trap.
|
|
250
|
+
if (trap.kind === 'filesystem') return normalizeBlockedPath(trap.path, baseDirectory);
|
|
251
|
+
if (trap.kind === 'internal' && trap.detail.file) {
|
|
252
|
+
return normalizeBlockedPath(trap.detail.file, baseDirectory);
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
@@ -407,40 +407,39 @@ function hasMinimumVersion(version: string, minimum: readonly [number, number, n
|
|
|
407
407
|
|
|
408
408
|
function decodeLandstripTrap(value: unknown): LandstripTrap | null {
|
|
409
409
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (!Array.isArray(payload)) return null;
|
|
417
|
-
const [operation, path, mechanism] = payload;
|
|
410
|
+
const record = value as Record<string, unknown>;
|
|
411
|
+
const mechanism = typeof record.mechanism === 'string' ? record.mechanism : '';
|
|
412
|
+
|
|
413
|
+
switch (record.kind) {
|
|
414
|
+
case 'filesystem': {
|
|
415
|
+
const { operation, path } = record;
|
|
418
416
|
if (!isLandstripOperation(operation) || typeof path !== 'string') return null;
|
|
419
|
-
return { kind, operation, path, mechanism
|
|
417
|
+
return { kind: 'filesystem', operation, path, mechanism };
|
|
420
418
|
}
|
|
421
|
-
case '
|
|
422
|
-
|
|
423
|
-
const [operation, target, mechanism] = payload;
|
|
419
|
+
case 'network': {
|
|
420
|
+
const { operation, target } = record;
|
|
424
421
|
if (typeof operation !== 'string' || typeof target !== 'string') return null;
|
|
425
|
-
return { kind, operation, target, mechanism
|
|
422
|
+
return { kind: 'network', operation, target, mechanism };
|
|
426
423
|
}
|
|
427
|
-
case '
|
|
428
|
-
|
|
429
|
-
const [program, message] = payload;
|
|
424
|
+
case 'launch': {
|
|
425
|
+
const { program, message } = record;
|
|
430
426
|
if (typeof program !== 'string') return null;
|
|
431
|
-
return { kind, program, message: typeof message === 'string' ? message : '' };
|
|
427
|
+
return { kind: 'launch', program, message: typeof message === 'string' ? message : '' };
|
|
432
428
|
}
|
|
433
|
-
case '
|
|
434
|
-
|
|
435
|
-
|
|
429
|
+
case 'usage': {
|
|
430
|
+
const { message } = record;
|
|
431
|
+
if (typeof message !== 'string') return null;
|
|
432
|
+
return { kind: 'usage', message };
|
|
436
433
|
}
|
|
437
|
-
case '
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
434
|
+
case 'internal': {
|
|
435
|
+
const detail: Record<string, string> = {};
|
|
436
|
+
const payload = record.detail;
|
|
437
|
+
if (typeof payload === 'object' && payload !== null && !Array.isArray(payload)) {
|
|
438
|
+
for (const [key, val] of Object.entries(payload as Record<string, unknown>)) {
|
|
439
|
+
detail[key] = typeof val === 'string' ? val : JSON.stringify(val);
|
|
440
|
+
}
|
|
442
441
|
}
|
|
443
|
-
return { kind,
|
|
442
|
+
return { kind: 'internal', detail };
|
|
444
443
|
}
|
|
445
444
|
default:
|
|
446
445
|
return null;
|
|
@@ -470,20 +469,20 @@ function parseLandstripErrors(output: string): LandstripTrap[] {
|
|
|
470
469
|
|
|
471
470
|
function formatLandstripTrap(trap: LandstripTrap): string {
|
|
472
471
|
switch (trap.kind) {
|
|
473
|
-
case '
|
|
472
|
+
case 'filesystem':
|
|
474
473
|
return `landstrip: filesystem ${trap.operation} denied (${trap.path})${
|
|
475
474
|
trap.mechanism ? ` [${trap.mechanism}]` : ''
|
|
476
475
|
}`;
|
|
477
|
-
case '
|
|
476
|
+
case 'network':
|
|
478
477
|
return `landstrip: network ${trap.operation} denied (${trap.target})${
|
|
479
478
|
trap.mechanism ? ` [${trap.mechanism}]` : ''
|
|
480
479
|
}`;
|
|
481
|
-
case '
|
|
480
|
+
case 'launch':
|
|
482
481
|
return `landstrip: launch failed (${trap.program})${trap.message ? `: ${trap.message}` : ''}`;
|
|
483
|
-
case '
|
|
482
|
+
case 'usage':
|
|
484
483
|
return `landstrip: usage error: ${trap.message}`;
|
|
485
|
-
case '
|
|
486
|
-
const detail = Object.entries(trap.
|
|
484
|
+
case 'internal': {
|
|
485
|
+
const detail = Object.entries(trap.detail)
|
|
487
486
|
.map(([key, val]) => `${key}: ${val}`)
|
|
488
487
|
.join(', ');
|
|
489
488
|
return `landstrip: internal error${detail ? ` (${detail})` : ''}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
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.
|
|
52
|
+
"@landstrip/landstrip": "^0.15.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@opencode-ai/plugin": "^1.17.7",
|
package/tui.ts
CHANGED
|
@@ -64,7 +64,7 @@ function sandboxSummary(baseDirectory: string, optionOverrides: SandboxConfigOve
|
|
|
64
64
|
`allow write: ${list(config.filesystem.allowWrite)}`,
|
|
65
65
|
`deny write: ${list(config.filesystem.denyWrite)}`,
|
|
66
66
|
'',
|
|
67
|
-
'esc or
|
|
67
|
+
'Press esc or enter to close',
|
|
68
68
|
].join('\n');
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -118,7 +118,10 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
118
118
|
activeId = undefined;
|
|
119
119
|
api.ui.dialog.clear();
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
// Defer: `clear()` above tears the dialog down by calling its `onClose`,
|
|
122
|
+
// and the host pops the stack asynchronously. Opening the next dialog
|
|
123
|
+
// synchronously here would race that teardown and get wiped.
|
|
124
|
+
queueMicrotask(pump);
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
async function replyPermission(
|
|
@@ -212,9 +215,11 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
212
215
|
() => {
|
|
213
216
|
// Dialog dismissed (esc) without a choice: drop our hold so the next
|
|
214
217
|
// pending permission can surface, but leave it unresolved upstream.
|
|
218
|
+
// The host pops the dialog itself; calling `clear()` here would re-enter
|
|
219
|
+
// this `onClose` (clear() invokes every entry's onClose) and loop until
|
|
220
|
+
// the stack overflows. Defer `pump()` so the pop settles first.
|
|
215
221
|
if (activeId === permission.id) activeId = undefined;
|
|
216
|
-
|
|
217
|
-
pump();
|
|
222
|
+
queueMicrotask(pump);
|
|
218
223
|
},
|
|
219
224
|
);
|
|
220
225
|
}
|
|
@@ -233,14 +238,14 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
233
238
|
const directory = api.state.path.directory || process.cwd();
|
|
234
239
|
const message = sandboxSummary(directory, optionOverrides);
|
|
235
240
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
// No `onConfirm`/`onClose` that call `clear()`: the host already pops the
|
|
242
|
+
// dialog on enter/esc/click, and its `clear()` re-invokes every entry's
|
|
243
|
+
// `onClose`, so a `clear()` in there recurses forever and freezes the TUI.
|
|
244
|
+
api.ui.dialog.replace(() =>
|
|
245
|
+
api.ui.DialogAlert({
|
|
246
|
+
title: 'Sandbox Configuration',
|
|
247
|
+
message,
|
|
248
|
+
}),
|
|
244
249
|
);
|
|
245
250
|
};
|
|
246
251
|
|