opencode-landstrip 0.15.16 → 0.15.18

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.
Files changed (2) hide show
  1. package/index.ts +34 -21
  2. 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, 9] as const;
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
- function matchesPattern(filePath: string, patterns: string[], baseDirectory: string): boolean {
131
- const abs = canonicalizePath(filePath, baseDirectory);
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
- return patterns.some((pattern) => {
134
- const absPattern = pattern.includes('*')
135
- ? expandPath(pattern, baseDirectory)
136
- : canonicalizePath(pattern, baseDirectory);
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
- return globToRegExp(absPattern).test(abs);
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
- const sep = absPattern.endsWith('/') ? '' : '/';
143
- return abs === absPattern || abs.startsWith(absPattern + sep);
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
- if (isBlockedByDenyRead(filePath, config, baseDirectory)) {
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 (!shouldPromptForRead(filePath, effectiveAllowRead, baseDirectory)) {
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.16",
3
+ "version": "0.15.18",
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.9"
52
+ "@landstrip/landstrip": "^0.15.15"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@opencode-ai/plugin": "^1.17.7",