opencode-landstrip 0.20.14 → 0.20.15
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 +36 -53
- package/package.json +2 -2
- package/shared.ts +9 -172
- package/tui.ts +39 -20
package/index.ts
CHANGED
|
@@ -15,30 +15,33 @@ import {
|
|
|
15
15
|
startFilterProxy,
|
|
16
16
|
} from '@landstrip/landstrip-api/proxy';
|
|
17
17
|
|
|
18
|
+
import type { LandstripTrap } from '@landstrip/landstrip-api';
|
|
18
19
|
import {
|
|
19
|
-
type LandstripTrap,
|
|
20
20
|
type SandboxConfig,
|
|
21
21
|
allowsAllDomains,
|
|
22
|
+
buildLandstripPolicy,
|
|
22
23
|
canonicalizePath,
|
|
23
24
|
controlResponseLine,
|
|
24
25
|
evaluateDomainAccess,
|
|
25
26
|
evaluateReadAccess,
|
|
26
27
|
evaluateWriteAccess,
|
|
27
|
-
extractDomainsFromCommand,
|
|
28
28
|
extractDeniedPath,
|
|
29
|
+
extractDomainsFromCommand,
|
|
29
30
|
formatLandstripTraps,
|
|
30
|
-
getConfigPaths,
|
|
31
31
|
isDomainAllowed,
|
|
32
|
-
isRecord,
|
|
33
32
|
isQueryTrap,
|
|
34
|
-
|
|
33
|
+
isRecord,
|
|
34
|
+
parseLandstripTraps,
|
|
35
|
+
parseTrapLine,
|
|
36
|
+
writeLandstripPolicyFile,
|
|
37
|
+
} from '@landstrip/landstrip-api/shared';
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
getConfigPaths,
|
|
35
41
|
landstripBinaryPath,
|
|
36
42
|
loadConfig,
|
|
37
43
|
normalizeOptions,
|
|
38
|
-
parseLandstripTraps,
|
|
39
|
-
parseTrapLine,
|
|
40
44
|
readDiscoveryPort,
|
|
41
|
-
writeLandstripPolicyFile,
|
|
42
45
|
trapSessionHelloLine,
|
|
43
46
|
} from './shared.js';
|
|
44
47
|
|
|
@@ -117,21 +120,12 @@ function evaluateWritePermission(
|
|
|
117
120
|
allowWriteOverrides: effectiveAllowWrite,
|
|
118
121
|
});
|
|
119
122
|
|
|
120
|
-
if (decision === 'denyAlways') {
|
|
121
|
-
return {
|
|
122
|
-
status: 'deny',
|
|
123
|
-
kind: 'write',
|
|
124
|
-
resource: filePath,
|
|
125
|
-
message: `Sandbox: write access denied for "${filePath}" (denyWriteAlways).`,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
123
|
if (decision === 'deny') {
|
|
130
124
|
return {
|
|
131
125
|
status: 'deny',
|
|
132
126
|
kind: 'write',
|
|
133
127
|
resource: filePath,
|
|
134
|
-
message: `Sandbox: write access denied for "${filePath}" (
|
|
128
|
+
message: `Sandbox: write access denied for "${filePath}" (denyWriteAlways).`,
|
|
135
129
|
};
|
|
136
130
|
}
|
|
137
131
|
|
|
@@ -143,7 +137,7 @@ function evaluateWritePermission(
|
|
|
143
137
|
status: 'ask',
|
|
144
138
|
kind: 'write',
|
|
145
139
|
resource: filePath,
|
|
146
|
-
message: `Sandbox: write access requires approval for "${filePath}"
|
|
140
|
+
message: `Sandbox: write access requires approval for "${filePath}".`,
|
|
147
141
|
};
|
|
148
142
|
}
|
|
149
143
|
|
|
@@ -228,7 +222,7 @@ function proxyEnv(
|
|
|
228
222
|
proxyToken?: string | null,
|
|
229
223
|
): Record<string, string> | undefined {
|
|
230
224
|
if (port === null) return undefined;
|
|
231
|
-
return createProxyEnvironment(port, proxyToken) as Record<string, string>;
|
|
225
|
+
return createProxyEnvironment(port, { token: proxyToken }) as Record<string, string>;
|
|
232
226
|
}
|
|
233
227
|
|
|
234
228
|
function shellQuote(value: string): string {
|
|
@@ -414,17 +408,8 @@ function extractOriginalCommand(wrappedCommand: string): string | null {
|
|
|
414
408
|
const pIdx = args.indexOf('-p');
|
|
415
409
|
const flagIdx = args.findIndex((arg, i) => i > pIdx && (arg === '-lc' || arg === '-c'));
|
|
416
410
|
if (flagIdx === -1) return null;
|
|
417
|
-
|
|
418
|
-
// recovering an expired wrapper never folds a fallback into the command.
|
|
419
|
-
const end = args.indexOf('||', flagIdx + 1);
|
|
420
|
-
return (end === -1 ? args.slice(flagIdx + 1) : args.slice(flagIdx + 1, end)).join(' ');
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function getToolPath(args: Record<string, unknown>): string | undefined {
|
|
424
|
-
const filePath = args.filePath ?? args.path;
|
|
425
|
-
return typeof filePath === 'string' ? filePath : undefined;
|
|
411
|
+
return args.slice(flagIdx + 1).join(' ');
|
|
426
412
|
}
|
|
427
|
-
|
|
428
413
|
function extractPatchPaths(patchText: string): string[] {
|
|
429
414
|
const paths: string[] = [];
|
|
430
415
|
|
|
@@ -453,13 +438,15 @@ function evaluateToolPermissions(
|
|
|
453
438
|
if (tool === 'read') {
|
|
454
439
|
const paths = Array.isArray(args.paths)
|
|
455
440
|
? args.paths.filter((path): path is string => typeof path === 'string')
|
|
456
|
-
:
|
|
441
|
+
: typeof args.path === 'string'
|
|
442
|
+
? [args.path]
|
|
443
|
+
: [];
|
|
457
444
|
return paths.map((path) =>
|
|
458
445
|
evaluateReadPermission(path, config, baseDirectory, effectiveAllowRead),
|
|
459
446
|
);
|
|
460
447
|
}
|
|
461
448
|
|
|
462
|
-
if (tool === 'glob' || tool === 'grep'
|
|
449
|
+
if (tool === 'glob' || tool === 'grep') {
|
|
463
450
|
const searchPath = typeof args.path === 'string' ? args.path : '.';
|
|
464
451
|
return [evaluateReadPermission(searchPath, config, baseDirectory, effectiveAllowRead)];
|
|
465
452
|
}
|
|
@@ -467,22 +454,28 @@ function evaluateToolPermissions(
|
|
|
467
454
|
if (tool === 'write' || tool === 'edit') {
|
|
468
455
|
const paths = Array.isArray(args.paths)
|
|
469
456
|
? args.paths.filter((path): path is string => typeof path === 'string')
|
|
470
|
-
:
|
|
457
|
+
: typeof args.path === 'string'
|
|
458
|
+
? [args.path]
|
|
459
|
+
: [];
|
|
471
460
|
return paths.map((path) =>
|
|
472
461
|
evaluateWritePermission(path, config, baseDirectory, effectiveAllowWrite),
|
|
473
462
|
);
|
|
474
463
|
}
|
|
475
464
|
|
|
476
|
-
if (
|
|
465
|
+
if (tool === 'patch' && typeof args.patchText === 'string') {
|
|
477
466
|
return extractPatchPaths(args.patchText).map((path) =>
|
|
478
467
|
evaluateWritePermission(path, config, baseDirectory, effectiveAllowWrite),
|
|
479
468
|
);
|
|
480
469
|
}
|
|
481
470
|
|
|
482
|
-
if (tool === '
|
|
483
|
-
|
|
471
|
+
if (tool === 'shell') {
|
|
472
|
+
const commands = Array.isArray(args.commands)
|
|
473
|
+
? args.commands.filter((command): command is string => typeof command === 'string')
|
|
474
|
+
: typeof args.command === 'string'
|
|
475
|
+
? [args.command]
|
|
476
|
+
: [];
|
|
477
|
+
return commands.flatMap((command) => evaluateCommandDomains(command, config));
|
|
484
478
|
}
|
|
485
|
-
|
|
486
479
|
return [];
|
|
487
480
|
}
|
|
488
481
|
|
|
@@ -672,7 +665,7 @@ const plugin: Plugin.Plugin = {
|
|
|
672
665
|
const proxy = allowNetwork
|
|
673
666
|
? null
|
|
674
667
|
: await startFilterProxy({
|
|
675
|
-
isDomainAllowed: (domain) => isDomainAllowed(domain, config),
|
|
668
|
+
isDomainAllowed: (domain) => isDomainAllowed(domain, config.network),
|
|
676
669
|
...(proxyCredentials ? { authorization: proxyCredentials.authorization } : {}),
|
|
677
670
|
});
|
|
678
671
|
const proxyPort = proxy ? proxy.port : null;
|
|
@@ -745,24 +738,14 @@ const plugin: Plugin.Plugin = {
|
|
|
745
738
|
if (!config) return;
|
|
746
739
|
|
|
747
740
|
const action = evaluation.action;
|
|
748
|
-
const
|
|
741
|
+
const resources = [...(evaluation.resources ?? [])];
|
|
749
742
|
const effectiveAllowRead = config.filesystem.allowRead;
|
|
750
743
|
const effectiveAllowWrite = config.filesystem.allowWrite;
|
|
751
744
|
const args: Record<string, unknown> = { ...evaluation.metadata };
|
|
752
|
-
if (action === 'read' || action === 'external_directory') args.paths =
|
|
753
|
-
if (action === '
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
? patterns
|
|
757
|
-
: [args.filepath ?? args.path].filter(
|
|
758
|
-
(path): path is string => typeof path === 'string',
|
|
759
|
-
);
|
|
760
|
-
}
|
|
761
|
-
if ((action === 'shell' || action === 'bash') && typeof args.command !== 'string') {
|
|
762
|
-
args.command = patterns[0];
|
|
763
|
-
}
|
|
764
|
-
const toolName =
|
|
765
|
-
action === 'shell' ? 'bash' : action === 'external_directory' ? 'read' : action;
|
|
745
|
+
if (action === 'read' || action === 'external_directory') args.paths = resources;
|
|
746
|
+
if (action === 'edit') args.paths = resources;
|
|
747
|
+
if (action === 'shell') args.commands = resources;
|
|
748
|
+
const toolName = action === 'external_directory' ? 'read' : action;
|
|
766
749
|
const decisions = evaluateToolPermissions(
|
|
767
750
|
toolName,
|
|
768
751
|
args,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.15",
|
|
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.20.
|
|
51
|
+
"@landstrip/landstrip-api": "^0.20.15",
|
|
52
52
|
"@opentui/core": ">=0.5.12",
|
|
53
53
|
"@opentui/solid": ">=0.5.12",
|
|
54
54
|
"solid-js": "1.9.12"
|
package/shared.ts
CHANGED
|
@@ -4,79 +4,11 @@
|
|
|
4
4
|
import { binaryPath } from '@landstrip/landstrip-api';
|
|
5
5
|
import {
|
|
6
6
|
deepMergeSandboxConfig,
|
|
7
|
-
extractDomainsFromCommand,
|
|
8
7
|
isRecord,
|
|
9
8
|
mergeArray,
|
|
10
9
|
parseSandboxConfig,
|
|
11
10
|
} from '@landstrip/landstrip-api/shared';
|
|
12
11
|
import type { SandboxConfig, SandboxConfigOverrides } from '@landstrip/landstrip-api/shared';
|
|
13
|
-
|
|
14
|
-
export type {
|
|
15
|
-
LandstripControlResponse,
|
|
16
|
-
LandstripDarwinPolicy,
|
|
17
|
-
LandstripFilesystemPolicy,
|
|
18
|
-
LandstripNetworkPolicy,
|
|
19
|
-
LandstripPathSelector,
|
|
20
|
-
LandstripPolicy,
|
|
21
|
-
LandstripTrap,
|
|
22
|
-
} from '@landstrip/landstrip-api';
|
|
23
|
-
export {
|
|
24
|
-
buildLandstripPolicy,
|
|
25
|
-
allowsAllDomains,
|
|
26
|
-
canonicalizeGlobPattern,
|
|
27
|
-
canonicalizePath,
|
|
28
|
-
controlResponseLine,
|
|
29
|
-
decodeLandstripTrap,
|
|
30
|
-
domainMatchesAny,
|
|
31
|
-
domainMatchesPattern,
|
|
32
|
-
expandHomePath,
|
|
33
|
-
expandPath,
|
|
34
|
-
globToRegExp,
|
|
35
|
-
formatLandstripTrap,
|
|
36
|
-
formatLandstripTraps,
|
|
37
|
-
isRecord,
|
|
38
|
-
isQueryTrap,
|
|
39
|
-
normalizePathSeparators,
|
|
40
|
-
parseLandstripTraps,
|
|
41
|
-
parseTrapLine,
|
|
42
|
-
pathUnderDirectory,
|
|
43
|
-
resolveFilesystemPatterns,
|
|
44
|
-
resolveFilesystemPolicy,
|
|
45
|
-
sessionAllows,
|
|
46
|
-
sessionScopeFor,
|
|
47
|
-
serializeLandstripPolicy,
|
|
48
|
-
writeLandstripPolicyFile,
|
|
49
|
-
deepMergeSandboxConfig,
|
|
50
|
-
mergeArray,
|
|
51
|
-
parseSandboxConfig,
|
|
52
|
-
matchesPathPattern,
|
|
53
|
-
matchPathSpecificity,
|
|
54
|
-
isPathReadAllowed,
|
|
55
|
-
isPathWriteAllowed,
|
|
56
|
-
shouldPromptForWrite,
|
|
57
|
-
evaluateReadAccess,
|
|
58
|
-
evaluateWriteAccess,
|
|
59
|
-
isDomainAllowed,
|
|
60
|
-
evaluateDomainAccess,
|
|
61
|
-
extractDomainsFromCommand,
|
|
62
|
-
extractCandidatePaths,
|
|
63
|
-
extractNativeDeniedPath,
|
|
64
|
-
extractDeniedPath,
|
|
65
|
-
type PathAccessDecision,
|
|
66
|
-
type DomainAccessDecision,
|
|
67
|
-
type EvaluateReadOptions,
|
|
68
|
-
type EvaluateWriteOptions,
|
|
69
|
-
type EvaluateDomainOptions,
|
|
70
|
-
type SandboxConfig,
|
|
71
|
-
type SandboxConfigFile,
|
|
72
|
-
type SandboxConfigOverrides,
|
|
73
|
-
type SandboxDarwinConfig,
|
|
74
|
-
type SandboxFilesystemConfig,
|
|
75
|
-
type SandboxNetworkConfig,
|
|
76
|
-
type SandboxShellConfig,
|
|
77
|
-
type SandboxWindowsConfig,
|
|
78
|
-
} from '@landstrip/landstrip-api/shared';
|
|
79
|
-
|
|
80
12
|
import { createHash } from 'node:crypto';
|
|
81
13
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
82
14
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -85,21 +17,12 @@ import { dirname, isAbsolute, join } from 'node:path';
|
|
|
85
17
|
|
|
86
18
|
const packageDir = dirname(fileURLToPath(import.meta.url));
|
|
87
19
|
|
|
88
|
-
export function normalizeConfig(value: unknown): SandboxConfigOverrides {
|
|
89
|
-
return parseSandboxConfig(value);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
20
|
export function normalizeOptions(options: unknown): SandboxConfigOverrides {
|
|
93
21
|
if (!isRecord(options)) return {};
|
|
94
|
-
if (options.config === undefined) return
|
|
22
|
+
if (options.config === undefined) return parseSandboxConfig(options);
|
|
95
23
|
if (!isRecord(options.config)) throw new Error('config must be an object');
|
|
96
|
-
return
|
|
24
|
+
return parseSandboxConfig(options.config);
|
|
97
25
|
}
|
|
98
|
-
|
|
99
|
-
export function deepMerge(base: SandboxConfig, overrides: SandboxConfigOverrides): SandboxConfig {
|
|
100
|
-
return deepMergeSandboxConfig(base, overrides);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
26
|
export function getConfigPaths(baseDirectory: string): { globalPath: string; projectPath: string } {
|
|
104
27
|
const configHome = process.env.XDG_CONFIG_HOME;
|
|
105
28
|
const configDirectory =
|
|
@@ -114,7 +37,7 @@ export function readConfigFile(configPath: string): SandboxConfigOverrides {
|
|
|
114
37
|
if (!existsSync(configPath)) return {};
|
|
115
38
|
|
|
116
39
|
try {
|
|
117
|
-
return
|
|
40
|
+
return parseSandboxConfig(JSON.parse(readFileSync(configPath, 'utf-8')));
|
|
118
41
|
} catch (error) {
|
|
119
42
|
throw new Error(
|
|
120
43
|
`Could not load ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -138,14 +61,17 @@ export function loadConfig(
|
|
|
138
61
|
const templateConfig: SandboxConfig = JSON.parse(readFileSync(templatePath, 'utf-8'));
|
|
139
62
|
templateConfig.filesystem.denyWrite.push(globalPath);
|
|
140
63
|
const globalOverrides = readConfigFile(globalPath);
|
|
141
|
-
const baseConfig =
|
|
64
|
+
const baseConfig = deepMergeSandboxConfig(templateConfig, globalOverrides);
|
|
142
65
|
|
|
143
|
-
return
|
|
66
|
+
return deepMergeSandboxConfig(
|
|
67
|
+
deepMergeSandboxConfig(baseConfig, readConfigFile(projectPath)),
|
|
68
|
+
optionOverrides,
|
|
69
|
+
);
|
|
144
70
|
}
|
|
145
71
|
|
|
146
72
|
export function writeConfigFile(configPath: string, update: SandboxConfigOverrides): void {
|
|
147
73
|
const next: Record<string, unknown> = { ...readConfigFile(configPath) };
|
|
148
|
-
for (const [key, value] of Object.entries(
|
|
74
|
+
for (const [key, value] of Object.entries(parseSandboxConfig(update))) {
|
|
149
75
|
if (!isRecord(value)) {
|
|
150
76
|
next[key] = value;
|
|
151
77
|
continue;
|
|
@@ -183,95 +109,6 @@ export function landstripBinaryPath(): string {
|
|
|
183
109
|
}
|
|
184
110
|
}
|
|
185
111
|
|
|
186
|
-
// Permission requests reach the plugin in slightly different shapes across the
|
|
187
|
-
// server hook and the TUI event bus, so the field-fallback parsing below is the
|
|
188
|
-
// single source of truth both entrypoints share.
|
|
189
|
-
export function permissionType(permission: Record<string, unknown>, fallback = ''): string {
|
|
190
|
-
if (typeof permission.permission === 'string') return permission.permission;
|
|
191
|
-
if (typeof permission.action === 'string') return permission.action;
|
|
192
|
-
if (typeof permission.type === 'string') return permission.type;
|
|
193
|
-
return fallback;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function permissionPattern(permission: Record<string, unknown>): string | undefined {
|
|
197
|
-
const patterns = permission.patterns;
|
|
198
|
-
if (Array.isArray(patterns))
|
|
199
|
-
return patterns.find((item): item is string => typeof item === 'string');
|
|
200
|
-
|
|
201
|
-
const pattern = permission.pattern;
|
|
202
|
-
if (typeof pattern === 'string') return pattern;
|
|
203
|
-
if (Array.isArray(pattern))
|
|
204
|
-
return pattern.find((item): item is string => typeof item === 'string');
|
|
205
|
-
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Every string pattern a permission carries, in declaration order, read from
|
|
211
|
-
* `patterns`, `pattern`, or `resources`. The plural complement to
|
|
212
|
-
* {@link permissionPattern} for callers that must inspect all patterns.
|
|
213
|
-
*/
|
|
214
|
-
export function permissionPatterns(permission: Record<string, unknown>): string[] {
|
|
215
|
-
const patterns = permission.patterns;
|
|
216
|
-
if (Array.isArray(patterns))
|
|
217
|
-
return patterns.filter((item): item is string => typeof item === 'string');
|
|
218
|
-
|
|
219
|
-
const pattern = permission.pattern;
|
|
220
|
-
if (typeof pattern === 'string') return [pattern];
|
|
221
|
-
if (Array.isArray(pattern))
|
|
222
|
-
return pattern.filter((item): item is string => typeof item === 'string');
|
|
223
|
-
|
|
224
|
-
const resources = permission.resources;
|
|
225
|
-
if (Array.isArray(resources))
|
|
226
|
-
return resources.filter((item): item is string => typeof item === 'string');
|
|
227
|
-
|
|
228
|
-
return [];
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// The concrete resource a permission concerns (a path or a domain), used to show
|
|
232
|
-
// the user exactly what they are approving and to persist the right allowlist.
|
|
233
|
-
export function permissionResource(permission: Record<string, unknown>): string | undefined {
|
|
234
|
-
const metadata = isRecord(permission.metadata) ? permission.metadata : {};
|
|
235
|
-
const type = permissionType(permission);
|
|
236
|
-
const pattern = permissionPattern(permission);
|
|
237
|
-
|
|
238
|
-
if (type === 'bash') {
|
|
239
|
-
const command = typeof metadata.command === 'string' ? metadata.command : pattern;
|
|
240
|
-
const domains = typeof command === 'string' ? extractDomainsFromCommand(command) : [];
|
|
241
|
-
return domains.length > 0 ? domains.join(', ') : (command ?? pattern);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (typeof metadata.filepath === 'string') return metadata.filepath;
|
|
245
|
-
if (typeof metadata.path === 'string') return metadata.path;
|
|
246
|
-
return pattern;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function updateForPermission(
|
|
250
|
-
permission: Record<string, unknown>,
|
|
251
|
-
): SandboxConfigOverrides | null {
|
|
252
|
-
const metadata = isRecord(permission.metadata) ? permission.metadata : {};
|
|
253
|
-
const type = permissionType(permission);
|
|
254
|
-
const pattern = permissionPattern(permission);
|
|
255
|
-
|
|
256
|
-
if (type === 'bash') {
|
|
257
|
-
const command = typeof metadata.command === 'string' ? metadata.command : pattern;
|
|
258
|
-
const domains = typeof command === 'string' ? extractDomainsFromCommand(command) : [];
|
|
259
|
-
return domains.length > 0 ? { network: { allowedDomains: domains } } : null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (type === 'read' || type === 'glob' || type === 'grep' || type === 'list') {
|
|
263
|
-
const filePath = typeof metadata.filepath === 'string' ? metadata.filepath : pattern;
|
|
264
|
-
return filePath ? { filesystem: { allowRead: [filePath] } } : null;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (type === 'edit' || type === 'write' || type === 'apply_patch') {
|
|
268
|
-
const filePath = typeof metadata.filepath === 'string' ? metadata.filepath : pattern;
|
|
269
|
-
return filePath ? { filesystem: { allowWrite: [filePath] } } : null;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
112
|
export interface TrapSessionHello {
|
|
276
113
|
kind: 'opencode-landstrip-session';
|
|
277
114
|
sessionID: string;
|
package/tui.ts
CHANGED
|
@@ -14,23 +14,28 @@ import { createSignal, onMount } from 'solid-js';
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
controlResponseLine,
|
|
17
|
+
evaluateReadAccess,
|
|
18
|
+
evaluateWriteAccess,
|
|
19
|
+
isPathReadAllowed,
|
|
20
|
+
isPathWriteAllowed,
|
|
21
|
+
isQueryTrap,
|
|
22
|
+
parseLandstripTraps,
|
|
23
|
+
sessionScopeFor,
|
|
24
|
+
} from '@landstrip/landstrip-api/shared';
|
|
25
|
+
|
|
26
|
+
import {
|
|
17
27
|
decodeTrapSessionHello,
|
|
18
28
|
getConfigPaths,
|
|
19
|
-
isQueryTrap,
|
|
20
29
|
loadConfig,
|
|
21
30
|
normalizeOptions,
|
|
22
31
|
nextSandboxPermissionIndex,
|
|
23
|
-
parseLandstripTraps,
|
|
24
32
|
removeDiscoveryFile,
|
|
25
|
-
sessionAllows,
|
|
26
33
|
sessionAllowancesFor,
|
|
27
34
|
sandboxConfigTarget,
|
|
28
|
-
sessionScopeFor,
|
|
29
35
|
rootSessionIDFor,
|
|
30
36
|
setSandboxConfigEnabled,
|
|
31
37
|
shouldRenderSandboxPermission,
|
|
32
38
|
type SessionAllowances,
|
|
33
|
-
updateForPermission,
|
|
34
39
|
writeConfigFile,
|
|
35
40
|
writeDiscoveryPort,
|
|
36
41
|
} from './shared.js';
|
|
@@ -463,21 +468,29 @@ const tui: TuiPlugin.Definition = {
|
|
|
463
468
|
const action = choice === 'deny' ? 'deny' : 'allow';
|
|
464
469
|
const verb = entry.operation === 'read' ? 'Read' : 'Write';
|
|
465
470
|
const directory = entry.directory;
|
|
466
|
-
const
|
|
471
|
+
const config = loadConfig(directory, optionOverrides);
|
|
472
|
+
let scope = sessionScopeFor(entry.path, directory);
|
|
473
|
+
if (
|
|
474
|
+
entry.operation === 'read'
|
|
475
|
+
? !isPathReadAllowed(entry.path, [scope], config.filesystem.denyRead, directory, {
|
|
476
|
+
requireAllowMatch: true,
|
|
477
|
+
})
|
|
478
|
+
: !isPathWriteAllowed(entry.path, [scope], config.filesystem.denyWrite, directory)
|
|
479
|
+
) {
|
|
480
|
+
scope = entry.path;
|
|
481
|
+
}
|
|
467
482
|
const allowances = sessionAllowancesFor(sessionAllowances, entry.sessionID);
|
|
468
483
|
const sessionPaths =
|
|
469
484
|
entry.operation === 'read' ? allowances.readPaths : allowances.writePaths;
|
|
470
|
-
|
|
471
485
|
let responseAction: 'allow' | 'deny' = action;
|
|
472
486
|
try {
|
|
473
487
|
if (action === 'allow') {
|
|
474
488
|
if (choice === 'project' || choice === 'global') {
|
|
475
489
|
const { globalPath, projectPath } = getConfigPaths(directory);
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
490
|
+
writeConfigFile(choice === 'project' ? projectPath : globalPath, {
|
|
491
|
+
filesystem:
|
|
492
|
+
entry.operation === 'read' ? { allowRead: [scope] } : { allowWrite: [scope] },
|
|
479
493
|
});
|
|
480
|
-
if (update) writeConfigFile(choice === 'project' ? projectPath : globalPath, update);
|
|
481
494
|
}
|
|
482
495
|
if (choice !== 'once') sessionPaths.add(scope);
|
|
483
496
|
}
|
|
@@ -581,23 +594,29 @@ const tui: TuiPlugin.Definition = {
|
|
|
581
594
|
if (seen.has(trap.query_id)) continue;
|
|
582
595
|
seen.add(trap.query_id);
|
|
583
596
|
|
|
584
|
-
|
|
597
|
+
const config = loadConfig(sessionDirectory, optionOverrides);
|
|
598
|
+
if (!config.enabled) {
|
|
585
599
|
respondQuery(socket, trap.query_id, 'allow');
|
|
586
600
|
continue;
|
|
587
601
|
}
|
|
588
602
|
|
|
589
603
|
if (trap.kind === 'filesystem') {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
604
|
+
const sessionAllowed =
|
|
605
|
+
trap.operation === 'read'
|
|
606
|
+
? evaluateReadAccess(
|
|
607
|
+
trap.path,
|
|
608
|
+
{ ...config.filesystem, allowRead: [...allowances.readPaths] },
|
|
609
|
+
{ baseDirectory: sessionDirectory, requireAllowMatch: true },
|
|
610
|
+
) === 'allow'
|
|
611
|
+
: evaluateWriteAccess(
|
|
612
|
+
trap.path,
|
|
613
|
+
{ ...config.filesystem, allowWrite: [...allowances.writePaths] },
|
|
614
|
+
{ baseDirectory: sessionDirectory },
|
|
615
|
+
) === 'allow';
|
|
616
|
+
if (sessionAllowed) {
|
|
597
617
|
respondQuery(socket, trap.query_id, 'allow');
|
|
598
618
|
continue;
|
|
599
619
|
}
|
|
600
|
-
|
|
601
620
|
const entry: FsQueryEntry = {
|
|
602
621
|
kind: 'fs-query',
|
|
603
622
|
id: `landstrip-${trap.operation}:${socketId}:${trap.query_id}`,
|