@mastra/vercel 1.0.0 → 1.0.2
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/CHANGELOG.md +103 -0
- package/LICENSE.md +30 -0
- package/dist/executor/index.d.ts +12 -0
- package/dist/executor/index.d.ts.map +1 -0
- package/dist/index.cjs +837 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +831 -0
- package/dist/index.js.map +1 -0
- package/dist/microvm/index.d.ts +118 -0
- package/dist/microvm/index.d.ts.map +1 -0
- package/dist/microvm/process-manager.d.ts +25 -0
- package/dist/microvm/process-manager.d.ts.map +1 -0
- package/dist/microvm-provider.d.ts +29 -0
- package/dist/microvm-provider.d.ts.map +1 -0
- package/dist/provider.d.ts +29 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/sandbox/index.d.ts +73 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/package.json +55 -51
- package/src/Vercel.test.ts +0 -61
- package/src/assets/vercel.png +0 -0
- package/src/index.ts +0 -67
- package/src/openapi-components.ts +0 -4184
- package/src/openapi-paths.ts +0 -52314
- package/src/openapi.ts +0 -28072
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,837 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var workspace = require('@mastra/core/workspace');
|
|
4
|
+
var sandbox = require('@vercel/sandbox');
|
|
5
|
+
|
|
6
|
+
// src/sandbox/index.ts
|
|
7
|
+
|
|
8
|
+
// src/executor/index.ts
|
|
9
|
+
function getExecutorSource(secret, env) {
|
|
10
|
+
const envEntries = Object.entries(env).map(([k, v]) => ` ${JSON.stringify(k)}: ${JSON.stringify(v)}`).join(",\n");
|
|
11
|
+
return `
|
|
12
|
+
const { execFileSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
const SANDBOX_SECRET = ${JSON.stringify(secret)};
|
|
15
|
+
const SANDBOX_ENV = {
|
|
16
|
+
${envEntries}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
module.exports = async (req, res) => {
|
|
20
|
+
// Auth check
|
|
21
|
+
const authHeader = req.headers['authorization'] || '';
|
|
22
|
+
if (!SANDBOX_SECRET || authHeader !== 'Bearer ' + SANDBOX_SECRET) {
|
|
23
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (req.method !== 'POST') {
|
|
27
|
+
return res.status(405).json({ error: 'Method not allowed' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const { command, args = [], env = {}, cwd, timeout = 55000 } = req.body || {};
|
|
31
|
+
|
|
32
|
+
if (!command) {
|
|
33
|
+
return res.status(400).json({ error: 'Missing required field: command' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const execCwd = cwd || '/tmp';
|
|
37
|
+
const execEnv = { ...process.env, ...SANDBOX_ENV, ...env };
|
|
38
|
+
|
|
39
|
+
// When args is empty the caller sent a full shell command string
|
|
40
|
+
// (e.g. "echo hello" or "ls -la | grep foo"). Run it through
|
|
41
|
+
// /bin/sh so builtins and pipes work. When args is non-empty the
|
|
42
|
+
// caller split the command properly \u2014 use execFileSync to avoid
|
|
43
|
+
// shell injection.
|
|
44
|
+
const useShell = !args || args.length === 0;
|
|
45
|
+
const execCommand = useShell ? '/bin/sh' : command;
|
|
46
|
+
const execArgs = useShell ? ['-c', command] : args;
|
|
47
|
+
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
let timedOut = false;
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const stdout = execFileSync(execCommand, execArgs, {
|
|
53
|
+
cwd: execCwd,
|
|
54
|
+
env: execEnv,
|
|
55
|
+
timeout,
|
|
56
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return res.status(200).json({
|
|
62
|
+
success: true,
|
|
63
|
+
exitCode: 0,
|
|
64
|
+
stdout: stdout || '',
|
|
65
|
+
stderr: '',
|
|
66
|
+
executionTimeMs: Date.now() - startTime,
|
|
67
|
+
timedOut: false,
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
timedOut = error.killed || false;
|
|
71
|
+
|
|
72
|
+
return res.status(200).json({
|
|
73
|
+
success: false,
|
|
74
|
+
exitCode: error.status != null ? error.status : 1,
|
|
75
|
+
stdout: error.stdout || '',
|
|
76
|
+
stderr: error.stderr || '',
|
|
77
|
+
executionTimeMs: Date.now() - startTime,
|
|
78
|
+
timedOut,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/sandbox/index.ts
|
|
86
|
+
var LOG_PREFIX = "[VercelSandbox]";
|
|
87
|
+
var VERCEL_API_BASE = "https://api.vercel.com";
|
|
88
|
+
function shellQuote(arg) {
|
|
89
|
+
if (/^[a-zA-Z0-9._\-\/=:@]+$/.test(arg)) return arg;
|
|
90
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
91
|
+
}
|
|
92
|
+
var VercelSandbox = class extends workspace.MastraSandbox {
|
|
93
|
+
id;
|
|
94
|
+
name = "VercelSandbox";
|
|
95
|
+
provider = "vercel";
|
|
96
|
+
status = "pending";
|
|
97
|
+
_token;
|
|
98
|
+
_teamId;
|
|
99
|
+
_projectName;
|
|
100
|
+
_regions;
|
|
101
|
+
_maxDuration;
|
|
102
|
+
_memory;
|
|
103
|
+
_env;
|
|
104
|
+
_commandTimeout;
|
|
105
|
+
_instructionsOverride;
|
|
106
|
+
_secret;
|
|
107
|
+
_deploymentUrl = null;
|
|
108
|
+
_deploymentId = null;
|
|
109
|
+
_protectionBypass = null;
|
|
110
|
+
_createdAt = null;
|
|
111
|
+
constructor(options = {}) {
|
|
112
|
+
super({ name: "VercelSandbox" });
|
|
113
|
+
this.id = `vercel-sandbox-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
114
|
+
this._token = options.token || process.env.VERCEL_TOKEN || "";
|
|
115
|
+
this._teamId = options.teamId;
|
|
116
|
+
this._projectName = options.projectName;
|
|
117
|
+
this._regions = options.regions ?? ["iad1"];
|
|
118
|
+
this._maxDuration = options.maxDuration ?? 60;
|
|
119
|
+
this._memory = options.memory ?? 1024;
|
|
120
|
+
this._env = options.env ?? {};
|
|
121
|
+
this._commandTimeout = options.commandTimeout ?? 55e3;
|
|
122
|
+
this._instructionsOverride = options.instructions;
|
|
123
|
+
this._secret = crypto.randomUUID();
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Lifecycle
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
async start() {
|
|
129
|
+
if (this._deploymentUrl) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!this._token) {
|
|
133
|
+
throw new Error(`${LOG_PREFIX} Missing Vercel API token. Set VERCEL_TOKEN env var or pass token option.`);
|
|
134
|
+
}
|
|
135
|
+
if (this._deploymentId) {
|
|
136
|
+
this.logger.debug(`${LOG_PREFIX} Cleaning up stale deployment ${this._deploymentId} before restarting...`);
|
|
137
|
+
try {
|
|
138
|
+
const resp = await this._vercelFetch(`/v13/deployments/${this._deploymentId}`, { method: "DELETE" });
|
|
139
|
+
if (!resp.ok && resp.status !== 404) {
|
|
140
|
+
this.logger.warn(`${LOG_PREFIX} Failed to delete stale deployment: ${resp.status}`);
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
this.logger.warn(`${LOG_PREFIX} Error deleting stale deployment:`, error);
|
|
144
|
+
}
|
|
145
|
+
this._deploymentId = null;
|
|
146
|
+
}
|
|
147
|
+
this.logger.debug(`${LOG_PREFIX} Deploying executor function...`);
|
|
148
|
+
const vercelJson = JSON.stringify({
|
|
149
|
+
functions: {
|
|
150
|
+
"api/execute.js": {
|
|
151
|
+
memory: this._memory,
|
|
152
|
+
maxDuration: this._maxDuration
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
regions: this._regions
|
|
156
|
+
});
|
|
157
|
+
const deploymentBody = {
|
|
158
|
+
name: this._projectName ?? `mastra-sandbox-${this.id}`,
|
|
159
|
+
files: [
|
|
160
|
+
{
|
|
161
|
+
file: "api/execute.js",
|
|
162
|
+
data: getExecutorSource(this._secret, this._env)
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
file: "vercel.json",
|
|
166
|
+
data: vercelJson
|
|
167
|
+
}
|
|
168
|
+
],
|
|
169
|
+
projectSettings: {
|
|
170
|
+
framework: null
|
|
171
|
+
},
|
|
172
|
+
target: "production"
|
|
173
|
+
};
|
|
174
|
+
const createResp = await this._vercelFetch("/v13/deployments", {
|
|
175
|
+
method: "POST",
|
|
176
|
+
body: JSON.stringify(deploymentBody)
|
|
177
|
+
});
|
|
178
|
+
if (!createResp.ok) {
|
|
179
|
+
const errorBody = await createResp.text();
|
|
180
|
+
throw new Error(`${LOG_PREFIX} Failed to create deployment: ${createResp.status} ${errorBody}`);
|
|
181
|
+
}
|
|
182
|
+
const deployment = await createResp.json();
|
|
183
|
+
this._deploymentId = deployment.id;
|
|
184
|
+
this.logger.debug(`${LOG_PREFIX} Deployment created: ${deployment.id}, polling for READY...`);
|
|
185
|
+
const maxWaitMs = 12e4;
|
|
186
|
+
const pollIntervalMs = 3e3;
|
|
187
|
+
const deadline = Date.now() + maxWaitMs;
|
|
188
|
+
while (Date.now() < deadline) {
|
|
189
|
+
const statusResp = await this._vercelFetch(`/v13/deployments/${deployment.id}`);
|
|
190
|
+
if (!statusResp.ok) {
|
|
191
|
+
throw new Error(`${LOG_PREFIX} Failed to check deployment status: ${statusResp.status}`);
|
|
192
|
+
}
|
|
193
|
+
const statusBody = await statusResp.json();
|
|
194
|
+
if (statusBody.readyState === "READY") {
|
|
195
|
+
this._deploymentUrl = `https://${statusBody.url}`;
|
|
196
|
+
this._createdAt = /* @__PURE__ */ new Date();
|
|
197
|
+
this.logger.debug(`${LOG_PREFIX} Deployment ready: ${this._deploymentUrl}`);
|
|
198
|
+
if (deployment.projectId) {
|
|
199
|
+
try {
|
|
200
|
+
this._protectionBypass = await this._acquireProtectionBypass(deployment.projectId);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
await fetch(`${this._deploymentUrl}/api/execute`, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: this._executorHeaders(),
|
|
208
|
+
body: JSON.stringify({ command: "echo", args: ["warm"] })
|
|
209
|
+
});
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (statusBody.readyState === "ERROR" || statusBody.readyState === "CANCELED") {
|
|
215
|
+
throw new Error(`${LOG_PREFIX} Deployment failed with state: ${statusBody.readyState}`);
|
|
216
|
+
}
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`${LOG_PREFIX} Deployment timed out after ${maxWaitMs}ms`);
|
|
220
|
+
}
|
|
221
|
+
async stop() {
|
|
222
|
+
this._deploymentUrl = null;
|
|
223
|
+
}
|
|
224
|
+
async destroy() {
|
|
225
|
+
if (this._deploymentId) {
|
|
226
|
+
try {
|
|
227
|
+
const resp = await this._vercelFetch(`/v13/deployments/${this._deploymentId}`, {
|
|
228
|
+
method: "DELETE"
|
|
229
|
+
});
|
|
230
|
+
if (!resp.ok) {
|
|
231
|
+
if (resp.status === 404) {
|
|
232
|
+
this.logger.debug(`${LOG_PREFIX} Deployment already deleted (404)`);
|
|
233
|
+
} else {
|
|
234
|
+
this.logger.warn(`${LOG_PREFIX} Failed to delete deployment: ${resp.status}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
this.logger.warn(`${LOG_PREFIX} Error deleting deployment:`, error);
|
|
239
|
+
}
|
|
240
|
+
this._deploymentId = null;
|
|
241
|
+
}
|
|
242
|
+
this._deploymentUrl = null;
|
|
243
|
+
this._protectionBypass = null;
|
|
244
|
+
}
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// Command Execution
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
async executeCommand(command, args, options) {
|
|
249
|
+
await this.ensureRunning();
|
|
250
|
+
if (!this._deploymentUrl) {
|
|
251
|
+
throw new workspace.SandboxNotReadyError(this.id);
|
|
252
|
+
}
|
|
253
|
+
const fullCommand = args?.length ? `${command} ${args.map((a) => shellQuote(a)).join(" ")}` : command;
|
|
254
|
+
this.logger.debug(`${LOG_PREFIX} Executing: ${fullCommand}`);
|
|
255
|
+
const body = {
|
|
256
|
+
command,
|
|
257
|
+
args: args ?? [],
|
|
258
|
+
env: options?.env ?? {},
|
|
259
|
+
cwd: options?.cwd ?? "/tmp",
|
|
260
|
+
timeout: options?.timeout ?? this._commandTimeout
|
|
261
|
+
};
|
|
262
|
+
const maxRetries = 2;
|
|
263
|
+
let lastError = null;
|
|
264
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
265
|
+
try {
|
|
266
|
+
const resp = await fetch(`${this._deploymentUrl}/api/execute`, {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: this._executorHeaders(),
|
|
269
|
+
body: JSON.stringify(body),
|
|
270
|
+
signal: options?.abortSignal
|
|
271
|
+
});
|
|
272
|
+
if ((resp.status === 429 || resp.status === 502 || resp.status === 503) && attempt < maxRetries) {
|
|
273
|
+
this.logger.debug(`${LOG_PREFIX} Retryable status ${resp.status}, attempt ${attempt + 1}/${maxRetries}`);
|
|
274
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * (attempt + 1)));
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (resp.status === 504) {
|
|
278
|
+
return {
|
|
279
|
+
command: fullCommand,
|
|
280
|
+
args,
|
|
281
|
+
success: false,
|
|
282
|
+
exitCode: 124,
|
|
283
|
+
stdout: "",
|
|
284
|
+
stderr: "Function execution timed out (504 Gateway Timeout)",
|
|
285
|
+
executionTimeMs: options?.timeout ?? this._commandTimeout,
|
|
286
|
+
timedOut: true
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (!resp.ok) {
|
|
290
|
+
const errorText = await resp.text();
|
|
291
|
+
throw new Error(`${LOG_PREFIX} Execute failed: ${resp.status} ${errorText}`);
|
|
292
|
+
}
|
|
293
|
+
const result = await resp.json();
|
|
294
|
+
if (options?.onStdout && result.stdout) {
|
|
295
|
+
options.onStdout(result.stdout);
|
|
296
|
+
}
|
|
297
|
+
if (options?.onStderr && result.stderr) {
|
|
298
|
+
options.onStderr(result.stderr);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
command: fullCommand,
|
|
302
|
+
args,
|
|
303
|
+
success: result.success,
|
|
304
|
+
exitCode: result.exitCode,
|
|
305
|
+
stdout: result.stdout,
|
|
306
|
+
stderr: result.stderr,
|
|
307
|
+
executionTimeMs: result.executionTimeMs,
|
|
308
|
+
timedOut: result.timedOut
|
|
309
|
+
};
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
lastError = error;
|
|
315
|
+
if (attempt < maxRetries) {
|
|
316
|
+
this.logger.debug(`${LOG_PREFIX} Request error, attempt ${attempt + 1}/${maxRetries}:`, error);
|
|
317
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3 * (attempt + 1)));
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
throw lastError ?? new Error(`${LOG_PREFIX} executeCommand failed after retries`);
|
|
323
|
+
}
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// Info & Instructions
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// Matches the resolveInstructions pattern in packages/core/src/workspace/utils.ts
|
|
328
|
+
getInstructions(opts) {
|
|
329
|
+
if (this._instructionsOverride === void 0) return this._getDefaultInstructions();
|
|
330
|
+
if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
|
|
331
|
+
const defaultInstructions = this._getDefaultInstructions();
|
|
332
|
+
return this._instructionsOverride({ defaultInstructions, requestContext: opts?.requestContext });
|
|
333
|
+
}
|
|
334
|
+
_getDefaultInstructions() {
|
|
335
|
+
return [
|
|
336
|
+
"Vercel serverless sandbox.",
|
|
337
|
+
"Limitations:",
|
|
338
|
+
"- Stateless: no persistent filesystem between invocations.",
|
|
339
|
+
"- No interactive shell or streaming stdin.",
|
|
340
|
+
"- No long-running or background processes.",
|
|
341
|
+
`- Maximum execution time: ${this._maxDuration} seconds.`,
|
|
342
|
+
"- Only /tmp is writable (ephemeral, cleared between invocations).",
|
|
343
|
+
"- Shell commands (pipes, builtins) are supported via /bin/sh -c."
|
|
344
|
+
].join("\n");
|
|
345
|
+
}
|
|
346
|
+
async getInfo() {
|
|
347
|
+
return {
|
|
348
|
+
id: this.id,
|
|
349
|
+
name: this.name,
|
|
350
|
+
provider: this.provider,
|
|
351
|
+
status: this.status,
|
|
352
|
+
createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
|
|
353
|
+
metadata: {
|
|
354
|
+
deploymentId: this._deploymentId,
|
|
355
|
+
deploymentUrl: this._deploymentUrl,
|
|
356
|
+
regions: this._regions,
|
|
357
|
+
maxDuration: this._maxDuration,
|
|
358
|
+
memory: this._memory
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// Private Helpers
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
/**
|
|
366
|
+
* Fetch an existing protection bypass token for the project, or create one
|
|
367
|
+
* if none exists. Returns the token string, or null if acquisition fails.
|
|
368
|
+
*/
|
|
369
|
+
async _acquireProtectionBypass(projectId) {
|
|
370
|
+
const projResp = await this._vercelFetch(`/v9/projects/${projectId}`);
|
|
371
|
+
if (projResp.ok) {
|
|
372
|
+
const project = await projResp.json();
|
|
373
|
+
const existing = project.protectionBypass ? Object.keys(project.protectionBypass)[0] : void 0;
|
|
374
|
+
if (existing) {
|
|
375
|
+
this.logger.debug(`${LOG_PREFIX} Using existing protection bypass token`);
|
|
376
|
+
return existing;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
this.logger.debug(`${LOG_PREFIX} Creating protection bypass token...`);
|
|
380
|
+
const createResp = await this._vercelFetch(`/v1/projects/${projectId}/protection-bypass`, {
|
|
381
|
+
method: "PATCH",
|
|
382
|
+
body: JSON.stringify({})
|
|
383
|
+
});
|
|
384
|
+
if (createResp.ok) {
|
|
385
|
+
const result = await createResp.json();
|
|
386
|
+
const created = result.protectionBypass ? Object.keys(result.protectionBypass)[0] : void 0;
|
|
387
|
+
if (created) {
|
|
388
|
+
this.logger.debug(`${LOG_PREFIX} Protection bypass token created`);
|
|
389
|
+
return created;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
this.logger.debug(`${LOG_PREFIX} Could not acquire protection bypass token (project may not require it)`);
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
_executorHeaders() {
|
|
396
|
+
const headers = {
|
|
397
|
+
"Content-Type": "application/json",
|
|
398
|
+
Authorization: `Bearer ${this._secret}`
|
|
399
|
+
};
|
|
400
|
+
if (this._protectionBypass) {
|
|
401
|
+
headers["x-vercel-protection-bypass"] = this._protectionBypass;
|
|
402
|
+
}
|
|
403
|
+
return headers;
|
|
404
|
+
}
|
|
405
|
+
async _vercelFetch(path, options = {}) {
|
|
406
|
+
const url = new URL(path, VERCEL_API_BASE);
|
|
407
|
+
if (this._teamId) {
|
|
408
|
+
url.searchParams.set("teamId", this._teamId);
|
|
409
|
+
}
|
|
410
|
+
return fetch(url.toString(), {
|
|
411
|
+
...options,
|
|
412
|
+
headers: {
|
|
413
|
+
"Content-Type": "application/json",
|
|
414
|
+
Authorization: `Bearer ${this._token}`,
|
|
415
|
+
...options.headers ?? {}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// src/provider.ts
|
|
422
|
+
var vercelSandboxProvider = {
|
|
423
|
+
id: "vercel",
|
|
424
|
+
name: "Vercel Sandbox",
|
|
425
|
+
description: "Serverless sandbox powered by Vercel Functions",
|
|
426
|
+
configSchema: {
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: {
|
|
429
|
+
token: { type: "string", description: "Vercel API token" },
|
|
430
|
+
teamId: { type: "string", description: "Vercel team ID" },
|
|
431
|
+
projectName: { type: "string", description: "Existing Vercel project name" },
|
|
432
|
+
regions: {
|
|
433
|
+
type: "array",
|
|
434
|
+
description: "Deployment regions",
|
|
435
|
+
items: { type: "string" },
|
|
436
|
+
default: ["iad1"]
|
|
437
|
+
},
|
|
438
|
+
maxDuration: { type: "number", description: "Function max duration in seconds", default: 60 },
|
|
439
|
+
memory: { type: "number", description: "Function memory in MB", default: 1024 },
|
|
440
|
+
env: {
|
|
441
|
+
type: "object",
|
|
442
|
+
description: "Environment variables",
|
|
443
|
+
additionalProperties: { type: "string" }
|
|
444
|
+
},
|
|
445
|
+
commandTimeout: { type: "number", description: "Per-invocation timeout in ms", default: 55e3 }
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
createSandbox: (config) => new VercelSandbox(config)
|
|
449
|
+
};
|
|
450
|
+
var VercelMicroVMProcessHandle = class extends workspace.ProcessHandle {
|
|
451
|
+
pid;
|
|
452
|
+
_command;
|
|
453
|
+
_startTime;
|
|
454
|
+
_timeout;
|
|
455
|
+
_exitCode;
|
|
456
|
+
_waitPromise = null;
|
|
457
|
+
_streamingPromise = null;
|
|
458
|
+
_killed = false;
|
|
459
|
+
constructor(command, startTime, options) {
|
|
460
|
+
super(options);
|
|
461
|
+
this.pid = command.cmdId;
|
|
462
|
+
this._command = command;
|
|
463
|
+
this._startTime = startTime;
|
|
464
|
+
this._timeout = options?.timeout;
|
|
465
|
+
}
|
|
466
|
+
get exitCode() {
|
|
467
|
+
return this._exitCode;
|
|
468
|
+
}
|
|
469
|
+
/** @internal Set by the process manager after streaming starts. */
|
|
470
|
+
set streamingPromise(p) {
|
|
471
|
+
this._streamingPromise = p;
|
|
472
|
+
}
|
|
473
|
+
async wait() {
|
|
474
|
+
if (!this._waitPromise) {
|
|
475
|
+
this._waitPromise = this._doWait();
|
|
476
|
+
}
|
|
477
|
+
return this._waitPromise;
|
|
478
|
+
}
|
|
479
|
+
async _doWait() {
|
|
480
|
+
const finishedPromise = this._command.wait().then((finished) => {
|
|
481
|
+
if (this._exitCode === void 0) this._exitCode = finished.exitCode;
|
|
482
|
+
}).catch(() => {
|
|
483
|
+
if (this._exitCode === void 0) this._exitCode = 1;
|
|
484
|
+
});
|
|
485
|
+
if (this._timeout) {
|
|
486
|
+
let timeoutId;
|
|
487
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
488
|
+
timeoutId = setTimeout(() => resolve("timeout"), this._timeout);
|
|
489
|
+
});
|
|
490
|
+
const outcome = await Promise.race([finishedPromise.then(() => "done"), timeoutPromise]);
|
|
491
|
+
clearTimeout(timeoutId);
|
|
492
|
+
if (outcome === "timeout") {
|
|
493
|
+
await this.kill();
|
|
494
|
+
this._exitCode = 124;
|
|
495
|
+
await this._streamingPromise?.catch(() => {
|
|
496
|
+
});
|
|
497
|
+
return {
|
|
498
|
+
success: false,
|
|
499
|
+
exitCode: 124,
|
|
500
|
+
stdout: this.stdout,
|
|
501
|
+
stderr: this.stderr || `Command timed out after ${this._timeout}ms`,
|
|
502
|
+
executionTimeMs: Date.now() - this._startTime,
|
|
503
|
+
killed: true,
|
|
504
|
+
timedOut: true
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
await finishedPromise;
|
|
509
|
+
}
|
|
510
|
+
await this._streamingPromise?.catch(() => {
|
|
511
|
+
});
|
|
512
|
+
if (this._killed) {
|
|
513
|
+
return {
|
|
514
|
+
success: false,
|
|
515
|
+
exitCode: this._exitCode ?? 137,
|
|
516
|
+
stdout: this.stdout,
|
|
517
|
+
stderr: this.stderr,
|
|
518
|
+
executionTimeMs: Date.now() - this._startTime,
|
|
519
|
+
killed: true,
|
|
520
|
+
timedOut: false
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
success: this._exitCode === 0,
|
|
525
|
+
exitCode: this._exitCode ?? 1,
|
|
526
|
+
stdout: this.stdout,
|
|
527
|
+
stderr: this.stderr,
|
|
528
|
+
executionTimeMs: Date.now() - this._startTime
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
async kill() {
|
|
532
|
+
if (this._exitCode !== void 0 && !this._killed) return false;
|
|
533
|
+
this._killed = true;
|
|
534
|
+
if (this._exitCode === void 0) this._exitCode = 137;
|
|
535
|
+
try {
|
|
536
|
+
await this._command.kill();
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
async sendStdin(_data) {
|
|
542
|
+
throw new Error("VercelMicroVMSandbox does not support sending stdin to running processes.");
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
var VercelMicroVMProcessManager = class extends workspace.SandboxProcessManager {
|
|
546
|
+
async spawn(command, options = {}) {
|
|
547
|
+
const mergedEnv = { ...this.env, ...options.env };
|
|
548
|
+
const env = Object.fromEntries(
|
|
549
|
+
Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
|
|
550
|
+
);
|
|
551
|
+
const cmd = await this.sandbox.sandbox.runCommand({
|
|
552
|
+
cmd: "sh",
|
|
553
|
+
args: ["-c", command],
|
|
554
|
+
...options.cwd ? { cwd: options.cwd } : {},
|
|
555
|
+
...Object.keys(env).length ? { env } : {},
|
|
556
|
+
detached: true
|
|
557
|
+
});
|
|
558
|
+
const handle = new VercelMicroVMProcessHandle(cmd, Date.now(), options);
|
|
559
|
+
const streamingPromise = (async () => {
|
|
560
|
+
for await (const log of cmd.logs()) {
|
|
561
|
+
if (log.stream === "stdout") handle.emitStdout(log.data);
|
|
562
|
+
else handle.emitStderr(log.data);
|
|
563
|
+
}
|
|
564
|
+
})().catch(() => {
|
|
565
|
+
});
|
|
566
|
+
handle.streamingPromise = streamingPromise;
|
|
567
|
+
this._tracked.set(handle.pid, handle);
|
|
568
|
+
return handle;
|
|
569
|
+
}
|
|
570
|
+
async list() {
|
|
571
|
+
const result = [];
|
|
572
|
+
for (const [pid, handle] of this._tracked) {
|
|
573
|
+
result.push({
|
|
574
|
+
pid,
|
|
575
|
+
command: handle.command,
|
|
576
|
+
running: handle.exitCode === void 0,
|
|
577
|
+
exitCode: handle.exitCode
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// src/microvm/index.ts
|
|
585
|
+
var LOG_PREFIX2 = "[VercelMicroVMSandbox]";
|
|
586
|
+
var VercelMicroVMSandbox = class extends workspace.MastraSandbox {
|
|
587
|
+
id;
|
|
588
|
+
name = "VercelMicroVMSandbox";
|
|
589
|
+
provider = "vercel-microvm";
|
|
590
|
+
status = "pending";
|
|
591
|
+
_sandbox = null;
|
|
592
|
+
_createdAt = null;
|
|
593
|
+
_sandboxName;
|
|
594
|
+
_token;
|
|
595
|
+
_teamId;
|
|
596
|
+
_projectId;
|
|
597
|
+
_runtime;
|
|
598
|
+
_timeout;
|
|
599
|
+
_vcpus;
|
|
600
|
+
_ports;
|
|
601
|
+
_env;
|
|
602
|
+
_metadata;
|
|
603
|
+
_instructionsOverride;
|
|
604
|
+
constructor(options = {}) {
|
|
605
|
+
super({
|
|
606
|
+
...options,
|
|
607
|
+
name: "VercelMicroVMSandbox",
|
|
608
|
+
processes: new VercelMicroVMProcessManager({ env: options.env ?? {} })
|
|
609
|
+
});
|
|
610
|
+
this.id = options.id ?? `vercel-microvm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
611
|
+
this._sandboxName = options.sandboxName;
|
|
612
|
+
this._token = options.token ?? process.env.VERCEL_TOKEN;
|
|
613
|
+
this._teamId = options.teamId ?? process.env.VERCEL_TEAM_ID;
|
|
614
|
+
this._projectId = options.projectId ?? process.env.VERCEL_PROJECT_ID;
|
|
615
|
+
this._runtime = options.runtime ?? "node24";
|
|
616
|
+
this._timeout = options.timeout ?? 3e5;
|
|
617
|
+
this._vcpus = options.resources?.vcpus;
|
|
618
|
+
this._ports = options.ports;
|
|
619
|
+
this._env = options.env ?? {};
|
|
620
|
+
this._metadata = options.metadata ?? {};
|
|
621
|
+
this._instructionsOverride = options.instructions;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* The underlying `@vercel/sandbox` instance.
|
|
625
|
+
* Throws if the sandbox has not been started yet.
|
|
626
|
+
*/
|
|
627
|
+
get sandbox() {
|
|
628
|
+
if (!this._sandbox) {
|
|
629
|
+
throw new workspace.SandboxNotReadyError(this.id);
|
|
630
|
+
}
|
|
631
|
+
return this._sandbox;
|
|
632
|
+
}
|
|
633
|
+
// ---------------------------------------------------------------------------
|
|
634
|
+
// Lifecycle
|
|
635
|
+
// ---------------------------------------------------------------------------
|
|
636
|
+
async start() {
|
|
637
|
+
if (this._sandbox) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const hasExplicitCreds = Boolean(this._token || this._teamId || this._projectId);
|
|
641
|
+
if (hasExplicitCreds && !(this._token && this._teamId && this._projectId)) {
|
|
642
|
+
throw new Error(
|
|
643
|
+
`${LOG_PREFIX2} Incomplete credentials. Provide token, teamId, and projectId together (or the VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID env vars), or omit all three to use the VERCEL_OIDC_TOKEN.`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
this.logger.debug(`${LOG_PREFIX2} Creating sandbox...`, { runtime: this._runtime, timeout: this._timeout });
|
|
647
|
+
this._sandbox = await sandbox.Sandbox.create({
|
|
648
|
+
...this._sandboxName ? { name: this._sandboxName } : {},
|
|
649
|
+
runtime: this._runtime,
|
|
650
|
+
timeout: this._timeout,
|
|
651
|
+
...this._vcpus ? { resources: { vcpus: this._vcpus } } : {},
|
|
652
|
+
...this._ports?.length ? { ports: this._ports } : {},
|
|
653
|
+
...Object.keys(this._env).length ? { env: this._env } : {},
|
|
654
|
+
...hasExplicitCreds ? { token: this._token, teamId: this._teamId, projectId: this._projectId } : {}
|
|
655
|
+
});
|
|
656
|
+
this._createdAt = /* @__PURE__ */ new Date();
|
|
657
|
+
this.logger.debug(`${LOG_PREFIX2} Sandbox ready: ${this._sandbox.name}`);
|
|
658
|
+
}
|
|
659
|
+
async stop() {
|
|
660
|
+
await this._teardown();
|
|
661
|
+
}
|
|
662
|
+
async destroy() {
|
|
663
|
+
await this._teardown();
|
|
664
|
+
}
|
|
665
|
+
async _teardown() {
|
|
666
|
+
if (!this._sandbox) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
await this._sandbox.stop();
|
|
671
|
+
} catch (error) {
|
|
672
|
+
this.logger.warn(`${LOG_PREFIX2} Error stopping sandbox:`, error);
|
|
673
|
+
}
|
|
674
|
+
this._sandbox = null;
|
|
675
|
+
}
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
// Command Execution
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
async executeCommand(command, args, options) {
|
|
680
|
+
await this.ensureRunning();
|
|
681
|
+
const startTime = Date.now();
|
|
682
|
+
const fullCommand = args?.length ? `${command} ${args.join(" ")}` : command;
|
|
683
|
+
this.logger.debug(`${LOG_PREFIX2} Executing: ${fullCommand}`, { cwd: options?.cwd });
|
|
684
|
+
const mergedEnv = { ...this._env, ...options?.env };
|
|
685
|
+
const env = Object.fromEntries(
|
|
686
|
+
Object.entries(mergedEnv).filter((entry) => entry[1] !== void 0)
|
|
687
|
+
);
|
|
688
|
+
let timeoutId;
|
|
689
|
+
const abortController = new AbortController();
|
|
690
|
+
const forwardAbort = () => abortController.abort();
|
|
691
|
+
if (options?.abortSignal) {
|
|
692
|
+
if (options.abortSignal.aborted) abortController.abort();
|
|
693
|
+
else options.abortSignal.addEventListener("abort", forwardAbort, { once: true });
|
|
694
|
+
}
|
|
695
|
+
const signal = abortController.signal;
|
|
696
|
+
const timeoutPromise = options?.timeout ? new Promise((resolve) => {
|
|
697
|
+
timeoutId = setTimeout(() => resolve("timeout"), options.timeout);
|
|
698
|
+
}) : null;
|
|
699
|
+
try {
|
|
700
|
+
const commandPromise = this.sandbox.runCommand({
|
|
701
|
+
cmd: command,
|
|
702
|
+
args: args ?? [],
|
|
703
|
+
...options?.cwd ? { cwd: options.cwd } : {},
|
|
704
|
+
...Object.keys(env).length ? { env } : {},
|
|
705
|
+
signal
|
|
706
|
+
});
|
|
707
|
+
const finished = timeoutPromise ? await Promise.race([commandPromise, timeoutPromise]) : await commandPromise;
|
|
708
|
+
if (finished === "timeout") {
|
|
709
|
+
abortController.abort();
|
|
710
|
+
return {
|
|
711
|
+
command: fullCommand,
|
|
712
|
+
args,
|
|
713
|
+
success: false,
|
|
714
|
+
exitCode: 124,
|
|
715
|
+
stdout: "",
|
|
716
|
+
stderr: `Command timed out after ${options.timeout}ms`,
|
|
717
|
+
executionTimeMs: Date.now() - startTime,
|
|
718
|
+
timedOut: true
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const [stdout, stderr] = await Promise.all([finished.stdout(), finished.stderr()]);
|
|
722
|
+
if (options?.onStdout && stdout) options.onStdout(stdout);
|
|
723
|
+
if (options?.onStderr && stderr) options.onStderr(stderr);
|
|
724
|
+
return {
|
|
725
|
+
command: fullCommand,
|
|
726
|
+
args,
|
|
727
|
+
success: finished.exitCode === 0,
|
|
728
|
+
exitCode: finished.exitCode,
|
|
729
|
+
stdout,
|
|
730
|
+
stderr,
|
|
731
|
+
executionTimeMs: Date.now() - startTime
|
|
732
|
+
};
|
|
733
|
+
} finally {
|
|
734
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
735
|
+
options?.abortSignal?.removeEventListener("abort", forwardAbort);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// ---------------------------------------------------------------------------
|
|
739
|
+
// Info & Instructions
|
|
740
|
+
// ---------------------------------------------------------------------------
|
|
741
|
+
getInfo() {
|
|
742
|
+
const domains = {};
|
|
743
|
+
if (this._sandbox && this._ports?.length) {
|
|
744
|
+
for (const port of this._ports) {
|
|
745
|
+
try {
|
|
746
|
+
domains[port] = this._sandbox.domain(port);
|
|
747
|
+
} catch {
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
id: this.id,
|
|
753
|
+
name: this.name,
|
|
754
|
+
provider: this.provider,
|
|
755
|
+
status: this.status,
|
|
756
|
+
createdAt: this._createdAt ?? /* @__PURE__ */ new Date(),
|
|
757
|
+
metadata: {
|
|
758
|
+
...this._metadata,
|
|
759
|
+
sandboxName: this._sandbox?.name,
|
|
760
|
+
runtime: this._runtime,
|
|
761
|
+
timeout: this._timeout,
|
|
762
|
+
...this._vcpus ? { vcpus: this._vcpus } : {},
|
|
763
|
+
...this._ports?.length ? { ports: this._ports, domains } : {}
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
// Matches the resolveInstructions pattern in @mastra/core/workspace.
|
|
768
|
+
getInstructions(opts) {
|
|
769
|
+
if (this._instructionsOverride === void 0) return this._getDefaultInstructions();
|
|
770
|
+
if (typeof this._instructionsOverride === "string") return this._instructionsOverride;
|
|
771
|
+
const defaultInstructions = this._getDefaultInstructions();
|
|
772
|
+
return this._instructionsOverride({ defaultInstructions, requestContext: opts?.requestContext });
|
|
773
|
+
}
|
|
774
|
+
_getDefaultInstructions() {
|
|
775
|
+
return [
|
|
776
|
+
"Vercel Sandbox: an ephemeral Firecracker MicroVM running Amazon Linux 2023.",
|
|
777
|
+
`- Runtime: ${this._runtime}. Working directory defaults to /vercel/sandbox.`,
|
|
778
|
+
"- Persistent filesystem within the session; state is lost when the sandbox stops.",
|
|
779
|
+
"- Runs as the vercel-sandbox user with sudo access (install packages via dnf).",
|
|
780
|
+
`- The sandbox auto-terminates after ${Math.round(this._timeout / 1e3)} seconds.`,
|
|
781
|
+
...this._ports?.length ? [`- Exposed ports: ${this._ports.join(", ")} (reachable via public HTTPS domains).`] : [],
|
|
782
|
+
"- Background/long-running processes are supported via the process tools.",
|
|
783
|
+
"- Filesystem mounting (FUSE) is not supported."
|
|
784
|
+
].join("\n");
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// src/microvm-provider.ts
|
|
789
|
+
var vercelMicroVMSandboxProvider = {
|
|
790
|
+
id: "vercel-microvm",
|
|
791
|
+
name: "Vercel Sandbox (MicroVM)",
|
|
792
|
+
description: "Ephemeral Firecracker MicroVM sandbox powered by Vercel Sandbox",
|
|
793
|
+
configSchema: {
|
|
794
|
+
type: "object",
|
|
795
|
+
properties: {
|
|
796
|
+
token: { type: "string", description: "Vercel API token (falls back to VERCEL_TOKEN; omit to use OIDC)" },
|
|
797
|
+
teamId: { type: "string", description: "Vercel team ID (falls back to VERCEL_TEAM_ID)" },
|
|
798
|
+
projectId: { type: "string", description: "Vercel project ID (falls back to VERCEL_PROJECT_ID)" },
|
|
799
|
+
runtime: {
|
|
800
|
+
type: "string",
|
|
801
|
+
description: "Sandbox runtime",
|
|
802
|
+
enum: ["node24", "node22", "node26", "python3.13"],
|
|
803
|
+
default: "node24"
|
|
804
|
+
},
|
|
805
|
+
timeout: { type: "number", description: "Auto-terminate timeout in milliseconds", default: 3e5 },
|
|
806
|
+
vcpus: { type: "number", description: "Number of vCPUs (2048 MB memory per vCPU)" },
|
|
807
|
+
ports: {
|
|
808
|
+
type: "array",
|
|
809
|
+
description: "Ports to expose (up to 4)",
|
|
810
|
+
items: { type: "number" }
|
|
811
|
+
},
|
|
812
|
+
env: {
|
|
813
|
+
type: "object",
|
|
814
|
+
description: "Environment variables",
|
|
815
|
+
additionalProperties: { type: "string" }
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
createSandbox: (config) => new VercelMicroVMSandbox({
|
|
820
|
+
token: config.token,
|
|
821
|
+
teamId: config.teamId,
|
|
822
|
+
projectId: config.projectId,
|
|
823
|
+
runtime: config.runtime,
|
|
824
|
+
timeout: config.timeout,
|
|
825
|
+
...config.vcpus ? { resources: { vcpus: config.vcpus } } : {},
|
|
826
|
+
ports: config.ports,
|
|
827
|
+
env: config.env
|
|
828
|
+
})
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
exports.VercelMicroVMProcessManager = VercelMicroVMProcessManager;
|
|
832
|
+
exports.VercelMicroVMSandbox = VercelMicroVMSandbox;
|
|
833
|
+
exports.VercelSandbox = VercelSandbox;
|
|
834
|
+
exports.vercelMicroVMSandboxProvider = vercelMicroVMSandboxProvider;
|
|
835
|
+
exports.vercelSandboxProvider = vercelSandboxProvider;
|
|
836
|
+
//# sourceMappingURL=index.cjs.map
|
|
837
|
+
//# sourceMappingURL=index.cjs.map
|