@parel/sandbox-vercel 0.1.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 +38 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +323 -0
- package/package.json +54 -0
- package/parel.plugin.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @parel/sandbox-vercel
|
|
2
|
+
|
|
3
|
+
> PAREL sandbox capability provider plugin for Vercel Sandbox.
|
|
4
|
+
|
|
5
|
+
A first-party runtime plugin for [PAREL](https://github.com/parall-hq/parel-opensource).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @parel/sandbox-vercel
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Provides the standard `parel.sandbox` capability from `@parel/capability-sandbox`
|
|
16
|
+
using the official `@vercel/sandbox` SDK. It supports filesystem operations,
|
|
17
|
+
argv command execution, detached processes, port domains, and lifecycle
|
|
18
|
+
management.
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
plugins:
|
|
22
|
+
- plugin: sandbox-vercel
|
|
23
|
+
config:
|
|
24
|
+
token: <vercel token>
|
|
25
|
+
teamId: <team id>
|
|
26
|
+
projectId: <project id>
|
|
27
|
+
name: parel-agent
|
|
28
|
+
runtime: node24
|
|
29
|
+
ports: [3000]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Named sandboxes are reused through `Sandbox.getOrCreate`. By default the plugin
|
|
33
|
+
deletes the sandbox on `session:end`; set `destroyOnSessionEnd: false` to stop it
|
|
34
|
+
instead.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT - see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Buffer } from "buffer";
|
|
3
|
+
import {
|
|
4
|
+
PAREL_SANDBOX_CAPABILITY
|
|
5
|
+
} from "@parel/capability-sandbox";
|
|
6
|
+
import { definePlugin, LifecycleEvent } from "@parel/plugin-sdk";
|
|
7
|
+
import { Sandbox as VercelSandboxClient } from "@vercel/sandbox";
|
|
8
|
+
|
|
9
|
+
// parel.plugin.json
|
|
10
|
+
var parel_plugin_default = {
|
|
11
|
+
name: "@parel/sandbox-vercel",
|
|
12
|
+
version: "0.0.0",
|
|
13
|
+
description: "Provides the standard parel.sandbox capability backed by Vercel Sandbox.",
|
|
14
|
+
provides: {
|
|
15
|
+
capabilities: ["parel.sandbox"]
|
|
16
|
+
},
|
|
17
|
+
requires: {
|
|
18
|
+
permissions: {
|
|
19
|
+
network: true,
|
|
20
|
+
store: true
|
|
21
|
+
},
|
|
22
|
+
secrets: {
|
|
23
|
+
token: {
|
|
24
|
+
description: "Vercel API token or OIDC token",
|
|
25
|
+
required: true
|
|
26
|
+
},
|
|
27
|
+
teamId: {
|
|
28
|
+
description: "Vercel team id",
|
|
29
|
+
required: true
|
|
30
|
+
},
|
|
31
|
+
projectId: {
|
|
32
|
+
description: "Vercel project id for Sandbox operations",
|
|
33
|
+
required: true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
config: {
|
|
38
|
+
name: "Optional named sandbox. Named sandboxes are reused with getOrCreate by default.",
|
|
39
|
+
runtime: "Optional Vercel Sandbox runtime, such as node24 or python3.13.",
|
|
40
|
+
ports: "Optional array of exposed ports.",
|
|
41
|
+
timeoutMs: "Optional sandbox timeout in milliseconds.",
|
|
42
|
+
destroyOnSessionEnd: "Defaults to true. When false, the sandbox is stopped instead of deleted."
|
|
43
|
+
},
|
|
44
|
+
execution: {
|
|
45
|
+
snapshot: {
|
|
46
|
+
store: "reset",
|
|
47
|
+
sandbox: "reset",
|
|
48
|
+
sideEffects: "require_approval"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/index.ts
|
|
54
|
+
var STORE_KEY = "vercel_sandbox_name";
|
|
55
|
+
function stringConfig(value) {
|
|
56
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
57
|
+
}
|
|
58
|
+
function numberConfig(value) {
|
|
59
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
60
|
+
}
|
|
61
|
+
function stringRecord(value) {
|
|
62
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
63
|
+
const record = {};
|
|
64
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
65
|
+
if (typeof entry === "string") record[key] = entry;
|
|
66
|
+
}
|
|
67
|
+
return Object.keys(record).length > 0 ? record : void 0;
|
|
68
|
+
}
|
|
69
|
+
function numberArray(value) {
|
|
70
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "number") : void 0;
|
|
71
|
+
}
|
|
72
|
+
function limitOutput(value, maxOutputChars) {
|
|
73
|
+
if (!maxOutputChars || value.length <= maxOutputChars) return value;
|
|
74
|
+
return value.slice(0, maxOutputChars);
|
|
75
|
+
}
|
|
76
|
+
function mapStatsType(stats) {
|
|
77
|
+
if (stats.isDirectory?.()) return "directory";
|
|
78
|
+
if (stats.isSymbolicLink?.()) return "symlink";
|
|
79
|
+
if (stats.isFile?.()) return "file";
|
|
80
|
+
return "unknown";
|
|
81
|
+
}
|
|
82
|
+
function mapDirentType(dirent) {
|
|
83
|
+
if (dirent.isDirectory?.()) return "directory";
|
|
84
|
+
if (dirent.isSymbolicLink?.()) return "symlink";
|
|
85
|
+
if (dirent.isFile?.()) return "file";
|
|
86
|
+
return "unknown";
|
|
87
|
+
}
|
|
88
|
+
function mapCommandStatus(command) {
|
|
89
|
+
if (command.exitCode === null) return "running";
|
|
90
|
+
return command.exitCode === 0 ? "exited" : "failed";
|
|
91
|
+
}
|
|
92
|
+
async function commandResult(command, opts) {
|
|
93
|
+
return {
|
|
94
|
+
stdout: limitOutput(await command.stdout(), opts?.maxOutputChars),
|
|
95
|
+
stderr: limitOutput(await command.stderr(), opts?.maxOutputChars),
|
|
96
|
+
exitCode: command.exitCode,
|
|
97
|
+
metadata: { provider: "vercel", commandId: command.cmdId }
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function runParams(command, opts) {
|
|
101
|
+
return {
|
|
102
|
+
cmd: command[0],
|
|
103
|
+
args: [...command.slice(1)],
|
|
104
|
+
cwd: opts?.cwd,
|
|
105
|
+
env: opts?.env,
|
|
106
|
+
timeoutMs: opts?.timeoutMs
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function buildCreateParams(config) {
|
|
110
|
+
const params = {};
|
|
111
|
+
for (const key of ["name", "runtime"]) {
|
|
112
|
+
const value = stringConfig(config[key]);
|
|
113
|
+
if (value) params[key] = value;
|
|
114
|
+
}
|
|
115
|
+
const timeout = numberConfig(config.timeoutMs);
|
|
116
|
+
if (timeout !== void 0) params.timeout = timeout;
|
|
117
|
+
const ports = numberArray(config.ports);
|
|
118
|
+
if (ports) params.ports = ports;
|
|
119
|
+
const env = stringRecord(config.env);
|
|
120
|
+
if (env) params.env = env;
|
|
121
|
+
const tags = stringRecord(config.tags);
|
|
122
|
+
if (tags) params.tags = tags;
|
|
123
|
+
return params;
|
|
124
|
+
}
|
|
125
|
+
var index_default = definePlugin({
|
|
126
|
+
name: "@parel/sandbox-vercel",
|
|
127
|
+
provides: parel_plugin_default.provides,
|
|
128
|
+
requires: parel_plugin_default.requires,
|
|
129
|
+
execution: parel_plugin_default.execution,
|
|
130
|
+
async setup(ctx) {
|
|
131
|
+
const token = stringConfig(ctx.config.token);
|
|
132
|
+
const teamId = stringConfig(ctx.config.teamId);
|
|
133
|
+
const projectId = stringConfig(ctx.config.projectId);
|
|
134
|
+
const destroyOnSessionEnd = ctx.config.destroyOnSessionEnd !== false;
|
|
135
|
+
let sandbox = null;
|
|
136
|
+
function credentials() {
|
|
137
|
+
if (!token || !teamId || !projectId) {
|
|
138
|
+
ctx.log.warn("Vercel Sandbox token, teamId, and projectId are required");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return { token, teamId, projectId };
|
|
142
|
+
}
|
|
143
|
+
function requireSandbox() {
|
|
144
|
+
if (!sandbox) throw new Error("Vercel sandbox not available");
|
|
145
|
+
return sandbox;
|
|
146
|
+
}
|
|
147
|
+
async function ensureSandbox() {
|
|
148
|
+
const creds = credentials();
|
|
149
|
+
if (!creds) return null;
|
|
150
|
+
const savedName = await ctx.store.get(STORE_KEY);
|
|
151
|
+
const name = stringConfig(ctx.config.name) ?? savedName;
|
|
152
|
+
const params = { ...buildCreateParams(ctx.config), ...creds };
|
|
153
|
+
if (name) {
|
|
154
|
+
sandbox = await VercelSandboxClient.getOrCreate({
|
|
155
|
+
...params,
|
|
156
|
+
name
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
sandbox = await VercelSandboxClient.create(params);
|
|
160
|
+
}
|
|
161
|
+
await ctx.store.set(STORE_KEY, sandbox.name);
|
|
162
|
+
ctx.log.info(`Vercel sandbox ready: ${sandbox.name}`);
|
|
163
|
+
return sandbox;
|
|
164
|
+
}
|
|
165
|
+
async function disposeSandbox() {
|
|
166
|
+
if (!sandbox) return;
|
|
167
|
+
try {
|
|
168
|
+
if (destroyOnSessionEnd) {
|
|
169
|
+
await sandbox.delete();
|
|
170
|
+
await ctx.store.delete(STORE_KEY);
|
|
171
|
+
} else {
|
|
172
|
+
await sandbox.stop();
|
|
173
|
+
}
|
|
174
|
+
} finally {
|
|
175
|
+
sandbox = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function execCommand(command, opts) {
|
|
179
|
+
const finished = await requireSandbox().runCommand(
|
|
180
|
+
runParams(command, opts)
|
|
181
|
+
);
|
|
182
|
+
return commandResult(finished, opts);
|
|
183
|
+
}
|
|
184
|
+
async function shellCommand(command, opts) {
|
|
185
|
+
const shell = opts?.shell ?? "sh";
|
|
186
|
+
const finished = await requireSandbox().runCommand({
|
|
187
|
+
cmd: shell,
|
|
188
|
+
args: ["-lc", command],
|
|
189
|
+
cwd: opts?.cwd,
|
|
190
|
+
env: opts?.env,
|
|
191
|
+
timeoutMs: opts?.timeoutMs
|
|
192
|
+
});
|
|
193
|
+
return commandResult(finished, opts);
|
|
194
|
+
}
|
|
195
|
+
function processHandle(command, original) {
|
|
196
|
+
return {
|
|
197
|
+
id: command.cmdId,
|
|
198
|
+
command: original,
|
|
199
|
+
async status() {
|
|
200
|
+
return mapCommandStatus(command);
|
|
201
|
+
},
|
|
202
|
+
async wait(opts) {
|
|
203
|
+
const finished = await command.wait(opts ? { signal: void 0 } : void 0);
|
|
204
|
+
return commandResult(finished, opts);
|
|
205
|
+
},
|
|
206
|
+
async kill(signal) {
|
|
207
|
+
await command.kill(signal);
|
|
208
|
+
},
|
|
209
|
+
async stdout(opts) {
|
|
210
|
+
return limitOutput(await command.stdout(), opts?.maxChars);
|
|
211
|
+
},
|
|
212
|
+
async stderr(opts) {
|
|
213
|
+
return limitOutput(await command.stderr(), opts?.maxChars);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const capability = {
|
|
218
|
+
get id() {
|
|
219
|
+
return sandbox?.name;
|
|
220
|
+
},
|
|
221
|
+
provider: "vercel",
|
|
222
|
+
supports: {
|
|
223
|
+
fs: true,
|
|
224
|
+
process: true,
|
|
225
|
+
shell: true,
|
|
226
|
+
spawn: true,
|
|
227
|
+
ports: true,
|
|
228
|
+
lifecycle: true,
|
|
229
|
+
network: "enabled"
|
|
230
|
+
},
|
|
231
|
+
fs: {
|
|
232
|
+
async readFile(path, opts) {
|
|
233
|
+
const content = await requireSandbox().fs.readFile(path, {
|
|
234
|
+
encoding: opts?.encoding === "base64" ? null : "utf8"
|
|
235
|
+
});
|
|
236
|
+
const text = Buffer.isBuffer(content) ? content.toString("base64") : content;
|
|
237
|
+
return opts?.maxChars ? limitOutput(text, opts.maxChars) : text;
|
|
238
|
+
},
|
|
239
|
+
async writeFile(path, content, opts) {
|
|
240
|
+
const data = opts?.encoding === "base64" ? Buffer.from(content, "base64") : content;
|
|
241
|
+
if (opts?.append && requireSandbox().fs.appendFile) {
|
|
242
|
+
await requireSandbox().fs.appendFile?.(path, data);
|
|
243
|
+
} else {
|
|
244
|
+
await requireSandbox().fs.writeFile(path, data);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
async listDir(path) {
|
|
248
|
+
return (await requireSandbox().fs.readdir(path, { withFileTypes: true })).map(
|
|
249
|
+
(entry) => ({
|
|
250
|
+
name: entry.name,
|
|
251
|
+
type: mapDirentType(entry)
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
async stat(path) {
|
|
256
|
+
const stats = await requireSandbox().fs.stat(path);
|
|
257
|
+
return {
|
|
258
|
+
path,
|
|
259
|
+
type: mapStatsType(stats),
|
|
260
|
+
size: stats.size,
|
|
261
|
+
mtimeMs: stats.mtimeMs,
|
|
262
|
+
mode: stats.mode
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
async exists(path) {
|
|
266
|
+
return requireSandbox().fs.exists(path);
|
|
267
|
+
},
|
|
268
|
+
async mkdir(path, opts) {
|
|
269
|
+
await requireSandbox().fs.mkdir(path, { recursive: opts?.recursive });
|
|
270
|
+
},
|
|
271
|
+
async remove(path, opts) {
|
|
272
|
+
await requireSandbox().fs.rm(path, { recursive: opts?.recursive, force: true });
|
|
273
|
+
},
|
|
274
|
+
async rename(from, to) {
|
|
275
|
+
await requireSandbox().fs.rename(from, to);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
process: {
|
|
279
|
+
exec: execCommand,
|
|
280
|
+
shell: shellCommand,
|
|
281
|
+
async spawn(command, opts) {
|
|
282
|
+
const handle = await requireSandbox().runCommand({
|
|
283
|
+
...runParams(command, opts),
|
|
284
|
+
detached: true
|
|
285
|
+
});
|
|
286
|
+
return processHandle(handle, command);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
ports: {
|
|
290
|
+
async expose(port) {
|
|
291
|
+
return { port, url: requireSandbox().domain(port), protocol: "https" };
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
lifecycle: {
|
|
295
|
+
async isRunning() {
|
|
296
|
+
return String(sandbox?.status ?? "running") === "running";
|
|
297
|
+
},
|
|
298
|
+
async stop() {
|
|
299
|
+
await requireSandbox().stop();
|
|
300
|
+
},
|
|
301
|
+
async extendTimeout(timeoutMs) {
|
|
302
|
+
await requireSandbox().extendTimeout?.(timeoutMs);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
ctx.hook(LifecycleEvent.SessionStart, async () => {
|
|
307
|
+
await ensureSandbox();
|
|
308
|
+
});
|
|
309
|
+
ctx.hook(LifecycleEvent.SessionResume, async () => {
|
|
310
|
+
await ensureSandbox();
|
|
311
|
+
});
|
|
312
|
+
ctx.hook(LifecycleEvent.SessionSuspend, async () => {
|
|
313
|
+
if (sandbox) await ctx.store.set(STORE_KEY, sandbox.name);
|
|
314
|
+
});
|
|
315
|
+
ctx.hook(LifecycleEvent.SessionEnd, async () => {
|
|
316
|
+
await disposeSandbox();
|
|
317
|
+
});
|
|
318
|
+
ctx.provide(PAREL_SANDBOX_CAPABILITY, capability);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
export {
|
|
322
|
+
index_default as default
|
|
323
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parel/sandbox-vercel",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "PAREL sandbox capability provider plugin for Vercel Sandbox.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/parall-hq/parel-opensource.git",
|
|
9
|
+
"directory": "js/plugins/sandbox-vercel"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/parall-hq/parel-opensource/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/parall-hq/parel-opensource#readme",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"bun": "./src/index.ts",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"parel.plugin.json",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"test": "vitest run --passWithNoTests",
|
|
32
|
+
"lint": "biome check src/"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@parel/capability-sandbox": "workspace:*",
|
|
36
|
+
"@parel/plugin-sdk": "workspace:*",
|
|
37
|
+
"@vercel/sandbox": "^2.1.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.9.1",
|
|
41
|
+
"tsup": "^8.0.0",
|
|
42
|
+
"typescript": "^5.8.0",
|
|
43
|
+
"vitest": "^4.1.8"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"exports": {
|
|
47
|
+
".": {
|
|
48
|
+
"types": "./dist/index.d.ts",
|
|
49
|
+
"import": "./dist/index.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@parel/sandbox-vercel",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Provides the standard parel.sandbox capability backed by Vercel Sandbox.",
|
|
5
|
+
"provides": {
|
|
6
|
+
"capabilities": ["parel.sandbox"]
|
|
7
|
+
},
|
|
8
|
+
"requires": {
|
|
9
|
+
"permissions": {
|
|
10
|
+
"network": true,
|
|
11
|
+
"store": true
|
|
12
|
+
},
|
|
13
|
+
"secrets": {
|
|
14
|
+
"token": {
|
|
15
|
+
"description": "Vercel API token or OIDC token",
|
|
16
|
+
"required": true
|
|
17
|
+
},
|
|
18
|
+
"teamId": {
|
|
19
|
+
"description": "Vercel team id",
|
|
20
|
+
"required": true
|
|
21
|
+
},
|
|
22
|
+
"projectId": {
|
|
23
|
+
"description": "Vercel project id for Sandbox operations",
|
|
24
|
+
"required": true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"config": {
|
|
29
|
+
"name": "Optional named sandbox. Named sandboxes are reused with getOrCreate by default.",
|
|
30
|
+
"runtime": "Optional Vercel Sandbox runtime, such as node24 or python3.13.",
|
|
31
|
+
"ports": "Optional array of exposed ports.",
|
|
32
|
+
"timeoutMs": "Optional sandbox timeout in milliseconds.",
|
|
33
|
+
"destroyOnSessionEnd": "Defaults to true. When false, the sandbox is stopped instead of deleted."
|
|
34
|
+
},
|
|
35
|
+
"execution": {
|
|
36
|
+
"snapshot": {
|
|
37
|
+
"store": "reset",
|
|
38
|
+
"sandbox": "reset",
|
|
39
|
+
"sideEffects": "require_approval"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|