opencode-landstrip 0.18.41 → 0.18.43
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 +50 -63
- package/package.json +2 -2
- package/shared.ts +4 -86
- package/tui.ts +14 -24
package/index.ts
CHANGED
|
@@ -74,17 +74,12 @@ 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
|
}
|
|
85
80
|
|
|
86
|
-
// Component count of an absolute path; "/" is 0.
|
|
87
|
-
// matching
|
|
81
|
+
// Component count of an absolute path; "/" is 0. Read rules use it to rank
|
|
82
|
+
// matching allow and deny patterns by specificity.
|
|
88
83
|
function pathDepth(absolutePath: string): number {
|
|
89
84
|
return absolutePath.split('/').filter((segment) => segment.length > 0).length;
|
|
90
85
|
}
|
|
@@ -140,7 +135,7 @@ function isDomainAllowed(domain: string, config: SandboxConfig): boolean {
|
|
|
140
135
|
);
|
|
141
136
|
}
|
|
142
137
|
|
|
143
|
-
function
|
|
138
|
+
function isReadAllowed(
|
|
144
139
|
path: string,
|
|
145
140
|
allowPatterns: string[],
|
|
146
141
|
denyPatterns: string[],
|
|
@@ -151,6 +146,16 @@ function isFilesystemAllowed(
|
|
|
151
146
|
return allowDepth >= 0 && allowDepth >= denyDepth;
|
|
152
147
|
}
|
|
153
148
|
|
|
149
|
+
function writeAccess(
|
|
150
|
+
path: string,
|
|
151
|
+
allowPatterns: string[],
|
|
152
|
+
denyPatterns: string[],
|
|
153
|
+
baseDirectory: string,
|
|
154
|
+
): 'allow' | 'deny' | 'unlisted' {
|
|
155
|
+
if (matchDepth(path, denyPatterns, baseDirectory) >= 0) return 'deny';
|
|
156
|
+
return matchDepth(path, allowPatterns, baseDirectory) >= 0 ? 'allow' : 'unlisted';
|
|
157
|
+
}
|
|
158
|
+
|
|
154
159
|
function extractCandidatePaths(command: string): string[] {
|
|
155
160
|
const paths: string[] = [];
|
|
156
161
|
const tokens = command.match(/[^\s"']+|"[^"]*"|'[^']*'/g) ?? [];
|
|
@@ -224,9 +229,7 @@ function evaluateReadPermission(
|
|
|
224
229
|
// everything else asks for approval (allow once/session/persist or reject)
|
|
225
230
|
// rather than being blocked outright. denyRead still hard-applies to bash
|
|
226
231
|
// through the landstrip binary policy, which has no way to prompt.
|
|
227
|
-
if (
|
|
228
|
-
isFilesystemAllowed(filePath, effectiveAllowRead, config.filesystem.denyRead, baseDirectory)
|
|
229
|
-
) {
|
|
232
|
+
if (isReadAllowed(filePath, effectiveAllowRead, config.filesystem.denyRead, baseDirectory)) {
|
|
230
233
|
return { status: 'allow', kind: 'read', resource: filePath, message: '' };
|
|
231
234
|
}
|
|
232
235
|
|
|
@@ -245,10 +248,14 @@ function evaluateWritePermission(
|
|
|
245
248
|
effectiveAllowWrite: string[],
|
|
246
249
|
): SandboxPermissionDecision {
|
|
247
250
|
const filePath = canonicalizePath(path, baseDirectory);
|
|
248
|
-
const
|
|
249
|
-
|
|
251
|
+
const access = writeAccess(
|
|
252
|
+
filePath,
|
|
253
|
+
effectiveAllowWrite,
|
|
254
|
+
config.filesystem.denyWrite,
|
|
255
|
+
baseDirectory,
|
|
256
|
+
);
|
|
250
257
|
|
|
251
|
-
if (
|
|
258
|
+
if (access === 'deny') {
|
|
252
259
|
return {
|
|
253
260
|
status: 'deny',
|
|
254
261
|
kind: 'write',
|
|
@@ -257,9 +264,7 @@ function evaluateWritePermission(
|
|
|
257
264
|
};
|
|
258
265
|
}
|
|
259
266
|
|
|
260
|
-
if (
|
|
261
|
-
isFilesystemAllowed(filePath, effectiveAllowWrite, config.filesystem.denyWrite, baseDirectory)
|
|
262
|
-
) {
|
|
267
|
+
if (access === 'allow') {
|
|
263
268
|
return { status: 'allow', kind: 'write', resource: filePath, message: '' };
|
|
264
269
|
}
|
|
265
270
|
|
|
@@ -310,12 +315,6 @@ function evaluateCommandDomains(
|
|
|
310
315
|
);
|
|
311
316
|
}
|
|
312
317
|
|
|
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
318
|
function parseVersion(version: string): [number, number, number] | null {
|
|
320
319
|
const match = version.match(/\b(\d+)\.(\d+)\.(\d+)\b/);
|
|
321
320
|
if (!match) return null;
|
|
@@ -438,8 +437,8 @@ function startTrapServer(
|
|
|
438
437
|
const operation = trap.operation;
|
|
439
438
|
const allowed =
|
|
440
439
|
operation === 'read'
|
|
441
|
-
?
|
|
442
|
-
:
|
|
440
|
+
? isReadAllowed(path, effectiveAllowRead, denyRead, baseDirectory)
|
|
441
|
+
: writeAccess(path, effectiveAllowWrite, denyWrite, baseDirectory) === 'allow';
|
|
443
442
|
if (allowed) {
|
|
444
443
|
trapSocket.write(controlResponseLine(queryId, 'allow'));
|
|
445
444
|
} else {
|
|
@@ -492,12 +491,6 @@ function trapPortAcceptsConnections(port: number): Promise<boolean> {
|
|
|
492
491
|
});
|
|
493
492
|
}
|
|
494
493
|
|
|
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
494
|
function buildWrappedCommand(
|
|
502
495
|
policyPath: string,
|
|
503
496
|
shell: string,
|
|
@@ -542,10 +535,6 @@ function isGeneratedWrappedCommand(command: string): boolean {
|
|
|
542
535
|
);
|
|
543
536
|
}
|
|
544
537
|
|
|
545
|
-
function landstripDescription(description: string): string {
|
|
546
|
-
return description.endsWith(' (landstrip)') ? description : `${description} (landstrip)`;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
538
|
function splitShellQuotedArgs(command: string): string[] {
|
|
550
539
|
const args: string[] = [];
|
|
551
540
|
let i = 0;
|
|
@@ -597,10 +586,6 @@ function getToolPath(args: Record<string, unknown>): string | undefined {
|
|
|
597
586
|
return typeof filePath === 'string' ? filePath : undefined;
|
|
598
587
|
}
|
|
599
588
|
|
|
600
|
-
function getSearchPath(args: Record<string, unknown>): string {
|
|
601
|
-
return typeof args.path === 'string' ? args.path : '.';
|
|
602
|
-
}
|
|
603
|
-
|
|
604
589
|
function extractPatchPaths(patchText: string): string[] {
|
|
605
590
|
const paths: string[] = [];
|
|
606
591
|
|
|
@@ -636,7 +621,8 @@ function evaluateToolPermissions(
|
|
|
636
621
|
}
|
|
637
622
|
|
|
638
623
|
if (tool === 'glob' || tool === 'grep' || tool === 'list') {
|
|
639
|
-
|
|
624
|
+
const searchPath = typeof args.path === 'string' ? args.path : '.';
|
|
625
|
+
return [evaluateReadPermission(searchPath, config, baseDirectory, effectiveAllowRead)];
|
|
640
626
|
}
|
|
641
627
|
|
|
642
628
|
if (tool === 'write' || tool === 'edit') {
|
|
@@ -679,16 +665,6 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
679
665
|
return `${callID}:${kind}:${resource}`;
|
|
680
666
|
}
|
|
681
667
|
|
|
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
668
|
function clearCallAllowances(callID: string): void {
|
|
693
669
|
for (const key of callAllowances) {
|
|
694
670
|
if (key.startsWith(`${callID}:`)) callAllowances.delete(key);
|
|
@@ -712,11 +688,6 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
712
688
|
throw errorWithConfigPaths(directory, decision.message);
|
|
713
689
|
}
|
|
714
690
|
|
|
715
|
-
function enforcePermission(callID: string, decision: SandboxPermissionDecision): void {
|
|
716
|
-
if (decision.status === 'allow' || hasCallAllowance(callID, decision)) return;
|
|
717
|
-
reportBlocked(decision);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
691
|
client.app
|
|
721
692
|
?.log?.({
|
|
722
693
|
body: {
|
|
@@ -787,7 +758,8 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
787
758
|
|
|
788
759
|
let version: string | null;
|
|
789
760
|
try {
|
|
790
|
-
|
|
761
|
+
const result = spawnSync(landstripBinaryPath(), ['--version'], { encoding: 'utf-8' });
|
|
762
|
+
version = result.status === 0 ? result.stdout.trim() : null;
|
|
791
763
|
} catch (error) {
|
|
792
764
|
landstripCheck = {
|
|
793
765
|
ok: false,
|
|
@@ -889,8 +861,9 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
889
861
|
const normalizedSessionID = sessionID?.trim() || undefined;
|
|
890
862
|
|
|
891
863
|
const rewriteDescription = (): void => {
|
|
892
|
-
if (typeof args.description === 'string')
|
|
893
|
-
args.description =
|
|
864
|
+
if (typeof args.description === 'string' && !args.description.endsWith(' (landstrip)')) {
|
|
865
|
+
args.description = `${args.description} (landstrip)`;
|
|
866
|
+
}
|
|
894
867
|
};
|
|
895
868
|
|
|
896
869
|
const existing = activeBash.get(callID);
|
|
@@ -965,9 +938,11 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
965
938
|
// The TUI owns interactive query handling. Fall back to an in-process
|
|
966
939
|
// broker when no TUI endpoint or session identity is available.
|
|
967
940
|
const interactiveSessionID = normalizedSessionID ?? '';
|
|
941
|
+
const discoveredPort =
|
|
942
|
+
process.platform === 'linux' && interactiveSessionID ? readDiscoveryPort(directory) : null;
|
|
968
943
|
const tuiTrapPort =
|
|
969
|
-
|
|
970
|
-
?
|
|
944
|
+
discoveredPort !== null && (await trapPortAcceptsConnections(discoveredPort))
|
|
945
|
+
? discoveredPort
|
|
971
946
|
: null;
|
|
972
947
|
const trapServer =
|
|
973
948
|
tuiTrapPort === null
|
|
@@ -1008,7 +983,9 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1008
983
|
|
|
1009
984
|
const hooks: Hooks = {
|
|
1010
985
|
config: async (config) => {
|
|
1011
|
-
|
|
986
|
+
const value: unknown = config;
|
|
987
|
+
configuredShell =
|
|
988
|
+
isRecord(value) && typeof value.shell === 'string' ? value.shell : undefined;
|
|
1012
989
|
},
|
|
1013
990
|
|
|
1014
991
|
'permission.ask': async (input, output) => {
|
|
@@ -1057,7 +1034,11 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1057
1034
|
if (approvals.length === 0) return;
|
|
1058
1035
|
|
|
1059
1036
|
output.status = 'ask';
|
|
1060
|
-
|
|
1037
|
+
if (callID) {
|
|
1038
|
+
for (const approval of approvals) {
|
|
1039
|
+
callAllowances.add(allowanceKey(callID, approval.kind, approval.resource));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1061
1042
|
},
|
|
1062
1043
|
|
|
1063
1044
|
'tool.execute.before': async (input, output) => {
|
|
@@ -1080,7 +1061,13 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1080
1061
|
config.filesystem.allowWrite,
|
|
1081
1062
|
);
|
|
1082
1063
|
for (const decision of decisions) {
|
|
1083
|
-
|
|
1064
|
+
if (
|
|
1065
|
+
decision.status === 'allow' ||
|
|
1066
|
+
(decision.status === 'ask' && hasCallAllowance(input.callID, decision))
|
|
1067
|
+
) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
reportBlocked(decision);
|
|
1084
1071
|
}
|
|
1085
1072
|
},
|
|
1086
1073
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.43",
|
|
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.43",
|
|
52
52
|
"@opentui/core": ">=0.3.4",
|
|
53
53
|
"@opentui/solid": ">=0.3.4",
|
|
54
54
|
"solid-js": "1.9.12"
|
package/shared.ts
CHANGED
|
@@ -1,33 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// Copyright (C) Jarkko Sakkinen 2026
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
type LandstripControlResponse,
|
|
7
|
-
type LandstripTrap,
|
|
8
|
-
} from '@landstrip/landstrip-api';
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
allowsAllDomains,
|
|
12
|
-
canonicalizeGlobPattern,
|
|
13
|
-
canonicalizePath,
|
|
14
|
-
controlResponseLine,
|
|
15
|
-
decodeLandstripTrap,
|
|
16
|
-
domainMatchesAny,
|
|
17
|
-
domainMatchesPattern,
|
|
18
|
-
expandHomePath,
|
|
19
|
-
expandPath,
|
|
20
|
-
globToRegExp,
|
|
21
|
-
formatLandstripTrap,
|
|
22
|
-
formatLandstripTraps,
|
|
23
|
-
isRecord,
|
|
24
|
-
normalizePathSeparators,
|
|
25
|
-
parseLandstripTraps,
|
|
26
|
-
pathUnderDirectory,
|
|
27
|
-
sessionAllows,
|
|
28
|
-
sessionScopeFor,
|
|
29
|
-
} from '@landstrip/landstrip-api/shared';
|
|
4
|
+
import { binaryPath } from '@landstrip/landstrip-api';
|
|
5
|
+
import { isRecord } from '@landstrip/landstrip-api/shared';
|
|
30
6
|
|
|
7
|
+
export type { LandstripControlResponse, LandstripTrap } from '@landstrip/landstrip-api';
|
|
31
8
|
export {
|
|
32
9
|
allowsAllDomains,
|
|
33
10
|
canonicalizeGlobPattern,
|
|
@@ -47,11 +24,7 @@ export {
|
|
|
47
24
|
pathUnderDirectory,
|
|
48
25
|
sessionAllows,
|
|
49
26
|
sessionScopeFor,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// Re-exported so index.ts/tui.ts can import the trap/response types from this
|
|
53
|
-
// module alongside the parsing functions that produce/consume them.
|
|
54
|
-
export type { LandstripControlResponse, LandstripTrap };
|
|
27
|
+
} from '@landstrip/landstrip-api/shared';
|
|
55
28
|
|
|
56
29
|
import { createHash } from 'node:crypto';
|
|
57
30
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -99,10 +72,6 @@ const LANDSTRIP_PACKAGE_NAMES = new Set([
|
|
|
99
72
|
'@landstrip/landstrip-win32-arm64',
|
|
100
73
|
]);
|
|
101
74
|
|
|
102
|
-
export function list(values: string[]): string {
|
|
103
|
-
return values.join(', ') || '(none)';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
75
|
function rejectUnknownFields(
|
|
107
76
|
value: Record<string, unknown>,
|
|
108
77
|
fields: readonly string[],
|
|
@@ -601,54 +570,3 @@ export function readDiscoveryPort(baseDirectory: string): number | null {
|
|
|
601
570
|
return null;
|
|
602
571
|
}
|
|
603
572
|
}
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Human-readable sandbox configuration report consumed by both the server
|
|
607
|
-
* command and the TUI inspector dialog.
|
|
608
|
-
*/
|
|
609
|
-
export function sandboxSummary(
|
|
610
|
-
config: SandboxConfig,
|
|
611
|
-
globalPath: string,
|
|
612
|
-
projectPath: string,
|
|
613
|
-
statusOverride?: string,
|
|
614
|
-
): string {
|
|
615
|
-
const networkMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
|
|
616
|
-
const allowed = list(config.network.allowedDomains);
|
|
617
|
-
const denied = list(config.network.deniedDomains);
|
|
618
|
-
const unixSockets = config.network.allowAllUnixSockets
|
|
619
|
-
? 'all'
|
|
620
|
-
: list(config.network.allowUnixSockets);
|
|
621
|
-
const denyRead = list(config.filesystem.denyRead);
|
|
622
|
-
const allowRead = list(config.filesystem.allowRead);
|
|
623
|
-
const allowWrite = list(config.filesystem.allowWrite);
|
|
624
|
-
const denyWrite = list(config.filesystem.denyWrite);
|
|
625
|
-
|
|
626
|
-
const status = statusOverride ?? (config.enabled ? 'active' : 'disabled by config');
|
|
627
|
-
let binary: string;
|
|
628
|
-
try {
|
|
629
|
-
binary = landstripBinaryPath();
|
|
630
|
-
} catch {
|
|
631
|
-
binary = '(unavailable)';
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return [
|
|
635
|
-
`Status: ${status}`,
|
|
636
|
-
`landstrip package binary: ${binary}`,
|
|
637
|
-
'',
|
|
638
|
-
'Config files',
|
|
639
|
-
`${projectPath} ${existsSync(projectPath) ? '(found)' : '(missing)'}`,
|
|
640
|
-
`${globalPath} ${existsSync(globalPath) ? '(found)' : '(missing)'}`,
|
|
641
|
-
'',
|
|
642
|
-
`Network: ${networkMode}`,
|
|
643
|
-
`allow network: ${config.network.allowNetwork ? 'yes' : 'no'}`,
|
|
644
|
-
`allowed: ${allowed}`,
|
|
645
|
-
`denied: ${denied}`,
|
|
646
|
-
`unix sockets: ${unixSockets}`,
|
|
647
|
-
'',
|
|
648
|
-
'Filesystem',
|
|
649
|
-
`deny read: ${denyRead}`,
|
|
650
|
-
`allow read: ${allowRead}`,
|
|
651
|
-
`allow write: ${allowWrite}`,
|
|
652
|
-
`deny write: ${denyWrite}`,
|
|
653
|
-
].join('\n');
|
|
654
|
-
}
|
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.
|
|
@@ -599,6 +585,10 @@ const tui: TuiPlugin = async (api, options, meta) => {
|
|
|
599
585
|
}
|
|
600
586
|
|
|
601
587
|
if (trap.kind === 'filesystem') {
|
|
588
|
+
if (trap.operation === 'write' && trap.reason === 'deny_match') {
|
|
589
|
+
respondQuery(socket, trap.query_id, 'deny');
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
602
592
|
const sessionPaths =
|
|
603
593
|
trap.operation === 'read' ? allowances.readPaths : allowances.writePaths;
|
|
604
594
|
if (sessionAllows(sessionPaths, trap.path)) {
|