opencode-landstrip 0.3.10 → 0.3.11
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 +262 -103
- package/landstrip.d.ts +3 -0
- package/package.json +8 -7
package/index.ts
CHANGED
|
@@ -74,6 +74,15 @@ interface BashSandboxState {
|
|
|
74
74
|
stop: (() => Promise<void>) | null;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
type SandboxPermissionKind = 'read' | 'write' | 'domain';
|
|
78
|
+
|
|
79
|
+
interface SandboxPermissionDecision {
|
|
80
|
+
status: 'allow' | 'ask' | 'deny';
|
|
81
|
+
kind: SandboxPermissionKind;
|
|
82
|
+
resource: string;
|
|
83
|
+
message: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
type ToastVariant = 'info' | 'success' | 'warning' | 'error';
|
|
78
87
|
|
|
79
88
|
const LANDSTRIP_VERSION = [0, 11, 0] as const;
|
|
@@ -434,6 +443,89 @@ function firstBlockedDomain(
|
|
|
434
443
|
return null;
|
|
435
444
|
}
|
|
436
445
|
|
|
446
|
+
function evaluateReadPermission(
|
|
447
|
+
path: string,
|
|
448
|
+
config: SandboxConfig,
|
|
449
|
+
baseDirectory: string,
|
|
450
|
+
effectiveAllowRead: string[],
|
|
451
|
+
): SandboxPermissionDecision {
|
|
452
|
+
const filePath = canonicalizePath(path, baseDirectory);
|
|
453
|
+
|
|
454
|
+
if (!shouldPromptForRead(filePath, effectiveAllowRead, baseDirectory)) {
|
|
455
|
+
return { status: 'allow', kind: 'read', resource: filePath, message: '' };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (isBlockedByDenyRead(filePath, config, baseDirectory)) {
|
|
459
|
+
return {
|
|
460
|
+
status: 'deny',
|
|
461
|
+
kind: 'read',
|
|
462
|
+
resource: filePath,
|
|
463
|
+
message: `Sandbox: read access denied for "${filePath}" (denyRead overrides allowRead).`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
status: 'ask',
|
|
469
|
+
kind: 'read',
|
|
470
|
+
resource: filePath,
|
|
471
|
+
message: `Sandbox: read access requires approval for "${filePath}" (not in filesystem.allowRead).`,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function evaluateWritePermission(
|
|
476
|
+
path: string,
|
|
477
|
+
config: SandboxConfig,
|
|
478
|
+
baseDirectory: string,
|
|
479
|
+
effectiveAllowWrite: string[],
|
|
480
|
+
): SandboxPermissionDecision {
|
|
481
|
+
const filePath = canonicalizePath(path, baseDirectory);
|
|
482
|
+
|
|
483
|
+
if (!shouldPromptForWrite(filePath, effectiveAllowWrite, baseDirectory)) {
|
|
484
|
+
return { status: 'allow', kind: 'write', resource: filePath, message: '' };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (matchesPattern(filePath, config.filesystem.denyWrite, baseDirectory)) {
|
|
488
|
+
return {
|
|
489
|
+
status: 'deny',
|
|
490
|
+
kind: 'write',
|
|
491
|
+
resource: filePath,
|
|
492
|
+
message: `Sandbox: write access denied for "${filePath}" (in filesystem.denyWrite).`,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
status: 'ask',
|
|
498
|
+
kind: 'write',
|
|
499
|
+
resource: filePath,
|
|
500
|
+
message: `Sandbox: write access requires approval for "${filePath}" (not in filesystem.allowWrite).`,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function evaluateDomainPermission(
|
|
505
|
+
domain: string,
|
|
506
|
+
config: SandboxConfig,
|
|
507
|
+
): SandboxPermissionDecision {
|
|
508
|
+
if (config.network.allowNetwork || domainMatchesAny(domain, config.network.allowedDomains)) {
|
|
509
|
+
return { status: 'allow', kind: 'domain', resource: domain, message: '' };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (domainMatchesAny(domain, config.network.deniedDomains)) {
|
|
513
|
+
return {
|
|
514
|
+
status: 'deny',
|
|
515
|
+
kind: 'domain',
|
|
516
|
+
resource: domain,
|
|
517
|
+
message: `Sandbox: network access denied for "${domain}" (is blocked by network.deniedDomains).`,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
status: 'ask',
|
|
523
|
+
kind: 'domain',
|
|
524
|
+
resource: domain,
|
|
525
|
+
message: `Sandbox: network access requires approval for "${domain}" (not in network.allowedDomains).`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
437
529
|
function landstripVersion(): string | null {
|
|
438
530
|
const result = spawnSync(binaryPath(), ['--version'], { encoding: 'utf-8' });
|
|
439
531
|
if (result.status !== 0) return null;
|
|
@@ -829,69 +921,14 @@ function errorWithConfigPaths(baseDirectory: string, message: string): Error {
|
|
|
829
921
|
return new Error(`${message}\n\nUpdate sandbox config in:\n ${projectPath}\n ${globalPath}`);
|
|
830
922
|
}
|
|
831
923
|
|
|
832
|
-
function assertReadAllowed(
|
|
833
|
-
path: string,
|
|
834
|
-
config: SandboxConfig,
|
|
835
|
-
baseDirectory: string,
|
|
836
|
-
effectiveAllowRead: string[],
|
|
837
|
-
): void {
|
|
838
|
-
const filePath = canonicalizePath(path, baseDirectory);
|
|
839
|
-
|
|
840
|
-
if (!shouldPromptForRead(filePath, effectiveAllowRead, baseDirectory)) return;
|
|
841
|
-
|
|
842
|
-
if (isBlockedByDenyRead(filePath, config, baseDirectory)) {
|
|
843
|
-
throw errorWithConfigPaths(
|
|
844
|
-
baseDirectory,
|
|
845
|
-
`Sandbox: read access denied for "${filePath}" (denyRead overrides allowRead).`,
|
|
846
|
-
);
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
throw errorWithConfigPaths(
|
|
850
|
-
baseDirectory,
|
|
851
|
-
`Sandbox: read access denied for "${filePath}" (not in filesystem.allowRead).`,
|
|
852
|
-
);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
function assertWriteAllowed(
|
|
856
|
-
path: string,
|
|
857
|
-
config: SandboxConfig,
|
|
858
|
-
baseDirectory: string,
|
|
859
|
-
effectiveAllowWrite: string[],
|
|
860
|
-
): void {
|
|
861
|
-
const filePath = canonicalizePath(path, baseDirectory);
|
|
862
|
-
|
|
863
|
-
if (!shouldPromptForWrite(filePath, effectiveAllowWrite, baseDirectory)) return;
|
|
864
|
-
|
|
865
|
-
if (matchesPattern(filePath, config.filesystem.denyWrite, baseDirectory)) {
|
|
866
|
-
throw errorWithConfigPaths(
|
|
867
|
-
baseDirectory,
|
|
868
|
-
`Sandbox: write access denied for "${filePath}" (in filesystem.denyWrite).`,
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
throw errorWithConfigPaths(
|
|
873
|
-
baseDirectory,
|
|
874
|
-
`Sandbox: write access denied for "${filePath}" (not in filesystem.allowWrite).`,
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function assertApplyPatchAllowed(
|
|
879
|
-
args: Record<string, unknown>,
|
|
880
|
-
config: SandboxConfig,
|
|
881
|
-
baseDirectory: string,
|
|
882
|
-
effectiveAllowWrite: string[],
|
|
883
|
-
): void {
|
|
884
|
-
if (typeof args.patchText !== 'string') return;
|
|
885
|
-
for (const path of extractPatchPaths(args.patchText))
|
|
886
|
-
assertWriteAllowed(path, config, baseDirectory, effectiveAllowWrite);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
924
|
const plugin: Plugin = async ({ client, directory }: PluginInput, options?: PluginOptions) => {
|
|
890
925
|
const optionOverrides = normalizeOptions(options);
|
|
891
926
|
const activeBash = new Map<string, BashSandboxState>();
|
|
892
927
|
const notified = new Set<string>();
|
|
893
928
|
const sessionAllowedReadPaths: string[] = [];
|
|
894
929
|
const sessionAllowedWritePaths: string[] = [];
|
|
930
|
+
const sessionAllowedDomains: string[] = [];
|
|
931
|
+
const callAllowances = new Set<string>();
|
|
895
932
|
let enabledNotified = false;
|
|
896
933
|
let sandboxDisabled = false;
|
|
897
934
|
let configuredShell: string | undefined;
|
|
@@ -905,6 +942,31 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
905
942
|
return [...config.filesystem.allowWrite, ...sessionAllowedWritePaths];
|
|
906
943
|
}
|
|
907
944
|
|
|
945
|
+
function getEffectiveAllowedDomains(config: SandboxConfig): string[] {
|
|
946
|
+
return [...config.network.allowedDomains, ...sessionAllowedDomains];
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function allowanceKey(callID: string, kind: SandboxPermissionKind, resource: string): string {
|
|
950
|
+
return `${callID}:${kind}:${resource}`;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function rememberCallAllowance(
|
|
954
|
+
callID: string | undefined,
|
|
955
|
+
decision: SandboxPermissionDecision,
|
|
956
|
+
): void {
|
|
957
|
+
if (!callID || decision.status === 'deny') return;
|
|
958
|
+
callAllowances.add(allowanceKey(callID, decision.kind, decision.resource));
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function hasCallAllowance(callID: string, decision: SandboxPermissionDecision): boolean {
|
|
962
|
+
return callAllowances.has(allowanceKey(callID, decision.kind, decision.resource));
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function enforcePermission(callID: string, decision: SandboxPermissionDecision): void {
|
|
966
|
+
if (decision.status === 'allow' || hasCallAllowance(callID, decision)) return;
|
|
967
|
+
throw errorWithConfigPaths(directory, decision.message);
|
|
968
|
+
}
|
|
969
|
+
|
|
908
970
|
function pushCommandText(
|
|
909
971
|
input: { sessionID: string },
|
|
910
972
|
output: { parts: unknown[] },
|
|
@@ -922,7 +984,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
922
984
|
function sandboxSummary(config: SandboxConfig): string {
|
|
923
985
|
const { globalPath, projectPath } = getConfigPaths(directory);
|
|
924
986
|
const networkMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
|
|
925
|
-
const allowed = config.
|
|
987
|
+
const allowed = getEffectiveAllowedDomains(config).join(', ') || '(none)';
|
|
926
988
|
const denied = config.network.deniedDomains.join(', ') || '(none)';
|
|
927
989
|
const denyRead = config.filesystem.denyRead.join(', ') || '(none)';
|
|
928
990
|
const allowRead = getEffectiveAllowRead(config).join(', ') || '(none)';
|
|
@@ -1130,27 +1192,37 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1130
1192
|
}
|
|
1131
1193
|
|
|
1132
1194
|
const allowNetwork = config.network.allowNetwork;
|
|
1195
|
+
const callAllowedDomains: string[] = [];
|
|
1196
|
+
const effectiveConfig = {
|
|
1197
|
+
...config,
|
|
1198
|
+
network: { ...config.network, allowedDomains: getEffectiveAllowedDomains(config) },
|
|
1199
|
+
};
|
|
1133
1200
|
|
|
1134
1201
|
if (!allowNetwork) {
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
`Sandbox: network access denied for "${blockedDomain.domain}" (${reason}).`,
|
|
1144
|
-
);
|
|
1202
|
+
for (const domain of extractDomainsFromCommand(args.command as string)) {
|
|
1203
|
+
const decision = evaluateDomainPermission(domain, effectiveConfig);
|
|
1204
|
+
if (decision.status === 'allow') continue;
|
|
1205
|
+
if (decision.status === 'ask' && hasCallAllowance(callID, decision)) {
|
|
1206
|
+
callAllowedDomains.push(domain);
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
throw errorWithConfigPaths(directory, decision.message);
|
|
1145
1210
|
}
|
|
1146
1211
|
}
|
|
1147
1212
|
|
|
1148
|
-
|
|
1213
|
+
if (callAllowedDomains.length > 0) {
|
|
1214
|
+
effectiveConfig.network = {
|
|
1215
|
+
...effectiveConfig.network,
|
|
1216
|
+
allowedDomains: [...effectiveConfig.network.allowedDomains, ...callAllowedDomains],
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const proxy = allowNetwork ? null : await startProxy(effectiveConfig);
|
|
1149
1221
|
const proxyPort = proxy ? proxy.port : null;
|
|
1150
1222
|
let policy: { dir: string; path: string };
|
|
1151
1223
|
|
|
1152
1224
|
try {
|
|
1153
|
-
policy = writePolicyFile(
|
|
1225
|
+
policy = writePolicyFile(effectiveConfig, directory, proxyPort);
|
|
1154
1226
|
} catch (error) {
|
|
1155
1227
|
if (proxy) await proxy.stop().catch(() => undefined);
|
|
1156
1228
|
throw error;
|
|
@@ -1181,6 +1253,80 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1181
1253
|
configuredShell = configuredShellPath(config);
|
|
1182
1254
|
},
|
|
1183
1255
|
|
|
1256
|
+
'permission.ask': async (input, output) => {
|
|
1257
|
+
const config = await activeConfig();
|
|
1258
|
+
if (!config) return;
|
|
1259
|
+
|
|
1260
|
+
const request = input as Record<string, unknown>;
|
|
1261
|
+
const permission =
|
|
1262
|
+
typeof request.type === 'string'
|
|
1263
|
+
? request.type
|
|
1264
|
+
: typeof request.permission === 'string'
|
|
1265
|
+
? request.permission
|
|
1266
|
+
: typeof request.action === 'string'
|
|
1267
|
+
? request.action
|
|
1268
|
+
: '';
|
|
1269
|
+
const metadata = isRecord(request.metadata) ? request.metadata : {};
|
|
1270
|
+
const tool = isRecord(request.tool) ? request.tool : undefined;
|
|
1271
|
+
const callID =
|
|
1272
|
+
typeof request.callID === 'string'
|
|
1273
|
+
? request.callID
|
|
1274
|
+
: typeof tool?.callID === 'string'
|
|
1275
|
+
? tool.callID
|
|
1276
|
+
: undefined;
|
|
1277
|
+
const patterns = Array.isArray(request.patterns)
|
|
1278
|
+
? request.patterns.filter((item): item is string => typeof item === 'string')
|
|
1279
|
+
: typeof request.pattern === 'string'
|
|
1280
|
+
? [request.pattern]
|
|
1281
|
+
: Array.isArray(request.resources)
|
|
1282
|
+
? request.resources.filter((item): item is string => typeof item === 'string')
|
|
1283
|
+
: [];
|
|
1284
|
+
|
|
1285
|
+
const decisions: SandboxPermissionDecision[] = [];
|
|
1286
|
+
const effectiveAllowRead = getEffectiveAllowRead(config);
|
|
1287
|
+
const effectiveAllowWrite = getEffectiveAllowWrite(config);
|
|
1288
|
+
|
|
1289
|
+
if (permission === 'read') {
|
|
1290
|
+
for (const pattern of patterns) {
|
|
1291
|
+
decisions.push(evaluateReadPermission(pattern, config, directory, effectiveAllowRead));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
if (permission === 'glob' || permission === 'grep' || permission === 'list') {
|
|
1296
|
+
const searchPath = typeof metadata.path === 'string' ? metadata.path : '.';
|
|
1297
|
+
decisions.push(evaluateReadPermission(searchPath, config, directory, effectiveAllowRead));
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (permission === 'edit') {
|
|
1301
|
+
const filepath =
|
|
1302
|
+
typeof metadata.filepath === 'string'
|
|
1303
|
+
? metadata.filepath
|
|
1304
|
+
: patterns.length === 1
|
|
1305
|
+
? patterns[0]
|
|
1306
|
+
: undefined;
|
|
1307
|
+
if (filepath) {
|
|
1308
|
+
decisions.push(evaluateWritePermission(filepath, config, directory, effectiveAllowWrite));
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (permission === 'bash') {
|
|
1313
|
+
const command = typeof metadata.command === 'string' ? metadata.command : patterns[0];
|
|
1314
|
+
if (typeof command === 'string' && !config.network.allowNetwork) {
|
|
1315
|
+
for (const domain of extractDomainsFromCommand(command)) {
|
|
1316
|
+
decisions.push(evaluateDomainPermission(domain, config));
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const decision =
|
|
1322
|
+
decisions.find((item) => item.status === 'deny') ??
|
|
1323
|
+
decisions.find((item) => item.status === 'ask');
|
|
1324
|
+
if (!decision) return;
|
|
1325
|
+
|
|
1326
|
+
output.status = decision.status;
|
|
1327
|
+
rememberCallAllowance(callID, decision);
|
|
1328
|
+
},
|
|
1329
|
+
|
|
1184
1330
|
'tool.execute.before': async (input, output) => {
|
|
1185
1331
|
if (!isRecord(output.args)) return;
|
|
1186
1332
|
|
|
@@ -1197,23 +1343,40 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1197
1343
|
|
|
1198
1344
|
if (input.tool === 'read') {
|
|
1199
1345
|
const path = getToolPath(output.args);
|
|
1200
|
-
if (path)
|
|
1346
|
+
if (path)
|
|
1347
|
+
enforcePermission(
|
|
1348
|
+
input.callID,
|
|
1349
|
+
evaluateReadPermission(path, config, directory, effectiveAllowRead),
|
|
1350
|
+
);
|
|
1201
1351
|
return;
|
|
1202
1352
|
}
|
|
1203
1353
|
|
|
1204
1354
|
if (input.tool === 'glob' || input.tool === 'grep' || input.tool === 'list') {
|
|
1205
|
-
|
|
1355
|
+
enforcePermission(
|
|
1356
|
+
input.callID,
|
|
1357
|
+
evaluateReadPermission(getSearchPath(output.args), config, directory, effectiveAllowRead),
|
|
1358
|
+
);
|
|
1206
1359
|
return;
|
|
1207
1360
|
}
|
|
1208
1361
|
|
|
1209
1362
|
if (input.tool === 'write' || input.tool === 'edit') {
|
|
1210
1363
|
const path = getToolPath(output.args);
|
|
1211
|
-
if (path)
|
|
1364
|
+
if (path)
|
|
1365
|
+
enforcePermission(
|
|
1366
|
+
input.callID,
|
|
1367
|
+
evaluateWritePermission(path, config, directory, effectiveAllowWrite),
|
|
1368
|
+
);
|
|
1212
1369
|
return;
|
|
1213
1370
|
}
|
|
1214
1371
|
|
|
1215
|
-
if (input.tool === 'apply_patch')
|
|
1216
|
-
|
|
1372
|
+
if (input.tool === 'apply_patch' && typeof output.args.patchText === 'string') {
|
|
1373
|
+
for (const path of extractPatchPaths(output.args.patchText)) {
|
|
1374
|
+
enforcePermission(
|
|
1375
|
+
input.callID,
|
|
1376
|
+
evaluateWritePermission(path, config, directory, effectiveAllowWrite),
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1217
1380
|
},
|
|
1218
1381
|
|
|
1219
1382
|
'shell.env': async (input, output) => {
|
|
@@ -1258,31 +1421,11 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1258
1421
|
|
|
1259
1422
|
const blockedPath = extractBlockedWritePath(outputText, directory, state.originalCommand);
|
|
1260
1423
|
if (blockedPath) {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
) {
|
|
1267
|
-
sessionAllowedWritePaths.push(blockedPath);
|
|
1268
|
-
await notifyOnce(
|
|
1269
|
-
`write-allow:${blockedPath}`,
|
|
1270
|
-
`Write access granted for session: "${blockedPath}"`,
|
|
1271
|
-
'warning',
|
|
1272
|
-
);
|
|
1273
|
-
}
|
|
1274
|
-
if (
|
|
1275
|
-
!sessionAllowedReadPaths.includes(blockedPath) &&
|
|
1276
|
-
!matchesPattern(blockedPath, config.filesystem.allowRead, directory) &&
|
|
1277
|
-
!matchesPattern(blockedPath, config.filesystem.denyRead, directory)
|
|
1278
|
-
) {
|
|
1279
|
-
sessionAllowedReadPaths.push(blockedPath);
|
|
1280
|
-
await notifyOnce(
|
|
1281
|
-
`read-allow:${blockedPath}`,
|
|
1282
|
-
`Read access granted for session: "${blockedPath}"`,
|
|
1283
|
-
'warning',
|
|
1284
|
-
);
|
|
1285
|
-
}
|
|
1424
|
+
await notifyOnce(
|
|
1425
|
+
`blocked:${blockedPath}`,
|
|
1426
|
+
`Sandbox blocked access to "${blockedPath}". Approve the related OpenCode permission prompt and retry if needed.`,
|
|
1427
|
+
'warning',
|
|
1428
|
+
);
|
|
1286
1429
|
}
|
|
1287
1430
|
|
|
1288
1431
|
await cleanupBash(input.callID);
|
|
@@ -1337,12 +1480,28 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1337
1480
|
const effectiveAllowWrite = getEffectiveAllowWrite(config);
|
|
1338
1481
|
|
|
1339
1482
|
for (const path of extractCandidatePaths(shellCommand)) {
|
|
1340
|
-
|
|
1341
|
-
|
|
1483
|
+
const readDecision = evaluateReadPermission(path, config, directory, effectiveAllowRead);
|
|
1484
|
+
if (readDecision.status === 'deny') {
|
|
1485
|
+
throw errorWithConfigPaths(directory, readDecision.message);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const writeDecision = evaluateWritePermission(
|
|
1489
|
+
path,
|
|
1490
|
+
config,
|
|
1491
|
+
directory,
|
|
1492
|
+
effectiveAllowWrite,
|
|
1493
|
+
);
|
|
1494
|
+
if (writeDecision.status === 'deny') {
|
|
1495
|
+
throw errorWithConfigPaths(directory, writeDecision.message);
|
|
1496
|
+
}
|
|
1342
1497
|
}
|
|
1343
1498
|
|
|
1344
1499
|
if (!config.network.allowNetwork) {
|
|
1345
|
-
const
|
|
1500
|
+
const effectiveConfig = {
|
|
1501
|
+
...config,
|
|
1502
|
+
network: { ...config.network, allowedDomains: getEffectiveAllowedDomains(config) },
|
|
1503
|
+
};
|
|
1504
|
+
const blockedDomain = firstBlockedDomain(shellCommand, effectiveConfig);
|
|
1346
1505
|
if (blockedDomain) {
|
|
1347
1506
|
const reason =
|
|
1348
1507
|
blockedDomain.reason === 'deniedDomains'
|
package/landstrip.d.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "Landlock-based sandboxing for opencode with landstrip",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landlock",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"files": [
|
|
17
17
|
"index.ts",
|
|
18
18
|
"tui.ts",
|
|
19
|
+
"landstrip.d.ts",
|
|
19
20
|
"README.md",
|
|
20
21
|
"sandbox.json"
|
|
21
22
|
],
|
|
@@ -50,17 +51,17 @@
|
|
|
50
51
|
"@jarkkojs/landstrip": "^0.11.0"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@opencode-ai/plugin": "^1.
|
|
54
|
-
"@opentui/core": ">=0.3.
|
|
55
|
-
"@opentui/keymap": ">=0.3.
|
|
56
|
-
"@opentui/solid": ">=0.3.
|
|
54
|
+
"@opencode-ai/plugin": "^1.17.3",
|
|
55
|
+
"@opentui/core": ">=0.3.4",
|
|
56
|
+
"@opentui/keymap": ">=0.3.4",
|
|
57
|
+
"@opentui/solid": ">=0.3.4",
|
|
57
58
|
"@types/node": "^24.0.0",
|
|
58
59
|
"oxfmt": "^0.53.0",
|
|
59
60
|
"oxlint": "^1.68.0",
|
|
60
61
|
"typescript": "^5.8.2"
|
|
61
62
|
},
|
|
62
63
|
"peerDependencies": {
|
|
63
|
-
"@opencode-ai/plugin": "^1.
|
|
64
|
+
"@opencode-ai/plugin": "^1.17.3"
|
|
64
65
|
},
|
|
65
66
|
"peerDependenciesMeta": {
|
|
66
67
|
"@opencode-ai/plugin": {
|
|
@@ -68,6 +69,6 @@
|
|
|
68
69
|
}
|
|
69
70
|
},
|
|
70
71
|
"engines": {
|
|
71
|
-
"opencode": ">=1.
|
|
72
|
+
"opencode": ">=1.17.3"
|
|
72
73
|
}
|
|
73
74
|
}
|