From 01169fcc6ccc5b3a66aead1558b06c7dd064346d Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Tue, 1 Sep 2026 17:23:21 -0700 Subject: [PATCH 01/11] =?UTF-8?q?docs(microvm):=20add=20a=20parallel=20Mic?= =?UTF-8?q?roVM=20docs=20tab=20so=20customers=20can=20prepare=20to=20migra?= =?UTF-8?q?te=20=E2=80=94=2021=20pages=20covering=20lifecycle,=20the=208-h?= =?UTF-8?q?our=20ceiling=20and=20endAt,=20filesystem-only=20checkpoints,?= =?UTF-8?q?=20sizes,=20the=20in-guest=20secrets=20tradeoff=20and=20the=20s?= =?UTF-8?q?upported/unsupported=20split,=20with=20the=20existing=20Sandbox?= =?UTF-8?q?es=20docs=20left=20untouched=20and=20still=20first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.json | 88 +++++++++++++++++++++++++++--- docs/microvm/checkpoints.mdx | 66 +++++++++++++++++++++++ docs/microvm/errors.mdx | 42 +++++++++++++++ docs/microvm/exec.mdx | 72 +++++++++++++++++++++++++ docs/microvm/faq.mdx | 56 +++++++++++++++++++ docs/microvm/files.mdx | 57 ++++++++++++++++++++ docs/microvm/hibernate.mdx | 49 +++++++++++++++++ docs/microvm/idle-timeout.mdx | 42 +++++++++++++++ docs/microvm/lifecycle.mdx | 62 +++++++++++++++++++++ docs/microvm/lifetime.mdx | 86 +++++++++++++++++++++++++++++ docs/microvm/migrate.mdx | 83 ++++++++++++++++++++++++++++ docs/microvm/not-supported.mdx | 48 +++++++++++++++++ docs/microvm/overview.mdx | 69 ++++++++++++++++++++++++ docs/microvm/preview-urls.mdx | 34 ++++++++++++ docs/microvm/quickstart.mdx | 92 ++++++++++++++++++++++++++++++++ docs/microvm/sdk-differences.mdx | 59 ++++++++++++++++++++ docs/microvm/secrets.mdx | 59 ++++++++++++++++++++ docs/microvm/security-model.mdx | 65 ++++++++++++++++++++++ docs/microvm/sizes.mdx | 50 +++++++++++++++++ docs/microvm/templates.mdx | 39 ++++++++++++++ docs/microvm/terminals.mdx | 38 +++++++++++++ docs/microvm/whats-different.mdx | 49 +++++++++++++++++ 22 files changed, 1298 insertions(+), 7 deletions(-) create mode 100644 docs/microvm/checkpoints.mdx create mode 100644 docs/microvm/errors.mdx create mode 100644 docs/microvm/exec.mdx create mode 100644 docs/microvm/faq.mdx create mode 100644 docs/microvm/files.mdx create mode 100644 docs/microvm/hibernate.mdx create mode 100644 docs/microvm/idle-timeout.mdx create mode 100644 docs/microvm/lifecycle.mdx create mode 100644 docs/microvm/lifetime.mdx create mode 100644 docs/microvm/migrate.mdx create mode 100644 docs/microvm/not-supported.mdx create mode 100644 docs/microvm/overview.mdx create mode 100644 docs/microvm/preview-urls.mdx create mode 100644 docs/microvm/quickstart.mdx create mode 100644 docs/microvm/sdk-differences.mdx create mode 100644 docs/microvm/secrets.mdx create mode 100644 docs/microvm/security-model.mdx create mode 100644 docs/microvm/sizes.mdx create mode 100644 docs/microvm/templates.mdx create mode 100644 docs/microvm/terminals.mdx create mode 100644 docs/microvm/whats-different.mdx diff --git a/docs/docs.json b/docs/docs.json index 740bbb43a..d73290d42 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -65,7 +65,10 @@ }, { "group": "Test and operate", - "pages": ["agents/playground", "agents/logs"] + "pages": [ + "agents/playground", + "agents/logs" + ] }, { "group": "Examples", @@ -84,7 +87,11 @@ "groups": [ { "group": "Getting Started", - "pages": ["introduction", "quickstart", "how-it-works"] + "pages": [ + "introduction", + "quickstart", + "how-it-works" + ] }, { "group": "Sandboxes", @@ -108,14 +115,18 @@ "group": "Usage", "tag": "Preview", "expanded": true, - "pages": ["sandboxes/usage"] + "pages": [ + "sandboxes/usage" + ] } ] }, { "group": "Browser Sessions", "tag": "Preview", - "pages": ["browser-sessions/overview"] + "pages": [ + "browser-sessions/overview" + ] }, { "group": "Reserved Capacity", @@ -159,7 +170,9 @@ "group": "Usage & Tags", "tag": "Preview", "expanded": true, - "pages": ["reference/typescript-sdk/usage"] + "pages": [ + "reference/typescript-sdk/usage" + ] }, "reference/typescript-sdk/exec", "reference/typescript-sdk/filesystem", @@ -331,11 +344,72 @@ }, { "group": "Resources", - "pages": ["troubleshooting"] + "pages": [ + "troubleshooting" + ] }, { "group": "Self-hosting", - "pages": ["self-hosting/overview", "self-hosting/gcp-development"] + "pages": [ + "self-hosting/overview", + "self-hosting/gcp-development" + ] + } + ] + }, + { + "tab": "Sandboxes · MicroVM", + "groups": [ + { + "group": "Start here", + "pages": [ + "microvm/overview", + "microvm/whats-different", + "microvm/quickstart", + "microvm/migrate" + ] + }, + { + "group": "Lifecycle", + "pages": [ + "microvm/lifecycle", + "microvm/lifetime", + "microvm/idle-timeout", + "microvm/hibernate" + ] + }, + { + "group": "Working in a sandbox", + "pages": [ + "microvm/exec", + "microvm/files", + "microvm/terminals", + "microvm/preview-urls" + ] + }, + { + "group": "State", + "pages": [ + "microvm/checkpoints", + "microvm/templates", + "microvm/secrets" + ] + }, + { + "group": "Limits", + "pages": [ + "microvm/not-supported", + "microvm/sizes", + "microvm/security-model" + ] + }, + { + "group": "Reference", + "pages": [ + "microvm/sdk-differences", + "microvm/errors", + "microvm/faq" + ] } ] } diff --git a/docs/microvm/checkpoints.mdx b/docs/microvm/checkpoints.mdx new file mode 100644 index 000000000..1888e6c93 --- /dev/null +++ b/docs/microvm/checkpoints.mdx @@ -0,0 +1,66 @@ +--- +title: "Checkpoints" +description: "Filesystem snapshots — what they capture, and what they do not" +--- + +A checkpoint captures the sandbox's **filesystem**. Restoring puts those files back. + +```typescript +const cp = await sandbox.createCheckpoint("after-install"); +// wait for status "ready" + +await sandbox.restoreCheckpoint(cp.checkpointId); +``` + + +Checkpoints on this runtime **do not capture live memory**. Restoring gives you your files back +on a freshly booted sandbox — not a process tree resumed mid-execution. + + +## Why + +The current runtime can snapshot a running VM's memory because we operate the hypervisor. The +MicroVM platform exposes no snapshot or memory-export operation at all, so a filesystem archive +is the only checkpoint that can exist here. This is an API ceiling, not a missing feature. + +## What this changes for you + +**If you checkpoint an installed environment** — dependencies, a built toolchain, a seeded +database directory — nothing changes. That is a filesystem, and it restores exactly. + +**If you checkpoint a warmed process** — a loaded model, an open connection pool, a debugger +paused at a breakpoint — that state is gone on restore. Your process starts again from scratch, +with its files in place. + +The practical test: if you would be happy with the result of `reboot` plus your files, a +checkpoint gives you that. + +## The `kind` argument + +The current runtime accepts `kind: "full"` for a memory-inclusive checkpoint. Here: + +- omitting `kind`, or asking for `disk_only`, works and produces a filesystem checkpoint +- explicitly asking for `full` is **refused**, rather than quietly giving you a disk-only + checkpoint under a name that promises more + +```typescript +await sandbox.createCheckpoint("env"); // fine +await sandbox.createCheckpoint("env", { kind: "disk_only" }); // fine +await sandbox.createCheckpoint("env", { kind: "full" }); // refused +``` + +## Restore is in-place + +`restoreCheckpoint` rolls the current sandbox's filesystem back to the checkpoint. It does not +create a new sandbox. + + +**Forking a new sandbox from a checkpoint is not available yet.** `createFromCheckpoint` fails +on this runtime. It is being worked on. Until then, restore into an existing sandbox. + + +## Checkpoints outlive their sandbox + +A checkpoint is stored durably and is not bound to the sandbox that produced it. That makes it +the tool for surviving the [8-hour ceiling](/microvm/lifetime): checkpoint before the deadline, +then restore onto a sandbox you create later. diff --git a/docs/microvm/errors.mdx b/docs/microvm/errors.mdx new file mode 100644 index 000000000..46eb38e0c --- /dev/null +++ b/docs/microvm/errors.mdx @@ -0,0 +1,42 @@ +--- +title: "Errors" +description: "What the distinct failures mean and whether retrying helps" +--- + +The runtime tries to make "retry" versus "change your request" obvious from the status code. + +## Creates + +| Status | Meaning | Retry? | +|---|---|---| +| `400` requested size not available | The size has no published image here. The message lists the sizes that do exist. | No — pick a listed size. | +| `503` out of capacity | A genuine shortage. | Yes, once something frees up. No `Retry-After`, because hammering a full region only adds rate limiting. | +| `503` rate limited | Creates are arriving faster than the provider accepts. | Yes — a `Retry-After: 1` header is set. | +| `429` concurrent sandbox limit | Your org is at its concurrency cap. | Yes, after ending a sandbox. | + +The distinction between the first two matters: an unavailable size is permanent for that +request, and reporting it as capacity would send you to support about a shortage that does not +exist. + +## Operations on a sandbox + +| Status | Meaning | +|---|---| +| `501` not supported for this sandbox | The operation does not exist on this runtime — `scale`, a `full` checkpoint, mounts. See [What's not supported](/microvm/not-supported). | +| `404` | The sandbox is gone. If it was hibernated, it may have reached its lifetime ceiling — check `endAt`. | +| `502` could not attach | The connection to the sandbox failed. Usually transient; retry once. | + + +A `501` is a statement about the runtime, not about your sandbox. Retrying will not change it, +and neither will recreating the sandbox. + + +## Things that used to be silent + +Several failures that previously looked like success now report honestly. If you have code that +assumed a `200` meant the operation happened, these are worth knowing about: + +- Asking for an idle timeout longer than the lifetime ceiling returns `applied: false` rather + than accepting a value that could never fire. +- A `full` checkpoint is refused rather than quietly producing a disk-only one. +- An unavailable size is refused rather than silently serving a different size. diff --git a/docs/microvm/exec.mdx b/docs/microvm/exec.mdx new file mode 100644 index 000000000..a83a86ab1 --- /dev/null +++ b/docs/microvm/exec.mdx @@ -0,0 +1,72 @@ +--- +title: "Running commands" +description: "One-shot commands and streaming sessions" +--- + +Command execution is unchanged from the current runtime. The same calls, the same semantics. + +## One-shot + +```typescript +const r = await sandbox.exec.run("ls -la /home/sandbox"); +r.stdout; // captured +r.stderr; // captured separately +r.exitCode; // the real exit code, not thrown +``` + +A non-zero exit is a value, not an exception — `exit 42` gives you `exitCode: 42`. + +Options work as you would expect: + +```typescript +await sandbox.exec.run("pwd && echo $TOKEN", { + cwd: "/tmp", + env: { TOKEN: "abc" }, + timeout: 120, +}); +``` + +## Streaming + +For long-running commands, or when you want output as it happens: + +```typescript +const session = await sandbox.exec.start("npm", { + args: ["run", "build"], + onStdout: (chunk) => process.stdout.write(new TextDecoder().decode(chunk)), + onStderr: (chunk) => process.stderr.write(new TextDecoder().decode(chunk)), + onExit: (code) => console.log("exited", code), +}); + +const exitCode = await session.done; +``` + +Stdout and stderr stay distinguishable, which matters if you redirect one and not the other. + +## Background work + +A session can outlive the call that started it: + +```typescript +const session = await sandbox.exec.background("python", { args: ["worker.py"] }); + +const sessions = await sandbox.exec.list(); +await sandbox.exec.kill(session.sessionId); +``` + + +The handle returned by `start`/`background` exposes `sessionId`, while entries from +`exec.list()` use `sessionID`. Both are typed correctly, but the casing differs — worth knowing +before you write a `.find()` against it. + + +## Reattaching + +```typescript +const session = await sandbox.exec.attach(sessionId, { + onStdout: (chunk) => { /* ... */ }, +}); +``` + +Attaching replays recent scrollback and then streams live output, so a client that reconnects +sees what it missed. diff --git a/docs/microvm/faq.mdx b/docs/microvm/faq.mdx new file mode 100644 index 000000000..5aa920ac4 --- /dev/null +++ b/docs/microvm/faq.mdx @@ -0,0 +1,56 @@ +--- +title: "FAQ" +description: "Questions worth answering before you migrate" +--- + + + + No. Your sandboxes keep running on the current runtime until your org is moved, and the + existing [Sandboxes](/introduction) docs stay accurate for as long as that is true. + + + + No. It is enforced by the platform underneath, not by a quota on our side, and it counts + hibernated time as well as running time. If you have work that needs a longer-lived + sandbox, it needs restructuring — see [Sandbox lifetime](/microvm/lifetime). + + + + Filesystem checkpoints restore normally. Checkpoints that captured live memory restore + their filesystem only — the memory image is ignored, and you get a fresh boot with your + files. + + + + For most workloads, no. Check for `scale`, `setAutoscale`, mounts, image builds and + `createFromCheckpoint`, and review anywhere you assume a sandbox lives indefinitely. The + checklist is on [Prepare to migrate](/microvm/migrate). + + + + Creates are, because sandboxes come from a warm pool rather than being scheduled onto a + host. Command execution and file operations are comparable. + + + + The guest runs without `CAP_SYS_ADMIN`, and it is absent from the capability bounding set, + so `mount` cannot be performed even by root. The same restriction removes a large class of + escape techniques. Copy data in, or read it over the network instead. + + + + Each one is archived to durable storage and its host released, so charges stop. When the + org resumes they are restored onto new hosts with their files intact. Running processes are + not preserved. + + + + Not within a single org — an org is on one runtime at a time. Testing against a separate + non-production org is the usual approach, and the same code runs against both. + + + + Tell us. That list is prioritised work, not a fixed decision, and knowing what is blocking + a real migration is what changes the order. + + diff --git a/docs/microvm/files.mdx b/docs/microvm/files.mdx new file mode 100644 index 000000000..5a33ec967 --- /dev/null +++ b/docs/microvm/files.mdx @@ -0,0 +1,57 @@ +--- +title: "Files" +description: "Reading and writing the sandbox filesystem" +--- + +Filesystem access is unchanged from the current runtime. + +```typescript +await sandbox.files.write("/home/sandbox/notes.txt", "hello"); +const text = await sandbox.files.read("/home/sandbox/notes.txt"); + +await sandbox.files.makeDir("/home/sandbox/work"); +const entries = await sandbox.files.list("/home/sandbox"); + +await sandbox.files.exists("/home/sandbox/notes.txt"); // true +await sandbox.files.remove("/home/sandbox/notes.txt"); +``` + +## Binary and large files + +```typescript +await sandbox.files.write("/home/sandbox/blob.bin", new Uint8Array([0, 1, 2, 255])); +const bytes = await sandbox.files.readBytes("/home/sandbox/blob.bin"); +``` + +Large writes stream rather than buffering, so multi-megabyte files are fine over the same call. + +## Signed URLs + +For uploads and downloads that should not go through your process, mint a URL and hand it out: + +```typescript +const download = await sandbox.downloadUrl("/home/sandbox/report.pdf"); +const upload = await sandbox.uploadUrl("/home/sandbox/incoming.zip"); + +await fetch(upload, { method: "PUT", body: fileContents }); +``` + +Both work the same as on the current runtime. + +## What does not exist here + + +**Mounts are not available.** FUSE, NFS, overlay and 9p all require a capability the guest does +not have, so there is no way to attach external storage as a filesystem inside a MicroVM +sandbox. This is a platform limitation, not a feature gap we are working through. + + +If you use mounts to bring a dataset into the sandbox, the alternatives are to copy it in with +`files.write` or a signed upload URL, or to have your code read it directly from object storage +over the network. + +## Files and the lifetime ceiling + +The sandbox filesystem does not outlive the sandbox, and sandboxes end after at most 8 hours. +Anything you need to keep should be checkpointed or written somewhere durable before then — +see [Sandbox lifetime](/microvm/lifetime). diff --git a/docs/microvm/hibernate.mdx b/docs/microvm/hibernate.mdx new file mode 100644 index 000000000..383d43274 --- /dev/null +++ b/docs/microvm/hibernate.mdx @@ -0,0 +1,49 @@ +--- +title: "Hibernate and wake" +description: "Suspending a sandbox in place, and what that does not do" +--- + +```typescript +await sandbox.hibernate(); +// ... later +await sandbox.wake(); +``` + +Hibernating suspends the host. The filesystem, the process tree and anything in memory are all +exactly as you left them when you wake it. + +## What is different here + +On the current runtime, hibernation writes a checkpoint to storage and releases the host, so a +sandbox can stay hibernated indefinitely and wake anywhere. + +Here it is a suspend in place, which has two consequences: + + +**Hibernated time counts against the 8-hour ceiling.** A sandbox hibernated overnight reaches +its deadline while asleep and is destroyed. It does not wake up the next morning. + + +**The sandbox stays on its host.** It does not move, which is why waking is fast — but it also +means hibernation is not a way to park something long-term. + +## When to use it + +Hibernation is for gaps measured in minutes to a few hours: an agent waiting on a human, a +session between turns, a build waiting for input. Combined with an +[idle timeout](/microvm/idle-timeout) it is a cost control. + +It is not a way to preserve a sandbox across days. For that, checkpoint the filesystem and +create a fresh sandbox later — see [Checkpoints](/microvm/checkpoints). + +## Waking a sandbox that has ended + +If a hibernated sandbox reached its ceiling, it is gone. Waking it fails, and the sandbox +reports as stopped. Check `endAt` before relying on a wake: + +```typescript +const info = await getSandbox(sandboxId); +if (Date.parse(info.endAt) < Date.now()) { + // the sandbox is past its deadline — restore from a checkpoint instead +} +``` diff --git a/docs/microvm/idle-timeout.mdx b/docs/microvm/idle-timeout.mdx new file mode 100644 index 000000000..14e2a5aa2 --- /dev/null +++ b/docs/microvm/idle-timeout.mdx @@ -0,0 +1,42 @@ +--- +title: "Idle timeout" +description: "Auto-hibernate an idle sandbox, within the lifetime ceiling" +--- + +A sandbox can hibernate itself after a period with no activity, so you are not paying for a box +nobody is using. + +```typescript +await sandbox.setTimeout(3600); // hibernate after an hour idle +``` + +Activity means your requests — commands, file operations, terminal input. Internal health +checks do not count as activity, so a sandbox nobody is using really does go idle. + +## The response tells you what was applied + +An idle timeout cannot outlive the sandbox. Because the lifetime ceiling is 8 hours, a longer +timeout could never fire, so the API reports what it actually did rather than accepting a value +it cannot honour: + +```json +// POST /api/sandboxes/{id}/timeout {"timeout": 3600} +{ "applied": true, "requested": 3600, "timeout": 3600 } + +// POST /api/sandboxes/{id}/timeout {"timeout": 86400} +{ "applied": false, "requested": 86400, "timeout": 0 } +``` + + +Check `applied`. A request above the ceiling is refused rather than silently clamped, so you +find out at the call site instead of discovering later that your sandbox never slept. + + +## What waking costs + +Waking is a resume of the suspended host, so it is fast and your filesystem and processes are +exactly where you left them. + +The catch is that the time spent hibernated still counted against the 8-hour ceiling. An idle +timeout saves you money; it does not extend the sandbox's life. See +[Sandbox lifetime](/microvm/lifetime). diff --git a/docs/microvm/lifecycle.mdx b/docs/microvm/lifecycle.mdx new file mode 100644 index 000000000..5c6a129e1 --- /dev/null +++ b/docs/microvm/lifecycle.mdx @@ -0,0 +1,62 @@ +--- +title: "Lifecycle" +description: "The states a MicroVM sandbox moves through" +--- + +## States + +| Status | Meaning | +|---|---| +| `running` | Alive and serving requests. | +| `hibernated` | Suspended in place, or archived by an org halt. Wake to use it again. | +| `stopped` | Ended — by you, by its idle timeout, or by reaching its lifetime ceiling. | +| `error` | Failed to start, or failed in a way it could not recover from. | + +## Creating + +Creates are served from a warm pool, so a sandbox is usually ready in well under a second. + +```typescript +const sandbox = await Sandbox.create({ apiKey }); +console.log(sandbox.sandboxId); +``` + +The response carries `endAt` — the deadline the platform will enforce. See +[Sandbox lifetime](/microvm/lifetime). + +## Ending + +```typescript +await sandbox.kill(); +``` + +A sandbox also ends on its own when its idle timeout expires, or when it reaches its 8-hour +ceiling. All three land in `stopped`. + +## Restarting without losing the disk + +`reboot` restarts the guest. Processes are cleared; the filesystem is kept. + +```typescript +await sandbox.files.write("/home/sandbox/keep.txt", "still here"); +await sandbox.reboot(); +await sandbox.files.read("/home/sandbox/keep.txt"); // "still here" +``` + +This is the tool for recovering a wedged process without losing your work. It does not reset +the lifetime ceiling — the host underneath is the same host. + +## Org-level halts + +If an org is halted for non-payment, its sandboxes are **archived and their hosts released**: +the filesystem is written to durable storage, then the host is given back so charges stop. + +When the org resumes, each sandbox is restored onto a new host with its files intact. Running +processes are not preserved — it comes back as a fresh boot holding your data, the same as any +checkpoint restore on this runtime. + + +This differs from the current runtime, which parks halted sandboxes in place. It has to: a +suspended sandbox would still hit the 8-hour ceiling and be destroyed, and a credit halt often +lasts longer than that. + diff --git a/docs/microvm/lifetime.mdx b/docs/microvm/lifetime.mdx new file mode 100644 index 000000000..c0fe00d22 --- /dev/null +++ b/docs/microvm/lifetime.mdx @@ -0,0 +1,86 @@ +--- +title: "Sandbox lifetime" +description: "The 8-hour ceiling, endAt, and how to design around them" +--- + +Every MicroVM sandbox has a hard end time. When it arrives, the provider destroys the host and +the sandbox's disk goes with it. + + +The ceiling is **8 hours**, and it counts **running and hibernated time together**. It cannot be +extended, and hibernating does not pause it. + + +This is the single largest behavioural difference from the current runtime, where a sandbox +lives until something ends it. + +## Reading the deadline + +Every sandbox reports when it will end: + +```typescript +const info = await fetch(`${apiUrl}/api/sandboxes/${sandboxId}`, { + headers: { "X-API-Key": apiKey }, +}).then((r) => r.json()); + +console.log(info.status); // "running" +console.log(info.endAt); // "2026-09-02T04:30:26Z" +``` + +`endAt` means *when this sandbox ends*: the scheduled deadline while it is alive, and the actual +end time once it has stopped. + + +**Do not compute the deadline yourself.** Sandboxes are served from a warm pool, so the host +often started before your create did. A sandbox may report seven and a half hours remaining +rather than eight. `endAt` is the truth; `createdAt + 8h` is not. + + +## Designing around it + +The ceiling is only a problem for work that assumes a sandbox is where state lives. Two shapes +work: + +### Checkpoint and recreate + +Before the deadline, checkpoint the filesystem, create a fresh sandbox and restore onto it. + +```typescript +const remainingMs = Date.parse(info.endAt) - Date.now(); + +if (remainingMs < 30 * 60 * 1000) { // half an hour left + const cp = await sandbox.createCheckpoint(`rollover-${Date.now()}`); + // wait for the checkpoint to report ready, then create a new sandbox + // and restore onto it before this one ends +} +``` + +You pay a restart, and anything that was only in memory is gone. Files survive. + +### Externalise the state + +The alternative is to stop caring: keep durable state in object storage or a database, and +treat every sandbox as disposable. Work that already looks like this needs no changes at all — +the ceiling stops being visible. + +## What happens at the deadline + +The host is destroyed. The sandbox's API row moves to a terminal state and `endAt` becomes the +time it actually ended. Requests to it after that point fail like any other stopped sandbox. + +There is no grace period and no warning event. If you need to act before the deadline, poll +`endAt` and act on the margin yourself. + +## Hibernation does not buy time + +On the current runtime, hibernating a sandbox parks it indefinitely. Here it suspends the host +in place — and suspended time counts against the same 8 hours. + +A sandbox hibernated overnight will not be there in the morning. See +[Hibernate and wake](/microvm/hibernate). + +## Idle timeouts are clamped to the ceiling + +You can ask for an idle timeout, but not one that outlives the sandbox. A request above the +ceiling is reported back as not applied rather than silently accepted — see +[Idle timeout](/microvm/idle-timeout). diff --git a/docs/microvm/migrate.mdx b/docs/microvm/migrate.mdx new file mode 100644 index 000000000..15768b79a --- /dev/null +++ b/docs/microvm/migrate.mdx @@ -0,0 +1,83 @@ +--- +title: "Prepare to migrate" +description: "How to find out whether your workload is affected, before anything moves" +--- + +Nothing about your sandboxes changes until your org is moved. This page is here so you can find +out ahead of time whether that move is a non-event or something you need to plan. + +## Step 1 — answer four questions + +Most workloads are unaffected. These four are what separate the two cases: + + + + Include hibernated time — it counts. If yes, that work needs restructuring before you move. + See [Sandbox lifetime](/microvm/lifetime). + + + If your checkpoints capture a warmed process — a loaded model, an open connection pool — + restore will not bring it back. If they capture an installed environment, you are fine. + See [Checkpoints](/microvm/checkpoints). + + + None of these exist on the new runtime. See [What's not supported](/microvm/not-supported). + + + Both are unavailable today. Fork is being worked on; image builds are not scheduled. + + + +If all four are "no", your migration is a configuration change and you can stop reading here. + +## Step 2 — find the calls in your code + +Search for the surfaces that behave differently: + +```bash +# Hard failures on the new runtime +rg -n "\.scale\(|setAutoscale|\.mounts\b|createFromCheckpoint|buildImage|image:" + +# Worth reviewing, since the semantics change +rg -n "createCheckpoint|restoreCheckpoint|\.hibernate\(|setTimeout" +``` + +## Step 3 — handle the lifetime ceiling + +If you found long-lived sandboxes, the shape that works is to stop treating the sandbox as the +place your state lives: + + + + Checkpoint before the deadline, create a fresh sandbox, restore. Costs a restart; keeps + your files. + + + Keep durable state outside the sandbox, so any sandbox is disposable and the ceiling stops + mattering. + + + +Read `endAt` and act on it rather than assuming a wall-clock 8 hours from your create — a +pooled sandbox starts with less than the full budget. + +## Step 4 — re-read the security model if you use secrets + +Secrets still reach your sandbox as sealed placeholders and are still substituted only for the +hosts they are scoped to. What changed is where the real value is held: on the new runtime it +lives in a root-owned process **inside** your sandbox rather than on a separate machine. + +Unprivileged code in the sandbox cannot read it, but a privilege escalation inside the sandbox +reaches secrets that it would not have reached before. If you model that risk, read +[Security model](/microvm/security-model). + +## Step 5 — test against it + +Ask us to move a non-production org first. The same SDK and the same code run against both +runtimes, so your existing test suite is the test. + + +If something in your workload does not fit what is described here, tell us before you are +scheduled to move rather than after. The constraints on this page are real, but which of them +we prioritise is not fixed. + diff --git a/docs/microvm/not-supported.mdx b/docs/microvm/not-supported.mdx new file mode 100644 index 000000000..c6030b98d --- /dev/null +++ b/docs/microvm/not-supported.mdx @@ -0,0 +1,48 @@ +--- +title: "What's not supported" +description: "Permanent limits and in-progress gaps, kept separate on purpose" +--- + +Two lists, because the difference matters when you are planning. The first will not change. +The second is work. + +## Permanent limits + +These come from the platform underneath. We are not going to ship them. + +| | Why | +|---|---| +| **Sandboxes living past 8 hours** | The provider destroys the host at its lifetime ceiling, counting running and hibernated time together. | +| **Live-memory checkpoints** | The platform exposes no snapshot or memory-export operation. A filesystem archive is the only checkpoint that can exist. | +| **Mounts (FUSE, NFS, overlay, 9p)** | The guest does not have `CAP_SYS_ADMIN`, and it is absent from the bounding set, so `mount` cannot be performed at all. | +| **Resizing a running sandbox** | Memory is a property of the image a sandbox launched from. It cannot be changed after launch. | +| **Live migration** | There are no hosts of ours to migrate between. | + +Calls into these surfaces are **refused**, not silently ignored. `scale()` returns a clear +"not supported for this sandbox" rather than reporting success and doing nothing. + +## Not available yet + +These are gaps in our implementation, not the platform's. + +| | Status | +|---|---| +| **Fork from a checkpoint** | `createFromCheckpoint` fails. Restore into an existing sandbox instead. In progress. | +| **Image builds** | Declarative `image:` manifests and the build pipeline are unavailable. Use [templates](/microvm/templates) or checkpoints. | +| **Checkpoint patches** | The patch API is not wired to this runtime. | +| **Promoting a checkpoint to full** | Depends on memory capture, so it is blocked behind a permanent limit. | + +## Behaves differently rather than missing + +| | | +|---|---| +| **Hibernate** | Suspends in place; hibernated time counts against the lifetime ceiling. | +| **Org halt** | Archives the filesystem and releases the host, rather than parking the sandbox. | +| **Idle timeout** | Clamped to the lifetime ceiling; the response tells you whether it was applied. | +| **Sizes** | Only published size tiers can be served. An unavailable size is refused with the list of sizes that exist. | + +## If something here blocks you + +The permanent list is genuinely fixed, but the priority order of the second list is not. If one +of those gaps is what stands between you and migrating, say so — that is useful information and +it changes what we work on next. diff --git a/docs/microvm/overview.mdx b/docs/microvm/overview.mdx new file mode 100644 index 000000000..6258b9738 --- /dev/null +++ b/docs/microvm/overview.mdx @@ -0,0 +1,69 @@ +--- +title: "MicroVM sandboxes" +description: "The runtime we are moving sandboxes onto, and what it means for your code" +--- + +MicroVM is the runtime OpenComputer sandboxes are moving to. It runs each sandbox as an +isolated microVM on managed infrastructure instead of on VM hosts we operate ourselves. + +These docs describe that runtime specifically. The [Sandboxes](/introduction) docs continue to +describe the current runtime and stay accurate for as long as your org is on it — nothing you +are running today changes because this page exists. + +## Why it exists + +Operating the VM fleet ourselves put a layer between your sandbox and us that had to be kept +healthy: host capacity, kernel versions, live migration, per-box tunnels. Most incidents came +from that layer rather than from sandboxes. Moving to managed microVMs removes it. + +What you get for that: + + + + Sandboxes are served from a warm pool. A create is typically well under a second, and + there is no host to schedule onto first. + + + No worker fleet, no per-box tunnel, no live migration. Less to go wrong between your code + and the sandbox. + + + +## What it costs you + +This is the part worth reading before you plan a migration. The new runtime is not a superset +of the old one — it is a different set of tradeoffs, and two of them are hard limits rather +than things we intend to fix: + + +**Sandboxes cannot live longer than 8 hours.** The provider destroys the host at that point, +counting running and hibernated time together. There is no extension, and the disk goes with +it. See [Sandbox lifetime](/microvm/lifetime). + + + +**Checkpoints capture the filesystem, not live memory.** Restoring gives you a fresh boot with +your files in place, not a running process tree frozen mid-execution. See +[Checkpoints](/microvm/checkpoints). + + +The full list, including what is temporarily missing versus permanently impossible, is on +[What's not supported](/microvm/not-supported). + +## What stays the same + +Most of the API does. Creating sandboxes, running commands, streaming output, reading and +writing files, terminals, preview URLs, signed URLs, webhooks and secrets all work through the +same SDK calls, and the same code runs on both runtimes. + +That is deliberate: the runtime is meant to be something you notice in your bill and your +latency graphs, not in your code. + + + + A side-by-side of the two runtimes. + + + A checklist for finding out whether your workload is affected. + + diff --git a/docs/microvm/preview-urls.mdx b/docs/microvm/preview-urls.mdx new file mode 100644 index 000000000..d0526d3e8 --- /dev/null +++ b/docs/microvm/preview-urls.mdx @@ -0,0 +1,34 @@ +--- +title: "Preview URLs" +description: "Exposing a port from inside the sandbox" +--- + +Preview URLs are unchanged from the current runtime. + +```typescript +const preview = await sandbox.createPreviewURL({ port: 3000 }); +console.log(preview.hostname); // sb--p3000. + +const all = await sandbox.listPreviewURLs(); +await sandbox.deletePreviewURL(3000); +``` + +Anything listening on that port inside the sandbox is reachable at the returned hostname. + +## Auth + +```typescript +const preview = await sandbox.createPreviewURL({ + port: 3000, + authConfig: { /* ... */ }, +}); +``` + +A preview URL can require a bearer token, and the token can be rotated with +`rotatePreviewAuthToken()`. Without auth configured, the URL is open to anyone who has it. + +## Lifetime + +A preview URL lives as long as its sandbox — which on this runtime is at most 8 hours. Links +you hand to someone else will stop working when the sandbox ends, so treat them as short-lived +rather than as stable addresses. diff --git a/docs/microvm/quickstart.mdx b/docs/microvm/quickstart.mdx new file mode 100644 index 000000000..48ebf11f4 --- /dev/null +++ b/docs/microvm/quickstart.mdx @@ -0,0 +1,92 @@ +--- +title: "Quickstart" +description: "Create a MicroVM sandbox and run something in it" +--- + +If you have used OpenComputer sandboxes before, this will look familiar — that is the point. + +## Create and run + + +```typescript TypeScript +import { Sandbox } from "@opencomputer/sdk"; + +const sandbox = await Sandbox.create({ apiKey: process.env.OPENCOMPUTER_API_KEY }); + +const result = await sandbox.exec.run("echo hello from a microVM"); +console.log(result.stdout); // hello from a microVM +console.log(result.exitCode); // 0 + +await sandbox.kill(); +``` + +```python Python +from opencomputer import Sandbox + +sandbox = await Sandbox.create() + +result = await sandbox.exec.run("echo hello from a microVM") +print(result.stdout) # hello from a microVM +print(result.exit_code) # 0 + +await sandbox.kill() +``` + + +## Files + +```typescript +await sandbox.files.write("/home/sandbox/hello.txt", "written from the SDK"); +const text = await sandbox.files.read("/home/sandbox/hello.txt"); + +const entries = await sandbox.files.list("/home/sandbox"); +``` + +Binary content round-trips through `readBytes`, and large files stream rather than buffering. + +## Stream a long-running command + +`exec.run` waits for the command to finish. When you want output as it happens, or the command +outlives the request, start a session instead: + +```typescript +const session = await sandbox.exec.start("sh", { + args: ["-c", "for i in 1 2 3; do echo line $i; sleep 1; done"], + onStdout: (chunk) => process.stdout.write(new TextDecoder().decode(chunk)), + onStderr: (chunk) => process.stderr.write(new TextDecoder().decode(chunk)), +}); + +const exitCode = await session.done; +``` + +Stdout and stderr stay separate, and the exit code is delivered when the process ends. + +## Know when your sandbox ends + +This is the one habit worth building that the current runtime never required. Every sandbox has +a hard end time: + +```typescript +const info = await fetch(`${apiUrl}/api/sandboxes/${sandbox.sandboxId}`, { + headers: { "X-API-Key": apiKey }, +}).then((r) => r.json()); + +console.log(info.endAt); // e.g. 2026-09-02T04:30:26Z +``` + + +`endAt` is measured from when the underlying host started, not from your create. A sandbox +served out of the warm pool may report slightly less than the full 8 hours — that is honest, +not a bug. See [Sandbox lifetime](/microvm/lifetime). + + +## Next + + + + The 8-hour ceiling and how to design around it. + + + Find out whether your workload is affected. + + diff --git a/docs/microvm/sdk-differences.mdx b/docs/microvm/sdk-differences.mdx new file mode 100644 index 000000000..178396296 --- /dev/null +++ b/docs/microvm/sdk-differences.mdx @@ -0,0 +1,59 @@ +--- +title: "SDK differences" +description: "What changes in your code, and the minimum SDK version" +--- + +The same SDK talks to both runtimes. Most code needs no changes at all. + +## Minimum version + + +Use TypeScript SDK **v0.15.8 or later** if you use terminals or streaming exec sessions. +Earlier versions could not open WebSockets against any runtime — the SDK's own HTTP/2 +connection pool captured the upgrade, and WebSockets cannot be negotiated over HTTP/2. + + +## Calls that fail on this runtime + +| Call | Behaviour | +|---|---| +| `sandbox.scale({ memoryMB })` | `501` — memory is fixed at launch. | +| `sandbox.setAutoscale(...)` | `501` — same reason. | +| `sandbox.mounts.*` | Not available. | +| `Sandbox.createFromCheckpoint(id)` | Fails today; fork is in progress. | +| `createCheckpoint(name, { kind: "full" })` | Refused. Omit `kind` for a filesystem checkpoint. | +| `Sandbox.create({ image: ... })` | Image builds are unavailable. | + +## Calls whose meaning changes + +| Call | On this runtime | +|---|---| +| `sandbox.hibernate()` | Suspends in place. The time still counts against the 8-hour ceiling. | +| `sandbox.restoreCheckpoint(id)` | Restores files onto a fresh boot — no live process state. | +| `sandbox.setTimeout(seconds)` | Clamped to the lifetime ceiling; check `applied` in the response. | +| `Sandbox.create({ memoryMB })` | Only published size tiers; anything else is refused with the list. | + +## Unchanged + +`exec.run`, `exec.start`, `exec.background`, `exec.attach`, `exec.list`, `exec.kill`, +`files.*`, `pty.create`, `createPreviewURL`, `downloadUrl`, `uploadUrl`, `reboot`, `kill`, +`isRunning`, webhooks and secret stores all behave identically. + +## Worth adding to your code + +Read `endAt` and act on it. It is the one genuinely new thing this runtime asks of you: + +```typescript +const info = await getSandbox(sandbox.sandboxId); +const msLeft = Date.parse(info.endAt) - Date.now(); + +if (msLeft < 30 * 60 * 1000) { + // checkpoint and roll over to a fresh sandbox +} +``` + +## A naming papercut + +`exec.start()` and `exec.background()` return a handle with `sessionId`, while `exec.list()` +returns entries with `sessionID`. Both are typed correctly and both runtimes behave the same +way, but the casing differs — matching them up needs care. diff --git a/docs/microvm/secrets.mdx b/docs/microvm/secrets.mdx new file mode 100644 index 000000000..61fd28db7 --- /dev/null +++ b/docs/microvm/secrets.mdx @@ -0,0 +1,59 @@ +--- +title: "Secrets" +description: "Sealed tokens, host scoping, and where the real value lives" +--- + +Secrets behave the same way from your code's point of view, and the protection model is the +same: your sandbox never holds the real value. + +## How it works + +Put secrets in a store, then name the store when you create a sandbox: + +```typescript +// the sandbox is created with the store attached +const sandbox = await Sandbox.create({ secretStore: "prod-keys" }); + +const out = await sandbox.exec.run('echo "$MY_TOKEN"'); +console.log(out.stdout); // osb_sealed_3cda2f3ca0a0e0509a338cab83298f43 +``` + +The environment variable holds a **sealed placeholder**, not your secret. When the sandbox +makes a request to a host that secret is scoped to, a proxy substitutes the real value into the +outbound request. Requests anywhere else carry the placeholder, which is worthless. + +That means bypassing the proxy fails closed — code that dials an upstream directly sends a +useless string rather than leaking your key. + +## Host scoping + +A secret is only substituted for the hosts it is allowed to reach: + +```typescript +// MY_TOKEN scoped to api.github.com +// a request to api.github.com -> the real value is substituted +// a request to anywhere else -> osb_sealed_... is sent instead +``` + +## Rotation + +Updating a secret's value takes effect on running sandboxes without a restart. The sealed +placeholder does not change — only what it resolves to — so nothing inside the sandbox needs to +know a rotation happened. + +## The one real difference + + +On this runtime the substituting proxy runs **inside your sandbox**, as root, rather than on a +separate machine. + + +Your code runs as the unprivileged `sandbox` user and cannot read root's memory, so the +day-to-day protection is unchanged. What changes is the blast radius of a privilege escalation +*inside* the sandbox: on the current runtime that would not reach your secrets, and here it +would. + +We made this choice deliberately over a shared regional proxy, which would have been a single +service holding every customer's secrets and a single point of failure on everyone's egress. +The reasoning, and what it means if you model this risk, is on +[Security model](/microvm/security-model). diff --git a/docs/microvm/security-model.mdx b/docs/microvm/security-model.mdx new file mode 100644 index 000000000..d8eaafff0 --- /dev/null +++ b/docs/microvm/security-model.mdx @@ -0,0 +1,65 @@ +--- +title: "Security model" +description: "Isolation, egress, and the one place this runtime is weaker" +--- + +## Isolation + +Each sandbox is its own microVM with its own kernel, on managed infrastructure. Your code runs +as the unprivileged `sandbox` user inside it. Sandboxes do not share a kernel with each other. + +The guest deliberately runs without `CAP_SYS_ADMIN`, which is why +[mounts are impossible](/microvm/files) — the same restriction that removes a large class of +container-escape techniques also removes `mount`. + +## Egress and secrets + +Outbound HTTPS can be restricted to an allowlist, and secrets are substituted only for the +hosts each secret is scoped to. Because the sandbox holds sealed placeholders rather than real +values, ignoring the proxy and dialing an upstream directly leaks nothing — the request carries +a worthless string. + +The sandbox also cannot use the proxy to reach cloud instance metadata; that is blocked +explicitly. + +## Where this runtime is weaker + + +The process holding your real secret values runs **inside your sandbox** as root, rather than +on a separate machine outside it. + + +On the current runtime, the substituting proxy runs on the VM host. A privilege escalation +inside a sandbox would not reach it, because it was on the other side of the VM boundary. + +Here there is no host of ours to put it on, so it runs as a root-owned process in the guest. +Customer code runs unprivileged and cannot read root's memory — but if code in your sandbox +escalates to root, it can reach the secrets scoped to that sandbox. + +### Why we chose this + +The alternative was a shared regional proxy service. That would have been a single service +holding every customer's secrets and a single point of failure on every customer's egress path — +bought precisely because we removed the host that made it unnecessary. Concentrating every +tenant's secrets in one place to protect against escalation within a single tenant is a worse +trade. + +### What to do about it + +For most workloads this is not a meaningful change: if untrusted code can escalate to root in +your sandbox, the secrets that sandbox is allowed to use are usually not the most interesting +thing it can reach. + +If you do model this risk: + +- Scope each secret to the narrowest set of hosts that works, so a compromised sandbox reaches + only what it needed. +- Give sandboxes running untrusted code their own store, rather than sharing one with trusted + workloads. +- Prefer short-lived credentials, which the 8-hour lifetime ceiling makes natural. + +## Data lifetime + +A sandbox's disk is destroyed with it, and every sandbox ends within 8 hours. Anything that +should persist has to be checkpointed or written somewhere durable — see +[Sandbox lifetime](/microvm/lifetime). diff --git a/docs/microvm/sizes.mdx b/docs/microvm/sizes.mdx new file mode 100644 index 000000000..6b4ec35a0 --- /dev/null +++ b/docs/microvm/sizes.mdx @@ -0,0 +1,50 @@ +--- +title: "Sandbox sizes" +description: "Choosing memory, and what happens when a size is unavailable" +--- + +On this runtime, a sandbox's memory is a property of the image it launches from. Each size we +offer in a region is a separately published image. + +```typescript +const sandbox = await Sandbox.create({ memoryMB: 4096 }); +``` + +Omitting `memoryMB` gives you the region's default size, which is the one kept in the warm pool +and therefore the fastest to start. + +## Unavailable sizes are refused + +If you ask for a size that has no published image in that region, the create is refused — with +the sizes that *are* available: + +```json +{ + "error": "requested sandbox size is not available in this region: 16384MB was requested; this region offers 4096 MB", + "hint": "request one of the listed sizes, or contact support to have another published" +} +``` + + +This is a `400`, not a `503`. The request names something that does not exist, so retrying will +never help — as opposed to a genuine capacity shortage, which is reported separately and is +worth retrying. + + +The important property is that you are never *silently* given a different size than you asked +for. Memory cannot be adjusted after launch, so a quiet downgrade would bill you for one size +and hand you another with no later point at which the mistake could be corrected. + +## Non-default sizes start colder + +Only the default size is kept warm. Another size has to cold-launch from its own image, which +takes noticeably longer than a pooled create. If start latency matters, prefer the default. + +## Resizing is not possible + + +`scale()` and autoscaling are not supported. Memory is fixed at launch by the image, so a +running sandbox cannot be resized. + + +If you size sandboxes dynamically today, decide the size at create time instead. diff --git a/docs/microvm/templates.mdx b/docs/microvm/templates.mdx new file mode 100644 index 000000000..5bc62aff9 --- /dev/null +++ b/docs/microvm/templates.mdx @@ -0,0 +1,39 @@ +--- +title: "Templates" +description: "Starting from a prepared environment" +--- + +Named templates work on this runtime. + +```typescript +const sandbox = await Sandbox.create({ templateID: "my-template" }); +``` + +A template create claims a standard pooled sandbox and unpacks the template's workspace onto +it. That means a template costs an archive rather than a separately published machine image, +and new templates are available immediately rather than after an image build. + +## What a template carries + +The workspace — your files, installed dependencies, anything on disk. It does not carry a +memory image, for the same reason [checkpoints](/microvm/checkpoints) do not. + +## Image builds are not available yet + + +Declarative image manifests (`image:` on create) and the image build pipeline are **not +supported** on this runtime. They depend on a build fleet that this runtime does not have. + + +If you build images today, that workflow needs an alternative before you migrate. The two that +work now: + + + + Prepare a workspace once and start sandboxes from it. + + + Install into a sandbox, checkpoint it, and restore that checkpoint when you need the + environment again. + + diff --git a/docs/microvm/terminals.mdx b/docs/microvm/terminals.mdx new file mode 100644 index 000000000..7d8db8240 --- /dev/null +++ b/docs/microvm/terminals.mdx @@ -0,0 +1,38 @@ +--- +title: "Interactive terminals" +description: "PTY sessions over WebSocket" +--- + +Terminals work the same as on the current runtime. + +```typescript +const pty = await sandbox.pty.create({ + onOutput: (data) => process.stdout.write(new TextDecoder().decode(data)), +}); + +pty.send("whoami\n"); +// ... later +pty.close(); +``` + +`pty.create()` resolves once the connection is open, so it is safe to `send` immediately after +awaiting it. + +## Requirements + +Terminals and streaming exec sessions use WebSockets. + + +Use SDK **v0.15.8 or later**. Earlier versions configured an HTTP/2 connection pool that also +captured WebSocket upgrades, which cannot be negotiated over HTTP/2 — terminals would fail to +connect. Newer versions give WebSockets their own HTTP/1.1 connection. + + +If you are driving the socket yourself rather than through the SDK, connect over HTTP/1.1. + +## What travels over the socket + +Raw terminal bytes in both directions. Resize is a separate call rather than an inline control +message, which keeps the stream a plain byte pipe. + +The wire format is identical on both runtimes — the same client code works against either. diff --git a/docs/microvm/whats-different.mdx b/docs/microvm/whats-different.mdx new file mode 100644 index 000000000..7385f940c --- /dev/null +++ b/docs/microvm/whats-different.mdx @@ -0,0 +1,49 @@ +--- +title: "What's different" +description: "A side-by-side of the current runtime and MicroVM" +--- + +Everything not listed here behaves identically, and the same SDK code runs on both. + +## At a glance + +| | Current runtime | MicroVM | +|---|---|---| +| Maximum lifetime | unbounded | **8 hours**, running + hibernated | +| Checkpoints | filesystem **and** live memory | filesystem only | +| Restore from checkpoint | resumes a running process tree | fresh boot with your files | +| Hibernate | checkpoint to storage, wake anywhere | suspend in place | +| Hibernated time | does not count against a lifetime | **counts against the 8 hours** | +| Resize a running sandbox | supported | not supported | +| Sizes | requested at create | fixed per published size tier | +| Mounts (FUSE/NFS/overlay) | supported | not possible | +| Image builds | supported | not yet | +| Fork from a checkpoint | supported | not yet | +| Checkpoint patches | supported | not yet | +| Secret storage | on a host outside your sandbox | inside your sandbox, as root | +| Live migration | supported | not applicable | +| Exec, files, PTY, preview URLs, webhooks, signed URLs | — | identical | + +## The two that will bite + +**The 8-hour ceiling** is the one to plan around. It is a property of the platform underneath, +not a quota we can raise, and it counts hibernated time. A sandbox you hibernate for the night +does not come back in the morning. If you have work that assumes a long-lived box, read +[Sandbox lifetime](/microvm/lifetime) before anything else. + +**Checkpoints don't capture memory.** If you use checkpoints to freeze a warmed-up process — +a loaded model, an open connection pool, a debugger mid-session — restoring will not give that +back. You get the filesystem, and your process starts again. If you use checkpoints to capture +an installed environment, nothing changes for you. + +## What gets better + +- Creates come from a warm pool, so start latency is lower and more consistent. +- There is no worker fleet to drain, so deploys stop disturbing running sandboxes. +- An unavailable sandbox size is now refused with the sizes that *are* available, instead of + being reported as a capacity problem. +- A sandbox row can no longer outlive its host, so "running" in the API means running. + + + The full list, split into permanent limits and things still in progress. + From 1d5b941efd2d82567d4ea4fd02aeec878482a053 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Tue, 1 Sep 2026 17:47:38 -0700 Subject: [PATCH 02/11] =?UTF-8?q?docs(microvm):=20document=20the=20real=20?= =?UTF-8?q?size=20enum=20(512MB-8GB,=20default=204GB,=20only=20the=20defau?= =?UTF-8?q?lt=20pooled)=20instead=20of=20implying=20any=20size=20can=20be?= =?UTF-8?q?=20published,=20and=20rewrite=20the=20secrets=20page=20as=20an?= =?UTF-8?q?=20explicit=20before/after=20=E2=80=94=20the=20substituting=20p?= =?UTF-8?q?roxy=20moved=20from=20the=20VM=20host=20into=20the=20guest=20as?= =?UTF-8?q?=20root,=20which=20changes=20only=20the=20blast=20radius=20of?= =?UTF-8?q?=20an=20in-sandbox=20privilege=20escalation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/microvm/errors.mdx | 2 +- docs/microvm/migrate.mdx | 10 ++- docs/microvm/not-supported.mdx | 2 +- docs/microvm/sdk-differences.mdx | 2 +- docs/microvm/secrets.mdx | 144 +++++++++++++++++++++++-------- docs/microvm/sizes.mdx | 88 ++++++++++++++----- docs/microvm/whats-different.mdx | 8 +- 7 files changed, 191 insertions(+), 65 deletions(-) diff --git a/docs/microvm/errors.mdx b/docs/microvm/errors.mdx index 46eb38e0c..55cfb8ea7 100644 --- a/docs/microvm/errors.mdx +++ b/docs/microvm/errors.mdx @@ -9,7 +9,7 @@ The runtime tries to make "retry" versus "change your request" obvious from the | Status | Meaning | Retry? | |---|---|---| -| `400` requested size not available | The size has no published image here. The message lists the sizes that do exist. | No — pick a listed size. | +| `400` requested size not available | That size is not published in this region, or is not one the platform offers at all. The message lists the sizes that do exist. | No — pick a listed size. | | `503` out of capacity | A genuine shortage. | Yes, once something frees up. No `Retry-After`, because hammering a full region only adds rate limiting. | | `503` rate limited | Creates are arriving faster than the provider accepts. | Yes — a `Retry-After: 1` header is set. | | `429` concurrent sandbox limit | Your org is at its concurrency cap. | Yes, after ending a sandbox. | diff --git a/docs/microvm/migrate.mdx b/docs/microvm/migrate.mdx index 15768b79a..386310d37 100644 --- a/docs/microvm/migrate.mdx +++ b/docs/microvm/migrate.mdx @@ -8,7 +8,7 @@ out ahead of time whether that move is a non-event or something you need to plan ## Step 1 — answer four questions -Most workloads are unaffected. These four are what separate the two cases: +Most workloads are unaffected. These five are what separate the two cases: @@ -20,6 +20,12 @@ Most workloads are unaffected. These four are what separate the two cases: restore will not bring it back. If they capture an installed environment, you are fine. See [Checkpoints](/microvm/checkpoints). + + 8 GB is the largest size the platform offers, and memory cannot be changed after launch. + If you run bigger sandboxes today, that work needs to fit in 8 GB or be split up. See + [Sandbox sizes](/microvm/sizes). + + None of these exist on the new runtime. See [What's not supported](/microvm/not-supported). @@ -28,7 +34,7 @@ Most workloads are unaffected. These four are what separate the two cases: -If all four are "no", your migration is a configuration change and you can stop reading here. +If all five are "no", your migration is a configuration change and you can stop reading here. ## Step 2 — find the calls in your code diff --git a/docs/microvm/not-supported.mdx b/docs/microvm/not-supported.mdx index c6030b98d..fb3a93e4d 100644 --- a/docs/microvm/not-supported.mdx +++ b/docs/microvm/not-supported.mdx @@ -39,7 +39,7 @@ These are gaps in our implementation, not the platform's. | **Hibernate** | Suspends in place; hibernated time counts against the lifetime ceiling. | | **Org halt** | Archives the filesystem and releases the host, rather than parking the sandbox. | | **Idle timeout** | Clamped to the lifetime ceiling; the response tells you whether it was applied. | -| **Sizes** | Only published size tiers can be served. An unavailable size is refused with the list of sizes that exist. | +| **Sizes** | A fixed enum (512 MB – 8 GB, default 4 GB), and only the tiers published in your region. Anything else is refused with the list that exists. | ## If something here blocks you diff --git a/docs/microvm/sdk-differences.mdx b/docs/microvm/sdk-differences.mdx index 178396296..f0c5bc414 100644 --- a/docs/microvm/sdk-differences.mdx +++ b/docs/microvm/sdk-differences.mdx @@ -31,7 +31,7 @@ connection pool captured the upgrade, and WebSockets cannot be negotiated over H | `sandbox.hibernate()` | Suspends in place. The time still counts against the 8-hour ceiling. | | `sandbox.restoreCheckpoint(id)` | Restores files onto a fresh boot — no live process state. | | `sandbox.setTimeout(seconds)` | Clamped to the lifetime ceiling; check `applied` in the response. | -| `Sandbox.create({ memoryMB })` | Only published size tiers; anything else is refused with the list. | +| `Sandbox.create({ memoryMB })` | A fixed enum up to 8 GB, and only tiers published in your region; anything else is refused with the list. | ## Unchanged diff --git a/docs/microvm/secrets.mdx b/docs/microvm/secrets.mdx index 61fd28db7..b159ca3e9 100644 --- a/docs/microvm/secrets.mdx +++ b/docs/microvm/secrets.mdx @@ -1,59 +1,133 @@ --- title: "Secrets" -description: "Sealed tokens, host scoping, and where the real value lives" +description: "What changed, what didn't, and what it means for your threat model" --- -Secrets behave the same way from your code's point of view, and the protection model is the -same: your sandbox never holds the real value. +**Your code does not change.** Secrets are declared the same way, arrive the same way, and are +substituted the same way. What changed is *where the process holding the real values runs* — +and that has one consequence worth understanding before you migrate. -## How it works +## How secrets work (unchanged) -Put secrets in a store, then name the store when you create a sandbox: +Put values in a secret store, then name the store when creating a sandbox: ```typescript -// the sandbox is created with the store attached const sandbox = await Sandbox.create({ secretStore: "prod-keys" }); const out = await sandbox.exec.run('echo "$MY_TOKEN"'); console.log(out.stdout); // osb_sealed_3cda2f3ca0a0e0509a338cab83298f43 ``` -The environment variable holds a **sealed placeholder**, not your secret. When the sandbox -makes a request to a host that secret is scoped to, a proxy substitutes the real value into the -outbound request. Requests anywhere else carry the placeholder, which is worthless. +The environment variable holds a **sealed placeholder**, never your secret. When the sandbox +makes a request to a host that secret is scoped to, a proxy swaps the real value into the +outbound request. All three of these properties are identical on both runtimes: + + + + Only an `osb_sealed_…` placeholder is present in the environment, on disk, and in any + process the customer can read. + + + Code that ignores the proxy and dials an upstream directly sends the placeholder — a + worthless string — rather than leaking the key. + + + A secret scoped to `api.github.com` is substituted there and nowhere else. Sending it to a + server you control yields the placeholder. + + + Updating a value takes effect on running sandboxes. The placeholder does not change — only + what it resolves to. + + + +## What changed + +The proxy moved **from a host outside your sandbox to a root-owned process inside it.** + +| | Current runtime | MicroVM | +|---|---|---| +| Where the proxy runs | on the VM host, outside your sandbox | inside your sandbox, as root | +| What your code sees | `osb_sealed_…` | `osb_sealed_…` (identical) | +| Who can read the real value | root on the VM host | root **in your sandbox** | +| Reachable by a sandbox escape? | no — it is across the VM boundary | yes, if code escalates to root | + +### Why it moved + +It had to. The old proxy ran on the VM hosts we operated. This runtime has no such host — that +is the entire point of it — so the proxy had to go somewhere else. + +The obvious alternative was a shared proxy service per region. We rejected that: it would be a +single service holding **every customer's** secrets and a single point of failure on every +customer's egress path, bought specifically to defend against escalation *within one tenant*. +Concentrating everyone's secrets to mitigate a single-tenant risk is a worse trade. + +So the proxy runs in the guest, owned by root, with your code running unprivileged. + +### What that actually means + +Your code runs as the unprivileged `sandbox` user and cannot read root's memory, so +day-to-day protection is unchanged. -That means bypassing the proxy fails closed — code that dials an upstream directly sends a -useless string rather than leaking your key. + +The difference is the blast radius of a **privilege escalation inside your sandbox**. On the +current runtime, root in the guest still could not reach the proxy. Here, it can reach the +secrets scoped to that sandbox. + -## Host scoping +Nothing else expands. A compromised sandbox still cannot reach another sandbox's secrets, and +the host-scoping rules still apply to whatever it does reach. + +## Does this affect you? + +For most workloads, no. If code in your sandbox can escalate to root, the secrets that sandbox +was already permitted to use are usually not the most valuable thing now reachable. + +It matters if you run genuinely untrusted code — arbitrary user submissions, an agent executing +code it wrote — **and** that sandbox holds credentials worth more than the work it is doing. + +If that describes you: + + + + Host scoping is enforced at the proxy. A secret usable only against one API is worth much + less to an attacker than one usable anywhere. + + + Do not share a store between untrusted execution and your trusted services. A sandbox can + only reach the store it was created with. + + + The 8-hour lifetime ceiling makes this natural — no sandbox outlives a same-day token. + + + Put them behind a service the sandbox calls, so the sandbox holds a token for your service + rather than the credential itself. + + -A secret is only substituted for the hosts it is allowed to reach: +## Rotation -```typescript -// MY_TOKEN scoped to api.github.com -// a request to api.github.com -> the real value is substituted -// a request to anywhere else -> osb_sealed_... is sent instead +``` +PUT /api/secret-stores/{id}/secrets/{name} {"value": "new-value"} ``` -## Rotation - -Updating a secret's value takes effect on running sandboxes without a restart. The sealed -placeholder does not change — only what it resolves to — so nothing inside the sandbox needs to -know a rotation happened. +Running sandboxes pick the new value up without a restart, and the sealed placeholder in their +environment is unchanged — nothing inside the sandbox needs to know a rotation happened. -## The one real difference + +If you use secret rotation, this is worth re-testing after you migrate. On earlier builds a +rotation against this runtime returned `200` with `refreshed: 0` and the sandbox kept serving +the old value. That is fixed, and the response now reports how many sandboxes were actually +refreshed — check that number rather than the status code. + - -On this runtime the substituting proxy runs **inside your sandbox**, as root, rather than on a -separate machine. - +## Egress allowlists -Your code runs as the unprivileged `sandbox` user and cannot read root's memory, so the -day-to-day protection is unchanged. What changes is the blast radius of a privilege escalation -*inside* the sandbox: on the current runtime that would not reach your secrets, and here it -would. +A store can restrict outbound HTTPS to a set of hosts. Requests to anything else are refused, +and this is unchanged between runtimes. The sandbox also cannot use the proxy to reach cloud +instance metadata. -We made this choice deliberately over a shared regional proxy, which would have been a single -service holding every customer's secrets and a single point of failure on everyone's egress. -The reasoning, and what it means if you model this risk, is on -[Security model](/microvm/security-model). + + Isolation boundaries, egress, and the full reasoning behind the proxy's placement. + diff --git a/docs/microvm/sizes.mdx b/docs/microvm/sizes.mdx index 6b4ec35a0..fd1f883d0 100644 --- a/docs/microvm/sizes.mdx +++ b/docs/microvm/sizes.mdx @@ -1,50 +1,92 @@ --- title: "Sandbox sizes" -description: "Choosing memory, and what happens when a size is unavailable" +description: "Which memory sizes exist, which are available to you, and what happens when one isn't" --- -On this runtime, a sandbox's memory is a property of the image it launches from. Each size we -offer in a region is a separately published image. +On this runtime a sandbox's memory is a property of the **image** it launches from, not a +parameter of the launch. The platform has no memory or vCPU field on the create call at all — +the only knob is the memory declared when an image is published. -```typescript -const sandbox = await Sandbox.create({ memoryMB: 4096 }); -``` +Two consequences follow, and they explain everything else on this page: the set of sizes is a +fixed list rather than any number you like, and a size cannot change after launch. + +## Sizes the platform supports + +These are the only values that can exist, on the `al2023-1` base image: + +| Memory | vCPU | Notes | +|---|---|---| +| 512 MB | shared | | +| 1024 MB | shared | | +| 2048 MB | 1 | first size with a full vCPU | +| **4096 MB** | 1 | **default** | +| 8192 MB | 1 (peak to 4×) | largest available | -Omitting `memoryMB` gives you the region's default size, which is the one kept in the warm pool -and therefore the fastest to start. + +This is an enum, not a range. `3072` and `16384` are not "not yet published" — they cannot be +published, because the platform does not offer them. The largest sandbox available is 8 GB. + + +CPU is not separately selectable. The platform allocates it with memory: a full vCPU from +2048 MB upwards, with peak bursting to roughly 4× on the largest size. **4096 MB is the default +specifically because it is the smallest size that reliably delivers a full vCPU.** -## Unavailable sizes are refused +## Which sizes are available to you -If you ask for a size that has no published image in that region, the create is refused — with -the sizes that *are* available: +Every size above is *possible*; which ones are *published in your region* is configuration. +A region typically publishes the default and a subset of the others. + +There is no endpoint that lists them. The reliable way to find out is to ask for one — an +unavailable size is refused with the list of sizes that region actually offers: ```json { - "error": "requested sandbox size is not available in this region: 16384MB was requested; this region offers 4096 MB", + "error": "requested sandbox size is not available in this region: 8192MB was requested; this region offers 4096 MB", "hint": "request one of the listed sizes, or contact support to have another published" } ``` -This is a `400`, not a `503`. The request names something that does not exist, so retrying will -never help — as opposed to a genuine capacity shortage, which is reported separately and is +That is a `400`, not a `503`. The request names something the region cannot serve, so retrying +never helps — as opposed to a real capacity shortage, which is reported separately and *is* worth retrying. -The important property is that you are never *silently* given a different size than you asked -for. Memory cannot be adjusted after launch, so a quiet downgrade would bill you for one size -and hand you another with no later point at which the mistake could be corrected. +If a size in the supported enum is not published in your region and you need it, ask us — that +is a matter of publishing another image, and it is quick. A size outside the enum is not. + +## Choosing one + +```typescript +const sandbox = await Sandbox.create(); // default, 4096 MB +const bigger = await Sandbox.create({ memoryMB: 8192 }); // if published in your region +``` + +Omitting `memoryMB` gives you the default. That is also the fastest option: + + +**Only the default size is kept warm.** Every other size cold-launches from its own image, +which takes a few seconds rather than the sub-second create you get from the pool. + + +Warm stock is per-image, so pooling every size would either multiply idle cost by the number of +tiers or split one pool between them and lose the latency the pool exists for. Deliberately, one +size is fast and the rest are correct. + +## You are never silently given the wrong size -## Non-default sizes start colder +If a size cannot be served, the create fails. It is never quietly served from the default image. -Only the default size is kept warm. Another size has to cold-launch from its own image, which -takes noticeably longer than a pooled create. If start latency matters, prefer the default. +This matters more here than on the current runtime: memory is fixed at launch, so there is no +later point at which a wrong size could be corrected — you would simply be billed for one size +and running on another for the life of the sandbox. ## Resizing is not possible -`scale()` and autoscaling are not supported. Memory is fixed at launch by the image, so a -running sandbox cannot be resized. +`scale()` and `setAutoscale()` return `501`. Memory belongs to the image, so a running sandbox +cannot be resized. -If you size sandboxes dynamically today, decide the size at create time instead. +If you size sandboxes dynamically today, that decision has to move to create time. See +[What's not supported](/microvm/not-supported). diff --git a/docs/microvm/whats-different.mdx b/docs/microvm/whats-different.mdx index 7385f940c..e99f03b36 100644 --- a/docs/microvm/whats-different.mdx +++ b/docs/microvm/whats-different.mdx @@ -15,7 +15,7 @@ Everything not listed here behaves identically, and the same SDK code runs on bo | Hibernate | checkpoint to storage, wake anywhere | suspend in place | | Hibernated time | does not count against a lifetime | **counts against the 8 hours** | | Resize a running sandbox | supported | not supported | -| Sizes | requested at create | fixed per published size tier | +| Sizes | any memory value at create | a fixed enum, 512 MB – 8 GB, default 4 GB | | Mounts (FUSE/NFS/overlay) | supported | not possible | | Image builds | supported | not yet | | Fork from a checkpoint | supported | not yet | @@ -24,13 +24,17 @@ Everything not listed here behaves identically, and the same SDK code runs on bo | Live migration | supported | not applicable | | Exec, files, PTY, preview URLs, webhooks, signed URLs | — | identical | -## The two that will bite +## The three that will bite **The 8-hour ceiling** is the one to plan around. It is a property of the platform underneath, not a quota we can raise, and it counts hibernated time. A sandbox you hibernate for the night does not come back in the morning. If you have work that assumes a long-lived box, read [Sandbox lifetime](/microvm/lifetime) before anything else. +**Memory tops out at 8 GB** and cannot be changed after launch. On the current runtime you ask +for whatever you need and can resize later; here the sizes are a fixed enum and the largest is +8 GB. See [Sandbox sizes](/microvm/sizes). + **Checkpoints don't capture memory.** If you use checkpoints to freeze a warmed-up process — a loaded model, an open connection pool, a debugger mid-session — restoring will not give that back. You get the filesystem, and your process starts again. If you use checkpoints to capture From da29d145b8938dab7ff68ce42d0348a628e21a8a Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Tue, 1 Sep 2026 17:54:11 -0700 Subject: [PATCH 03/11] =?UTF-8?q?docs(microvm):=20drop=20the=20per-tier=20?= =?UTF-8?q?vCPU=20table=20=E2=80=94=20CPU=20allocation=20is=20not=20measur?= =?UTF-8?q?able=20from=20inside=20the=20guest=20(nproc=20reports=20the=20h?= =?UTF-8?q?ost,=20not=20the=20share)=20so=20the=20numbers=20were=20unsubst?= =?UTF-8?q?antiated;=20say=20instead=20that=20CPU=20follows=20memory=20and?= =?UTF-8?q?=20that=20cpuCount=20and=20diskMB=20are=20accepted=20but=20are?= =?UTF-8?q?=20not=20controls=20on=20this=20runtime?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/microvm/sdk-differences.mdx | 2 ++ docs/microvm/sizes.mdx | 30 ++++++++++++++++++++---------- docs/microvm/whats-different.mdx | 4 +++- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/microvm/sdk-differences.mdx b/docs/microvm/sdk-differences.mdx index f0c5bc414..a789cbe15 100644 --- a/docs/microvm/sdk-differences.mdx +++ b/docs/microvm/sdk-differences.mdx @@ -32,6 +32,8 @@ connection pool captured the upgrade, and WebSockets cannot be negotiated over H | `sandbox.restoreCheckpoint(id)` | Restores files onto a fresh boot — no live process state. | | `sandbox.setTimeout(seconds)` | Clamped to the lifetime ceiling; check `applied` in the response. | | `Sandbox.create({ memoryMB })` | A fixed enum up to 8 GB, and only tiers published in your region; anything else is refused with the list. | +| `Sandbox.create({ cpuCount })` | Accepted, but not a control — CPU is allocated with memory. | +| `Sandbox.create({ diskMB })` | Not a control here; disk comes from the image. | ## Unchanged diff --git a/docs/microvm/sizes.mdx b/docs/microvm/sizes.mdx index fd1f883d0..6d2bebd9a 100644 --- a/docs/microvm/sizes.mdx +++ b/docs/microvm/sizes.mdx @@ -14,22 +14,32 @@ fixed list rather than any number you like, and a size cannot change after launc These are the only values that can exist, on the `al2023-1` base image: -| Memory | vCPU | Notes | -|---|---|---| -| 512 MB | shared | | -| 1024 MB | shared | | -| 2048 MB | 1 | first size with a full vCPU | -| **4096 MB** | 1 | **default** | -| 8192 MB | 1 (peak to 4×) | largest available | +| Memory | +|---| +| 512 MB | +| 1024 MB | +| 2048 MB | +| **4096 MB** — default | +| 8192 MB | This is an enum, not a range. `3072` and `16384` are not "not yet published" — they cannot be published, because the platform does not offer them. The largest sandbox available is 8 GB. -CPU is not separately selectable. The platform allocates it with memory: a full vCPU from -2048 MB upwards, with peak bursting to roughly 4× on the largest size. **4096 MB is the default -specifically because it is the smallest size that reliably delivers a full vCPU.** +## CPU + +CPU is **not separately selectable** on this runtime. The platform allocates it in proportion to +the memory size, and there is no knob for it. + + +`cpuCount` is accepted by the create API for compatibility, but it does not size the sandbox +here. Do not rely on it — set `memoryMB` and let CPU follow. + + +Measuring CPU from inside the guest is misleading: `nproc` and `/proc/cpuinfo` report the +underlying machine rather than your sandbox's share, so a 4 GB sandbox may report 8 processors. +Benchmark actual throughput rather than reading the core count. ## Which sizes are available to you diff --git a/docs/microvm/whats-different.mdx b/docs/microvm/whats-different.mdx index e99f03b36..19f85ec59 100644 --- a/docs/microvm/whats-different.mdx +++ b/docs/microvm/whats-different.mdx @@ -16,6 +16,7 @@ Everything not listed here behaves identically, and the same SDK code runs on bo | Hibernated time | does not count against a lifetime | **counts against the 8 hours** | | Resize a running sandbox | supported | not supported | | Sizes | any memory value at create | a fixed enum, 512 MB – 8 GB, default 4 GB | +| CPU | selectable via `cpuCount`, scales with memory | allocated by the platform; `cpuCount` is not a control | | Mounts (FUSE/NFS/overlay) | supported | not possible | | Image builds | supported | not yet | | Fork from a checkpoint | supported | not yet | @@ -33,7 +34,8 @@ does not come back in the morning. If you have work that assumes a long-lived bo **Memory tops out at 8 GB** and cannot be changed after launch. On the current runtime you ask for whatever you need and can resize later; here the sizes are a fixed enum and the largest is -8 GB. See [Sandbox sizes](/microvm/sizes). +8 GB. CPU is no longer selectable either — `cpuCount` is accepted but does not size the sandbox. +See [Sandbox sizes](/microvm/sizes). **Checkpoints don't capture memory.** If you use checkpoints to freeze a warmed-up process — a loaded model, an open connection pool, a debugger mid-session — restoring will not give that From 80322417d6a2186a3bb6f47c18637fab2d4659ea Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Tue, 1 Sep 2026 18:13:30 -0700 Subject: [PATCH 04/11] =?UTF-8?q?docs(microvm):=20warn=20that=20a=20non-de?= =?UTF-8?q?fault=20cpuCount=20actively=20costs=20you=20=E2=80=94=20it=20ca?= =?UTF-8?q?nnot=20reach=20the=20platform's=20launch=20call,=20and=20it=20o?= =?UTF-8?q?pts=20the=20create=20out=20of=20the=20warm=20pool=20into=20a=20?= =?UTF-8?q?cold=20launch=20for=20no=20benefit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/microvm/sizes.mdx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/microvm/sizes.mdx b/docs/microvm/sizes.mdx index 6d2bebd9a..e1b551310 100644 --- a/docs/microvm/sizes.mdx +++ b/docs/microvm/sizes.mdx @@ -33,10 +33,17 @@ CPU is **not separately selectable** on this runtime. The platform allocates it the memory size, and there is no knob for it. -`cpuCount` is accepted by the create API for compatibility, but it does not size the sandbox -here. Do not rely on it — set `memoryMB` and let CPU follow. +`cpuCount` is accepted by the create API for compatibility, but it is **not a control** on this +runtime — the platform's launch call has no CPU parameter, so the value cannot reach it. +Worse than useless: passing `cpuCount` greater than 1 makes a create **slower**. Only the +standard shape is served from the warm pool, so a create carrying a non-default `cpuCount` opts +out of the fast path and cold-launches — in exchange for nothing, because the sandbox is +identical either way. + +Leave it unset. + Measuring CPU from inside the guest is misleading: `nproc` and `/proc/cpuinfo` report the underlying machine rather than your sandbox's share, so a 4 GB sandbox may report 8 processors. Benchmark actual throughput rather than reading the core count. From 085baf762c4d90361a37e5d11075ffdef96c9afc Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Tue, 1 Sep 2026 18:23:31 -0700 Subject: [PATCH 05/11] =?UTF-8?q?docs(microvm):=20two=20migration=20blocke?= =?UTF-8?q?rs=20found=20on=20review=20=E2=80=94=20templates=20carrying=20a?= =?UTF-8?q?=20rootfs=20image=20are=20refused=20outright=20(which=20is=20wh?= =?UTF-8?q?at=20most=20existing=20templates=20are,=20so=20they=20need=20re?= =?UTF-8?q?building),=20and=20disk=20is=20fixed=20at=20~16GB=20with=20disk?= =?UTF-8?q?MB=20accepted-but-ignored,=20down=20from=20a=2020GB=20default;?= =?UTF-8?q?=20both=20added=20to=20the=20checklist,=20FAQ=20and=20compariso?= =?UTF-8?q?n=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/microvm/faq.mdx | 13 +++++++ docs/microvm/migrate.mdx | 15 ++++++-- docs/microvm/not-supported.mdx | 2 ++ docs/microvm/sizes.mdx | 16 +++++++++ docs/microvm/templates.mdx | 61 ++++++++++++++++++++++++-------- docs/microvm/whats-different.mdx | 2 ++ 6 files changed, 93 insertions(+), 16 deletions(-) diff --git a/docs/microvm/faq.mdx b/docs/microvm/faq.mdx index 5aa920ac4..9a9f82b25 100644 --- a/docs/microvm/faq.mdx +++ b/docs/microvm/faq.mdx @@ -21,6 +21,19 @@ description: "Questions worth answering before you migrate" files. + + Probably not. A template that carries a rootfs image — which is what templates built on the + current runtime are — is refused at create with a clear error, because this runtime can only + replay the workspace half and silently dropping the rest would give you a template that + looks like it worked and hasn't. Rebuild them as workspace templates or checkpoints. See + [Templates](/microvm/templates). + + + + About 16 GB, fixed. `diskMB` is accepted but ignored, so you cannot ask for more. That is + less than the 20 GB default on the current runtime, and well under its 256 GB ceiling. + + For most workloads, no. Check for `scale`, `setAutoscale`, mounts, image builds and `createFromCheckpoint`, and review anywhere you assume a sandbox lives indefinitely. The diff --git a/docs/microvm/migrate.mdx b/docs/microvm/migrate.mdx index 386310d37..113dfa95e 100644 --- a/docs/microvm/migrate.mdx +++ b/docs/microvm/migrate.mdx @@ -8,7 +8,7 @@ out ahead of time whether that move is a non-event or something you need to plan ## Step 1 — answer four questions -Most workloads are unaffected. These five are what separate the two cases: +Most workloads are unaffected. These seven are what separate the two cases: @@ -26,6 +26,17 @@ Most workloads are unaffected. These five are what separate the two cases: [Sandbox sizes](/microvm/sizes). + + A template that carries a rootfs image is refused outright. Most existing templates do. + They need rebuilding as workspace-only templates or checkpoints. See + [Templates](/microvm/templates). + + + + Disk is fixed at roughly 16 GB and `diskMB` is ignored — less than the 20 GB you get today. + Data beyond that needs to live outside the sandbox. + + None of these exist on the new runtime. See [What's not supported](/microvm/not-supported). @@ -34,7 +45,7 @@ Most workloads are unaffected. These five are what separate the two cases: -If all five are "no", your migration is a configuration change and you can stop reading here. +If all seven are "no", your migration is a configuration change and you can stop reading here. ## Step 2 — find the calls in your code diff --git a/docs/microvm/not-supported.mdx b/docs/microvm/not-supported.mdx index fb3a93e4d..599a737ba 100644 --- a/docs/microvm/not-supported.mdx +++ b/docs/microvm/not-supported.mdx @@ -16,6 +16,7 @@ These come from the platform underneath. We are not going to ship them. | **Live-memory checkpoints** | The platform exposes no snapshot or memory-export operation. A filesystem archive is the only checkpoint that can exist. | | **Mounts (FUSE, NFS, overlay, 9p)** | The guest does not have `CAP_SYS_ADMIN`, and it is absent from the bounding set, so `mount` cannot be performed at all. | | **Resizing a running sandbox** | Memory is a property of the image a sandbox launched from. It cannot be changed after launch. | +| **Choosing disk size** | Disk comes from the image and is fixed at ~16 GB. `diskMB` is accepted but ignored. | | **Live migration** | There are no hosts of ours to migrate between. | Calls into these surfaces are **refused**, not silently ignored. `scale()` returns a clear @@ -31,6 +32,7 @@ These are gaps in our implementation, not the platform's. | **Image builds** | Declarative `image:` manifests and the build pipeline are unavailable. Use [templates](/microvm/templates) or checkpoints. | | **Checkpoint patches** | The patch API is not wired to this runtime. | | **Promoting a checkpoint to full** | Depends on memory capture, so it is blocked behind a permanent limit. | +| **Templates carrying a rootfs image** | Refused at create. Templates built on the current runtime capture the whole disk; this runtime can only replay the workspace. See [Templates](/microvm/templates). | ## Behaves differently rather than missing diff --git a/docs/microvm/sizes.mdx b/docs/microvm/sizes.mdx index e1b551310..b52eb5399 100644 --- a/docs/microvm/sizes.mdx +++ b/docs/microvm/sizes.mdx @@ -48,6 +48,22 @@ Measuring CPU from inside the guest is misleading: `nproc` and `/proc/cpuinfo` r underlying machine rather than your sandbox's share, so a 4 GB sandbox may report 8 processors. Benchmark actual throughput rather than reading the core count. +## Disk + +Disk is **fixed at roughly 16 GB** and is not configurable. It comes from the image, the same +way memory does. + + +`diskMB` is accepted by the create API and has **no effect** — asking for 64 GB returns `201` +and gives you the standard ~16 GB. Do not rely on it. + + +This is smaller than the current runtime's 20 GB default, and much smaller than the 256 GB +ceiling it allows. If any workload needs more disk than 16 GB, it needs somewhere else to put +the data — object storage, or a service it streams from — before you migrate. + +Roughly 14 GB of the 16 GB is free on a fresh sandbox; the rest is the base image. + ## Which sizes are available to you Every size above is *possible*; which ones are *published in your region* is configuration. diff --git a/docs/microvm/templates.mdx b/docs/microvm/templates.mdx index 5bc62aff9..29e14c77d 100644 --- a/docs/microvm/templates.mdx +++ b/docs/microvm/templates.mdx @@ -1,36 +1,69 @@ --- title: "Templates" -description: "Starting from a prepared environment" +description: "Which templates carry over, and which do not" --- -Named templates work on this runtime. +Templates work on this runtime, but **the templates you have today probably do not.** This is +the migration issue most likely to affect you, and it is worth checking before anything moves. + +## How a template is applied here + +A template create claims a standard pooled sandbox and unpacks the template's **workspace +archive** on top of it. That keeps the fast pooled create and makes a template cost a tarball +rather than a separately published machine image. ```typescript const sandbox = await Sandbox.create({ templateID: "my-template" }); ``` -A template create claims a standard pooled sandbox and unpacks the template's workspace onto -it. That means a template costs an archive rather than a separately published machine image, -and new templates are available immediately rather than after an image build. +## The catch: rootfs templates are refused + + +A template that carries a **rootfs image** cannot be used on this runtime. The create fails with +*"template … carries a rootfs image, which this runtime cannot restore."* + + +Templates built on the current runtime capture the **whole disk** — every system-level change, +not just your workspace. This runtime can only replay the workspace half, and doing that +silently would hand you your files while dropping every system change the template existed for: +a template that looks like it worked and hasn't. + +So it refuses instead. That is the right failure, but it means: + +**Any template that installed system packages, changed system configuration, or was built +before your org moved will need rebuilding on the new runtime.** -## What a template carries +## What to do -The workspace — your files, installed dependencies, anything on disk. It does not carry a -memory image, for the same reason [checkpoints](/microvm/checkpoints) do not. + + + A refused template fails loudly at create with the message above. That is the fastest audit + — you do not need to inspect anything. + + + Start a sandbox on the new runtime, install what the template provided, and capture it as a + [checkpoint](/microvm/checkpoints) or a new template. What can be captured is the + filesystem, so anything you can install with files and packages carries over. + + + If the template depended on changes outside the workspace, they need to live somewhere else + — either in a published image (talk to us) or as a step your sandbox runs on boot. + + -## Image builds are not available yet +## Image builds are not available Declarative image manifests (`image:` on create) and the image build pipeline are **not -supported** on this runtime. They depend on a build fleet that this runtime does not have. +supported** on this runtime. They depend on a build fleet it does not have. -If you build images today, that workflow needs an alternative before you migrate. The two that -work now: +If you build images today, that workflow needs an alternative before you migrate: - - Prepare a workspace once and start sandboxes from it. + + Prepare a workspace once and start sandboxes from it — as long as it does not need a + rootfs. Install into a sandbox, checkpoint it, and restore that checkpoint when you need the diff --git a/docs/microvm/whats-different.mdx b/docs/microvm/whats-different.mdx index 19f85ec59..dcd9b9a8f 100644 --- a/docs/microvm/whats-different.mdx +++ b/docs/microvm/whats-different.mdx @@ -17,6 +17,8 @@ Everything not listed here behaves identically, and the same SDK code runs on bo | Resize a running sandbox | supported | not supported | | Sizes | any memory value at create | a fixed enum, 512 MB – 8 GB, default 4 GB | | CPU | selectable via `cpuCount`, scales with memory | allocated by the platform; `cpuCount` is not a control | +| Disk | 20 GB default, up to 256 GB via `diskMB` | **fixed ~16 GB**; `diskMB` is ignored | +| Existing templates | work | **refused if they carry a rootfs** — most do | | Mounts (FUSE/NFS/overlay) | supported | not possible | | Image builds | supported | not yet | | Fork from a checkpoint | supported | not yet | From c47b118d677e92d43c1ff5db5656fa2b4fe65526 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Tue, 1 Sep 2026 18:33:57 -0700 Subject: [PATCH 06/11] =?UTF-8?q?docs(microvm):=20add=20the=20size-tier=20?= =?UTF-8?q?prerequisites=20runbook=20derived=20from=20publish.sh=20and=20m?= =?UTF-8?q?icrovm=5Fcommon.go,=20and=20correct=20the=20customer-facing=20r?= =?UTF-8?q?ange=20to=20the=202-8GB=20we=20can=20actually=20offer=20?= =?UTF-8?q?=E2=80=94=20publish.sh=20treats=202048=20as=20the=20vCPU=20floo?= =?UTF-8?q?r=20and=20warns=20below=20it,=20so=20listing=20512/1024=20as=20?= =?UTF-8?q?available=20sizes=20was=20wrong?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/microvm/SIZE-TIERS.md | 112 +++++++++++++++++++++++++++++++ docs/microvm/not-supported.mdx | 2 +- docs/microvm/sdk-differences.mdx | 2 +- docs/microvm/sizes.mdx | 42 ++++-------- docs/microvm/whats-different.mdx | 2 +- 5 files changed, 127 insertions(+), 33 deletions(-) create mode 100644 deploy/microvm/SIZE-TIERS.md diff --git a/deploy/microvm/SIZE-TIERS.md b/deploy/microvm/SIZE-TIERS.md new file mode 100644 index 000000000..dad2b1b9b --- /dev/null +++ b/deploy/microvm/SIZE-TIERS.md @@ -0,0 +1,112 @@ +# Offering a MicroVM size tier + +Memory is the only sizing knob this platform has, and it lives on the **image**, not the launch: +`RunMicrovmInput` carries no memory or vCPU field. So a size tier *is* an image. Offering N +sizes means publishing N images and selecting one per create. + +Nothing here is optional — a tier that is built but not configured is refused at create, and a +tier that is configured but not built fails at launch. + +## Prerequisites (once per account/region) + +| | | +|---|---| +| Build role | `arn:aws:iam:::role/opensandbox-microvm-build`, or override `MICROVM_BUILD_ROLE_ARN` | +| Base image | `arn:aws:lambda::aws:microvm-image:al2023-1`, or override `MICROVM_BASE_IMAGE_ARN` | +| Architecture | **ARM_64 only.** `build.sh` cross-compiles to match; `publish.sh` states it explicitly so the two cannot drift | +| S3 bucket | somewhere to put the artifact zip | +| AWS CLI | with `lambda-microvms` support | + +## Per tier + +Every tier is the **same artifact** published under a different name with a different memory +value. Build once, publish N times. + +```bash +# 1. Package + upload the artifact (once, shared by every tier) +./deploy/microvm/build.sh # → s3:///agent-image.zip + +# 2. Publish one image per tier +MICROVM_IMAGE_NAME=opensandbox-agent-prod-8192 \ +MICROVM_IMAGE_MEMORY_MB=8192 \ + ./deploy/microvm/publish.sh s3:///agent-image.zip +``` + +`publish.sh` polls until the build leaves `CREATING` and prints `latestActiveImageVersion`. +A tier is not usable until that appears. + +### Memory floor + +`MICROVM_IMAGE_MEMORY_MB` defaults to **2048**, and the script warns below it: + +> 2048 MiB is the smallest baseline that still gets a full vCPU under Lambda's baseline-peak +> model, and peak scales to 4x it. Keep that as the floor — below it a box may not get a full +> vCPU, and there is no way to ask for one. + +512 and 1024 tiers are publishable but should be treated as unsupported: CPU is allocated as a +function of memory, so a sub-2048 tier may not get a full vCPU and nothing can compensate. + +### Naming + +**Name images per environment.** Dev and prod share an AWS account, and the image ARN is the +only ownership signal anything has. `opensandbox-agent-dev-8192` and `opensandbox-agent-prod-8192` +must be distinct images, or one environment's tooling can act on the other's boxes. + +## Wiring the cell + +Publishing an image does nothing on its own. The cell has to be told about it: + +```bash +# /etc/opensandbox/server.env +OPENSANDBOX_MICROVM_IMAGE_ARN=arn:aws:lambda:us-east-1::microvm-image:opensandbox-agent-prod +OPENSANDBOX_MICROVM_DEFAULT_MEMORY_MB=4096 +OPENSANDBOX_MICROVM_SIZE_IMAGES="2048=arn:...:opensandbox-agent-prod-2048,8192=arn:...:opensandbox-agent-prod-8192" +``` + +Then restart the control plane — the config is read at startup. + +- `IMAGE_ARN` is the **default** tier, and the only one the warm pool stocks. +- `SIZE_IMAGES` is every **other** tier, `mb=arn` comma-separated. A tier absent from this map + is refused at create, never silently served from the default image. +- A malformed entry is dropped with a log line rather than failing startup. That tier then + refuses instead of becoming a wrong-size sandbox — so **check the logs**, a typo is silent + apart from that line. + +### DEFAULT_MEMORY_MB must match the image + +`OPENSANDBOX_MICROVM_DEFAULT_MEMORY_MB` must equal the default image's actual +`minimumMemoryInMiB`. It is what metering reads: + +> if it drifts from the image, every sandbox on that image is billed for the wrong size + +Left at 0 it falls back to a built-in baseline of 4096. If you publish the default image at any +other size, set this explicitly. + +## Verifying + +On startup the control plane logs the tiers it will serve: + +``` +microvm: size tiers — default 4096MB pooled, cold-only: [2048 8192] +``` + +Then confirm end to end — an unconfigured tier must be refused, not downsized: + +```bash +# a configured tier → 201 +curl -X POST "$API/api/sandboxes" -H "X-API-Key: $KEY" -d '{"memoryMB":8192}' + +# an unconfigured one → 400, listing what IS offered +curl -X POST "$API/api/sandboxes" -H "X-API-Key: $KEY" -d '{"memoryMB":3072}' +# {"error":"requested sandbox size is not available in this region: 3072MB was requested; +# this region offers 4096 MB, 2048, 8192"} +``` + +## Costs of a non-default tier + +Only the default tier is pooled. Warm stock is per-image, so pooling every tier would either +multiply idle spend by the number of tiers or split one pool between them and lose the latency +the pool exists for. + +A non-default tier therefore **cold-launches (~3s)** rather than being claimed from the pool. +That is the deliberate trade: one size is fast, the rest are correct. diff --git a/docs/microvm/not-supported.mdx b/docs/microvm/not-supported.mdx index 599a737ba..7d8b494f0 100644 --- a/docs/microvm/not-supported.mdx +++ b/docs/microvm/not-supported.mdx @@ -41,7 +41,7 @@ These are gaps in our implementation, not the platform's. | **Hibernate** | Suspends in place; hibernated time counts against the lifetime ceiling. | | **Org halt** | Archives the filesystem and releases the host, rather than parking the sandbox. | | **Idle timeout** | Clamped to the lifetime ceiling; the response tells you whether it was applied. | -| **Sizes** | A fixed enum (512 MB – 8 GB, default 4 GB), and only the tiers published in your region. Anything else is refused with the list that exists. | +| **Sizes** | Fixed steps (2 GB – 8 GB, default 4 GB), and only the tiers published in your region. Anything else is refused with the list that exists. | ## If something here blocks you diff --git a/docs/microvm/sdk-differences.mdx b/docs/microvm/sdk-differences.mdx index a789cbe15..b7d08eab3 100644 --- a/docs/microvm/sdk-differences.mdx +++ b/docs/microvm/sdk-differences.mdx @@ -31,7 +31,7 @@ connection pool captured the upgrade, and WebSockets cannot be negotiated over H | `sandbox.hibernate()` | Suspends in place. The time still counts against the 8-hour ceiling. | | `sandbox.restoreCheckpoint(id)` | Restores files onto a fresh boot — no live process state. | | `sandbox.setTimeout(seconds)` | Clamped to the lifetime ceiling; check `applied` in the response. | -| `Sandbox.create({ memoryMB })` | A fixed enum up to 8 GB, and only tiers published in your region; anything else is refused with the list. | +| `Sandbox.create({ memoryMB })` | Fixed steps up to 8 GB, and only tiers published in your region; anything else is refused with the list. | | `Sandbox.create({ cpuCount })` | Accepted, but not a control — CPU is allocated with memory. | | `Sandbox.create({ diskMB })` | Not a control here; disk comes from the image. | diff --git a/docs/microvm/sizes.mdx b/docs/microvm/sizes.mdx index b52eb5399..5de4195b7 100644 --- a/docs/microvm/sizes.mdx +++ b/docs/microvm/sizes.mdx @@ -12,41 +12,23 @@ fixed list rather than any number you like, and a size cannot change after launc ## Sizes the platform supports -These are the only values that can exist, on the `al2023-1` base image: +Memory belongs to the image, so a size only exists if we have published an image for it. The +practical range is **2 GB to 8 GB**, in these steps: -| Memory | -|---| -| 512 MB | -| 1024 MB | -| 2048 MB | -| **4096 MB** — default | -| 8192 MB | +| Memory | | +|---|---| +| 2048 MB | smallest size that reliably gets a full vCPU | +| **4096 MB** | **default** — the only size served from the warm pool | +| 8192 MB | largest available | -This is an enum, not a range. `3072` and `16384` are not "not yet published" — they cannot be -published, because the platform does not offer them. The largest sandbox available is 8 GB. +This is a set of steps, not a range. `3072` is not a size — it is either published or it is not, +and nothing in between exists. 8 GB is the ceiling. -## CPU - -CPU is **not separately selectable** on this runtime. The platform allocates it in proportion to -the memory size, and there is no knob for it. - - -`cpuCount` is accepted by the create API for compatibility, but it is **not a control** on this -runtime — the platform's launch call has no CPU parameter, so the value cannot reach it. - - -Worse than useless: passing `cpuCount` greater than 1 makes a create **slower**. Only the -standard shape is served from the warm pool, so a create carrying a non-default `cpuCount` opts -out of the fast path and cold-launches — in exchange for nothing, because the sandbox is -identical either way. - -Leave it unset. - -Measuring CPU from inside the guest is misleading: `nproc` and `/proc/cpuinfo` report the -underlying machine rather than your sandbox's share, so a 4 GB sandbox may report 8 processors. -Benchmark actual throughput rather than reading the core count. +Smaller images (1024 MB and below) are technically publishable but we do not offer them: CPU is +allocated as a function of memory, so below 2 GB a sandbox may not get a full vCPU and there is +no way to ask for one. ## Disk diff --git a/docs/microvm/whats-different.mdx b/docs/microvm/whats-different.mdx index dcd9b9a8f..07b6a40ed 100644 --- a/docs/microvm/whats-different.mdx +++ b/docs/microvm/whats-different.mdx @@ -15,7 +15,7 @@ Everything not listed here behaves identically, and the same SDK code runs on bo | Hibernate | checkpoint to storage, wake anywhere | suspend in place | | Hibernated time | does not count against a lifetime | **counts against the 8 hours** | | Resize a running sandbox | supported | not supported | -| Sizes | any memory value at create | a fixed enum, 512 MB – 8 GB, default 4 GB | +| Sizes | any memory value at create | fixed steps, 2 GB – 8 GB, default 4 GB | | CPU | selectable via `cpuCount`, scales with memory | allocated by the platform; `cpuCount` is not a control | | Disk | 20 GB default, up to 256 GB via `diskMB` | **fixed ~16 GB**; `diskMB` is ignored | | Existing templates | work | **refused if they carry a rootfs** — most do | From c1c9417a773d89a2ebc33aae90e3593e76b88f54 Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Tue, 1 Sep 2026 18:36:51 -0700 Subject: [PATCH 07/11] =?UTF-8?q?docs(microvm):=20offer=201GB=20and=20pres?= =?UTF-8?q?ent=20sizes=20as=20a=20map=20onto=20the=20current=20runtime's?= =?UTF-8?q?=20tiers=20=E2=80=94=201/4/8GB=20carry=20over,=202GB=20is=20new?= =?UTF-8?q?,=2016GB=20has=20no=20equivalent;=20the=20sub-2048=20publish=20?= =?UTF-8?q?warning=20is=20expected=20for=20the=201GB=20tier=20since=20the?= =?UTF-8?q?=20current=201GB=20tier=20is=20already=20best-effort=20CPU,=20s?= =?UTF-8?q?o=20it=20is=20not=20a=20regression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/microvm/SIZE-TIERS.md | 23 +++++++++++++++++------ docs/microvm/migrate.mdx | 6 +++--- docs/microvm/not-supported.mdx | 2 +- docs/microvm/sdk-differences.mdx | 2 +- docs/microvm/sizes.mdx | 26 ++++++++++++++------------ docs/microvm/whats-different.mdx | 10 +++++----- 6 files changed, 41 insertions(+), 28 deletions(-) diff --git a/deploy/microvm/SIZE-TIERS.md b/deploy/microvm/SIZE-TIERS.md index dad2b1b9b..a153a4832 100644 --- a/deploy/microvm/SIZE-TIERS.md +++ b/deploy/microvm/SIZE-TIERS.md @@ -35,16 +35,27 @@ MICROVM_IMAGE_MEMORY_MB=8192 \ `publish.sh` polls until the build leaves `CREATING` and prints `latestActiveImageVersion`. A tier is not usable until that appears. -### Memory floor +### Tiers we offer, and the sub-2048 warning -`MICROVM_IMAGE_MEMORY_MB` defaults to **2048**, and the script warns below it: +Publish these to mirror the current runtime's tiers as closely as the platform allows: + +| Tier | Why | +|---|---| +| 1024 | matches the current 1 GB tier, which is already documented as best-effort CPU | +| 2048 | smallest size that reliably gets a full vCPU | +| 4096 | the default; also the only pooled tier | +| 8192 | matches the current 8 GB tier, and is the platform ceiling | + +There is no 16384 — the current runtime's 16 GB tier has no equivalent here. + +`publish.sh` **warns** when `MICROVM_IMAGE_MEMORY_MB` is below 2048: > 2048 MiB is the smallest baseline that still gets a full vCPU under Lambda's baseline-peak -> model, and peak scales to 4x it. Keep that as the floor — below it a box may not get a full -> vCPU, and there is no way to ask for one. +> model, and peak scales to 4x it. -512 and 1024 tiers are publishable but should be treated as unsupported: CPU is allocated as a -function of memory, so a sub-2048 tier may not get a full vCPU and nothing can compensate. +That warning is **expected and safe to ignore for the 1024 tier**. CPU is allocated as a +function of memory, so a 1 GB box gets best-effort CPU — which is exactly what the current +runtime's 1 GB tier already provides, so it is not a regression. Do not publish below 1024. ### Naming diff --git a/docs/microvm/migrate.mdx b/docs/microvm/migrate.mdx index 113dfa95e..d36a1a858 100644 --- a/docs/microvm/migrate.mdx +++ b/docs/microvm/migrate.mdx @@ -21,9 +21,9 @@ Most workloads are unaffected. These seven are what separate the two cases: See [Checkpoints](/microvm/checkpoints). - 8 GB is the largest size the platform offers, and memory cannot be changed after launch. - If you run bigger sandboxes today, that work needs to fit in 8 GB or be split up. See - [Sandbox sizes](/microvm/sizes). + The 1 GB, 4 GB and 8 GB tiers carry over, but there is **no 16 GB tier** and memory cannot + be changed after launch. If you run 16 GB sandboxes today, that work needs to fit in 8 GB or + be split up. See [Sandbox sizes](/microvm/sizes). diff --git a/docs/microvm/not-supported.mdx b/docs/microvm/not-supported.mdx index 7d8b494f0..baf9be940 100644 --- a/docs/microvm/not-supported.mdx +++ b/docs/microvm/not-supported.mdx @@ -41,7 +41,7 @@ These are gaps in our implementation, not the platform's. | **Hibernate** | Suspends in place; hibernated time counts against the lifetime ceiling. | | **Org halt** | Archives the filesystem and releases the host, rather than parking the sandbox. | | **Idle timeout** | Clamped to the lifetime ceiling; the response tells you whether it was applied. | -| **Sizes** | Fixed steps (2 GB – 8 GB, default 4 GB), and only the tiers published in your region. Anything else is refused with the list that exists. | +| **Sizes** | Fixed steps (1–8 GB, default 4 GB), and only the tiers published in your region. No 16 GB tier. Anything else is refused with the list that exists. | ## If something here blocks you diff --git a/docs/microvm/sdk-differences.mdx b/docs/microvm/sdk-differences.mdx index b7d08eab3..4e469195d 100644 --- a/docs/microvm/sdk-differences.mdx +++ b/docs/microvm/sdk-differences.mdx @@ -31,7 +31,7 @@ connection pool captured the upgrade, and WebSockets cannot be negotiated over H | `sandbox.hibernate()` | Suspends in place. The time still counts against the 8-hour ceiling. | | `sandbox.restoreCheckpoint(id)` | Restores files onto a fresh boot — no live process state. | | `sandbox.setTimeout(seconds)` | Clamped to the lifetime ceiling; check `applied` in the response. | -| `Sandbox.create({ memoryMB })` | Fixed steps up to 8 GB, and only tiers published in your region; anything else is refused with the list. | +| `Sandbox.create({ memoryMB })` | Fixed steps 1–8 GB, and only tiers published in your region; anything else is refused with the list. | | `Sandbox.create({ cpuCount })` | Accepted, but not a control — CPU is allocated with memory. | | `Sandbox.create({ diskMB })` | Not a control here; disk comes from the image. | diff --git a/docs/microvm/sizes.mdx b/docs/microvm/sizes.mdx index 5de4195b7..92218f9e1 100644 --- a/docs/microvm/sizes.mdx +++ b/docs/microvm/sizes.mdx @@ -12,23 +12,25 @@ fixed list rather than any number you like, and a size cannot change after launc ## Sizes the platform supports -Memory belongs to the image, so a size only exists if we have published an image for it. The -practical range is **2 GB to 8 GB**, in these steps: +Memory belongs to the image, so a size exists only if we have published an image for it. The +sizes mirror the current runtime's tiers as closely as the platform allows: -| Memory | | -|---|---| -| 2048 MB | smallest size that reliably gets a full vCPU | -| **4096 MB** | **default** — the only size served from the warm pool | -| 8192 MB | largest available | +| Memory | On the current runtime | On MicroVM | +|---|---|---| +| 1 GB | 1 vCPU (best-effort) | same — CPU is best-effort at this size | +| 2 GB | — | available (no equivalent tier today) | +| **4 GB** | 1 vCPU | **default**, and the only size served from the warm pool | +| 8 GB | 2 vCPU | available | +| 16 GB | 4 vCPU | **not available** — 8 GB is the ceiling | -This is a set of steps, not a range. `3072` is not a size — it is either published or it is not, -and nothing in between exists. 8 GB is the ceiling. +These are steps, not a range. `3072` is not a size — a tier is either published or it is not, +and nothing in between exists. -Smaller images (1024 MB and below) are technically publishable but we do not offer them: CPU is -allocated as a function of memory, so below 2 GB a sandbox may not get a full vCPU and there is -no way to ask for one. +The one gap is **16 GB**. If you run 16 GB sandboxes today, that work has to fit in 8 GB or be +split before you migrate — memory cannot be raised after launch either. See +[Prepare to migrate](/microvm/migrate). ## Disk diff --git a/docs/microvm/whats-different.mdx b/docs/microvm/whats-different.mdx index 07b6a40ed..3ae4ee3ee 100644 --- a/docs/microvm/whats-different.mdx +++ b/docs/microvm/whats-different.mdx @@ -15,7 +15,7 @@ Everything not listed here behaves identically, and the same SDK code runs on bo | Hibernate | checkpoint to storage, wake anywhere | suspend in place | | Hibernated time | does not count against a lifetime | **counts against the 8 hours** | | Resize a running sandbox | supported | not supported | -| Sizes | any memory value at create | fixed steps, 2 GB – 8 GB, default 4 GB | +| Sizes | any memory value at create | fixed steps, 1–8 GB, default 4 GB; no 16 GB | | CPU | selectable via `cpuCount`, scales with memory | allocated by the platform; `cpuCount` is not a control | | Disk | 20 GB default, up to 256 GB via `diskMB` | **fixed ~16 GB**; `diskMB` is ignored | | Existing templates | work | **refused if they carry a rootfs** — most do | @@ -34,10 +34,10 @@ not a quota we can raise, and it counts hibernated time. A sandbox you hibernate does not come back in the morning. If you have work that assumes a long-lived box, read [Sandbox lifetime](/microvm/lifetime) before anything else. -**Memory tops out at 8 GB** and cannot be changed after launch. On the current runtime you ask -for whatever you need and can resize later; here the sizes are a fixed enum and the largest is -8 GB. CPU is no longer selectable either — `cpuCount` is accepted but does not size the sandbox. -See [Sandbox sizes](/microvm/sizes). +**Memory tops out at 8 GB** and cannot be changed after launch. The 1 GB, 4 GB and 8 GB tiers +carry over; the 16 GB tier does not exist here, and there is no resizing to fall back on. CPU is +no longer selectable either — `cpuCount` is accepted but does not size the sandbox. See +[Sandbox sizes](/microvm/sizes). **Checkpoints don't capture memory.** If you use checkpoints to freeze a warmed-up process — a loaded model, an open connection pool, a debugger mid-session — restoring will not give that From 2dae0b9a8090ddd4aeaeca369b3af7f27ce9775f Mon Sep 17 00:00:00 2001 From: Brian Reardon Date: Wed, 2 Sep 2026 09:11:38 -0700 Subject: [PATCH 08/11] =?UTF-8?q?docs:=20restructure=20the=20v2=20docs=20a?= =?UTF-8?q?s=20a=20Mintlify=20version=20rather=20than=20a=20MicroVM=20tab,?= =?UTF-8?q?=20per=20review=20=E2=80=94=20v2=20mirrors=20v1's=20page=20stru?= =?UTF-8?q?cture=20one-for-one=20under=20v2/**,=20drops=20all=20infrastruc?= =?UTF-8?q?ture=20vocabulary=20in=20favour=20of=20'v2=20with=20breaking=20?= =?UTF-8?q?changes',=20keeps=20removed=20features=20as=20honest=20not-avai?= =?UTF-8?q?lable=20pages=20so=20the=20structures=20still=20match,=20and=20?= =?UTF-8?q?adds=20a=20dedicated=20migrating-from-v1=20guide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/docs.json | 919 ++++++++++++------ docs/microvm/errors.mdx | 42 - docs/microvm/exec.mdx | 72 -- docs/microvm/faq.mdx | 69 -- docs/microvm/files.mdx | 57 -- docs/microvm/hibernate.mdx | 49 - docs/microvm/idle-timeout.mdx | 42 - docs/microvm/lifecycle.mdx | 62 -- docs/microvm/migrate.mdx | 100 -- docs/microvm/not-supported.mdx | 50 - docs/microvm/overview.mdx | 69 -- docs/microvm/preview-urls.mdx | 34 - docs/microvm/quickstart.mdx | 92 -- docs/microvm/sdk-differences.mdx | 61 -- docs/microvm/security-model.mdx | 65 -- docs/microvm/terminals.mdx | 38 - docs/microvm/whats-different.mdx | 57 -- .../capacity/create-reservations.mdx | 68 ++ .../api-reference/capacity/get-calendar.mdx | 36 + .../capacity/list-reservations.mdx | 53 + docs/v2/api-reference/checkpoints/create.mdx | 43 + docs/v2/api-reference/checkpoints/delete.mdx | 20 + docs/v2/api-reference/checkpoints/fork.mdx | 38 + docs/v2/api-reference/checkpoints/list.mdx | 25 + docs/v2/api-reference/checkpoints/restore.mdx | 20 + docs/v2/api-reference/exec/create-session.mdx | 49 + docs/v2/api-reference/exec/kill-session.mdx | 24 + docs/v2/api-reference/exec/list-sessions.mdx | 27 + docs/v2/api-reference/exec/run.mdx | 40 + docs/v2/api-reference/files/delete.mdx | 20 + .../files/generate-download-url.mdx | 27 + .../files/generate-upload-url.mdx | 27 + .../v2/api-reference/files/list-directory.mdx | 23 + docs/v2/api-reference/files/mkdir.mdx | 20 + docs/v2/api-reference/files/read.mdx | 20 + .../api-reference/files/signed-download.mdx | 36 + docs/v2/api-reference/files/signed-upload.mdx | 36 + docs/v2/api-reference/files/write.mdx | 20 + docs/v2/api-reference/overview.mdx | 48 + docs/v2/api-reference/patches/create.mdx | 34 + docs/v2/api-reference/patches/delete.mdx | 20 + docs/v2/api-reference/patches/list.mdx | 26 + docs/v2/api-reference/preview/create.mdx | 35 + docs/v2/api-reference/preview/delete.mdx | 20 + docs/v2/api-reference/preview/list.mdx | 25 + docs/v2/api-reference/preview/rotate-auth.mdx | 31 + docs/v2/api-reference/pty/create.mdx | 31 + docs/v2/api-reference/pty/kill.mdx | 20 + docs/v2/api-reference/pty/resize.mdx | 32 + docs/v2/api-reference/sandboxes/create.mdx | 84 ++ docs/v2/api-reference/sandboxes/delete.mdx | 16 + docs/v2/api-reference/sandboxes/get-tags.mdx | 28 + docs/v2/api-reference/sandboxes/get.mdx | 27 + docs/v2/api-reference/sandboxes/hibernate.mdx | 21 + docs/v2/api-reference/sandboxes/list.mdx | 25 + docs/v2/api-reference/sandboxes/set-tags.mdx | 47 + .../api-reference/sandboxes/set-timeout.mdx | 20 + docs/v2/api-reference/sandboxes/wake.mdx | 25 + docs/v2/api-reference/snapshots/create.mdx | 29 + docs/v2/api-reference/snapshots/delete.mdx | 16 + docs/v2/api-reference/snapshots/get.mdx | 25 + docs/v2/api-reference/snapshots/list.mdx | 23 + .../api-reference/usage/get-sandbox-usage.mdx | 197 ++++ docs/v2/api-reference/usage/get-usage.mdx | 219 +++++ docs/v2/api-reference/usage/list-tags.mdx | 31 + docs/v2/api-reference/webhooks/create.mdx | 57 ++ docs/v2/api-reference/webhooks/delete.mdx | 15 + .../webhooks/deliveries-list.mdx | 45 + .../api-reference/webhooks/delivery-get.mdx | 39 + docs/v2/api-reference/webhooks/get.mdx | 26 + docs/v2/api-reference/webhooks/list.mdx | 26 + docs/v2/api-reference/webhooks/redeliver.mdx | 24 + docs/v2/api-reference/webhooks/test.mdx | 34 + docs/v2/api-reference/webhooks/update.mdx | 48 + docs/v2/browser-sessions/overview.mdx | 295 ++++++ docs/v2/cli/checkpoint.mdx | 81 ++ docs/v2/cli/exec.mdx | 82 ++ docs/v2/cli/overview.mdx | 164 ++++ docs/v2/cli/patch.mdx | 53 + docs/v2/cli/preview.mdx | 93 ++ docs/v2/cli/sandbox.mdx | 85 ++ docs/v2/cli/secrets.mdx | 52 + docs/v2/cli/shell.mdx | 63 ++ docs/v2/guides/agent-skill.mdx | 45 + docs/v2/guides/browser-automation.mdx | 284 ++++++ docs/v2/guides/build-a-lovable-clone.mdx | 187 ++++ docs/v2/how-it-works.mdx | 84 ++ docs/v2/introduction.mdx | 91 ++ docs/v2/migrating-from-v1.mdx | 160 +++ docs/v2/quickstart.mdx | 75 ++ docs/v2/reference/cli/agent.mdx | 69 ++ docs/v2/reference/cli/auth.mdx | 61 ++ docs/v2/reference/cli/checkpoint.mdx | 69 ++ docs/v2/reference/cli/config.mdx | 27 + docs/v2/reference/cli/exec.mdx | 61 ++ docs/v2/reference/cli/overview.mdx | 52 + docs/v2/reference/cli/patch.mdx | 37 + docs/v2/reference/cli/preview.mdx | 53 + docs/v2/reference/cli/sandbox.mdx | 159 +++ docs/v2/reference/cli/scaling.mdx | 14 + docs/v2/reference/cli/shell.mdx | 19 + docs/v2/reference/python-sdk/exec.mdx | 225 +++++ docs/v2/reference/python-sdk/filesystem.mdx | 135 +++ docs/v2/reference/python-sdk/image.mdx | 132 +++ docs/v2/reference/python-sdk/overview.mdx | 43 + docs/v2/reference/python-sdk/pty.mdx | 40 + docs/v2/reference/python-sdk/sandbox.mdx | 330 +++++++ docs/v2/reference/python-sdk/scaling.mdx | 14 + docs/v2/reference/python-sdk/secrets.mdx | 189 ++++ docs/v2/reference/python-sdk/snapshots.mdx | 87 ++ docs/v2/reference/typescript-sdk/exec.mdx | 234 +++++ .../reference/typescript-sdk/filesystem.mdx | 135 +++ docs/v2/reference/typescript-sdk/image.mdx | 144 +++ docs/v2/reference/typescript-sdk/overview.mdx | 57 ++ docs/v2/reference/typescript-sdk/pty.mdx | 34 + docs/v2/reference/typescript-sdk/sandbox.mdx | 458 +++++++++ docs/v2/reference/typescript-sdk/scaling.mdx | 14 + docs/v2/reference/typescript-sdk/secrets.mdx | 225 +++++ .../v2/reference/typescript-sdk/snapshots.mdx | 95 ++ docs/v2/reference/typescript-sdk/usage.mdx | 297 ++++++ docs/v2/reserved-capacity/calendar.mdx | 160 +++ docs/v2/reserved-capacity/concepts.mdx | 138 +++ docs/v2/reserved-capacity/overview.mdx | 122 +++ docs/v2/reserved-capacity/reserving.mdx | 174 ++++ .../reserved-capacity/usage-and-overage.mdx | 140 +++ docs/v2/sandboxes/burst-sandboxes.mdx | 234 +++++ .../{microvm => v2/sandboxes}/checkpoints.mdx | 8 +- docs/v2/sandboxes/elasticity.mdx | 14 + docs/v2/sandboxes/interactive-terminals.mdx | 100 ++ docs/{microvm => v2/sandboxes}/lifetime.mdx | 6 +- docs/v2/sandboxes/mounts.mdx | 14 + docs/v2/sandboxes/overview.mdx | 237 +++++ docs/v2/sandboxes/patches.mdx | 14 + docs/v2/sandboxes/preview-urls.mdx | 249 +++++ docs/v2/sandboxes/running-commands.mdx | 318 ++++++ docs/{microvm => v2/sandboxes}/secrets.mdx | 6 +- docs/v2/sandboxes/signed-urls.mdx | 162 +++ docs/{microvm => v2/sandboxes}/sizes.mdx | 8 +- docs/{microvm => v2/sandboxes}/templates.mdx | 12 +- docs/v2/sandboxes/timeout.mdx | 133 +++ docs/v2/sandboxes/usage.mdx | 186 ++++ docs/v2/sandboxes/webhooks.mdx | 200 ++++ docs/v2/sandboxes/working-with-files.mdx | 232 +++++ docs/v2/self-hosting/gcp-development.mdx | 420 ++++++++ docs/v2/self-hosting/overview.mdx | 215 ++++ docs/v2/troubleshooting.mdx | 98 ++ 146 files changed, 11729 insertions(+), 1293 deletions(-) delete mode 100644 docs/microvm/errors.mdx delete mode 100644 docs/microvm/exec.mdx delete mode 100644 docs/microvm/faq.mdx delete mode 100644 docs/microvm/files.mdx delete mode 100644 docs/microvm/hibernate.mdx delete mode 100644 docs/microvm/idle-timeout.mdx delete mode 100644 docs/microvm/lifecycle.mdx delete mode 100644 docs/microvm/migrate.mdx delete mode 100644 docs/microvm/not-supported.mdx delete mode 100644 docs/microvm/overview.mdx delete mode 100644 docs/microvm/preview-urls.mdx delete mode 100644 docs/microvm/quickstart.mdx delete mode 100644 docs/microvm/sdk-differences.mdx delete mode 100644 docs/microvm/security-model.mdx delete mode 100644 docs/microvm/terminals.mdx delete mode 100644 docs/microvm/whats-different.mdx create mode 100644 docs/v2/api-reference/capacity/create-reservations.mdx create mode 100644 docs/v2/api-reference/capacity/get-calendar.mdx create mode 100644 docs/v2/api-reference/capacity/list-reservations.mdx create mode 100644 docs/v2/api-reference/checkpoints/create.mdx create mode 100644 docs/v2/api-reference/checkpoints/delete.mdx create mode 100644 docs/v2/api-reference/checkpoints/fork.mdx create mode 100644 docs/v2/api-reference/checkpoints/list.mdx create mode 100644 docs/v2/api-reference/checkpoints/restore.mdx create mode 100644 docs/v2/api-reference/exec/create-session.mdx create mode 100644 docs/v2/api-reference/exec/kill-session.mdx create mode 100644 docs/v2/api-reference/exec/list-sessions.mdx create mode 100644 docs/v2/api-reference/exec/run.mdx create mode 100644 docs/v2/api-reference/files/delete.mdx create mode 100644 docs/v2/api-reference/files/generate-download-url.mdx create mode 100644 docs/v2/api-reference/files/generate-upload-url.mdx create mode 100644 docs/v2/api-reference/files/list-directory.mdx create mode 100644 docs/v2/api-reference/files/mkdir.mdx create mode 100644 docs/v2/api-reference/files/read.mdx create mode 100644 docs/v2/api-reference/files/signed-download.mdx create mode 100644 docs/v2/api-reference/files/signed-upload.mdx create mode 100644 docs/v2/api-reference/files/write.mdx create mode 100644 docs/v2/api-reference/overview.mdx create mode 100644 docs/v2/api-reference/patches/create.mdx create mode 100644 docs/v2/api-reference/patches/delete.mdx create mode 100644 docs/v2/api-reference/patches/list.mdx create mode 100644 docs/v2/api-reference/preview/create.mdx create mode 100644 docs/v2/api-reference/preview/delete.mdx create mode 100644 docs/v2/api-reference/preview/list.mdx create mode 100644 docs/v2/api-reference/preview/rotate-auth.mdx create mode 100644 docs/v2/api-reference/pty/create.mdx create mode 100644 docs/v2/api-reference/pty/kill.mdx create mode 100644 docs/v2/api-reference/pty/resize.mdx create mode 100644 docs/v2/api-reference/sandboxes/create.mdx create mode 100644 docs/v2/api-reference/sandboxes/delete.mdx create mode 100644 docs/v2/api-reference/sandboxes/get-tags.mdx create mode 100644 docs/v2/api-reference/sandboxes/get.mdx create mode 100644 docs/v2/api-reference/sandboxes/hibernate.mdx create mode 100644 docs/v2/api-reference/sandboxes/list.mdx create mode 100644 docs/v2/api-reference/sandboxes/set-tags.mdx create mode 100644 docs/v2/api-reference/sandboxes/set-timeout.mdx create mode 100644 docs/v2/api-reference/sandboxes/wake.mdx create mode 100644 docs/v2/api-reference/snapshots/create.mdx create mode 100644 docs/v2/api-reference/snapshots/delete.mdx create mode 100644 docs/v2/api-reference/snapshots/get.mdx create mode 100644 docs/v2/api-reference/snapshots/list.mdx create mode 100644 docs/v2/api-reference/usage/get-sandbox-usage.mdx create mode 100644 docs/v2/api-reference/usage/get-usage.mdx create mode 100644 docs/v2/api-reference/usage/list-tags.mdx create mode 100644 docs/v2/api-reference/webhooks/create.mdx create mode 100644 docs/v2/api-reference/webhooks/delete.mdx create mode 100644 docs/v2/api-reference/webhooks/deliveries-list.mdx create mode 100644 docs/v2/api-reference/webhooks/delivery-get.mdx create mode 100644 docs/v2/api-reference/webhooks/get.mdx create mode 100644 docs/v2/api-reference/webhooks/list.mdx create mode 100644 docs/v2/api-reference/webhooks/redeliver.mdx create mode 100644 docs/v2/api-reference/webhooks/test.mdx create mode 100644 docs/v2/api-reference/webhooks/update.mdx create mode 100644 docs/v2/browser-sessions/overview.mdx create mode 100644 docs/v2/cli/checkpoint.mdx create mode 100644 docs/v2/cli/exec.mdx create mode 100644 docs/v2/cli/overview.mdx create mode 100644 docs/v2/cli/patch.mdx create mode 100644 docs/v2/cli/preview.mdx create mode 100644 docs/v2/cli/sandbox.mdx create mode 100644 docs/v2/cli/secrets.mdx create mode 100644 docs/v2/cli/shell.mdx create mode 100644 docs/v2/guides/agent-skill.mdx create mode 100644 docs/v2/guides/browser-automation.mdx create mode 100644 docs/v2/guides/build-a-lovable-clone.mdx create mode 100644 docs/v2/how-it-works.mdx create mode 100644 docs/v2/introduction.mdx create mode 100644 docs/v2/migrating-from-v1.mdx create mode 100644 docs/v2/quickstart.mdx create mode 100644 docs/v2/reference/cli/agent.mdx create mode 100644 docs/v2/reference/cli/auth.mdx create mode 100644 docs/v2/reference/cli/checkpoint.mdx create mode 100644 docs/v2/reference/cli/config.mdx create mode 100644 docs/v2/reference/cli/exec.mdx create mode 100644 docs/v2/reference/cli/overview.mdx create mode 100644 docs/v2/reference/cli/patch.mdx create mode 100644 docs/v2/reference/cli/preview.mdx create mode 100644 docs/v2/reference/cli/sandbox.mdx create mode 100644 docs/v2/reference/cli/scaling.mdx create mode 100644 docs/v2/reference/cli/shell.mdx create mode 100644 docs/v2/reference/python-sdk/exec.mdx create mode 100644 docs/v2/reference/python-sdk/filesystem.mdx create mode 100644 docs/v2/reference/python-sdk/image.mdx create mode 100644 docs/v2/reference/python-sdk/overview.mdx create mode 100644 docs/v2/reference/python-sdk/pty.mdx create mode 100644 docs/v2/reference/python-sdk/sandbox.mdx create mode 100644 docs/v2/reference/python-sdk/scaling.mdx create mode 100644 docs/v2/reference/python-sdk/secrets.mdx create mode 100644 docs/v2/reference/python-sdk/snapshots.mdx create mode 100644 docs/v2/reference/typescript-sdk/exec.mdx create mode 100644 docs/v2/reference/typescript-sdk/filesystem.mdx create mode 100644 docs/v2/reference/typescript-sdk/image.mdx create mode 100644 docs/v2/reference/typescript-sdk/overview.mdx create mode 100644 docs/v2/reference/typescript-sdk/pty.mdx create mode 100644 docs/v2/reference/typescript-sdk/sandbox.mdx create mode 100644 docs/v2/reference/typescript-sdk/scaling.mdx create mode 100644 docs/v2/reference/typescript-sdk/secrets.mdx create mode 100644 docs/v2/reference/typescript-sdk/snapshots.mdx create mode 100644 docs/v2/reference/typescript-sdk/usage.mdx create mode 100644 docs/v2/reserved-capacity/calendar.mdx create mode 100644 docs/v2/reserved-capacity/concepts.mdx create mode 100644 docs/v2/reserved-capacity/overview.mdx create mode 100644 docs/v2/reserved-capacity/reserving.mdx create mode 100644 docs/v2/reserved-capacity/usage-and-overage.mdx create mode 100644 docs/v2/sandboxes/burst-sandboxes.mdx rename docs/{microvm => v2/sandboxes}/checkpoints.mdx (85%) create mode 100644 docs/v2/sandboxes/elasticity.mdx create mode 100644 docs/v2/sandboxes/interactive-terminals.mdx rename docs/{microvm => v2/sandboxes}/lifetime.mdx (94%) create mode 100644 docs/v2/sandboxes/mounts.mdx create mode 100644 docs/v2/sandboxes/overview.mdx create mode 100644 docs/v2/sandboxes/patches.mdx create mode 100644 docs/v2/sandboxes/preview-urls.mdx create mode 100644 docs/v2/sandboxes/running-commands.mdx rename docs/{microvm => v2/sandboxes}/secrets.mdx (96%) create mode 100644 docs/v2/sandboxes/signed-urls.mdx rename docs/{microvm => v2/sandboxes}/sizes.mdx (94%) rename docs/{microvm => v2/sandboxes}/templates.mdx (82%) create mode 100644 docs/v2/sandboxes/timeout.mdx create mode 100644 docs/v2/sandboxes/usage.mdx create mode 100644 docs/v2/sandboxes/webhooks.mdx create mode 100644 docs/v2/sandboxes/working-with-files.mdx create mode 100644 docs/v2/self-hosting/gcp-development.mdx create mode 100644 docs/v2/self-hosting/overview.mdx create mode 100644 docs/v2/troubleshooting.mdx diff --git a/docs/docs.json b/docs/docs.json index d73290d42..cd20fc8f6 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -22,393 +22,684 @@ }, "favicon": "/images/favicon.svg", "navigation": { - "tabs": [ + "versions": [ { - "tab": "Serverless Agents", - "groups": [ + "version": "v1", + "tabs": [ { - "group": "Getting started", - "pages": [ - "agents/overview", - "agents/mental-model", - "agents/quickstart", - "agents/projects" - ] - }, - { - "group": "Concepts", - "pages": [ - "agents/reactive-agents", - "agents/sessions", - "agents/schedules", - "agents/webhooks", - "agents/capabilities", - "agents/channels", - "agents/outboxes", - "agents/secrets", - "agents/byok", - "agents/deployments" - ] - }, - { - "group": "Agent API", - "pages": [ - "agents/hooks", - "agents/inputs", - "agents/models", - "agents/tools", - "agents/mcp", - "agents/skills", - "agents/subagents", - "agents/session-data" - ] - }, - { - "group": "Test and operate", - "pages": [ - "agents/playground", - "agents/logs" - ] - }, - { - "group": "Examples", - "pages": [ - "agents/examples/gtm-engineer", - "agents/examples/feature-flag-hygiene", - "agents/examples/github-actions-triage", - "agents/examples/pr-review", - "agents/examples/test-coverage" - ] - } - ] - }, - { - "tab": "Sandboxes", - "groups": [ - { - "group": "Getting Started", - "pages": [ - "introduction", - "quickstart", - "how-it-works" - ] - }, - { - "group": "Sandboxes", - "pages": [ - "sandboxes/overview", - "sandboxes/running-commands", - "sandboxes/working-with-files", - "sandboxes/mounts", - "sandboxes/signed-urls", - "sandboxes/interactive-terminals", - "sandboxes/timeout", - "sandboxes/checkpoints", - "sandboxes/templates", - "sandboxes/secrets", - "sandboxes/patches", - "sandboxes/preview-urls", - "sandboxes/webhooks", - "sandboxes/elasticity", - "sandboxes/burst-sandboxes", - { - "group": "Usage", - "tag": "Preview", - "expanded": true, + "tab": "Serverless Agents", + "groups": [ + { + "group": "Getting started", + "pages": [ + "agents/overview", + "agents/mental-model", + "agents/quickstart", + "agents/projects" + ] + }, + { + "group": "Concepts", + "pages": [ + "agents/reactive-agents", + "agents/sessions", + "agents/schedules", + "agents/webhooks", + "agents/capabilities", + "agents/channels", + "agents/outboxes", + "agents/secrets", + "agents/byok", + "agents/deployments" + ] + }, + { + "group": "Agent API", "pages": [ - "sandboxes/usage" + "agents/hooks", + "agents/inputs", + "agents/models", + "agents/tools", + "agents/mcp", + "agents/skills", + "agents/subagents", + "agents/session-data" + ] + }, + { + "group": "Test and operate", + "pages": [ + "agents/playground", + "agents/logs" + ] + }, + { + "group": "Examples", + "pages": [ + "agents/examples/gtm-engineer", + "agents/examples/feature-flag-hygiene", + "agents/examples/github-actions-triage", + "agents/examples/pr-review", + "agents/examples/test-coverage" ] } ] }, { - "group": "Browser Sessions", - "tag": "Preview", - "pages": [ - "browser-sessions/overview" - ] - }, - { - "group": "Reserved Capacity", - "tag": "Preview", - "pages": [ - "reserved-capacity/overview", - "reserved-capacity/concepts", - "reserved-capacity/calendar", - "reserved-capacity/reserving", - "reserved-capacity/usage-and-overage" - ] - }, - { - "group": "CLI", - "pages": [ - "cli/overview", - "cli/sandbox", - "cli/exec", - "cli/shell", - "cli/checkpoint", - "cli/patch", - "cli/preview", - "cli/secrets" - ] - }, - { - "group": "Guides", - "pages": [ - "guides/build-a-lovable-clone", - "guides/browser-automation", - "guides/agent-skill" - ] - }, - { - "group": "TypeScript SDK", - "pages": [ - "reference/typescript-sdk/overview", - "reference/typescript-sdk/sandbox", - "reference/typescript-sdk/scaling", + "tab": "Sandboxes", + "groups": [ { - "group": "Usage & Tags", - "tag": "Preview", - "expanded": true, + "group": "Getting Started", "pages": [ - "reference/typescript-sdk/usage" + "introduction", + "quickstart", + "how-it-works" ] }, - "reference/typescript-sdk/exec", - "reference/typescript-sdk/filesystem", - "reference/typescript-sdk/pty", - "reference/typescript-sdk/image", - "reference/typescript-sdk/snapshots", - "reference/typescript-sdk/secrets" - ] - }, - { - "group": "Python SDK", - "pages": [ - "reference/python-sdk/overview", - "reference/python-sdk/sandbox", - "reference/python-sdk/scaling", - "reference/python-sdk/exec", - "reference/python-sdk/filesystem", - "reference/python-sdk/pty", - "reference/python-sdk/image", - "reference/python-sdk/snapshots", - "reference/python-sdk/secrets" - ] - }, - { - "group": "CLI Reference", - "pages": [ - "reference/cli/overview", - "reference/cli/sandbox", - "reference/cli/scaling", - "reference/cli/exec", - "reference/cli/shell", - "reference/cli/checkpoint", - "reference/cli/patch", - "reference/cli/preview", - "reference/cli/agent", - "reference/cli/auth", - "reference/cli/config" - ] - }, - { - "group": "API Reference", - "pages": [ - "api-reference/overview", { "group": "Sandboxes", "pages": [ - "api-reference/sandboxes/create", - "api-reference/sandboxes/list", - "api-reference/sandboxes/get", - "api-reference/sandboxes/delete", - "api-reference/sandboxes/set-timeout", - "api-reference/sandboxes/hibernate", - "api-reference/sandboxes/wake", + "sandboxes/overview", + "sandboxes/running-commands", + "sandboxes/working-with-files", + "sandboxes/mounts", + "sandboxes/signed-urls", + "sandboxes/interactive-terminals", + "sandboxes/timeout", + "sandboxes/checkpoints", + "sandboxes/templates", + "sandboxes/secrets", + "sandboxes/patches", + "sandboxes/preview-urls", + "sandboxes/webhooks", + "sandboxes/elasticity", + "sandboxes/burst-sandboxes", { - "group": "Tags", + "group": "Usage", "tag": "Preview", + "expanded": true, "pages": [ - "api-reference/sandboxes/get-tags", - "api-reference/sandboxes/set-tags" + "sandboxes/usage" ] } ] }, { - "group": "Webhooks", + "group": "Browser Sessions", "tag": "Preview", "pages": [ - "api-reference/webhooks/create", - "api-reference/webhooks/list", - "api-reference/webhooks/get", - "api-reference/webhooks/update", - "api-reference/webhooks/delete", - "api-reference/webhooks/test", - { - "group": "Deliveries", - "pages": [ - "api-reference/webhooks/deliveries-list", - "api-reference/webhooks/delivery-get", - "api-reference/webhooks/redeliver" - ] - } + "browser-sessions/overview" ] }, { - "group": "Usage", + "group": "Reserved Capacity", "tag": "Preview", "pages": [ - "api-reference/usage/get-usage", - "api-reference/usage/get-sandbox-usage", - "api-reference/usage/list-tags" + "reserved-capacity/overview", + "reserved-capacity/concepts", + "reserved-capacity/calendar", + "reserved-capacity/reserving", + "reserved-capacity/usage-and-overage" ] }, { - "group": "Reserved Capacity", + "group": "CLI", "pages": [ - "api-reference/capacity/get-calendar", - "api-reference/capacity/create-reservations", - "api-reference/capacity/list-reservations" + "cli/overview", + "cli/sandbox", + "cli/exec", + "cli/shell", + "cli/checkpoint", + "cli/patch", + "cli/preview", + "cli/secrets" ] }, { - "group": "Commands", + "group": "Guides", "pages": [ - "api-reference/exec/run", - "api-reference/exec/create-session", - "api-reference/exec/list-sessions", - "api-reference/exec/kill-session" + "guides/build-a-lovable-clone", + "guides/browser-automation", + "guides/agent-skill" ] }, { - "group": "Filesystem", + "group": "TypeScript SDK", "pages": [ - "api-reference/files/read", - "api-reference/files/write", - "api-reference/files/list-directory", - "api-reference/files/mkdir", - "api-reference/files/delete", - "api-reference/files/generate-download-url", - "api-reference/files/generate-upload-url", - "api-reference/files/signed-download", - "api-reference/files/signed-upload" + "reference/typescript-sdk/overview", + "reference/typescript-sdk/sandbox", + "reference/typescript-sdk/scaling", + { + "group": "Usage & Tags", + "tag": "Preview", + "expanded": true, + "pages": [ + "reference/typescript-sdk/usage" + ] + }, + "reference/typescript-sdk/exec", + "reference/typescript-sdk/filesystem", + "reference/typescript-sdk/pty", + "reference/typescript-sdk/image", + "reference/typescript-sdk/snapshots", + "reference/typescript-sdk/secrets" ] }, { - "group": "Checkpoints", + "group": "Python SDK", "pages": [ - "api-reference/checkpoints/create", - "api-reference/checkpoints/list", - "api-reference/checkpoints/restore", - "api-reference/checkpoints/fork", - "api-reference/checkpoints/delete" + "reference/python-sdk/overview", + "reference/python-sdk/sandbox", + "reference/python-sdk/scaling", + "reference/python-sdk/exec", + "reference/python-sdk/filesystem", + "reference/python-sdk/pty", + "reference/python-sdk/image", + "reference/python-sdk/snapshots", + "reference/python-sdk/secrets" ] }, { - "group": "Patches", + "group": "CLI Reference", "pages": [ - "api-reference/patches/create", - "api-reference/patches/list", - "api-reference/patches/delete" + "reference/cli/overview", + "reference/cli/sandbox", + "reference/cli/scaling", + "reference/cli/exec", + "reference/cli/shell", + "reference/cli/checkpoint", + "reference/cli/patch", + "reference/cli/preview", + "reference/cli/agent", + "reference/cli/auth", + "reference/cli/config" ] }, { - "group": "Preview URLs", + "group": "API Reference", "pages": [ - "api-reference/preview/create", - "api-reference/preview/list", - "api-reference/preview/delete", - "api-reference/preview/rotate-auth" + "api-reference/overview", + { + "group": "Sandboxes", + "pages": [ + "api-reference/sandboxes/create", + "api-reference/sandboxes/list", + "api-reference/sandboxes/get", + "api-reference/sandboxes/delete", + "api-reference/sandboxes/set-timeout", + "api-reference/sandboxes/hibernate", + "api-reference/sandboxes/wake", + { + "group": "Tags", + "tag": "Preview", + "pages": [ + "api-reference/sandboxes/get-tags", + "api-reference/sandboxes/set-tags" + ] + } + ] + }, + { + "group": "Webhooks", + "tag": "Preview", + "pages": [ + "api-reference/webhooks/create", + "api-reference/webhooks/list", + "api-reference/webhooks/get", + "api-reference/webhooks/update", + "api-reference/webhooks/delete", + "api-reference/webhooks/test", + { + "group": "Deliveries", + "pages": [ + "api-reference/webhooks/deliveries-list", + "api-reference/webhooks/delivery-get", + "api-reference/webhooks/redeliver" + ] + } + ] + }, + { + "group": "Usage", + "tag": "Preview", + "pages": [ + "api-reference/usage/get-usage", + "api-reference/usage/get-sandbox-usage", + "api-reference/usage/list-tags" + ] + }, + { + "group": "Reserved Capacity", + "pages": [ + "api-reference/capacity/get-calendar", + "api-reference/capacity/create-reservations", + "api-reference/capacity/list-reservations" + ] + }, + { + "group": "Commands", + "pages": [ + "api-reference/exec/run", + "api-reference/exec/create-session", + "api-reference/exec/list-sessions", + "api-reference/exec/kill-session" + ] + }, + { + "group": "Filesystem", + "pages": [ + "api-reference/files/read", + "api-reference/files/write", + "api-reference/files/list-directory", + "api-reference/files/mkdir", + "api-reference/files/delete", + "api-reference/files/generate-download-url", + "api-reference/files/generate-upload-url", + "api-reference/files/signed-download", + "api-reference/files/signed-upload" + ] + }, + { + "group": "Checkpoints", + "pages": [ + "api-reference/checkpoints/create", + "api-reference/checkpoints/list", + "api-reference/checkpoints/restore", + "api-reference/checkpoints/fork", + "api-reference/checkpoints/delete" + ] + }, + { + "group": "Patches", + "pages": [ + "api-reference/patches/create", + "api-reference/patches/list", + "api-reference/patches/delete" + ] + }, + { + "group": "Preview URLs", + "pages": [ + "api-reference/preview/create", + "api-reference/preview/list", + "api-reference/preview/delete", + "api-reference/preview/rotate-auth" + ] + }, + { + "group": "Snapshots", + "pages": [ + "api-reference/snapshots/create", + "api-reference/snapshots/list", + "api-reference/snapshots/get", + "api-reference/snapshots/delete" + ] + }, + { + "group": "PTY", + "pages": [ + "api-reference/pty/create", + "api-reference/pty/resize", + "api-reference/pty/kill" + ] + } ] }, { - "group": "Snapshots", + "group": "Resources", "pages": [ - "api-reference/snapshots/create", - "api-reference/snapshots/list", - "api-reference/snapshots/get", - "api-reference/snapshots/delete" + "troubleshooting" ] }, { - "group": "PTY", + "group": "Self-hosting", "pages": [ - "api-reference/pty/create", - "api-reference/pty/resize", - "api-reference/pty/kill" + "self-hosting/overview", + "self-hosting/gcp-development" ] } ] - }, - { - "group": "Resources", - "pages": [ - "troubleshooting" - ] - }, - { - "group": "Self-hosting", - "pages": [ - "self-hosting/overview", - "self-hosting/gcp-development" - ] } ] }, { - "tab": "Sandboxes · MicroVM", - "groups": [ - { - "group": "Start here", - "pages": [ - "microvm/overview", - "microvm/whats-different", - "microvm/quickstart", - "microvm/migrate" - ] - }, - { - "group": "Lifecycle", - "pages": [ - "microvm/lifecycle", - "microvm/lifetime", - "microvm/idle-timeout", - "microvm/hibernate" - ] - }, - { - "group": "Working in a sandbox", - "pages": [ - "microvm/exec", - "microvm/files", - "microvm/terminals", - "microvm/preview-urls" - ] - }, - { - "group": "State", - "pages": [ - "microvm/checkpoints", - "microvm/templates", - "microvm/secrets" - ] - }, + "version": "v2", + "tabs": [ { - "group": "Limits", - "pages": [ - "microvm/not-supported", - "microvm/sizes", - "microvm/security-model" + "tab": "Serverless Agents", + "groups": [ + { + "group": "Getting started", + "pages": [ + "agents/overview", + "agents/mental-model", + "agents/quickstart", + "agents/projects" + ] + }, + { + "group": "Concepts", + "pages": [ + "agents/reactive-agents", + "agents/sessions", + "agents/schedules", + "agents/webhooks", + "agents/capabilities", + "agents/channels", + "agents/outboxes", + "agents/secrets", + "agents/byok", + "agents/deployments" + ] + }, + { + "group": "Agent API", + "pages": [ + "agents/hooks", + "agents/inputs", + "agents/models", + "agents/tools", + "agents/mcp", + "agents/skills", + "agents/subagents", + "agents/session-data" + ] + }, + { + "group": "Test and operate", + "pages": [ + "agents/playground", + "agents/logs" + ] + }, + { + "group": "Examples", + "pages": [ + "agents/examples/gtm-engineer", + "agents/examples/feature-flag-hygiene", + "agents/examples/github-actions-triage", + "agents/examples/pr-review", + "agents/examples/test-coverage" + ] + } ] }, { - "group": "Reference", - "pages": [ - "microvm/sdk-differences", - "microvm/errors", - "microvm/faq" + "tab": "Sandboxes", + "groups": [ + { + "group": "Getting Started", + "pages": [ + "v2/introduction", + "v2/quickstart", + "v2/how-it-works", + "v2/migrating-from-v1" + ] + }, + { + "group": "Sandboxes", + "pages": [ + "v2/sandboxes/overview", + "v2/sandboxes/running-commands", + "v2/sandboxes/working-with-files", + "v2/sandboxes/mounts", + "v2/sandboxes/signed-urls", + "v2/sandboxes/interactive-terminals", + "v2/sandboxes/lifetime", + "v2/sandboxes/sizes", + "v2/sandboxes/timeout", + "v2/sandboxes/checkpoints", + "v2/sandboxes/templates", + "v2/sandboxes/secrets", + "v2/sandboxes/patches", + "v2/sandboxes/preview-urls", + "v2/sandboxes/webhooks", + "v2/sandboxes/elasticity", + "v2/sandboxes/burst-sandboxes", + { + "group": "Usage", + "tag": "Preview", + "expanded": true, + "pages": [ + "v2/sandboxes/usage" + ] + } + ] + }, + { + "group": "Browser Sessions", + "tag": "Preview", + "pages": [ + "v2/browser-sessions/overview" + ] + }, + { + "group": "Reserved Capacity", + "tag": "Preview", + "pages": [ + "v2/reserved-capacity/overview", + "v2/reserved-capacity/concepts", + "v2/reserved-capacity/calendar", + "v2/reserved-capacity/reserving", + "v2/reserved-capacity/usage-and-overage" + ] + }, + { + "group": "CLI", + "pages": [ + "v2/cli/overview", + "v2/cli/sandbox", + "v2/cli/exec", + "v2/cli/shell", + "v2/cli/checkpoint", + "v2/cli/patch", + "v2/cli/preview", + "v2/cli/secrets" + ] + }, + { + "group": "Guides", + "pages": [ + "v2/guides/build-a-lovable-clone", + "v2/guides/browser-automation", + "v2/guides/agent-skill" + ] + }, + { + "group": "TypeScript SDK", + "pages": [ + "v2/reference/typescript-sdk/overview", + "v2/reference/typescript-sdk/sandbox", + "v2/reference/typescript-sdk/scaling", + { + "group": "Usage & Tags", + "tag": "Preview", + "expanded": true, + "pages": [ + "v2/reference/typescript-sdk/usage" + ] + }, + "v2/reference/typescript-sdk/exec", + "v2/reference/typescript-sdk/filesystem", + "v2/reference/typescript-sdk/pty", + "v2/reference/typescript-sdk/image", + "v2/reference/typescript-sdk/snapshots", + "v2/reference/typescript-sdk/secrets" + ] + }, + { + "group": "Python SDK", + "pages": [ + "v2/reference/python-sdk/overview", + "v2/reference/python-sdk/sandbox", + "v2/reference/python-sdk/scaling", + "v2/reference/python-sdk/exec", + "v2/reference/python-sdk/filesystem", + "v2/reference/python-sdk/pty", + "v2/reference/python-sdk/image", + "v2/reference/python-sdk/snapshots", + "v2/reference/python-sdk/secrets" + ] + }, + { + "group": "CLI Reference", + "pages": [ + "v2/reference/cli/overview", + "v2/reference/cli/sandbox", + "v2/reference/cli/scaling", + "v2/reference/cli/exec", + "v2/reference/cli/shell", + "v2/reference/cli/checkpoint", + "v2/reference/cli/patch", + "v2/reference/cli/preview", + "v2/reference/cli/agent", + "v2/reference/cli/auth", + "v2/reference/cli/config" + ] + }, + { + "group": "API Reference", + "pages": [ + "v2/api-reference/overview", + { + "group": "Sandboxes", + "pages": [ + "v2/api-reference/sandboxes/create", + "v2/api-reference/sandboxes/list", + "v2/api-reference/sandboxes/get", + "v2/api-reference/sandboxes/delete", + "v2/api-reference/sandboxes/set-timeout", + "v2/api-reference/sandboxes/hibernate", + "v2/api-reference/sandboxes/wake", + { + "group": "Tags", + "tag": "Preview", + "pages": [ + "v2/api-reference/sandboxes/get-tags", + "v2/api-reference/sandboxes/set-tags" + ] + } + ] + }, + { + "group": "Webhooks", + "tag": "Preview", + "pages": [ + "v2/api-reference/webhooks/create", + "v2/api-reference/webhooks/list", + "v2/api-reference/webhooks/get", + "v2/api-reference/webhooks/update", + "v2/api-reference/webhooks/delete", + "v2/api-reference/webhooks/test", + { + "group": "Deliveries", + "pages": [ + "v2/api-reference/webhooks/deliveries-list", + "v2/api-reference/webhooks/delivery-get", + "v2/api-reference/webhooks/redeliver" + ] + } + ] + }, + { + "group": "Usage", + "tag": "Preview", + "pages": [ + "v2/api-reference/usage/get-usage", + "v2/api-reference/usage/get-sandbox-usage", + "v2/api-reference/usage/list-tags" + ] + }, + { + "group": "Reserved Capacity", + "pages": [ + "v2/api-reference/capacity/get-calendar", + "v2/api-reference/capacity/create-reservations", + "v2/api-reference/capacity/list-reservations" + ] + }, + { + "group": "Commands", + "pages": [ + "v2/api-reference/exec/run", + "v2/api-reference/exec/create-session", + "v2/api-reference/exec/list-sessions", + "v2/api-reference/exec/kill-session" + ] + }, + { + "group": "Filesystem", + "pages": [ + "v2/api-reference/files/read", + "v2/api-reference/files/write", + "v2/api-reference/files/list-directory", + "v2/api-reference/files/mkdir", + "v2/api-reference/files/delete", + "v2/api-reference/files/generate-download-url", + "v2/api-reference/files/generate-upload-url", + "v2/api-reference/files/signed-download", + "v2/api-reference/files/signed-upload" + ] + }, + { + "group": "Checkpoints", + "pages": [ + "v2/api-reference/checkpoints/create", + "v2/api-reference/checkpoints/list", + "v2/api-reference/checkpoints/restore", + "v2/api-reference/checkpoints/fork", + "v2/api-reference/checkpoints/delete" + ] + }, + { + "group": "Patches", + "pages": [ + "v2/api-reference/patches/create", + "v2/api-reference/patches/list", + "v2/api-reference/patches/delete" + ] + }, + { + "group": "Preview URLs", + "pages": [ + "v2/api-reference/preview/create", + "v2/api-reference/preview/list", + "v2/api-reference/preview/delete", + "v2/api-reference/preview/rotate-auth" + ] + }, + { + "group": "Snapshots", + "pages": [ + "v2/api-reference/snapshots/create", + "v2/api-reference/snapshots/list", + "v2/api-reference/snapshots/get", + "v2/api-reference/snapshots/delete" + ] + }, + { + "group": "PTY", + "pages": [ + "v2/api-reference/pty/create", + "v2/api-reference/pty/resize", + "v2/api-reference/pty/kill" + ] + } + ] + }, + { + "group": "Resources", + "pages": [ + "v2/troubleshooting" + ] + }, + { + "group": "Self-hosting", + "pages": [ + "v2/self-hosting/overview", + "v2/self-hosting/gcp-development" + ] + } ] } ] diff --git a/docs/microvm/errors.mdx b/docs/microvm/errors.mdx deleted file mode 100644 index 55cfb8ea7..000000000 --- a/docs/microvm/errors.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: "Errors" -description: "What the distinct failures mean and whether retrying helps" ---- - -The runtime tries to make "retry" versus "change your request" obvious from the status code. - -## Creates - -| Status | Meaning | Retry? | -|---|---|---| -| `400` requested size not available | That size is not published in this region, or is not one the platform offers at all. The message lists the sizes that do exist. | No — pick a listed size. | -| `503` out of capacity | A genuine shortage. | Yes, once something frees up. No `Retry-After`, because hammering a full region only adds rate limiting. | -| `503` rate limited | Creates are arriving faster than the provider accepts. | Yes — a `Retry-After: 1` header is set. | -| `429` concurrent sandbox limit | Your org is at its concurrency cap. | Yes, after ending a sandbox. | - -The distinction between the first two matters: an unavailable size is permanent for that -request, and reporting it as capacity would send you to support about a shortage that does not -exist. - -## Operations on a sandbox - -| Status | Meaning | -|---|---| -| `501` not supported for this sandbox | The operation does not exist on this runtime — `scale`, a `full` checkpoint, mounts. See [What's not supported](/microvm/not-supported). | -| `404` | The sandbox is gone. If it was hibernated, it may have reached its lifetime ceiling — check `endAt`. | -| `502` could not attach | The connection to the sandbox failed. Usually transient; retry once. | - - -A `501` is a statement about the runtime, not about your sandbox. Retrying will not change it, -and neither will recreating the sandbox. - - -## Things that used to be silent - -Several failures that previously looked like success now report honestly. If you have code that -assumed a `200` meant the operation happened, these are worth knowing about: - -- Asking for an idle timeout longer than the lifetime ceiling returns `applied: false` rather - than accepting a value that could never fire. -- A `full` checkpoint is refused rather than quietly producing a disk-only one. -- An unavailable size is refused rather than silently serving a different size. diff --git a/docs/microvm/exec.mdx b/docs/microvm/exec.mdx deleted file mode 100644 index a83a86ab1..000000000 --- a/docs/microvm/exec.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: "Running commands" -description: "One-shot commands and streaming sessions" ---- - -Command execution is unchanged from the current runtime. The same calls, the same semantics. - -## One-shot - -```typescript -const r = await sandbox.exec.run("ls -la /home/sandbox"); -r.stdout; // captured -r.stderr; // captured separately -r.exitCode; // the real exit code, not thrown -``` - -A non-zero exit is a value, not an exception — `exit 42` gives you `exitCode: 42`. - -Options work as you would expect: - -```typescript -await sandbox.exec.run("pwd && echo $TOKEN", { - cwd: "/tmp", - env: { TOKEN: "abc" }, - timeout: 120, -}); -``` - -## Streaming - -For long-running commands, or when you want output as it happens: - -```typescript -const session = await sandbox.exec.start("npm", { - args: ["run", "build"], - onStdout: (chunk) => process.stdout.write(new TextDecoder().decode(chunk)), - onStderr: (chunk) => process.stderr.write(new TextDecoder().decode(chunk)), - onExit: (code) => console.log("exited", code), -}); - -const exitCode = await session.done; -``` - -Stdout and stderr stay distinguishable, which matters if you redirect one and not the other. - -## Background work - -A session can outlive the call that started it: - -```typescript -const session = await sandbox.exec.background("python", { args: ["worker.py"] }); - -const sessions = await sandbox.exec.list(); -await sandbox.exec.kill(session.sessionId); -``` - - -The handle returned by `start`/`background` exposes `sessionId`, while entries from -`exec.list()` use `sessionID`. Both are typed correctly, but the casing differs — worth knowing -before you write a `.find()` against it. - - -## Reattaching - -```typescript -const session = await sandbox.exec.attach(sessionId, { - onStdout: (chunk) => { /* ... */ }, -}); -``` - -Attaching replays recent scrollback and then streams live output, so a client that reconnects -sees what it missed. diff --git a/docs/microvm/faq.mdx b/docs/microvm/faq.mdx deleted file mode 100644 index 9a9f82b25..000000000 --- a/docs/microvm/faq.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "FAQ" -description: "Questions worth answering before you migrate" ---- - - - - No. Your sandboxes keep running on the current runtime until your org is moved, and the - existing [Sandboxes](/introduction) docs stay accurate for as long as that is true. - - - - No. It is enforced by the platform underneath, not by a quota on our side, and it counts - hibernated time as well as running time. If you have work that needs a longer-lived - sandbox, it needs restructuring — see [Sandbox lifetime](/microvm/lifetime). - - - - Filesystem checkpoints restore normally. Checkpoints that captured live memory restore - their filesystem only — the memory image is ignored, and you get a fresh boot with your - files. - - - - Probably not. A template that carries a rootfs image — which is what templates built on the - current runtime are — is refused at create with a clear error, because this runtime can only - replay the workspace half and silently dropping the rest would give you a template that - looks like it worked and hasn't. Rebuild them as workspace templates or checkpoints. See - [Templates](/microvm/templates). - - - - About 16 GB, fixed. `diskMB` is accepted but ignored, so you cannot ask for more. That is - less than the 20 GB default on the current runtime, and well under its 256 GB ceiling. - - - - For most workloads, no. Check for `scale`, `setAutoscale`, mounts, image builds and - `createFromCheckpoint`, and review anywhere you assume a sandbox lives indefinitely. The - checklist is on [Prepare to migrate](/microvm/migrate). - - - - Creates are, because sandboxes come from a warm pool rather than being scheduled onto a - host. Command execution and file operations are comparable. - - - - The guest runs without `CAP_SYS_ADMIN`, and it is absent from the capability bounding set, - so `mount` cannot be performed even by root. The same restriction removes a large class of - escape techniques. Copy data in, or read it over the network instead. - - - - Each one is archived to durable storage and its host released, so charges stop. When the - org resumes they are restored onto new hosts with their files intact. Running processes are - not preserved. - - - - Not within a single org — an org is on one runtime at a time. Testing against a separate - non-production org is the usual approach, and the same code runs against both. - - - - Tell us. That list is prioritised work, not a fixed decision, and knowing what is blocking - a real migration is what changes the order. - - diff --git a/docs/microvm/files.mdx b/docs/microvm/files.mdx deleted file mode 100644 index 5a33ec967..000000000 --- a/docs/microvm/files.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: "Files" -description: "Reading and writing the sandbox filesystem" ---- - -Filesystem access is unchanged from the current runtime. - -```typescript -await sandbox.files.write("/home/sandbox/notes.txt", "hello"); -const text = await sandbox.files.read("/home/sandbox/notes.txt"); - -await sandbox.files.makeDir("/home/sandbox/work"); -const entries = await sandbox.files.list("/home/sandbox"); - -await sandbox.files.exists("/home/sandbox/notes.txt"); // true -await sandbox.files.remove("/home/sandbox/notes.txt"); -``` - -## Binary and large files - -```typescript -await sandbox.files.write("/home/sandbox/blob.bin", new Uint8Array([0, 1, 2, 255])); -const bytes = await sandbox.files.readBytes("/home/sandbox/blob.bin"); -``` - -Large writes stream rather than buffering, so multi-megabyte files are fine over the same call. - -## Signed URLs - -For uploads and downloads that should not go through your process, mint a URL and hand it out: - -```typescript -const download = await sandbox.downloadUrl("/home/sandbox/report.pdf"); -const upload = await sandbox.uploadUrl("/home/sandbox/incoming.zip"); - -await fetch(upload, { method: "PUT", body: fileContents }); -``` - -Both work the same as on the current runtime. - -## What does not exist here - - -**Mounts are not available.** FUSE, NFS, overlay and 9p all require a capability the guest does -not have, so there is no way to attach external storage as a filesystem inside a MicroVM -sandbox. This is a platform limitation, not a feature gap we are working through. - - -If you use mounts to bring a dataset into the sandbox, the alternatives are to copy it in with -`files.write` or a signed upload URL, or to have your code read it directly from object storage -over the network. - -## Files and the lifetime ceiling - -The sandbox filesystem does not outlive the sandbox, and sandboxes end after at most 8 hours. -Anything you need to keep should be checkpointed or written somewhere durable before then — -see [Sandbox lifetime](/microvm/lifetime). diff --git a/docs/microvm/hibernate.mdx b/docs/microvm/hibernate.mdx deleted file mode 100644 index 383d43274..000000000 --- a/docs/microvm/hibernate.mdx +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: "Hibernate and wake" -description: "Suspending a sandbox in place, and what that does not do" ---- - -```typescript -await sandbox.hibernate(); -// ... later -await sandbox.wake(); -``` - -Hibernating suspends the host. The filesystem, the process tree and anything in memory are all -exactly as you left them when you wake it. - -## What is different here - -On the current runtime, hibernation writes a checkpoint to storage and releases the host, so a -sandbox can stay hibernated indefinitely and wake anywhere. - -Here it is a suspend in place, which has two consequences: - - -**Hibernated time counts against the 8-hour ceiling.** A sandbox hibernated overnight reaches -its deadline while asleep and is destroyed. It does not wake up the next morning. - - -**The sandbox stays on its host.** It does not move, which is why waking is fast — but it also -means hibernation is not a way to park something long-term. - -## When to use it - -Hibernation is for gaps measured in minutes to a few hours: an agent waiting on a human, a -session between turns, a build waiting for input. Combined with an -[idle timeout](/microvm/idle-timeout) it is a cost control. - -It is not a way to preserve a sandbox across days. For that, checkpoint the filesystem and -create a fresh sandbox later — see [Checkpoints](/microvm/checkpoints). - -## Waking a sandbox that has ended - -If a hibernated sandbox reached its ceiling, it is gone. Waking it fails, and the sandbox -reports as stopped. Check `endAt` before relying on a wake: - -```typescript -const info = await getSandbox(sandboxId); -if (Date.parse(info.endAt) < Date.now()) { - // the sandbox is past its deadline — restore from a checkpoint instead -} -``` diff --git a/docs/microvm/idle-timeout.mdx b/docs/microvm/idle-timeout.mdx deleted file mode 100644 index 14e2a5aa2..000000000 --- a/docs/microvm/idle-timeout.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: "Idle timeout" -description: "Auto-hibernate an idle sandbox, within the lifetime ceiling" ---- - -A sandbox can hibernate itself after a period with no activity, so you are not paying for a box -nobody is using. - -```typescript -await sandbox.setTimeout(3600); // hibernate after an hour idle -``` - -Activity means your requests — commands, file operations, terminal input. Internal health -checks do not count as activity, so a sandbox nobody is using really does go idle. - -## The response tells you what was applied - -An idle timeout cannot outlive the sandbox. Because the lifetime ceiling is 8 hours, a longer -timeout could never fire, so the API reports what it actually did rather than accepting a value -it cannot honour: - -```json -// POST /api/sandboxes/{id}/timeout {"timeout": 3600} -{ "applied": true, "requested": 3600, "timeout": 3600 } - -// POST /api/sandboxes/{id}/timeout {"timeout": 86400} -{ "applied": false, "requested": 86400, "timeout": 0 } -``` - - -Check `applied`. A request above the ceiling is refused rather than silently clamped, so you -find out at the call site instead of discovering later that your sandbox never slept. - - -## What waking costs - -Waking is a resume of the suspended host, so it is fast and your filesystem and processes are -exactly where you left them. - -The catch is that the time spent hibernated still counted against the 8-hour ceiling. An idle -timeout saves you money; it does not extend the sandbox's life. See -[Sandbox lifetime](/microvm/lifetime). diff --git a/docs/microvm/lifecycle.mdx b/docs/microvm/lifecycle.mdx deleted file mode 100644 index 5c6a129e1..000000000 --- a/docs/microvm/lifecycle.mdx +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: "Lifecycle" -description: "The states a MicroVM sandbox moves through" ---- - -## States - -| Status | Meaning | -|---|---| -| `running` | Alive and serving requests. | -| `hibernated` | Suspended in place, or archived by an org halt. Wake to use it again. | -| `stopped` | Ended — by you, by its idle timeout, or by reaching its lifetime ceiling. | -| `error` | Failed to start, or failed in a way it could not recover from. | - -## Creating - -Creates are served from a warm pool, so a sandbox is usually ready in well under a second. - -```typescript -const sandbox = await Sandbox.create({ apiKey }); -console.log(sandbox.sandboxId); -``` - -The response carries `endAt` — the deadline the platform will enforce. See -[Sandbox lifetime](/microvm/lifetime). - -## Ending - -```typescript -await sandbox.kill(); -``` - -A sandbox also ends on its own when its idle timeout expires, or when it reaches its 8-hour -ceiling. All three land in `stopped`. - -## Restarting without losing the disk - -`reboot` restarts the guest. Processes are cleared; the filesystem is kept. - -```typescript -await sandbox.files.write("/home/sandbox/keep.txt", "still here"); -await sandbox.reboot(); -await sandbox.files.read("/home/sandbox/keep.txt"); // "still here" -``` - -This is the tool for recovering a wedged process without losing your work. It does not reset -the lifetime ceiling — the host underneath is the same host. - -## Org-level halts - -If an org is halted for non-payment, its sandboxes are **archived and their hosts released**: -the filesystem is written to durable storage, then the host is given back so charges stop. - -When the org resumes, each sandbox is restored onto a new host with its files intact. Running -processes are not preserved — it comes back as a fresh boot holding your data, the same as any -checkpoint restore on this runtime. - - -This differs from the current runtime, which parks halted sandboxes in place. It has to: a -suspended sandbox would still hit the 8-hour ceiling and be destroyed, and a credit halt often -lasts longer than that. - diff --git a/docs/microvm/migrate.mdx b/docs/microvm/migrate.mdx deleted file mode 100644 index d36a1a858..000000000 --- a/docs/microvm/migrate.mdx +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: "Prepare to migrate" -description: "How to find out whether your workload is affected, before anything moves" ---- - -Nothing about your sandboxes changes until your org is moved. This page is here so you can find -out ahead of time whether that move is a non-event or something you need to plan. - -## Step 1 — answer four questions - -Most workloads are unaffected. These seven are what separate the two cases: - - - - Include hibernated time — it counts. If yes, that work needs restructuring before you move. - See [Sandbox lifetime](/microvm/lifetime). - - - If your checkpoints capture a warmed process — a loaded model, an open connection pool — - restore will not bring it back. If they capture an installed environment, you are fine. - See [Checkpoints](/microvm/checkpoints). - - - The 1 GB, 4 GB and 8 GB tiers carry over, but there is **no 16 GB tier** and memory cannot - be changed after launch. If you run 16 GB sandboxes today, that work needs to fit in 8 GB or - be split up. See [Sandbox sizes](/microvm/sizes). - - - - A template that carries a rootfs image is refused outright. Most existing templates do. - They need rebuilding as workspace-only templates or checkpoints. See - [Templates](/microvm/templates). - - - - Disk is fixed at roughly 16 GB and `diskMB` is ignored — less than the 20 GB you get today. - Data beyond that needs to live outside the sandbox. - - - - None of these exist on the new runtime. See [What's not supported](/microvm/not-supported). - - - Both are unavailable today. Fork is being worked on; image builds are not scheduled. - - - -If all seven are "no", your migration is a configuration change and you can stop reading here. - -## Step 2 — find the calls in your code - -Search for the surfaces that behave differently: - -```bash -# Hard failures on the new runtime -rg -n "\.scale\(|setAutoscale|\.mounts\b|createFromCheckpoint|buildImage|image:" - -# Worth reviewing, since the semantics change -rg -n "createCheckpoint|restoreCheckpoint|\.hibernate\(|setTimeout" -``` - -## Step 3 — handle the lifetime ceiling - -If you found long-lived sandboxes, the shape that works is to stop treating the sandbox as the -place your state lives: - - - - Checkpoint before the deadline, create a fresh sandbox, restore. Costs a restart; keeps - your files. - - - Keep durable state outside the sandbox, so any sandbox is disposable and the ceiling stops - mattering. - - - -Read `endAt` and act on it rather than assuming a wall-clock 8 hours from your create — a -pooled sandbox starts with less than the full budget. - -## Step 4 — re-read the security model if you use secrets - -Secrets still reach your sandbox as sealed placeholders and are still substituted only for the -hosts they are scoped to. What changed is where the real value is held: on the new runtime it -lives in a root-owned process **inside** your sandbox rather than on a separate machine. - -Unprivileged code in the sandbox cannot read it, but a privilege escalation inside the sandbox -reaches secrets that it would not have reached before. If you model that risk, read -[Security model](/microvm/security-model). - -## Step 5 — test against it - -Ask us to move a non-production org first. The same SDK and the same code run against both -runtimes, so your existing test suite is the test. - - -If something in your workload does not fit what is described here, tell us before you are -scheduled to move rather than after. The constraints on this page are real, but which of them -we prioritise is not fixed. - diff --git a/docs/microvm/not-supported.mdx b/docs/microvm/not-supported.mdx deleted file mode 100644 index baf9be940..000000000 --- a/docs/microvm/not-supported.mdx +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: "What's not supported" -description: "Permanent limits and in-progress gaps, kept separate on purpose" ---- - -Two lists, because the difference matters when you are planning. The first will not change. -The second is work. - -## Permanent limits - -These come from the platform underneath. We are not going to ship them. - -| | Why | -|---|---| -| **Sandboxes living past 8 hours** | The provider destroys the host at its lifetime ceiling, counting running and hibernated time together. | -| **Live-memory checkpoints** | The platform exposes no snapshot or memory-export operation. A filesystem archive is the only checkpoint that can exist. | -| **Mounts (FUSE, NFS, overlay, 9p)** | The guest does not have `CAP_SYS_ADMIN`, and it is absent from the bounding set, so `mount` cannot be performed at all. | -| **Resizing a running sandbox** | Memory is a property of the image a sandbox launched from. It cannot be changed after launch. | -| **Choosing disk size** | Disk comes from the image and is fixed at ~16 GB. `diskMB` is accepted but ignored. | -| **Live migration** | There are no hosts of ours to migrate between. | - -Calls into these surfaces are **refused**, not silently ignored. `scale()` returns a clear -"not supported for this sandbox" rather than reporting success and doing nothing. - -## Not available yet - -These are gaps in our implementation, not the platform's. - -| | Status | -|---|---| -| **Fork from a checkpoint** | `createFromCheckpoint` fails. Restore into an existing sandbox instead. In progress. | -| **Image builds** | Declarative `image:` manifests and the build pipeline are unavailable. Use [templates](/microvm/templates) or checkpoints. | -| **Checkpoint patches** | The patch API is not wired to this runtime. | -| **Promoting a checkpoint to full** | Depends on memory capture, so it is blocked behind a permanent limit. | -| **Templates carrying a rootfs image** | Refused at create. Templates built on the current runtime capture the whole disk; this runtime can only replay the workspace. See [Templates](/microvm/templates). | - -## Behaves differently rather than missing - -| | | -|---|---| -| **Hibernate** | Suspends in place; hibernated time counts against the lifetime ceiling. | -| **Org halt** | Archives the filesystem and releases the host, rather than parking the sandbox. | -| **Idle timeout** | Clamped to the lifetime ceiling; the response tells you whether it was applied. | -| **Sizes** | Fixed steps (1–8 GB, default 4 GB), and only the tiers published in your region. No 16 GB tier. Anything else is refused with the list that exists. | - -## If something here blocks you - -The permanent list is genuinely fixed, but the priority order of the second list is not. If one -of those gaps is what stands between you and migrating, say so — that is useful information and -it changes what we work on next. diff --git a/docs/microvm/overview.mdx b/docs/microvm/overview.mdx deleted file mode 100644 index 6258b9738..000000000 --- a/docs/microvm/overview.mdx +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: "MicroVM sandboxes" -description: "The runtime we are moving sandboxes onto, and what it means for your code" ---- - -MicroVM is the runtime OpenComputer sandboxes are moving to. It runs each sandbox as an -isolated microVM on managed infrastructure instead of on VM hosts we operate ourselves. - -These docs describe that runtime specifically. The [Sandboxes](/introduction) docs continue to -describe the current runtime and stay accurate for as long as your org is on it — nothing you -are running today changes because this page exists. - -## Why it exists - -Operating the VM fleet ourselves put a layer between your sandbox and us that had to be kept -healthy: host capacity, kernel versions, live migration, per-box tunnels. Most incidents came -from that layer rather than from sandboxes. Moving to managed microVMs removes it. - -What you get for that: - - - - Sandboxes are served from a warm pool. A create is typically well under a second, and - there is no host to schedule onto first. - - - No worker fleet, no per-box tunnel, no live migration. Less to go wrong between your code - and the sandbox. - - - -## What it costs you - -This is the part worth reading before you plan a migration. The new runtime is not a superset -of the old one — it is a different set of tradeoffs, and two of them are hard limits rather -than things we intend to fix: - - -**Sandboxes cannot live longer than 8 hours.** The provider destroys the host at that point, -counting running and hibernated time together. There is no extension, and the disk goes with -it. See [Sandbox lifetime](/microvm/lifetime). - - - -**Checkpoints capture the filesystem, not live memory.** Restoring gives you a fresh boot with -your files in place, not a running process tree frozen mid-execution. See -[Checkpoints](/microvm/checkpoints). - - -The full list, including what is temporarily missing versus permanently impossible, is on -[What's not supported](/microvm/not-supported). - -## What stays the same - -Most of the API does. Creating sandboxes, running commands, streaming output, reading and -writing files, terminals, preview URLs, signed URLs, webhooks and secrets all work through the -same SDK calls, and the same code runs on both runtimes. - -That is deliberate: the runtime is meant to be something you notice in your bill and your -latency graphs, not in your code. - - - - A side-by-side of the two runtimes. - - - A checklist for finding out whether your workload is affected. - - diff --git a/docs/microvm/preview-urls.mdx b/docs/microvm/preview-urls.mdx deleted file mode 100644 index d0526d3e8..000000000 --- a/docs/microvm/preview-urls.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: "Preview URLs" -description: "Exposing a port from inside the sandbox" ---- - -Preview URLs are unchanged from the current runtime. - -```typescript -const preview = await sandbox.createPreviewURL({ port: 3000 }); -console.log(preview.hostname); // sb--p3000. - -const all = await sandbox.listPreviewURLs(); -await sandbox.deletePreviewURL(3000); -``` - -Anything listening on that port inside the sandbox is reachable at the returned hostname. - -## Auth - -```typescript -const preview = await sandbox.createPreviewURL({ - port: 3000, - authConfig: { /* ... */ }, -}); -``` - -A preview URL can require a bearer token, and the token can be rotated with -`rotatePreviewAuthToken()`. Without auth configured, the URL is open to anyone who has it. - -## Lifetime - -A preview URL lives as long as its sandbox — which on this runtime is at most 8 hours. Links -you hand to someone else will stop working when the sandbox ends, so treat them as short-lived -rather than as stable addresses. diff --git a/docs/microvm/quickstart.mdx b/docs/microvm/quickstart.mdx deleted file mode 100644 index 48ebf11f4..000000000 --- a/docs/microvm/quickstart.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: "Quickstart" -description: "Create a MicroVM sandbox and run something in it" ---- - -If you have used OpenComputer sandboxes before, this will look familiar — that is the point. - -## Create and run - - -```typescript TypeScript -import { Sandbox } from "@opencomputer/sdk"; - -const sandbox = await Sandbox.create({ apiKey: process.env.OPENCOMPUTER_API_KEY }); - -const result = await sandbox.exec.run("echo hello from a microVM"); -console.log(result.stdout); // hello from a microVM -console.log(result.exitCode); // 0 - -await sandbox.kill(); -``` - -```python Python -from opencomputer import Sandbox - -sandbox = await Sandbox.create() - -result = await sandbox.exec.run("echo hello from a microVM") -print(result.stdout) # hello from a microVM -print(result.exit_code) # 0 - -await sandbox.kill() -``` - - -## Files - -```typescript -await sandbox.files.write("/home/sandbox/hello.txt", "written from the SDK"); -const text = await sandbox.files.read("/home/sandbox/hello.txt"); - -const entries = await sandbox.files.list("/home/sandbox"); -``` - -Binary content round-trips through `readBytes`, and large files stream rather than buffering. - -## Stream a long-running command - -`exec.run` waits for the command to finish. When you want output as it happens, or the command -outlives the request, start a session instead: - -```typescript -const session = await sandbox.exec.start("sh", { - args: ["-c", "for i in 1 2 3; do echo line $i; sleep 1; done"], - onStdout: (chunk) => process.stdout.write(new TextDecoder().decode(chunk)), - onStderr: (chunk) => process.stderr.write(new TextDecoder().decode(chunk)), -}); - -const exitCode = await session.done; -``` - -Stdout and stderr stay separate, and the exit code is delivered when the process ends. - -## Know when your sandbox ends - -This is the one habit worth building that the current runtime never required. Every sandbox has -a hard end time: - -```typescript -const info = await fetch(`${apiUrl}/api/sandboxes/${sandbox.sandboxId}`, { - headers: { "X-API-Key": apiKey }, -}).then((r) => r.json()); - -console.log(info.endAt); // e.g. 2026-09-02T04:30:26Z -``` - - -`endAt` is measured from when the underlying host started, not from your create. A sandbox -served out of the warm pool may report slightly less than the full 8 hours — that is honest, -not a bug. See [Sandbox lifetime](/microvm/lifetime). - - -## Next - - - - The 8-hour ceiling and how to design around it. - - - Find out whether your workload is affected. - - diff --git a/docs/microvm/sdk-differences.mdx b/docs/microvm/sdk-differences.mdx deleted file mode 100644 index 4e469195d..000000000 --- a/docs/microvm/sdk-differences.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: "SDK differences" -description: "What changes in your code, and the minimum SDK version" ---- - -The same SDK talks to both runtimes. Most code needs no changes at all. - -## Minimum version - - -Use TypeScript SDK **v0.15.8 or later** if you use terminals or streaming exec sessions. -Earlier versions could not open WebSockets against any runtime — the SDK's own HTTP/2 -connection pool captured the upgrade, and WebSockets cannot be negotiated over HTTP/2. - - -## Calls that fail on this runtime - -| Call | Behaviour | -|---|---| -| `sandbox.scale({ memoryMB })` | `501` — memory is fixed at launch. | -| `sandbox.setAutoscale(...)` | `501` — same reason. | -| `sandbox.mounts.*` | Not available. | -| `Sandbox.createFromCheckpoint(id)` | Fails today; fork is in progress. | -| `createCheckpoint(name, { kind: "full" })` | Refused. Omit `kind` for a filesystem checkpoint. | -| `Sandbox.create({ image: ... })` | Image builds are unavailable. | - -## Calls whose meaning changes - -| Call | On this runtime | -|---|---| -| `sandbox.hibernate()` | Suspends in place. The time still counts against the 8-hour ceiling. | -| `sandbox.restoreCheckpoint(id)` | Restores files onto a fresh boot — no live process state. | -| `sandbox.setTimeout(seconds)` | Clamped to the lifetime ceiling; check `applied` in the response. | -| `Sandbox.create({ memoryMB })` | Fixed steps 1–8 GB, and only tiers published in your region; anything else is refused with the list. | -| `Sandbox.create({ cpuCount })` | Accepted, but not a control — CPU is allocated with memory. | -| `Sandbox.create({ diskMB })` | Not a control here; disk comes from the image. | - -## Unchanged - -`exec.run`, `exec.start`, `exec.background`, `exec.attach`, `exec.list`, `exec.kill`, -`files.*`, `pty.create`, `createPreviewURL`, `downloadUrl`, `uploadUrl`, `reboot`, `kill`, -`isRunning`, webhooks and secret stores all behave identically. - -## Worth adding to your code - -Read `endAt` and act on it. It is the one genuinely new thing this runtime asks of you: - -```typescript -const info = await getSandbox(sandbox.sandboxId); -const msLeft = Date.parse(info.endAt) - Date.now(); - -if (msLeft < 30 * 60 * 1000) { - // checkpoint and roll over to a fresh sandbox -} -``` - -## A naming papercut - -`exec.start()` and `exec.background()` return a handle with `sessionId`, while `exec.list()` -returns entries with `sessionID`. Both are typed correctly and both runtimes behave the same -way, but the casing differs — matching them up needs care. diff --git a/docs/microvm/security-model.mdx b/docs/microvm/security-model.mdx deleted file mode 100644 index d8eaafff0..000000000 --- a/docs/microvm/security-model.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: "Security model" -description: "Isolation, egress, and the one place this runtime is weaker" ---- - -## Isolation - -Each sandbox is its own microVM with its own kernel, on managed infrastructure. Your code runs -as the unprivileged `sandbox` user inside it. Sandboxes do not share a kernel with each other. - -The guest deliberately runs without `CAP_SYS_ADMIN`, which is why -[mounts are impossible](/microvm/files) — the same restriction that removes a large class of -container-escape techniques also removes `mount`. - -## Egress and secrets - -Outbound HTTPS can be restricted to an allowlist, and secrets are substituted only for the -hosts each secret is scoped to. Because the sandbox holds sealed placeholders rather than real -values, ignoring the proxy and dialing an upstream directly leaks nothing — the request carries -a worthless string. - -The sandbox also cannot use the proxy to reach cloud instance metadata; that is blocked -explicitly. - -## Where this runtime is weaker - - -The process holding your real secret values runs **inside your sandbox** as root, rather than -on a separate machine outside it. - - -On the current runtime, the substituting proxy runs on the VM host. A privilege escalation -inside a sandbox would not reach it, because it was on the other side of the VM boundary. - -Here there is no host of ours to put it on, so it runs as a root-owned process in the guest. -Customer code runs unprivileged and cannot read root's memory — but if code in your sandbox -escalates to root, it can reach the secrets scoped to that sandbox. - -### Why we chose this - -The alternative was a shared regional proxy service. That would have been a single service -holding every customer's secrets and a single point of failure on every customer's egress path — -bought precisely because we removed the host that made it unnecessary. Concentrating every -tenant's secrets in one place to protect against escalation within a single tenant is a worse -trade. - -### What to do about it - -For most workloads this is not a meaningful change: if untrusted code can escalate to root in -your sandbox, the secrets that sandbox is allowed to use are usually not the most interesting -thing it can reach. - -If you do model this risk: - -- Scope each secret to the narrowest set of hosts that works, so a compromised sandbox reaches - only what it needed. -- Give sandboxes running untrusted code their own store, rather than sharing one with trusted - workloads. -- Prefer short-lived credentials, which the 8-hour lifetime ceiling makes natural. - -## Data lifetime - -A sandbox's disk is destroyed with it, and every sandbox ends within 8 hours. Anything that -should persist has to be checkpointed or written somewhere durable — see -[Sandbox lifetime](/microvm/lifetime). diff --git a/docs/microvm/terminals.mdx b/docs/microvm/terminals.mdx deleted file mode 100644 index 7d8db8240..000000000 --- a/docs/microvm/terminals.mdx +++ /dev/null @@ -1,38 +0,0 @@ ---- -title: "Interactive terminals" -description: "PTY sessions over WebSocket" ---- - -Terminals work the same as on the current runtime. - -```typescript -const pty = await sandbox.pty.create({ - onOutput: (data) => process.stdout.write(new TextDecoder().decode(data)), -}); - -pty.send("whoami\n"); -// ... later -pty.close(); -``` - -`pty.create()` resolves once the connection is open, so it is safe to `send` immediately after -awaiting it. - -## Requirements - -Terminals and streaming exec sessions use WebSockets. - - -Use SDK **v0.15.8 or later**. Earlier versions configured an HTTP/2 connection pool that also -captured WebSocket upgrades, which cannot be negotiated over HTTP/2 — terminals would fail to -connect. Newer versions give WebSockets their own HTTP/1.1 connection. - - -If you are driving the socket yourself rather than through the SDK, connect over HTTP/1.1. - -## What travels over the socket - -Raw terminal bytes in both directions. Resize is a separate call rather than an inline control -message, which keeps the stream a plain byte pipe. - -The wire format is identical on both runtimes — the same client code works against either. diff --git a/docs/microvm/whats-different.mdx b/docs/microvm/whats-different.mdx deleted file mode 100644 index 3ae4ee3ee..000000000 --- a/docs/microvm/whats-different.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: "What's different" -description: "A side-by-side of the current runtime and MicroVM" ---- - -Everything not listed here behaves identically, and the same SDK code runs on both. - -## At a glance - -| | Current runtime | MicroVM | -|---|---|---| -| Maximum lifetime | unbounded | **8 hours**, running + hibernated | -| Checkpoints | filesystem **and** live memory | filesystem only | -| Restore from checkpoint | resumes a running process tree | fresh boot with your files | -| Hibernate | checkpoint to storage, wake anywhere | suspend in place | -| Hibernated time | does not count against a lifetime | **counts against the 8 hours** | -| Resize a running sandbox | supported | not supported | -| Sizes | any memory value at create | fixed steps, 1–8 GB, default 4 GB; no 16 GB | -| CPU | selectable via `cpuCount`, scales with memory | allocated by the platform; `cpuCount` is not a control | -| Disk | 20 GB default, up to 256 GB via `diskMB` | **fixed ~16 GB**; `diskMB` is ignored | -| Existing templates | work | **refused if they carry a rootfs** — most do | -| Mounts (FUSE/NFS/overlay) | supported | not possible | -| Image builds | supported | not yet | -| Fork from a checkpoint | supported | not yet | -| Checkpoint patches | supported | not yet | -| Secret storage | on a host outside your sandbox | inside your sandbox, as root | -| Live migration | supported | not applicable | -| Exec, files, PTY, preview URLs, webhooks, signed URLs | — | identical | - -## The three that will bite - -**The 8-hour ceiling** is the one to plan around. It is a property of the platform underneath, -not a quota we can raise, and it counts hibernated time. A sandbox you hibernate for the night -does not come back in the morning. If you have work that assumes a long-lived box, read -[Sandbox lifetime](/microvm/lifetime) before anything else. - -**Memory tops out at 8 GB** and cannot be changed after launch. The 1 GB, 4 GB and 8 GB tiers -carry over; the 16 GB tier does not exist here, and there is no resizing to fall back on. CPU is -no longer selectable either — `cpuCount` is accepted but does not size the sandbox. See -[Sandbox sizes](/microvm/sizes). - -**Checkpoints don't capture memory.** If you use checkpoints to freeze a warmed-up process — -a loaded model, an open connection pool, a debugger mid-session — restoring will not give that -back. You get the filesystem, and your process starts again. If you use checkpoints to capture -an installed environment, nothing changes for you. - -## What gets better - -- Creates come from a warm pool, so start latency is lower and more consistent. -- There is no worker fleet to drain, so deploys stop disturbing running sandboxes. -- An unavailable sandbox size is now refused with the sizes that *are* available, instead of - being reported as a capacity problem. -- A sandbox row can no longer outlive its host, so "running" in the API means running. - - - The full list, split into permanent limits and things still in progress. - diff --git a/docs/v2/api-reference/capacity/create-reservations.mdx b/docs/v2/api-reference/capacity/create-reservations.mdx new file mode 100644 index 000000000..000c94b86 --- /dev/null +++ b/docs/v2/api-reference/capacity/create-reservations.mdx @@ -0,0 +1,68 @@ +--- +title: 'Create Reservation' +api: 'POST /api/capacity/reservations' +--- + +Commit capacity across one or more 15-minute UTC intervals in a single +atomic write. The result is one reservation event with a server-generated +`reservationId` covering all intervals in the request. See +[Reserving capacity](/v2/reserved-capacity/reserving) for the full write +contract. + +Send an `Idempotency-Key` header so retries return the original result. +Same key with a different body returns `idempotency_key_conflict`. + +Reservations are non-refundable — there is no cancellation, modification, +or transfer. + + + One entry per 15-minute interval to reserve. + + + + Interval start. RFC 3339, UTC, aligned to 15 minutes (`:00`, `:15`, `:30`, `:45`). + + + + Interval end. Must equal `startsAt + 15 minutes`. Multi-interval spans are rejected. + + + + Positive multiple of 4 (the grain is 1 GB-hour = 4 GB × 15 min). Other + values are rejected. + + + +```json 200 +{ + "reservationId": "9f67b8f7-7b91-4d2d-b1cb-19d0d0a14562", + "createdAt": "2026-04-28T18:00:05Z", + "intervals": [ + { + "startsAt": "2026-04-29T02:00:00Z", + "endsAt": "2026-04-29T02:15:00Z", + "capacityGb": 16 + }, + { + "startsAt": "2026-04-29T02:15:00Z", + "endsAt": "2026-04-29T02:30:00Z", + "capacityGb": 16 + } + ] +} +``` + +```json 409 capacity_not_available +{ + "error": "capacity_not_available", + "intervals": [ + { + "startsAt": "2026-04-29T02:00:00Z", + "requestedGb": 80, + "reservableGb": 28, + "reason": "insufficient_capacity" + } + ] +} +``` + diff --git a/docs/v2/api-reference/capacity/get-calendar.mdx b/docs/v2/api-reference/capacity/get-calendar.mdx new file mode 100644 index 000000000..f910a8ba0 --- /dev/null +++ b/docs/v2/api-reference/capacity/get-calendar.mdx @@ -0,0 +1,36 @@ +--- +title: 'Get Calendar' +api: 'GET /api/capacity/calendar' +--- + +Return a planning snapshot of 15-minute UTC intervals in the requested window. +One row per interval with current `reservedGb`, `reservableGb`, and the +per-org `reservationLimitGb`. See +[Reading the calendar](/v2/reserved-capacity/calendar) for field semantics. + + + Window start. RFC 3339, aligned to 15 minutes, UTC. + + + + Window end. RFC 3339, aligned to 15 minutes, UTC. + + + +```json 200 +{ + "from": "2026-04-29T02:00:00Z", + "to": "2026-04-29T04:00:00Z", + "resource": "memory_gb", + "intervals": [ + { + "startsAt": "2026-04-29T02:00:00Z", + "endsAt": "2026-04-29T02:15:00Z", + "reservationLimitGb": 300, + "reservedGb": 80, + "reservableGb": 220 + } + ] +} +``` + diff --git a/docs/v2/api-reference/capacity/list-reservations.mdx b/docs/v2/api-reference/capacity/list-reservations.mdx new file mode 100644 index 000000000..7f60c91c9 --- /dev/null +++ b/docs/v2/api-reference/capacity/list-reservations.mdx @@ -0,0 +1,53 @@ +--- +title: 'List Reservations' +api: 'GET /api/capacity/reservations' +--- + +Return reservation events made by the authenticated org, in +reverse-chronological order by `createdAt`. Each reservation row includes +all the intervals it committed to. Useful for end-of-month reconciliation +and audit. + + + Return reservations whose `createdAt >= from`. RFC 3339, UTC. + + + + Return reservations whose `createdAt < to`. RFC 3339, UTC. + + + + Pagination cursor from a previous response's `nextCursor`. + + + + Maximum reservations per page. Server-defined ceiling applies. + + + +```json 200 +{ + "from": "2026-04-01T00:00:00Z", + "to": "2026-05-01T00:00:00Z", + "reservations": [ + { + "reservationId": "9f67b8f7-7b91-4d2d-b1cb-19d0d0a14562", + "createdAt": "2026-04-28T18:00:05Z", + "intervals": [ + { + "startsAt": "2026-04-29T02:00:00Z", + "endsAt": "2026-04-29T02:15:00Z", + "capacityGb": 16 + }, + { + "startsAt": "2026-04-29T02:15:00Z", + "endsAt": "2026-04-29T02:30:00Z", + "capacityGb": 16 + } + ] + } + ], + "nextCursor": null +} +``` + diff --git a/docs/v2/api-reference/checkpoints/create.mdx b/docs/v2/api-reference/checkpoints/create.mdx new file mode 100644 index 000000000..8ebc1370a --- /dev/null +++ b/docs/v2/api-reference/checkpoints/create.mdx @@ -0,0 +1,43 @@ +--- +title: 'Create Checkpoint' +api: 'POST /api/sandboxes/{id}/checkpoints' +--- + +Create a checkpoint of the sandbox state. Each sandbox can have up to 10 full +checkpoints and up to 100 disk-only checkpoints. By default, creating past the +limit for that checkpoint type returns an error. Set `retentionPolicy.mode` to +`delete_oldest` to delete the oldest eligible checkpoint of the same type first +so the new checkpoint can be created. + + + Sandbox ID + + + + Checkpoint name (unique per sandbox) + + + + Checkpoint type. Use `full` to preserve disk, memory, and CPU state, or + `disk_only` to preserve only disk state with a larger per-sandbox limit. + + + + Optional retention policy. Use `{ "mode": "delete_oldest", "maxCount": 10 }` + for full checkpoints or `{ "mode": "delete_oldest", "maxCount": 100 }` for + disk-only checkpoints to delete the oldest eligible checkpoint of the same + type before creating a new one. + + + +```json 201 +{ + "id": "cp-abc123", + "sandboxID": "sb-abc123", + "name": "before-migration", + "status": "processing", + "sizeBytes": 0, + "createdAt": "2025-01-15T10:30:00Z" +} +``` + diff --git a/docs/v2/api-reference/checkpoints/delete.mdx b/docs/v2/api-reference/checkpoints/delete.mdx new file mode 100644 index 000000000..a30340924 --- /dev/null +++ b/docs/v2/api-reference/checkpoints/delete.mdx @@ -0,0 +1,20 @@ +--- +title: 'Delete Checkpoint' +api: 'DELETE /api/sandboxes/{id}/checkpoints/{checkpointId}' +--- + +Delete a checkpoint. + + + Sandbox ID + + + + Checkpoint ID + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/checkpoints/fork.mdx b/docs/v2/api-reference/checkpoints/fork.mdx new file mode 100644 index 000000000..69ffab925 --- /dev/null +++ b/docs/v2/api-reference/checkpoints/fork.mdx @@ -0,0 +1,38 @@ +--- +title: 'Fork from Checkpoint' +api: 'POST /api/sandboxes/from-checkpoint/{checkpointId}' +--- + +Create a new sandbox from a checkpoint. + + + Checkpoint ID + + + + Idle timeout for the new sandbox (default: `300`) + + + + Environment variables to override on the fork. Keys that match the checkpoint's stored envs are replaced; new keys are added. + + + + Name of a secret store to attach. If the checkpoint already has a store, secrets are merged — the new store's values win on collision and egress allowlists are aggregated. + + + + Memory for the forked sandbox, in MB. Clamped to a valid range: the floor is the checkpoint's own memory (a fork can't start smaller than the snapshot it restores, so a smaller value is ignored) and the ceiling is 16384 (16 GB; larger values are capped). The `memoryMB` field in the response reports the effective value after clamping. + + + +```json 201 +{ + "sandboxID": "sb-def456", + "status": "running", + "region": "use2", + "workerID": "w-use2-abc123", + "memoryMB": 4096 +} +``` + diff --git a/docs/v2/api-reference/checkpoints/list.mdx b/docs/v2/api-reference/checkpoints/list.mdx new file mode 100644 index 000000000..fc03b046d --- /dev/null +++ b/docs/v2/api-reference/checkpoints/list.mdx @@ -0,0 +1,25 @@ +--- +title: 'List Checkpoints' +api: 'GET /api/sandboxes/{id}/checkpoints' +--- + +List all checkpoints for a sandbox. + + + Sandbox ID + + + +```json 200 +[ + { + "id": "cp-abc123", + "sandboxID": "sb-abc123", + "name": "before-migration", + "status": "ready", + "sizeBytes": 134217728, + "createdAt": "2025-01-15T10:30:00Z" + } +] +``` + diff --git a/docs/v2/api-reference/checkpoints/restore.mdx b/docs/v2/api-reference/checkpoints/restore.mdx new file mode 100644 index 000000000..31ea59d84 --- /dev/null +++ b/docs/v2/api-reference/checkpoints/restore.mdx @@ -0,0 +1,20 @@ +--- +title: 'Restore Checkpoint' +api: 'POST /api/sandboxes/{id}/checkpoints/{checkpointId}/restore' +--- + +Revert the sandbox in-place to a checkpoint. All changes since the checkpoint are lost. + + + Sandbox ID + + + + Checkpoint ID + + + +```json 200 +{} +``` + diff --git a/docs/v2/api-reference/exec/create-session.mdx b/docs/v2/api-reference/exec/create-session.mdx new file mode 100644 index 000000000..0b0a6c5a2 --- /dev/null +++ b/docs/v2/api-reference/exec/create-session.mdx @@ -0,0 +1,49 @@ +--- +title: 'Create Exec Session' +api: 'POST /api/sandboxes/{id}/exec' +--- + +Start a long-running command as a session. Attach via WebSocket to stream I/O. + + + Sandbox ID + + + + Command to execute + + + + Command arguments + + + + Environment variables + + + + Working directory + + + + Timeout in seconds + + + + Seconds to keep running after all clients disconnect + + + +```json 201 +{ + "sessionID": "es-abc123", + "sandboxID": "sb-abc123", + "command": "node", + "args": ["server.js"], + "running": true, + "exitCode": null, + "startedAt": "2025-01-15T10:30:00Z", + "attachedClients": 0 +} +``` + diff --git a/docs/v2/api-reference/exec/kill-session.mdx b/docs/v2/api-reference/exec/kill-session.mdx new file mode 100644 index 000000000..167cf6c89 --- /dev/null +++ b/docs/v2/api-reference/exec/kill-session.mdx @@ -0,0 +1,24 @@ +--- +title: 'Kill Exec Session' +api: 'POST /api/sandboxes/{id}/exec/{sessionID}/kill' +--- + +Kill an exec session. + + + Sandbox ID + + + + Exec session ID + + + + Signal number (default: `9` / SIGKILL) + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/exec/list-sessions.mdx b/docs/v2/api-reference/exec/list-sessions.mdx new file mode 100644 index 000000000..91784e985 --- /dev/null +++ b/docs/v2/api-reference/exec/list-sessions.mdx @@ -0,0 +1,27 @@ +--- +title: 'List Exec Sessions' +api: 'GET /api/sandboxes/{id}/exec' +--- + +List all exec sessions for a sandbox. + + + Sandbox ID + + + +```json 200 +[ + { + "sessionID": "es-abc123", + "sandboxID": "sb-abc123", + "command": "node", + "args": ["server.js"], + "running": true, + "exitCode": null, + "startedAt": "2025-01-15T10:30:00Z", + "attachedClients": 0 + } +] +``` + diff --git a/docs/v2/api-reference/exec/run.mdx b/docs/v2/api-reference/exec/run.mdx new file mode 100644 index 000000000..581808f8b --- /dev/null +++ b/docs/v2/api-reference/exec/run.mdx @@ -0,0 +1,40 @@ +--- +title: 'Run Command' +api: 'POST /api/sandboxes/{id}/exec/run' +--- + +Execute a command synchronously and return the result. + + + Sandbox ID + + + + Command to execute + + + + Command arguments + + + + Environment variables + + + + Working directory + + + + Timeout in seconds (default: `60`) + + + +```json 200 +{ + "exitCode": 0, + "stdout": "Hello, World!\n", + "stderr": "" +} +``` + diff --git a/docs/v2/api-reference/files/delete.mdx b/docs/v2/api-reference/files/delete.mdx new file mode 100644 index 000000000..ebb76fc16 --- /dev/null +++ b/docs/v2/api-reference/files/delete.mdx @@ -0,0 +1,20 @@ +--- +title: 'Delete File or Directory' +api: 'DELETE /api/sandboxes/{id}/files' +--- + +Delete a file or directory. + + + Sandbox ID + + + + Path to the file or directory to delete + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/files/generate-download-url.mdx b/docs/v2/api-reference/files/generate-download-url.mdx new file mode 100644 index 000000000..107c2d137 --- /dev/null +++ b/docs/v2/api-reference/files/generate-download-url.mdx @@ -0,0 +1,27 @@ +--- +title: 'Generate Download URL' +api: 'POST /api/sandboxes/{id}/files/download-url' +--- + +Generates a signed URL for downloading a file without an API key. See [Signed URLs](/v2/sandboxes/signed-urls) for usage guide. + + + Sandbox ID + + + + Absolute path to the file + + + + URL lifetime in seconds (max: 86400) + + + +```json 200 +{ + "url": "https://app.opencomputer.dev/api/sandboxes/sb-xxx/files/download?path=%2Fapp%2Foutput.zip&expires=1773906434&signature=abc123", + "expiresAt": "2025-01-01T01:00:00Z" +} +``` + diff --git a/docs/v2/api-reference/files/generate-upload-url.mdx b/docs/v2/api-reference/files/generate-upload-url.mdx new file mode 100644 index 000000000..673fea1a7 --- /dev/null +++ b/docs/v2/api-reference/files/generate-upload-url.mdx @@ -0,0 +1,27 @@ +--- +title: 'Generate Upload URL' +api: 'POST /api/sandboxes/{id}/files/upload-url' +--- + +Generates a signed URL for uploading a file without an API key. See [Signed URLs](/v2/sandboxes/signed-urls) for usage guide. + + + Sandbox ID + + + + Absolute path for the destination file + + + + URL lifetime in seconds (max: 86400) + + + +```json 200 +{ + "url": "https://app.opencomputer.dev/api/sandboxes/sb-xxx/files/upload?path=%2Fapp%2Finput.csv&expires=1773906434&signature=abc123", + "expiresAt": "2025-01-01T01:00:00Z" +} +``` + diff --git a/docs/v2/api-reference/files/list-directory.mdx b/docs/v2/api-reference/files/list-directory.mdx new file mode 100644 index 000000000..ec02d8320 --- /dev/null +++ b/docs/v2/api-reference/files/list-directory.mdx @@ -0,0 +1,23 @@ +--- +title: 'List Directory' +api: 'GET /api/sandboxes/{id}/files/list' +--- + +List contents of a directory. + + + Sandbox ID + + + + Directory path (default: `/`) + + + +```json 200 +[ + { "name": "app", "isDir": true, "size": 4096, "path": "/app" }, + { "name": "README.md", "isDir": false, "size": 1234, "path": "/README.md" } +] +``` + diff --git a/docs/v2/api-reference/files/mkdir.mdx b/docs/v2/api-reference/files/mkdir.mdx new file mode 100644 index 000000000..efc85da68 --- /dev/null +++ b/docs/v2/api-reference/files/mkdir.mdx @@ -0,0 +1,20 @@ +--- +title: 'Create Directory' +api: 'POST /api/sandboxes/{id}/files/mkdir' +--- + +Create a new directory. + + + Sandbox ID + + + + Path for the new directory + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/files/read.mdx b/docs/v2/api-reference/files/read.mdx new file mode 100644 index 000000000..0a6c45c84 --- /dev/null +++ b/docs/v2/api-reference/files/read.mdx @@ -0,0 +1,20 @@ +--- +title: 'Read File' +api: 'GET /api/sandboxes/{id}/files' +--- + +Returns file content as plain text (not JSON). + + + Sandbox ID + + + + Absolute path to the file + + + +```text 200 +Hello, World! +``` + diff --git a/docs/v2/api-reference/files/signed-download.mdx b/docs/v2/api-reference/files/signed-download.mdx new file mode 100644 index 000000000..1cc185536 --- /dev/null +++ b/docs/v2/api-reference/files/signed-download.mdx @@ -0,0 +1,36 @@ +--- +title: 'Signed Download' +api: 'GET /api/sandboxes/{id}/files/download' +--- + +Download a file using a signed URL. No authentication required — the signature and expiry are validated from query parameters. + +Generate a signed URL first via [Generate Download URL](/v2/api-reference/files/generate-download-url). + + + Sandbox ID + + + + Absolute path to the file + + + + Unix timestamp when the URL expires + + + + HMAC-SHA256 signature + + + +```text 200 +(file content with Content-Length header) +``` + +```json 403 +{ + "error": "signed URL has expired" +} +``` + diff --git a/docs/v2/api-reference/files/signed-upload.mdx b/docs/v2/api-reference/files/signed-upload.mdx new file mode 100644 index 000000000..be8840a04 --- /dev/null +++ b/docs/v2/api-reference/files/signed-upload.mdx @@ -0,0 +1,36 @@ +--- +title: 'Signed Upload' +api: 'PUT /api/sandboxes/{id}/files/upload' +--- + +Upload a file using a signed URL. No authentication required — the signature and expiry are validated from query parameters. Send file content as the request body. + +Generate a signed URL first via [Generate Upload URL](/v2/api-reference/files/generate-upload-url). + + + Sandbox ID + + + + Absolute path for the destination file + + + + Unix timestamp when the URL expires + + + + HMAC-SHA256 signature + + + +```text 204 +(no content) +``` + +```json 403 +{ + "error": "invalid signature" +} +``` + diff --git a/docs/v2/api-reference/files/write.mdx b/docs/v2/api-reference/files/write.mdx new file mode 100644 index 000000000..76d980715 --- /dev/null +++ b/docs/v2/api-reference/files/write.mdx @@ -0,0 +1,20 @@ +--- +title: 'Write File' +api: 'PUT /api/sandboxes/{id}/files' +--- + +Write content to a file. The request body is written as raw file content (not JSON). + + + Sandbox ID + + + + Absolute path for the destination file + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/overview.mdx b/docs/v2/api-reference/overview.mdx new file mode 100644 index 000000000..2476206cf --- /dev/null +++ b/docs/v2/api-reference/overview.mdx @@ -0,0 +1,48 @@ +--- +title: "API Reference" +description: "Complete REST API reference for OpenComputer" +--- + +## Base URL + +``` +https://app.opencomputer.dev/api +``` + +## Authentication + +All requests require an API key passed via the `X-API-Key` header. + +```bash +curl https://app.opencomputer.dev/api/sandboxes \ + -H "X-API-Key: $OPENCOMPUTER_API_KEY" +``` + +## WebSocket Authentication + +WebSocket endpoints accept auth via query parameter: + +``` +wss://app.opencomputer.dev/api/sandboxes/{id}/exec/{sessionID}?api_key= +``` + +## Error Format + +All errors use a consistent JSON envelope: + +```json +{ + "error": "descriptive error message" +} +``` + +| Status Code | Meaning | +| --- | --- | +| `400` | Invalid request (missing fields, bad values) | +| `401` | Missing or invalid authentication | +| `403` | Insufficient permissions | +| `404` | Resource not found | +| `409` | Conflict (duplicate resource) | +| `429` | Quota exceeded | +| `500` | Internal server error | +| `503` | Feature unavailable in current deployment mode | diff --git a/docs/v2/api-reference/patches/create.mdx b/docs/v2/api-reference/patches/create.mdx new file mode 100644 index 000000000..17dbf0154 --- /dev/null +++ b/docs/v2/api-reference/patches/create.mdx @@ -0,0 +1,34 @@ +--- +title: 'Create Patch' +api: 'POST /api/sandboxes/checkpoints/{checkpointId}/patches' +--- + +Create a patch script that runs when a sandbox is spawned from a checkpoint. Patches apply in sequence order. + + + Checkpoint ID + + + + Bash script to execute on spawn + + + + Human-readable description + + + +```json 201 +{ + "patch": { + "id": "pa-abc123", + "checkpointId": "cp-abc123", + "script": "apt install -y curl", + "description": "Install curl", + "strategy": "on_wake", + "sequence": 1, + "createdAt": "2025-01-15T10:30:00Z" + } +} +``` + diff --git a/docs/v2/api-reference/patches/delete.mdx b/docs/v2/api-reference/patches/delete.mdx new file mode 100644 index 000000000..7f28b0858 --- /dev/null +++ b/docs/v2/api-reference/patches/delete.mdx @@ -0,0 +1,20 @@ +--- +title: 'Delete Patch' +api: 'DELETE /api/sandboxes/checkpoints/{checkpointId}/patches/{patchId}' +--- + +Delete a patch. + + + Checkpoint ID + + + + Patch ID + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/patches/list.mdx b/docs/v2/api-reference/patches/list.mdx new file mode 100644 index 000000000..050a95959 --- /dev/null +++ b/docs/v2/api-reference/patches/list.mdx @@ -0,0 +1,26 @@ +--- +title: 'List Patches' +api: 'GET /api/sandboxes/checkpoints/{checkpointId}/patches' +--- + +List all patches for a checkpoint. + + + Checkpoint ID + + + +```json 200 +[ + { + "id": "pa-abc123", + "checkpointId": "cp-abc123", + "script": "apt install -y curl", + "description": "Install curl", + "strategy": "on_wake", + "sequence": 1, + "createdAt": "2025-01-15T10:30:00Z" + } +] +``` + diff --git a/docs/v2/api-reference/preview/create.mdx b/docs/v2/api-reference/preview/create.mdx new file mode 100644 index 000000000..91fd171c6 --- /dev/null +++ b/docs/v2/api-reference/preview/create.mdx @@ -0,0 +1,35 @@ +--- +title: 'Create Preview URL' +api: 'POST /api/sandboxes/{id}/preview' +--- + +Create a preview URL to expose a port from the sandbox. + + + Sandbox ID + + + + Container port (1–65535) + + + + Custom domain + + + + Authentication configuration + + + +```json 201 +{ + "id": "pv-abc123", + "sandboxId": "sb-abc123", + "hostname": "sb-abc123-p3000.workers.opencomputer.dev", + "port": 3000, + "sslStatus": "active", + "createdAt": "2025-01-15T10:30:00Z" +} +``` + diff --git a/docs/v2/api-reference/preview/delete.mdx b/docs/v2/api-reference/preview/delete.mdx new file mode 100644 index 000000000..1aeec6eac --- /dev/null +++ b/docs/v2/api-reference/preview/delete.mdx @@ -0,0 +1,20 @@ +--- +title: 'Delete Preview URL' +api: 'DELETE /api/sandboxes/{id}/preview/{port}' +--- + +Delete a preview URL. + + + Sandbox ID + + + + Port number + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/preview/list.mdx b/docs/v2/api-reference/preview/list.mdx new file mode 100644 index 000000000..532b85b61 --- /dev/null +++ b/docs/v2/api-reference/preview/list.mdx @@ -0,0 +1,25 @@ +--- +title: 'List Preview URLs' +api: 'GET /api/sandboxes/{id}/preview' +--- + +List all preview URLs for a sandbox. + + + Sandbox ID + + + +```json 200 +[ + { + "id": "pv-abc123", + "sandboxId": "sb-abc123", + "hostname": "sb-abc123-p3000.workers.opencomputer.dev", + "port": 3000, + "sslStatus": "active", + "createdAt": "2025-01-15T10:30:00Z" + } +] +``` + diff --git a/docs/v2/api-reference/preview/rotate-auth.mdx b/docs/v2/api-reference/preview/rotate-auth.mdx new file mode 100644 index 000000000..fcaf9ef71 --- /dev/null +++ b/docs/v2/api-reference/preview/rotate-auth.mdx @@ -0,0 +1,31 @@ +--- +title: 'Rotate Preview-URL Auth Token' +api: 'POST /api/sandboxes/{id}/preview/rotate' +--- + +Mint a new bearer token for the sandbox's preview-URL auth gate. The old token stops working immediately — there is no zero-downtime dual-token mode, so roll the new token out to your caller before discarding the old one. + +If the sandbox was originally created without [`previewAuth`](/v2/api-reference/sandboxes/create), this call installs a token and starts enforcing the gate from that point on. + +The plaintext is returned exactly once in the response. Only the SHA-256 hash is persisted server-side; the server cannot show you the token again later. + + + Sandbox ID + + + +```json 200 +{ + "previewAuthToken": "qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI", + "scheme": "bearer" +} +``` + + +### Errors + +| Status | When | +| --- | --- | +| `404` | Sandbox not found, or owned by a different org | +| `410` | Sandbox is `stopped` or `error` | +| `503` | Database not configured (combined-mode CP without PG) | diff --git a/docs/v2/api-reference/pty/create.mdx b/docs/v2/api-reference/pty/create.mdx new file mode 100644 index 000000000..f6ebe2631 --- /dev/null +++ b/docs/v2/api-reference/pty/create.mdx @@ -0,0 +1,31 @@ +--- +title: 'Create PTY Session' +api: 'POST /api/sandboxes/{id}/pty' +--- + +Create an interactive terminal session. + + + Sandbox ID + + + + Terminal columns (default: `80`) + + + + Terminal rows (default: `24`) + + + + Shell path (default: `/bin/bash`) + + + +```json 201 +{ + "sessionID": "ps-abc123", + "sandboxID": "sb-abc123" +} +``` + diff --git a/docs/v2/api-reference/pty/kill.mdx b/docs/v2/api-reference/pty/kill.mdx new file mode 100644 index 000000000..d57a967f2 --- /dev/null +++ b/docs/v2/api-reference/pty/kill.mdx @@ -0,0 +1,20 @@ +--- +title: 'Kill PTY Session' +api: 'DELETE /api/sandboxes/{id}/pty/{sessionID}' +--- + +Terminate a PTY session. + + + Sandbox ID + + + + PTY session ID + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/pty/resize.mdx b/docs/v2/api-reference/pty/resize.mdx new file mode 100644 index 000000000..d106e65a1 --- /dev/null +++ b/docs/v2/api-reference/pty/resize.mdx @@ -0,0 +1,32 @@ +--- +title: 'Resize PTY' +api: 'POST /api/sandboxes/{id}/pty/{sessionID}/resize' +--- + +Resize a PTY session. + + + PTY resize is HTTP-only — not exposed in the TypeScript or Python SDKs. The SDKs handle resize automatically for interactive sessions. + + + + Sandbox ID + + + + PTY session ID + + + + New column count + + + + New row count + + + +```json 200 +{} +``` + diff --git a/docs/v2/api-reference/sandboxes/create.mdx b/docs/v2/api-reference/sandboxes/create.mdx new file mode 100644 index 000000000..3ed9c135a --- /dev/null +++ b/docs/v2/api-reference/sandboxes/create.mdx @@ -0,0 +1,84 @@ +--- +title: 'Create Sandbox' +api: 'POST /api/sandboxes' +--- + +Create a new sandbox. + + + Template name (default: `"base"`) + + + + Idle timeout in seconds (default: `300`) + + + + CPU cores. If omitted but `memoryMB` is set, inferred automatically. + + + + Memory in MB. If omitted but `cpuCount` is set, inferred automatically. + + + + Create a [Burst Sandbox](/v2/sandboxes/burst-sandboxes). Disk is preserved across infrastructure restarts; processes may restart. + + +The 1 GB tier provides 1 vCPU on a best-effort basis. For guaranteed CPU allocation, use the 4 GB tier or above. + +If both `cpuCount` and `memoryMB` are provided, they must match a platform tier. + + + Environment variables as key-value pairs + + + + Arbitrary key-value pairs + + + + Declarative image manifest (see [Image builder](/v2/sandboxes/templates#image-configuration)) + + + + Name of a pre-built snapshot for instant boot + + + + Register [webhook](/v2/sandboxes/webhooks) destination(s) for this sandbox's lifecycle events, pinned to this sandbox. Registering inline (rather than via a separate call) means the endpoints exist **before** `sandbox.created` / `sandbox.ready` are relayed, so you don't miss the first events. + + - `url` *(string, required)*: HTTPS endpoint. + - `secret` *(string)*: signing secret; omit and one is generated and returned on the response (`webhooks[].secret`), also re-fetchable later. + - `eventTypes` *(string[])*: event-type allow-list (default all). + + Each spec is validated up-front (same rules as [`POST /api/webhooks`](/v2/api-reference/webhooks/create)): a non-HTTPS `url` or unknown `eventTypes` is rejected with `400`. Registration is otherwise **best-effort** — if a spec fails to register downstream, the sandbox is still created; inspect the echoed `webhooks: [{ id, url, secret? }]` to see what registered. + + + + Opt in to bearer-token authentication on the sandbox's preview URLs. When set, every request to `https://sb--p.` must include an `Authorization: Bearer ` (or `X-OC-Preview-Token: `) header; missing or wrong → 401. + + - `scheme` *(string)*: must be `"bearer"`. Reserved for HMAC/JWT later. + - `token` *(string)*: `"auto"` (or omitted) → server generates a 256-bit random token. An explicit string of at least 16 characters lets you bring your own. + + The plaintext is returned exactly once in the response as `previewAuthToken`; only its SHA-256 hash is stored. Use [`POST /api/sandboxes/{id}/preview/rotate`](/v2/api-reference/preview/rotate-auth) to mint a new one. + + Omit this field for the legacy open behavior — preview URLs respond to anyone who can reach the hostname. + + + +```json 201 +{ + "sandboxID": "sb-abc123", + "status": "running", + "region": "use2", + "workerID": "w-use2-abc123", + "previewAuthToken": "qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI", + "webhooks": [ + { "id": "whk_3f9a2c", "url": "https://app.example.com/oc-webhook", "secret": "whsec_Hk9…" } + ] +} +``` + +`previewAuthToken` is only present when `previewAuth` was set in the request — read it once and store it durably; the server will not return it again. `webhooks` is present only when `webhooks` were requested; each `secret` (`whsec_…`) is returned here and stays re-fetchable via [`GET /api/webhooks/{id}/secret`](/v2/api-reference/webhooks/get). + diff --git a/docs/v2/api-reference/sandboxes/delete.mdx b/docs/v2/api-reference/sandboxes/delete.mdx new file mode 100644 index 000000000..da2563c60 --- /dev/null +++ b/docs/v2/api-reference/sandboxes/delete.mdx @@ -0,0 +1,16 @@ +--- +title: 'Kill Sandbox' +api: 'DELETE /api/sandboxes/{id}' +--- + +Terminate and remove a sandbox. + + + Sandbox ID + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/sandboxes/get-tags.mdx b/docs/v2/api-reference/sandboxes/get-tags.mdx new file mode 100644 index 000000000..f7c4949c9 --- /dev/null +++ b/docs/v2/api-reference/sandboxes/get-tags.mdx @@ -0,0 +1,28 @@ +--- +title: 'Get Sandbox Tags' +api: 'GET /api/sandboxes/{id}/tags' +--- + +Read the current tag set for a sandbox. + + + **Preview:** Usage and tag APIs are new. Endpoints, response fields, + and SDK method names may change before GA; temporary inaccuracies or + rough edges are possible while the surface settles. + + + + Sandbox ID. + + + +```json 200 +{ + "tags": { "env": "prod", "team": "payments" }, + "tagsLastUpdatedAt": "2026-04-19T14:02:00Z" +} +``` + + +A sandbox with no tags returns `"tags": {}` and `"tagsLastUpdatedAt": +null`. diff --git a/docs/v2/api-reference/sandboxes/get.mdx b/docs/v2/api-reference/sandboxes/get.mdx new file mode 100644 index 000000000..03131e787 --- /dev/null +++ b/docs/v2/api-reference/sandboxes/get.mdx @@ -0,0 +1,27 @@ +--- +title: 'Get Sandbox' +api: 'GET /api/sandboxes/{id}' +--- + +Get sandbox details by ID. Available on both control plane and worker. + + + Sandbox ID + + + +```json 200 +{ + "sandboxID": "sb-abc123", + "status": "running", + "region": "use2", + "workerID": "w-use2-abc123", + "tags": { "env": "prod", "team": "payments" }, + "tagsLastUpdatedAt": "2026-04-19T14:02:00Z" +} +``` + + +`tags` and `tagsLastUpdatedAt` are always present — empty object and +`null` respectively when no tags are set. Update them with +[`PUT /api/sandboxes/{id}/tags`](/v2/api-reference/sandboxes/set-tags). diff --git a/docs/v2/api-reference/sandboxes/hibernate.mdx b/docs/v2/api-reference/sandboxes/hibernate.mdx new file mode 100644 index 000000000..b35ef49e2 --- /dev/null +++ b/docs/v2/api-reference/sandboxes/hibernate.mdx @@ -0,0 +1,21 @@ +--- +title: 'Hibernate Sandbox' +api: 'POST /api/sandboxes/{id}/hibernate' +--- + +Snapshot VM state and stop the sandbox. Control plane only. + + + Sandbox ID + + + +```json 200 +{ + "sandboxID": "sb-abc123", + "hibernationKey": "checkpoints/sb-abc123/1234567890.tar.zst", + "sizeBytes": 134217728, + "status": "hibernated" +} +``` + diff --git a/docs/v2/api-reference/sandboxes/list.mdx b/docs/v2/api-reference/sandboxes/list.mdx new file mode 100644 index 000000000..4f656535a --- /dev/null +++ b/docs/v2/api-reference/sandboxes/list.mdx @@ -0,0 +1,25 @@ +--- +title: 'List Sandboxes' +api: 'GET /api/sandboxes' +--- + +List all running sandboxes. + + +```json 200 +[ + { + "sandboxID": "sb-abc123", + "status": "running", + "region": "use2", + "workerID": "w-use2-abc123", + "tags": { "env": "prod" }, + "tagsLastUpdatedAt": "2026-04-19T14:02:00Z" + } +] +``` + + +Each entry carries `tags` (always present, empty object when unset) +and `tagsLastUpdatedAt` (null when unset). To filter by tag, use +[`GET /api/usage?groupBy=sandbox&filter[tag:]=`](/v2/api-reference/usage/get-usage). diff --git a/docs/v2/api-reference/sandboxes/set-tags.mdx b/docs/v2/api-reference/sandboxes/set-tags.mdx new file mode 100644 index 000000000..9a3dd4811 --- /dev/null +++ b/docs/v2/api-reference/sandboxes/set-tags.mdx @@ -0,0 +1,47 @@ +--- +title: 'Set Sandbox Tags' +api: 'PUT /api/sandboxes/{id}/tags' +--- + +Replace the full tag set for a sandbox. Partial updates are not +supported — GET, modify, PUT. `{}` clears all tags. + + + **Preview:** Usage and tag APIs are new. Endpoints, response fields, + and SDK method names may change before GA; temporary inaccuracies or + rough edges are possible while the surface settles. + + + + Sandbox ID. + + + + Each top-level key in the body is a tag name; the value is a + string. Flat map only — nested objects are rejected. + + +### Validation + +- At most 50 tag keys per sandbox. +- Keys: 1–128 characters, `A–Z a–z 0–9 _ . - :`. `:` is allowed as + a user namespace separator. +- Values: 0–256 UTF-8 characters. +- The `oc:` key prefix is reserved for future system-set tags. + + +```json 200 +{ + "tags": { "env": "staging", "team": "growth" }, + "tagsLastUpdatedAt": "2026-04-22T20:31:14Z" +} +``` + + + +Retagging rewrites attribution going forward: a sandbox re-tagged +from `team=payments` to `team=growth` will appear under `growth` in +every subsequent /usage query, including for time before the retag. +Surface `tagsLastUpdatedAt` alongside usage in dashboards so readers +can see when tags changed. + diff --git a/docs/v2/api-reference/sandboxes/set-timeout.mdx b/docs/v2/api-reference/sandboxes/set-timeout.mdx new file mode 100644 index 000000000..310938f6a --- /dev/null +++ b/docs/v2/api-reference/sandboxes/set-timeout.mdx @@ -0,0 +1,20 @@ +--- +title: 'Set Timeout' +api: 'POST /api/sandboxes/{id}/timeout' +--- + +Update the idle timeout for a sandbox. + + + Sandbox ID + + + + New timeout in seconds (must be > 0) + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/sandboxes/wake.mdx b/docs/v2/api-reference/sandboxes/wake.mdx new file mode 100644 index 000000000..e618ecc38 --- /dev/null +++ b/docs/v2/api-reference/sandboxes/wake.mdx @@ -0,0 +1,25 @@ +--- +title: 'Wake Sandbox' +api: 'POST /api/sandboxes/{id}/wake' +--- + +Resume a hibernated sandbox. Control plane only. + + + Sandbox ID + + + + Idle timeout after wake (default: `300`) + + + +```json 200 +{ + "sandboxID": "sb-abc123", + "status": "running", + "region": "use2", + "workerID": "w-use2-abc123" +} +``` + diff --git a/docs/v2/api-reference/snapshots/create.mdx b/docs/v2/api-reference/snapshots/create.mdx new file mode 100644 index 000000000..fbed5ac19 --- /dev/null +++ b/docs/v2/api-reference/snapshots/create.mdx @@ -0,0 +1,29 @@ +--- +title: 'Create Snapshot' +api: 'POST /api/snapshots' +--- + +Create a pre-built sandbox environment from a declarative image manifest. + + + Unique snapshot name + + + + Declarative image manifest (see [Image builder](/v2/sandboxes/templates#image-configuration)) + + + +```json 201 +{ + "id": "snap-abc123", + "name": "data-science", + "status": "building", + "contentHash": "sha256:...", + "checkpointId": "", + "manifest": { "steps": [] }, + "createdAt": "2025-01-15T10:30:00Z", + "lastUsedAt": "" +} +``` + diff --git a/docs/v2/api-reference/snapshots/delete.mdx b/docs/v2/api-reference/snapshots/delete.mdx new file mode 100644 index 000000000..062f22bd4 --- /dev/null +++ b/docs/v2/api-reference/snapshots/delete.mdx @@ -0,0 +1,16 @@ +--- +title: 'Delete Snapshot' +api: 'DELETE /api/snapshots/{name}' +--- + +Delete a snapshot. + + + Snapshot name + + + +```json 204 +{} +``` + diff --git a/docs/v2/api-reference/snapshots/get.mdx b/docs/v2/api-reference/snapshots/get.mdx new file mode 100644 index 000000000..93d94a70e --- /dev/null +++ b/docs/v2/api-reference/snapshots/get.mdx @@ -0,0 +1,25 @@ +--- +title: 'Get Snapshot' +api: 'GET /api/snapshots/{name}' +--- + +Get snapshot details by name. + + + Snapshot name + + + +```json 200 +{ + "id": "snap-abc123", + "name": "data-science", + "status": "ready", + "contentHash": "sha256:...", + "checkpointId": "cp-abc123", + "manifest": { "steps": [] }, + "createdAt": "2025-01-15T10:30:00Z", + "lastUsedAt": "2025-01-16T08:00:00Z" +} +``` + diff --git a/docs/v2/api-reference/snapshots/list.mdx b/docs/v2/api-reference/snapshots/list.mdx new file mode 100644 index 000000000..a7fd9e071 --- /dev/null +++ b/docs/v2/api-reference/snapshots/list.mdx @@ -0,0 +1,23 @@ +--- +title: 'List Snapshots' +api: 'GET /api/snapshots' +--- + +List all snapshots. + + +```json 200 +[ + { + "id": "snap-abc123", + "name": "data-science", + "status": "ready", + "contentHash": "sha256:...", + "checkpointId": "cp-abc123", + "manifest": { "steps": [] }, + "createdAt": "2025-01-15T10:30:00Z", + "lastUsedAt": "2025-01-16T08:00:00Z" + } +] +``` + diff --git a/docs/v2/api-reference/usage/get-sandbox-usage.mdx b/docs/v2/api-reference/usage/get-sandbox-usage.mdx new file mode 100644 index 000000000..a1cd65678 --- /dev/null +++ b/docs/v2/api-reference/usage/get-sandbox-usage.mdx @@ -0,0 +1,197 @@ +--- +title: 'Sandbox Usage' +api: 'GET /api/sandboxes/{id}/usage' +--- + +Per-sandbox memory usage over a window, as 1-minute points plus +envelope totals. Allocated memory is the tier the sandbox was +provisioned at, integrated over time; used memory is the actual +resident memory consumed by the sandbox process, sampled every 60s. + + + **Preview:** Usage and tag APIs are new. Endpoints, response fields, + and SDK method names may change before GA; temporary inaccuracies or + rough edges are possible while the surface settles. + + +## Request + + + Sandbox ID. + + + + Lower bound. Accepts either an ISO date (`2026-05-27`, interpreted + as UTC midnight) or an RFC3339 timestamp. Default: now minus 1 hour. + + + + Upper bound. Same accepted formats as `from`. Default: now. Window + must be ≤ 30 days. + + +## Response + + + Echo of the requested sandbox ID. + + + + The sandbox's display name (set at create time). Absent if no alias + was provided. + + + + RFC3339 lower bound of the window the response covers — either the + caller's `from`, or the server default (`now - 1h`) when omitted. + + + + RFC3339 upper bound. Either the caller's `to`, or the server + default (`now`) when omitted. + + + + Envelope totals for the full window. Each additive field equals the + sum of the matching field across `points[]` — useful for getting + the headline numbers without iterating. + + + + Total GiB-seconds the sandbox was provisioned for. This is the + physical quantity the bill is computed from. For a sandbox at + 1 GiB running for 1 hour, this is `3600`. + + + + Total GiB-seconds the sandbox actually consumed (integrated + resident memory). Compare against `memoryAllocatedGbSeconds` to + see headroom — `1 - used/allocated` is wasted provisioning. + + + + Number of seconds the sandbox was provisioned (i.e., had an + open scale event) within the window. A 1-hour window where the + sandbox ran the whole time returns `3600`. + + + + Maximum provisioned memory tier (MiB) seen in the window. + Reflects the highest size the sandbox was resized to. + + + + Maximum measured resident memory (MiB) in any sample during the + window. Useful for detecting workloads that briefly approached + their tier ceiling. + + + + + + Time-ordered array of 1-minute buckets covering `[from, to)`. + Buckets are minute-aligned in UTC. Boundary buckets are clamped to + the original window — a mid-minute `from` produces a partial first + bucket whose integrals reflect only the overlap with the requested + range. + + Minutes where the sandbox was not provisioned (before first start, + after final stop) appear with all fields at `0` rather than as + missing entries — gaps read as continuous flat zero on a chart. + + + + RFC3339 timestamp of the bucket start (minute-aligned, UTC). + The bucket covers `[ts, ts + 1 minute)`. + + + + Provisioned memory integrated over this minute, in GiB-seconds. + Summing this across all points reproduces + `totals.memoryAllocatedGbSeconds` exactly. + + + + Measured resident memory integrated over this minute, in + GiB-seconds. Same compositional property — summing reproduces + `totals.memoryUsedGbSeconds`. + + + + Seconds within this minute the sandbox was provisioned. Usually + `60`; `0` when not running; a fractional value (e.g., `30`) + appears in the resize or start/stop bucket where the sandbox + began or ended part-way through the minute. + + + + Time-weighted average of the provisioned memory tier (MiB) + across this bucket. When a resize falls inside the bucket, this + shows the blend (e.g., 30s at 2048 + 30s at 4096 → `3072`). + Designed to render directly as a step line on a chart. + + + + Average measured resident memory (MiB) across samples that fell + in this bucket. The natural Y value for a "memory used" chart. + + + + Maximum measured resident memory (MiB) across samples in this + bucket. Catches sub-minute spikes that the average smooths out. + + + + +### Units + +All `*GbSeconds` fields are **GiB-seconds** (binary, 230 +bytes/GiB), not decimal GB. For a sandbox provisioned at 1024 MiB +running for one minute, `memoryAllocatedGbSeconds` is +`1024/1024 × 60 = 60`. For a measured RSS of 612 MiB, +`memoryUsedGbSeconds` is `612/1024 × 60 ≈ 35.86`. + + +```json 200 +{ + "sandboxId": "sb-abc", + "alias": "my-agent", + "from": "2026-05-27T00:00:00Z", + "to": "2026-05-28T00:00:00Z", + "totals": { + "memoryAllocatedGbSeconds": 86400, + "memoryUsedGbSeconds": 51640, + "uptimeSeconds": 86400, + "memoryAllocatedPeakMb": 1024, + "memoryUsedPeakMb": 742 + }, + "points": [ + { + "ts": "2026-05-27T00:00:00Z", + "memoryAllocatedGbSeconds": 60.0, + "memoryUsedGbSeconds": 35.86, + "uptimeSeconds": 60, + "allocatedMemoryMb": 1024, + "usedMemoryMbAvg": 612, + "usedMemoryMbPeak": 720 + }, + { + "ts": "2026-05-27T00:01:00Z", + "memoryAllocatedGbSeconds": 60.0, + "memoryUsedGbSeconds": 40.02, + "uptimeSeconds": 60, + "allocatedMemoryMb": 1024, + "usedMemoryMbAvg": 683, + "usedMemoryMbPeak": 742 + } + ] +} +``` + + +### Use the aggregator for cross-sandbox questions + +To rank sandboxes by usage, use [`GET /api/usage`](/v2/api-reference/usage/get-usage) +— that endpoint serves cross-sandbox aggregates and supports +filter/sort/cursor. The per-sandbox endpoint here is for "look at +this one sandbox in detail." diff --git a/docs/v2/api-reference/usage/get-usage.mdx b/docs/v2/api-reference/usage/get-usage.mdx new file mode 100644 index 000000000..0906148de --- /dev/null +++ b/docs/v2/api-reference/usage/get-usage.mdx @@ -0,0 +1,219 @@ +--- +title: 'Usage Aggregator' +api: 'GET /api/usage' +--- + +Aggregate usage grouped by sandbox or by tag. Dollars are not exposed +— the platform returns the same physical quantities the invoice is +computed from. For per-sandbox time-series drilldown (memory +utilization over time, 1-minute resolution), see +[`GET /api/sandboxes/:id/usage`](/v2/api-reference/usage/get-sandbox-usage). + + + **Preview:** Usage and tag APIs are new. Endpoints, response fields, + and SDK method names may change before GA; temporary inaccuracies or + rough edges are possible while the surface settles. + + + +All `*GbSeconds` fields are **GiB-seconds** (binary, 230 +bytes/GiB), not decimal GB. For a sandbox provisioned at 1024 MiB +running for one second, `memoryGbSeconds` is `1`. + + + +Freshness: accurate to the minute under normal conditions. A sandbox +that exited uncleanly continues to accrue against its provisioned +tier until the platform reconciles state — the same behavior the +billing pipeline uses, so the numbers here match the invoice. + + +## Request + + + `sandbox` for top sandboxes, or `tag:` for usage by tag value. + Keys may contain `:` — everything after the first `:` is the tag + key (so `groupBy=tag:team:payments` groups by the key + `team:payments`). + + + + Inclusive lower bound. Accepts either an ISO date (`2026-05-27`, + interpreted as UTC midnight) or an RFC3339 timestamp. Default: now + minus 30 days. Max window: 90 days. + + + + Exclusive upper bound. Same accepted formats as `from`. Default: + now. + + + + One param per dimension. Comma-separated values are OR'd within + that dimension; different `filter[...]` params are AND'd across + dimensions. Passing the same `filter[...]` key twice returns 400 — + put multiple OR values in one comma-separated string instead. Empty + value (`filter[tag:team]=`) matches sandboxes without the `team` + key. + + + + `-memoryGbSeconds` (default) or `-diskOverageGbSeconds`. + + + + Max items per page. Default 50, max 500. + + + + Opaque cursor from a prior response's `nextCursor`. + + +## Response + + + RFC3339 echo of the effective window lower bound. + + + + RFC3339 echo of the effective window upper bound. + + + + Echo of the requested `groupBy` value. + + + + Aggregate across **all rows matching the window and filters**, not + just the current page. Use this for headline numbers; sum `items[]` + if you need a per-page subtotal instead. + + + + Total provisioned memory × time across matching sandboxes, in + GiB-seconds. + + + + Total disk-overage × time. Disk usage within the free + allowance does not contribute; this is the billable excess. + + + + + + Paginated rows, sorted by `sort` (default: largest first). + + When `groupBy=sandbox`, each row is one sandbox: + + + + The sandbox identifier. + + + Display name set at create time. Absent when none was provided. + + + Current sandbox status (e.g., `running`, `hibernated`, + `stopped`). Absent when no session exists. + + + Current tag set on the sandbox. Empty object when none. + + + RFC3339 timestamp of the most recent tag change, or `null` if + no tags exist. + + + Provisioned memory × time for this sandbox in the window. + + + Disk overage × time for this sandbox. + + + + When `groupBy=tag:`, each row is one tag value: + + + + The tag key from the request (e.g., `team`). + + + The tag value for this group. + + + Total provisioned memory × time across sandboxes carrying + `tagKey=tagValue`. + + + Total disk overage × time for the same group. + + + Distinct sandboxes contributing to this row. + + + + + + Only present when `groupBy=tag:`. Sibling bucket for sandboxes + that lack the grouping key — without it, untagged sandboxes would + silently disappear from the rollup. Same shape as a tag row minus + `tagKey`/`tagValue`. + + + + Total provisioned memory × time across untagged sandboxes. + + + Total disk overage × time. + + + Distinct sandboxes in the untagged bucket. + + + + + + Opaque cursor to pass as `cursor=` on the next request, or `null` + when no further pages exist. + + + +```json groupBy=sandbox +{ + "from": "2026-03-23T00:00:00Z", + "to": "2026-04-22T00:00:00Z", + "groupBy": "sandbox", + "total": { "memoryGbSeconds": 20000, "diskOverageGbSeconds": 360 }, + "items": [ + { + "sandboxId": "sb-abc", + "alias": "my-agent", + "status": "running", + "tags": { "env": "prod", "team": "payments" }, + "tagsLastUpdatedAt": "2026-04-19T14:02:00Z", + "memoryGbSeconds": 8000, + "diskOverageGbSeconds": 120 + } + ], + "nextCursor": null +} +``` + +```json groupBy=tag:team +{ + "from": "2026-03-23T00:00:00Z", + "to": "2026-04-22T00:00:00Z", + "groupBy": "tag:team", + "total": { "memoryGbSeconds": 19000, "diskOverageGbSeconds": 340 }, + "untagged": { "memoryGbSeconds": 1000, "diskOverageGbSeconds": 20, "sandboxCount": 2 }, + "items": [ + { "tagKey": "team", "tagValue": "payments", + "memoryGbSeconds": 8000, "diskOverageGbSeconds": 120, "sandboxCount": 12 }, + { "tagKey": "team", "tagValue": "growth", + "memoryGbSeconds": 4000, "diskOverageGbSeconds": 50, "sandboxCount": 5 } + ], + "nextCursor": null +} +``` + diff --git a/docs/v2/api-reference/usage/list-tags.mdx b/docs/v2/api-reference/usage/list-tags.mdx new file mode 100644 index 000000000..ede6fb64d --- /dev/null +++ b/docs/v2/api-reference/usage/list-tags.mdx @@ -0,0 +1,31 @@ +--- +title: 'List Tag Keys' +api: 'GET /api/tags' +--- + +List all tag keys set on any sandbox in the org, with per-key counts +of tagged sandboxes and distinct values. Useful for building +group-by pickers without querying the full tag set. + + + **Preview:** Usage and tag APIs are new. Endpoints, response fields, + and SDK method names may change before GA; temporary inaccuracies or + rough edges are possible while the surface settles. + + + +```json 200 +{ + "keys": [ + { "key": "team", "sandboxCount": 17, "valueCount": 4 }, + { "key": "env", "sandboxCount": 23, "valueCount": 3 } + ] +} +``` + + + +Tag keys on torn-down sandboxes are retained so historical drilldowns +keep resolving. As a result `sandboxCount` may include sandboxes +that are no longer active. + diff --git a/docs/v2/api-reference/webhooks/create.mdx b/docs/v2/api-reference/webhooks/create.mdx new file mode 100644 index 000000000..c3f9fc7dc --- /dev/null +++ b/docs/v2/api-reference/webhooks/create.mdx @@ -0,0 +1,57 @@ +--- +title: 'Create Webhook' +api: 'POST /api/webhooks' +--- + +Register a webhook destination for sandbox lifecycle events. See [Webhooks](/v2/sandboxes/webhooks). + + + HTTPS endpoint to deliver to. (SSRF protection is applied by the delivery provider at send time, not at registration.) + + + + Event-type allow-list — exact (`sandbox.stopped`) or prefix (`sandbox.*`). Default: all event types. Types outside the sandbox taxonomy are rejected with `400`. + + + + Scope to a single sandbox. Omit to receive events for all of the org's sandboxes. + + + + Signing secret. Omit and one is generated (`whsec_…`). The secret is returned in the create response and is re-fetchable any time via [`GET /api/webhooks/{id}/secret`](/v2/api-reference/webhooks/get). + + + + Optional display name for the destination. + + + + Whether the destination is active. Default `true`. `false` pauses delivery. + + + + Optional. A retried create with the same key **and same body** returns the **same** destination (`200`) instead of a duplicate. Reusing the key with a **different** body is a `409` conflict. + + +Without an `Idempotency-Key`, each call creates a **new** destination and returns **`201`** — there is no get-or-create by `name`. With one, a retried call returns the same destination with **`200`**. + +**Validation (all `400`):** `url` must be HTTPS; each `eventTypes` entry must be a known sandbox event type or a `prefix.*` wildcard. + + +```json 201 +{ + "id": "whk_3f9a2c", + "name": "prod", + "url": "https://app.example.com/oc-webhook", + "eventTypes": ["sandbox.stopped"], + "sandboxId": null, + "enabled": true, + "hasSecret": true, + "secret": "whsec_Hk9…", + "createdAt": "2026-06-24T12:00:00Z", + "updatedAt": "2026-06-24T12:00:00Z" +} +``` + +The `secret` is returned here and stays re-fetchable via [`GET /api/webhooks/{id}/secret`](/v2/api-reference/webhooks/get) — store it to verify deliveries. + diff --git a/docs/v2/api-reference/webhooks/delete.mdx b/docs/v2/api-reference/webhooks/delete.mdx new file mode 100644 index 000000000..650b26d08 --- /dev/null +++ b/docs/v2/api-reference/webhooks/delete.mdx @@ -0,0 +1,15 @@ +--- +title: 'Delete Webhook' +api: 'DELETE /api/webhooks/{id}' +--- + +Delete a destination. It stops receiving events, drops out of [list](/v2/api-reference/webhooks/list), and its delivery-provider endpoint is removed. Delivery history for a deleted destination is no longer queryable. See [Webhooks](/v2/sandboxes/webhooks). + + + Destination ID (`whk_…`). + + + +```json 204 +``` + diff --git a/docs/v2/api-reference/webhooks/deliveries-list.mdx b/docs/v2/api-reference/webhooks/deliveries-list.mdx new file mode 100644 index 000000000..bef27b242 --- /dev/null +++ b/docs/v2/api-reference/webhooks/deliveries-list.mdx @@ -0,0 +1,45 @@ +--- +title: 'List Deliveries' +api: 'GET /api/webhooks/{id}/deliveries' +--- + +List a destination's most recent delivery attempts (up to 50). Each entry is a delivery-provider +attempt record. The attempt index lags actual delivery by a few seconds, and resolving a single +message ([get](/v2/api-reference/webhooks/delivery-get)) / [redeliver](/v2/api-reference/webhooks/redeliver) +can lag longer (~30s+) — retry with backoff. See [Webhooks](/v2/sandboxes/webhooks#deliveries). + + + Destination ID (`whk_…`). + + + + The message id — the key for [get](/v2/api-reference/webhooks/delivery-get) / [redeliver](/v2/api-reference/webhooks/redeliver); equals the `svix-id` header on the delivery. + + + + The id of this specific attempt. + + + + One of `success`, `pending`, or `failed`. + + + + The consumer's HTTP response code, when the attempt reached it. + + + +```json 200 +{ + "data": [ + { + "id": "msg_2aB…", + "attemptId": "atmpt_9fE…", + "status": "success", + "responseStatusCode": 200, + "timestamp": "2026-06-24T12:00:01Z" + } + ] +} +``` + diff --git a/docs/v2/api-reference/webhooks/delivery-get.mdx b/docs/v2/api-reference/webhooks/delivery-get.mdx new file mode 100644 index 000000000..7c66f434c --- /dev/null +++ b/docs/v2/api-reference/webhooks/delivery-get.mdx @@ -0,0 +1,39 @@ +--- +title: 'Get Delivery' +api: 'GET /api/webhooks/{id}/deliveries/{deliveryId}' +--- + +Fetch one delivered message by id. See [Webhooks](/v2/sandboxes/webhooks#deliveries). + + +The delivery provider's attempt index and message store are eventually consistent: a message id can +appear in [list deliveries](/v2/api-reference/webhooks/deliveries-list) **before** this endpoint (and +[redeliver](/v2/api-reference/webhooks/redeliver)) can resolve it — observed up to ~30s+ after the +attempt. So a `404` shortly after a delivery is transient — **retry with backoff** rather than +treating it as final. + + + + Destination ID (`whk_…`). + + + + Message id (`msg_…`, the `id` from [list deliveries](/v2/api-reference/webhooks/deliveries-list); equals the `svix-id` header). + + + +```json 200 +{ + "id": "msg_2aB…", + "eventType": "sandbox.stopped", + "eventId": "sb-3f9a…:sandbox.stopped", + "payload": { + "type": "sandbox.stopped", + "sandboxId": "sb-3f9a…", + "eventId": "sb-3f9a…:sandbox.stopped", + "event": { "id": "sb-3f9a…:sandbox.stopped", "ts": "2026-06-24T12:00:00Z", "orgId": "org_…", "sandboxId": "sb-3f9a…", "type": "sandbox.stopped", "data": { "reason": "user_requested" } } + }, + "timestamp": "2026-06-24T12:00:00Z" +} +``` + diff --git a/docs/v2/api-reference/webhooks/get.mdx b/docs/v2/api-reference/webhooks/get.mdx new file mode 100644 index 000000000..7724086aa --- /dev/null +++ b/docs/v2/api-reference/webhooks/get.mdx @@ -0,0 +1,26 @@ +--- +title: 'Get Webhook' +api: 'GET /api/webhooks/{id}' +--- + +Fetch one webhook destination. The response carries `hasSecret` (not the secret itself); fetch the secret value any time via `GET /api/webhooks/{id}/secret` → `{ "secret": "whsec_…" }`. See [Webhooks](/v2/sandboxes/webhooks). + + + Destination ID (`whk_…`). + + + +```json 200 +{ + "id": "whk_3f9a2c", + "name": "prod", + "url": "https://app.example.com/oc-webhook", + "eventTypes": ["sandbox.stopped"], + "sandboxId": null, + "enabled": true, + "hasSecret": true, + "createdAt": "2026-06-24T12:00:00Z", + "updatedAt": "2026-06-24T12:00:00Z" +} +``` + diff --git a/docs/v2/api-reference/webhooks/list.mdx b/docs/v2/api-reference/webhooks/list.mdx new file mode 100644 index 000000000..0b6ce4cfa --- /dev/null +++ b/docs/v2/api-reference/webhooks/list.mdx @@ -0,0 +1,26 @@ +--- +title: 'List Webhooks' +api: 'GET /api/webhooks' +--- + +List the org's webhook destinations. Deleted destinations are excluded. See [Webhooks](/v2/sandboxes/webhooks). + + +```json 200 +{ + "data": [ + { + "id": "whk_3f9a2c", + "name": "prod", + "url": "https://app.example.com/oc-webhook", + "eventTypes": ["sandbox.stopped"], + "sandboxId": null, + "enabled": true, + "hasSecret": true, + "createdAt": "2026-06-24T12:00:00Z", + "updatedAt": "2026-06-24T12:00:00Z" + } + ] +} +``` + diff --git a/docs/v2/api-reference/webhooks/redeliver.mdx b/docs/v2/api-reference/webhooks/redeliver.mdx new file mode 100644 index 000000000..7ff306069 --- /dev/null +++ b/docs/v2/api-reference/webhooks/redeliver.mdx @@ -0,0 +1,24 @@ +--- +title: 'Redeliver' +api: 'POST /api/webhooks/{id}/deliveries/{deliveryId}/redeliver' +--- + +Re-send a message to the endpoint. The redelivery carries the **same** `svix-id`, so a receiver that dedupes treats it as the same message. Use it when the original never landed. See [Webhooks](/v2/sandboxes/webhooks#deliveries). + + + Destination ID (`whk_…`). + + + + Message id (`msg_…`, from [list deliveries](/v2/api-reference/webhooks/deliveries-list)). + + + +```json 200 +{ + "ok": true +} +``` + + +The new attempt appears under [deliveries](/v2/api-reference/webhooks/deliveries-list). diff --git a/docs/v2/api-reference/webhooks/test.mdx b/docs/v2/api-reference/webhooks/test.mdx new file mode 100644 index 000000000..d731044d9 --- /dev/null +++ b/docs/v2/api-reference/webhooks/test.mdx @@ -0,0 +1,34 @@ +--- +title: 'Test Webhook' +api: 'POST /api/webhooks/{id}/test' +--- + +Enqueue a sample event to exercise your endpoint. It sends a real, signed message of a concrete type +the destination is subscribed to (the first of its `eventTypes`, defaulting to `sandbox.created`), +carrying the **normal delivery envelope** with `event.data.test = true` — so you exercise your real +verifier and parser. Delivery is **asynchronous** — this returns once the message is accepted; check +[deliveries](/v2/api-reference/webhooks/deliveries-list) for the outcome. + + +The sample is published through your org's webhook app, so it is **not** isolated to this one +destination: any other destination subscribed to the same event type (and matching scope) also +receives it (likewise marked `event.data.test = true`). + + +See [Webhooks](/v2/sandboxes/webhooks). + + + Destination ID (`whk_…`). + + + +```json 200 +{ + "ok": true, + "eventType": "sandbox.created", + "messageId": "msg_2aB…" +} +``` + + +`messageId` is the delivery's message id — find its attempt under [deliveries](/v2/api-reference/webhooks/deliveries-list). diff --git a/docs/v2/api-reference/webhooks/update.mdx b/docs/v2/api-reference/webhooks/update.mdx new file mode 100644 index 000000000..d8f8a319a --- /dev/null +++ b/docs/v2/api-reference/webhooks/update.mdx @@ -0,0 +1,48 @@ +--- +title: 'Update Webhook' +api: 'PATCH /api/webhooks/{id}' +--- + +Update a destination — pause/resume, retune filters, change the URL, or rotate the secret. See [Webhooks](/v2/sandboxes/webhooks). + + + Destination ID (`whk_…`). + + + + New HTTPS endpoint. + + + + Replace the event-type allow-list (unknown types are rejected with `400`). Pass `null` to clear it (deliver all types). + + + + Pause (`false`) or resume (`true`) delivery. Pausing does **not** drop events — events that occur while paused are queued and delivered when you re-enable. + + + + Rotate to a **new** generated signing secret, returned as `secret` in the response. The previous secret stays valid for a short rollover window so in-flight deliveries still verify. (To set a specific secret, create a new destination.) + + + + Rename the destination. + + +`sandboxId` (scope) is **immutable** — set it at create; it can't be changed here. The current signing secret is re-fetchable any time via [`GET /api/webhooks/{id}/secret`](/v2/api-reference/webhooks/get). + + +```json 200 +{ + "id": "whk_3f9a2c", + "name": "prod", + "url": "https://app.example.com/oc-webhook", + "eventTypes": ["sandbox.stopped", "sandbox.ready"], + "sandboxId": null, + "enabled": true, + "hasSecret": true, + "createdAt": "2026-06-24T12:00:00Z", + "updatedAt": "2026-06-24T12:05:00Z" +} +``` + diff --git a/docs/v2/browser-sessions/overview.mdx b/docs/v2/browser-sessions/overview.mdx new file mode 100644 index 000000000..3111e9213 --- /dev/null +++ b/docs/v2/browser-sessions/overview.mdx @@ -0,0 +1,295 @@ +--- +title: "Browser Sessions" +description: "Preview: create cloud browser sessions for Playwright, Magnitude, and profile-backed web automation" +--- + + +Browser Sessions are currently in invite-only preview. Access is enabled for +approved organizations, and API routes, SDK method names, response fields, +limits, and pricing may change before general availability. + + +Browser Sessions create managed Chromium sessions for web agents and browser +automation. OpenComputer handles API-key auth and org ownership, then returns +browser connection URLs for tools such as Playwright and Magnitude. + +If your organization has not been invited to the preview, Browser Session API +calls may fail even when your OpenComputer API key is valid. + +Use Browser Sessions when you want a cloud browser without installing Chromium +inside an OpenComputer sandbox. Use [sandboxes](/v2/sandboxes/overview) when you +need a full Linux VM with your own browser runtime and filesystem. + +## Create a Browser + + + +```typescript TypeScript +import { Browser } from "@opencomputer/sdk"; + +const browser = await Browser.create({ + headless: false, + stealth: true, + startUrl: "https://example.com", + timeoutSeconds: 120, +}); + +console.log(browser.id); +console.log(browser.cdpWsUrl); +console.log(browser.liveViewUrl); + +await browser.delete(); +``` + +```python Python +from opencomputer import Browser + +browser = await Browser.create( + headless=False, + stealth=True, + start_url="https://example.com", + timeout_seconds=120, +) + +print(browser.id) +print(browser.cdp_ws_url) +print(browser.live_view_url) + +await browser.delete() +``` + + + +Headful browsers return a live-view URL. Headless browsers are lighter, but do +not provide a live view. Telemetry is enabled by default for all browser +sessions. Set `telemetry: false` / `telemetry=False` to disable it, or pass a +Kernel telemetry configuration object directly. + +Replay recording is disabled by default. Set `recording: true` / +`recording=True` to enable replay recording for a headful browser session. + +## Profile Auth Checks + +Saved profiles can start an asynchronous auth check for a site. The SDK returns +a run immediately; `wait()` polls with short requests until the run completes. + +```typescript TypeScript +const profile = await BrowserProfile.connect("linkedin-profile"); + +const run = await profile.checkAuth({ + homepage: "https://www.linkedin.com/feed/", + user: "motatoes", + mode: "vision", + compareFresh: true, +}); + +const result = await run.wait(); +console.log(result.status, result.result); +``` + +Auth checks are read-only. They create temporary browser sessions with profile +saving disabled and compare the saved profile against a fresh browser when +`compareFresh` is enabled. + +## Playwright + +The browser response includes a CDP WebSocket URL. Pass it directly to +Playwright's `connectOverCDP`. + + + +```typescript TypeScript +import { Browser } from "@opencomputer/sdk"; +import { chromium } from "playwright"; + +const ocBrowser = await Browser.create({ + headless: false, + startUrl: "https://example.com", +}); + +try { + const browser = await chromium.connectOverCDP(ocBrowser.cdpWsUrl); + const context = browser.contexts()[0] || await browser.newContext(); + const page = context.pages()[0] || await context.newPage(); + + await page.goto("https://example.com"); + console.log(await page.title()); + + await browser.close(); +} finally { + await ocBrowser.delete(); +} +``` + +```python Python +from opencomputer import Browser +from playwright.async_api import async_playwright + +oc_browser = await Browser.create( + headless=False, + start_url="https://example.com", +) + +try: + async with async_playwright() as p: + browser = await p.chromium.connect_over_cdp(oc_browser.cdp_ws_url) + context = browser.contexts[0] if browser.contexts else await browser.new_context() + page = context.pages[0] if context.pages else await context.new_page() + + await page.goto("https://example.com") + print(await page.title()) + + await browser.close() +finally: + await oc_browser.delete() +``` + + + +## Magnitude + +Magnitude can use a Playwright-connected browser. Create the OpenComputer +browser first, connect over CDP, then hand the page to your Magnitude agent. + +```typescript TypeScript +import { Browser } from "@opencomputer/sdk"; +import { chromium } from "playwright"; +import { startBrowserAgent } from "magnitude-core"; + +const ocBrowser = await Browser.create({ + headless: false, + stealth: true, + startUrl: "https://example.com", +}); + +try { + const browser = await chromium.connectOverCDP(ocBrowser.cdpWsUrl); + const context = browser.contexts()[0] || await browser.newContext(); + const page = context.pages()[0] || await context.newPage(); + + const agent = await startBrowserAgent({ page }); + await agent.act("Find the page title and summarize what this site is for."); + + await browser.close(); +} finally { + await ocBrowser.delete(); +} +``` + + +Magnitude package names and constructors can vary by version. The +OpenComputer-specific step is stable: create a `Browser`, connect to +`cdpWsUrl` with Playwright, then pass the resulting page or browser object to +Magnitude. + + +## Save and Load Profiles + +Profiles persist browser state such as cookies and local storage across browser +sessions. Profiles are scoped to your OpenComputer org; another org cannot +list, load, or delete your profiles. + +Create a profile once: + + + +```typescript TypeScript +import { BrowserProfile } from "@opencomputer/sdk"; + +const profile = await BrowserProfile.create({ + name: "github-login", +}); +``` + +```python Python +from opencomputer import BrowserProfile + +profile = await BrowserProfile.create(name="github-login") +``` + + + +Use the profile when creating a browser: + + + +```typescript TypeScript +import { Browser } from "@opencomputer/sdk"; + +const browser = await Browser.create({ + profile: { + id: profile.id, + saveChanges: true, + }, + headless: false, +}); + +// Log in or update session state, then delete the browser. +// With saveChanges enabled, changes are saved back to the profile. +await browser.delete(); +``` + +```python Python +from opencomputer import Browser + +browser = await Browser.create( + profile={ + "id": profile.id, + "save_changes": True, + }, + headless=False, +) + +# Log in or update session state, then delete the browser. +# With save_changes enabled, changes are saved back to the profile. +await browser.delete() +``` + + + +Load the same profile later by ID or name: + + + +```typescript TypeScript +import { Browser, BrowserProfile } from "@opencomputer/sdk"; + +const profile = await BrowserProfile.connect("github-login"); + +const browser = await Browser.create({ + profile: { + id: profile.id, + saveChanges: true, + }, + headless: false, +}); +``` + +```python Python +from opencomputer import Browser, BrowserProfile + +profile = await BrowserProfile.connect("github-login") + +browser = await Browser.create( + profile={ + "id": profile.id, + "save_changes": True, + }, + headless=False, +) +``` + + + +## Authentication + +The SDK uses your OpenComputer API key: + +```bash +export OPENCOMPUTER_API_KEY="osb_..." +``` + + +For local development against a non-production Browser Sessions endpoint, set +`OPENCOMPUTER_BROWSER_API_URL`. + diff --git a/docs/v2/cli/checkpoint.mdx b/docs/v2/cli/checkpoint.mdx new file mode 100644 index 000000000..7e8ec5aea --- /dev/null +++ b/docs/v2/cli/checkpoint.mdx @@ -0,0 +1,81 @@ +--- +title: "Checkpoints" +description: "Snapshot, fork, and restore from the CLI" +--- + +## Creating a Checkpoint + +Capture the current state of a running sandbox: + +```bash +oc checkpoint create sb-abc123 --name before-migration +# or using the shortcut: +oc cp create sb-abc123 --name before-migration +``` + +The checkpoint saves the filesystem and installed state. Status starts as `processing` and transitions to `ready`. + +```bash +oc cp list sb-abc123 +# ID NAME STATUS SIZE CREATED +# cp-7f3a1b2c before-migration ready 128 MB 2025-01-15T10:30:00Z +``` + +## Forking from a Checkpoint + +Create new sandboxes from a saved checkpoint. Each fork is independent: + +```bash +# Spawn two independent sandboxes from the same checkpoint +ID1=$(oc cp spawn cp-7f3a1b2c --json | jq -r '.sandboxID') +ID2=$(oc cp spawn cp-7f3a1b2c --json | jq -r '.sandboxID') + +# Run different experiments +oc exec $ID1 --wait -- ./experiment-a.sh +oc exec $ID2 --wait -- ./experiment-b.sh +``` + +Forked sandboxes start with a fresh boot from the saved disk state — don't assume running processes carry over. + +## Restoring + +Revert a sandbox in-place to a checkpoint. All changes since the checkpoint are lost: + +```bash +oc cp restore sb-abc123 cp-7f3a1b2c +``` + +## Listing and Deleting + +```bash +oc cp list sb-abc123 +oc cp delete sb-abc123 cp-7f3a1b2c +``` + +Each sandbox can have up to 10 full checkpoints and up to 100 disk-only checkpoints. + +To create a new checkpoint at the limit, opt into automatic deletion of the oldest eligible checkpoint of the same type: + +```bash +oc cp create sb-abc123 \ + --name autosave \ + --kind disk_only \ + --retention-policy delete_oldest \ + --retention-max-count 100 +``` + +Retention skips public checkpoints, patched checkpoints, and checkpoints that are still referenced by forked sandboxes. + +## Checkpoint vs Hibernate + +| | Checkpoint | Hibernate | +| --- | --- | --- | +| Original sandbox | Keeps running | Stopped | +| Can fork | Yes — unlimited new sandboxes | No | +| Use case | Branching, parallel testing | Pause and resume, cost savings | + +Use checkpoints when you need to explore multiple paths from the same state. Use hibernation when you just want to pause and resume later. + + + SDK usage: [Checkpoints](/v2/sandboxes/checkpoints). Full flags: [CLI Reference](/reference/cli#oc-checkpoint). + diff --git a/docs/v2/cli/exec.mdx b/docs/v2/cli/exec.mdx new file mode 100644 index 000000000..f11df05a1 --- /dev/null +++ b/docs/v2/cli/exec.mdx @@ -0,0 +1,82 @@ +--- +title: "Running Commands" +description: "Execute shell commands from the CLI" +--- + +## Running a Command + +Use `--wait` to run a command synchronously — output streams to your terminal and the CLI exits with the process exit code: + +```bash +oc exec sb-abc123 --wait -- echo "Hello, World!" +oc exec sb-abc123 --wait -- npm run build +``` + +Without `--wait`, `oc exec` creates an **exec session** and prints the session ID with attach instructions. This is useful for long-running commands: + +```bash +oc exec sb-abc123 -- node server.js +# → Session es-xyz created. Attach with: oc exec attach sb-abc es-xyz +``` + +## Working Directory & Environment + +```bash +oc exec sb-abc123 --wait --cwd /app --env NODE_ENV=production -- npm run build +oc exec sb-abc123 --wait --env API_KEY=xxx --env DEBUG=true -- ./run.sh +``` + +## Timeouts + +Set a timeout with `--timeout` (in seconds). Default is 0 (no timeout). When exceeded, the command is killed: + +```bash +oc exec sb-abc123 --wait --timeout 30 -- npm test +``` + +## Creating an Exec Session + +Plain `oc exec` (without `--wait`) creates a session for long-running commands. The command keeps running even after you disconnect: + +```bash +oc exec sb-abc123 -- python train.py +# Session es-abc created +``` + +## Capturing Output as JSON + +Combine `--json` with `--wait` for scripting: + +```bash +RESULT=$(oc exec sb-abc123 --json --wait -- node -e 'console.log("hello")') +echo $RESULT | jq '.stdout' # "hello\n" +echo $RESULT | jq '.exitCode' # 0 +``` + +## Managing Exec Sessions + +```bash +# List active sessions +oc exec list sb-abc123 + +# Kill a session +oc exec kill sb-abc123 es-xyz +oc exec kill sb-abc123 es-xyz --signal 15 # SIGTERM +``` + + + `oc exec attach` exists as a command but is not yet implemented. It prints guidance to use the SDK or a WebSocket client. Use `oc exec list` and `oc exec kill` to manage sessions from the CLI. + + +## Shell vs Exec + +| Use Case | Command | +| --- | --- | +| Single command, capture output | `oc exec --wait` | +| Scripting and automation | `oc exec --wait --json` | +| Long-running background process | `oc exec` (no `--wait`) | +| Interactive development, debugging | [`oc shell`](/v2/cli/shell) | + + + SDK usage: [Running Commands](/v2/sandboxes/running-commands). Full flags: [CLI Reference](/reference/cli#oc-exec). + diff --git a/docs/v2/cli/overview.mdx b/docs/v2/cli/overview.mdx new file mode 100644 index 000000000..22d3d8ca7 --- /dev/null +++ b/docs/v2/cli/overview.mdx @@ -0,0 +1,164 @@ +--- +title: "CLI" +description: "Manage sandboxes from your terminal" +--- + +The `oc` CLI lets you manage OpenComputer sandboxes directly from your terminal — create sandboxes, run commands, open interactive shells, manage checkpoints, and more. + +## Installation + + + +```bash macOS (Apple Silicon) +curl -fsSL https://github.com/diggerhq/opencomputer/releases/latest/download/oc-darwin-arm64 -o /usr/local/bin/oc +chmod +x /usr/local/bin/oc +``` + +```bash macOS (Intel) +curl -fsSL https://github.com/diggerhq/opencomputer/releases/latest/download/oc-darwin-amd64 -o /usr/local/bin/oc +chmod +x /usr/local/bin/oc +``` + +```bash Linux (x86_64) +curl -fsSL https://github.com/diggerhq/opencomputer/releases/latest/download/oc-linux-amd64 -o /usr/local/bin/oc +chmod +x /usr/local/bin/oc +``` + +```bash Linux (ARM64) +curl -fsSL https://github.com/diggerhq/opencomputer/releases/latest/download/oc-linux-arm64 -o /usr/local/bin/oc +chmod +x /usr/local/bin/oc +``` + + + +## Sign in + +```bash +oc login +oc whoami +``` + +`oc login` prints a short confirmation code and opens the hosted sign-in page. +Use `oc login --no-browser` over SSH or when a coding agent is running the +command for you. The CLI stores the resulting credential in +`~/.oc/config.json` with private file permissions; it never prints the key. + +```bash +oc logout # revoke the CLI-created key, then clear it locally +oc logout --local # clear local login state without claiming remote revocation +``` + +`oc login` authenticates the local CLI. CI, servers, and SDK applications +should continue to receive an explicit org key through their secret manager and +`OPENCOMPUTER_API_KEY`. + +### Credential resolution + +| Priority | Source | Example | +| --- | --- | --- | +| 1 (highest) | CLI flags | `--api-key=xxx` | +| 2 | Environment variables | `OPENCOMPUTER_API_KEY` | +| 3 | Config file | `oc login` or `oc config set api-key …` | +| 4 (lowest) | Defaults | `https://app.opencomputer.dev` | + +An active `--api-key` or `OPENCOMPUTER_API_KEY` override must be removed before +`oc login` or remote `oc logout`; `oc logout --local` can still clear the saved +CLI login. Manually setting an API key remains supported: + +```bash +oc config set api-key "$OPENCOMPUTER_API_KEY" +oc config show +``` + +## Global Flags + +| Flag | Environment Variable | Description | +| --- | --- | --- | +| `--api-key` | `OPENCOMPUTER_API_KEY` | API key for authentication | +| `--api-url` | `OPENCOMPUTER_API_URL` | Control plane URL | +| `--json` | — | Output as JSON instead of tables | + +## Key Workflows + +### Quick Start + +```bash +# Create a sandbox +oc create + +# Run a command and wait for the result +oc exec sb-abc123 --wait -- echo "Hello from the cloud" + +# Open an interactive shell +oc shell sb-abc123 + +# Clean up +oc sandbox kill sb-abc123 +``` + +### JSON Output & Scripting + +All commands support `--json` for machine-readable output: + +```bash +# Get a sandbox ID programmatically +ID=$(oc create --json | jq -r '.sandboxID') + +# List all running sandbox IDs +oc ls --json | jq -r '.[].sandboxID' + +# Run a command and capture the result +RESULT=$(oc exec $ID --json --wait -- npm test) +echo $RESULT | jq '.exitCode' +``` + +### Top-level Shortcuts + +| Shortcut | Expands to | +| --- | --- | +| `oc create` | `oc sandbox create` | +| `oc ls` | `oc sandbox list` | +| `oc cp` | `oc checkpoint` | + +### Create and Shell In + +```bash +oc shell $(oc create --json | jq -r '.sandboxID') +``` + +### Hibernate for Cost Savings + +```bash +oc sandbox hibernate sb-abc123 +# ... hours later ... +oc sandbox wake sb-abc123 +oc shell sb-abc123 +``` + +### Checkpoint and Fork + +```bash +oc cp create sb-abc --name ready-state +ID1=$(oc cp spawn cp-xyz --json | jq -r '.sandboxID') +ID2=$(oc cp spawn cp-xyz --json | jq -r '.sandboxID') +oc exec $ID1 --wait -- ./test-a.sh +oc exec $ID2 --wait -- ./test-b.sh +``` + +## Command Reference + +| Command | Description | +| --- | --- | +| [`oc sandbox`](/v2/cli/sandbox) | Create, list, kill, hibernate, wake | +| [`oc exec`](/v2/cli/exec) | Run commands, manage exec sessions | +| [`oc shell`](/v2/cli/shell) | Interactive PTY terminal | +| [`oc checkpoint`](/v2/cli/checkpoint) | Snapshot, fork, restore | +| [`oc patch`](/v2/cli/patch) | Checkpoint-attached scripts | +| [`oc preview`](/v2/cli/preview) | Expose ports to the internet | +| [`oc agent`](/v2/reference/cli/agent) | Invoke durable agents and manage Hook URLs | +| [`oc login`](/v2/reference/cli/auth) | Sign in, inspect identity, and log out | +| [`oc config`](/reference/cli#oc-config) | Configure API key and API URL | + + + Full flag reference for every command: [CLI Reference](/reference/cli). + diff --git a/docs/v2/cli/patch.mdx b/docs/v2/cli/patch.mdx new file mode 100644 index 000000000..79e28d1aa --- /dev/null +++ b/docs/v2/cli/patch.mdx @@ -0,0 +1,53 @@ +--- +title: "Patches" +description: "Attach scripts to checkpoints from the CLI" +--- + +## What Patches Do + +A patch is a shell script attached to a checkpoint. Patches run in order every time a sandbox is forked from that checkpoint — layer setup without re-creating the checkpoint. + +## Creating a Patch + +From a file or stdin: + +```bash +# From a file +oc patch create cp-7f3a1b2c --script ./setup.sh --description "Install deps" + +# One-liner from stdin +echo "npm install -g typescript" | oc patch create cp-7f3a1b2c --script=- +``` + +## Layering Setup + +Add multiple patches — they execute in creation order on every fork: + +```bash +oc patch create cp-7f3a1b2c --script ./install-node.sh --description "Node.js 20" +oc patch create cp-7f3a1b2c --script ./app-config.sh --description "App config" + +# Each spawned sandbox gets both patches applied in order +oc cp spawn cp-7f3a1b2c +``` + +## Inspecting Patches + +Review what will run before forking: + +```bash +oc patch list cp-7f3a1b2c +# ID SEQ DESCRIPTION STRATEGY CREATED +# pa-abc123 1 Node.js 20 on_wake 2025-01-15T10:30:00Z +# pa-def456 2 App config on_wake 2025-01-15T10:31:00Z +``` + +## Removing a Patch + +```bash +oc patch delete cp-7f3a1b2c pa-abc123 +``` + + + SDK usage: [Patches](/v2/sandboxes/patches). Full flags: [CLI Reference](/reference/cli#oc-patch). + diff --git a/docs/v2/cli/preview.mdx b/docs/v2/cli/preview.mdx new file mode 100644 index 000000000..60d734b2d --- /dev/null +++ b/docs/v2/cli/preview.mdx @@ -0,0 +1,93 @@ +--- +title: "Preview URLs" +description: "Expose sandbox ports from the CLI" +--- + +## Exposing a Port + +Create a public HTTPS URL that proxies traffic to a port inside your sandbox: + +```bash +oc preview create sb-abc123 --port 3000 +# → hostname: sb-abc123-p3000.workers.opencomputer.dev +``` + +## Sharing a Dev Server + +Start a server inside the sandbox, expose it, and share the URL: + +```bash +# Start a dev server in the background +oc exec sb-abc123 -- bash -c 'cd /app && npm run dev &' + +# Expose port 3000 +oc preview create sb-abc123 --port 3000 + +# Get the URL +oc preview list sb-abc123 +``` + +## Multiple Ports + +Expose multiple services from the same sandbox: + +```bash +oc preview create sb-abc123 --port 3000 # Frontend +oc preview create sb-abc123 --port 8080 # API server + +oc preview list sb-abc123 +# PORT HOSTNAME SSL CREATED +# 3000 sb-abc123-p3000.workers.opencomputer.dev active ... +# 8080 sb-abc123-p8080.workers.opencomputer.dev active ... +``` + +## Custom Domains + +Pass `--domain` to use your own domain. DNS must point to OpenComputer's ingress, and the domain must be verified in the dashboard. SSL is provisioned automatically. + +```bash +oc preview create sb-abc123 --port 3000 --domain preview.myapp.com +``` + +## Cleanup + +```bash +oc preview delete sb-abc123 3000 +``` + +Preview URLs persist across hibernation/wake cycles — no need to re-create them. + +## Bearer-Token Authentication + +By default, anyone who knows the preview hostname can hit your sandbox's port. To require an `Authorization: Bearer ` header on every request, opt in at create time: + +```bash +oc sandbox create --preview-auth +# Created sandbox sb-abc123 (status: running) +# Preview auth token (shown once): qx2sSi5IYXWBvnnRqwK9Ky_cIAI-x0Vx1bPCt0XMxsI +``` + +Then call your preview URL with the token: + +```bash +curl -H "Authorization: Bearer qx2sSi5IYX..." https://sb-abc123-p3000.workers.opencomputer.dev/ +``` + +Bring your own token if your gateway already has a shared secret: + +```bash +oc sandbox create --preview-auth-token "$GATEWAY_TOKEN" +``` + +Rotate the token (old one stops working immediately): + +```bash +oc preview rotate-auth sb-abc123 +# New preview auth token (shown once): +``` + +Token is shown exactly once and only its SHA-256 hash is stored. See [Authentication](/v2/sandboxes/preview-urls#authentication) for the SDK equivalents. + + + SDK usage: [Preview URLs](/v2/sandboxes/preview-urls). Full flags: [CLI Reference](/reference/cli#oc-preview). + diff --git a/docs/v2/cli/sandbox.mdx b/docs/v2/cli/sandbox.mdx new file mode 100644 index 000000000..26a96d1a4 --- /dev/null +++ b/docs/v2/cli/sandbox.mdx @@ -0,0 +1,85 @@ +--- +title: "Sandbox Management" +description: "Create, manage, and control sandbox lifecycles from the CLI" +--- + +## Creating a Sandbox + +`oc create` (shortcut for `oc sandbox create`) provisions a new sandbox VM: + +```bash +oc create +oc create --timeout 600 --cpu 2 --memory 2048 --env NODE_ENV=production +``` + +The sandbox ID is printed on success. Use `--json` to capture it programmatically: + +```bash +ID=$(oc create --json | jq -r '.sandboxID') +``` + +## Listing & Inspecting + +```bash +# List all sandboxes +oc ls + +# Detailed info for a specific sandbox +oc sandbox get sb-abc123 +``` + +`oc ls` shows a table with ID, template, status, CPU, memory, and age. Add `--json` for machine-readable output. + +## Hibernation & Wake + +Save state and stop paying for compute. Wake resumes the same sandbox — the platform attempts fast snapshot restore with a cold-boot fallback if needed. + +```bash +oc sandbox hibernate sb-abc123 + +# ... hours later ... +oc sandbox wake sb-abc123 +oc shell sb-abc123 +``` + +The sandbox keeps the same ID across hibernate/wake cycles. Preview URLs remain active. + +## Adjusting Timeout + +The idle timeout resets on every operation (exec, file access, agent activity). Default: 300s. + +```bash +oc sandbox set-timeout sb-abc123 3600 # 1 hour +``` + +## Killing a Sandbox + +```bash +oc sandbox kill sb-abc123 +``` + +All data is lost unless you've created a [checkpoint](/v2/cli/checkpoint) first. + +## Common Patterns + +### Create and Shell In + +```bash +oc shell $(oc create --json | jq -r '.sandboxID') +``` + +### Filter Running Sandboxes + +```bash +oc ls --json | jq '.[] | select(.status == "running") | .sandboxID' +``` + +### Batch Cleanup + +```bash +oc ls --json | jq -r '.[].sandboxID' | xargs -I{} oc sandbox kill {} +``` + + + SDK usage: [Sandboxes](/v2/sandboxes/overview). Full flags: [CLI Reference](/reference/cli#oc-sandbox). + diff --git a/docs/v2/cli/secrets.mdx b/docs/v2/cli/secrets.mdx new file mode 100644 index 000000000..bc097e960 --- /dev/null +++ b/docs/v2/cli/secrets.mdx @@ -0,0 +1,52 @@ +--- +title: "Secrets" +description: "Manage secret stores and secrets from the CLI" +--- + +## Secret stores + +```bash +# Create a store (with optional egress restrictions) +oc secret-store create --name my-secrets --egress-allowlist api.anthropic.com + +# List stores +oc secret-store list + +# Update a store +oc secret-store update --egress-allowlist api.anthropic.com,*.openai.com + +# Delete a store (and all its secrets) +oc secret-store delete +``` + +## Secrets + +```bash +# Set a secret +oc secret set ANTHROPIC_API_KEY sk-ant-... + +# Set from stdin +echo "sk-ant-..." | oc secret set ANTHROPIC_API_KEY --from-stdin + +# Restrict a secret to specific hosts +oc secret set ANTHROPIC_API_KEY sk-ant-... --allowed-hosts api.anthropic.com + +# List secrets (names only — values are never returned) +oc secret list + +# Delete a secret +oc secret delete ANTHROPIC_API_KEY +``` + +## Using secrets in sandboxes + +```bash +# Create a sandbox with a secret store +oc create --secret-store my-secrets + +# Env vars are sealed — real values are never in the VM +oc exec -- echo '$ANTHROPIC_API_KEY' +# osb_sealed_7f3a9c... (not the real key) +``` + +For how secrets work under the hood, see [Secrets](/v2/sandboxes/secrets). diff --git a/docs/v2/cli/shell.mdx b/docs/v2/cli/shell.mdx new file mode 100644 index 000000000..cbf36d8ca --- /dev/null +++ b/docs/v2/cli/shell.mdx @@ -0,0 +1,63 @@ +--- +title: "Shell" +description: "Interactive terminal sessions from the CLI" +--- + +## Opening a Shell + +```bash +oc shell sb-abc123 +``` + +Opens a full interactive terminal over WebSocket. Press `Ctrl+D` or type `exit` to disconnect. + +## Choosing a Shell + +```bash +oc shell sb-abc123 --shell /bin/zsh +``` + +Default: `/bin/bash`. + +## What Works + +The PTY supports everything you'd expect from a real terminal: + +- **Editors:** vim, nano +- **Monitors:** top, htop +- **Tab completion** and command history +- **Colors** and ANSI escape codes +- **Terminal resizing** — automatic, resize events forwarded + +Full-screen applications like `tmux` and `less` work correctly. + +## Shell vs Exec + +| Use Case | Command | +| --- | --- | +| Run a single command, capture output | `oc exec --wait` | +| Scripting and automation | `oc exec --wait --json` | +| Interactive development | `oc shell` | +| Debugging | `oc shell` | + +## Tips + +### Create and Shell In + +```bash +oc shell $(oc create --json | jq -r '.sandboxID') +``` + +### Shell with Custom Environment + +Set up the environment before shelling in: + +```bash +ID=$(oc create --json | jq -r '.sandboxID') +oc exec $ID --wait -- bash -c 'echo "export PATH=/app/bin:\$PATH" >> ~/.bashrc' +oc shell $ID +``` + + + SDK usage: [Interactive Terminals](/v2/sandboxes/interactive-terminals). Full flags: [CLI Reference](/reference/cli#oc-shell). + diff --git a/docs/v2/guides/agent-skill.mdx b/docs/v2/guides/agent-skill.mdx new file mode 100644 index 000000000..195dda06c --- /dev/null +++ b/docs/v2/guides/agent-skill.mdx @@ -0,0 +1,45 @@ +--- +title: "Agent Skill" +description: "Use AI agents to manage cloud sandboxes with natural language" +--- + +The OpenComputer skill lets AI agents like Claude Code create and manage cloud sandboxes using the `oc` CLI. Instead of writing commands yourself, just describe what you want. + +## Install + +```bash +npx skills add diggerhq/opencomputer +``` + +This works with Claude Code, Codex, Cursor, and any agent that supports the [Agent Skills](https://agentskills.io) standard. + +## Prerequisites + +The `oc` CLI must be installed and configured: + +```bash +curl -fsSL https://raw.githubusercontent.com/diggerhq/opencomputer/main/scripts/install.sh | bash +oc login +``` + +If the command prints a browser URL or confirmation code, relay it to the user +and wait for them to approve it. The CLI resumes without asking anyone to paste +an API key into chat. + +## Usage + +The skill activates automatically when you mention sandboxes or the `oc` CLI. Just ask naturally: + +**Create a custom base image:** +> Create a base image for me that includes a placeholder React app I can use as a starter kit. Checkpoint it so I can spawn copies later. + +**Fork and experiment:** +> Spawn two sandboxes from my react-starter checkpoint. In the first one, add Tailwind CSS. In the second, add Material UI. + +**Run commands:** +> Install PostgreSQL in my sandbox and run the test suite + +**Manage state:** +> Checkpoint my sandbox before I try this migration, so I can roll back if it breaks + +The agent handles all the `oc` commands under the hood — creating sandboxes, running `exec`, taking checkpoints, applying patches, and cleaning up. diff --git a/docs/v2/guides/browser-automation.mdx b/docs/v2/guides/browser-automation.mdx new file mode 100644 index 000000000..7388cd7d9 --- /dev/null +++ b/docs/v2/guides/browser-automation.mdx @@ -0,0 +1,284 @@ +--- +title: "Browser Automation" +description: "Run headless and headed browsers inside OpenComputer sandboxes — for scraping, logged-in workflows, and AI-driven automation." +--- + +OpenComputer sandboxes are full Linux VMs, which means you can run a real Chromium browser inside them the same way you would on a laptop. This guide walks through the setup that makes it *actually work*: the right Chromium flavor, the system libraries you need, the OpenComputer-specific networking quirks, and how to persist browser state across runs. + +The examples use [**libretto**](https://libretto.sh), an AI-friendly CLI + library on top of Playwright. Everything here applies equally to raw Playwright, Puppeteer, [browser-use](https://browser-use.com/), or [Browserbase](https://www.browserbase.com/) — libretto is just a convenient default. + + + + Runnable reference implementation of everything in this guide. + + + CLI + library reference for the browser-automation tool used in the examples. + + + +--- + +## When to reach for a browser (vs. an API) + +- The target has no API (flight aggregators, airline sites, most SaaS admin UIs). +- The target needs a real logged-in session (cookies + localStorage + JS-challenge cookies). +- You want **AI-driven interaction** — describe a task in English, let the agent figure out the clicks. +- You need screenshots / visual artefacts for verification. + +If the site has a good API, use the API. A browser is slower, heavier, and flakier. But when you need it, OpenComputer gives you real VMs — not containers — so you can run full Chromium with no Docker-flavored limitations. + +--- + +## Step 1: Build a snapshot with Chromium pre-installed + +Browser setup is heavy (apt packages + Chromium binary is ~500MB). Bake it into a [template](/v2/sandboxes/templates) once, launch sandboxes from it in seconds. + +```typescript build-snapshot.ts +import { Image, Snapshots } from "@opencomputer/sdk/node"; + +// Runtime deps Chromium links against on Ubuntu 22.04. Matches Playwright's +// published dependency list. +const CHROMIUM_DEPS = [ + "libnss3", "libnspr4", "libatk1.0-0", "libatk-bridge2.0-0", "libcups2", + "libdrm2", "libxkbcommon0", "libxcomposite1", "libxdamage1", "libxfixes3", + "libxrandr2", "libxext6", "libgbm1", "libpango-1.0-0", "libcairo2", + "libasound2", "fonts-liberation", + // libnss3-tools gives us `certutil` — needed at runtime to trust OC's + // egress-proxy CA in Chromium (Chromium ignores SSL_CERT_FILE). + "libnss3-tools", +]; + +const image = Image.base() + .aptInstall(CHROMIUM_DEPS) + .workdir("/home/sandbox") + .runCommands( + "cd /home/sandbox && npm init -y >/dev/null", + // Install libretto + AI adapter locally so `npx libretto` resolves without + // a cold npm fetch at runtime. + "cd /home/sandbox && npm install --no-audit --no-fund libretto @ai-sdk/anthropic", + // Playwright downloads its own Chromium build — we want headless-shell + // for lightweight scraping, or full chromium for headed/VNC scenarios. + "cd /home/sandbox && npx --yes playwright install chromium-headless-shell", + ); + +const snapshots = new Snapshots(); +await snapshots.create({ name: "browser", image }); +``` + + +**Don't apt-install `chromium-browser` on Ubuntu 22.04.** That package is a snap shim that won't run in a minimal VM. Either install Google Chrome via its apt repo (`google-chrome-stable`), or rely on Playwright's bundled Chromium (recommended — it's purpose-built for automation). + + +--- + +## Step 2: Per-boot setup that can't be baked in + +A few things need to run at sandbox startup rather than during snapshot build, because they depend on per-sandbox state. + +```typescript launch.ts +import { Sandbox, SecretStore } from "@opencomputer/sdk/node"; + +const sandbox = await Sandbox.create({ + snapshot: "browser", + envs: { + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!, + // Keep Playwright/Chromium off the small tmpfs /dev/shm and /tmp — + // write profile dirs, cache, everything to the data disk. + TMPDIR: "/home/sandbox/tmp", + }, + secretStore: "browser-egress", // see "Networking" section below +}); + +// Populate /etc/hosts — the guest kernel sets up /etc/resolv.conf but not +// /etc/hosts, and libretto's Playwright CDP client hardcodes http://localhost +// which will ENOTFOUND without this. +await sandbox.commands.run( + "grep -q 'localhost' /etc/hosts || " + + "(printf '127.0.0.1 localhost\\n::1 localhost\\n' | sudo tee -a /etc/hosts)", +); + +// Trust OC's egress-proxy CA in Chromium's NSS store. Chromium ignores +// SSL_CERT_FILE / NODE_EXTRA_CA_CERTS — the env vars OC sets for libraries +// have no effect on the browser's TLS validation. +await sandbox.commands.run( + [ + "mkdir -p /home/sandbox/.pki/nssdb", + "certutil -d sql:/home/sandbox/.pki/nssdb -N --empty-password || true", + "certutil -d sql:/home/sandbox/.pki/nssdb -A -n opensandbox-proxy -t 'TC,C,T' " + + "-i /usr/local/share/ca-certificates/opensandbox-proxy.crt", + ].join(" && "), +); + +await sandbox.commands.run("mkdir -p /home/sandbox/tmp && chmod 700 /home/sandbox/tmp"); +``` + +--- + +## Networking: the `secretStore` requirement + + +This is the single most common gotcha. A sandbox without a `secretStore` attached **has no outbound egress at all** — every HTTPS request gets a `407` from the internal proxy. + + +OpenComputer routes all outbound traffic through a secrets-injection proxy. The proxy only accepts traffic from sandboxes that have at least one sealed secret registered, because session registration happens as part of sealing secrets. + +The workaround is to create a `SecretStore` with a wildcard egress allowlist and at least one (possibly dummy) entry: + +```typescript +import { SecretStore } from "@opencomputer/sdk/node"; + +const stores = await SecretStore.list(); +let store = stores.find((s) => s.name === "browser-egress"); +if (!store) { + store = await SecretStore.create({ + name: "browser-egress", + egressAllowlist: ["*"], // wildcard — scope this down for production + }); +} +await SecretStore.setSecret(store.id, "PLACEHOLDER", "not-used-just-triggers-session"); + +const sandbox = await Sandbox.create({ + snapshot: "browser", + secretStore: "browser-egress", +}); +``` + +With the store attached, outbound traffic flows normally and the `opensandbox-proxy.crt` we trusted earlier lets Chromium validate the MITM-rewritten certs. + +--- + +## Step 3: Run a headless browser + +With the snapshot and per-boot setup in place, driving the browser is a normal libretto session: + +```typescript +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +const run = promisify(execFile); + +// Open a page in a named session — libretto persists cookies/localStorage +// per session name in .libretto/sessions//. +await run("npx", ["libretto", "open", "https://example.com", "--session", "demo", "--headless"], { + cwd: "/home/sandbox", +}); + +// AI snapshot — Claude analyzes the page and returns a summary + selectors. +const { stdout } = await run("npx", [ + "libretto", "snapshot", "--session", "demo", + "--objective", "Describe the main content and interactive elements.", + "--context", "Freshly loaded page", +], { cwd: "/home/sandbox", maxBuffer: 8 * 1024 * 1024 }); + +console.log(stdout); +``` + +The `snapshot` command requires an `ANTHROPIC_API_KEY` (or an OpenAI / Gemini / Vertex key — configure via `.libretto/config.json`). It's the AI-driven feature that makes libretto different from raw Playwright. + +--- + +## Step 4: Run a headed browser (for interactive login) + +For workflows where the user needs to log in themselves, you can render the browser to a virtual display and expose it via VNC. This lets you embed the running browser in a web UI. + +Install the VNC stack at runtime (same no-rebuild pattern as per-boot setup): + +```typescript +await sandbox.commands.run( + "sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq xvfb x11vnc novnc websockify", +); + +// Start Xvfb → x11vnc → websockify as long-lived exec sessions so they +// persist across individual commands. +const xvfb = await sandbox.exec.start("Xvfb", { + args: [":99", "-screen", "0", "1280x800x24", "-ac"], +}); +await new Promise((r) => setTimeout(r, 1500)); + +const x11vnc = await sandbox.exec.start("x11vnc", { + args: ["-display", ":99", "-forever", "-shared", "-nopw", "-rfbport", "5900", "-quiet"], +}); + +const websockify = await sandbox.exec.start("websockify", { + args: ["--web=/usr/share/novnc/", "6080", "localhost:5900"], +}); +``` + +Set `DISPLAY=:99` in the sandbox envs when you create it, then open with `--headed`: + +```typescript +await run("npx", ["libretto", "open", "https://app.example.com", "--session", "login", "--headed"], { + cwd: "/home/sandbox", + env: { ...process.env, DISPLAY: ":99" }, +}); +``` + +The VNC WebSocket is available on port 6080 — get its preview URL with `sandbox.getPreviewDomain(6080)` and embed `