opencode-landstrip 0.18.41 → 0.18.42
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 +21 -49
- package/package.json +2 -2
- package/tui.ts +10 -24
package/index.ts
CHANGED
|
@@ -74,11 +74,6 @@ const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
|
|
|
74
74
|
const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
|
|
75
75
|
const DISCOVERY_CONNECT_TIMEOUT_MS = 250;
|
|
76
76
|
|
|
77
|
-
function configuredShellPath(config: unknown): string | undefined {
|
|
78
|
-
if (!isRecord(config)) return undefined;
|
|
79
|
-
return typeof config.shell === 'string' ? config.shell : undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
77
|
function normalizePathForMatch(filePath: string): string {
|
|
83
78
|
return process.platform === 'win32' ? normalizePathSeparators(filePath).toLowerCase() : filePath;
|
|
84
79
|
}
|
|
@@ -310,12 +305,6 @@ function evaluateCommandDomains(
|
|
|
310
305
|
);
|
|
311
306
|
}
|
|
312
307
|
|
|
313
|
-
function landstripVersion(): string | null {
|
|
314
|
-
const result = spawnSync(landstripBinaryPath(), ['--version'], { encoding: 'utf-8' });
|
|
315
|
-
if (result.status !== 0) return null;
|
|
316
|
-
return result.stdout.trim();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
308
|
function parseVersion(version: string): [number, number, number] | null {
|
|
320
309
|
const match = version.match(/\b(\d+)\.(\d+)\.(\d+)\b/);
|
|
321
310
|
if (!match) return null;
|
|
@@ -492,12 +481,6 @@ function trapPortAcceptsConnections(port: number): Promise<boolean> {
|
|
|
492
481
|
});
|
|
493
482
|
}
|
|
494
483
|
|
|
495
|
-
async function readLiveDiscoveryPort(baseDirectory: string): Promise<number | null> {
|
|
496
|
-
const port = readDiscoveryPort(baseDirectory);
|
|
497
|
-
if (port === null) return null;
|
|
498
|
-
return (await trapPortAcceptsConnections(port)) ? port : null;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
484
|
function buildWrappedCommand(
|
|
502
485
|
policyPath: string,
|
|
503
486
|
shell: string,
|
|
@@ -542,10 +525,6 @@ function isGeneratedWrappedCommand(command: string): boolean {
|
|
|
542
525
|
);
|
|
543
526
|
}
|
|
544
527
|
|
|
545
|
-
function landstripDescription(description: string): string {
|
|
546
|
-
return description.endsWith(' (landstrip)') ? description : `${description} (landstrip)`;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
528
|
function splitShellQuotedArgs(command: string): string[] {
|
|
550
529
|
const args: string[] = [];
|
|
551
530
|
let i = 0;
|
|
@@ -597,10 +576,6 @@ function getToolPath(args: Record<string, unknown>): string | undefined {
|
|
|
597
576
|
return typeof filePath === 'string' ? filePath : undefined;
|
|
598
577
|
}
|
|
599
578
|
|
|
600
|
-
function getSearchPath(args: Record<string, unknown>): string {
|
|
601
|
-
return typeof args.path === 'string' ? args.path : '.';
|
|
602
|
-
}
|
|
603
|
-
|
|
604
579
|
function extractPatchPaths(patchText: string): string[] {
|
|
605
580
|
const paths: string[] = [];
|
|
606
581
|
|
|
@@ -636,7 +611,8 @@ function evaluateToolPermissions(
|
|
|
636
611
|
}
|
|
637
612
|
|
|
638
613
|
if (tool === 'glob' || tool === 'grep' || tool === 'list') {
|
|
639
|
-
|
|
614
|
+
const searchPath = typeof args.path === 'string' ? args.path : '.';
|
|
615
|
+
return [evaluateReadPermission(searchPath, config, baseDirectory, effectiveAllowRead)];
|
|
640
616
|
}
|
|
641
617
|
|
|
642
618
|
if (tool === 'write' || tool === 'edit') {
|
|
@@ -679,16 +655,6 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
679
655
|
return `${callID}:${kind}:${resource}`;
|
|
680
656
|
}
|
|
681
657
|
|
|
682
|
-
function rememberCallAllowances(
|
|
683
|
-
callID: string | undefined,
|
|
684
|
-
approvals: readonly SandboxPermissionDecision[],
|
|
685
|
-
): void {
|
|
686
|
-
if (!callID) return;
|
|
687
|
-
for (const approval of approvals) {
|
|
688
|
-
callAllowances.add(allowanceKey(callID, approval.kind, approval.resource));
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
658
|
function clearCallAllowances(callID: string): void {
|
|
693
659
|
for (const key of callAllowances) {
|
|
694
660
|
if (key.startsWith(`${callID}:`)) callAllowances.delete(key);
|
|
@@ -712,11 +678,6 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
712
678
|
throw errorWithConfigPaths(directory, decision.message);
|
|
713
679
|
}
|
|
714
680
|
|
|
715
|
-
function enforcePermission(callID: string, decision: SandboxPermissionDecision): void {
|
|
716
|
-
if (decision.status === 'allow' || hasCallAllowance(callID, decision)) return;
|
|
717
|
-
reportBlocked(decision);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
681
|
client.app
|
|
721
682
|
?.log?.({
|
|
722
683
|
body: {
|
|
@@ -787,7 +748,8 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
787
748
|
|
|
788
749
|
let version: string | null;
|
|
789
750
|
try {
|
|
790
|
-
|
|
751
|
+
const result = spawnSync(landstripBinaryPath(), ['--version'], { encoding: 'utf-8' });
|
|
752
|
+
version = result.status === 0 ? result.stdout.trim() : null;
|
|
791
753
|
} catch (error) {
|
|
792
754
|
landstripCheck = {
|
|
793
755
|
ok: false,
|
|
@@ -889,8 +851,9 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
889
851
|
const normalizedSessionID = sessionID?.trim() || undefined;
|
|
890
852
|
|
|
891
853
|
const rewriteDescription = (): void => {
|
|
892
|
-
if (typeof args.description === 'string')
|
|
893
|
-
args.description =
|
|
854
|
+
if (typeof args.description === 'string' && !args.description.endsWith(' (landstrip)')) {
|
|
855
|
+
args.description = `${args.description} (landstrip)`;
|
|
856
|
+
}
|
|
894
857
|
};
|
|
895
858
|
|
|
896
859
|
const existing = activeBash.get(callID);
|
|
@@ -965,9 +928,11 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
965
928
|
// The TUI owns interactive query handling. Fall back to an in-process
|
|
966
929
|
// broker when no TUI endpoint or session identity is available.
|
|
967
930
|
const interactiveSessionID = normalizedSessionID ?? '';
|
|
931
|
+
const discoveredPort =
|
|
932
|
+
process.platform === 'linux' && interactiveSessionID ? readDiscoveryPort(directory) : null;
|
|
968
933
|
const tuiTrapPort =
|
|
969
|
-
|
|
970
|
-
?
|
|
934
|
+
discoveredPort !== null && (await trapPortAcceptsConnections(discoveredPort))
|
|
935
|
+
? discoveredPort
|
|
971
936
|
: null;
|
|
972
937
|
const trapServer =
|
|
973
938
|
tuiTrapPort === null
|
|
@@ -1008,7 +973,9 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1008
973
|
|
|
1009
974
|
const hooks: Hooks = {
|
|
1010
975
|
config: async (config) => {
|
|
1011
|
-
|
|
976
|
+
const value: unknown = config;
|
|
977
|
+
configuredShell =
|
|
978
|
+
isRecord(value) && typeof value.shell === 'string' ? value.shell : undefined;
|
|
1012
979
|
},
|
|
1013
980
|
|
|
1014
981
|
'permission.ask': async (input, output) => {
|
|
@@ -1057,7 +1024,11 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1057
1024
|
if (approvals.length === 0) return;
|
|
1058
1025
|
|
|
1059
1026
|
output.status = 'ask';
|
|
1060
|
-
|
|
1027
|
+
if (callID) {
|
|
1028
|
+
for (const approval of approvals) {
|
|
1029
|
+
callAllowances.add(allowanceKey(callID, approval.kind, approval.resource));
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1061
1032
|
},
|
|
1062
1033
|
|
|
1063
1034
|
'tool.execute.before': async (input, output) => {
|
|
@@ -1080,7 +1051,8 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1080
1051
|
config.filesystem.allowWrite,
|
|
1081
1052
|
);
|
|
1082
1053
|
for (const decision of decisions) {
|
|
1083
|
-
|
|
1054
|
+
if (decision.status === 'allow' || hasCallAllowance(input.callID, decision)) continue;
|
|
1055
|
+
reportBlocked(decision);
|
|
1084
1056
|
}
|
|
1085
1057
|
},
|
|
1086
1058
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.42",
|
|
4
4
|
"description": "OS-level AI command sandboxing for OpenCode with Landstrip",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landlock",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"ci:test": "npm test"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@landstrip/landstrip-api": "^0.18.
|
|
51
|
+
"@landstrip/landstrip-api": "^0.18.42",
|
|
52
52
|
"@opentui/core": ">=0.3.4",
|
|
53
53
|
"@opentui/solid": ">=0.3.4",
|
|
54
54
|
"solid-js": "1.9.12"
|
package/tui.ts
CHANGED
|
@@ -118,11 +118,6 @@ function formatPath(input: string, base: string): string {
|
|
|
118
118
|
return absolute;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
function selectedForeground(background: RGBA): RGBA {
|
|
122
|
-
const luminance = 0.2126 * background.r + 0.7152 * background.g + 0.0722 * background.b;
|
|
123
|
-
return luminance > 0.5 ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
121
|
const tui: TuiPlugin = async (api, options, meta) => {
|
|
127
122
|
const optionOverrides = normalizeOptions(options);
|
|
128
123
|
const enabledManagedByOptions = optionOverrides.enabled !== undefined;
|
|
@@ -176,7 +171,10 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
176
171
|
},
|
|
177
172
|
children: jsx('text', {
|
|
178
173
|
get fg() {
|
|
179
|
-
|
|
174
|
+
if (selected() !== index) return theme.textMuted;
|
|
175
|
+
const background = theme.warning;
|
|
176
|
+
const luminance = 0.2126 * background.r + 0.7152 * background.g + 0.0722 * background.b;
|
|
177
|
+
return luminance > 0.5 ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255);
|
|
180
178
|
},
|
|
181
179
|
children: option.label,
|
|
182
180
|
}),
|
|
@@ -333,14 +331,11 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
333
331
|
);
|
|
334
332
|
}
|
|
335
333
|
|
|
336
|
-
function currentRouteSessionID(): string | undefined {
|
|
337
|
-
const route = api.route.current;
|
|
338
|
-
if (route.name !== 'session' || !route.params) return undefined;
|
|
339
|
-
return typeof route.params.sessionID === 'string' ? route.params.sessionID : undefined;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
334
|
function pump(): void {
|
|
343
|
-
const
|
|
335
|
+
const route = api.route.current;
|
|
336
|
+
if (route.name !== 'session' || !route.params) return;
|
|
337
|
+
const routeSessionID =
|
|
338
|
+
typeof route.params.sessionID === 'string' ? route.params.sessionID : undefined;
|
|
344
339
|
if (!routeSessionID) return;
|
|
345
340
|
|
|
346
341
|
const active = activeEntry();
|
|
@@ -360,8 +355,8 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
360
355
|
if (index === -1) return;
|
|
361
356
|
const [next] = queue.splice(index, 1);
|
|
362
357
|
if (!next) return;
|
|
363
|
-
|
|
364
|
-
|
|
358
|
+
activeId = next.id;
|
|
359
|
+
setActiveEntry(next);
|
|
365
360
|
}
|
|
366
361
|
|
|
367
362
|
function enqueueEntry(entry: QueueEntry): void {
|
|
@@ -505,10 +500,6 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
505
500
|
}
|
|
506
501
|
}
|
|
507
502
|
|
|
508
|
-
function showFsQuery(entry: FsQueryEntry): void {
|
|
509
|
-
activeId = entry.id;
|
|
510
|
-
setActiveEntry(entry);
|
|
511
|
-
}
|
|
512
503
|
function resolveNetworkQuery(entry: NetworkQueryEntry, choice: NetworkQueryChoice): void {
|
|
513
504
|
if (resolved.has(entry.id)) return;
|
|
514
505
|
const action = choice === 'deny' ? 'deny' : 'allow';
|
|
@@ -533,11 +524,6 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
533
524
|
}
|
|
534
525
|
}
|
|
535
526
|
|
|
536
|
-
function showNetworkQuery(entry: NetworkQueryEntry): void {
|
|
537
|
-
activeId = entry.id;
|
|
538
|
-
setActiveEntry(entry);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
527
|
// Query-response socket server (Linux-only — landstrip's socket protocol lives
|
|
542
528
|
// in the seccomp broker). The server plugin connects each sandboxed run's
|
|
543
529
|
// fd 3 here via a /dev/tcp redirect and we answer held writes interactively.
|