@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 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).
@@ -0,0 +1,5 @@
1
+ import { ParelPlugin } from '@parel/plugin-sdk';
2
+
3
+ declare const _default: ParelPlugin;
4
+
5
+ export { _default as default };
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
+ }