opencode-landstrip 0.3.13 → 0.14.1
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 +152 -102
- 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, 5] 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 {
|
|
@@ -130,6 +101,38 @@ function canonicalizePath(filePath: string, baseDirectory: string): string {
|
|
|
130
101
|
}
|
|
131
102
|
}
|
|
132
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Translates an absolute glob pattern to a regular expression using standard
|
|
106
|
+
* path semantics: `**` crosses directory boundaries (and `**/` may match zero
|
|
107
|
+
* segments), while a single `*` is confined to one path segment.
|
|
108
|
+
*/
|
|
109
|
+
function globToRegExp(globPattern: string): RegExp {
|
|
110
|
+
let regex = '';
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < globPattern.length; i++) {
|
|
113
|
+
const char = globPattern[i];
|
|
114
|
+
if (char === '*') {
|
|
115
|
+
if (globPattern[i + 1] === '*') {
|
|
116
|
+
i++;
|
|
117
|
+
if (globPattern[i + 1] === '/') {
|
|
118
|
+
i++;
|
|
119
|
+
regex += '(?:.*/)?';
|
|
120
|
+
} else {
|
|
121
|
+
regex += '.*';
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
regex += '[^/]*';
|
|
125
|
+
}
|
|
126
|
+
} else if (/[.+^${}()|[\]\\]/.test(char)) {
|
|
127
|
+
regex += `\\${char}`;
|
|
128
|
+
} else {
|
|
129
|
+
regex += char;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return new RegExp(`^${regex}$`);
|
|
134
|
+
}
|
|
135
|
+
|
|
133
136
|
function matchesPattern(filePath: string, patterns: string[], baseDirectory: string): boolean {
|
|
134
137
|
const abs = canonicalizePath(filePath, baseDirectory);
|
|
135
138
|
|
|
@@ -139,8 +142,7 @@ function matchesPattern(filePath: string, patterns: string[], baseDirectory: str
|
|
|
139
142
|
: canonicalizePath(pattern, baseDirectory);
|
|
140
143
|
|
|
141
144
|
if (pattern.includes('*')) {
|
|
142
|
-
|
|
143
|
-
return new RegExp(`^${escaped}$`).test(abs);
|
|
145
|
+
return globToRegExp(absPattern).test(abs);
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
const sep = absPattern.endsWith('/') ? '' : '/';
|
|
@@ -242,13 +244,16 @@ function extractBlockedPath(
|
|
|
242
244
|
);
|
|
243
245
|
if (match) return normalizeBlockedPath(match[1], baseDirectory);
|
|
244
246
|
|
|
245
|
-
// Landstrip structured
|
|
247
|
+
// Landstrip structured trap format carrying a denied path
|
|
246
248
|
const landstripErrors = parseLandstripErrors(output);
|
|
247
|
-
for (const
|
|
248
|
-
if (
|
|
249
|
+
for (const trap of landstripErrors) {
|
|
250
|
+
if (trap.kind === 'Filesystem') return normalizeBlockedPath(trap.path, baseDirectory);
|
|
251
|
+
if (trap.kind === 'Internal' && trap.fields.file) {
|
|
252
|
+
return normalizeBlockedPath(trap.fields.file, baseDirectory);
|
|
253
|
+
}
|
|
249
254
|
}
|
|
250
255
|
|
|
251
|
-
// If landstrip reported
|
|
256
|
+
// If landstrip reported a trap but without a path, try to
|
|
252
257
|
// extract the blocked path from the command itself
|
|
253
258
|
if (landstripErrors.length > 0 && command) {
|
|
254
259
|
for (const candidate of extractCandidatePaths(command)) {
|
|
@@ -297,10 +302,6 @@ function evaluateReadPermission(
|
|
|
297
302
|
): SandboxPermissionDecision {
|
|
298
303
|
const filePath = canonicalizePath(path, baseDirectory);
|
|
299
304
|
|
|
300
|
-
if (!shouldPromptForRead(filePath, effectiveAllowRead, baseDirectory)) {
|
|
301
|
-
return { status: 'allow', kind: 'read', resource: filePath, message: '' };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
305
|
if (isBlockedByDenyRead(filePath, config, baseDirectory)) {
|
|
305
306
|
return {
|
|
306
307
|
status: 'deny',
|
|
@@ -310,6 +311,10 @@ function evaluateReadPermission(
|
|
|
310
311
|
};
|
|
311
312
|
}
|
|
312
313
|
|
|
314
|
+
if (!shouldPromptForRead(filePath, effectiveAllowRead, baseDirectory)) {
|
|
315
|
+
return { status: 'allow', kind: 'read', resource: filePath, message: '' };
|
|
316
|
+
}
|
|
317
|
+
|
|
313
318
|
return {
|
|
314
319
|
status: 'ask',
|
|
315
320
|
kind: 'read',
|
|
@@ -326,10 +331,6 @@ function evaluateWritePermission(
|
|
|
326
331
|
): SandboxPermissionDecision {
|
|
327
332
|
const filePath = canonicalizePath(path, baseDirectory);
|
|
328
333
|
|
|
329
|
-
if (!shouldPromptForWrite(filePath, effectiveAllowWrite, baseDirectory)) {
|
|
330
|
-
return { status: 'allow', kind: 'write', resource: filePath, message: '' };
|
|
331
|
-
}
|
|
332
|
-
|
|
333
334
|
if (matchesPattern(filePath, config.filesystem.denyWrite, baseDirectory)) {
|
|
334
335
|
return {
|
|
335
336
|
status: 'deny',
|
|
@@ -339,6 +340,10 @@ function evaluateWritePermission(
|
|
|
339
340
|
};
|
|
340
341
|
}
|
|
341
342
|
|
|
343
|
+
if (!shouldPromptForWrite(filePath, effectiveAllowWrite, baseDirectory)) {
|
|
344
|
+
return { status: 'allow', kind: 'write', resource: filePath, message: '' };
|
|
345
|
+
}
|
|
346
|
+
|
|
342
347
|
return {
|
|
343
348
|
status: 'ask',
|
|
344
349
|
kind: 'write',
|
|
@@ -351,7 +356,7 @@ function evaluateDomainPermission(
|
|
|
351
356
|
domain: string,
|
|
352
357
|
config: SandboxConfig,
|
|
353
358
|
): SandboxPermissionDecision {
|
|
354
|
-
if (config.network.allowNetwork
|
|
359
|
+
if (config.network.allowNetwork) {
|
|
355
360
|
return { status: 'allow', kind: 'domain', resource: domain, message: '' };
|
|
356
361
|
}
|
|
357
362
|
|
|
@@ -364,6 +369,10 @@ function evaluateDomainPermission(
|
|
|
364
369
|
};
|
|
365
370
|
}
|
|
366
371
|
|
|
372
|
+
if (domainMatchesAny(domain, config.network.allowedDomains)) {
|
|
373
|
+
return { status: 'allow', kind: 'domain', resource: domain, message: '' };
|
|
374
|
+
}
|
|
375
|
+
|
|
367
376
|
return {
|
|
368
377
|
status: 'ask',
|
|
369
378
|
kind: 'domain',
|
|
@@ -396,63 +405,94 @@ function hasMinimumVersion(version: string, minimum: readonly [number, number, n
|
|
|
396
405
|
return true;
|
|
397
406
|
}
|
|
398
407
|
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
408
|
+
function decodeLandstripTrap(value: unknown): LandstripTrap | null {
|
|
409
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
|
|
410
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
411
|
+
if (entries.length !== 1) return null;
|
|
412
|
+
const [kind, payload] = entries[0];
|
|
413
|
+
|
|
414
|
+
switch (kind) {
|
|
415
|
+
case 'Filesystem': {
|
|
416
|
+
if (!Array.isArray(payload)) return null;
|
|
417
|
+
const [operation, path, mechanism] = payload;
|
|
418
|
+
if (!isLandstripOperation(operation) || typeof path !== 'string') return null;
|
|
419
|
+
return { kind, operation, path, mechanism: typeof mechanism === 'string' ? mechanism : '' };
|
|
411
420
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
+
case 'Network': {
|
|
422
|
+
if (!Array.isArray(payload)) return null;
|
|
423
|
+
const [operation, target, mechanism] = payload;
|
|
424
|
+
if (typeof operation !== 'string' || typeof target !== 'string') return null;
|
|
425
|
+
return { kind, operation, target, mechanism: typeof mechanism === 'string' ? mechanism : '' };
|
|
426
|
+
}
|
|
427
|
+
case 'Launch': {
|
|
428
|
+
if (!Array.isArray(payload)) return null;
|
|
429
|
+
const [program, message] = payload;
|
|
430
|
+
if (typeof program !== 'string') return null;
|
|
431
|
+
return { kind, program, message: typeof message === 'string' ? message : '' };
|
|
432
|
+
}
|
|
433
|
+
case 'Usage': {
|
|
434
|
+
if (typeof payload !== 'string') return null;
|
|
435
|
+
return { kind, message: payload };
|
|
436
|
+
}
|
|
437
|
+
case 'Internal': {
|
|
438
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) return null;
|
|
439
|
+
const fields: Record<string, string> = {};
|
|
440
|
+
for (const [key, val] of Object.entries(payload as Record<string, unknown>)) {
|
|
441
|
+
fields[key] = typeof val === 'string' ? val : JSON.stringify(val);
|
|
421
442
|
}
|
|
422
|
-
|
|
423
|
-
|
|
443
|
+
return { kind, fields };
|
|
444
|
+
}
|
|
445
|
+
default:
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
424
449
|
|
|
425
|
-
|
|
450
|
+
function parseLandstripErrors(output: string): LandstripTrap[] {
|
|
451
|
+
const traps: LandstripTrap[] = [];
|
|
426
452
|
|
|
427
|
-
|
|
453
|
+
for (const line of output.split('\n')) {
|
|
454
|
+
const trimmed = line.trim();
|
|
455
|
+
if (trimmed.length === 0 || trimmed[0] !== '{') continue;
|
|
456
|
+
|
|
457
|
+
let parsed: unknown;
|
|
458
|
+
try {
|
|
459
|
+
parsed = JSON.parse(trimmed);
|
|
460
|
+
} catch {
|
|
461
|
+
continue;
|
|
428
462
|
}
|
|
463
|
+
|
|
464
|
+
const trap = decodeLandstripTrap(parsed);
|
|
465
|
+
if (trap) traps.push(trap);
|
|
429
466
|
}
|
|
430
467
|
|
|
431
|
-
return
|
|
468
|
+
return traps;
|
|
432
469
|
}
|
|
433
470
|
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
471
|
+
function formatLandstripTrap(trap: LandstripTrap): string {
|
|
472
|
+
switch (trap.kind) {
|
|
473
|
+
case 'Filesystem':
|
|
474
|
+
return `landstrip: filesystem ${trap.operation} denied (${trap.path})${
|
|
475
|
+
trap.mechanism ? ` [${trap.mechanism}]` : ''
|
|
476
|
+
}`;
|
|
477
|
+
case 'Network':
|
|
478
|
+
return `landstrip: network ${trap.operation} denied (${trap.target})${
|
|
479
|
+
trap.mechanism ? ` [${trap.mechanism}]` : ''
|
|
480
|
+
}`;
|
|
481
|
+
case 'Launch':
|
|
482
|
+
return `landstrip: launch failed (${trap.program})${trap.message ? `: ${trap.message}` : ''}`;
|
|
483
|
+
case 'Usage':
|
|
484
|
+
return `landstrip: usage error: ${trap.message}`;
|
|
485
|
+
case 'Internal': {
|
|
486
|
+
const detail = Object.entries(trap.fields)
|
|
487
|
+
.map(([key, val]) => `${key}: ${val}`)
|
|
488
|
+
.join(', ');
|
|
489
|
+
return `landstrip: internal error${detail ? ` (${detail})` : ''}`;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
452
493
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
.join('\n');
|
|
494
|
+
function formatLandstripErrors(traps: LandstripTrap[]): string {
|
|
495
|
+
return traps.map(formatLandstripTrap).join('\n');
|
|
456
496
|
}
|
|
457
497
|
|
|
458
498
|
function splitHostPort(target: string, defaultPort: number): { host: string; port: number } | null {
|
|
@@ -542,10 +582,15 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
|
|
|
542
582
|
return;
|
|
543
583
|
}
|
|
544
584
|
|
|
585
|
+
let connected = false;
|
|
545
586
|
const upstream = connectNet(endpoint.port, endpoint.host, () => {
|
|
587
|
+
connected = true;
|
|
546
588
|
client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
547
589
|
pipeSockets(client, upstream, rest);
|
|
548
590
|
});
|
|
591
|
+
upstream.on('error', () => {
|
|
592
|
+
if (!connected) denyProxyRequest(client, '502 Bad Gateway');
|
|
593
|
+
});
|
|
549
594
|
}
|
|
550
595
|
|
|
551
596
|
async function handleHttp(client: Socket, headerText: string, rest: Buffer): Promise<void> {
|
|
@@ -584,10 +629,15 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
|
|
|
584
629
|
const rewrittenHeader = lines
|
|
585
630
|
.filter((line) => !line.toLowerCase().startsWith('proxy-connection:'))
|
|
586
631
|
.join('\r\n');
|
|
632
|
+
let connected = false;
|
|
587
633
|
const upstream = connectNet(port, url.hostname, () => {
|
|
634
|
+
connected = true;
|
|
588
635
|
upstream.write(`${rewrittenHeader}\r\n\r\n`);
|
|
589
636
|
pipeSockets(client, upstream, rest);
|
|
590
637
|
});
|
|
638
|
+
upstream.on('error', () => {
|
|
639
|
+
if (!connected) denyProxyRequest(client, '502 Bad Gateway');
|
|
640
|
+
});
|
|
591
641
|
}
|
|
592
642
|
|
|
593
643
|
function handleClient(client: Socket): void {
|
|
@@ -948,7 +998,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
948
998
|
if (!version) {
|
|
949
999
|
landstripCheck = {
|
|
950
1000
|
ok: false,
|
|
951
|
-
reason: `landstrip was not found. Reinstall with: npm install @
|
|
1001
|
+
reason: `landstrip was not found. Reinstall with: npm install @landstrip/landstrip`,
|
|
952
1002
|
};
|
|
953
1003
|
return landstripCheck;
|
|
954
1004
|
}
|
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.1",
|
|
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
|
|