opencode-landstrip 0.15.14 → 0.15.16
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 +25 -106
- package/package.json +1 -1
- package/sandbox.json +1 -1
- package/shared.ts +23 -1
package/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
loadConfig,
|
|
23
23
|
normalizeOptions,
|
|
24
24
|
parseLandstripTraps,
|
|
25
|
+
permissionPatterns,
|
|
26
|
+
permissionType,
|
|
25
27
|
readDiscoveryPort,
|
|
26
28
|
} from './shared.js';
|
|
27
29
|
|
|
@@ -269,23 +271,6 @@ function isBlockedByDenyRead(path: string, config: SandboxConfig, baseDirectory:
|
|
|
269
271
|
return matchesPattern(path, config.filesystem.denyRead, baseDirectory);
|
|
270
272
|
}
|
|
271
273
|
|
|
272
|
-
function firstBlockedDomain(
|
|
273
|
-
command: string,
|
|
274
|
-
config: SandboxConfig,
|
|
275
|
-
): { domain: string; reason: 'allowedDomains' | 'deniedDomains' } | null {
|
|
276
|
-
for (const domain of extractDomainsFromCommand(command)) {
|
|
277
|
-
if (domainMatchesAny(domain, config.network.deniedDomains)) {
|
|
278
|
-
return { domain, reason: 'deniedDomains' };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (!domainMatchesAny(domain, config.network.allowedDomains)) {
|
|
282
|
-
return { domain, reason: 'allowedDomains' };
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return null;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
274
|
function evaluateReadPermission(
|
|
290
275
|
path: string,
|
|
291
276
|
config: SandboxConfig,
|
|
@@ -754,27 +739,12 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
754
739
|
const optionOverrides = normalizeOptions(options);
|
|
755
740
|
const activeBash = new Map<string, BashSandboxState>();
|
|
756
741
|
const notified = new Set<string>();
|
|
757
|
-
const sessionAllowedReadPaths: string[] = [];
|
|
758
|
-
const sessionAllowedWritePaths: string[] = [];
|
|
759
|
-
const sessionAllowedDomains: string[] = [];
|
|
760
742
|
const callAllowances = new Set<string>();
|
|
761
743
|
let enabledNotified = false;
|
|
762
744
|
let sandboxDisabled = false;
|
|
763
745
|
let configuredShell: string | undefined;
|
|
764
746
|
let landstripCheck: { ok: true; version: string } | { ok: false; reason: string } | undefined;
|
|
765
747
|
|
|
766
|
-
function getEffectiveAllowRead(config: SandboxConfig): string[] {
|
|
767
|
-
return [...config.filesystem.allowRead, ...sessionAllowedReadPaths];
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function getEffectiveAllowWrite(config: SandboxConfig): string[] {
|
|
771
|
-
return [...config.filesystem.allowWrite, ...sessionAllowedWritePaths];
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
function getEffectiveAllowedDomains(config: SandboxConfig): string[] {
|
|
775
|
-
return [...config.network.allowedDomains, ...sessionAllowedDomains];
|
|
776
|
-
}
|
|
777
|
-
|
|
778
748
|
function allowanceKey(callID: string, kind: SandboxPermissionKind, resource: string): string {
|
|
779
749
|
return `${callID}:${kind}:${resource}`;
|
|
780
750
|
}
|
|
@@ -791,8 +761,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
791
761
|
return callAllowances.has(allowanceKey(callID, decision.kind, decision.resource));
|
|
792
762
|
}
|
|
793
763
|
|
|
794
|
-
function
|
|
795
|
-
if (decision.status === 'allow' || hasCallAllowance(callID, decision)) return;
|
|
764
|
+
function reportBlocked(decision: SandboxPermissionDecision): never {
|
|
796
765
|
client.tui
|
|
797
766
|
?.showToast?.({
|
|
798
767
|
body: {
|
|
@@ -805,6 +774,11 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
805
774
|
throw errorWithConfigPaths(directory, decision.message);
|
|
806
775
|
}
|
|
807
776
|
|
|
777
|
+
function enforcePermission(callID: string, decision: SandboxPermissionDecision): void {
|
|
778
|
+
if (decision.status === 'allow' || hasCallAllowance(callID, decision)) return;
|
|
779
|
+
reportBlocked(decision);
|
|
780
|
+
}
|
|
781
|
+
|
|
808
782
|
function pushCommandText(
|
|
809
783
|
input: { sessionID: string },
|
|
810
784
|
output: { parts: unknown[] },
|
|
@@ -822,11 +796,11 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
822
796
|
function sandboxSummary(config: SandboxConfig): string {
|
|
823
797
|
const { globalPath, projectPath } = getConfigPaths(directory);
|
|
824
798
|
const networkMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
|
|
825
|
-
const allowed =
|
|
799
|
+
const allowed = config.network.allowedDomains.join(', ') || '(none)';
|
|
826
800
|
const denied = config.network.deniedDomains.join(', ') || '(none)';
|
|
827
801
|
const denyRead = config.filesystem.denyRead.join(', ') || '(none)';
|
|
828
|
-
const allowRead =
|
|
829
|
-
const allowWrite =
|
|
802
|
+
const allowRead = config.filesystem.allowRead.join(', ') || '(none)';
|
|
803
|
+
const allowWrite = config.filesystem.allowWrite.join(', ') || '(none)';
|
|
830
804
|
const denyWrite = config.filesystem.denyWrite.join(', ') || '(none)';
|
|
831
805
|
|
|
832
806
|
return [
|
|
@@ -1050,7 +1024,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1050
1024
|
const callAllowedDomains: string[] = [];
|
|
1051
1025
|
const effectiveConfig = {
|
|
1052
1026
|
...config,
|
|
1053
|
-
network: { ...config.network
|
|
1027
|
+
network: { ...config.network },
|
|
1054
1028
|
};
|
|
1055
1029
|
|
|
1056
1030
|
if (!allowNetwork) {
|
|
@@ -1114,14 +1088,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1114
1088
|
if (!config) return;
|
|
1115
1089
|
|
|
1116
1090
|
const request = input as Record<string, unknown>;
|
|
1117
|
-
const permission =
|
|
1118
|
-
typeof request.type === 'string'
|
|
1119
|
-
? request.type
|
|
1120
|
-
: typeof request.permission === 'string'
|
|
1121
|
-
? request.permission
|
|
1122
|
-
: typeof request.action === 'string'
|
|
1123
|
-
? request.action
|
|
1124
|
-
: '';
|
|
1091
|
+
const permission = permissionType(request);
|
|
1125
1092
|
const metadata = isRecord(request.metadata) ? request.metadata : {};
|
|
1126
1093
|
const tool = isRecord(request.tool) ? request.tool : undefined;
|
|
1127
1094
|
const callID =
|
|
@@ -1130,17 +1097,11 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1130
1097
|
: typeof tool?.callID === 'string'
|
|
1131
1098
|
? tool.callID
|
|
1132
1099
|
: undefined;
|
|
1133
|
-
const patterns =
|
|
1134
|
-
? request.patterns.filter((item): item is string => typeof item === 'string')
|
|
1135
|
-
: typeof request.pattern === 'string'
|
|
1136
|
-
? [request.pattern]
|
|
1137
|
-
: Array.isArray(request.resources)
|
|
1138
|
-
? request.resources.filter((item): item is string => typeof item === 'string')
|
|
1139
|
-
: [];
|
|
1100
|
+
const patterns = permissionPatterns(request);
|
|
1140
1101
|
|
|
1141
1102
|
const decisions: SandboxPermissionDecision[] = [];
|
|
1142
|
-
const effectiveAllowRead =
|
|
1143
|
-
const effectiveAllowWrite =
|
|
1103
|
+
const effectiveAllowRead = config.filesystem.allowRead;
|
|
1104
|
+
const effectiveAllowWrite = config.filesystem.allowWrite;
|
|
1144
1105
|
|
|
1145
1106
|
if (permission === 'read') {
|
|
1146
1107
|
for (const pattern of patterns) {
|
|
@@ -1189,8 +1150,8 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1189
1150
|
const config = await activeConfig();
|
|
1190
1151
|
if (!config) return;
|
|
1191
1152
|
|
|
1192
|
-
const effectiveAllowRead =
|
|
1193
|
-
const effectiveAllowWrite =
|
|
1153
|
+
const effectiveAllowRead = config.filesystem.allowRead;
|
|
1154
|
+
const effectiveAllowWrite = config.filesystem.allowWrite;
|
|
1194
1155
|
|
|
1195
1156
|
if (input.tool === 'bash') {
|
|
1196
1157
|
await prepareBash(input.callID, output.args, config);
|
|
@@ -1373,23 +1334,12 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1373
1334
|
const config = await activeConfig();
|
|
1374
1335
|
if (!config) return;
|
|
1375
1336
|
|
|
1376
|
-
const effectiveAllowRead =
|
|
1377
|
-
const effectiveAllowWrite =
|
|
1337
|
+
const effectiveAllowRead = config.filesystem.allowRead;
|
|
1338
|
+
const effectiveAllowWrite = config.filesystem.allowWrite;
|
|
1378
1339
|
|
|
1379
1340
|
for (const path of extractCandidatePaths(shellCommand)) {
|
|
1380
1341
|
const readDecision = evaluateReadPermission(path, config, directory, effectiveAllowRead);
|
|
1381
|
-
if (readDecision.status === 'deny')
|
|
1382
|
-
client.tui
|
|
1383
|
-
?.showToast?.({
|
|
1384
|
-
body: {
|
|
1385
|
-
title: 'Sandbox blocked',
|
|
1386
|
-
message: readDecision.message.slice(0, 120),
|
|
1387
|
-
variant: 'error',
|
|
1388
|
-
},
|
|
1389
|
-
})
|
|
1390
|
-
?.catch?.(() => undefined);
|
|
1391
|
-
throw errorWithConfigPaths(directory, readDecision.message);
|
|
1392
|
-
}
|
|
1342
|
+
if (readDecision.status === 'deny') reportBlocked(readDecision);
|
|
1393
1343
|
|
|
1394
1344
|
const writeDecision = evaluateWritePermission(
|
|
1395
1345
|
path,
|
|
@@ -1397,44 +1347,13 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1397
1347
|
directory,
|
|
1398
1348
|
effectiveAllowWrite,
|
|
1399
1349
|
);
|
|
1400
|
-
if (writeDecision.status === 'deny')
|
|
1401
|
-
client.tui
|
|
1402
|
-
?.showToast?.({
|
|
1403
|
-
body: {
|
|
1404
|
-
title: 'Sandbox blocked',
|
|
1405
|
-
message: writeDecision.message.slice(0, 120),
|
|
1406
|
-
variant: 'error',
|
|
1407
|
-
},
|
|
1408
|
-
})
|
|
1409
|
-
?.catch?.(() => undefined);
|
|
1410
|
-
throw errorWithConfigPaths(directory, writeDecision.message);
|
|
1411
|
-
}
|
|
1350
|
+
if (writeDecision.status === 'deny') reportBlocked(writeDecision);
|
|
1412
1351
|
}
|
|
1413
1352
|
|
|
1414
1353
|
if (!config.network.allowNetwork) {
|
|
1415
|
-
const
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
};
|
|
1419
|
-
const blockedDomain = firstBlockedDomain(shellCommand, effectiveConfig);
|
|
1420
|
-
if (blockedDomain) {
|
|
1421
|
-
const reason =
|
|
1422
|
-
blockedDomain.reason === 'deniedDomains'
|
|
1423
|
-
? 'is blocked by network.deniedDomains'
|
|
1424
|
-
: 'is not in network.allowedDomains';
|
|
1425
|
-
client.tui
|
|
1426
|
-
?.showToast?.({
|
|
1427
|
-
body: {
|
|
1428
|
-
title: 'Sandbox blocked',
|
|
1429
|
-
message: `Network access denied for "${blockedDomain.domain}"`,
|
|
1430
|
-
variant: 'error',
|
|
1431
|
-
},
|
|
1432
|
-
})
|
|
1433
|
-
?.catch?.(() => undefined);
|
|
1434
|
-
throw errorWithConfigPaths(
|
|
1435
|
-
directory,
|
|
1436
|
-
`Sandbox: network access denied for "${blockedDomain.domain}" (${reason}).`,
|
|
1437
|
-
);
|
|
1354
|
+
for (const domain of extractDomainsFromCommand(shellCommand)) {
|
|
1355
|
+
const decision = evaluateDomainPermission(domain, config);
|
|
1356
|
+
if (decision.status !== 'allow') reportBlocked(decision);
|
|
1438
1357
|
}
|
|
1439
1358
|
}
|
|
1440
1359
|
}
|
package/package.json
CHANGED
package/sandbox.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"filesystem": {
|
|
12
12
|
"denyRead": ["/Users", "/home"],
|
|
13
|
-
"allowRead": [".", "~/.gitconfig", "/dev/null"],
|
|
13
|
+
"allowRead": [".", "~/.gitconfig", "~/.config/git/config", "/dev/null"],
|
|
14
14
|
"allowWrite": [".", "/dev/null"],
|
|
15
15
|
"denyWrite": ["**/.env", "**/.env.*", "**/*.pem", "**/*.key"]
|
|
16
16
|
}
|
package/shared.ts
CHANGED
|
@@ -48,7 +48,7 @@ export const DEFAULT_CONFIG: SandboxConfig = {
|
|
|
48
48
|
},
|
|
49
49
|
filesystem: {
|
|
50
50
|
denyRead: ['/Users', '/home'],
|
|
51
|
-
allowRead: ['.', '~/.gitconfig', '/dev/null'],
|
|
51
|
+
allowRead: ['.', '~/.gitconfig', '~/.config/git/config', '/dev/null'],
|
|
52
52
|
allowWrite: ['.', '/dev/null'],
|
|
53
53
|
denyWrite: ['**/.env', '**/.env.*', '**/*.pem', '**/*.key'],
|
|
54
54
|
},
|
|
@@ -267,6 +267,28 @@ export function permissionPattern(permission: Record<string, unknown>): string |
|
|
|
267
267
|
return undefined;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Every string pattern a permission carries, in declaration order, read from
|
|
272
|
+
* `patterns`, `pattern`, or `resources`. The plural complement to
|
|
273
|
+
* {@link permissionPattern} for callers that must inspect all patterns.
|
|
274
|
+
*/
|
|
275
|
+
export function permissionPatterns(permission: Record<string, unknown>): string[] {
|
|
276
|
+
const patterns = permission.patterns;
|
|
277
|
+
if (Array.isArray(patterns))
|
|
278
|
+
return patterns.filter((item): item is string => typeof item === 'string');
|
|
279
|
+
|
|
280
|
+
const pattern = permission.pattern;
|
|
281
|
+
if (typeof pattern === 'string') return [pattern];
|
|
282
|
+
if (Array.isArray(pattern))
|
|
283
|
+
return pattern.filter((item): item is string => typeof item === 'string');
|
|
284
|
+
|
|
285
|
+
const resources = permission.resources;
|
|
286
|
+
if (Array.isArray(resources))
|
|
287
|
+
return resources.filter((item): item is string => typeof item === 'string');
|
|
288
|
+
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
|
|
270
292
|
export function permissionLabel(permission: Record<string, unknown>): string {
|
|
271
293
|
const type = permissionType(permission, 'permission');
|
|
272
294
|
const title = typeof permission.title === 'string' ? permission.title : type;
|