@prized/sdk 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +625 -2
- package/dist/cjs/commands.d.ts +168 -0
- package/dist/cjs/commands.js +567 -0
- package/dist/cjs/errors.d.ts +73 -0
- package/dist/cjs/errors.js +258 -0
- package/dist/cjs/files.d.ts +86 -0
- package/dist/cjs/files.js +366 -0
- package/dist/cjs/http.d.ts +210 -0
- package/dist/cjs/http.js +570 -0
- package/dist/cjs/index.d.ts +12 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/outcome.d.ts +75 -0
- package/dist/cjs/outcome.js +167 -0
- package/dist/cjs/output.d.ts +59 -0
- package/dist/cjs/output.js +288 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/sandbox.d.ts +218 -0
- package/dist/cjs/sandbox.js +810 -0
- package/dist/cjs/shell.d.ts +152 -0
- package/dist/cjs/shell.js +393 -0
- package/dist/cjs/toolbox.d.ts +48 -0
- package/dist/cjs/toolbox.js +86 -0
- package/dist/cjs/types.d.ts +99 -0
- package/dist/cjs/types.js +102 -0
- package/dist/cjs/version.d.ts +2 -0
- package/dist/cjs/version.js +5 -0
- package/dist/esm/commands.d.ts +168 -0
- package/dist/esm/commands.js +560 -0
- package/dist/esm/errors.d.ts +73 -0
- package/dist/esm/errors.js +250 -0
- package/dist/esm/files.d.ts +86 -0
- package/dist/esm/files.js +360 -0
- package/dist/esm/http.d.ts +210 -0
- package/dist/esm/http.js +553 -0
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/outcome.d.ts +75 -0
- package/dist/esm/outcome.js +152 -0
- package/dist/esm/output.d.ts +59 -0
- package/dist/esm/output.js +280 -0
- package/dist/esm/sandbox.d.ts +218 -0
- package/dist/esm/sandbox.js +806 -0
- package/dist/esm/shell.d.ts +152 -0
- package/dist/esm/shell.js +379 -0
- package/dist/esm/toolbox.d.ts +48 -0
- package/dist/esm/toolbox.js +81 -0
- package/dist/esm/types.d.ts +99 -0
- package/dist/esm/types.js +95 -0
- package/dist/esm/version.d.ts +2 -0
- package/dist/esm/version.js +2 -0
- package/package.json +53 -4
package/README.md
CHANGED
|
@@ -1,3 +1,626 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Prized Sandboxes (beta): TypeScript SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Create sandboxes (microVMs your code runs in) from TypeScript: run commands,
|
|
4
|
+
move files, pause for the price of storage, resume where you left off, and
|
|
5
|
+
fork a running or paused sandbox into many live copies.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { Sandbox } from '@prized/sdk';
|
|
9
|
+
|
|
10
|
+
const sandbox = await Sandbox.create({ template: 'prized-base-v1' });
|
|
11
|
+
const { stdout } = await sandbox.exec('python3 -c "print(6 * 7)"');
|
|
12
|
+
console.log(stdout); // 42
|
|
13
|
+
await sandbox.delete();
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Sandboxes are in beta. A workspace on free credit alone runs them on the
|
|
17
|
+
free tier: up to 10 at once, each up to 2 vCPU and 4 GB of memory and 60
|
|
18
|
+
minutes of running. A top-up or a paid plan lifts the limits: a sandbox then
|
|
19
|
+
runs up to 7 days at a stretch, and a paused one is kept until you delete it.
|
|
20
|
+
|
|
21
|
+
- [Install](#install)
|
|
22
|
+
- [Authentication](#authentication)
|
|
23
|
+
- [Create, connect, list](#create-connect-list)
|
|
24
|
+
- [Run commands](#run-commands)
|
|
25
|
+
- [Background commands](#background-commands)
|
|
26
|
+
- [Files](#files)
|
|
27
|
+
- [Pause, resume, refresh, delete](#pause-resume-refresh-delete)
|
|
28
|
+
- [Run time: extend anytime](#run-time-extend-anytime)
|
|
29
|
+
- [Fork](#fork)
|
|
30
|
+
- [Templates](#templates)
|
|
31
|
+
- [Errors](#errors)
|
|
32
|
+
- [Retries and timeouts](#retries-and-timeouts)
|
|
33
|
+
- [Limits](#limits)
|
|
34
|
+
- [Runtimes](#runtimes)
|
|
35
|
+
- [License](#license)
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm install @prized/sdk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
No runtime dependencies. ESM and CommonJS, with type declarations.
|
|
44
|
+
|
|
45
|
+
## Authentication
|
|
46
|
+
|
|
47
|
+
Make an API key on the [API keys page](https://prized.dev/dashboard/workspace/api-keys)
|
|
48
|
+
and put it in `PRIZED_API_KEY`, or pass it in. New to Prized?
|
|
49
|
+
[Open Sandboxes](https://prized.dev/onboarding/sandboxes) signs you up (free
|
|
50
|
+
credit, no card) and makes one.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const sandbox = await Sandbox.create({ apiKey: 'dcp_…' });
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The key acts as you and sees every sandbox in your workspace. A key limited
|
|
57
|
+
to fewer permissions needs **Sandboxes: Write** to create, run commands and
|
|
58
|
+
move files; **Sandboxes: Read** is enough for `list`, `connect`, `refresh` and
|
|
59
|
+
`templates`.
|
|
60
|
+
|
|
61
|
+
Every entry point (`create`, `connect`, `list`, `deleteByLabels`, `templates`) takes the same
|
|
62
|
+
client options:
|
|
63
|
+
|
|
64
|
+
| Option | Default | |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `apiKey` | `process.env.PRIZED_API_KEY` | |
|
|
67
|
+
| `baseUrl` | `https://api.prized.dev` | The API origin. |
|
|
68
|
+
| `edgeUrl` | the sandbox's own `toolboxBase` | Where commands and files go. |
|
|
69
|
+
| `fetch` | global `fetch` | Bring your own (for a proxy, custom agent, tests). |
|
|
70
|
+
| `maxRetries` | `3` | See [Retries](#retries-and-timeouts). |
|
|
71
|
+
| `requestTimeoutMs` | `60000` | Per API request. |
|
|
72
|
+
| `pollIntervalMs` | `500` | The longest the SDK's own polling goes between checks of the sandbox (the first come sooner). It does not cover the request itself, or the time the API holds a create, pause or resume for you (up to 8 seconds). |
|
|
73
|
+
| `pollOutageMs` | `120000` | How long a streamed or long `exec` keeps polling through a dropped connection (or a 5xx, or a rate limit) before it stops the command. `exec` takes it per call too. |
|
|
74
|
+
|
|
75
|
+
A sandbox remembers the options it was made with: its methods need none.
|
|
76
|
+
|
|
77
|
+
## Create, connect, list
|
|
78
|
+
|
|
79
|
+
### `Sandbox.create(options?)`
|
|
80
|
+
|
|
81
|
+
Creates a sandbox and resolves once it is `running`, ready for commands.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const sandbox = await Sandbox.create({
|
|
85
|
+
template: 'prized-agent-v1', // default 'prized-base-v1'
|
|
86
|
+
resources: { vcpu: 2, memoryGb: 4, diskGb: 20 }, // or memoryMb: 4096; omitted fields take the template default
|
|
87
|
+
envVars: { NODE_ENV: 'test' }, // for every command in the sandbox
|
|
88
|
+
labels: { job: 'nightly', owner: 'ci' }, // filter on them in Sandbox.list
|
|
89
|
+
maxRuntimeMin: 120, // run time in minutes; deleted when it ends (paid: 24 h by default, up to 7 days)
|
|
90
|
+
autoPauseMin: 15, // pause after 15 idle minutes (0: never)
|
|
91
|
+
autoDeleteMin: 60, // delete 60 minutes after pausing (0: never)
|
|
92
|
+
timeoutMs: 180_000, // the request, its retries and the wait for running
|
|
93
|
+
idempotencyKey: 'nightly-2026-10-14', // a retried create returns the same sandbox
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Commands and file transfers count as activity for `autoPauseMin`.
|
|
98
|
+
|
|
99
|
+
A large shape (more than 16 vCPU or about 58 GB of memory) can take a minute
|
|
100
|
+
or two to start: give such a `create` a `timeoutMs` of `300_000` or more.
|
|
101
|
+
|
|
102
|
+
If the sandbox fails to start or is not running within `timeoutMs`, it is
|
|
103
|
+
deleted and `create` throws (`sandbox_error` with the `reason`, or
|
|
104
|
+
`timeout`). Create, `pause` and `resume` ask the API to answer once the change
|
|
105
|
+
has landed, and only poll if it has not within a few seconds. `wait: false`
|
|
106
|
+
resolves as soon as the API accepts the create (state `starting`), still
|
|
107
|
+
within `timeoutMs`. Every create carries an idempotency key (the SDK makes
|
|
108
|
+
one when you give none), so the SDK's own retries never make a second
|
|
109
|
+
sandbox.
|
|
110
|
+
|
|
111
|
+
### Many at once
|
|
112
|
+
|
|
113
|
+
Run a batch a few at a time, not all at once: a workspace caps how many
|
|
114
|
+
sandboxes it holds (10 on the free tier), and a quota refusal is not retried.
|
|
115
|
+
Each worker waits for its sandbox to be gone before it takes the next
|
|
116
|
+
command, because a deleting sandbox still counts for a moment. If a command
|
|
117
|
+
fails, the other workers finish the one they hold and stop, and `runAll`
|
|
118
|
+
throws once every sandbox is deleted.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
async function runAll(commands: string[], limit = 4): Promise<string[]> {
|
|
122
|
+
const out: string[] = [];
|
|
123
|
+
let next = 0;
|
|
124
|
+
let failed = false;
|
|
125
|
+
const worker = async () => {
|
|
126
|
+
try {
|
|
127
|
+
while (!failed && next < commands.length) {
|
|
128
|
+
const i = next++;
|
|
129
|
+
const sandbox = await Sandbox.create({ resources: { vcpu: 2, memoryGb: 4 }, labels: { job: 'batch' } });
|
|
130
|
+
try {
|
|
131
|
+
const r = await sandbox.exec(commands[i], { timeoutMs: 600_000 });
|
|
132
|
+
if (r.exitCode !== 0) throw new Error(`${commands[i]} exited ${r.exitCode}: ${r.stderr}`);
|
|
133
|
+
out[i] = r.stdout;
|
|
134
|
+
} finally {
|
|
135
|
+
await sandbox.delete({ wait: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
failed = true; // the other workers finish the command they hold, then stop
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
// allSettled, not all: every sandbox is deleted before runAll returns or throws.
|
|
144
|
+
const results = await Promise.allSettled(Array.from({ length: Math.min(limit, commands.length) }, worker));
|
|
145
|
+
for (const r of results) if (r.status === 'rejected') throw r.reason;
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### `Sandbox.connect(id, options?)`
|
|
151
|
+
|
|
152
|
+
An existing sandbox by id, in whatever state it is in.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const sandbox = await Sandbox.connect('sbx_0123456789abcdef');
|
|
156
|
+
if (sandbox.state === 'paused') await sandbox.resume();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### `Sandbox.list(options?)`
|
|
160
|
+
|
|
161
|
+
Your workspace's sandboxes, newest first (deleted ones are left out).
|
|
162
|
+
Filter by labels (all must match) and state:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const mine = await Sandbox.list({ labels: { job: 'nightly' }, state: ['running', 'paused'] });
|
|
166
|
+
await Promise.all(mine.map((s) => s.delete()));
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### `Sandbox.deleteByLabels(labels, options?)`
|
|
170
|
+
|
|
171
|
+
Deletes every sandbox carrying all of `labels` (at least one) in one call and
|
|
172
|
+
resolves with their ids. It is safe to repeat (one already deleting is listed
|
|
173
|
+
again; none matching resolves `[]`). A sandbox being deleted still counts
|
|
174
|
+
toward your quota until `Sandbox.list({ labels })` no longer returns it.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const ids = await Sandbox.deleteByLabels({ job: 'nightly' });
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### What a sandbox knows
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
sandbox.id // 'sbx_…'
|
|
184
|
+
sandbox.state // 'starting' | 'running' | 'pausing' | 'paused' | 'resizing' | 'deleting' | 'deleted' | 'error'
|
|
185
|
+
sandbox.template // 'prized-base-v1'
|
|
186
|
+
sandbox.resources // { vcpu, memoryMb, memoryGb, diskGb }
|
|
187
|
+
sandbox.labels
|
|
188
|
+
sandbox.createdAt // Date
|
|
189
|
+
sandbox.expiresAt // Date: when its run time ends (null while a paused sandbox's clock is stopped)
|
|
190
|
+
sandbox.errorReason // why it is in 'error', when it is
|
|
191
|
+
sandbox.info // all of the above, plus autoPauseMin, autoDeleteMin, lastActiveAt, raw
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
These reflect the last answer from the API; `await sandbox.refresh()` reads
|
|
195
|
+
them fresh.
|
|
196
|
+
|
|
197
|
+
## Run commands
|
|
198
|
+
|
|
199
|
+
### `sandbox.exec(command, options?)`
|
|
200
|
+
|
|
201
|
+
Runs a command and resolves with its output. `command` is a shell string, or
|
|
202
|
+
an argv array the SDK quotes for you.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const r = await sandbox.exec('npm test', {
|
|
206
|
+
cwd: 'app', // relative paths start in /home/prized/work (the default cwd)
|
|
207
|
+
env: { CI: '1' }, // for this command, on top of the sandbox's envVars
|
|
208
|
+
timeoutMs: 120_000, // default 60 000; 0 = no limit
|
|
209
|
+
});
|
|
210
|
+
r.stdout; // string
|
|
211
|
+
r.stderr; // string, kept separate
|
|
212
|
+
r.exitCode; // number
|
|
213
|
+
r.durationMs;
|
|
214
|
+
r.truncated; // true if stdout or stderr hit the 4 MiB per-call cap
|
|
215
|
+
|
|
216
|
+
await sandbox.exec(['git', 'commit', '-m', "it's done"]); // quoted for you
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
One call keeps the first 4 MiB of stdout and of stderr, and `truncated` says
|
|
220
|
+
when it cut one. Pass `fullOutput: true` for builds, test suites and anything
|
|
221
|
+
else whose output you need whole: the result keeps up to 100 MiB of each
|
|
222
|
+
stream (`truncated` past that). It is still one request when the output is
|
|
223
|
+
small; past 4 MiB the SDK reads it back from files in
|
|
224
|
+
`/home/prized/.prized/exec/`, then removes them. With it, a command a signal
|
|
225
|
+
ends gets `exitCode` 128+n (a plain exec may give -1), as a background command
|
|
226
|
+
does.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const { stdout, exitCode } = await sandbox.exec('npm test', { timeoutMs: 180_000, fullOutput: true });
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
A non-zero exit code is not an error: check `exitCode`. A command that runs
|
|
233
|
+
past its timeout is killed, and the call throws `CommandTimeoutError` (a
|
|
234
|
+
`PrizedError` with `code` `command_timeout`) whose `result` has what the
|
|
235
|
+
command printed before (`exitCode: -1`, `timedOut: true`):
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import { CommandTimeoutError } from '@prized/sdk';
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await sandbox.exec('npm test', { timeoutMs: 120_000 });
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (err instanceof CommandTimeoutError) console.log(err.result.stdout);
|
|
244
|
+
else throw err;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Otherwise the SDK throws only when the command could not be run (see
|
|
249
|
+
[Errors](#errors)).
|
|
250
|
+
|
|
251
|
+
Stream output as it arrives with `onStdout` / `onStderr`:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
await sandbox.exec('npm install', {
|
|
255
|
+
timeoutMs: 600_000,
|
|
256
|
+
onStdout: (chunk) => process.stdout.write(chunk),
|
|
257
|
+
onStderr: (chunk) => process.stderr.write(chunk),
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
How it runs: a command with no callbacks and a timeout up to 4 minutes is
|
|
262
|
+
one request that answers when the command finishes. With `onStdout` /
|
|
263
|
+
`onStderr`, a longer timeout, or `timeoutMs: 0`, the SDK starts the command
|
|
264
|
+
in the background and polls its output instead, so a long build is never cut
|
|
265
|
+
off by an idle connection and its output is never truncated. The result is
|
|
266
|
+
the same either way. If the connection drops meanwhile, the SDK keeps
|
|
267
|
+
polling for up to 2 minutes (`pollOutageMs`, on the call or the client)
|
|
268
|
+
before it gives up and stops the command; a refused key, or an answer that
|
|
269
|
+
settles it (the sandbox deleted or not running), stops it at once.
|
|
270
|
+
Background and streamed commands run with `PYTHONUNBUFFERED=1` unless
|
|
271
|
+
their environment already sets it (`env: { PYTHONUNBUFFERED: '' }` keeps
|
|
272
|
+
Python's buffering), so a Python program's output arrives as it prints.
|
|
273
|
+
|
|
274
|
+
Commands run as the user `prized` (home `/home/prized`, no sudo). The
|
|
275
|
+
templates come with git, curl, Python 3 with pip and venv, Node 22 with npm
|
|
276
|
+
and pnpm; `prized-agent-v1` adds Claude Code and Codex. Install what else
|
|
277
|
+
you need into your home: Python packages into a venv
|
|
278
|
+
(`python3 -m venv ~/.venv && ~/.venv/bin/pip install numpy`; plain
|
|
279
|
+
`pip install`, `--user` too, is refused), and `npm` with a user prefix.
|
|
280
|
+
|
|
281
|
+
## Background commands
|
|
282
|
+
|
|
283
|
+
`background: true` starts the command and returns a `CommandHandle` at once.
|
|
284
|
+
The command runs in its own session: it keeps running after your process
|
|
285
|
+
exits, survives a pause and resume, and while it runs, a kill reaches every
|
|
286
|
+
process it started (not one that left its process group, such as a daemon).
|
|
287
|
+
Once the command has ended, `kill()` resolves false and signals nothing.
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
const server = await sandbox.exec('npm run dev', { background: true, cwd: 'app' });
|
|
291
|
+
server.id; // 'run_…'
|
|
292
|
+
server.pid; // its process group id inside the sandbox
|
|
293
|
+
|
|
294
|
+
await server.status(); // { running: true, exitCode: null }
|
|
295
|
+
await server.logs(); // { stdout, stderr }: everything printed so far
|
|
296
|
+
|
|
297
|
+
await server.kill(); // SIGTERM to the command and its children; resolves false if it had already exited
|
|
298
|
+
await server.kill('SIGKILL');
|
|
299
|
+
|
|
300
|
+
const job = await sandbox.exec('make build', { background: true, timeoutMs: 30 * 60_000 });
|
|
301
|
+
const result = await job.wait({
|
|
302
|
+
onStdout: (chunk) => process.stdout.write(chunk), // replays from the start, then live
|
|
303
|
+
timeoutMs: 45 * 60_000, // bounds the wait only; the command keeps running
|
|
304
|
+
});
|
|
305
|
+
result.exitCode;
|
|
306
|
+
await job.cleanup(); // remove its output files from the sandbox
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
A background command has no time limit unless you give `timeoutMs`
|
|
310
|
+
(enforced to the second: the command gets SIGTERM, then SIGKILL five seconds
|
|
311
|
+
later, and `wait()` throws `CommandTimeoutError`). Its output is kept under
|
|
312
|
+
`~/.prized/runs/<id>/` in the sandbox (`stdout`, `stderr`, `exit`) until you
|
|
313
|
+
call `cleanup()` after the command has ended: after `wait()` resolves or
|
|
314
|
+
throws `CommandTimeoutError` (after a `kill()`, await `wait()` first). On a
|
|
315
|
+
command still running, `cleanup()` does nothing. After it, the handle's
|
|
316
|
+
calls reject with `not_found`. A command that prints without end fills the
|
|
317
|
+
sandbox's disk there, so redirect such output yourself.
|
|
318
|
+
`exitCode` is `-1` when a command timed out or ended without reporting one
|
|
319
|
+
(for example after `SIGKILL`).
|
|
320
|
+
|
|
321
|
+
## Files
|
|
322
|
+
|
|
323
|
+
`sandbox.files` reads and writes the sandbox's disk. Paths are absolute or
|
|
324
|
+
relative to `/home/prized/work`. Writes, reads, uploads and downloads must
|
|
325
|
+
stay inside `/home/prized`, are at most 100 MiB each, and do not follow
|
|
326
|
+
symbolic links. A write replaces the file whole, and a file it replaces keeps
|
|
327
|
+
its mode, so a script made executable stays executable; a new file gets the
|
|
328
|
+
sandbox's default (`0600` in `prized-base-v1` and `prized-agent-v1`). `mode` sets the permission
|
|
329
|
+
bits.
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
await sandbox.files.write('app/main.py', 'print("hi")\n'); // string, Uint8Array, ArrayBuffer or Blob; parents are created
|
|
333
|
+
await sandbox.files.write('app/run.sh', '#!/bin/sh\necho hi\n', { mode: 0o755 }); // executable from the start
|
|
334
|
+
const text = await sandbox.files.read('app/main.py'); // UTF-8 string
|
|
335
|
+
const bytes = await sandbox.files.readBytes('/home/prized/work/out.bin'); // Uint8Array
|
|
336
|
+
|
|
337
|
+
const entries = await sandbox.files.list('app'); // default: /home/prized/work
|
|
338
|
+
// [{ name, path, size, isDir, isSymlink, mode: '-rw-r--r--', modified: Date }]
|
|
339
|
+
|
|
340
|
+
await sandbox.files.exists('app/main.py'); // true
|
|
341
|
+
await sandbox.files.remove('app/old.txt'); // a missing path is fine
|
|
342
|
+
await sandbox.files.remove('build', { recursive: true }); // directories need recursive
|
|
343
|
+
|
|
344
|
+
// Node, Bun and Deno: copy local files in and out
|
|
345
|
+
await sandbox.files.upload('./dist/bundle.js', 'app/bundle.js');
|
|
346
|
+
await sandbox.files.download('app/report.html', './report.html'); // parent directories are created
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Pause, resume, refresh, delete
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
await sandbox.pause(); // keeps memory and disk (until you delete it; free tier: 30 days); only storage is billed. Resolves when paused.
|
|
353
|
+
await sandbox.resume(); // every process carries on where it was. Resolves when running.
|
|
354
|
+
// (A command or file call on a paused sandbox resumes it too.)
|
|
355
|
+
await sandbox.refresh(); // read the sandbox again (state, labels, …); not counted as activity
|
|
356
|
+
await sandbox.keepAlive(); // count as activity now: the autoPauseMin idle clock starts over
|
|
357
|
+
await sandbox.delete(); // resolves once accepted; the sandbox goes deleting, then deleted
|
|
358
|
+
await sandbox.delete({ wait: true }); // also wait until it is gone
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`pause` and `resume` take `{ wait: false }` to return without waiting, and
|
|
362
|
+
`{ timeoutMs }` to bound the request, its retries and the wait.
|
|
363
|
+
`delete`'s `{ timeoutMs }` (default 120 000) bounds the whole call. A sandbox that is deleting still counts
|
|
364
|
+
toward your workspace's sandbox quota until it is gone.
|
|
365
|
+
|
|
366
|
+
## Run time: extend anytime
|
|
367
|
+
|
|
368
|
+
A sandbox is deleted when its run time ends. On a paid plan that is 24 hours
|
|
369
|
+
by default (`maxRuntimeMin` on create), up to 7 days, and every resume
|
|
370
|
+
restarts the clock with a fresh `maxRuntimeMin`; a paused sandbox's clock
|
|
371
|
+
does not run, and it is kept, memory and disk, until you delete it (or its
|
|
372
|
+
`autoDeleteMin` does). On the free tier it is 60 minutes of running in all:
|
|
373
|
+
paused time does not count, a resume carries on the time left, and a paused
|
|
374
|
+
one is kept 30 days after its last use.
|
|
375
|
+
|
|
376
|
+
Change it while the sandbox runs with the CLI or the API
|
|
377
|
+
([Sandbox API: Run time](https://prized.dev/docs/sandboxes-api#run-time)):
|
|
378
|
+
|
|
379
|
+
```sh
|
|
380
|
+
prized sandbox extend sbx_0123456789abcdef --by 2h # add 2 hours; never shortens (POST /api/v1/sandboxes/{id}/extend)
|
|
381
|
+
prized sandbox timeout sbx_0123456789abcdef --in 30m # deleted 30 minutes from now; may shorten (POST …/timeout)
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
On a paid plan an extend stops at 7 days from now; on the free tier neither
|
|
385
|
+
moves the end later.
|
|
386
|
+
|
|
387
|
+
<!-- Held while sandbox ports at a URL are switched off: this section stays
|
|
388
|
+
out of the rendered README until they are on. In the change that turns them
|
|
389
|
+
on, delete these five lines and the closing line after the section, take
|
|
390
|
+
"a sandbox's ports at a URL" out of "Not yet" under Limits, and delete the
|
|
391
|
+
test "the README holds A port at a URL" (test/preview.test.ts).
|
|
392
|
+
## A port at a URL
|
|
393
|
+
|
|
394
|
+
`sandbox.previewUrl(port)` is the https URL a port of the sandbox answers at:
|
|
395
|
+
a web app, an API, or a dev server listening on that port in the sandbox on
|
|
396
|
+
`127.0.0.1` or `0.0.0.0`. HTTP, websockets and server-sent events are
|
|
397
|
+
forwarded. A request needs a workspace-wide API key with Sandboxes: Write in
|
|
398
|
+
the `Prized-Key` header (`previewHeaders()`; the header never reaches your
|
|
399
|
+
app).
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
await sandbox.exec('nohup python3 -m http.server 3000 >/dev/null 2>&1 &');
|
|
403
|
+
const page = await fetch(sandbox.previewUrl(3000), {
|
|
404
|
+
headers: sandbox.previewHeaders(),
|
|
405
|
+
redirect: 'manual', // never resend your key to wherever a redirect points
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Keep `redirect: 'manual'`. `fetch` sends a header like `Prized-Key` on to
|
|
410
|
+
any host a redirect names (so do axios, got, Python's requests and
|
|
411
|
+
`curl -L`), so a redirect from your app could hand that host your API key.
|
|
412
|
+
The preview URL keeps a redirect on its own origin: a `Location` on
|
|
413
|
+
`localhost:<port>` (where your app sees itself) comes back as that path on
|
|
414
|
+
the preview URL, and one that points anywhere else comes back as
|
|
415
|
+
`Prized-Location` instead, which no client follows. To follow a redirect,
|
|
416
|
+
resolve its `Location` against the preview URL and send `previewHeaders()`
|
|
417
|
+
again only when the result has the preview URL's origin, scheme included.
|
|
418
|
+
Call preview URLs from server-side code: a browser hides `Location` from
|
|
419
|
+
`'manual'`.
|
|
420
|
+
|
|
421
|
+
Nothing is created: the URL is the sandbox's own, and every port has one
|
|
422
|
+
(but 22 and 81). A request to a paused sandbox resumes it and waits up to 10
|
|
423
|
+
seconds for it, as `exec` does; send `Prized-Auto-Resume: false` to get
|
|
424
|
+
`503` instead. `previewUrl` throws `preview_unavailable` where this is not
|
|
425
|
+
turned on.
|
|
426
|
+
-->
|
|
427
|
+
|
|
428
|
+
## Fork
|
|
429
|
+
|
|
430
|
+
`sandbox.fork({ count })` copies a **running or paused** sandbox into `count`
|
|
431
|
+
new sandboxes. Each copy has the parent's disk and every one of its processes
|
|
432
|
+
running, and is an ordinary sandbox from then on: its own id, its own
|
|
433
|
+
billing, its own pause, resume and delete. A paused parent stays paused.
|
|
434
|
+
The copies keep the parent's labels unless you pass `labels`, which gives
|
|
435
|
+
them only the labels you pass.
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
await sandbox.exec('git clone https://github.com/acme/app && cd app && npm ci');
|
|
439
|
+
const copies = await sandbox.fork({ count: 4, labels: { role: 'worker' } });
|
|
440
|
+
const results = await Promise.all(copies.map((copy, i) => copy.exec(`cd app && npm test -- --shard=${i + 1}/4`)));
|
|
441
|
+
await Promise.all(copies.map((copy) => copy.delete()));
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
`fork` resolves once every copy is running. It is
|
|
445
|
+
all or nothing: if any copy fails, or the wait runs out, every copy is
|
|
446
|
+
deleted and `fork` throws (the parent is left as it was). It takes
|
|
447
|
+
`wait: false`, `timeoutMs` (the request and the wait together), and
|
|
448
|
+
`idempotencyKey` like `create`. While forking is paused for maintenance it
|
|
449
|
+
answers `sandbox_fork_disabled`: retry later.
|
|
450
|
+
|
|
451
|
+
On a paid workspace each copy gets a fresh lifetime from the fork: the
|
|
452
|
+
parent's `maxRuntimeMin` by default, or `maxRuntimeMin` on the fork (up to
|
|
453
|
+
the workspace's maximum). On the free tier, copies keep the parent's
|
|
454
|
+
deletion time by default; `maxRuntimeMin` can only shorten it. Read each
|
|
455
|
+
copy's deletion time from its `expiresAt`.
|
|
456
|
+
|
|
457
|
+
## Templates
|
|
458
|
+
|
|
459
|
+
```ts
|
|
460
|
+
const templates = await Sandbox.templates();
|
|
461
|
+
// [{ id: 'prized-base-v1', defaults: { vcpu, memoryMb, memoryGb, diskGb }, min, max, durability, defaultMaxRuntimeMin }]
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
| Template | What is in it |
|
|
465
|
+
|---|---|
|
|
466
|
+
| `prized-base-v1` | Ubuntu, git, curl, build tools, Python 3, Node 22 |
|
|
467
|
+
| `prized-agent-v1` | everything in base, plus Claude Code and Codex (bring your own credentials) |
|
|
468
|
+
|
|
469
|
+
## Errors
|
|
470
|
+
|
|
471
|
+
Every failure is a `PrizedError`:
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
import { PrizedError, Sandbox } from '@prized/sdk';
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
await Sandbox.create();
|
|
478
|
+
} catch (err) {
|
|
479
|
+
if (err instanceof PrizedError) {
|
|
480
|
+
err.status; // HTTP status, or null when nothing came back
|
|
481
|
+
err.code; // machine-readable: branch on this
|
|
482
|
+
err.message; // a sentence for people
|
|
483
|
+
err.reason; // finer detail, when there is one
|
|
484
|
+
err.sandboxId; // the sandbox it concerns, when there is one
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
| `code` | Status | Meaning |
|
|
490
|
+
|---|---|---|
|
|
491
|
+
| `paid_balance_required` | 409 | The workspace has only free credit, and sandboxes need a paid balance right now. Add funds or pick a paid plan. |
|
|
492
|
+
| `insufficient_balance` | 409 | The balance is empty. |
|
|
493
|
+
| `sandbox_quota_exceeded` | 429 | A workspace quota is full; `reason` says which: `sandbox_limit`, `paused_limit`, `running_vcpu`, `running_memory`, `retained_disk`, `max_runtime`. Paused sandboxes don't count toward `sandbox_limit`, but they keep their disk, so delete the ones you no longer need, or ask for more. On the free tier the body says `"tier": "free"` and gives `limit` in the reason's unit (a count, vCPU, MB, GB, minutes; `err.body?.limit`): a top-up lifts it. |
|
|
494
|
+
| `template_not_found` | 404 | No such template. |
|
|
495
|
+
| `invalid_resources` | 422 | The shape is outside the template's bounds, or past the free tier's per-sandbox size (`"tier": "free"` and `limit`, in vCPU or MB, in the body); `reason` names the field. |
|
|
496
|
+
| `unauthorized` / `token_expired` | 401 | The API key was refused. |
|
|
497
|
+
| `key_access_denied` | 403 | The key lacks the Sandboxes permission. |
|
|
498
|
+
| `not_found` | 404 | No such sandbox (or file) visible to this key. For a sandbox, most often one deleted when its run time ended; the message names it. |
|
|
499
|
+
| `sandbox_not_running` | 409 | The sandbox is not running and the call could not wake it: it is resizing (`refresh()` until it leaves `resizing`, then retry), failed, or being deleted. A paused sandbox is resumed by any command or file call. The SDK never turns waking off; a request that carries `Prized-Auto-Resume: false` (added by a custom `fetch`, say) gets this for a paused or starting sandbox too: drop the header to let the call wake it and wait. |
|
|
500
|
+
| `sandbox_not_ready` | 503 | A paused or starting sandbox did not reach running within the edge's 10-second wait. The wait starts when the edge, having checked the key, finds the sandbox not running, and calls with the same key arriving during it share it, so a later call may wait less. Nothing ran; retry. |
|
|
501
|
+
| `parent_not_running` | 409 | Only a running or paused sandbox can be forked. |
|
|
502
|
+
| `parent_not_forkable` | 409 | This sandbox cannot be forked. `reason` `resized_while_paused`: it has no memory to copy; resume it, then fork it. `reason` `parent_too_big`: its machine cannot hold even one copy beside it; fork a smaller sandbox. |
|
|
503
|
+
| `fork_capacity` | 409 | The parent's machine has room for only `fits` more copies (`err.body?.fits`). If `fits` is above 0, fork that many or fewer; or try again later, or start a new sandbox and fork that one, keeping the parent until it runs. The new sandbox and each copy count toward your quota: at the limit, delete sandboxes you no longer need with `delete({ wait: true })` (a deleting sandbox still counts), or top up on the free tier. Copy out what you need first, and never delete a parent while a copy forked from it is still starting. |
|
|
504
|
+
| `fork_unsupported_host` | 409 | The parent's machine cannot start copies of it. With `reason` `host_retiring` (`err.reason`), the machine is being replaced: it keeps the parent but starts no new sandboxes, so no retry or smaller `count` helps. Create a new sandbox, set it up again, and fork that one. |
|
|
505
|
+
| `sandbox_capacity` | 503 | No sandbox capacity is free right now. Nothing was created or billed; the SDK retries it after `Retry-After`. |
|
|
506
|
+
| `sandbox_fork_disabled` | 503 | Forking is paused for maintenance; retry later. |
|
|
507
|
+
| `sandboxes_disabled` / `sandbox_create_disabled` | 503 | Sandboxes are switched off right now. |
|
|
508
|
+
| `rate_limited` | 429 | Too many requests (retried for you, honouring `Retry-After`). |
|
|
509
|
+
| `sandbox_unreachable` | 502 | The sandbox did not answer. A command may or may not have run. |
|
|
510
|
+
| `path_outside_root` / `path_not_absolute` / `invalid_path` | 403 / 400 | A file path outside `/home/prized`, or with `..`. |
|
|
511
|
+
| `file_too_large` | 413 | Over the 100 MiB transfer limit. |
|
|
512
|
+
| `idempotency_conflict` | 409 | The idempotency key was used for a different request. |
|
|
513
|
+
| `sandbox_error` | | The sandbox failed while you waited; `reason` says why. |
|
|
514
|
+
| `sandbox_deleted` | 410 | The sandbox was deleted before or while a command or file call ran; a command that was running stopped with it. `pause()` and `resume()` of a sandbox being deleted throw it too. Not retried. A wait that finds the sandbox deleted throws it too, with no status. |
|
|
515
|
+
| `command_timeout` | | A command ran past its own `timeoutMs` and was killed. Thrown as `CommandTimeoutError`; `err.result` has what it printed. |
|
|
516
|
+
| `timeout` | | A wait ran past its `timeoutMs`, or a call past its bound ([Retries](#retries-and-timeouts)). One that ended because a retry could not start in time says what the last attempt met (also in `cause`). |
|
|
517
|
+
| `network_error` | | No answer: the API or the sandbox could not be reached, or the connection dropped. |
|
|
518
|
+
| `bad_response` | 2xx | An answer the SDK could not read. For a command or file write it did happen, and the message says so. |
|
|
519
|
+
| `missing_api_key` / `invalid_api_key` / `invalid_argument` | | Fix the call. `invalid_api_key`: the key has characters no key has (the docs' placeholder, a stray character); it is never quoted back. |
|
|
520
|
+
| `command_failed` | | A file helper (`list`, `remove`, `exists`) failed in the sandbox; `message` has its stderr. |
|
|
521
|
+
|
|
522
|
+
## Retries and timeouts
|
|
523
|
+
|
|
524
|
+
- Reads, state changes (`pause`, `resume`, `delete`, `keepAlive`),
|
|
525
|
+
downloads, and creates and forks (which always carry an idempotency key)
|
|
526
|
+
are retried up to `maxRetries` times on a dropped connection, on 429, 502,
|
|
527
|
+
503 and 504, and on every answer a command is retried on (below), with
|
|
528
|
+
exponential backoff and jitter, honouring `Retry-After`. A read
|
|
529
|
+
(`Sandbox.list`, `connect`, `refresh`, `templates`, a file download, and
|
|
530
|
+
the polling while `create`, `pause`, `resume`, `fork` or `delete` waits) is
|
|
531
|
+
retried on any other 500 too; a write is not, since it may already have
|
|
532
|
+
happened. Quota, balance and switched-off refusals are never retried.
|
|
533
|
+
- Retries stay within a call's bound: `timeoutMs` for `create`, `pause`,
|
|
534
|
+
`resume`, `fork` and `delete` (the request, its retries and the wait, its
|
|
535
|
+
polls included; with `wait: false` too), twice `requestTimeoutMs` (2
|
|
536
|
+
minutes by default) for `connect`, `list`, `templates`, `refresh` and
|
|
537
|
+
`keepAlive`, and 10 minutes for a file transfer. `Retry-After` is honoured
|
|
538
|
+
up to a minute. A retry that could not start in time is not sent: the call
|
|
539
|
+
throws `timeout`. A wait's last poll goes out just before its deadline.
|
|
540
|
+
- A command, an upload (`files.write`, `files.upload`), a `files.remove` or
|
|
541
|
+
a `kill` is sent again only when it provably never arrived: the connection never
|
|
542
|
+
opened, or the answer says so (rate limited, the sandbox not ready). If an
|
|
543
|
+
answer is lost after it was delivered, the SDK throws
|
|
544
|
+
(`sandbox_unreachable`, `network_error`, `timeout`) rather than risk running a command
|
|
545
|
+
twice or overwriting what the sandbox wrote to the file since.
|
|
546
|
+
- Did it run? When a command answered in one call (`exec` without
|
|
547
|
+
`background`, `onStdout` or `onStderr`, and a `timeoutMs` other than 0 and
|
|
548
|
+
at most 4 minutes), a file write or a `files.remove` fails without a plain refusal,
|
|
549
|
+
the error keeps its `code` (a 2xx it could not read is `bad_response`)
|
|
550
|
+
and its `message` says which of three it was. It never reached the sandbox (the connection never opened, or
|
|
551
|
+
the answer proves it, such as `sandbox_not_ready`): it had no effect. No
|
|
552
|
+
clear answer came back (a dropped connection, a timeout, or any other 5xx):
|
|
553
|
+
it may or may not have run, and the message says how to check (read the
|
|
554
|
+
file back, `files.exists()`). A 2xx came back that could not be read
|
|
555
|
+
(`bad_response`): it ran. A `kill` says the same of its signal
|
|
556
|
+
(`status()` tells you whether the command is still running). A background
|
|
557
|
+
command (or a streamed or long `exec`, which runs as one) whose start got
|
|
558
|
+
no clear answer is stopped by the SDK, and the error says so, or, when the
|
|
559
|
+
SDK could not confirm the stop, that it may still be running and under
|
|
560
|
+
which run id. So is a streamed or long `exec` whose polling fails while
|
|
561
|
+
it runs (a dropped connection or a 5xx for longer than `pollOutageMs`, a
|
|
562
|
+
refused key): the error keeps the poll's `code` and `status`, has the
|
|
563
|
+
poll's own error as its `cause`, and says whether the SDK stopped the
|
|
564
|
+
command or it may still be running. An error that settles it (the
|
|
565
|
+
sandbox deleted or not running, the run not found) is thrown as it came.
|
|
566
|
+
- Deleting what a failed create or fork made (a few seconds) comes on top
|
|
567
|
+
of `timeoutMs`.
|
|
568
|
+
- `create`, `exec` and waits take an `AbortSignal` (`signal`). Aborting a
|
|
569
|
+
`create` or a handle's `wait()` stops the call, not the sandbox or the
|
|
570
|
+
command (`handle.kill()` stops a command). Aborting an `exec` stops its
|
|
571
|
+
command too, as does anything else that ends a streamed or long one early
|
|
572
|
+
(an `onStdout` that throws, say): the call rejects once the SDK has tried
|
|
573
|
+
to stop it (a round trip or two; it gives up after about 20 seconds). With
|
|
574
|
+
`background: true` that covers the start only. The `aborted` error says
|
|
575
|
+
what became of the command: it never reached the sandbox (aborted before
|
|
576
|
+
it was sent); it may or may not have run, or run in part, since the
|
|
577
|
+
sandbox kills a command (and its process group) whose connection closes;
|
|
578
|
+
it ran (its answer had begun to arrive); or the SDK stopped its run, or
|
|
579
|
+
could not.
|
|
580
|
+
- A `create` or `fork` that may have made sandboxes it never handed back
|
|
581
|
+
deletes them, as a failed create does (in the background; the call rejects
|
|
582
|
+
at once): one aborted mid-request, or one where any attempt got no answer,
|
|
583
|
+
a 5xx other than a refusal, or an answer the SDK could not read, even if a
|
|
584
|
+
later retry was refused (rate limited, say). The SDK finds them by sending
|
|
585
|
+
the idempotency key again, after any `Retry-After` (up to a minute). With
|
|
586
|
+
your own `idempotencyKey` they are left instead, since that key may name an
|
|
587
|
+
earlier call's sandboxes: resend the call with the key to get them back.
|
|
588
|
+
This cleanup is best effort: if Prized cannot be reached, or stops taking
|
|
589
|
+
new sandboxes meanwhile (`sandbox_create_disabled`), one may be left until
|
|
590
|
+
its run time ends, or, if it pauses first (`autoPauseMin`), until you
|
|
591
|
+
delete it (free tier: 30 days after its last use). Label your sandboxes, and `Sandbox.list({ labels })`
|
|
592
|
+
finds any leftovers.
|
|
593
|
+
|
|
594
|
+
## Limits
|
|
595
|
+
|
|
596
|
+
| | |
|
|
597
|
+
|---|---|
|
|
598
|
+
| Shape | up to 32 vCPU, 128 GB memory and 128 GB disk per sandbox, within each template's bounds (`Sandbox.templates()`) |
|
|
599
|
+
| Run time | paid plans: 24 hours by default, up to 7 days, restarting on every resume; the free tier: 60 minutes of running in all. Extend anytime with the CLI or API |
|
|
600
|
+
| Paused | kept until you delete it; on the free tier, 30 days after its last use |
|
|
601
|
+
| `autoPauseMin`, `autoDeleteMin` | 0 (never) to 40 320 (28 days) |
|
|
602
|
+
| Labels | up to 32; keys up to 64 characters, values up to 256 |
|
|
603
|
+
| `envVars` | up to 32; names up to 64 characters, values up to 16 KiB |
|
|
604
|
+
| Command output | 4 MiB per stream for one blocking call, 100 MiB with `fullOutput: true` (background and streamed commands have no cap) |
|
|
605
|
+
| One blocking command | up to 4 minutes in the SDK (longer timeouts run in the background automatically) |
|
|
606
|
+
| File transfers | 100 MiB per read or write, inside `/home/prized` |
|
|
607
|
+
| Fork | up to 256 copies per call, within your quotas |
|
|
608
|
+
|
|
609
|
+
Quotas per workspace (sandboxes, running vCPU and memory, retained disk,
|
|
610
|
+
runtime) are on the pricing and docs pages at prized.dev; a create or resume
|
|
611
|
+
past one fails with `sandbox_quota_exceeded`.
|
|
612
|
+
|
|
613
|
+
Not yet: custom templates (from a Dockerfile), egress allow and deny lists,
|
|
614
|
+
a sandbox's ports at a URL, more than 32 vCPU or 128 GB, GPUs, and regions
|
|
615
|
+
other than US West.
|
|
616
|
+
|
|
617
|
+
## Runtimes
|
|
618
|
+
|
|
619
|
+
Node 18 or newer, Bun, Deno, and edge runtimes with a global `fetch`.
|
|
620
|
+
`files.upload` and `files.download` need a local file system (Node, Bun,
|
|
621
|
+
Deno); everywhere else use `files.write` and `files.readBytes`. Keep your
|
|
622
|
+
API key on a server: never ship it to a browser.
|
|
623
|
+
|
|
624
|
+
## License
|
|
625
|
+
|
|
626
|
+
Apache-2.0. The full text is in the `LICENSE` file that ships with the package.
|