opencode-landstrip 0.18.42 → 0.18.43
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 +30 -15
- package/package.json +2 -2
- package/shared.ts +4 -86
- package/tui.ts +4 -0
package/index.ts
CHANGED
|
@@ -78,8 +78,8 @@ function normalizePathForMatch(filePath: string): string {
|
|
|
78
78
|
return process.platform === 'win32' ? normalizePathSeparators(filePath).toLowerCase() : filePath;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
// Component count of an absolute path; "/" is 0.
|
|
82
|
-
// matching
|
|
81
|
+
// Component count of an absolute path; "/" is 0. Read rules use it to rank
|
|
82
|
+
// matching allow and deny patterns by specificity.
|
|
83
83
|
function pathDepth(absolutePath: string): number {
|
|
84
84
|
return absolutePath.split('/').filter((segment) => segment.length > 0).length;
|
|
85
85
|
}
|
|
@@ -135,7 +135,7 @@ function isDomainAllowed(domain: string, config: SandboxConfig): boolean {
|
|
|
135
135
|
);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
function
|
|
138
|
+
function isReadAllowed(
|
|
139
139
|
path: string,
|
|
140
140
|
allowPatterns: string[],
|
|
141
141
|
denyPatterns: string[],
|
|
@@ -146,6 +146,16 @@ function isFilesystemAllowed(
|
|
|
146
146
|
return allowDepth >= 0 && allowDepth >= denyDepth;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function writeAccess(
|
|
150
|
+
path: string,
|
|
151
|
+
allowPatterns: string[],
|
|
152
|
+
denyPatterns: string[],
|
|
153
|
+
baseDirectory: string,
|
|
154
|
+
): 'allow' | 'deny' | 'unlisted' {
|
|
155
|
+
if (matchDepth(path, denyPatterns, baseDirectory) >= 0) return 'deny';
|
|
156
|
+
return matchDepth(path, allowPatterns, baseDirectory) >= 0 ? 'allow' : 'unlisted';
|
|
157
|
+
}
|
|
158
|
+
|
|
149
159
|
function extractCandidatePaths(command: string): string[] {
|
|
150
160
|
const paths: string[] = [];
|
|
151
161
|
const tokens = command.match(/[^\s"']+|"[^"]*"|'[^']*'/g) ?? [];
|
|
@@ -219,9 +229,7 @@ function evaluateReadPermission(
|
|
|
219
229
|
// everything else asks for approval (allow once/session/persist or reject)
|
|
220
230
|
// rather than being blocked outright. denyRead still hard-applies to bash
|
|
221
231
|
// through the landstrip binary policy, which has no way to prompt.
|
|
222
|
-
if (
|
|
223
|
-
isFilesystemAllowed(filePath, effectiveAllowRead, config.filesystem.denyRead, baseDirectory)
|
|
224
|
-
) {
|
|
232
|
+
if (isReadAllowed(filePath, effectiveAllowRead, config.filesystem.denyRead, baseDirectory)) {
|
|
225
233
|
return { status: 'allow', kind: 'read', resource: filePath, message: '' };
|
|
226
234
|
}
|
|
227
235
|
|
|
@@ -240,10 +248,14 @@ function evaluateWritePermission(
|
|
|
240
248
|
effectiveAllowWrite: string[],
|
|
241
249
|
): SandboxPermissionDecision {
|
|
242
250
|
const filePath = canonicalizePath(path, baseDirectory);
|
|
243
|
-
const
|
|
244
|
-
|
|
251
|
+
const access = writeAccess(
|
|
252
|
+
filePath,
|
|
253
|
+
effectiveAllowWrite,
|
|
254
|
+
config.filesystem.denyWrite,
|
|
255
|
+
baseDirectory,
|
|
256
|
+
);
|
|
245
257
|
|
|
246
|
-
if (
|
|
258
|
+
if (access === 'deny') {
|
|
247
259
|
return {
|
|
248
260
|
status: 'deny',
|
|
249
261
|
kind: 'write',
|
|
@@ -252,9 +264,7 @@ function evaluateWritePermission(
|
|
|
252
264
|
};
|
|
253
265
|
}
|
|
254
266
|
|
|
255
|
-
if (
|
|
256
|
-
isFilesystemAllowed(filePath, effectiveAllowWrite, config.filesystem.denyWrite, baseDirectory)
|
|
257
|
-
) {
|
|
267
|
+
if (access === 'allow') {
|
|
258
268
|
return { status: 'allow', kind: 'write', resource: filePath, message: '' };
|
|
259
269
|
}
|
|
260
270
|
|
|
@@ -427,8 +437,8 @@ function startTrapServer(
|
|
|
427
437
|
const operation = trap.operation;
|
|
428
438
|
const allowed =
|
|
429
439
|
operation === 'read'
|
|
430
|
-
?
|
|
431
|
-
:
|
|
440
|
+
? isReadAllowed(path, effectiveAllowRead, denyRead, baseDirectory)
|
|
441
|
+
: writeAccess(path, effectiveAllowWrite, denyWrite, baseDirectory) === 'allow';
|
|
432
442
|
if (allowed) {
|
|
433
443
|
trapSocket.write(controlResponseLine(queryId, 'allow'));
|
|
434
444
|
} else {
|
|
@@ -1051,7 +1061,12 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1051
1061
|
config.filesystem.allowWrite,
|
|
1052
1062
|
);
|
|
1053
1063
|
for (const decision of decisions) {
|
|
1054
|
-
if (
|
|
1064
|
+
if (
|
|
1065
|
+
decision.status === 'allow' ||
|
|
1066
|
+
(decision.status === 'ask' && hasCallAllowance(input.callID, decision))
|
|
1067
|
+
) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1055
1070
|
reportBlocked(decision);
|
|
1056
1071
|
}
|
|
1057
1072
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.43",
|
|
4
4
|
"description": "OS-level AI command sandboxing for OpenCode with Landstrip",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landlock",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"ci:test": "npm test"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@landstrip/landstrip-api": "^0.18.
|
|
51
|
+
"@landstrip/landstrip-api": "^0.18.43",
|
|
52
52
|
"@opentui/core": ">=0.3.4",
|
|
53
53
|
"@opentui/solid": ">=0.3.4",
|
|
54
54
|
"solid-js": "1.9.12"
|
package/shared.ts
CHANGED
|
@@ -1,33 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) Jarkko Sakkinen 2026
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
type LandstripControlResponse,
|
|
7
|
-
type LandstripTrap,
|
|
8
|
-
} from '@landstrip/landstrip-api';
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
allowsAllDomains,
|
|
12
|
-
canonicalizeGlobPattern,
|
|
13
|
-
canonicalizePath,
|
|
14
|
-
controlResponseLine,
|
|
15
|
-
decodeLandstripTrap,
|
|
16
|
-
domainMatchesAny,
|
|
17
|
-
domainMatchesPattern,
|
|
18
|
-
expandHomePath,
|
|
19
|
-
expandPath,
|
|
20
|
-
globToRegExp,
|
|
21
|
-
formatLandstripTrap,
|
|
22
|
-
formatLandstripTraps,
|
|
23
|
-
isRecord,
|
|
24
|
-
normalizePathSeparators,
|
|
25
|
-
parseLandstripTraps,
|
|
26
|
-
pathUnderDirectory,
|
|
27
|
-
sessionAllows,
|
|
28
|
-
sessionScopeFor,
|
|
29
|
-
} from '@landstrip/landstrip-api/shared';
|
|
4
|
+
import { binaryPath } from '@landstrip/landstrip-api';
|
|
5
|
+
import { isRecord } from '@landstrip/landstrip-api/shared';
|
|
30
6
|
|
|
7
|
+
export type { LandstripControlResponse, LandstripTrap } from '@landstrip/landstrip-api';
|
|
31
8
|
export {
|
|
32
9
|
allowsAllDomains,
|
|
33
10
|
canonicalizeGlobPattern,
|
|
@@ -47,11 +24,7 @@ export {
|
|
|
47
24
|
pathUnderDirectory,
|
|
48
25
|
sessionAllows,
|
|
49
26
|
sessionScopeFor,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// Re-exported so index.ts/tui.ts can import the trap/response types from this
|
|
53
|
-
// module alongside the parsing functions that produce/consume them.
|
|
54
|
-
export type { LandstripControlResponse, LandstripTrap };
|
|
27
|
+
} from '@landstrip/landstrip-api/shared';
|
|
55
28
|
|
|
56
29
|
import { createHash } from 'node:crypto';
|
|
57
30
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -99,10 +72,6 @@ const LANDSTRIP_PACKAGE_NAMES = new Set([
|
|
|
99
72
|
'@landstrip/landstrip-win32-arm64',
|
|
100
73
|
]);
|
|
101
74
|
|
|
102
|
-
export function list(values: string[]): string {
|
|
103
|
-
return values.join(', ') || '(none)';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
75
|
function rejectUnknownFields(
|
|
107
76
|
value: Record<string, unknown>,
|
|
108
77
|
fields: readonly string[],
|
|
@@ -601,54 +570,3 @@ export function readDiscoveryPort(baseDirectory: string): number | null {
|
|
|
601
570
|
return null;
|
|
602
571
|
}
|
|
603
572
|
}
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Human-readable sandbox configuration report consumed by both the server
|
|
607
|
-
* command and the TUI inspector dialog.
|
|
608
|
-
*/
|
|
609
|
-
export function sandboxSummary(
|
|
610
|
-
config: SandboxConfig,
|
|
611
|
-
globalPath: string,
|
|
612
|
-
projectPath: string,
|
|
613
|
-
statusOverride?: string,
|
|
614
|
-
): string {
|
|
615
|
-
const networkMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
|
|
616
|
-
const allowed = list(config.network.allowedDomains);
|
|
617
|
-
const denied = list(config.network.deniedDomains);
|
|
618
|
-
const unixSockets = config.network.allowAllUnixSockets
|
|
619
|
-
? 'all'
|
|
620
|
-
: list(config.network.allowUnixSockets);
|
|
621
|
-
const denyRead = list(config.filesystem.denyRead);
|
|
622
|
-
const allowRead = list(config.filesystem.allowRead);
|
|
623
|
-
const allowWrite = list(config.filesystem.allowWrite);
|
|
624
|
-
const denyWrite = list(config.filesystem.denyWrite);
|
|
625
|
-
|
|
626
|
-
const status = statusOverride ?? (config.enabled ? 'active' : 'disabled by config');
|
|
627
|
-
let binary: string;
|
|
628
|
-
try {
|
|
629
|
-
binary = landstripBinaryPath();
|
|
630
|
-
} catch {
|
|
631
|
-
binary = '(unavailable)';
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return [
|
|
635
|
-
`Status: ${status}`,
|
|
636
|
-
`landstrip package binary: ${binary}`,
|
|
637
|
-
'',
|
|
638
|
-
'Config files',
|
|
639
|
-
`${projectPath} ${existsSync(projectPath) ? '(found)' : '(missing)'}`,
|
|
640
|
-
`${globalPath} ${existsSync(globalPath) ? '(found)' : '(missing)'}`,
|
|
641
|
-
'',
|
|
642
|
-
`Network: ${networkMode}`,
|
|
643
|
-
`allow network: ${config.network.allowNetwork ? 'yes' : 'no'}`,
|
|
644
|
-
`allowed: ${allowed}`,
|
|
645
|
-
`denied: ${denied}`,
|
|
646
|
-
`unix sockets: ${unixSockets}`,
|
|
647
|
-
'',
|
|
648
|
-
'Filesystem',
|
|
649
|
-
`deny read: ${denyRead}`,
|
|
650
|
-
`allow read: ${allowRead}`,
|
|
651
|
-
`allow write: ${allowWrite}`,
|
|
652
|
-
`deny write: ${denyWrite}`,
|
|
653
|
-
].join('\n');
|
|
654
|
-
}
|
package/tui.ts
CHANGED
|
@@ -585,6 +585,10 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
if (trap.kind === 'filesystem') {
|
|
588
|
+
if (trap.operation === 'write' && trap.reason === 'deny_match') {
|
|
589
|
+
respondQuery(socket, trap.query_id, 'deny');
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
588
592
|
const sessionPaths =
|
|
589
593
|
trap.operation === 'read' ? allowances.readPaths : allowances.writePaths;
|
|
590
594
|
if (sessionAllows(sessionPaths, trap.path)) {
|