opencode-landstrip 0.16.15 → 0.16.17
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 +107 -18
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
parseLandstripTraps,
|
|
25
25
|
permissionPatterns,
|
|
26
26
|
permissionType,
|
|
27
|
-
readDiscoveryPort,
|
|
28
27
|
sandboxSummary,
|
|
29
28
|
sessionScopeFor,
|
|
30
29
|
} from './shared.js';
|
|
@@ -46,6 +45,9 @@ interface BashSandboxState {
|
|
|
46
45
|
policyDir: string;
|
|
47
46
|
port: number | null;
|
|
48
47
|
stop: (() => Promise<void>) | null;
|
|
48
|
+
trapServer: ReturnType<typeof createServer> | null;
|
|
49
|
+
trapServerPort: number | null;
|
|
50
|
+
trapLines: string[];
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
type SandboxPermissionKind = 'read' | 'write' | 'domain';
|
|
@@ -633,20 +635,83 @@ function shellArgs(shell: string, command: string): string[] {
|
|
|
633
635
|
return [shell, '-lc', command];
|
|
634
636
|
}
|
|
635
637
|
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
638
|
+
// Start a local TCP server that landstrip connects its trap fd to. Traps are
|
|
639
|
+
// handled in-process: query traps are answered immediately against the active
|
|
640
|
+
// config, and info traps are collected for post-execution error reporting.
|
|
641
|
+
function startTrapServer(
|
|
642
|
+
effectiveAllowRead: string[],
|
|
643
|
+
effectiveAllowWrite: string[],
|
|
644
|
+
denyRead: string[],
|
|
645
|
+
denyWrite: string[],
|
|
646
|
+
baseDirectory: string,
|
|
647
|
+
sessionAllowedReadPaths: Set<string>,
|
|
648
|
+
sessionAllowedWritePaths: Set<string>,
|
|
649
|
+
): Promise<{ server: ReturnType<typeof createServer>; port: number; trapLines: string[] }> {
|
|
650
|
+
const trapLines: string[] = [];
|
|
651
|
+
const server = createServer((trapSocket) => {
|
|
652
|
+
let buffer = '';
|
|
653
|
+
trapSocket.on('data', (data: Buffer) => {
|
|
654
|
+
buffer += data.toString('utf8');
|
|
655
|
+
let nl = buffer.indexOf('\n');
|
|
656
|
+
while (nl !== -1) {
|
|
657
|
+
const line = buffer.slice(0, nl);
|
|
658
|
+
buffer = buffer.slice(nl + 1);
|
|
659
|
+
nl = buffer.indexOf('\n');
|
|
660
|
+
if (line.length === 0) continue;
|
|
661
|
+
let obj: Record<string, unknown> | null = null;
|
|
662
|
+
try {
|
|
663
|
+
const parsed: unknown = JSON.parse(line);
|
|
664
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
665
|
+
obj = parsed as Record<string, unknown>;
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
obj = null;
|
|
669
|
+
}
|
|
670
|
+
if (
|
|
671
|
+
obj &&
|
|
672
|
+
obj.state === 'query' &&
|
|
673
|
+
typeof obj.query_id === 'number' &&
|
|
674
|
+
(obj.operation === 'read' || obj.operation === 'write') &&
|
|
675
|
+
typeof obj.path === 'string'
|
|
676
|
+
) {
|
|
677
|
+
const path = canonicalizePath(obj.path, baseDirectory);
|
|
678
|
+
const operation = obj.operation as 'read' | 'write';
|
|
679
|
+
// Per landstrip policy: a deny rule overrides allow only when more
|
|
680
|
+
// specific; a tie or more-specific allow carves the path back in.
|
|
681
|
+
const denyReadDepth = matchDepth(path, denyRead, baseDirectory);
|
|
682
|
+
const allowReadDepth = matchDepth(path, effectiveAllowRead, baseDirectory);
|
|
683
|
+
const denyWriteDepth = matchDepth(path, denyWrite, baseDirectory);
|
|
684
|
+
const allowWriteDepth = matchDepth(path, effectiveAllowWrite, baseDirectory);
|
|
685
|
+
const allowed =
|
|
686
|
+
operation === 'read'
|
|
687
|
+
? !(denyReadDepth > allowReadDepth) && allowReadDepth >= 0
|
|
688
|
+
: !(denyWriteDepth > allowWriteDepth) && allowWriteDepth >= 0;
|
|
689
|
+
if (allowed) {
|
|
690
|
+
trapSocket.write(JSON.stringify({ query_id: obj.query_id, action: 'allow' }) + '\n');
|
|
691
|
+
} else {
|
|
692
|
+
// Auto-grant via session scope and allow so the command proceeds.
|
|
693
|
+
const scope = sessionScopeFor(path, baseDirectory);
|
|
694
|
+
if (operation === 'read') sessionAllowedReadPaths.add(scope);
|
|
695
|
+
else sessionAllowedWritePaths.add(scope);
|
|
696
|
+
trapSocket.write(JSON.stringify({ query_id: obj.query_id, action: 'allow' }) + '\n');
|
|
697
|
+
trapLines.push(line);
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
trapLines.push(line);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
trapSocket.on('error', () => {});
|
|
705
|
+
});
|
|
642
706
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
707
|
+
return new Promise((resolve, reject) => {
|
|
708
|
+
server.on('error', reject);
|
|
709
|
+
server.listen(0, '127.0.0.1', () => {
|
|
710
|
+
server.removeListener('error', reject);
|
|
711
|
+
const address = server.address() as AddressInfo;
|
|
712
|
+
resolve({ server, port: address.port, trapLines });
|
|
713
|
+
});
|
|
714
|
+
});
|
|
650
715
|
}
|
|
651
716
|
|
|
652
717
|
function buildWrappedCommand(
|
|
@@ -1001,6 +1066,11 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1001
1066
|
|
|
1002
1067
|
activeBash.delete(callID);
|
|
1003
1068
|
if (state.stop) await state.stop().catch(() => undefined);
|
|
1069
|
+
if (state.trapServer) {
|
|
1070
|
+
await new Promise<void>((resolve) => {
|
|
1071
|
+
state.trapServer!.close(() => resolve());
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1004
1074
|
rmSync(state.policyDir, { recursive: true, force: true });
|
|
1005
1075
|
}
|
|
1006
1076
|
|
|
@@ -1083,11 +1153,24 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1083
1153
|
}
|
|
1084
1154
|
|
|
1085
1155
|
const originalCommand = args.command as string;
|
|
1156
|
+
|
|
1157
|
+
// Start a local trap server so landstrip's query traps are answered
|
|
1158
|
+
// in-process instead of relying on the TUI plugin's discovery port.
|
|
1159
|
+
const trapServer = await startTrapServer(
|
|
1160
|
+
effectiveConfig.filesystem.allowRead,
|
|
1161
|
+
effectiveConfig.filesystem.allowWrite,
|
|
1162
|
+
effectiveConfig.filesystem.denyRead,
|
|
1163
|
+
effectiveConfig.filesystem.denyWrite,
|
|
1164
|
+
directory,
|
|
1165
|
+
sessionAllowedReadPaths,
|
|
1166
|
+
sessionAllowedWritePaths,
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1086
1169
|
const wrappedCommand = buildWrappedCommand(
|
|
1087
1170
|
policy.path,
|
|
1088
1171
|
configuredShell ?? process.env.SHELL ?? '/bin/sh',
|
|
1089
1172
|
originalCommand,
|
|
1090
|
-
|
|
1173
|
+
trapServer.port,
|
|
1091
1174
|
);
|
|
1092
1175
|
|
|
1093
1176
|
activeBash.set(callID, {
|
|
@@ -1096,6 +1179,9 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1096
1179
|
policyDir: policy.dir,
|
|
1097
1180
|
port: proxyPort,
|
|
1098
1181
|
stop: proxy ? proxy.stop : null,
|
|
1182
|
+
trapServer: trapServer.server,
|
|
1183
|
+
trapServerPort: trapServer.port,
|
|
1184
|
+
trapLines: trapServer.trapLines,
|
|
1099
1185
|
});
|
|
1100
1186
|
|
|
1101
1187
|
args.command = wrappedCommand;
|
|
@@ -1239,9 +1325,12 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1239
1325
|
}
|
|
1240
1326
|
|
|
1241
1327
|
const outputText = output?.output ?? '';
|
|
1242
|
-
// Query traps were already resolved
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1328
|
+
// Query traps were already resolved in-process by the local trap server;
|
|
1329
|
+
// only terminal (info) traps and trap-server-collected lines belong in
|
|
1330
|
+
// the after-the-fact toast.
|
|
1331
|
+
const serverTrapOutput = state.trapLines.join('\n');
|
|
1332
|
+
const combinedOutput = serverTrapOutput ? outputText + '\n' + serverTrapOutput : outputText;
|
|
1333
|
+
const errors = parseLandstripTraps(combinedOutput).filter(
|
|
1245
1334
|
(trap: LandstripTrap) => !(trap.kind === 'filesystem' && trap.state === 'query'),
|
|
1246
1335
|
);
|
|
1247
1336
|
if (errors.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.17",
|
|
4
4
|
"description": "Landlock-based sandboxing for opencode with landstrip",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landlock",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"ci:test": "npm test"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@landstrip/landstrip": "^0.16.
|
|
52
|
+
"@landstrip/landstrip": "^0.16.13"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@opencode-ai/plugin": "^1.17.7",
|