opencode-landstrip 0.14.0 → 0.14.1

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 +57 -12
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -58,7 +58,7 @@ interface SandboxPermissionDecision {
58
58
 
59
59
  type ToastVariant = 'info' | 'success' | 'warning' | 'error';
60
60
 
61
- const LANDSTRIP_VERSION = [0, 14, 0] as const;
61
+ const LANDSTRIP_VERSION = [0, 14, 5] as const;
62
62
  const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
63
63
  const LANDSTRIP_OPERATIONS = new Set<'read' | 'write'>(['read', 'write']);
64
64
  const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
@@ -101,6 +101,38 @@ function canonicalizePath(filePath: string, baseDirectory: string): string {
101
101
  }
102
102
  }
103
103
 
104
+ /**
105
+ * Translates an absolute glob pattern to a regular expression using standard
106
+ * path semantics: `**` crosses directory boundaries (and `**​/` may match zero
107
+ * segments), while a single `*` is confined to one path segment.
108
+ */
109
+ function globToRegExp(globPattern: string): RegExp {
110
+ let regex = '';
111
+
112
+ for (let i = 0; i < globPattern.length; i++) {
113
+ const char = globPattern[i];
114
+ if (char === '*') {
115
+ if (globPattern[i + 1] === '*') {
116
+ i++;
117
+ if (globPattern[i + 1] === '/') {
118
+ i++;
119
+ regex += '(?:.*/)?';
120
+ } else {
121
+ regex += '.*';
122
+ }
123
+ } else {
124
+ regex += '[^/]*';
125
+ }
126
+ } else if (/[.+^${}()|[\]\\]/.test(char)) {
127
+ regex += `\\${char}`;
128
+ } else {
129
+ regex += char;
130
+ }
131
+ }
132
+
133
+ return new RegExp(`^${regex}$`);
134
+ }
135
+
104
136
  function matchesPattern(filePath: string, patterns: string[], baseDirectory: string): boolean {
105
137
  const abs = canonicalizePath(filePath, baseDirectory);
106
138
 
@@ -110,8 +142,7 @@ function matchesPattern(filePath: string, patterns: string[], baseDirectory: str
110
142
  : canonicalizePath(pattern, baseDirectory);
111
143
 
112
144
  if (pattern.includes('*')) {
113
- const escaped = absPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
114
- return new RegExp(`^${escaped}$`).test(abs);
145
+ return globToRegExp(absPattern).test(abs);
115
146
  }
116
147
 
117
148
  const sep = absPattern.endsWith('/') ? '' : '/';
@@ -271,10 +302,6 @@ function evaluateReadPermission(
271
302
  ): SandboxPermissionDecision {
272
303
  const filePath = canonicalizePath(path, baseDirectory);
273
304
 
274
- if (!shouldPromptForRead(filePath, effectiveAllowRead, baseDirectory)) {
275
- return { status: 'allow', kind: 'read', resource: filePath, message: '' };
276
- }
277
-
278
305
  if (isBlockedByDenyRead(filePath, config, baseDirectory)) {
279
306
  return {
280
307
  status: 'deny',
@@ -284,6 +311,10 @@ function evaluateReadPermission(
284
311
  };
285
312
  }
286
313
 
314
+ if (!shouldPromptForRead(filePath, effectiveAllowRead, baseDirectory)) {
315
+ return { status: 'allow', kind: 'read', resource: filePath, message: '' };
316
+ }
317
+
287
318
  return {
288
319
  status: 'ask',
289
320
  kind: 'read',
@@ -300,10 +331,6 @@ function evaluateWritePermission(
300
331
  ): SandboxPermissionDecision {
301
332
  const filePath = canonicalizePath(path, baseDirectory);
302
333
 
303
- if (!shouldPromptForWrite(filePath, effectiveAllowWrite, baseDirectory)) {
304
- return { status: 'allow', kind: 'write', resource: filePath, message: '' };
305
- }
306
-
307
334
  if (matchesPattern(filePath, config.filesystem.denyWrite, baseDirectory)) {
308
335
  return {
309
336
  status: 'deny',
@@ -313,6 +340,10 @@ function evaluateWritePermission(
313
340
  };
314
341
  }
315
342
 
343
+ if (!shouldPromptForWrite(filePath, effectiveAllowWrite, baseDirectory)) {
344
+ return { status: 'allow', kind: 'write', resource: filePath, message: '' };
345
+ }
346
+
316
347
  return {
317
348
  status: 'ask',
318
349
  kind: 'write',
@@ -325,7 +356,7 @@ function evaluateDomainPermission(
325
356
  domain: string,
326
357
  config: SandboxConfig,
327
358
  ): SandboxPermissionDecision {
328
- if (config.network.allowNetwork || domainMatchesAny(domain, config.network.allowedDomains)) {
359
+ if (config.network.allowNetwork) {
329
360
  return { status: 'allow', kind: 'domain', resource: domain, message: '' };
330
361
  }
331
362
 
@@ -338,6 +369,10 @@ function evaluateDomainPermission(
338
369
  };
339
370
  }
340
371
 
372
+ if (domainMatchesAny(domain, config.network.allowedDomains)) {
373
+ return { status: 'allow', kind: 'domain', resource: domain, message: '' };
374
+ }
375
+
341
376
  return {
342
377
  status: 'ask',
343
378
  kind: 'domain',
@@ -547,10 +582,15 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
547
582
  return;
548
583
  }
549
584
 
585
+ let connected = false;
550
586
  const upstream = connectNet(endpoint.port, endpoint.host, () => {
587
+ connected = true;
551
588
  client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
552
589
  pipeSockets(client, upstream, rest);
553
590
  });
591
+ upstream.on('error', () => {
592
+ if (!connected) denyProxyRequest(client, '502 Bad Gateway');
593
+ });
554
594
  }
555
595
 
556
596
  async function handleHttp(client: Socket, headerText: string, rest: Buffer): Promise<void> {
@@ -589,10 +629,15 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
589
629
  const rewrittenHeader = lines
590
630
  .filter((line) => !line.toLowerCase().startsWith('proxy-connection:'))
591
631
  .join('\r\n');
632
+ let connected = false;
592
633
  const upstream = connectNet(port, url.hostname, () => {
634
+ connected = true;
593
635
  upstream.write(`${rewrittenHeader}\r\n\r\n`);
594
636
  pipeSockets(client, upstream, rest);
595
637
  });
638
+ upstream.on('error', () => {
639
+ if (!connected) denyProxyRequest(client, '502 Bad Gateway');
640
+ });
596
641
  }
597
642
 
598
643
  function handleClient(client: Socket): void {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-landstrip",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Landlock-based sandboxing for opencode with landstrip",
5
5
  "keywords": [
6
6
  "landlock",