opencode-landstrip 0.17.19 → 0.17.21
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 +136 -130
- package/package.json +4 -4
- package/tui.ts +2 -1
package/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type SandboxConfig,
|
|
16
16
|
type SandboxFilesystemConfig,
|
|
17
17
|
controlResponseLine,
|
|
18
|
+
decodeLandstripTrap,
|
|
18
19
|
extractDomainsFromCommand,
|
|
19
20
|
formatLandstripTraps,
|
|
20
21
|
getConfigPaths,
|
|
@@ -29,16 +30,12 @@ import {
|
|
|
29
30
|
sessionScopeFor,
|
|
30
31
|
} from './shared.js';
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
network: {
|
|
34
|
-
allowNetwork: boolean;
|
|
35
|
-
allowLocalBinding: boolean;
|
|
36
|
-
allowAllUnixSockets: boolean;
|
|
37
|
-
allowUnixSockets: string[];
|
|
33
|
+
type LandstripPolicy = {
|
|
34
|
+
network: Omit<SandboxConfig['network'], 'allowedDomains' | 'deniedDomains'> & {
|
|
38
35
|
httpProxyPort?: number;
|
|
39
36
|
};
|
|
40
37
|
filesystem: SandboxFilesystemConfig;
|
|
41
|
-
}
|
|
38
|
+
};
|
|
42
39
|
|
|
43
40
|
interface BashSandboxState {
|
|
44
41
|
originalCommand: string;
|
|
@@ -219,8 +216,23 @@ function allowsAllDomains(allowedDomains: string[]): boolean {
|
|
|
219
216
|
return allowedDomains.includes('*');
|
|
220
217
|
}
|
|
221
218
|
|
|
222
|
-
function
|
|
223
|
-
return
|
|
219
|
+
function isDomainAllowed(domain: string, config: SandboxConfig): boolean {
|
|
220
|
+
return (
|
|
221
|
+
config.network.allowNetwork ||
|
|
222
|
+
(!domainMatchesAny(domain, config.network.deniedDomains) &&
|
|
223
|
+
domainMatchesAny(domain, config.network.allowedDomains))
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isFilesystemAllowed(
|
|
228
|
+
path: string,
|
|
229
|
+
allowPatterns: string[],
|
|
230
|
+
denyPatterns: string[],
|
|
231
|
+
baseDirectory: string,
|
|
232
|
+
): boolean {
|
|
233
|
+
const allowDepth = matchDepth(path, allowPatterns, baseDirectory);
|
|
234
|
+
const denyDepth = matchDepth(path, denyPatterns, baseDirectory);
|
|
235
|
+
return allowDepth >= 0 && allowDepth >= denyDepth;
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
function extractCandidatePaths(command: string): string[] {
|
|
@@ -250,24 +262,24 @@ function extractBlockedPath(
|
|
|
250
262
|
let match = output.match(
|
|
251
263
|
/(?:\/bin\/bash|bash|sh): (?:line \d+: )?([^:\n]+): (?:Operation not permitted|Permission denied)/,
|
|
252
264
|
);
|
|
253
|
-
if (match?.[1]) return
|
|
265
|
+
if (match?.[1]) return canonicalizePath(match[1], baseDirectory);
|
|
254
266
|
|
|
255
267
|
// ls/cat/cp: cannot open/access/stat '/path': Permission denied
|
|
256
268
|
match = output.match(
|
|
257
269
|
/^[a-zA-Z0-9_-]+: cannot (?:open|access|stat|create)(?: directory)? '?([^'\n]+?)'?(?: for (?:reading|writing))?: Permission denied$/m,
|
|
258
270
|
);
|
|
259
|
-
if (match?.[1]) return
|
|
271
|
+
if (match?.[1]) return canonicalizePath(match[1], baseDirectory);
|
|
260
272
|
|
|
261
273
|
// Generic: cmd: /absolute/path: Permission denied or Operation not permitted
|
|
262
274
|
match = output.match(
|
|
263
275
|
/^[a-zA-Z0-9_-]+: (\/[^\n:]+): (?:Operation not permitted|Permission denied)$/m,
|
|
264
276
|
);
|
|
265
|
-
if (match?.[1]) return
|
|
277
|
+
if (match?.[1]) return canonicalizePath(match[1], baseDirectory);
|
|
266
278
|
|
|
267
279
|
// Landstrip structured trap format carrying a denied path
|
|
268
280
|
const landstripTraps = parseLandstripTraps(output);
|
|
269
281
|
for (const trap of landstripTraps) {
|
|
270
|
-
if (trap.kind === 'filesystem') return
|
|
282
|
+
if (trap.kind === 'filesystem') return canonicalizePath(trap.path, baseDirectory);
|
|
271
283
|
}
|
|
272
284
|
|
|
273
285
|
if (
|
|
@@ -290,15 +302,15 @@ function evaluateReadPermission(
|
|
|
290
302
|
effectiveAllowRead: string[],
|
|
291
303
|
): SandboxPermissionDecision {
|
|
292
304
|
const filePath = canonicalizePath(path, baseDirectory);
|
|
293
|
-
const allowDepth = matchDepth(filePath, effectiveAllowRead, baseDirectory);
|
|
294
|
-
const denyDepth = matchDepth(filePath, config.filesystem.denyRead, baseDirectory);
|
|
295
305
|
|
|
296
306
|
// Reads are interactive, so the read tool never hard-denies: a path covered by
|
|
297
307
|
// allowRead at least as specifically as any denyRead is allowed silently;
|
|
298
308
|
// everything else asks for approval (allow once/session/persist or reject)
|
|
299
309
|
// rather than being blocked outright. denyRead still hard-applies to bash
|
|
300
310
|
// through the landstrip binary policy, which has no way to prompt.
|
|
301
|
-
if (
|
|
311
|
+
if (
|
|
312
|
+
isFilesystemAllowed(filePath, effectiveAllowRead, config.filesystem.denyRead, baseDirectory)
|
|
313
|
+
) {
|
|
302
314
|
return { status: 'allow', kind: 'read', resource: filePath, message: '' };
|
|
303
315
|
}
|
|
304
316
|
|
|
@@ -329,7 +341,9 @@ function evaluateWritePermission(
|
|
|
329
341
|
};
|
|
330
342
|
}
|
|
331
343
|
|
|
332
|
-
if (
|
|
344
|
+
if (
|
|
345
|
+
isFilesystemAllowed(filePath, effectiveAllowWrite, config.filesystem.denyWrite, baseDirectory)
|
|
346
|
+
) {
|
|
333
347
|
return { status: 'allow', kind: 'write', resource: filePath, message: '' };
|
|
334
348
|
}
|
|
335
349
|
|
|
@@ -358,7 +372,7 @@ function evaluateDomainPermission(
|
|
|
358
372
|
};
|
|
359
373
|
}
|
|
360
374
|
|
|
361
|
-
if (
|
|
375
|
+
if (isDomainAllowed(domain, config)) {
|
|
362
376
|
return { status: 'allow', kind: 'domain', resource: domain, message: '' };
|
|
363
377
|
}
|
|
364
378
|
|
|
@@ -370,6 +384,16 @@ function evaluateDomainPermission(
|
|
|
370
384
|
};
|
|
371
385
|
}
|
|
372
386
|
|
|
387
|
+
function evaluateCommandDomains(
|
|
388
|
+
command: string,
|
|
389
|
+
config: SandboxConfig,
|
|
390
|
+
): SandboxPermissionDecision[] {
|
|
391
|
+
if (config.network.allowNetwork) return [];
|
|
392
|
+
return extractDomainsFromCommand(command).map((domain) =>
|
|
393
|
+
evaluateDomainPermission(domain, config),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
373
397
|
function landstripVersion(): string | null {
|
|
374
398
|
const result = spawnSync(landstripBinaryPath(), ['--version'], { encoding: 'utf-8' });
|
|
375
399
|
if (result.status !== 0) return null;
|
|
@@ -467,11 +491,6 @@ function writePolicyFile(
|
|
|
467
491
|
function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () => Promise<void> }> {
|
|
468
492
|
const sockets = new Set<Socket>();
|
|
469
493
|
|
|
470
|
-
function domainAllowed(domain: string): boolean {
|
|
471
|
-
if (domainMatchesAny(domain, config.network.deniedDomains)) return false;
|
|
472
|
-
return domainMatchesAny(domain, config.network.allowedDomains);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
494
|
async function handleConnect(client: Socket, target: string, rest: Buffer): Promise<void> {
|
|
476
495
|
const endpoint = splitHostPort(target, 443);
|
|
477
496
|
if (!endpoint || !Number.isFinite(endpoint.port)) {
|
|
@@ -479,7 +498,7 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
|
|
|
479
498
|
return;
|
|
480
499
|
}
|
|
481
500
|
|
|
482
|
-
if (!
|
|
501
|
+
if (!isDomainAllowed(endpoint.host, config)) {
|
|
483
502
|
denyProxyRequest(client);
|
|
484
503
|
return;
|
|
485
504
|
}
|
|
@@ -525,7 +544,7 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
|
|
|
525
544
|
url = new URL(`http://${host}${rawTarget}`);
|
|
526
545
|
}
|
|
527
546
|
|
|
528
|
-
if (!
|
|
547
|
+
if (!isDomainAllowed(url.hostname, config)) {
|
|
529
548
|
denyProxyRequest(client);
|
|
530
549
|
return;
|
|
531
550
|
}
|
|
@@ -660,33 +679,25 @@ function startTrapServer(
|
|
|
660
679
|
buffer = buffer.slice(nl + 1);
|
|
661
680
|
nl = buffer.indexOf('\n');
|
|
662
681
|
if (line.length === 0) continue;
|
|
663
|
-
let
|
|
682
|
+
let trap: LandstripTrap | null = null;
|
|
664
683
|
try {
|
|
665
|
-
|
|
666
|
-
if (typeof parsed === 'object' && parsed !== null) {
|
|
667
|
-
obj = parsed as Record<string, unknown>;
|
|
668
|
-
}
|
|
684
|
+
trap = decodeLandstripTrap(JSON.parse(line));
|
|
669
685
|
} catch {
|
|
670
|
-
|
|
686
|
+
trap = null;
|
|
671
687
|
}
|
|
672
|
-
if (
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
// specific; a tie or more-specific allow carves the path back in.
|
|
682
|
-
const denyReadDepth = matchDepth(path, denyRead, baseDirectory);
|
|
683
|
-
const allowReadDepth = matchDepth(path, effectiveAllowRead, baseDirectory);
|
|
684
|
-
const denyWriteDepth = matchDepth(path, denyWrite, baseDirectory);
|
|
685
|
-
const allowWriteDepth = matchDepth(path, effectiveAllowWrite, baseDirectory);
|
|
688
|
+
if (
|
|
689
|
+
(trap?.kind === 'filesystem' || trap?.kind === 'network') &&
|
|
690
|
+
trap.state === 'query' &&
|
|
691
|
+
trap.query_id
|
|
692
|
+
) {
|
|
693
|
+
const queryId = trap.query_id;
|
|
694
|
+
if (trap.kind === 'filesystem') {
|
|
695
|
+
const path = canonicalizePath(trap.path, baseDirectory);
|
|
696
|
+
const operation = trap.operation;
|
|
686
697
|
const allowed =
|
|
687
698
|
operation === 'read'
|
|
688
|
-
?
|
|
689
|
-
:
|
|
699
|
+
? isFilesystemAllowed(path, effectiveAllowRead, denyRead, baseDirectory)
|
|
700
|
+
: isFilesystemAllowed(path, effectiveAllowWrite, denyWrite, baseDirectory);
|
|
690
701
|
if (allowed) {
|
|
691
702
|
trapSocket.write(controlResponseLine(queryId, 'allow'));
|
|
692
703
|
} else {
|
|
@@ -698,15 +709,15 @@ function startTrapServer(
|
|
|
698
709
|
trapLines.push(line);
|
|
699
710
|
}
|
|
700
711
|
} else if (
|
|
701
|
-
|
|
702
|
-
|
|
712
|
+
trap.kind === 'network' &&
|
|
713
|
+
(trap.operation === 'connect' || trap.operation === 'bind')
|
|
703
714
|
) {
|
|
704
715
|
// No policy field expresses "allow this address:port" (allowedDomains
|
|
705
716
|
// is hostname-based and enforced by the HTTP(S) proxy, not the broker),
|
|
706
717
|
// so — matching the filesystem branch above — auto-grant, remember it
|
|
707
718
|
// for the rest of the session, and surface it afterward rather than
|
|
708
719
|
// hanging or hard-denying an already-approved command.
|
|
709
|
-
const target =
|
|
720
|
+
const target = trap.target;
|
|
710
721
|
if (!sessionAllowedTargets.has(target)) {
|
|
711
722
|
sessionAllowedTargets.add(target);
|
|
712
723
|
trapLines.push(line);
|
|
@@ -843,6 +854,45 @@ function extractPatchPaths(patchText: string): string[] {
|
|
|
843
854
|
return paths;
|
|
844
855
|
}
|
|
845
856
|
|
|
857
|
+
function evaluateToolPermissions(
|
|
858
|
+
tool: string,
|
|
859
|
+
args: Record<string, unknown>,
|
|
860
|
+
config: SandboxConfig,
|
|
861
|
+
baseDirectory: string,
|
|
862
|
+
effectiveAllowRead: string[],
|
|
863
|
+
effectiveAllowWrite: string[],
|
|
864
|
+
): SandboxPermissionDecision[] {
|
|
865
|
+
if (tool === 'read') {
|
|
866
|
+
const paths = Array.isArray(args.paths)
|
|
867
|
+
? args.paths.filter((path): path is string => typeof path === 'string')
|
|
868
|
+
: [getToolPath(args)].filter((path): path is string => path !== undefined);
|
|
869
|
+
return paths.map((path) =>
|
|
870
|
+
evaluateReadPermission(path, config, baseDirectory, effectiveAllowRead),
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (tool === 'glob' || tool === 'grep' || tool === 'list') {
|
|
875
|
+
return [evaluateReadPermission(getSearchPath(args), config, baseDirectory, effectiveAllowRead)];
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (tool === 'write' || tool === 'edit') {
|
|
879
|
+
const path = getToolPath(args);
|
|
880
|
+
return path ? [evaluateWritePermission(path, config, baseDirectory, effectiveAllowWrite)] : [];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (tool === 'apply_patch' && typeof args.patchText === 'string') {
|
|
884
|
+
return extractPatchPaths(args.patchText).map((path) =>
|
|
885
|
+
evaluateWritePermission(path, config, baseDirectory, effectiveAllowWrite),
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (tool === 'bash' && typeof args.command === 'string') {
|
|
890
|
+
return evaluateCommandDomains(args.command, config);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return [];
|
|
894
|
+
}
|
|
895
|
+
|
|
846
896
|
function errorWithConfigPaths(baseDirectory: string, message: string): Error {
|
|
847
897
|
const { globalPath, projectPath } = getConfigPaths(baseDirectory);
|
|
848
898
|
return new Error(`${message}\n\nUpdate sandbox config in:\n ${projectPath}\n ${globalPath}`);
|
|
@@ -857,12 +907,16 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
857
907
|
const sessionAllowedWritePaths = new Set<string>();
|
|
858
908
|
const sessionAllowedTargets = new Set<string>();
|
|
859
909
|
|
|
910
|
+
function mergeAllowances(configured: string[], session: Set<string>): string[] {
|
|
911
|
+
return [...configured, ...session];
|
|
912
|
+
}
|
|
913
|
+
|
|
860
914
|
function getEffectiveAllowRead(config: SandboxConfig): string[] {
|
|
861
|
-
return
|
|
915
|
+
return mergeAllowances(config.filesystem.allowRead, sessionAllowedReadPaths);
|
|
862
916
|
}
|
|
863
917
|
|
|
864
918
|
function getEffectiveAllowWrite(config: SandboxConfig): string[] {
|
|
865
|
-
return
|
|
919
|
+
return mergeAllowances(config.filesystem.allowWrite, sessionAllowedWritePaths);
|
|
866
920
|
}
|
|
867
921
|
let enabledNotified = false;
|
|
868
922
|
let configuredShell: string | undefined;
|
|
@@ -1139,11 +1193,10 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1139
1193
|
};
|
|
1140
1194
|
|
|
1141
1195
|
if (!allowNetwork) {
|
|
1142
|
-
for (const
|
|
1143
|
-
const decision = evaluateDomainPermission(domain, effectiveConfig);
|
|
1196
|
+
for (const decision of evaluateCommandDomains(args.command as string, effectiveConfig)) {
|
|
1144
1197
|
if (decision.status === 'allow') continue;
|
|
1145
1198
|
if (decision.status === 'ask' && hasCallAllowance(callID, decision)) {
|
|
1146
|
-
callAllowedDomains.push(
|
|
1199
|
+
callAllowedDomains.push(decision.resource);
|
|
1147
1200
|
continue;
|
|
1148
1201
|
}
|
|
1149
1202
|
throw errorWithConfigPaths(directory, decision.message);
|
|
@@ -1212,7 +1265,10 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1212
1265
|
|
|
1213
1266
|
'permission.ask': async (input, output) => {
|
|
1214
1267
|
const config = await activeConfig();
|
|
1215
|
-
if (!config)
|
|
1268
|
+
if (!config) {
|
|
1269
|
+
output.status = 'allow';
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1216
1272
|
|
|
1217
1273
|
const request = input as Record<string, unknown>;
|
|
1218
1274
|
const permission = permissionType(request);
|
|
@@ -1226,41 +1282,22 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1226
1282
|
: undefined;
|
|
1227
1283
|
const patterns = permissionPatterns(request);
|
|
1228
1284
|
|
|
1229
|
-
const decisions: SandboxPermissionDecision[] = [];
|
|
1230
1285
|
const effectiveAllowRead = getEffectiveAllowRead(config);
|
|
1231
1286
|
const effectiveAllowWrite = getEffectiveAllowWrite(config);
|
|
1232
|
-
|
|
1233
|
-
if (permission === 'read')
|
|
1234
|
-
for (const pattern of patterns) {
|
|
1235
|
-
decisions.push(evaluateReadPermission(pattern, config, directory, effectiveAllowRead));
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
if (permission === 'glob' || permission === 'grep' || permission === 'list') {
|
|
1240
|
-
const searchPath = typeof metadata.path === 'string' ? metadata.path : '.';
|
|
1241
|
-
decisions.push(evaluateReadPermission(searchPath, config, directory, effectiveAllowRead));
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1287
|
+
const args: Record<string, unknown> = { ...metadata };
|
|
1288
|
+
if (permission === 'read') args.paths = patterns;
|
|
1244
1289
|
if (permission === 'edit') {
|
|
1245
|
-
|
|
1246
|
-
typeof metadata.filepath === 'string'
|
|
1247
|
-
? metadata.filepath
|
|
1248
|
-
: patterns.length === 1
|
|
1249
|
-
? patterns[0]
|
|
1250
|
-
: undefined;
|
|
1251
|
-
if (filepath) {
|
|
1252
|
-
decisions.push(evaluateWritePermission(filepath, config, directory, effectiveAllowWrite));
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
if (permission === 'bash') {
|
|
1257
|
-
const command = typeof metadata.command === 'string' ? metadata.command : patterns[0];
|
|
1258
|
-
if (typeof command === 'string' && !config.network.allowNetwork) {
|
|
1259
|
-
for (const domain of extractDomainsFromCommand(command)) {
|
|
1260
|
-
decisions.push(evaluateDomainPermission(domain, config));
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1290
|
+
args.path = typeof metadata.filepath === 'string' ? metadata.filepath : patterns[0];
|
|
1263
1291
|
}
|
|
1292
|
+
if (permission === 'bash' && typeof args.command !== 'string') args.command = patterns[0];
|
|
1293
|
+
const decisions = evaluateToolPermissions(
|
|
1294
|
+
permission,
|
|
1295
|
+
args,
|
|
1296
|
+
config,
|
|
1297
|
+
directory,
|
|
1298
|
+
effectiveAllowRead,
|
|
1299
|
+
effectiveAllowWrite,
|
|
1300
|
+
);
|
|
1264
1301
|
|
|
1265
1302
|
const decision =
|
|
1266
1303
|
decisions.find((item) => item.status === 'deny') ??
|
|
@@ -1277,49 +1314,21 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1277
1314
|
const config = await activeConfig();
|
|
1278
1315
|
if (!config) return;
|
|
1279
1316
|
|
|
1280
|
-
const effectiveAllowRead = getEffectiveAllowRead(config);
|
|
1281
|
-
const effectiveAllowWrite = getEffectiveAllowWrite(config);
|
|
1282
|
-
|
|
1283
1317
|
if (input.tool === 'bash') {
|
|
1284
1318
|
await prepareBash(input.callID, output.args, config);
|
|
1285
1319
|
return;
|
|
1286
1320
|
}
|
|
1287
1321
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
if (input.tool === 'glob' || input.tool === 'grep' || input.tool === 'list') {
|
|
1299
|
-
enforcePermission(
|
|
1300
|
-
input.callID,
|
|
1301
|
-
evaluateReadPermission(getSearchPath(output.args), config, directory, effectiveAllowRead),
|
|
1302
|
-
);
|
|
1303
|
-
return;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
if (input.tool === 'write' || input.tool === 'edit') {
|
|
1307
|
-
const path = getToolPath(output.args);
|
|
1308
|
-
if (path)
|
|
1309
|
-
enforcePermission(
|
|
1310
|
-
input.callID,
|
|
1311
|
-
evaluateWritePermission(path, config, directory, effectiveAllowWrite),
|
|
1312
|
-
);
|
|
1313
|
-
return;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
if (input.tool === 'apply_patch' && typeof output.args.patchText === 'string') {
|
|
1317
|
-
for (const path of extractPatchPaths(output.args.patchText)) {
|
|
1318
|
-
enforcePermission(
|
|
1319
|
-
input.callID,
|
|
1320
|
-
evaluateWritePermission(path, config, directory, effectiveAllowWrite),
|
|
1321
|
-
);
|
|
1322
|
-
}
|
|
1322
|
+
const decisions = evaluateToolPermissions(
|
|
1323
|
+
input.tool,
|
|
1324
|
+
output.args,
|
|
1325
|
+
config,
|
|
1326
|
+
directory,
|
|
1327
|
+
getEffectiveAllowRead(config),
|
|
1328
|
+
getEffectiveAllowWrite(config),
|
|
1329
|
+
);
|
|
1330
|
+
for (const decision of decisions) {
|
|
1331
|
+
enforcePermission(input.callID, decision);
|
|
1323
1332
|
}
|
|
1324
1333
|
},
|
|
1325
1334
|
|
|
@@ -1430,11 +1439,8 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1430
1439
|
if (writeDecision.status === 'deny') reportBlocked(writeDecision);
|
|
1431
1440
|
}
|
|
1432
1441
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
const decision = evaluateDomainPermission(domain, config);
|
|
1436
|
-
if (decision.status !== 'allow') reportBlocked(decision);
|
|
1437
|
-
}
|
|
1442
|
+
for (const decision of evaluateCommandDomains(shellCommand, config)) {
|
|
1443
|
+
if (decision.status !== 'allow') reportBlocked(decision);
|
|
1438
1444
|
}
|
|
1439
1445
|
}
|
|
1440
1446
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.21",
|
|
4
4
|
"description": "Landlock-based sandboxing for opencode with landstrip",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landlock",
|
|
@@ -37,18 +37,18 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
|
-
"fmt": "oxfmt index.ts tui.ts shared.ts package.json tsconfig.json sandbox.json .oxfmtrc.json README.md test/*.
|
|
40
|
+
"fmt": "oxfmt index.ts tui.ts shared.ts package.json tsconfig.json sandbox.json .oxfmtrc.json README.md test/*.mjs",
|
|
41
41
|
"lint": "oxlint index.ts tui.ts shared.ts",
|
|
42
42
|
"check": "tsc --noEmit",
|
|
43
43
|
"test": "node --test test/*.test.mjs",
|
|
44
44
|
"all": "npm run fmt && npm run lint && npm run check && npm test",
|
|
45
|
-
"ci:fmt": "oxfmt --check index.ts tui.ts shared.ts package.json tsconfig.json sandbox.json .oxfmtrc.json README.md test/*.
|
|
45
|
+
"ci:fmt": "oxfmt --check index.ts tui.ts shared.ts package.json tsconfig.json sandbox.json .oxfmtrc.json README.md test/*.mjs",
|
|
46
46
|
"ci:lint": "npm run lint",
|
|
47
47
|
"ci:check": "npm run check",
|
|
48
48
|
"ci:test": "npm test"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@landstrip/landstrip": "^0.17.
|
|
51
|
+
"@landstrip/landstrip": "^0.17.21",
|
|
52
52
|
"@opentui/solid": ">=0.3.4",
|
|
53
53
|
"solid-js": "1.9.12"
|
|
54
54
|
},
|
package/tui.ts
CHANGED
|
@@ -444,7 +444,8 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
const unsubscribeAsked = api.event.on('permission.asked', (event) => {
|
|
447
|
-
const directory = api.state.path.directory
|
|
447
|
+
const directory = api.state.path.directory;
|
|
448
|
+
if (!directory) return;
|
|
448
449
|
if (!loadConfig(directory, optionOverrides).enabled) return;
|
|
449
450
|
const pending = event.properties as PendingPermission;
|
|
450
451
|
enqueue(pending);
|