opencode-landstrip 0.15.16 → 0.15.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 +34 -21
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -57,7 +57,7 @@ interface SandboxPermissionDecision {
|
|
|
57
57
|
|
|
58
58
|
type ToastVariant = 'info' | 'success' | 'warning' | 'error';
|
|
59
59
|
|
|
60
|
-
const LANDSTRIP_VERSION = [0, 15,
|
|
60
|
+
const LANDSTRIP_VERSION = [0, 15, 14] as const;
|
|
61
61
|
const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
|
|
62
62
|
const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
|
|
63
63
|
|
|
@@ -127,21 +127,37 @@ function globToRegExp(globPattern: string): RegExp {
|
|
|
127
127
|
return new RegExp(`^${regex}$`);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
// Component count of an absolute path; "/" is 0. Used to rank how specific a
|
|
131
|
+
// matching pattern is so the most specific allow/deny rule wins.
|
|
132
|
+
function pathDepth(absolutePath: string): number {
|
|
133
|
+
return absolutePath.split('/').filter((segment) => segment.length > 0).length;
|
|
134
|
+
}
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
// The depth of the most specific pattern that matches `filePath`, or -1 when
|
|
137
|
+
// none match. A glob is anchored to the whole path, so it ranks at the path's
|
|
138
|
+
// own depth; a literal pattern ranks at the depth of the prefix it covers.
|
|
139
|
+
function matchDepth(filePath: string, patterns: string[], baseDirectory: string): number {
|
|
140
|
+
const abs = canonicalizePath(filePath, baseDirectory);
|
|
141
|
+
let depth = -1;
|
|
137
142
|
|
|
143
|
+
for (const pattern of patterns) {
|
|
138
144
|
if (pattern.includes('*')) {
|
|
139
|
-
|
|
145
|
+
const absPattern = expandPath(pattern, baseDirectory);
|
|
146
|
+
if (globToRegExp(absPattern).test(abs)) depth = Math.max(depth, pathDepth(abs));
|
|
147
|
+
} else {
|
|
148
|
+
const absPattern = canonicalizePath(pattern, baseDirectory);
|
|
149
|
+
const sep = absPattern.endsWith('/') ? '' : '/';
|
|
150
|
+
if (abs === absPattern || abs.startsWith(absPattern + sep)) {
|
|
151
|
+
depth = Math.max(depth, pathDepth(absPattern));
|
|
152
|
+
}
|
|
140
153
|
}
|
|
154
|
+
}
|
|
141
155
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
156
|
+
return depth;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function matchesPattern(filePath: string, patterns: string[], baseDirectory: string): boolean {
|
|
160
|
+
return matchDepth(filePath, patterns, baseDirectory) >= 0;
|
|
145
161
|
}
|
|
146
162
|
|
|
147
163
|
function resolveFilesystemPatterns(patterns: string[], baseDirectory: string): string[] {
|
|
@@ -164,10 +180,6 @@ function resolveFilesystemConfig(
|
|
|
164
180
|
};
|
|
165
181
|
}
|
|
166
182
|
|
|
167
|
-
function shouldPromptForRead(path: string, allowRead: string[], baseDirectory: string): boolean {
|
|
168
|
-
return allowRead.length === 0 || !matchesPattern(path, allowRead, baseDirectory);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
183
|
function shouldPromptForWrite(path: string, allowWrite: string[], baseDirectory: string): boolean {
|
|
172
184
|
return allowWrite.length === 0 || !matchesPattern(path, allowWrite, baseDirectory);
|
|
173
185
|
}
|
|
@@ -267,10 +279,6 @@ function extractBlockedWritePath(
|
|
|
267
279
|
return extractBlockedPath(output, baseDirectory, command);
|
|
268
280
|
}
|
|
269
281
|
|
|
270
|
-
function isBlockedByDenyRead(path: string, config: SandboxConfig, baseDirectory: string): boolean {
|
|
271
|
-
return matchesPattern(path, config.filesystem.denyRead, baseDirectory);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
282
|
function evaluateReadPermission(
|
|
275
283
|
path: string,
|
|
276
284
|
config: SandboxConfig,
|
|
@@ -278,8 +286,13 @@ function evaluateReadPermission(
|
|
|
278
286
|
effectiveAllowRead: string[],
|
|
279
287
|
): SandboxPermissionDecision {
|
|
280
288
|
const filePath = canonicalizePath(path, baseDirectory);
|
|
289
|
+
const allowDepth = matchDepth(filePath, effectiveAllowRead, baseDirectory);
|
|
290
|
+
const denyDepth = matchDepth(filePath, config.filesystem.denyRead, baseDirectory);
|
|
281
291
|
|
|
282
|
-
|
|
292
|
+
// The most specific rule wins, matching landstrip's read policy so the bash
|
|
293
|
+
// and read tools agree: a denyRead overrides allowRead only when it is more
|
|
294
|
+
// specific, while a tie or a more specific allowRead carves the path back in.
|
|
295
|
+
if (denyDepth > allowDepth) {
|
|
283
296
|
return {
|
|
284
297
|
status: 'deny',
|
|
285
298
|
kind: 'read',
|
|
@@ -288,7 +301,7 @@ function evaluateReadPermission(
|
|
|
288
301
|
};
|
|
289
302
|
}
|
|
290
303
|
|
|
291
|
-
if (
|
|
304
|
+
if (allowDepth >= 0) {
|
|
292
305
|
return { status: 'allow', kind: 'read', resource: filePath, message: '' };
|
|
293
306
|
}
|
|
294
307
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.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.15.
|
|
52
|
+
"@landstrip/landstrip": "^0.15.14"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@opencode-ai/plugin": "^1.17.7",
|