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.
- package/index.ts +57 -12
- 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,
|
|
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
|
-
|
|
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
|
|
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 {
|