opencode-landstrip 0.18.34 → 0.18.36
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 +23 -70
- package/index.ts +56 -467
- package/package.json +2 -2
- package/shared.ts +100 -37
- package/tui.ts +290 -154
- package/LICENSE +0 -201
package/shared.ts
CHANGED
|
@@ -9,14 +9,19 @@ import {
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
allowsAllDomains,
|
|
12
|
+
canonicalizeGlobPattern,
|
|
13
|
+
canonicalizePath,
|
|
12
14
|
controlResponseLine,
|
|
13
15
|
decodeLandstripTrap,
|
|
14
16
|
domainMatchesAny,
|
|
15
17
|
domainMatchesPattern,
|
|
16
18
|
expandHomePath,
|
|
19
|
+
expandPath,
|
|
20
|
+
globToRegExp,
|
|
17
21
|
formatLandstripTrap,
|
|
18
22
|
formatLandstripTraps,
|
|
19
23
|
isRecord,
|
|
24
|
+
normalizePathSeparators,
|
|
20
25
|
parseLandstripTraps,
|
|
21
26
|
pathUnderDirectory,
|
|
22
27
|
sessionAllows,
|
|
@@ -25,14 +30,19 @@ import {
|
|
|
25
30
|
|
|
26
31
|
export {
|
|
27
32
|
allowsAllDomains,
|
|
33
|
+
canonicalizeGlobPattern,
|
|
34
|
+
canonicalizePath,
|
|
28
35
|
controlResponseLine,
|
|
29
36
|
decodeLandstripTrap,
|
|
30
37
|
domainMatchesAny,
|
|
31
38
|
domainMatchesPattern,
|
|
32
39
|
expandHomePath,
|
|
40
|
+
expandPath,
|
|
41
|
+
globToRegExp,
|
|
33
42
|
formatLandstripTrap,
|
|
34
43
|
formatLandstripTraps,
|
|
35
44
|
isRecord,
|
|
45
|
+
normalizePathSeparators,
|
|
36
46
|
parseLandstripTraps,
|
|
37
47
|
pathUnderDirectory,
|
|
38
48
|
sessionAllows,
|
|
@@ -93,57 +103,97 @@ export function list(values: string[]): string {
|
|
|
93
103
|
return values.join(', ') || '(none)';
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
function rejectUnknownFields(
|
|
107
|
+
value: Record<string, unknown>,
|
|
108
|
+
fields: readonly string[],
|
|
109
|
+
prefix = '',
|
|
110
|
+
): void {
|
|
111
|
+
for (const field of Object.keys(value)) {
|
|
112
|
+
if (!fields.includes(field)) throw new Error(`unknown sandbox field ${prefix}${field}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function booleanValue(value: unknown, field: string): boolean | undefined {
|
|
117
|
+
if (value === undefined) return undefined;
|
|
118
|
+
if (typeof value !== 'boolean') throw new Error(`${field} must be a boolean`);
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function stringArray(value: unknown, field: string): string[] | undefined {
|
|
123
|
+
if (value === undefined) return undefined;
|
|
124
|
+
if (!Array.isArray(value) || [...value].some((item) => typeof item !== 'string')) {
|
|
125
|
+
throw new Error(`${field} must be an array of strings`);
|
|
126
|
+
}
|
|
127
|
+
return [...value];
|
|
99
128
|
}
|
|
100
129
|
|
|
101
130
|
function normalizeNetworkConfig(value: unknown): Partial<SandboxNetworkConfig> | undefined {
|
|
102
|
-
if (
|
|
131
|
+
if (value === undefined) return undefined;
|
|
132
|
+
if (!isRecord(value)) throw new Error('network must be an object');
|
|
133
|
+
rejectUnknownFields(
|
|
134
|
+
value,
|
|
135
|
+
[
|
|
136
|
+
'allowNetwork',
|
|
137
|
+
'allowLocalBinding',
|
|
138
|
+
'allowAllUnixSockets',
|
|
139
|
+
'allowUnixSockets',
|
|
140
|
+
'allowedDomains',
|
|
141
|
+
'deniedDomains',
|
|
142
|
+
],
|
|
143
|
+
'network.',
|
|
144
|
+
);
|
|
103
145
|
|
|
104
146
|
const config: Partial<SandboxNetworkConfig> = {};
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
147
|
+
const allowNetwork = booleanValue(value.allowNetwork, 'network.allowNetwork');
|
|
148
|
+
if (allowNetwork !== undefined) config.allowNetwork = allowNetwork;
|
|
149
|
+
const allowLocalBinding = booleanValue(value.allowLocalBinding, 'network.allowLocalBinding');
|
|
150
|
+
if (allowLocalBinding !== undefined) config.allowLocalBinding = allowLocalBinding;
|
|
151
|
+
const allowAllUnixSockets = booleanValue(
|
|
152
|
+
value.allowAllUnixSockets,
|
|
153
|
+
'network.allowAllUnixSockets',
|
|
154
|
+
);
|
|
155
|
+
if (allowAllUnixSockets !== undefined) config.allowAllUnixSockets = allowAllUnixSockets;
|
|
110
156
|
|
|
111
|
-
const allowUnixSockets = stringArray(value.allowUnixSockets);
|
|
157
|
+
const allowUnixSockets = stringArray(value.allowUnixSockets, 'network.allowUnixSockets');
|
|
112
158
|
if (allowUnixSockets) config.allowUnixSockets = allowUnixSockets;
|
|
113
159
|
|
|
114
|
-
const allowedDomains = stringArray(value.allowedDomains);
|
|
160
|
+
const allowedDomains = stringArray(value.allowedDomains, 'network.allowedDomains');
|
|
115
161
|
if (allowedDomains) config.allowedDomains = allowedDomains;
|
|
116
162
|
|
|
117
|
-
const deniedDomains = stringArray(value.deniedDomains);
|
|
163
|
+
const deniedDomains = stringArray(value.deniedDomains, 'network.deniedDomains');
|
|
118
164
|
if (deniedDomains) config.deniedDomains = deniedDomains;
|
|
119
165
|
|
|
120
166
|
return config;
|
|
121
167
|
}
|
|
122
168
|
|
|
123
169
|
function normalizeFilesystemConfig(value: unknown): Partial<SandboxFilesystemConfig> | undefined {
|
|
124
|
-
if (
|
|
170
|
+
if (value === undefined) return undefined;
|
|
171
|
+
if (!isRecord(value)) throw new Error('filesystem must be an object');
|
|
172
|
+
rejectUnknownFields(value, ['denyRead', 'allowRead', 'allowWrite', 'denyWrite'], 'filesystem.');
|
|
125
173
|
|
|
126
174
|
const config: Partial<SandboxFilesystemConfig> = {};
|
|
127
|
-
const denyRead = stringArray(value.denyRead);
|
|
175
|
+
const denyRead = stringArray(value.denyRead, 'filesystem.denyRead');
|
|
128
176
|
if (denyRead) config.denyRead = denyRead;
|
|
129
177
|
|
|
130
|
-
const allowRead = stringArray(value.allowRead);
|
|
178
|
+
const allowRead = stringArray(value.allowRead, 'filesystem.allowRead');
|
|
131
179
|
if (allowRead) config.allowRead = allowRead;
|
|
132
180
|
|
|
133
|
-
const allowWrite = stringArray(value.allowWrite);
|
|
181
|
+
const allowWrite = stringArray(value.allowWrite, 'filesystem.allowWrite');
|
|
134
182
|
if (allowWrite) config.allowWrite = allowWrite;
|
|
135
183
|
|
|
136
|
-
const denyWrite = stringArray(value.denyWrite);
|
|
184
|
+
const denyWrite = stringArray(value.denyWrite, 'filesystem.denyWrite');
|
|
137
185
|
if (denyWrite) config.denyWrite = denyWrite;
|
|
138
186
|
|
|
139
187
|
return config;
|
|
140
188
|
}
|
|
141
189
|
|
|
142
190
|
export function normalizeConfig(value: unknown): SandboxConfigOverrides {
|
|
143
|
-
if (!isRecord(value))
|
|
191
|
+
if (!isRecord(value)) throw new Error('sandbox config must be an object');
|
|
192
|
+
rejectUnknownFields(value, ['enabled', 'network', 'filesystem']);
|
|
144
193
|
|
|
145
194
|
const config: SandboxConfigOverrides = {};
|
|
146
|
-
|
|
195
|
+
const enabled = booleanValue(value.enabled, 'enabled');
|
|
196
|
+
if (enabled !== undefined) config.enabled = enabled;
|
|
147
197
|
|
|
148
198
|
const network = normalizeNetworkConfig(value.network);
|
|
149
199
|
if (network) config.network = network;
|
|
@@ -156,7 +206,9 @@ export function normalizeConfig(value: unknown): SandboxConfigOverrides {
|
|
|
156
206
|
|
|
157
207
|
export function normalizeOptions(options: unknown): SandboxConfigOverrides {
|
|
158
208
|
if (!isRecord(options)) return {};
|
|
159
|
-
|
|
209
|
+
if (options.config === undefined) return normalizeConfig(options);
|
|
210
|
+
if (!isRecord(options.config)) throw new Error('config must be an object');
|
|
211
|
+
return normalizeConfig(options.config);
|
|
160
212
|
}
|
|
161
213
|
|
|
162
214
|
function mergeArray(base: string[], override?: string[]): string[] {
|
|
@@ -194,15 +246,16 @@ export function getConfigPaths(baseDirectory: string): { globalPath: string; pro
|
|
|
194
246
|
};
|
|
195
247
|
}
|
|
196
248
|
|
|
197
|
-
|
|
198
|
-
// parsed, so callers can refuse to overwrite a corrupted config.
|
|
199
|
-
export function readConfigFile(configPath: string): SandboxConfigOverrides | null {
|
|
249
|
+
export function readConfigFile(configPath: string): SandboxConfigOverrides {
|
|
200
250
|
if (!existsSync(configPath)) return {};
|
|
201
251
|
|
|
202
252
|
try {
|
|
203
253
|
return normalizeConfig(JSON.parse(readFileSync(configPath, 'utf-8')));
|
|
204
|
-
} catch {
|
|
205
|
-
|
|
254
|
+
} catch (error) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Could not load ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
257
|
+
{ cause: error },
|
|
258
|
+
);
|
|
206
259
|
}
|
|
207
260
|
}
|
|
208
261
|
|
|
@@ -219,17 +272,14 @@ export function loadConfig(
|
|
|
219
272
|
}
|
|
220
273
|
|
|
221
274
|
const templateConfig: SandboxConfig = JSON.parse(readFileSync(templatePath, 'utf-8'));
|
|
222
|
-
const globalOverrides = readConfigFile(globalPath)
|
|
275
|
+
const globalOverrides = readConfigFile(globalPath);
|
|
223
276
|
const baseConfig = deepMerge(templateConfig, globalOverrides);
|
|
224
277
|
|
|
225
|
-
return deepMerge(deepMerge(baseConfig, readConfigFile(projectPath)
|
|
278
|
+
return deepMerge(deepMerge(baseConfig, readConfigFile(projectPath)), optionOverrides);
|
|
226
279
|
}
|
|
227
280
|
|
|
228
281
|
export function writeConfigFile(configPath: string, update: SandboxConfigOverrides): void {
|
|
229
282
|
const current = readConfigFile(configPath);
|
|
230
|
-
if (current === null) {
|
|
231
|
-
throw new Error(`Config file ${configPath} is corrupted; refusing to overwrite`);
|
|
232
|
-
}
|
|
233
283
|
|
|
234
284
|
const templateConfig: SandboxConfig = JSON.parse(
|
|
235
285
|
readFileSync(join(packageDir, 'sandbox.json'), 'utf-8'),
|
|
@@ -482,19 +532,32 @@ export function discoveryFilePath(baseDirectory: string): string {
|
|
|
482
532
|
return join(discoveryDir(), `port-${directoryHash(baseDirectory)}.json`);
|
|
483
533
|
}
|
|
484
534
|
|
|
485
|
-
// /
|
|
535
|
+
// /landstrip toggles the persisted `enabled` flag. Write it where the setting
|
|
486
536
|
// already lives — the project config if it sets `enabled`, otherwise the global
|
|
487
537
|
// config — and return the scope written so the UI can report it.
|
|
538
|
+
export function sandboxConfigTarget(baseDirectory: string): {
|
|
539
|
+
scope: 'project' | 'global';
|
|
540
|
+
path: string;
|
|
541
|
+
} {
|
|
542
|
+
const { globalPath, projectPath } = getConfigPaths(baseDirectory);
|
|
543
|
+
const useProject = readConfigFile(projectPath).enabled !== undefined;
|
|
544
|
+
return useProject
|
|
545
|
+
? { scope: 'project', path: projectPath }
|
|
546
|
+
: { scope: 'global', path: globalPath };
|
|
547
|
+
}
|
|
548
|
+
|
|
488
549
|
export function setSandboxConfigEnabled(
|
|
489
550
|
baseDirectory: string,
|
|
490
551
|
enabled: boolean,
|
|
552
|
+
optionOverrides: SandboxConfigOverrides = {},
|
|
491
553
|
): 'project' | 'global' {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
554
|
+
if (optionOverrides.enabled !== undefined) {
|
|
555
|
+
throw new Error('Sandbox state is managed by plugin options');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const target = sandboxConfigTarget(baseDirectory);
|
|
559
|
+
writeConfigFile(target.path, { enabled });
|
|
560
|
+
return target.scope;
|
|
498
561
|
}
|
|
499
562
|
|
|
500
563
|
export function writeDiscoveryPort(baseDirectory: string, port: number): void {
|