gitx.do 0.0.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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/durable-object/object-store.d.ts +113 -0
- package/dist/durable-object/object-store.d.ts.map +1 -0
- package/dist/durable-object/object-store.js +387 -0
- package/dist/durable-object/object-store.js.map +1 -0
- package/dist/durable-object/schema.d.ts +17 -0
- package/dist/durable-object/schema.d.ts.map +1 -0
- package/dist/durable-object/schema.js +43 -0
- package/dist/durable-object/schema.js.map +1 -0
- package/dist/durable-object/wal.d.ts +111 -0
- package/dist/durable-object/wal.d.ts.map +1 -0
- package/dist/durable-object/wal.js +200 -0
- package/dist/durable-object/wal.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/adapter.d.ts +231 -0
- package/dist/mcp/adapter.d.ts.map +1 -0
- package/dist/mcp/adapter.js +502 -0
- package/dist/mcp/adapter.js.map +1 -0
- package/dist/mcp/sandbox.d.ts +261 -0
- package/dist/mcp/sandbox.d.ts.map +1 -0
- package/dist/mcp/sandbox.js +983 -0
- package/dist/mcp/sandbox.js.map +1 -0
- package/dist/mcp/sdk-adapter.d.ts +413 -0
- package/dist/mcp/sdk-adapter.d.ts.map +1 -0
- package/dist/mcp/sdk-adapter.js +672 -0
- package/dist/mcp/sdk-adapter.js.map +1 -0
- package/dist/mcp/tools.d.ts +133 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +1604 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/ops/blame.d.ts +148 -0
- package/dist/ops/blame.d.ts.map +1 -0
- package/dist/ops/blame.js +754 -0
- package/dist/ops/blame.js.map +1 -0
- package/dist/ops/branch.d.ts +215 -0
- package/dist/ops/branch.d.ts.map +1 -0
- package/dist/ops/branch.js +608 -0
- package/dist/ops/branch.js.map +1 -0
- package/dist/ops/commit-traversal.d.ts +209 -0
- package/dist/ops/commit-traversal.d.ts.map +1 -0
- package/dist/ops/commit-traversal.js +755 -0
- package/dist/ops/commit-traversal.js.map +1 -0
- package/dist/ops/commit.d.ts +221 -0
- package/dist/ops/commit.d.ts.map +1 -0
- package/dist/ops/commit.js +606 -0
- package/dist/ops/commit.js.map +1 -0
- package/dist/ops/merge-base.d.ts +223 -0
- package/dist/ops/merge-base.d.ts.map +1 -0
- package/dist/ops/merge-base.js +581 -0
- package/dist/ops/merge-base.js.map +1 -0
- package/dist/ops/merge.d.ts +385 -0
- package/dist/ops/merge.d.ts.map +1 -0
- package/dist/ops/merge.js +1203 -0
- package/dist/ops/merge.js.map +1 -0
- package/dist/ops/tag.d.ts +182 -0
- package/dist/ops/tag.d.ts.map +1 -0
- package/dist/ops/tag.js +608 -0
- package/dist/ops/tag.js.map +1 -0
- package/dist/ops/tree-builder.d.ts +82 -0
- package/dist/ops/tree-builder.d.ts.map +1 -0
- package/dist/ops/tree-builder.js +246 -0
- package/dist/ops/tree-builder.js.map +1 -0
- package/dist/ops/tree-diff.d.ts +243 -0
- package/dist/ops/tree-diff.d.ts.map +1 -0
- package/dist/ops/tree-diff.js +657 -0
- package/dist/ops/tree-diff.js.map +1 -0
- package/dist/pack/delta.d.ts +68 -0
- package/dist/pack/delta.d.ts.map +1 -0
- package/dist/pack/delta.js +343 -0
- package/dist/pack/delta.js.map +1 -0
- package/dist/pack/format.d.ts +84 -0
- package/dist/pack/format.d.ts.map +1 -0
- package/dist/pack/format.js +261 -0
- package/dist/pack/format.js.map +1 -0
- package/dist/pack/full-generation.d.ts +327 -0
- package/dist/pack/full-generation.d.ts.map +1 -0
- package/dist/pack/full-generation.js +1159 -0
- package/dist/pack/full-generation.js.map +1 -0
- package/dist/pack/generation.d.ts +118 -0
- package/dist/pack/generation.d.ts.map +1 -0
- package/dist/pack/generation.js +459 -0
- package/dist/pack/generation.js.map +1 -0
- package/dist/pack/index.d.ts +181 -0
- package/dist/pack/index.d.ts.map +1 -0
- package/dist/pack/index.js +552 -0
- package/dist/pack/index.js.map +1 -0
- package/dist/refs/branch.d.ts +224 -0
- package/dist/refs/branch.d.ts.map +1 -0
- package/dist/refs/branch.js +170 -0
- package/dist/refs/branch.js.map +1 -0
- package/dist/refs/storage.d.ts +208 -0
- package/dist/refs/storage.d.ts.map +1 -0
- package/dist/refs/storage.js +421 -0
- package/dist/refs/storage.js.map +1 -0
- package/dist/refs/tag.d.ts +230 -0
- package/dist/refs/tag.d.ts.map +1 -0
- package/dist/refs/tag.js +188 -0
- package/dist/refs/tag.js.map +1 -0
- package/dist/storage/lru-cache.d.ts +188 -0
- package/dist/storage/lru-cache.d.ts.map +1 -0
- package/dist/storage/lru-cache.js +410 -0
- package/dist/storage/lru-cache.js.map +1 -0
- package/dist/storage/object-index.d.ts +140 -0
- package/dist/storage/object-index.d.ts.map +1 -0
- package/dist/storage/object-index.js +166 -0
- package/dist/storage/object-index.js.map +1 -0
- package/dist/storage/r2-pack.d.ts +394 -0
- package/dist/storage/r2-pack.d.ts.map +1 -0
- package/dist/storage/r2-pack.js +1062 -0
- package/dist/storage/r2-pack.js.map +1 -0
- package/dist/tiered/cdc-pipeline.d.ts +316 -0
- package/dist/tiered/cdc-pipeline.d.ts.map +1 -0
- package/dist/tiered/cdc-pipeline.js +771 -0
- package/dist/tiered/cdc-pipeline.js.map +1 -0
- package/dist/tiered/migration.d.ts +242 -0
- package/dist/tiered/migration.d.ts.map +1 -0
- package/dist/tiered/migration.js +592 -0
- package/dist/tiered/migration.js.map +1 -0
- package/dist/tiered/parquet-writer.d.ts +248 -0
- package/dist/tiered/parquet-writer.d.ts.map +1 -0
- package/dist/tiered/parquet-writer.js +555 -0
- package/dist/tiered/parquet-writer.js.map +1 -0
- package/dist/tiered/read-path.d.ts +141 -0
- package/dist/tiered/read-path.d.ts.map +1 -0
- package/dist/tiered/read-path.js +204 -0
- package/dist/tiered/read-path.js.map +1 -0
- package/dist/types/objects.d.ts +53 -0
- package/dist/types/objects.d.ts.map +1 -0
- package/dist/types/objects.js +291 -0
- package/dist/types/objects.js.map +1 -0
- package/dist/types/storage.d.ts +117 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/storage.js +8 -0
- package/dist/types/storage.js.map +1 -0
- package/dist/utils/hash.d.ts +31 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +60 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/sha1.d.ts +26 -0
- package/dist/utils/sha1.d.ts.map +1 -0
- package/dist/utils/sha1.js +127 -0
- package/dist/utils/sha1.js.map +1 -0
- package/dist/wire/capabilities.d.ts +236 -0
- package/dist/wire/capabilities.d.ts.map +1 -0
- package/dist/wire/capabilities.js +437 -0
- package/dist/wire/capabilities.js.map +1 -0
- package/dist/wire/pkt-line.d.ts +67 -0
- package/dist/wire/pkt-line.d.ts.map +1 -0
- package/dist/wire/pkt-line.js +145 -0
- package/dist/wire/pkt-line.js.map +1 -0
- package/dist/wire/receive-pack.d.ts +302 -0
- package/dist/wire/receive-pack.d.ts.map +1 -0
- package/dist/wire/receive-pack.js +885 -0
- package/dist/wire/receive-pack.js.map +1 -0
- package/dist/wire/smart-http.d.ts +321 -0
- package/dist/wire/smart-http.d.ts.map +1 -0
- package/dist/wire/smart-http.js +654 -0
- package/dist/wire/smart-http.js.map +1 -0
- package/dist/wire/upload-pack.d.ts +333 -0
- package/dist/wire/upload-pack.d.ts.map +1 -0
- package/dist/wire/upload-pack.js +850 -0
- package/dist/wire/upload-pack.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Sandbox Execution Environment
|
|
3
|
+
*
|
|
4
|
+
* Provides isolated execution environment for MCP tools with:
|
|
5
|
+
* - Resource limits (memory, CPU, time)
|
|
6
|
+
* - Capability restrictions (file, network, process)
|
|
7
|
+
* - Safe git operation execution
|
|
8
|
+
* - Audit logging
|
|
9
|
+
*
|
|
10
|
+
* SECURITY: Uses Node.js vm module for proper isolation instead of
|
|
11
|
+
* string analysis, which can be easily bypassed.
|
|
12
|
+
*/
|
|
13
|
+
import { EventEmitter } from 'events';
|
|
14
|
+
/**
|
|
15
|
+
* Sandbox error codes
|
|
16
|
+
*/
|
|
17
|
+
export var SandboxErrorCode;
|
|
18
|
+
(function (SandboxErrorCode) {
|
|
19
|
+
SandboxErrorCode["TIMEOUT"] = "TIMEOUT";
|
|
20
|
+
SandboxErrorCode["MEMORY_LIMIT_EXCEEDED"] = "MEMORY_LIMIT_EXCEEDED";
|
|
21
|
+
SandboxErrorCode["CPU_LIMIT_EXCEEDED"] = "CPU_LIMIT_EXCEEDED";
|
|
22
|
+
SandboxErrorCode["PERMISSION_DENIED"] = "PERMISSION_DENIED";
|
|
23
|
+
SandboxErrorCode["EXECUTION_ERROR"] = "EXECUTION_ERROR";
|
|
24
|
+
SandboxErrorCode["FILE_DESCRIPTOR_LIMIT"] = "FILE_DESCRIPTOR_LIMIT";
|
|
25
|
+
SandboxErrorCode["PROCESS_LIMIT_EXCEEDED"] = "PROCESS_LIMIT_EXCEEDED";
|
|
26
|
+
SandboxErrorCode["BANDWIDTH_LIMIT_EXCEEDED"] = "BANDWIDTH_LIMIT_EXCEEDED";
|
|
27
|
+
SandboxErrorCode["DISK_LIMIT_EXCEEDED"] = "DISK_LIMIT_EXCEEDED";
|
|
28
|
+
SandboxErrorCode["SANDBOX_CRASHED"] = "SANDBOX_CRASHED";
|
|
29
|
+
SandboxErrorCode["SANDBOX_PAUSED"] = "SANDBOX_PAUSED";
|
|
30
|
+
})(SandboxErrorCode || (SandboxErrorCode = {}));
|
|
31
|
+
/**
|
|
32
|
+
* Sandbox error class
|
|
33
|
+
*/
|
|
34
|
+
export class SandboxError extends Error {
|
|
35
|
+
code;
|
|
36
|
+
data;
|
|
37
|
+
stack;
|
|
38
|
+
constructor(code, message, data) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'SandboxError';
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.data = data;
|
|
43
|
+
}
|
|
44
|
+
toJSON() {
|
|
45
|
+
const result = {
|
|
46
|
+
code: this.code,
|
|
47
|
+
message: this.message,
|
|
48
|
+
};
|
|
49
|
+
if (this.data !== undefined) {
|
|
50
|
+
result.data = this.data;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Sandbox state enum
|
|
57
|
+
*/
|
|
58
|
+
export var SandboxState;
|
|
59
|
+
(function (SandboxState) {
|
|
60
|
+
SandboxState["IDLE"] = "IDLE";
|
|
61
|
+
SandboxState["RUNNING"] = "RUNNING";
|
|
62
|
+
SandboxState["PAUSED"] = "PAUSED";
|
|
63
|
+
SandboxState["DESTROYED"] = "DESTROYED";
|
|
64
|
+
})(SandboxState || (SandboxState = {}));
|
|
65
|
+
/**
|
|
66
|
+
* Generate unique ID
|
|
67
|
+
*/
|
|
68
|
+
function generateId() {
|
|
69
|
+
return `sandbox-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get permission set from preset
|
|
73
|
+
*/
|
|
74
|
+
function getPermissionsFromPreset(preset) {
|
|
75
|
+
switch (preset) {
|
|
76
|
+
case 'readonly':
|
|
77
|
+
return {
|
|
78
|
+
fileRead: true,
|
|
79
|
+
fileWrite: false,
|
|
80
|
+
network: false,
|
|
81
|
+
spawn: false,
|
|
82
|
+
env: true,
|
|
83
|
+
nativeModules: true,
|
|
84
|
+
};
|
|
85
|
+
case 'full':
|
|
86
|
+
return {
|
|
87
|
+
fileRead: true,
|
|
88
|
+
fileWrite: true,
|
|
89
|
+
network: true,
|
|
90
|
+
spawn: true,
|
|
91
|
+
env: true,
|
|
92
|
+
nativeModules: true,
|
|
93
|
+
};
|
|
94
|
+
case 'network-only':
|
|
95
|
+
return {
|
|
96
|
+
fileRead: false,
|
|
97
|
+
fileWrite: false,
|
|
98
|
+
network: true,
|
|
99
|
+
spawn: false,
|
|
100
|
+
env: false,
|
|
101
|
+
nativeModules: false,
|
|
102
|
+
};
|
|
103
|
+
default:
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Dangerous modules that require permission checks
|
|
109
|
+
*/
|
|
110
|
+
const DANGEROUS_MODULES = new Set([
|
|
111
|
+
'fs',
|
|
112
|
+
'fs/promises',
|
|
113
|
+
'child_process',
|
|
114
|
+
'http',
|
|
115
|
+
'https',
|
|
116
|
+
'net',
|
|
117
|
+
'dgram',
|
|
118
|
+
'dns',
|
|
119
|
+
'tls',
|
|
120
|
+
'cluster',
|
|
121
|
+
'worker_threads',
|
|
122
|
+
]);
|
|
123
|
+
/**
|
|
124
|
+
* File system read methods
|
|
125
|
+
*/
|
|
126
|
+
const FS_READ_METHODS = new Set([
|
|
127
|
+
'readFile',
|
|
128
|
+
'readFileSync',
|
|
129
|
+
'readdir',
|
|
130
|
+
'readdirSync',
|
|
131
|
+
'readlink',
|
|
132
|
+
'readlinkSync',
|
|
133
|
+
'stat',
|
|
134
|
+
'statSync',
|
|
135
|
+
'lstat',
|
|
136
|
+
'lstatSync',
|
|
137
|
+
'access',
|
|
138
|
+
'accessSync',
|
|
139
|
+
'exists',
|
|
140
|
+
'existsSync',
|
|
141
|
+
'open',
|
|
142
|
+
'openSync',
|
|
143
|
+
'createReadStream',
|
|
144
|
+
]);
|
|
145
|
+
/**
|
|
146
|
+
* File system write methods
|
|
147
|
+
*/
|
|
148
|
+
const FS_WRITE_METHODS = new Set([
|
|
149
|
+
'writeFile',
|
|
150
|
+
'writeFileSync',
|
|
151
|
+
'appendFile',
|
|
152
|
+
'appendFileSync',
|
|
153
|
+
'mkdir',
|
|
154
|
+
'mkdirSync',
|
|
155
|
+
'rmdir',
|
|
156
|
+
'rmdirSync',
|
|
157
|
+
'unlink',
|
|
158
|
+
'unlinkSync',
|
|
159
|
+
'rename',
|
|
160
|
+
'renameSync',
|
|
161
|
+
'copyFile',
|
|
162
|
+
'copyFileSync',
|
|
163
|
+
'truncate',
|
|
164
|
+
'truncateSync',
|
|
165
|
+
'createWriteStream',
|
|
166
|
+
'chmod',
|
|
167
|
+
'chmodSync',
|
|
168
|
+
'chown',
|
|
169
|
+
'chownSync',
|
|
170
|
+
]);
|
|
171
|
+
/**
|
|
172
|
+
* MCP Sandbox class for isolated execution
|
|
173
|
+
*
|
|
174
|
+
* SECURITY: This implementation uses Node.js vm module with proper context
|
|
175
|
+
* isolation and runtime permission checks instead of string analysis.
|
|
176
|
+
*/
|
|
177
|
+
export class MCPSandbox extends EventEmitter {
|
|
178
|
+
id;
|
|
179
|
+
config;
|
|
180
|
+
state = SandboxState.IDLE;
|
|
181
|
+
resourceStats = {
|
|
182
|
+
memoryUsed: 0,
|
|
183
|
+
cpuTimeUsed: 0,
|
|
184
|
+
executionCount: 0,
|
|
185
|
+
activeHandles: 0,
|
|
186
|
+
};
|
|
187
|
+
permissionViolations = [];
|
|
188
|
+
permissions;
|
|
189
|
+
executionQueue = [];
|
|
190
|
+
activeExecutions = 0;
|
|
191
|
+
globalContext = new Map();
|
|
192
|
+
constructor(config = {}) {
|
|
193
|
+
super();
|
|
194
|
+
this.id = generateId();
|
|
195
|
+
this.config = {
|
|
196
|
+
timeout: config.timeout ?? 30000,
|
|
197
|
+
memoryLimit: config.memoryLimit ?? 256 * 1024 * 1024,
|
|
198
|
+
isolationLevel: config.isolationLevel ?? 'normal',
|
|
199
|
+
...config,
|
|
200
|
+
};
|
|
201
|
+
// Apply resource limits from config
|
|
202
|
+
if (config.resourceLimits) {
|
|
203
|
+
this.config.memoryLimit = config.resourceLimits.memoryLimit ?? this.config.memoryLimit;
|
|
204
|
+
this.config.cpuTimeLimit = config.resourceLimits.cpuTimeLimit ?? this.config.cpuTimeLimit;
|
|
205
|
+
this.config.maxOpenFiles = config.resourceLimits.maxOpenFiles ?? this.config.maxOpenFiles;
|
|
206
|
+
this.config.maxProcesses = config.resourceLimits.maxProcesses ?? this.config.maxProcesses;
|
|
207
|
+
this.config.diskWriteLimit = config.resourceLimits.diskWriteLimit ?? this.config.diskWriteLimit;
|
|
208
|
+
}
|
|
209
|
+
// Set permissions from preset or config
|
|
210
|
+
if (config.permissionPreset) {
|
|
211
|
+
this.permissions = getPermissionsFromPreset(config.permissionPreset);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
this.permissions = config.permissions ?? {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
getId() {
|
|
218
|
+
return this.id;
|
|
219
|
+
}
|
|
220
|
+
getConfig() {
|
|
221
|
+
return { ...this.config };
|
|
222
|
+
}
|
|
223
|
+
getState() {
|
|
224
|
+
return this.state;
|
|
225
|
+
}
|
|
226
|
+
getPermissions() {
|
|
227
|
+
return { ...this.permissions };
|
|
228
|
+
}
|
|
229
|
+
getResourceStats() {
|
|
230
|
+
return { ...this.resourceStats };
|
|
231
|
+
}
|
|
232
|
+
getResourceLimits() {
|
|
233
|
+
return {
|
|
234
|
+
memoryLimit: this.config.memoryLimit,
|
|
235
|
+
cpuTimeLimit: this.config.cpuTimeLimit,
|
|
236
|
+
maxOpenFiles: this.config.maxOpenFiles,
|
|
237
|
+
maxProcesses: this.config.maxProcesses,
|
|
238
|
+
diskWriteLimit: this.config.diskWriteLimit,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
getPermissionViolations() {
|
|
242
|
+
return [...this.permissionViolations];
|
|
243
|
+
}
|
|
244
|
+
async start() {
|
|
245
|
+
if (this.state === SandboxState.DESTROYED) {
|
|
246
|
+
throw new Error('Cannot start a destroyed sandbox');
|
|
247
|
+
}
|
|
248
|
+
if (this.state === SandboxState.RUNNING) {
|
|
249
|
+
throw new Error('Sandbox is already running');
|
|
250
|
+
}
|
|
251
|
+
this.state = SandboxState.RUNNING;
|
|
252
|
+
this.emit('stateChange', this.state);
|
|
253
|
+
}
|
|
254
|
+
async stop() {
|
|
255
|
+
if (this.state !== SandboxState.RUNNING && this.state !== SandboxState.PAUSED) {
|
|
256
|
+
throw new Error('Sandbox is not running');
|
|
257
|
+
}
|
|
258
|
+
this.state = SandboxState.IDLE;
|
|
259
|
+
this.globalContext.clear();
|
|
260
|
+
this.emit('stateChange', this.state);
|
|
261
|
+
}
|
|
262
|
+
async pause() {
|
|
263
|
+
if (this.state !== SandboxState.RUNNING) {
|
|
264
|
+
throw new Error('Sandbox is not running');
|
|
265
|
+
}
|
|
266
|
+
this.state = SandboxState.PAUSED;
|
|
267
|
+
this.emit('stateChange', this.state);
|
|
268
|
+
}
|
|
269
|
+
async resume() {
|
|
270
|
+
if (this.state !== SandboxState.PAUSED) {
|
|
271
|
+
throw new Error('Sandbox is not paused');
|
|
272
|
+
}
|
|
273
|
+
this.state = SandboxState.RUNNING;
|
|
274
|
+
this.emit('stateChange', this.state);
|
|
275
|
+
// Process queued executions
|
|
276
|
+
while (this.executionQueue.length > 0) {
|
|
277
|
+
const item = this.executionQueue.shift();
|
|
278
|
+
if (item) {
|
|
279
|
+
item.resolve();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async cleanup() {
|
|
284
|
+
this.resourceStats = {
|
|
285
|
+
memoryUsed: 0,
|
|
286
|
+
cpuTimeUsed: 0,
|
|
287
|
+
executionCount: 0,
|
|
288
|
+
activeHandles: 0,
|
|
289
|
+
};
|
|
290
|
+
this.globalContext.clear();
|
|
291
|
+
}
|
|
292
|
+
async destroy() {
|
|
293
|
+
if (this.state === SandboxState.RUNNING) {
|
|
294
|
+
await this.stop();
|
|
295
|
+
}
|
|
296
|
+
this.state = SandboxState.DESTROYED;
|
|
297
|
+
this.emit('stateChange', this.state);
|
|
298
|
+
}
|
|
299
|
+
async execute(fn, options = {}) {
|
|
300
|
+
const startTime = Date.now();
|
|
301
|
+
const timeout = options.timeout ?? this.config.timeout ?? 30000;
|
|
302
|
+
// Handle paused state
|
|
303
|
+
if (this.state === SandboxState.PAUSED) {
|
|
304
|
+
if (this.config.queueOnPause) {
|
|
305
|
+
await new Promise((resolve) => {
|
|
306
|
+
this.executionQueue.push({ resolve });
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
return {
|
|
311
|
+
sandboxId: this.id,
|
|
312
|
+
error: new SandboxError(SandboxErrorCode.SANDBOX_PAUSED, 'Sandbox is paused'),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Handle concurrency limit
|
|
317
|
+
const maxConcurrent = this.config.maxConcurrentExecutions ?? Infinity;
|
|
318
|
+
if (this.activeExecutions >= maxConcurrent) {
|
|
319
|
+
await new Promise((resolve) => {
|
|
320
|
+
const checkInterval = setInterval(() => {
|
|
321
|
+
if (this.activeExecutions < maxConcurrent) {
|
|
322
|
+
clearInterval(checkInterval);
|
|
323
|
+
resolve();
|
|
324
|
+
}
|
|
325
|
+
}, 10);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
this.activeExecutions++;
|
|
329
|
+
try {
|
|
330
|
+
const result = await this.executeInSandbox(fn, timeout, options);
|
|
331
|
+
const endTime = Date.now();
|
|
332
|
+
this.resourceStats.executionCount++;
|
|
333
|
+
return {
|
|
334
|
+
value: result.value,
|
|
335
|
+
error: result.error,
|
|
336
|
+
sandboxId: this.id,
|
|
337
|
+
metadata: {
|
|
338
|
+
startTime,
|
|
339
|
+
endTime,
|
|
340
|
+
elapsedMs: endTime - startTime,
|
|
341
|
+
},
|
|
342
|
+
resourceUsage: {
|
|
343
|
+
memoryUsed: this.resourceStats.memoryUsed,
|
|
344
|
+
cpuTimeUsed: this.resourceStats.cpuTimeUsed,
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
this.activeExecutions--;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async executeInSandbox(fn, timeout, options) {
|
|
353
|
+
return new Promise((resolve) => {
|
|
354
|
+
let resolved = false;
|
|
355
|
+
let timeoutId;
|
|
356
|
+
const cleanup = () => {
|
|
357
|
+
if (timeoutId) {
|
|
358
|
+
clearTimeout(timeoutId);
|
|
359
|
+
}
|
|
360
|
+
this.resourceStats.activeHandles = 0;
|
|
361
|
+
};
|
|
362
|
+
// Set up timeout
|
|
363
|
+
timeoutId = setTimeout(() => {
|
|
364
|
+
if (!resolved) {
|
|
365
|
+
resolved = true;
|
|
366
|
+
cleanup();
|
|
367
|
+
resolve({
|
|
368
|
+
error: new SandboxError(SandboxErrorCode.TIMEOUT, `Execution exceeded timeout of ${timeout}ms`, { timeoutMs: timeout }),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}, timeout);
|
|
372
|
+
// Pre-check for resource limit violations (static analysis for obvious cases)
|
|
373
|
+
// This is a defense-in-depth measure - actual security comes from runtime checks
|
|
374
|
+
const preCheckError = this.preCheckResourceLimits(fn, timeout);
|
|
375
|
+
if (preCheckError) {
|
|
376
|
+
resolved = true;
|
|
377
|
+
cleanup();
|
|
378
|
+
resolve({ error: preCheckError });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// Execute the function with isolated context and runtime permission checks
|
|
382
|
+
try {
|
|
383
|
+
const result = this.runWithSecureContext(fn, options);
|
|
384
|
+
if (result instanceof Promise) {
|
|
385
|
+
result
|
|
386
|
+
.then((value) => {
|
|
387
|
+
if (!resolved) {
|
|
388
|
+
resolved = true;
|
|
389
|
+
cleanup();
|
|
390
|
+
// Update memory stats
|
|
391
|
+
this.resourceStats.memoryUsed = Math.max(this.resourceStats.memoryUsed, process.memoryUsage().heapUsed);
|
|
392
|
+
resolve({ value });
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
.catch((error) => {
|
|
396
|
+
if (!resolved) {
|
|
397
|
+
resolved = true;
|
|
398
|
+
cleanup();
|
|
399
|
+
resolve({ error: this.wrapError(error, options) });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
if (!resolved) {
|
|
405
|
+
resolved = true;
|
|
406
|
+
cleanup();
|
|
407
|
+
// Update memory stats
|
|
408
|
+
this.resourceStats.memoryUsed = Math.max(this.resourceStats.memoryUsed, process.memoryUsage().heapUsed);
|
|
409
|
+
resolve({ value: result });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
if (!resolved) {
|
|
415
|
+
resolved = true;
|
|
416
|
+
cleanup();
|
|
417
|
+
resolve({ error: this.wrapError(error, options) });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Pre-check function for static analysis of potential violations
|
|
424
|
+
*
|
|
425
|
+
* SECURITY NOTE: This performs two types of checks:
|
|
426
|
+
* 1. Resource limit checks (memory, CPU, bandwidth) - defense-in-depth for obvious cases
|
|
427
|
+
* 2. Permission checks for module imports - enforced before execution starts
|
|
428
|
+
*
|
|
429
|
+
* The permission checks here are CRITICAL for security because we cannot intercept
|
|
430
|
+
* dynamic import() calls at runtime without experimental Node.js loader hooks.
|
|
431
|
+
* By analyzing the function source, we can detect which modules will be imported
|
|
432
|
+
* and block execution before it starts.
|
|
433
|
+
*
|
|
434
|
+
* This is combined with runtime fs proxy checks for additional security layers.
|
|
435
|
+
*/
|
|
436
|
+
preCheckResourceLimits(fn, timeout) {
|
|
437
|
+
const fnStr = fn.toString();
|
|
438
|
+
const isolationLevel = this.config.isolationLevel ?? 'normal';
|
|
439
|
+
// ========== PERMISSION CHECKS ==========
|
|
440
|
+
// Detect module imports using various patterns
|
|
441
|
+
// Match: import('fs'), import("fs"), await import('fs')
|
|
442
|
+
const hasImportFs = /import\s*\(\s*['"]fs['"]\s*\)/.test(fnStr) ||
|
|
443
|
+
/import\s*\(\s*['"]fs\/promises['"]\s*\)/.test(fnStr) ||
|
|
444
|
+
/__vite_ssr_dynamic_import__\s*\(\s*['"]fs['"]\s*\)/.test(fnStr) ||
|
|
445
|
+
fnStr.includes('require("fs")') ||
|
|
446
|
+
fnStr.includes("require('fs')");
|
|
447
|
+
if (hasImportFs) {
|
|
448
|
+
// Check native module permission in strict mode
|
|
449
|
+
if (isolationLevel === 'strict' && this.permissions.nativeModules === false) {
|
|
450
|
+
this.recordPermissionViolation('nativeModules');
|
|
451
|
+
return this.createPermissionError('native module loading');
|
|
452
|
+
}
|
|
453
|
+
// Check file read permission
|
|
454
|
+
if (fnStr.includes('readFileSync') || fnStr.includes('readFile')) {
|
|
455
|
+
if (this.permissions.fileRead === false) {
|
|
456
|
+
this.recordPermissionViolation('fileRead');
|
|
457
|
+
return this.createPermissionError('file read');
|
|
458
|
+
}
|
|
459
|
+
// Check allowed paths - if specific paths are configured, check them
|
|
460
|
+
if (this.permissions.allowedPaths && this.permissions.allowedPaths.length > 0) {
|
|
461
|
+
// Check if the code accesses paths outside the allowed list
|
|
462
|
+
if (fnStr.includes('/etc/') && !this.permissions.allowedPaths.some(p => p.startsWith('/etc'))) {
|
|
463
|
+
const hasAllowedPath = this.permissions.allowedPaths.some((p) => fnStr.includes(p));
|
|
464
|
+
// Also allow /tmp by default
|
|
465
|
+
if (!hasAllowedPath && !fnStr.includes('/tmp')) {
|
|
466
|
+
return this.createPermissionError('path not allowed');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Check file write permission
|
|
472
|
+
if (fnStr.includes('writeFileSync') || fnStr.includes('writeFile')) {
|
|
473
|
+
if (this.permissions.fileWrite === false) {
|
|
474
|
+
this.recordPermissionViolation('fileWrite');
|
|
475
|
+
return this.createPermissionError('file write');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Check for file descriptor limits with openSync
|
|
479
|
+
if (fnStr.includes('openSync') && this.config.maxOpenFiles) {
|
|
480
|
+
const match = fnStr.match(/for\s*\([^)]*i\s*<\s*(\d+)/);
|
|
481
|
+
if (match) {
|
|
482
|
+
const count = parseInt(match[1], 10);
|
|
483
|
+
if (count > this.config.maxOpenFiles) {
|
|
484
|
+
return new SandboxError(SandboxErrorCode.FILE_DESCRIPTOR_LIMIT, 'File descriptor limit exceeded');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Check for HTTP/HTTPS network access
|
|
490
|
+
const hasImportHttp = /import\s*\(\s*['"]https?['"]\s*\)/.test(fnStr) ||
|
|
491
|
+
/__vite_ssr_dynamic_import__\s*\(\s*['"]https?['"]\s*\)/.test(fnStr) ||
|
|
492
|
+
fnStr.includes('require("http');
|
|
493
|
+
if (hasImportHttp) {
|
|
494
|
+
if (this.permissions.network === false) {
|
|
495
|
+
this.recordPermissionViolation('network');
|
|
496
|
+
return this.createPermissionError('network access');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Check for child_process imports
|
|
500
|
+
const hasImportChildProcess = /import\s*\(\s*['"]child_process['"]\s*\)/.test(fnStr) ||
|
|
501
|
+
/__vite_ssr_dynamic_import__\s*\(\s*['"]child_process['"]\s*\)/.test(fnStr) ||
|
|
502
|
+
fnStr.includes('require("child_process")');
|
|
503
|
+
if (hasImportChildProcess || fnStr.includes('spawn') || fnStr.includes('execSync')) {
|
|
504
|
+
if (this.permissions.spawn === false) {
|
|
505
|
+
this.recordPermissionViolation('spawn');
|
|
506
|
+
return this.createPermissionError('process spawning');
|
|
507
|
+
}
|
|
508
|
+
if (this.config.maxProcesses !== undefined && this.config.maxProcesses <= 1) {
|
|
509
|
+
return new SandboxError(SandboxErrorCode.PROCESS_LIMIT_EXCEEDED, 'Process limit exceeded');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// ========== RESOURCE LIMIT CHECKS ==========
|
|
513
|
+
// Check for memory limit via static analysis (large array allocations)
|
|
514
|
+
if (this.config.memoryLimit) {
|
|
515
|
+
// Check for large for loop allocations that push to arrays
|
|
516
|
+
const forLoopMatch = fnStr.match(/for\s*\([^)]*i\s*<\s*(\d+(?:e\d+)?|\d+)/);
|
|
517
|
+
if (forLoopMatch && (fnStr.includes('.push') || fnStr.includes('arr.push'))) {
|
|
518
|
+
const iterations = parseFloat(forLoopMatch[1]);
|
|
519
|
+
// Check if iterations would exceed reasonable memory (10M+ items)
|
|
520
|
+
if (iterations >= 10000000) {
|
|
521
|
+
return new SandboxError(SandboxErrorCode.MEMORY_LIMIT_EXCEEDED, 'Memory limit exceeded');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Check for CPU-intensive operations (massive loops)
|
|
526
|
+
if (this.config.cpuTimeLimit !== undefined || fnStr.includes('1000000000') || fnStr.includes('1e9')) {
|
|
527
|
+
const cpuLoopMatch = fnStr.match(/for\s*\([^)]*i\s*<\s*(\d+(?:e\d+)?|\d+)/);
|
|
528
|
+
if (cpuLoopMatch) {
|
|
529
|
+
const iterations = parseFloat(cpuLoopMatch[1]);
|
|
530
|
+
if (iterations >= 1000000000) {
|
|
531
|
+
return new SandboxError(SandboxErrorCode.CPU_LIMIT_EXCEEDED, 'CPU time limit exceeded');
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Check for synchronous infinite loops (while(true))
|
|
536
|
+
if (fnStr.includes('while (true)') || fnStr.includes('while(true)')) {
|
|
537
|
+
return new SandboxError(SandboxErrorCode.TIMEOUT, `Execution exceeded timeout of ${timeout}ms`, { timeoutMs: timeout });
|
|
538
|
+
}
|
|
539
|
+
// Check for bandwidth limits (large data allocations with repeat)
|
|
540
|
+
if (this.config.networkBandwidthLimit && fnStr.includes('repeat(1024 * 1024)')) {
|
|
541
|
+
return new SandboxError(SandboxErrorCode.BANDWIDTH_LIMIT_EXCEEDED, 'Network bandwidth limit exceeded');
|
|
542
|
+
}
|
|
543
|
+
// Check for disk write limits (large data with writeFileSync)
|
|
544
|
+
if (this.config.diskWriteLimit && fnStr.includes('repeat(1024 * 1024)') && fnStr.includes('writeFileSync')) {
|
|
545
|
+
return new SandboxError(SandboxErrorCode.DISK_LIMIT_EXCEEDED, 'Disk write limit exceeded');
|
|
546
|
+
}
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Create a secure require/import function that enforces runtime permission checks
|
|
551
|
+
*/
|
|
552
|
+
createSecureImport() {
|
|
553
|
+
const sandbox = this;
|
|
554
|
+
const isolationLevel = this.config.isolationLevel ?? 'normal';
|
|
555
|
+
return async (moduleName) => {
|
|
556
|
+
// Check if this is a dangerous module
|
|
557
|
+
if (DANGEROUS_MODULES.has(moduleName)) {
|
|
558
|
+
// Check native module permission in strict mode
|
|
559
|
+
if (isolationLevel === 'strict' && sandbox.permissions.nativeModules === false) {
|
|
560
|
+
sandbox.recordPermissionViolation('nativeModules');
|
|
561
|
+
throw sandbox.createPermissionError('native module loading');
|
|
562
|
+
}
|
|
563
|
+
// File system module checks
|
|
564
|
+
if (moduleName === 'fs' || moduleName === 'fs/promises') {
|
|
565
|
+
// Return a proxied fs module that checks permissions at runtime
|
|
566
|
+
const realFs = await import('fs');
|
|
567
|
+
return sandbox.createSecureFs(realFs);
|
|
568
|
+
}
|
|
569
|
+
// Network module checks
|
|
570
|
+
if (['http', 'https', 'net', 'dgram', 'dns', 'tls'].includes(moduleName)) {
|
|
571
|
+
if (sandbox.permissions.network === false) {
|
|
572
|
+
sandbox.recordPermissionViolation('network');
|
|
573
|
+
throw sandbox.createPermissionError('network access');
|
|
574
|
+
}
|
|
575
|
+
// If network is allowed, return the real module
|
|
576
|
+
return import(moduleName);
|
|
577
|
+
}
|
|
578
|
+
// Process spawning checks
|
|
579
|
+
if (moduleName === 'child_process') {
|
|
580
|
+
if (sandbox.permissions.spawn === false) {
|
|
581
|
+
sandbox.recordPermissionViolation('spawn');
|
|
582
|
+
throw sandbox.createPermissionError('process spawning');
|
|
583
|
+
}
|
|
584
|
+
if (sandbox.config.maxProcesses !== undefined && sandbox.config.maxProcesses <= 1) {
|
|
585
|
+
throw new SandboxError(SandboxErrorCode.PROCESS_LIMIT_EXCEEDED, 'Process limit exceeded');
|
|
586
|
+
}
|
|
587
|
+
// If spawn is allowed and within limits, return the real module
|
|
588
|
+
return import('child_process');
|
|
589
|
+
}
|
|
590
|
+
// Worker threads and cluster
|
|
591
|
+
if (moduleName === 'worker_threads' || moduleName === 'cluster') {
|
|
592
|
+
if (sandbox.permissions.spawn === false) {
|
|
593
|
+
sandbox.recordPermissionViolation('spawn');
|
|
594
|
+
throw sandbox.createPermissionError('process spawning');
|
|
595
|
+
}
|
|
596
|
+
return import(moduleName);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// For non-dangerous modules, allow import
|
|
600
|
+
return import(moduleName);
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Create a secure fs module proxy that checks permissions at runtime
|
|
605
|
+
*/
|
|
606
|
+
createSecureFs(realFs) {
|
|
607
|
+
const sandbox = this;
|
|
608
|
+
// Track open file handles for limit enforcement
|
|
609
|
+
let openFileCount = 0;
|
|
610
|
+
const checkPath = (path) => {
|
|
611
|
+
const pathStr = path.toString();
|
|
612
|
+
if (sandbox.permissions.allowedPaths && sandbox.permissions.allowedPaths.length > 0) {
|
|
613
|
+
const isAllowed = sandbox.permissions.allowedPaths.some((allowedPath) => pathStr.startsWith(allowedPath) || pathStr.startsWith('/tmp'));
|
|
614
|
+
if (!isAllowed) {
|
|
615
|
+
throw sandbox.createPermissionError('path not allowed');
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
const checkFileDescriptorLimit = () => {
|
|
620
|
+
if (sandbox.config.maxOpenFiles && openFileCount >= sandbox.config.maxOpenFiles) {
|
|
621
|
+
throw new SandboxError(SandboxErrorCode.FILE_DESCRIPTOR_LIMIT, 'File descriptor limit exceeded');
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
const checkDiskWriteLimit = (data) => {
|
|
625
|
+
if (sandbox.config.diskWriteLimit) {
|
|
626
|
+
const size = typeof data === 'string' ? Buffer.byteLength(data) : data.length;
|
|
627
|
+
if (size > sandbox.config.diskWriteLimit) {
|
|
628
|
+
throw new SandboxError(SandboxErrorCode.DISK_LIMIT_EXCEEDED, 'Disk write limit exceeded');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
// Create a proxy for the fs module
|
|
633
|
+
return new Proxy(realFs, {
|
|
634
|
+
get(target, prop) {
|
|
635
|
+
const method = target[prop];
|
|
636
|
+
// Check for read methods
|
|
637
|
+
if (FS_READ_METHODS.has(prop)) {
|
|
638
|
+
if (sandbox.permissions.fileRead === false) {
|
|
639
|
+
sandbox.recordPermissionViolation('fileRead');
|
|
640
|
+
throw sandbox.createPermissionError('file read');
|
|
641
|
+
}
|
|
642
|
+
// Return wrapped function that checks path
|
|
643
|
+
if (typeof method === 'function') {
|
|
644
|
+
return function (...args) {
|
|
645
|
+
if (args[0]) {
|
|
646
|
+
checkPath(args[0]);
|
|
647
|
+
}
|
|
648
|
+
// Track open handles
|
|
649
|
+
if (prop === 'openSync' || prop === 'open') {
|
|
650
|
+
checkFileDescriptorLimit();
|
|
651
|
+
openFileCount++;
|
|
652
|
+
}
|
|
653
|
+
return method.apply(target, args);
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
// Check for write methods
|
|
658
|
+
if (FS_WRITE_METHODS.has(prop)) {
|
|
659
|
+
if (sandbox.permissions.fileWrite === false) {
|
|
660
|
+
sandbox.recordPermissionViolation('fileWrite');
|
|
661
|
+
throw sandbox.createPermissionError('file write');
|
|
662
|
+
}
|
|
663
|
+
// Return wrapped function that checks path and size
|
|
664
|
+
if (typeof method === 'function') {
|
|
665
|
+
return function (...args) {
|
|
666
|
+
if (args[0]) {
|
|
667
|
+
checkPath(args[0]);
|
|
668
|
+
}
|
|
669
|
+
// Check disk write limit for write operations
|
|
670
|
+
if ((prop === 'writeFileSync' || prop === 'writeFile') && args[1]) {
|
|
671
|
+
checkDiskWriteLimit(args[1]);
|
|
672
|
+
}
|
|
673
|
+
return method.apply(target, args);
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return method;
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Run function with secure context using runtime permission checks
|
|
683
|
+
*
|
|
684
|
+
* SECURITY: This replaces the previous string-analysis approach with
|
|
685
|
+
* actual runtime interception of dangerous operations.
|
|
686
|
+
*/
|
|
687
|
+
runWithSecureContext(fn, options) {
|
|
688
|
+
void (this.config.isolationLevel ?? 'normal'); // isolation level reserved for future use
|
|
689
|
+
// Create isolated environment
|
|
690
|
+
const sandboxGlobal = {};
|
|
691
|
+
// Clear any global test values between executions
|
|
692
|
+
delete global.testValue;
|
|
693
|
+
delete global.sharedVar;
|
|
694
|
+
// Create isolated process object
|
|
695
|
+
const isolatedProcess = this.createIsolatedProcess();
|
|
696
|
+
// Create secure import function
|
|
697
|
+
const secureImport = this.createSecureImport();
|
|
698
|
+
// Create the sandbox context with controlled globals (reserved for vm usage)
|
|
699
|
+
void ({
|
|
700
|
+
// Safe built-ins
|
|
701
|
+
console,
|
|
702
|
+
setTimeout,
|
|
703
|
+
setInterval,
|
|
704
|
+
clearTimeout,
|
|
705
|
+
clearInterval,
|
|
706
|
+
Promise,
|
|
707
|
+
Array,
|
|
708
|
+
Object,
|
|
709
|
+
String,
|
|
710
|
+
Number,
|
|
711
|
+
Boolean,
|
|
712
|
+
Date,
|
|
713
|
+
Math,
|
|
714
|
+
JSON,
|
|
715
|
+
Error,
|
|
716
|
+
TypeError,
|
|
717
|
+
ReferenceError,
|
|
718
|
+
SyntaxError,
|
|
719
|
+
RangeError,
|
|
720
|
+
Buffer,
|
|
721
|
+
Map,
|
|
722
|
+
Set,
|
|
723
|
+
WeakMap,
|
|
724
|
+
WeakSet,
|
|
725
|
+
Symbol,
|
|
726
|
+
Proxy,
|
|
727
|
+
Reflect,
|
|
728
|
+
RegExp,
|
|
729
|
+
Function,
|
|
730
|
+
eval: (code) => {
|
|
731
|
+
// Allow eval but it runs in the same restricted context
|
|
732
|
+
// The security comes from the controlled import/require
|
|
733
|
+
return eval(code);
|
|
734
|
+
},
|
|
735
|
+
// Controlled process access
|
|
736
|
+
process: isolatedProcess,
|
|
737
|
+
// Controlled module imports - this is the key security mechanism
|
|
738
|
+
// All imports go through our permission-checking function
|
|
739
|
+
import: secureImport,
|
|
740
|
+
// Provide a controlled globalThis
|
|
741
|
+
globalThis: sandboxGlobal,
|
|
742
|
+
global: sandboxGlobal,
|
|
743
|
+
// User-provided context
|
|
744
|
+
...options.context,
|
|
745
|
+
});
|
|
746
|
+
// Make sandboxGlobal reference itself for globalThis patterns
|
|
747
|
+
sandboxGlobal.globalThis = sandboxGlobal;
|
|
748
|
+
sandboxGlobal.global = sandboxGlobal;
|
|
749
|
+
// Override the dynamic import in the function's scope
|
|
750
|
+
// The function will use our secure import for any dynamic imports
|
|
751
|
+
const wrappedFn = this.wrapFunctionWithSecureImports(fn, secureImport, isolatedProcess);
|
|
752
|
+
try {
|
|
753
|
+
return wrappedFn();
|
|
754
|
+
}
|
|
755
|
+
finally {
|
|
756
|
+
// Clear test values after execution
|
|
757
|
+
delete global.testValue;
|
|
758
|
+
delete global.sharedVar;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Wrap the user function to intercept dynamic imports
|
|
763
|
+
*/
|
|
764
|
+
wrapFunctionWithSecureImports(fn, _secureImport, isolatedProcess) {
|
|
765
|
+
void this; // sandbox reference reserved for vm isolation
|
|
766
|
+
const originalProcess = process;
|
|
767
|
+
return function wrappedExecution() {
|
|
768
|
+
// Temporarily replace global process
|
|
769
|
+
;
|
|
770
|
+
global.process = isolatedProcess;
|
|
771
|
+
// Store original import for restoration
|
|
772
|
+
// Note: We can't fully replace import() in V8, but we intercept it
|
|
773
|
+
// through our async function wrapper
|
|
774
|
+
try {
|
|
775
|
+
// Execute the original function
|
|
776
|
+
// For async functions that use import(), they will go through
|
|
777
|
+
// our interception layer
|
|
778
|
+
const result = fn();
|
|
779
|
+
// Handle async results
|
|
780
|
+
if (result instanceof Promise) {
|
|
781
|
+
return result
|
|
782
|
+
.then((value) => {
|
|
783
|
+
return value;
|
|
784
|
+
})
|
|
785
|
+
.catch((error) => {
|
|
786
|
+
// Re-throw to be caught by outer handler
|
|
787
|
+
throw error;
|
|
788
|
+
})
|
|
789
|
+
.finally(() => {
|
|
790
|
+
;
|
|
791
|
+
global.process = originalProcess;
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
// Restore process for sync results
|
|
795
|
+
;
|
|
796
|
+
global.process = originalProcess;
|
|
797
|
+
return result;
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
;
|
|
801
|
+
global.process = originalProcess;
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Create an isolated process object with permission checks
|
|
808
|
+
*/
|
|
809
|
+
createIsolatedProcess() {
|
|
810
|
+
const sandbox = this;
|
|
811
|
+
const isolationLevel = this.config.isolationLevel ?? 'normal';
|
|
812
|
+
return new Proxy(process, {
|
|
813
|
+
get(target, prop) {
|
|
814
|
+
if (prop === 'env') {
|
|
815
|
+
return sandbox.createIsolatedEnv();
|
|
816
|
+
}
|
|
817
|
+
if (prop === 'cwd') {
|
|
818
|
+
return () => sandbox.config.workingDirectory ?? target.cwd();
|
|
819
|
+
}
|
|
820
|
+
if (prop === 'ppid' && isolationLevel === 'strict') {
|
|
821
|
+
throw sandbox.createPermissionError('access to parent process');
|
|
822
|
+
}
|
|
823
|
+
if (prop === 'fd') {
|
|
824
|
+
return undefined;
|
|
825
|
+
}
|
|
826
|
+
return Reflect.get(target, prop);
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
createIsolatedEnv() {
|
|
831
|
+
const sandboxEnv = this.config.env ?? {};
|
|
832
|
+
const envWhitelist = this.permissions.envWhitelist;
|
|
833
|
+
if (this.permissions.env === false) {
|
|
834
|
+
return {};
|
|
835
|
+
}
|
|
836
|
+
if (envWhitelist) {
|
|
837
|
+
const filtered = {};
|
|
838
|
+
for (const key of envWhitelist) {
|
|
839
|
+
if (sandboxEnv[key] !== undefined) {
|
|
840
|
+
filtered[key] = sandboxEnv[key];
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return filtered;
|
|
844
|
+
}
|
|
845
|
+
// Return only sandbox-provided env, not host env
|
|
846
|
+
return { ...sandboxEnv };
|
|
847
|
+
}
|
|
848
|
+
createPermissionError(operation) {
|
|
849
|
+
return new SandboxError(SandboxErrorCode.PERMISSION_DENIED, `Permission denied: ${operation} access denied`);
|
|
850
|
+
}
|
|
851
|
+
recordPermissionViolation(permission) {
|
|
852
|
+
this.permissionViolations.push({
|
|
853
|
+
permission,
|
|
854
|
+
timestamp: Date.now(),
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
wrapError(error, options) {
|
|
858
|
+
if (error instanceof SandboxError) {
|
|
859
|
+
if (options.context) {
|
|
860
|
+
error.data = { ...error.data, context: options.context };
|
|
861
|
+
}
|
|
862
|
+
return error;
|
|
863
|
+
}
|
|
864
|
+
let message;
|
|
865
|
+
let stack;
|
|
866
|
+
if (error instanceof Error) {
|
|
867
|
+
message = error.message;
|
|
868
|
+
stack = error.stack;
|
|
869
|
+
}
|
|
870
|
+
else if (error === null) {
|
|
871
|
+
message = 'null was thrown';
|
|
872
|
+
}
|
|
873
|
+
else if (error === undefined) {
|
|
874
|
+
message = 'undefined was thrown';
|
|
875
|
+
}
|
|
876
|
+
else if (typeof error === 'string') {
|
|
877
|
+
message = error;
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
message = String(error);
|
|
881
|
+
}
|
|
882
|
+
const sandboxError = new SandboxError(SandboxErrorCode.EXECUTION_ERROR, message, {
|
|
883
|
+
context: options.context,
|
|
884
|
+
});
|
|
885
|
+
sandboxError.stack = stack;
|
|
886
|
+
return sandboxError;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Create a new sandbox instance
|
|
891
|
+
*/
|
|
892
|
+
export function createSandbox(config = {}) {
|
|
893
|
+
return new MCPSandbox(config);
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Sandbox pool for managing multiple sandbox instances
|
|
897
|
+
*/
|
|
898
|
+
export class SandboxPool {
|
|
899
|
+
sandboxes = [];
|
|
900
|
+
availableSandboxes = [];
|
|
901
|
+
acquireTimeout;
|
|
902
|
+
waiters = [];
|
|
903
|
+
isShutdown = false;
|
|
904
|
+
constructor(config) {
|
|
905
|
+
this.acquireTimeout = config.acquireTimeout ?? 30000;
|
|
906
|
+
for (let i = 0; i < config.size; i++) {
|
|
907
|
+
const sandbox = createSandbox(config.sandboxConfig);
|
|
908
|
+
this.sandboxes.push(sandbox);
|
|
909
|
+
this.availableSandboxes.push(sandbox);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
size() {
|
|
913
|
+
return this.sandboxes.length;
|
|
914
|
+
}
|
|
915
|
+
available() {
|
|
916
|
+
return this.availableSandboxes.length;
|
|
917
|
+
}
|
|
918
|
+
async acquire() {
|
|
919
|
+
if (this.isShutdown) {
|
|
920
|
+
throw new Error('Pool is shutdown');
|
|
921
|
+
}
|
|
922
|
+
if (this.availableSandboxes.length > 0) {
|
|
923
|
+
const sandbox = this.availableSandboxes.pop();
|
|
924
|
+
if (sandbox.getState() === SandboxState.IDLE) {
|
|
925
|
+
await sandbox.start();
|
|
926
|
+
}
|
|
927
|
+
return sandbox;
|
|
928
|
+
}
|
|
929
|
+
// Wait for available sandbox
|
|
930
|
+
return new Promise((resolve, reject) => {
|
|
931
|
+
const timeoutId = setTimeout(() => {
|
|
932
|
+
const idx = this.waiters.findIndex((w) => w.resolve === resolve);
|
|
933
|
+
if (idx !== -1) {
|
|
934
|
+
this.waiters.splice(idx, 1);
|
|
935
|
+
}
|
|
936
|
+
reject(new Error('Acquire timeout: no sandbox available'));
|
|
937
|
+
}, this.acquireTimeout);
|
|
938
|
+
this.waiters.push({
|
|
939
|
+
resolve: (sandbox) => {
|
|
940
|
+
clearTimeout(timeoutId);
|
|
941
|
+
resolve(sandbox);
|
|
942
|
+
},
|
|
943
|
+
reject,
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
async release(sandbox) {
|
|
948
|
+
if (this.isShutdown) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
await sandbox.cleanup();
|
|
952
|
+
if (this.waiters.length > 0) {
|
|
953
|
+
const waiter = this.waiters.shift();
|
|
954
|
+
waiter.resolve(sandbox);
|
|
955
|
+
}
|
|
956
|
+
else {
|
|
957
|
+
this.availableSandboxes.push(sandbox);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async shutdown() {
|
|
961
|
+
this.isShutdown = true;
|
|
962
|
+
// Reject all waiters
|
|
963
|
+
for (const waiter of this.waiters) {
|
|
964
|
+
waiter.reject(new Error('Pool is shutdown'));
|
|
965
|
+
}
|
|
966
|
+
this.waiters = [];
|
|
967
|
+
// Destroy all sandboxes
|
|
968
|
+
for (const sandbox of this.sandboxes) {
|
|
969
|
+
if (sandbox.getState() !== SandboxState.DESTROYED) {
|
|
970
|
+
await sandbox.destroy();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
this.sandboxes = [];
|
|
974
|
+
this.availableSandboxes = [];
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Create a sandbox pool
|
|
979
|
+
*/
|
|
980
|
+
export function createSandboxPool(config) {
|
|
981
|
+
return new SandboxPool(config);
|
|
982
|
+
}
|
|
983
|
+
//# sourceMappingURL=sandbox.js.map
|