btca-server 1.0.962 → 2.0.0
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/package.json +3 -3
- package/src/agent/agent.test.ts +31 -24
- package/src/agent/index.ts +8 -2
- package/src/agent/loop.ts +303 -346
- package/src/agent/service.ts +252 -233
- package/src/agent/types.ts +2 -2
- package/src/collections/index.ts +2 -1
- package/src/collections/service.ts +352 -345
- package/src/config/config.test.ts +3 -1
- package/src/config/index.ts +615 -727
- package/src/config/remote.ts +214 -369
- package/src/context/index.ts +6 -12
- package/src/context/transaction.ts +23 -30
- package/src/effect/errors.ts +45 -0
- package/src/effect/layers.ts +26 -0
- package/src/effect/runtime.ts +19 -0
- package/src/effect/services.ts +154 -0
- package/src/index.ts +291 -369
- package/src/metrics/index.ts +46 -46
- package/src/pricing/models-dev.ts +104 -106
- package/src/providers/auth.ts +159 -200
- package/src/providers/index.ts +19 -2
- package/src/providers/model.ts +115 -135
- package/src/providers/openai.ts +3 -3
- package/src/resources/impls/git.ts +123 -146
- package/src/resources/impls/npm.test.ts +16 -5
- package/src/resources/impls/npm.ts +66 -75
- package/src/resources/index.ts +6 -1
- package/src/resources/schema.ts +7 -6
- package/src/resources/service.test.ts +13 -12
- package/src/resources/service.ts +153 -112
- package/src/stream/index.ts +1 -1
- package/src/stream/service.test.ts +5 -5
- package/src/stream/service.ts +282 -293
- package/src/tools/glob.ts +126 -141
- package/src/tools/grep.ts +205 -210
- package/src/tools/index.ts +8 -4
- package/src/tools/list.ts +118 -140
- package/src/tools/read.ts +209 -235
- package/src/tools/virtual-sandbox.ts +91 -83
- package/src/validation/index.ts +18 -22
- package/src/vfs/virtual-fs.test.ts +37 -25
- package/src/vfs/virtual-fs.ts +218 -216
package/src/tools/read.ts
CHANGED
|
@@ -4,266 +4,240 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { Result } from 'better-result';
|
|
8
7
|
|
|
9
8
|
import type { ToolContext } from './context.ts';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
isPdf?: boolean;
|
|
42
|
-
isBinary?: boolean;
|
|
43
|
-
};
|
|
44
|
-
// For images/PDFs, we return attachments
|
|
45
|
-
attachments?: Array<{
|
|
46
|
-
type: 'file';
|
|
47
|
-
mime: string;
|
|
48
|
-
data: string; // base64
|
|
49
|
-
}>;
|
|
9
|
+
import { resolveSandboxPathWithSymlinks } from './virtual-sandbox.ts';
|
|
10
|
+
import {
|
|
11
|
+
existsInVirtualFs,
|
|
12
|
+
readVirtualFsFile,
|
|
13
|
+
readVirtualFsFileBuffer,
|
|
14
|
+
readdirVirtualFs
|
|
15
|
+
} from '../vfs/virtual-fs.ts';
|
|
16
|
+
|
|
17
|
+
const MAX_LINES = 2000;
|
|
18
|
+
const MAX_BYTES = 50 * 1024;
|
|
19
|
+
const MAX_LINE_LENGTH = 2000;
|
|
20
|
+
|
|
21
|
+
export const ReadToolParameters = z.object({
|
|
22
|
+
path: z.string().describe('The absolute path to the file to read'),
|
|
23
|
+
offset: z.coerce.number().optional().describe('The line number to start reading from (0-based)'),
|
|
24
|
+
limit: z.coerce.number().optional().describe('The number of lines to read (defaults to 2000)')
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type ReadToolParametersType = z.infer<typeof ReadToolParameters>;
|
|
28
|
+
|
|
29
|
+
export type ReadToolResult = {
|
|
30
|
+
title: string;
|
|
31
|
+
output: string;
|
|
32
|
+
metadata: {
|
|
33
|
+
lines: number;
|
|
34
|
+
truncated: boolean;
|
|
35
|
+
truncatedByLines?: boolean;
|
|
36
|
+
truncatedByBytes?: boolean;
|
|
37
|
+
isImage?: boolean;
|
|
38
|
+
isPdf?: boolean;
|
|
39
|
+
isBinary?: boolean;
|
|
50
40
|
};
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return false;
|
|
41
|
+
attachments?: Array<{
|
|
42
|
+
type: 'file';
|
|
43
|
+
mime: string;
|
|
44
|
+
data: string;
|
|
45
|
+
}>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
49
|
+
'.png',
|
|
50
|
+
'.jpg',
|
|
51
|
+
'.jpeg',
|
|
52
|
+
'.gif',
|
|
53
|
+
'.webp',
|
|
54
|
+
'.bmp',
|
|
55
|
+
'.ico',
|
|
56
|
+
'.svg'
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const PDF_EXTENSIONS = new Set(['.pdf']);
|
|
60
|
+
|
|
61
|
+
const isBinaryBuffer = (bytes: Uint8Array): boolean => {
|
|
62
|
+
for (const byte of bytes) {
|
|
63
|
+
if (byte === 0) return true;
|
|
75
64
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
ok: (entries) => entries.map((entry) => entry.name),
|
|
97
|
-
err: () => []
|
|
98
|
-
});
|
|
99
|
-
suggestions = files
|
|
100
|
-
.filter((f) => f.toLowerCase().includes(filename.toLowerCase().slice(0, 3)))
|
|
101
|
-
.slice(0, 5);
|
|
102
|
-
|
|
103
|
-
const suggestionText =
|
|
104
|
-
suggestions.length > 0
|
|
105
|
-
? `\nDid you mean:\n${suggestions.map((s) => ` - ${s}`).join('\n')}`
|
|
106
|
-
: '';
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
title: params.path,
|
|
110
|
-
output: `File not found: ${params.path}${suggestionText}`,
|
|
111
|
-
metadata: {
|
|
112
|
-
lines: 0,
|
|
113
|
-
truncated: false
|
|
114
|
-
}
|
|
115
|
-
};
|
|
65
|
+
return false;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const executeReadTool = async (
|
|
69
|
+
params: ReadToolParametersType,
|
|
70
|
+
context: ToolContext
|
|
71
|
+
): Promise<ReadToolResult> => {
|
|
72
|
+
const { basePath, vfsId } = context;
|
|
73
|
+
const resolvedPath = await resolveSandboxPathWithSymlinks(basePath, params.path, vfsId);
|
|
74
|
+
const exists = await existsInVirtualFs(resolvedPath, vfsId);
|
|
75
|
+
if (!exists) {
|
|
76
|
+
const dir = path.dirname(resolvedPath);
|
|
77
|
+
const filename = path.basename(resolvedPath);
|
|
78
|
+
|
|
79
|
+
let files: string[] = [];
|
|
80
|
+
try {
|
|
81
|
+
const entries = await readdirVirtualFs(dir, vfsId);
|
|
82
|
+
files = entries.map((entry) => entry.name);
|
|
83
|
+
} catch {
|
|
84
|
+
files = [];
|
|
116
85
|
}
|
|
117
86
|
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
87
|
+
const suggestions = files
|
|
88
|
+
.filter((f) => f.toLowerCase().includes(filename.toLowerCase().slice(0, 3)))
|
|
89
|
+
.slice(0, 5);
|
|
90
|
+
const suggestionText =
|
|
91
|
+
suggestions.length > 0
|
|
92
|
+
? `\nDid you mean:\n${suggestions.map((s) => ` - ${s}`).join('\n')}`
|
|
93
|
+
: '';
|
|
125
94
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{
|
|
136
|
-
type: 'file',
|
|
137
|
-
mime,
|
|
138
|
-
data: base64
|
|
139
|
-
}
|
|
140
|
-
]
|
|
141
|
-
};
|
|
142
|
-
}
|
|
95
|
+
return {
|
|
96
|
+
title: params.path,
|
|
97
|
+
output: `File not found: ${params.path}${suggestionText}`,
|
|
98
|
+
metadata: {
|
|
99
|
+
lines: 0,
|
|
100
|
+
truncated: false
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
143
104
|
|
|
144
|
-
|
|
145
|
-
if (PDF_EXTENSIONS.has(ext)) {
|
|
146
|
-
const bytes = await VirtualFs.readFileBuffer(resolvedPath, vfsId);
|
|
147
|
-
const base64 = Buffer.from(bytes).toString('base64');
|
|
105
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
148
106
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
lines: 0,
|
|
154
|
-
truncated: false,
|
|
155
|
-
isPdf: true
|
|
156
|
-
},
|
|
157
|
-
attachments: [
|
|
158
|
-
{
|
|
159
|
-
type: 'file',
|
|
160
|
-
mime: 'application/pdf',
|
|
161
|
-
data: base64
|
|
162
|
-
}
|
|
163
|
-
]
|
|
164
|
-
};
|
|
165
|
-
}
|
|
107
|
+
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
108
|
+
const bytes = await readVirtualFsFileBuffer(resolvedPath, vfsId);
|
|
109
|
+
const base64 = Buffer.from(bytes).toString('base64');
|
|
110
|
+
const mime = getImageMime(ext);
|
|
166
111
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
112
|
+
return {
|
|
113
|
+
title: params.path,
|
|
114
|
+
output: `[Image file: ${path.basename(resolvedPath)}]`,
|
|
115
|
+
metadata: {
|
|
116
|
+
lines: 0,
|
|
117
|
+
truncated: false,
|
|
118
|
+
isImage: true
|
|
119
|
+
},
|
|
120
|
+
attachments: [
|
|
121
|
+
{
|
|
122
|
+
type: 'file',
|
|
123
|
+
mime,
|
|
124
|
+
data: base64
|
|
176
125
|
}
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Read text file
|
|
181
|
-
const text = await VirtualFs.readFile(resolvedPath, vfsId);
|
|
182
|
-
const allLines = text.split('\n');
|
|
183
|
-
|
|
184
|
-
const offset = params.offset ?? 0;
|
|
185
|
-
const limit = params.limit ?? MAX_LINES;
|
|
186
|
-
|
|
187
|
-
// Apply truncation
|
|
188
|
-
let truncatedByLines = false;
|
|
189
|
-
let truncatedByBytes = false;
|
|
190
|
-
|
|
191
|
-
const outputLines: string[] = [];
|
|
192
|
-
let totalBytes = 0;
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
}
|
|
193
129
|
|
|
194
|
-
|
|
130
|
+
if (PDF_EXTENSIONS.has(ext)) {
|
|
131
|
+
const bytes = await readVirtualFsFileBuffer(resolvedPath, vfsId);
|
|
132
|
+
const base64 = Buffer.from(bytes).toString('base64');
|
|
195
133
|
|
|
196
|
-
|
|
197
|
-
|
|
134
|
+
return {
|
|
135
|
+
title: params.path,
|
|
136
|
+
output: `[PDF file: ${path.basename(resolvedPath)}]`,
|
|
137
|
+
metadata: {
|
|
138
|
+
lines: 0,
|
|
139
|
+
truncated: false,
|
|
140
|
+
isPdf: true
|
|
141
|
+
},
|
|
142
|
+
attachments: [
|
|
143
|
+
{
|
|
144
|
+
type: 'file',
|
|
145
|
+
mime: 'application/pdf',
|
|
146
|
+
data: base64
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
};
|
|
150
|
+
}
|
|
198
151
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
152
|
+
if (isBinaryBuffer(await readVirtualFsFileBuffer(resolvedPath, vfsId))) {
|
|
153
|
+
return {
|
|
154
|
+
title: params.path,
|
|
155
|
+
output: `[Binary file: ${path.basename(resolvedPath)}]`,
|
|
156
|
+
metadata: {
|
|
157
|
+
lines: 0,
|
|
158
|
+
truncated: false,
|
|
159
|
+
isBinary: true
|
|
202
160
|
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
203
163
|
|
|
204
|
-
|
|
164
|
+
const text = await readVirtualFsFile(resolvedPath, vfsId);
|
|
165
|
+
const allLines = text.split('\n');
|
|
205
166
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
167
|
+
const offset = params.offset ?? 0;
|
|
168
|
+
const limit = params.limit ?? MAX_LINES;
|
|
210
169
|
|
|
211
|
-
|
|
212
|
-
|
|
170
|
+
let truncatedByLines = false;
|
|
171
|
+
let truncatedByBytes = false;
|
|
172
|
+
const outputLines: string[] = [];
|
|
173
|
+
let totalBytes = 0;
|
|
174
|
+
const endLine = Math.min(allLines.length, offset + limit);
|
|
175
|
+
|
|
176
|
+
for (let i = offset; i < endLine; i++) {
|
|
177
|
+
let line = allLines[i] ?? '';
|
|
178
|
+
if (line.length > MAX_LINE_LENGTH) {
|
|
179
|
+
line = line.substring(0, MAX_LINE_LENGTH) + '...';
|
|
213
180
|
}
|
|
214
181
|
|
|
215
|
-
|
|
216
|
-
|
|
182
|
+
const lineBytes = Buffer.byteLength(line, 'utf8');
|
|
183
|
+
if (totalBytes + lineBytes > MAX_BYTES) {
|
|
184
|
+
truncatedByBytes = true;
|
|
185
|
+
break;
|
|
217
186
|
}
|
|
218
187
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const lineNum = (index + offset + 1).toString().padStart(5, ' ');
|
|
223
|
-
return `${lineNum}\t${line}`;
|
|
224
|
-
})
|
|
225
|
-
.join('\n');
|
|
188
|
+
outputLines.push(line);
|
|
189
|
+
totalBytes += lineBytes;
|
|
190
|
+
}
|
|
226
191
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const remaining = allLines.length - offset - outputLines.length;
|
|
231
|
-
if (remaining > 0) {
|
|
232
|
-
truncationMessage = `\n\n[Truncated: ${remaining} more lines. Use offset=${offset + outputLines.length} to continue reading.]`;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
192
|
+
if (outputLines.length < endLine - offset || endLine < allLines.length) {
|
|
193
|
+
truncatedByLines = !truncatedByBytes && outputLines.length >= limit;
|
|
194
|
+
}
|
|
235
195
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
196
|
+
const formattedOutput = outputLines
|
|
197
|
+
.map((line, index) => {
|
|
198
|
+
const lineNum = (index + offset + 1).toString().padStart(5, ' ');
|
|
199
|
+
return `${lineNum}\t${line}`;
|
|
200
|
+
})
|
|
201
|
+
.join('\n');
|
|
202
|
+
|
|
203
|
+
let truncationMessage = '';
|
|
204
|
+
if (truncatedByBytes || truncatedByLines) {
|
|
205
|
+
const remaining = allLines.length - offset - outputLines.length;
|
|
206
|
+
if (remaining > 0) {
|
|
207
|
+
truncationMessage = `\n\n[Truncated: ${remaining} more lines. Use offset=${offset + outputLines.length} to continue reading.]`;
|
|
208
|
+
}
|
|
246
209
|
}
|
|
247
210
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return 'image/gif';
|
|
257
|
-
case '.webp':
|
|
258
|
-
return 'image/webp';
|
|
259
|
-
case '.bmp':
|
|
260
|
-
return 'image/bmp';
|
|
261
|
-
case '.ico':
|
|
262
|
-
return 'image/x-icon';
|
|
263
|
-
case '.svg':
|
|
264
|
-
return 'image/svg+xml';
|
|
265
|
-
default:
|
|
266
|
-
return 'application/octet-stream';
|
|
211
|
+
return {
|
|
212
|
+
title: params.path,
|
|
213
|
+
output: formattedOutput + truncationMessage,
|
|
214
|
+
metadata: {
|
|
215
|
+
lines: outputLines.length,
|
|
216
|
+
truncated: truncatedByBytes || truncatedByLines,
|
|
217
|
+
truncatedByLines,
|
|
218
|
+
truncatedByBytes
|
|
267
219
|
}
|
|
220
|
+
};
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const getImageMime = (ext: string): string => {
|
|
224
|
+
switch (ext) {
|
|
225
|
+
case '.png':
|
|
226
|
+
return 'image/png';
|
|
227
|
+
case '.jpg':
|
|
228
|
+
case '.jpeg':
|
|
229
|
+
return 'image/jpeg';
|
|
230
|
+
case '.gif':
|
|
231
|
+
return 'image/gif';
|
|
232
|
+
case '.webp':
|
|
233
|
+
return 'image/webp';
|
|
234
|
+
case '.bmp':
|
|
235
|
+
return 'image/bmp';
|
|
236
|
+
case '.ico':
|
|
237
|
+
return 'image/x-icon';
|
|
238
|
+
case '.svg':
|
|
239
|
+
return 'image/svg+xml';
|
|
240
|
+
default:
|
|
241
|
+
return 'application/octet-stream';
|
|
268
242
|
}
|
|
269
|
-
}
|
|
243
|
+
};
|
|
@@ -1,103 +1,111 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { VirtualFs } from '../vfs/virtual-fs.ts';
|
|
3
|
+
import { existsInVirtualFs, realpathVirtualFs, statVirtualFs } from '../vfs/virtual-fs.ts';
|
|
6
4
|
|
|
7
5
|
const posix = path.posix;
|
|
8
6
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
readonly basePath: string;
|
|
7
|
+
export class PathEscapeError extends Error {
|
|
8
|
+
readonly _tag = 'PathEscapeError';
|
|
9
|
+
readonly requestedPath: string;
|
|
10
|
+
readonly basePath: string;
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.requestedPath = requestedPath;
|
|
20
|
-
this.basePath = basePath;
|
|
21
|
-
}
|
|
12
|
+
constructor(requestedPath: string, basePath: string) {
|
|
13
|
+
super(`Path "${requestedPath}" is outside the allowed directory "${basePath}". Access denied.`);
|
|
14
|
+
this.requestedPath = requestedPath;
|
|
15
|
+
this.basePath = basePath;
|
|
22
16
|
}
|
|
17
|
+
}
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
export class PathNotFoundError extends Error {
|
|
20
|
+
readonly _tag = 'PathNotFoundError';
|
|
21
|
+
readonly requestedPath: string;
|
|
27
22
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
23
|
+
constructor(requestedPath: string) {
|
|
24
|
+
super(`Path "${requestedPath}" does not exist.`);
|
|
25
|
+
this.requestedPath = requestedPath;
|
|
32
26
|
}
|
|
27
|
+
}
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (relative.startsWith('..') || posix.isAbsolute(relative)) {
|
|
43
|
-
throw new PathEscapeError(requestedPath, basePath);
|
|
44
|
-
}
|
|
29
|
+
export const resolveSandboxPath = (basePath: string, requestedPath: string): string => {
|
|
30
|
+
const normalizedBase = posix.resolve('/', basePath);
|
|
31
|
+
const resolved = posix.isAbsolute(requestedPath)
|
|
32
|
+
? posix.resolve(requestedPath)
|
|
33
|
+
: posix.resolve(normalizedBase, requestedPath);
|
|
34
|
+
const normalized = posix.normalize(resolved);
|
|
35
|
+
const relative = posix.relative(normalizedBase, normalized);
|
|
45
36
|
|
|
46
|
-
|
|
37
|
+
if (relative.startsWith('..') || posix.isAbsolute(relative)) {
|
|
38
|
+
throw new PathEscapeError(requestedPath, basePath);
|
|
47
39
|
}
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
41
|
+
return normalized;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const resolveSandboxPathWithSymlinks = async (
|
|
45
|
+
basePath: string,
|
|
46
|
+
requestedPath: string,
|
|
47
|
+
vfsId?: string
|
|
48
|
+
) => {
|
|
49
|
+
const resolved = resolveSandboxPath(basePath, requestedPath);
|
|
50
|
+
try {
|
|
51
|
+
return await realpathVirtualFs(resolved, vfsId);
|
|
52
|
+
} catch {
|
|
53
|
+
return resolved;
|
|
60
54
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const sandboxPathExists = async (
|
|
58
|
+
basePath: string,
|
|
59
|
+
requestedPath: string,
|
|
60
|
+
vfsId?: string
|
|
61
|
+
) => {
|
|
62
|
+
try {
|
|
63
|
+
const resolved = resolveSandboxPath(basePath, requestedPath);
|
|
64
|
+
return await existsInVirtualFs(resolved, vfsId);
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
70
67
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const sandboxPathIsDirectory = async (
|
|
71
|
+
basePath: string,
|
|
72
|
+
requestedPath: string,
|
|
73
|
+
vfsId?: string
|
|
74
|
+
) => {
|
|
75
|
+
try {
|
|
76
|
+
const resolved = resolveSandboxPath(basePath, requestedPath);
|
|
77
|
+
const stats = await statVirtualFs(resolved, vfsId);
|
|
78
|
+
return stats.isDirectory;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const sandboxPathIsFile = async (
|
|
85
|
+
basePath: string,
|
|
86
|
+
requestedPath: string,
|
|
87
|
+
vfsId?: string
|
|
88
|
+
) => {
|
|
89
|
+
try {
|
|
90
|
+
const resolved = resolveSandboxPath(basePath, requestedPath);
|
|
91
|
+
const stats = await statVirtualFs(resolved, vfsId);
|
|
92
|
+
return stats.isFile;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
90
95
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const validateSandboxPath = async (
|
|
99
|
+
basePath: string,
|
|
100
|
+
requestedPath: string,
|
|
101
|
+
vfsId?: string
|
|
102
|
+
) => {
|
|
103
|
+
const resolved = resolveSandboxPath(basePath, requestedPath);
|
|
104
|
+
if (!(await existsInVirtualFs(resolved, vfsId))) {
|
|
105
|
+
throw new PathNotFoundError(requestedPath);
|
|
98
106
|
}
|
|
107
|
+
return resolved;
|
|
108
|
+
};
|
|
99
109
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
}
|
|
110
|
+
export const getSandboxRelativePath = (basePath: string, resolvedPath: string): string =>
|
|
111
|
+
posix.relative(basePath, resolvedPath);
|