diff --git a/deploy/microvm/SIZE-TIERS.md b/deploy/microvm/SIZE-TIERS.md new file mode 100644 index 000000000..a153a4832 --- /dev/null +++ b/deploy/microvm/SIZE-TIERS.md @@ -0,0 +1,123 @@ +# 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. + +### Tiers we offer, and the sub-2048 warning + +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. + +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 + +**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/docs.json b/docs/docs.json index 740bbb43a..466d04e85 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,12 @@ "groups": [ { "group": "Getting Started", - "pages": ["introduction", "quickstart", "how-it-works"] + "pages": [ + "introduction", + "quickstart", + "how-it-works", + "migrating-from-v1" + ] }, { "group": "Sandboxes", @@ -95,6 +103,8 @@ "sandboxes/mounts", "sandboxes/signed-urls", "sandboxes/interactive-terminals", + "sandboxes/lifetime", + "sandboxes/sizes", "sandboxes/timeout", "sandboxes/checkpoints", "sandboxes/templates", @@ -108,14 +118,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 +173,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,16 +347,25 @@ }, { "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" + ] } ] } ] }, + "banner": { + "content": "✨ You are viewing the **v2** SDK docs, which have not gone into effect yet. For the docs that describe the platform as it works today, go to [docs-v1.opencomputer.dev](https://docs-v1.opencomputer.dev).", + "dismissible": false + }, "logo": { "light": "/images/logo-light.svg", "dark": "/images/logo-dark.svg", diff --git a/docs/guides/browser-automation.mdx b/docs/guides/browser-automation.mdx index f82524d85..24ec8166c 100644 --- a/docs/guides/browser-automation.mdx +++ b/docs/guides/browser-automation.mdx @@ -31,7 +31,7 @@ If the site has a good API, use the API. A browser is slower, heavier, and flaki ## Step 1: Build a snapshot with Chromium pre-installed -Browser setup is heavy (apt packages + Chromium binary is ~500MB). Bake it into a [named snapshot](/sandboxes/snapshots) once, launch sandboxes from it in seconds. +Browser setup is heavy (apt packages + Chromium binary is ~500MB). Bake it into a [template](/sandboxes/templates) once, launch sandboxes from it in seconds. ```typescript build-snapshot.ts import { Image, Snapshots } from "@opencomputer/sdk/node"; @@ -227,7 +227,7 @@ await run("npx", ["libretto", "save", "app.example.com", "--session", "login"], // → writes /home/sandbox/.libretto/profiles/app.example.com.json ``` -The profile lives on the data disk, which survives sandbox hibernation. For cross-sandbox persistence, [snapshot the sandbox](/sandboxes/snapshots) after login and launch future sandboxes from that warm snapshot — they'll boot already logged in. +The profile lives on the data disk, which survives sandbox hibernation. For cross-sandbox persistence, [checkpoint the sandbox](/sandboxes/checkpoints) after login and launch future sandboxes from that warm snapshot — they'll boot already logged in. --- @@ -280,5 +280,5 @@ OC's preview-URL edge buffers response bodies — streaming chunks don't reach t ## Next steps - Read the [libretto docs](https://libretto.sh/docs) for the full CLI + library reference. -- See [Snapshots](/sandboxes/snapshots) for how to checkpoint a warmed-up browser VM. +- See [Checkpoints](/sandboxes/checkpoints) for how to checkpoint a warmed-up browser VM. - See [Secret Stores](/sandboxes/secrets) for scoping egress and sealing real credentials. diff --git a/docs/migrating-from-v1.mdx b/docs/migrating-from-v1.mdx new file mode 100644 index 000000000..3b88c2eac --- /dev/null +++ b/docs/migrating-from-v1.mdx @@ -0,0 +1,160 @@ +--- +title: "Migrating from v1" +description: "Every breaking change between v1 and v2, and how to find out whether you are affected" +--- + +v2 is the same API with the same SDK calls. Most code runs unchanged. What follows is the +complete list of behaviour that differs, ordered by how likely it is to matter. + +Nothing changes for you until your org is moved to v2. The v1 documentation stays accurate +until then and is archived permanently at [docs-v1.opencomputer.dev](https://docs-v1.opencomputer.dev). + +## Am I affected? + +Seven questions. If every answer is "no", your migration is a configuration change. + + + + v2 sandboxes have a hard 8-hour lifetime, counting hibernated time. See + [Sandbox lifetime](/sandboxes/lifetime). + + + v2 checkpoints capture the filesystem only. Restoring gives a fresh boot with your files, + not a resumed process tree. See [Checkpoints](/sandboxes/checkpoints). + + + A template that carries a rootfs image is refused. Most v1 templates do. See + [Templates](/sandboxes/templates). + + + The 16 GB tier does not exist in v2, and memory cannot change after launch. + + + Disk is fixed at ~16 GB in v2, down from 20 GB, and `diskMB` is ignored. + + + None of these exist in v2. + + + Image builds are unavailable. Fork is unavailable today and being worked on. + + + +## Finding the calls in your code + +```bash +# Hard failures in v2 +rg -n "\.scale\(|setAutoscale|\.mounts\b|createFromCheckpoint|buildImage|image:" + +# Behaviour changes — review these +rg -n "createCheckpoint|restoreCheckpoint|\.hibernate\(|setTimeout|cpuCount|diskMB" +``` + +## The breaking changes + +### Sandboxes end after 8 hours + + +A v2 sandbox is destroyed 8 hours after its host started, counting running **and** hibernated +time. It cannot be extended, and the disk goes with it. + + +Read `endAt` on the sandbox rather than computing a deadline — sandboxes come from a warm pool, +so the host often started before your create did: + +```typescript +const info = await getSandbox(sandbox.sandboxId); +const msLeft = Date.parse(info.endAt) - Date.now(); +``` + +Work that assumed a long-lived sandbox needs to checkpoint and roll over, or keep its state +outside the sandbox. + +### Checkpoints are filesystem-only + +v1 captures disk **and** memory. v2 captures the filesystem. Restoring gives you a freshly +booted sandbox with your files in place. + +If your checkpoints capture an installed environment, nothing changes. If they capture a warmed +process — a loaded model, an open connection pool — that state is gone on restore. + +`kind: "full"` is refused rather than silently producing a disk-only checkpoint. + +### Templates built on v1 are refused + +A v1 template captures the whole disk. v2 can only replay the workspace half, and doing that +silently would hand you your files while dropping every system change the template existed for. +So it refuses, loudly, at create. + +Rebuild affected templates as workspace templates or checkpoints on v2. + +### Sizes are fixed steps, and 16 GB is gone + +| Memory | v1 | v2 | +|---|---|---| +| 1 GB | ✓ | ✓ | +| 2 GB | — | ✓ | +| 4 GB | ✓ | ✓ default | +| 8 GB | ✓ | ✓ | +| 16 GB | ✓ | **not available** | + +Memory cannot be changed after launch. `cpuCount` and `diskMB` are accepted for compatibility +but are not controls in v2 — CPU follows memory, and disk is fixed at ~16 GB. + +### Hibernation suspends rather than parks + +v1 hibernation writes a checkpoint and releases the host, so a sandbox can stay hibernated +indefinitely. v2 suspends in place, and that time still counts against the 8-hour lifetime. A +sandbox hibernated overnight will not be there in the morning. + +### Idle timeouts are clamped + +A timeout longer than the remaining lifetime cannot fire, so v2 reports what it applied instead +of accepting it: + +```json +{ "applied": false, "requested": 86400, "timeout": 0 } +``` + +Check `applied`. + +### Secrets: same model, one difference + +Sealed placeholders, host-scoped substitution, fail-closed bypass and restart-free rotation all +work exactly as in v1, and your code does not change. + +The difference is where the real values are held: in v2 the substituting proxy runs inside your +sandbox as a root-owned process, rather than outside it. Your code runs unprivileged and cannot +read them, but a privilege escalation **inside** a sandbox now reaches the secrets scoped to +that sandbox. See [Secrets](/sandboxes/secrets). + +### Not available in v2 + +| | | +|---|---| +| `scale()`, `setAutoscale()` | memory is fixed at launch | +| Mounts (FUSE, NFS, overlay) | the guest cannot perform `mount` | +| Image builds (`image:`) | no build pipeline in v2 | +| Fork from a checkpoint | in progress | +| Checkpoint patches | not wired up | +| Live migration | not applicable | + +Each of these is **refused** rather than silently ignored. + +## What improves + +- Creates are served from a warm pool, so they start faster and more consistently. +- Deploys no longer disturb running sandboxes. +- An unavailable size is refused with the sizes that exist, instead of being reported as a + capacity problem. +- A sandbox that reports `running` is running — rows can no longer outlive their host. + +## Testing before you move + +Ask us to move a non-production org first. The same SDK and the same code run against both +versions, so your existing test suite is the test. + + +If something in your workload is blocked by the "in progress" items above, tell us. That list is +prioritised work, not a fixed decision. + diff --git a/docs/reference/cli/scaling.mdx b/docs/reference/cli/scaling.mdx index 62f42815c..4eb9712cf 100644 --- a/docs/reference/cli/scaling.mdx +++ b/docs/reference/cli/scaling.mdx @@ -1,121 +1,14 @@ --- -title: "oc sandbox scaling" -description: "Resize sandboxes, configure autoscale, and manage scaling locks from the CLI" +title: "Scaling" +description: "Not available in v2" --- -The CLI groups three related actions for sizing a sandbox: + +**Not available in v2.** Memory is fixed at launch in v2, so scaling calls return `501`. + -| Command | What it does | -| --- | --- | -| [`oc sandbox autoscale`](#oc-sandbox-autoscale-id) | Turn the platform autoscaler on/off and inspect its config | -| [`oc sandbox scale`](#oc-sandbox-scale-id-memory-mb) | Manually resize once | -| [`oc sandbox lock`](#oc-sandbox-lock-id) / [`unlock`](#oc-sandbox-unlock-id) / [`lock-status`](#oc-sandbox-lock-status-id) | Freeze or unfreeze the current size | +Set the size when you create the sandbox — see [Sandbox sizes](/sandboxes/sizes). -CPU follows memory per the platform's tier table; you don't pick CPU separately. - -For the underlying concepts and how these three modes interact, see the [Elasticity](/sandboxes/elasticity) guide. - ---- - -## `oc sandbox autoscale ` - -Configure or inspect per-sandbox autoscale. [HTTP API →](/api-reference/sandboxes/autoscale) - -Run with no flags to print the current configuration: - -```bash -oc sandbox autoscale sb-abc123 -# Autoscale enabled for sb-abc123 (1024–16384 MB) -``` - -Enable with bounds: - -```bash -oc sandbox autoscale sb-abc123 --on --min 1024 --max 16384 -``` - -Disable: - -```bash -oc sandbox autoscale sb-abc123 --off -``` - -When enabled, the platform watches memory pressure and resizes the sandbox between `--min` and `--max`: - -- **Scale up** on a single 1-min sample above 75 % memory utilization. Cooldown 60 s between up-scales. -- **Scale down** only when the 1-min, 5-min, AND 15-min averages all sit below 25 %. Cooldown 5 min between down-scales. - -The asymmetry is deliberate: rapid response when the user notices lag, conservative shrink after sustained idle. - -**Flags** - -| Flag | Description | -| --- | --- | -| `--on` | Enable autoscale (requires `--min` and `--max`). | -| `--off` | Disable autoscale. Mutually exclusive with `--on`. | -| `--min N` | Minimum memory in MB. | -| `--max N` | Maximum memory in MB. Must be ≥ `--min`. | - -**Errors** - -- `scaling_locked` — the sandbox has a scaling lock active. Run `oc sandbox unlock ` first. -- `402 Payment Required` — `--max` exceeds your plan cap. - ---- - -## `oc sandbox scale ` - -Manually resize a sandbox to a specific memory tier. [HTTP API →](/api-reference/sandboxes/scale) - -```bash -oc sandbox scale sb-abc123 8192 -# Scaled sb-abc123 to 8192MB / 400% CPU -``` - -A manual scale **disables autoscale** on this sandbox as a side effect — explicit intent overrides the loop. Re-enable with `oc sandbox autoscale --on` if you want size to track load again. - -**Errors** - -- `scaling_locked` — the sandbox has a scaling lock active. Run `oc sandbox unlock ` first. -- `402 Payment Required` — requested size exceeds your plan cap. - ---- - -## `oc sandbox lock ` - -Pin a sandbox at its current size. [HTTP API →](/api-reference/sandboxes/scaling-lock) - -```bash -oc sandbox lock sb-abc123 -# Sandbox sb-abc123 locked (scaling disabled) -``` - -While locked: - -- `oc sandbox scale` is rejected with `scaling_locked`. -- `oc sandbox autoscale --on` is rejected with `scaling_locked`. -- The platform autoscaler skips the sandbox entirely. - -Locking **also** disables autoscale (single knob — "I don't want this scaling, period"). Unlocking does NOT re-enable autoscale; run `oc sandbox autoscale --on` explicitly if you want it back. - ---- - -## `oc sandbox unlock ` - -Clear the scaling lock. - -```bash -oc sandbox unlock sb-abc123 -# Sandbox sb-abc123 unlocked -``` - ---- - -## `oc sandbox lock-status ` - -Print the current scaling-lock state. - -```bash -oc sandbox lock-status sb-abc123 -# Sandbox sb-abc123 is unlocked -``` +This page exists so the v2 docs mirror v1 page-for-page. See +[Migrating from v1](/migrating-from-v1) for the full list of removed and changed behaviour, +and [the v1 page](/reference/cli/scaling) for how this worked before. diff --git a/docs/reference/python-sdk/scaling.mdx b/docs/reference/python-sdk/scaling.mdx index b8138a6fa..7756dc651 100644 --- a/docs/reference/python-sdk/scaling.mdx +++ b/docs/reference/python-sdk/scaling.mdx @@ -1,171 +1,14 @@ --- title: "Scaling" -description: "Resize a sandbox manually, autoscale on memory pressure, or freeze its resources." +description: "Not available in v2" --- -OpenComputer sandboxes can change size at runtime. There are three knobs: + +**Not available in v2.** Memory is fixed at launch in v2, so scaling calls return `501`. + -| What you want | Use | -| --- | --- | -| Resize once, predictably | [`scale()`](#sandbox-scale-memory-mb) | -| Track memory pressure automatically | [`set_autoscale()`](#sandbox-set-autoscale) | -| Freeze the current size | [`set_scaling_lock()`](#sandbox-set-scaling-lock-locked) | +Set the size when you create the sandbox — see [Sandbox sizes](/sandboxes/sizes). -CPU follows memory per the platform's tier table. You don't pick CPU separately. - -## How the three interact - -- **Manual `scale()`** disables autoscale on the sandbox as a side effect — explicit user intent overrides the loop. Re-enable with `set_autoscale(enabled=True, ...)` after if you want. -- **Setting a scaling lock** disables autoscale at the same time (single knob: "I don't want this scaling, period"). While locked, both `scale()` and `set_autoscale(enabled=True)` raise `ScalingLockedError`. Unlocking does NOT auto-re-enable autoscale. -- **Plan caps** apply everywhere. Free-tier orgs are capped at 4 GB. Calls above the cap raise `PlanLimitError`. - ---- - -## `sandbox.scale(memory_mb)` - -Manually resize the sandbox. [HTTP API →](/api-reference/sandboxes/scale) - - - Target memory in MB. - - -**Returns:** `dict` with `sandboxID`, `memoryMB`, `cpuPercent`. - -**Raises:** - -- `ScalingLockedError` — sandbox has a scaling lock active. -- `PlanLimitError` — `memory_mb` exceeds the org's plan cap. - -```python -from opencomputer import Sandbox, ScalingLockedError, PlanLimitError - -sandbox = await Sandbox.connect("sb-abc123") - -try: - result = await sandbox.scale(memory_mb=8192) - print(f"scaled to {result['memoryMB']}MB / {result['cpuPercent']}% CPU") -except ScalingLockedError: - print("sandbox is locked — unlock to scale") -except PlanLimitError: - print("upgrade required for larger instances") -``` - ---- - -## `sandbox.set_autoscale(enabled, *, min_memory_mb=None, max_memory_mb=None)` - -Enable or disable per-sandbox autoscale. - - - Whether autoscale should be active. - - - - Lower bound when `enabled=True`. - - - - Upper bound when `enabled=True`. Must be ≥ `min_memory_mb`. - - -**Returns:** `dict` with `sandboxID`, `enabled`, `minMemoryMB`, `maxMemoryMB`. - -**Raises:** - -- `ScalingLockedError` — sandbox has a scaling lock active. -- `PlanLimitError` — `max_memory_mb` exceeds the org's plan cap. - -When `enabled=True`, the platform watches the sandbox's memory pressure and resizes it within the bounds: - -- **Scale up** on a single 1-min sample above 75% memory utilization. Cooldown 60s between up-scales. -- **Scale down** only when 1-min, 5-min, AND 15-min averages all sit below 25%. Cooldown 5 min between down-scales. - -The asymmetry matches user perception: rapid response when the user notices lag, conservative shrink after sustained idle. The 15-min window is the dominant constraint on shrink, so a sandbox that briefly idles and resumes won't sawtooth. - -```python -await sandbox.set_autoscale( - enabled=True, - min_memory_mb=1024, - max_memory_mb=16384, -) -``` - -To turn off: - -```python -await sandbox.set_autoscale(enabled=False) -``` - ---- - -## `sandbox.get_autoscale()` - -Get the current autoscale configuration. - -**Returns:** `dict` with `sandboxID`, `enabled`, `minMemoryMB`, `maxMemoryMB`. - -```python -cfg = await sandbox.get_autoscale() -if cfg["enabled"]: - print(f"autoscale {cfg['minMemoryMB']}–{cfg['maxMemoryMB']} MB") -``` - ---- - -## `sandbox.set_scaling_lock(locked)` - -Lock or unlock the sandbox's resources against any size change. - - - `True` to freeze, `False` to allow scaling again. - - -**Returns:** `dict` with `sandboxID`, `locked`. - -While locked: - -- `scale()` raises `ScalingLockedError`. -- `set_autoscale(enabled=True)` raises `ScalingLockedError`. -- The platform autoscaler skips this sandbox entirely. - -Locking ALSO disables autoscale (single knob — "I don't want this scaling"). Unlocking does **not** re-enable autoscale; call `set_autoscale(enabled=True, ...)` explicitly if you want it back. - -```python -# Pin a sandbox at its current size during a critical workload -await sandbox.set_scaling_lock(True) - -# ... - -# Allow scaling again after -await sandbox.set_scaling_lock(False) -``` - ---- - -## `sandbox.get_scaling_lock()` - -Get the current scaling-lock state. - -**Returns:** `dict` with `sandboxID`, `locked`. - ---- - -## Errors - -### `ScalingLockedError` - -Raised when the sandbox has a scaling lock active. Has a class attribute `code = "scaling_locked"` for parity with the HTTP API's error code. - -```python -from opencomputer import ScalingLockedError - -try: - await sandbox.scale(memory_mb=8192) -except ScalingLockedError: - # unlock first, or surface to user - ... -``` - -### `PlanLimitError` - -Raised when the requested size exceeds the org's plan cap. The HTTP API returns 402 Payment Required for this case. +This page exists so the v2 docs mirror v1 page-for-page. See +[Migrating from v1](/migrating-from-v1) for the full list of removed and changed behaviour, +and [the v1 page](/reference/python-sdk/scaling) for how this worked before. diff --git a/docs/reference/typescript-sdk/scaling.mdx b/docs/reference/typescript-sdk/scaling.mdx index 0b01de583..5cb71ea49 100644 --- a/docs/reference/typescript-sdk/scaling.mdx +++ b/docs/reference/typescript-sdk/scaling.mdx @@ -1,179 +1,14 @@ --- title: "Scaling" -description: "Resize a sandbox manually, autoscale on memory pressure, or freeze its resources." +description: "Not available in v2" --- -OpenComputer sandboxes can change size at runtime. There are three knobs: + +**Not available in v2.** Memory is fixed at launch in v2, so scaling calls return `501`. + -| What you want | Use | -| --- | --- | -| Resize once, predictably | [`scale()`](#sandbox-scale-opts) | -| Track memory pressure automatically | [`setAutoscale()`](#sandbox-setautoscale-opts) | -| Freeze the current size | [`setScalingLock()`](#sandbox-setscalinglock-locked) | +Set the size when you create the sandbox — see [Sandbox sizes](/sandboxes/sizes). -CPU follows memory per the platform's tier table. You don't pick CPU separately. - -## How the three interact - -- **Manual `scale()`** disables autoscale on the sandbox as a side effect — explicit user intent overrides the loop. Re-enable autoscale with `setAutoscale({ enabled: true, ... })` after if you want. -- **Setting a scaling lock** disables autoscale at the same time (single-knob: "I don't want this scaling, period"). While locked, both `scale()` and `setAutoscale({ enabled: true })` reject with `ScalingLockedError`. Unlocking does NOT auto-re-enable autoscale. -- **Plan caps** apply everywhere. Free-tier orgs are capped at 4 GB. Calls above the cap throw `PlanLimitError`. - ---- - -## `sandbox.scale(opts)` - -Manually resize the sandbox. [HTTP API →](/api-reference/sandboxes/scale) - - - Target memory in MB. - - -**Returns:** `Promise<{ sandboxID: string; memoryMB: number; cpuPercent: number }>` - -**Throws:** - -- `ScalingLockedError` — sandbox has a scaling lock active. -- `PlanLimitError` — `memoryMB` exceeds the org's plan cap. - -```typescript -import { Sandbox, ScalingLockedError, PlanLimitError } from "@opencomputer/sdk"; - -const sandbox = await Sandbox.connect("sb-abc123"); - -try { - const result = await sandbox.scale({ memoryMB: 8192 }); - console.log(`scaled to ${result.memoryMB}MB / ${result.cpuPercent}% CPU`); -} catch (err) { - if (err instanceof ScalingLockedError) { - console.warn("sandbox is locked — unlock to scale"); - } else if (err instanceof PlanLimitError) { - console.warn("upgrade required for larger instances"); - } else { - throw err; - } -} -``` - ---- - -## `sandbox.setAutoscale(opts)` - -Enable or disable per-sandbox autoscale. - - - Whether autoscale should be active. - - - - Lower bound when `enabled=true`. - - - - Upper bound when `enabled=true`. Must be ≥ `minMemoryMB`. - - -**Returns:** `Promise<{ sandboxID: string; enabled: boolean; minMemoryMB: number; maxMemoryMB: number }>` - -**Throws:** - -- `ScalingLockedError` — sandbox has a scaling lock active. -- `PlanLimitError` — `maxMemoryMB` exceeds the org's plan cap. - -When `enabled=true`, the platform watches the sandbox's memory pressure and resizes it within the bounds: - -- **Scale up** on a single 1-min sample above 75% memory utilization. Cooldown 60s between up-scales. -- **Scale down** only when 1-min, 5-min, AND 15-min averages all sit below 25%. Cooldown 5 min between down-scales. - -The asymmetry matches user perception: rapid response when the user notices lag, conservative shrink after sustained idle. The 15-min window is the dominant constraint on shrink, so a sandbox that briefly idles and resumes won't sawtooth. - -```typescript -await sandbox.setAutoscale({ - enabled: true, - minMemoryMB: 1024, - maxMemoryMB: 16384, -}); -``` - -To turn off: - -```typescript -await sandbox.setAutoscale({ enabled: false }); -``` - ---- - -## `sandbox.getAutoscale()` - -Get the current autoscale configuration. - -**Returns:** `Promise<{ sandboxID: string; enabled: boolean; minMemoryMB: number; maxMemoryMB: number }>` - -```typescript -const cfg = await sandbox.getAutoscale(); -if (cfg.enabled) { - console.log(`autoscale ${cfg.minMemoryMB}–${cfg.maxMemoryMB} MB`); -} -``` - ---- - -## `sandbox.setScalingLock(locked)` - -Lock or unlock the sandbox's resources against any size change. - - - `true` to freeze, `false` to allow scaling again. - - -**Returns:** `Promise<{ sandboxID: string; locked: boolean }>` - -While locked: - -- `scale()` rejects with `ScalingLockedError`. -- `setAutoscale({ enabled: true })` rejects with `ScalingLockedError`. -- The platform autoscaler skips this sandbox entirely. - -Locking ALSO disables autoscale (single knob — "I don't want this scaling"). Unlocking does **not** re-enable autoscale; call `setAutoscale({ enabled: true, ... })` explicitly if you want it back. - -```typescript -// Pin a sandbox at its current size during a critical workload -await sandbox.setScalingLock(true); - -// ... - -// Allow scaling again after -await sandbox.setScalingLock(false); -``` - ---- - -## `sandbox.getScalingLock()` - -Get the current scaling-lock state. - -**Returns:** `Promise<{ sandboxID: string; locked: boolean }>` - ---- - -## Errors - -### `ScalingLockedError` - -Thrown when the sandbox has a scaling lock active. Has a `code` property of `"scaling_locked"` for parity with the HTTP API's error code. - -```typescript -import { ScalingLockedError } from "@opencomputer/sdk"; - -try { - await sandbox.scale({ memoryMB: 8192 }); -} catch (err) { - if (err instanceof ScalingLockedError) { - // unlock first, or surface to user - } -} -``` - -### `PlanLimitError` - -Thrown when the requested size exceeds the org's plan cap. The HTTP API returns 402 Payment Required for this case. +This page exists so the v2 docs mirror v1 page-for-page. See +[Migrating from v1](/migrating-from-v1) for the full list of removed and changed behaviour, +and [the v1 page](/reference/typescript-sdk/scaling) for how this worked before. diff --git a/docs/sandboxes/checkpoints.mdx b/docs/sandboxes/checkpoints.mdx index 8ab1eb42c..087d41800 100644 --- a/docs/sandboxes/checkpoints.mdx +++ b/docs/sandboxes/checkpoints.mdx @@ -1,224 +1,66 @@ --- title: "Checkpoints" -description: "Snapshot and fork sandbox state" +description: "Filesystem snapshots — what they capture, and what they do not" --- -A checkpoint is a named snapshot of a running sandbox. Fork new sandboxes from it to start from a known-good environment — like git branches for VMs. +A checkpoint captures the sandbox's **filesystem**. Restoring puts those files back. -## Checkpoint Types - -OpenComputer supports two checkpoint modes: - -| Type | Preserves | Use when | -| --- | --- | --- | -| Full checkpoint | Disk, memory, and CPU state | You want the fastest fork or restore path from an exact running VM state. This is best for templates, important milestones, and branches you expect to fork often. | -| Disk-only checkpoint | Rootfs and workspace disk state | You want lightweight, frequent snapshots of files, installed packages, and workspace changes. Forks boot from the saved disks, so they are cheaper to keep but may take longer to become ready. | - -Full checkpoints are the default. Use `kind: "disk_only"` when creating autosaves or high-frequency checkpoints where disk state is enough. - - - -```typescript TypeScript -import { Sandbox } from "@opencomputer/sdk"; - -const sandbox = await Sandbox.create(); -await sandbox.exec.run("npm install && npm run build", { cwd: "/app" }); - -// Checkpoint after setup -const cp = await sandbox.createCheckpoint("after-build"); - -// Fork two independent sandboxes -const a = await Sandbox.createFromCheckpoint(cp.id); -const b = await Sandbox.createFromCheckpoint(cp.id); - -await a.exec.run("npm run test:unit", { cwd: "/app" }); -await b.exec.run("npm run test:e2e", { cwd: "/app" }); -``` - -```python Python -from opencomputer import Sandbox - -sandbox = await Sandbox.create() -await sandbox.exec.run("npm install && npm run build", cwd="/app") - -cp = await sandbox.create_checkpoint("after-build") - -a = await Sandbox.create_from_checkpoint(cp["id"]) -b = await Sandbox.create_from_checkpoint(cp["id"]) - -await a.exec.run("npm run test:unit", cwd="/app") -await b.exec.run("npm run test:e2e", cwd="/app") -``` - - - -## Checkpoints vs Hibernation - -| | Checkpoint | Hibernation | -| --- | --- | --- | -| Purpose | Fork new sandboxes from saved state | Pause and resume the **same** sandbox | -| Original sandbox | Keeps running | Stopped | -| Can fork | Yes — unlimited new sandboxes | No | -| Count | Up to 10 per sandbox, with optional oldest-checkpoint cleanup | One hibernation state | -| Use case | Parallel testing, branching experiments | Cost savings, idle timeout | - -## What Gets Preserved - -Checkpoints capture the filesystem and installed state. Forked sandboxes start with a fresh boot from that disk state — the platform attempts warm restore when possible, but may fall back to a cold boot. Don't assume running processes carry over. - -## API Reference - -### Create Checkpoint - - - -```typescript TypeScript -const checkpoint = await sandbox.createCheckpoint("before-migration"); -// checkpoint.id, checkpoint.status ("processing" → "ready") -``` - -```python Python -checkpoint = await sandbox.create_checkpoint("before-migration") -# checkpoint["id"], checkpoint["status"] -``` - -```bash CLI -oc cp create sb-abc123 --name before-migration -``` - - - -Checkpoint name must be unique within the sandbox. Status transitions from `processing` to `ready` (or `failed`). - -Each sandbox can have up to 10 full checkpoints and up to 100 disk-only checkpoints. To create a new checkpoint without failing at the limit, pass a retention policy that deletes the oldest eligible checkpoint of the same type first: - - - -```typescript TypeScript -const checkpoint = await sandbox.createCheckpoint("autosave", { - kind: "disk_only", - retentionPolicy: { mode: "delete_oldest", maxCount: 100 }, -}); -``` - -```python Python -checkpoint = await sandbox.create_checkpoint( - "autosave", - kind="disk_only", - retention_policy={"mode": "delete_oldest", "maxCount": 100}, -) -``` - -```bash CLI -oc cp create sb-abc123 \ - --name autosave \ - --kind disk_only \ - --retention-policy delete_oldest \ - --retention-max-count 100 -``` - - - -Retention skips checkpoints that are public, have patches, or are still referenced by forked sandboxes. If no eligible checkpoint of the requested type can be deleted, creation fails instead of deleting protected state. - -### List Checkpoints - - - -```typescript TypeScript -const checkpoints = await sandbox.listCheckpoints(); -``` - -```python Python -checkpoints = await sandbox.list_checkpoints() -``` - - - -### Fork from Checkpoint - -Creates a new sandbox from a checkpoint: - - - -```typescript TypeScript -const forked = await Sandbox.createFromCheckpoint(checkpointId, { - timeout: 600, -}); -``` +```typescript +const cp = await sandbox.createCheckpoint("after-install"); +// wait for status "ready" -```python Python -forked = await Sandbox.create_from_checkpoint(checkpoint_id, timeout=600) +await sandbox.restoreCheckpoint(cp.checkpointId); ``` - + +Checkpoints on v2 **do not capture live memory**. Restoring gives you your files back +on a freshly booted sandbox — not a process tree resumed mid-execution. + -### Restore Checkpoint +## Why -Revert a sandbox in-place. All changes since the checkpoint are lost: +The current runtime can snapshot a running VM's memory because we operate the hypervisor. The +v2 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 -```typescript TypeScript -await sandbox.restoreCheckpoint(checkpointId); -``` +**If you checkpoint an installed environment** — dependencies, a built toolchain, a seeded +database directory — nothing changes. That is a filesystem, and it restores exactly. -```python Python -await sandbox.restore_checkpoint(checkpoint_id) -``` +**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. -### Delete Checkpoint +## The `kind` argument - +The current runtime accepts `kind: "full"` for a memory-inclusive checkpoint. Here: -```typescript TypeScript -await sandbox.deleteCheckpoint(checkpointId); -``` +- 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 -```python Python -await sandbox.delete_checkpoint(checkpoint_id) +```typescript +await sandbox.createCheckpoint("env"); // fine +await sandbox.createCheckpoint("env", { kind: "disk_only" }); // fine +await sandbox.createCheckpoint("env", { kind: "full" }); // refused ``` - - -## CheckpointInfo +## Restore is in-place -| Field | TypeScript | Python | Description | -| --- | --- | --- | --- | -| ID | `id` | `id` | Checkpoint UUID | -| Sandbox ID | `sandboxId` | `sandboxID` | Source sandbox | -| Name | `name` | `name` | Human-readable name | -| Status | `status` | `status` | `processing`, `ready`, or `failed` | -| Size | `sizeBytes` | `sizeBytes` | Snapshot size in bytes | -| Created | `createdAt` | `createdAt` | Timestamp | +`restoreCheckpoint` rolls the current sandbox's filesystem back to the checkpoint. It does not +create a new sandbox. - The TypeScript SDK returns typed `CheckpointInfo` objects. Python returns raw dictionaries with the HTTP API field names. +**Forking a new sandbox from a checkpoint is not available yet.** `createFromCheckpoint` fails +on v2. It is being worked on. Until then, restore into an existing sandbox. -## Example: Parallel Exploration - -Try multiple approaches from the same starting point: - -```typescript -const sandbox = await Sandbox.create(); -await sandbox.exec.run("git clone https://github.com/user/repo /app"); -const cp = await sandbox.createCheckpoint("fresh-clone"); - -// Try three different migration strategies in parallel -const strategies = ["--strategy=ours", "--strategy=theirs", "--strategy=recursive"]; -const results = await Promise.all( - strategies.map(async (strategy) => { - const fork = await Sandbox.createFromCheckpoint(cp.id); - const result = await fork.exec.run(`cd /app && git merge origin/main ${strategy}`); - await fork.kill(); - return { strategy, exitCode: result.exitCode }; - }), -); -``` +## Checkpoints outlive their sandbox - - CLI equivalent: [`oc checkpoint`](/cli/checkpoint). Full reference: [TypeScript SDK](/reference/typescript-sdk#sandbox) · [Python SDK](/reference/python-sdk#sandbox) · [HTTP API](/reference/api#checkpoints). - +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](/sandboxes/lifetime): checkpoint before the deadline, +then restore onto a sandbox you create later. diff --git a/docs/sandboxes/elasticity.mdx b/docs/sandboxes/elasticity.mdx index 5a1333c5b..33e0ff177 100644 --- a/docs/sandboxes/elasticity.mdx +++ b/docs/sandboxes/elasticity.mdx @@ -1,307 +1,14 @@ --- title: "Elasticity" -description: "Dynamically scale sandbox memory and CPU — automatically, manually, or via the in-VM API" +description: "Not available in v2" --- -A sandbox's memory and CPU can be resized at runtime. The platform's recommended path is **Autoscaling** — opt the sandbox in once and we resize it for you based on observed memory pressure. Lower-level controls are also available if you want to drive sizing yourself or freeze it. + +**Not available in v2.** A sandbox's memory is fixed by the image it launched from and cannot be changed afterwards, so there is nothing to scale. `scale()`, `setAutoscale()` and the in-VM scale endpoint all return `501`. + -CPU scales proportionally to memory in all modes (1 vCPU per ~4 GB up to 16 GB / 4 vCPU). The permissible memory tiers are: +Choose the size at create time instead — see [Sandbox sizes](/sandboxes/sizes). Sizes are fixed steps from 1 GB to 8 GB. -| Memory | vCPU | -| --- | --- | -| 1 GB | 1 (best-effort) | -| 4 GB | 1 | -| 8 GB | 2 | -| 16 GB | 4 | - -If you want a sandbox to **stay at a fixed size**, see [Locking Resources](#locking-resources). - -## Autoscaling - -Opt a sandbox into the per-sandbox autoscaler and the platform resizes it for you based on observed memory pressure. Autoscale is **opt-in per sandbox** and **disabled by default**. - -This is the recommended way to size a sandbox: turn it on once with bounds you're comfortable with, then leave it. Manual `scale` and the in-VM API are escape hatches for cases where the autoscaler isn't a good fit (e.g. you want a hard guarantee for a benchmark, or you're scripting your own logic). - -### Enable - - -```typescript TypeScript -await sandbox.setAutoscale({ - enabled: true, - minMemoryMB: 1024, - maxMemoryMB: 16384, -}); -``` - -```python Python -await sandbox.set_autoscale( - enabled=True, - min_memory_mb=1024, - max_memory_mb=16384, -) -``` - -```bash CLI -oc sandbox autoscale sb-abc123 --on --min 1024 --max 16384 -``` - -```bash HTTP -curl -sf -X PUT "$API/api/sandboxes/$SANDBOX_ID/autoscale" \ - -H "X-API-Key: $API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"enabled": true, "minMemoryMB": 1024, "maxMemoryMB": 16384}' -``` - - -`minMemoryMB` and `maxMemoryMB` must be values from the tier table above. The autoscaler keeps the sandbox between these bounds, in tier steps. - -### Behavior - -- **Scale up**: when the sandbox's 1-minute average memory utilization exceeds 75 %, jump to the next tier. 60-second cooldown between up-events. -- **Scale down**: when **all** of the 1-min, 5-min, and 15-min utilization averages stay below 25 %, drop one tier. 5-minute cooldown between down-events. Down requires at least 15 minutes of low-utilization data, so a brief idle pause won't shrink you. -- **Manual override**: any explicit `scale` call disables autoscale on that sandbox. Re-enable explicitly via `setAutoscale({ enabled: true, ... })` (or the equivalent in any SDK) if you want it back. - -### Disable - - -```typescript TypeScript -await sandbox.setAutoscale({ enabled: false }); -``` - -```python Python -await sandbox.set_autoscale(enabled=False) -``` - -```bash CLI -oc sandbox autoscale sb-abc123 --off -``` - -```bash HTTP -curl -sf -X PUT "$API/api/sandboxes/$SANDBOX_ID/autoscale" \ - -H "X-API-Key: $API_KEY" \ - -d '{"enabled": false}' -``` - - -### Inspect - - -```typescript TypeScript -const cfg = await sandbox.getAutoscale(); -// { sandboxID: "sb-...", enabled: true, minMemoryMB: 1024, maxMemoryMB: 16384 } -``` - -```python Python -cfg = await sandbox.get_autoscale() -# {"sandboxID": "sb-...", "enabled": True, "minMemoryMB": 1024, "maxMemoryMB": 16384} -``` - -```bash CLI -oc sandbox autoscale sb-abc123 -# Autoscale enabled for sb-abc123 (1024–16384 MB) -``` - -```bash HTTP -curl -sf "$API/api/sandboxes/$SANDBOX_ID/autoscale" -H "X-API-Key: $API_KEY" -# {"sandboxID":"sb-...","enabled":true,"minMemoryMB":1024,"maxMemoryMB":16384} -``` - - -## Manual Scaling (Control Plane) - -When you want to resize once — predictable size for a benchmark, a one-off scale-up before a known-heavy task, an operator response to an alert — call `scale` from your application or operator tooling: - - -```typescript TypeScript -await sandbox.scale({ memoryMB: 4096 }); -``` - -```python Python -await sandbox.scale(memory_mb=4096) -``` - -```bash CLI -oc sandbox scale sb-abc123 4096 -``` - -```bash HTTP -curl -sf -X POST "$API/api/sandboxes/$SANDBOX_ID/scale" \ - -H "X-API-Key: $API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"memoryMB": 4096}' -``` - - -A manual scale **disables autoscale** on the sandbox — explicit user intent overrides the loop. Re-enable it later if you want size to track load again. - -The endpoint can return: - -- **403 `scaling_locked`** — the sandbox is locked. See [Locking Resources](#locking-resources). -- **409 `oom_floor`** — the requested size would force a guest OOM-kill because the current working set exceeds it. Free memory inside the guest, then retry. -- **402 Payment Required** — the requested size exceeds your plan cap. - -## In-VM Scaling - -Sandboxes expose an internal metadata API at `http://169.254.169.254` that lets code **inside** the VM scale itself. Useful for workload-aware scripts where the workload knows its own demand better than the platform autoscaler can infer (e.g. "scale up before this build, back down after"). - - - This endpoint is only reachable from inside the sandbox. It is not exposed through the control plane API or SDKs. - - -### Scale Memory - -Send a `POST` to `/v1/scale` with the desired memory in MB. CPU is adjusted proportionally. - -```bash -curl -s -X POST http://169.254.169.254/v1/scale \ - -H "Content-Type: application/json" \ - -d '{"memoryMB": 4096}' -``` - -### Check Current Limits - -```bash -curl -s http://169.254.169.254/v1/limits -``` - -### Example: Rust Compilation - -Rust builds are memory-hungry. You can alias `cargo` to scale up before compilation and scale back down after: - -```bash -alias cargo='_cargo_scaled' -_cargo_scaled() { - # Scale up to 16GB / 4 vCPU before build - curl -sf -X POST http://169.254.169.254/v1/scale \ - -H "Content-Type: application/json" -d '{"memoryMB": 16384}' - - # Run the actual cargo command - command cargo "$@" - local exit_code=$? - - # Scale back down to 4GB / 1 vCPU - curl -sf -X POST http://169.254.169.254/v1/scale \ - -H "Content-Type: application/json" -d '{"memoryMB": 4096}' - - return $exit_code -} -``` - -Add this to your sandbox's `~/.bashrc` or inject it via `sandbox.exec.run` so every `cargo build`, `cargo test`, etc. automatically gets the extra resources. - -### Example: Custom Scaling Loop - -A simple shell script that monitors memory pressure and scales automatically. Useful when you want fine-grained control inside the sandbox itself; for the platform-managed equivalent see [Autoscaling](#autoscaling) above. - -```bash -#!/bin/sh -SCALE_API="http://169.254.169.254/v1/scale" -MIN_MB=1024 -MAX_MB=8192 -SCALE_UP_THRESHOLD=80 -SCALE_DOWN_THRESHOLD=30 -COOLDOWN=30 -last_scale=0 - -while true; do - mem_total=$(awk '/MemTotal/{print $2}' /proc/meminfo) - mem_avail=$(awk '/MemAvailable/{print $2}' /proc/meminfo) - usage_pct=$(( (mem_total - mem_avail) * 100 / mem_total )) - total_mb=$((mem_total / 1024)) - now=$(date +%s) - elapsed=$((now - last_scale)) - - if [ $usage_pct -gt $SCALE_UP_THRESHOLD ] && [ $elapsed -gt $COOLDOWN ]; then - new_mb=$((total_mb * 2)) - [ $new_mb -gt $MAX_MB ] && new_mb=$MAX_MB - if [ $new_mb -gt $total_mb ]; then - echo "usage=$usage_pct% -> scaling UP to ${new_mb}MB" - curl -sf -X POST "$SCALE_API" \ - -H "Content-Type: application/json" \ - -d "{\"memoryMB\":$new_mb}" - last_scale=$now - fi - elif [ $usage_pct -lt $SCALE_DOWN_THRESHOLD ] && [ $total_mb -gt $MIN_MB ] && [ $elapsed -gt $COOLDOWN ]; then - new_mb=$((total_mb / 2)) - [ $new_mb -lt $MIN_MB ] && new_mb=$MIN_MB - if [ $new_mb -lt $total_mb ]; then - echo "usage=$usage_pct% -> scaling DOWN to ${new_mb}MB" - curl -sf -X POST "$SCALE_API" \ - -H "Content-Type: application/json" \ - -d "{\"memoryMB\":$new_mb}" - last_scale=$now - fi - fi - - sleep 5 -done -``` - -## Locking Resources - -If you want a sandbox to **stay at a fixed size** — predictable billing, a benchmark, a long-running pinned workload — lock it. Locking blocks both manual scaling and autoscaling on the sandbox. - - -```typescript TypeScript -await sandbox.setScalingLock(true); -``` - -```python Python -await sandbox.set_scaling_lock(True) -``` - -```bash CLI -oc sandbox lock sb-abc123 -``` - -```bash HTTP -curl -sf -X PUT "$API/api/sandboxes/$SANDBOX_ID/scaling-lock" \ - -H "X-API-Key: $API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"locked": true}' -``` - - -While locked: - -- `scale` returns **403** `scaling_locked` -- `setAutoscale({ enabled: true })` returns **403** `scaling_locked` -- The autoscaler skips the sandbox - -Locking automatically disables autoscale on the sandbox, so you have a single user-facing toggle. Unlocking does **not** auto-re-enable autoscale; turn it back on explicitly if you want it. - - -```typescript TypeScript -// Unlock -await sandbox.setScalingLock(false); - -// Inspect -const lock = await sandbox.getScalingLock(); -// { sandboxID: "sb-...", locked: false } -``` - -```python Python -# Unlock -await sandbox.set_scaling_lock(False) - -# Inspect -lock = await sandbox.get_scaling_lock() -# {"sandboxID": "sb-...", "locked": False} -``` - -```bash CLI -oc sandbox unlock sb-abc123 -oc sandbox lock-status sb-abc123 -``` - -```bash HTTP -# Unlock -curl -sf -X PUT "$API/api/sandboxes/$SANDBOX_ID/scaling-lock" \ - -H "X-API-Key: $API_KEY" \ - -d '{"locked": false}' - -# Inspect -curl -sf "$API/api/sandboxes/$SANDBOX_ID/scaling-lock" -H "X-API-Key: $API_KEY" -# {"sandboxID":"sb-...","locked":false} -``` - +This page exists so the v2 docs mirror v1 page-for-page. See +[Migrating from v1](/migrating-from-v1) for the full list of removed and changed behaviour, +and [the v1 page](/sandboxes/elasticity) for how this worked before. diff --git a/docs/sandboxes/lifetime.mdx b/docs/sandboxes/lifetime.mdx new file mode 100644 index 000000000..7bcd01402 --- /dev/null +++ b/docs/sandboxes/lifetime.mdx @@ -0,0 +1,86 @@ +--- +title: "Sandbox lifetime" +description: "The 8-hour ceiling, endAt, and how to design around them" +--- + +Every v2 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](/sandboxes/timeout). + +## 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](/sandboxes/timeout). diff --git a/docs/sandboxes/mounts.mdx b/docs/sandboxes/mounts.mdx index a39274760..f388af716 100644 --- a/docs/sandboxes/mounts.mdx +++ b/docs/sandboxes/mounts.mdx @@ -1,424 +1,14 @@ --- title: "Mounts" -description: "Mount S3, GCS, Azure Blob, SFTP, WebDAV, and Dropbox into a sandbox via FUSE" -tag: "Preview" +description: "Not available in v2" --- - - **Preview:** the mounts API is new. Endpoints, request shape, and SDK - method names may change before GA. - - -Mount remote filesystems directly into your sandbox so your code reads and -writes them like local files — no SDK calls, no chunked downloads, no -boilerplate. Perfect for pulling in datasets, model weights, or a customer's -bucket without staging anything to disk first. - -```typescript -import { Sandbox } from "@opencomputer/sdk"; - -const sandbox = await Sandbox.create(); - -await sandbox.mounts.add({ - path: "/mnt/data", - remote: "s3:my-bucket/datasets", - backend: "s3", - creds: { - access_key_id: process.env.AWS_ACCESS_KEY_ID!, - secret_access_key: process.env.AWS_SECRET_ACCESS_KEY!, - region: "us-east-1", - }, -}); - -// Now anything in the sandbox can read s3://my-bucket/datasets as /mnt/data: -await sandbox.exec.run("ls /mnt/data"); -await sandbox.exec.run("head /mnt/data/train.csv"); -``` - -## How it works - -Mounts use [`rclone mount`](https://rclone.org/commands/rclone_mount/) under the -hood — a single binary that speaks ~40 backends (S3, GCS, Azure Blob, SFTP, -WebDAV, Dropbox, and more). When you call `mounts.add()`: - -1. The control plane templates an rclone config from your `backend` + `creds`. -2. The config is written to a tmpfs file inside the VM (mode `0600` — the - sandbox user can't read it). -3. `rclone mount` is spawned as a background daemon, exposing the remote at - the path you specified. -4. Every process in the sandbox can now read/write that path. - -Credentials live only inside the VM's tmpfs for the lifetime of the mount. -The worker keeps no copy. - -## Supported Backends - -`backend` selects how `creds` are templated. The keys you pass map directly to -[rclone config fields](https://rclone.org/docs/) for that backend type. - -| Backend | Common keys | -| ------------- | --------------------------------------------------------------------------- | -| `s3` | `access_key_id`, `secret_access_key`, `region`, `provider` (default `AWS`), `endpoint` | -| `gcs` | `service_account_credentials` (JSON string) or `service_account_file` | -| `azureblob` | `account`, `key` *or* `sas_url` | -| `sftp` | `host`, `user`, `pass` or `key_file`, `port` | -| `webdav` | `url`, `vendor`, `user`, `pass` | -| `dropbox` | `token` | - -For anything not in this list, or for advanced tuning, pass `rcloneConfig` -directly with a raw rclone config string — see [Custom Config](#custom-config) -below. - -## Examples - -### S3 (AWS) - - - -```typescript TypeScript -await sandbox.mounts.add({ - path: "/mnt/data", - remote: "s3:my-bucket", - backend: "s3", - creds: { - access_key_id: process.env.AWS_ACCESS_KEY_ID!, - secret_access_key: process.env.AWS_SECRET_ACCESS_KEY!, - region: "us-east-1", - }, -}); -``` - -```python Python -await sandbox.mounts.add( - path="/mnt/data", - remote="s3:my-bucket", - backend="s3", - creds={ - "access_key_id": os.environ["AWS_ACCESS_KEY_ID"], - "secret_access_key": os.environ["AWS_SECRET_ACCESS_KEY"], - "region": "us-east-1", - }, -) -``` - - - -### S3-compatible (MinIO, R2, Tigris, etc.) - -Override the `provider` and point at the endpoint: - -```typescript -await sandbox.mounts.add({ - path: "/mnt/r2", - remote: "r2:my-bucket", - backend: "s3", - creds: { - provider: "Cloudflare", - access_key_id: "...", - secret_access_key: "...", - endpoint: "https://.r2.cloudflarestorage.com", - region: "auto", - }, -}); -``` - -### Google Cloud Storage - - - -```typescript TypeScript -import fs from "node:fs"; - -await sandbox.mounts.add({ - path: "/mnt/gcs", - remote: "gcs:my-bucket", - backend: "gcs", - creds: { - service_account_credentials: fs.readFileSync("./sa-key.json", "utf8"), - }, -}); -``` - -```python Python -with open("./sa-key.json") as f: - sa_json = f.read() - -await sandbox.mounts.add( - path="/mnt/gcs", - remote="gcs:my-bucket", - backend="gcs", - creds={"service_account_credentials": sa_json}, -) -``` - - - -### SFTP - -```typescript -await sandbox.mounts.add({ - path: "/mnt/remote", - remote: "ssh:/home/data", - backend: "sftp", - creds: { - host: "data.example.com", - user: "agent", - pass: process.env.SSH_PASSWORD!, // or key_file - }, -}); -``` - -### Custom Config - -For backends not in the typed list — or to tune things like `--vfs-cache-mode` -or `--dir-cache-time` — pass an rclone config string directly. The section -name in the config must match the part of `remote` before the colon. - -```typescript -await sandbox.mounts.add({ - path: "/mnt/box", - remote: "box:Reports", - rcloneConfig: ` -[box] -type = box -token = {"access_token":"...","refresh_token":"...","expiry":"..."} -`.trim(), - mountOptions: ["--dir-cache-time", "1m"], -}); -``` - -## Read-only by default - -Mounts are read-only unless you explicitly opt into read-write: - -```typescript -await sandbox.mounts.add({ - path: "/mnt/writable", - remote: "s3:scratch", - backend: "s3", - creds: {...}, - readOnly: false, // opts into RW; uses --vfs-cache-mode writes -}); -``` - - Object-store FUSE mounts have well-known write footguns — concurrent writers - to the same key can produce surprising results, and small-write workloads - amplify request counts (and your bill). Prefer object-store SDKs for - write-heavy workloads; use mounts for read-heavy access and append-style - artifacts. +**Not available in v2.** The sandbox guest runs without `CAP_SYS_ADMIN`, and it is absent from the capability bounding set, so `mount` cannot be performed at all — not even by root. This is a platform limit, not a feature gap. -## Listing and removing - -```typescript -const mounts = await sandbox.mounts.list(); -// [{ path: "/mnt/data", remote: "s3:my-bucket", backend: "s3", readOnly: true }] - -await sandbox.mounts.remove("/mnt/data"); -``` - -## Hibernate behavior - -Mounts survive hibernate/wake transparently. The VM snapshot captures the live -FUSE mount and the rclone daemon process; when the sandbox wakes, both are -restored as-is. No re-mount call needed — the mount is exactly where you left -it, with the same credentials, serving the same path. - -```typescript -await sandbox.mounts.add({ path: "/mnt/data", remote: "s3:my-bucket", backend: "s3", creds }); - -await sandbox.hibernate(); -await sandbox.wake(); - -await sandbox.exec.run("ls /mnt/data"); // still works, no re-mount needed -``` - -When you want a mount gone, call `remove()` explicitly: - -```typescript -await sandbox.mounts.remove("/mnt/data"); -``` - -That triggers an actual `fusermount3 -u` in the VM and drops the registry -entry. No magic teardown on hibernate. - -### Stale credentials - -If the credentials behind a mount get rotated or revoked while the sandbox is -hibernated, the in-VM rclone daemon — restored intact from the snapshot — is -still holding the *old* keys and will start hitting auth errors on the next -request. Fix: `mounts.remove(path)` then `mounts.add(...)` again with fresh -credentials. - -## CLI - -```bash -oc mounts add sb-abc123 \ - --path /mnt/data \ - --remote s3:my-bucket \ - --backend s3 \ - --cred access_key_id=AKIA... \ - --cred secret_access_key=... \ - --cred region=us-east-1 - -oc mounts list sb-abc123 -oc mounts rm sb-abc123 /mnt/data -``` - -For raw rclone configs, point `--config-file` at a local file: - -```bash -oc mounts add sb-abc123 --path /mnt/box --remote box:Reports --config-file ./rclone.conf -``` - -## Troubleshooting - -**`sandbox image is missing rclone and/or fusermount3`** — the sandbox is -running on an older base image. Recreate the sandbox from the latest default -template, or build an image off the current `Dockerfile.default`. - -**Mount succeeds but `ls` returns empty** — usually a creds / -permission issue on the remote side. SSH in (`oc shell`) and run -`rclone ls : --config /run/oc-agent/mounts/.conf` to see the real -error. Or `ls -la` the mount point — FUSE errors surface as filesystem errors. - -**`fuse: bad mount point ... permission denied`** — make sure the target path -isn't already a non-empty directory you don't own. The mount call creates the -path with `sandbox:sandbox` ownership; pre-existing roots can conflict. - - - CLI equivalent: [`oc mounts`](/cli/mounts). Full reference: - [TypeScript SDK](/reference/typescript-sdk#mounts) · - [Python SDK](/reference/python-sdk#mounts). - - -## Bring your own FUSE - - - Advanced — most users want the rclone path above. Reach for this only if you - already have a FUSE-ready filesystem. - - -Under the hood, `mounts.add()` picks a **driver** — the daemon that actually -establishes the FUSE mount. Everything above uses the default `rclone` driver. -If you already have a FUSE-ready filesystem (your own VFS, gcsfuse, s3fs, …) and -don't want rclone as a middle layer, use the **`command` driver**: you hand us -the daemon's argv and we run it as the sandbox user, inject your env/secrets, -wait for the mount to come live, and tear it down on `remove` — the same -lifecycle as an rclone mount. - -```typescript -await sandbox.mounts.add({ - path: "/mnt/data", - driver: "command", - command: ["my-vfs-fuse", "--bucket", "gs://my-bucket", "{mountpoint}"], - secrets: { MY_VFS_TOKEN: process.env.MY_VFS_TOKEN! }, -}); -``` - -In Python this is a separate method, `mounts.add_command(...)`. - -### The `{mountpoint}` token - -`{mountpoint}` (or `{path}`) in your argv is replaced with `path` before the -command runs, so you don't repeat the path twice. The call above runs: - -``` -my-vfs-fuse --bucket gs://my-bucket /mnt/data -``` - -You can also hardcode the path — the token is just a convenience. - -### env and secrets - -- **`env`** — plain environment variables for the daemon. Recorded and returned - by `list()`. -- **`secrets`** — credentials. Injected into the daemon's **process - environment** (never the command line, so they don't leak via `ps`), never - written to disk on the worker, and never returned by `list()`. - -For daemons that need a credentials *file* (e.g. a service-account JSON), write -it first with the [filesystem API](/sandboxes/filesystem) and point the command -at it. - -`readOnly` is **advisory** for this driver — your command must honor it (we also -export `OC_MOUNT_READONLY=1`). Unlike rclone, the platform can't enforce -read-only on an arbitrary daemon. - -### Requirements - -Your daemon must: - -- be present in the sandbox — bake it into your image, or install/download it at - runtime before mounting; -- mount at the mountpoint and keep running (a foreground daemon is fine — we - background it; a self-daemonizing one works too); -- exit when its mount is unmounted (standard libfuse behavior) so `remove` - cleans up. - -The platform handles the rest: `/dev/fuse` access, creating the mountpoint, and -`-o allow_other` support (`user_allow_other` is set in `/etc/fuse.conf`). If the -mount doesn't come up within the timeout, the call fails with the **daemon's own -log tail** so you can see why (a bad flag, missing binary, auth error, etc.). - -### Example: gcsfuse - -[gcsfuse](https://cloud.google.com/storage/docs/gcsfuse-install) is Google's own -GCS-to-FUSE adapter. Install it in your image (or at runtime), write your -service-account key with the [filesystem API](/sandboxes/filesystem), and mount: - -```typescript -await sandbox.files.write("/run/gcp-sa.json", process.env.GCP_SA_JSON!); - -await sandbox.mounts.add({ - path: "/mnt/bucket", - driver: "command", - command: [ - "gcsfuse", "--foreground", "--implicit-dirs", - "-o", "allow_other", - "--key-file", "/run/gcp-sa.json", - "my-bucket", "{mountpoint}", - ], -}); - -await sandbox.exec.run("ls /mnt/bucket"); // GCS objects, read like local files -``` - -For a **public** bucket, skip the key and pass `--anonymous-access`: - -```typescript -command: ["gcsfuse", "--foreground", "--anonymous-access", "-o", "allow_other", - "gcp-public-data-landsat", "{mountpoint}"], -``` - -### Example: bindfs - -A minimal local FUSE — mirror one directory onto another path: - -```typescript -await sandbox.exec.run("apt-get update && apt-get install -y bindfs"); -await sandbox.mounts.add({ - path: "/mnt/mirror", - driver: "command", - command: ["bindfs", "-f", "-o", "allow_other", "/home/sandbox/data", "{mountpoint}"], - readOnly: false, -}); -// reads/writes under /mnt/mirror now pass through to /home/sandbox/data -``` - -### CLI - -Pass `--command` once per argv element; `--env`/`--secret` take `key=value`: - -```bash -oc mounts add sb-abc123 \ - --path /mnt/mirror \ - --command bindfs --command -f --command -o --command allow_other \ - --command /home/sandbox/data --command '{mountpoint}' \ - --secret MY_VFS_TOKEN=... -``` +To bring data into a v2 sandbox, copy it in with `files.write` or a signed upload URL, or have your code read it from object storage over the network. -If the mount doesn't come up, the error includes the **daemon's log tail** — -usually a missing binary, a bad flag, or an auth failure. Reproduce by hand with -`oc shell` and run the same command. +This page exists so the v2 docs mirror v1 page-for-page. See +[Migrating from v1](/migrating-from-v1) for the full list of removed and changed behaviour, +and [the v1 page](/sandboxes/mounts) for how this worked before. diff --git a/docs/sandboxes/patches.mdx b/docs/sandboxes/patches.mdx index f885d263a..1c5234fdd 100644 --- a/docs/sandboxes/patches.mdx +++ b/docs/sandboxes/patches.mdx @@ -1,144 +1,14 @@ --- -title: "Patches" -description: "Scripts that run when forking from a checkpoint" +title: "Checkpoint patches" +description: "Not available in v2" --- -A patch is a shell script attached to a checkpoint. Every time a sandbox is forked from that checkpoint, patches run automatically — inject configuration, update dependencies, or customize state without modifying the checkpoint itself. + +**Not available in v2.** The checkpoint patch API is not wired up in v2. + - +Capture a fresh [checkpoint](/sandboxes/checkpoints) instead of layering a patch onto an existing one. -```typescript TypeScript -import { Sandbox } from "@opencomputer/sdk"; - -// Attach a patch to an existing checkpoint -const result = await Sandbox.createCheckpointPatch(checkpointId, { - script: 'echo "export API_KEY=$1" >> /root/.bashrc', - description: "Inject API key at fork time", -}); -console.log(result.patch.id, result.patch.sequence); - -// Fork — patch runs automatically -const sandbox = await Sandbox.createFromCheckpoint(checkpointId); -``` - -```python Python -from opencomputer import Sandbox - -result = await Sandbox.create_checkpoint_patch( - checkpoint_id, - script='echo "export API_KEY=$1" >> /root/.bashrc', - description="Inject API key at fork time", -) - -sandbox = await Sandbox.create_from_checkpoint(checkpoint_id) -``` - - - -## API Reference - -### Create Patch - -Patch methods are **static** on the Sandbox class — they operate on checkpoints, not sandbox instances. - - - -```typescript TypeScript -const result = await Sandbox.createCheckpointPatch(checkpointId, { - script: "apt-get update && apt-get install -y redis-server", - description: "Install Redis", -}); -// result.patch.id, result.patch.sequence -``` - -```python Python -result = await Sandbox.create_checkpoint_patch( - checkpoint_id, - script="apt-get update && apt-get install -y redis-server", - description="Install Redis", -) -``` - - - -| Parameter | Type | Required | Description | -| --- | --- | --- | --- | -| `checkpointId` | string | Yes | Target checkpoint | -| `script` | string | Yes | Bash script to execute on fork | -| `description` | string | No | Human-readable description | - -### List Patches - - - -```typescript TypeScript -const patches = await Sandbox.listCheckpointPatches(checkpointId); -for (const p of patches) { - console.log(p.sequence, p.description); -} -``` - -```python Python -patches = await Sandbox.list_checkpoint_patches(checkpoint_id) -``` - - - -### Delete Patch - - - -```typescript TypeScript -await Sandbox.deleteCheckpointPatch(checkpointId, patchId); -``` - -```python Python -await Sandbox.delete_checkpoint_patch(checkpoint_id, patch_id) -``` - - - -## When Patches Run - -Patches execute when a sandbox is **forked** from the checkpoint (via `Sandbox.createFromCheckpoint()` or `oc checkpoint spawn`). The strategy is always `on_wake`. - -## Execution Order - -Patches run in creation order (by `sequence` number). If you create three patches, they execute as patch 1 → patch 2 → patch 3 every time a sandbox is forked. - -## PatchInfo - -| Field | Type | Description | -| --- | --- | --- | -| `id` | string | Patch UUID | -| `checkpointId` | string | Parent checkpoint | -| `script` | string | Bash script content | -| `description` | string | Human-readable description | -| `strategy` | string | Always `"on_wake"` | -| `sequence` | number | Execution order | -| `createdAt` | string | Timestamp | - -## Example: Environment-Specific Forks - -Use patches to create different environments from one checkpoint: - -```typescript -// Base checkpoint has the app installed -const cpId = "cp-abc123"; - -// Patch for staging -await Sandbox.createCheckpointPatch(cpId, { - script: ` - echo 'DATABASE_URL=postgres://staging-db/app' >> /app/.env - echo 'LOG_LEVEL=debug' >> /app/.env - `, - description: "Staging environment config", -}); - -// Every fork now gets staging config -const staging = await Sandbox.createFromCheckpoint(cpId); -``` - - - CLI equivalent: [`oc patch`](/cli/patch). Full reference: [TypeScript SDK](/reference/typescript-sdk#sandbox) · [Python SDK](/reference/python-sdk#sandbox) · [HTTP API](/reference/api#checkpoint-patches). - +This page exists so the v2 docs mirror v1 page-for-page. See +[Migrating from v1](/migrating-from-v1) for the full list of removed and changed behaviour, +and [the v1 page](/sandboxes/patches) for how this worked before. diff --git a/docs/sandboxes/secrets.mdx b/docs/sandboxes/secrets.mdx index 01c63de47..45251835a 100644 --- a/docs/sandboxes/secrets.mdx +++ b/docs/sandboxes/secrets.mdx @@ -1,322 +1,133 @@ --- title: "Secrets" -description: "Inject encrypted secrets into sandboxes without exposing them to the VM" +description: "What changed, what didn't, and what it means for your threat model" --- -Secrets let you pass API keys, tokens, and credentials into sandboxes without the real values ever entering the VM. They are encrypted at rest, sealed into opaque tokens at boot, and only revealed by a host-side proxy on outbound HTTPS requests. +**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) -```mermaid -sequenceDiagram - participant You as Your Code - participant S as Server - participant W as Worker - participant VM as Sandbox VM - participant P as MITM Proxy - participant API as External API +Put values in a secret store, then name the store when creating a sandbox: - You->>S: Store secret (sk-ant-...) - Note over S: Encrypted with AES-256-GCM
in Postgres +```typescript +const sandbox = await Sandbox.create({ secretStore: "prod-keys" }); - You->>S: Create sandbox with secret store - S->>S: Decrypt secrets - S->>W: Send decrypted secrets - - W->>W: Seal into opaque tokens - Note over W: sk-ant-... → osb_sealed_7f3a9c... - - W->>VM: Inject sealed env vars - Note over VM: $API_KEY = osb_sealed_7f3a9c...
Real secret NEVER in VM memory - - VM->>P: HTTPS request with sealed token - P->>P: Replace sealed token with real secret - P->>API: Request with real API key - API-->>VM: Response +const out = await sandbox.exec.run('echo "$MY_TOKEN"'); +console.log(out.stdout); // osb_sealed_3cda2f3ca0a0e0509a338cab83298f43 ``` - -The real secret value only exists in the host-side proxy's memory — it is never written to disk, never sent to the VM, and never visible via `env` or `/proc` inside the sandbox. - - -## Quick start - -Create a secret store, add a secret, and launch a sandbox that uses it: - - - -```typescript TypeScript -import { Sandbox, SecretStore } from '@opencomputer/sdk'; - -// 1. Create a secret store with egress restrictions -const store = await SecretStore.create({ - name: 'my-agent-secrets', - egressAllowlist: ['api.anthropic.com'], -}); - -// 2. Add an encrypted secret -await SecretStore.setSecret(store.id, 'ANTHROPIC_API_KEY', 'sk-ant-...'); - -// 3. Create a sandbox — secrets are injected as sealed tokens -const sandbox = await Sandbox.create({ - secretStore: 'my-agent-secrets', - timeout: 600, -}); +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 | v2 | +|---|---|---| +| 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. + + +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. + + +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. + + + +## Rotation -// Inside the VM, the env var is sealed — not the real key -const result = await sandbox.exec.run('echo $ANTHROPIC_API_KEY'); -console.log(result.stdout); // "osb_sealed_7f3a9c..." - -// But HTTPS requests to allowed hosts get the real value via the proxy -const apiResult = await sandbox.exec.run(` - curl -s https://api.anthropic.com/v1/messages \\ - -H "x-api-key: $ANTHROPIC_API_KEY" \\ - -H "anthropic-version: 2023-06-01" \\ - -H "content-type: application/json" \\ - -d '{"model":"claude-haiku-4-5-20251001","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}' -`); -console.log(apiResult.stdout); // 200 OK — real key was substituted by the proxy ``` - -```python Python -from opencomputer import Sandbox, SecretStore - -# 1. Create a secret store with egress restrictions -store = await SecretStore.create( - name='my-agent-secrets', - egress_allowlist=['api.anthropic.com'], -) - -# 2. Add an encrypted secret -await SecretStore.set_secret(store['id'], 'ANTHROPIC_API_KEY', 'sk-ant-...') - -# 3. Create a sandbox — secrets are injected as sealed tokens -sandbox = await Sandbox.create( - secret_store='my-agent-secrets', - timeout=600, -) - -# Inside the VM, the env var is sealed — not the real key -result = await sandbox.exec.run('echo $ANTHROPIC_API_KEY') -print(result.stdout) # "osb_sealed_7f3a9c..." - -# But HTTPS requests to allowed hosts get the real value via the proxy -api_result = await sandbox.exec.run( - 'curl -s https://api.anthropic.com/v1/messages ' - '-H "x-api-key: $ANTHROPIC_API_KEY" ' - '-H "anthropic-version: 2023-06-01" ' - '-H "content-type: application/json" ' - '-d \'{"model":"claude-haiku-4-5-20251001","max_tokens":10,"messages":[{"role":"user","content":"hi"}]}\'' -) -print(api_result.stdout) # 200 OK — real key was substituted by the proxy +PUT /api/secret-stores/{id}/secrets/{name} {"value": "new-value"} ``` - - -## Per-secret host restrictions - -You can restrict individual secrets so they are only substituted in requests to specific hosts. This prevents a compromised dependency from exfiltrating secrets to an attacker-controlled server. - - +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. -```typescript TypeScript -// This secret will only be substituted in requests to api.anthropic.com -await SecretStore.setSecret(store.id, 'ANTHROPIC_API_KEY', 'sk-ant-...', { - allowedHosts: ['api.anthropic.com'], -}); - -// This secret works on any allowed egress host -await SecretStore.setSecret(store.id, 'GENERIC_TOKEN', 'tok-...'); -``` - -```python Python -# This secret will only be substituted in requests to api.anthropic.com -await SecretStore.set_secret( - store['id'], 'ANTHROPIC_API_KEY', 'sk-ant-...', - allowed_hosts=['api.anthropic.com'], -) - -# This secret works on any allowed egress host -await SecretStore.set_secret(store['id'], 'GENERIC_TOKEN', 'tok-...') -``` - - + +If you use secret rotation, this is worth re-testing after you migrate. On earlier builds a +rotation against v2 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. + ## Egress allowlists -Secret stores can restrict which hosts the sandbox can make HTTPS requests to. Requests to hosts not on the list are blocked by the proxy. - - - -```typescript TypeScript -const store = await SecretStore.create({ - name: 'restricted-store', - egressAllowlist: ['api.anthropic.com', '*.openai.com'], -}); -``` - -```python Python -store = await SecretStore.create( - name='restricted-store', - egress_allowlist=['api.anthropic.com', '*.openai.com'], -) -``` - - - -Supports exact matches (`api.anthropic.com`) and wildcards (`*.openai.com`). An empty allowlist means all hosts are allowed. - -## Managing secrets - - - -```typescript TypeScript -// List all secret stores -const stores = await SecretStore.list(); - -// List secrets in a store (metadata only — values are never returned) -const entries = await SecretStore.listSecrets(store.id); - -// Delete a secret -await SecretStore.deleteSecret(store.id, 'OLD_KEY'); - -// Delete a store and all its secrets -await SecretStore.delete(store.id); -``` - -```python Python -# List all secret stores -stores = await SecretStore.list() - -# List secrets in a store (metadata only — values are never returned) -entries = await SecretStore.list_secrets(store['id']) - -# Delete a secret -await SecretStore.delete_secret(store['id'], 'OLD_KEY') - -# Delete a store and all its secrets -await SecretStore.delete(store['id']) -``` - - - -## Secrets with snapshots and checkpoints - -You can attach a secret store when creating a sandbox from a snapshot or checkpoint, even if the original didn't have one. This is useful for baking a base environment (e.g., installed dependencies) and then giving each fork its own scoped credentials. - -### Snapshot template with secrets - -Pre-build a snapshot once, then create sandboxes from it with different credentials: - - - -```typescript TypeScript -import { Sandbox, Snapshots, Image } from '@opencomputer/sdk/node'; - -// Pre-build a reusable snapshot -const snapshots = new Snapshots(); -await snapshots.create({ - name: 'data-pipeline', - image: Image.base().aptInstall(['python3-pip']).pipInstall(['pandas']), -}); - -// Create sandboxes from the snapshot, each with their own credentials -const worker1 = await Sandbox.create({ - snapshot: 'data-pipeline', - secretStore: 'worker-1-keys', -}); -const worker2 = await Sandbox.create({ - snapshot: 'data-pipeline', - secretStore: 'worker-2-keys', -}); -``` - -```python Python -from opencomputer import Sandbox, Snapshots, Image - -# Pre-build a reusable snapshot -snapshots = Snapshots() -await snapshots.create( - name='data-pipeline', - image=Image.base().apt_install(['python3-pip']).pip_install(['pandas']), -) - -# Create sandboxes from the snapshot, each with their own credentials -worker1 = await Sandbox.create( - snapshot='data-pipeline', - secret_store='worker-1-keys', -) -worker2 = await Sandbox.create( - snapshot='data-pipeline', - secret_store='worker-2-keys', -) -``` - - - -### Checkpoint fork with secrets - -Layer a new secret store on top of an existing checkpoint's store: - - - -```typescript TypeScript -// Bake a base snapshot with git credentials -const base = await Sandbox.create({ secretStore: 'git-creds' }); -await base.exec.run('git clone https://github.com/org/repo /app'); -const cp = await base.createCheckpoint('repo-cloned'); - -// Fork with sandbox-specific API credentials (layered on top of git-creds) -const worker1 = await Sandbox.createFromCheckpoint(cp.id, { - secretStore: 'worker-1-keys', -}); -const worker2 = await Sandbox.createFromCheckpoint(cp.id, { - secretStore: 'worker-2-keys', -}); -``` - -```python Python -# Bake a base snapshot with git credentials -base = await Sandbox.create(secret_store='git-creds') -await base.exec.run('git clone https://github.com/org/repo /app') -cp = await base.create_checkpoint('repo-cloned') - -# Fork with sandbox-specific API credentials (layered on top of git-creds) -worker1 = await Sandbox.create_from_checkpoint(cp['id'], - secret_store='worker-1-keys', -) -worker2 = await Sandbox.create_from_checkpoint(cp['id'], - secret_store='worker-2-keys', -) -``` - - - -### Layering rules - -When a checkpoint already has a secret store and you attach another at fork time, the stores are **merged**: - -- **Secrets**: Both stores' secrets are available. On name collision, the fork's store wins. -- **Egress allowlists**: Aggregated (union of both stores' lists). -- **Per-secret host restrictions**: Follow the winning secret's store. - -This means a base snapshot can provide broad credentials (e.g., git access) while each fork adds its own scoped credentials (e.g., limited API keys). - -On a fork-of-fork, the checkpoint's merged result becomes the new base — there's no unbounded chain of stores to resolve. - -## Security properties - -| Property | Detail | -| --- | --- | -| **Encryption at rest** | AES-256-GCM in Postgres, key via `OPENSANDBOX_SECRET_ENCRYPTION_KEY` | -| **Never in VM memory** | Env vars contain opaque `osb_sealed_*` tokens | -| **Host-side only** | Real values exist only in the MITM proxy process on the worker host | -| **Egress control** | Allowlists restrict which domains receive secrets | -| **Per-secret scoping** | Individual secrets can be locked to specific hosts | -| **Values never returned** | The API only returns secret names and metadata, never values | - -## Next steps +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. -- [CLI reference](/cli/secrets) — manage secret stores and secrets from the command line -- [TypeScript SDK reference](/reference/typescript-sdk#secret-stores) — full API reference -- [Python SDK reference](/reference/python-sdk#secret-stores) — full API reference + + Isolation boundaries, egress, and the full reasoning behind the proxy's placement. + diff --git a/docs/sandboxes/sizes.mdx b/docs/sandboxes/sizes.mdx new file mode 100644 index 000000000..25d7a11a7 --- /dev/null +++ b/docs/sandboxes/sizes.mdx @@ -0,0 +1,109 @@ +--- +title: "Sandbox sizes" +description: "Which memory sizes exist, which are available to you, and what happens when one isn't" +--- + +On v2 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. + +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 + +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 | On the current runtime | On v2 | +|---|---|---| +| 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 | + + +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. + + +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](/migrating-from-v1). + +## 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. +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: 8192MB was requested; this region offers 4096 MB", + "hint": "request one of the listed sizes, or contact support to have another published" +} +``` + + +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. + + +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 + +If a size cannot be served, the create fails. It is never quietly served from the default image. + +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 `setAutoscale()` return `501`. Memory belongs to the image, so a running sandbox +cannot be resized. + + +If you size sandboxes dynamically today, that decision has to move to create time. See +[What's not supported](/migrating-from-v1). diff --git a/docs/sandboxes/templates.mdx b/docs/sandboxes/templates.mdx index 803e2b483..3ee466472 100644 --- a/docs/sandboxes/templates.mdx +++ b/docs/sandboxes/templates.mdx @@ -1,340 +1,72 @@ --- title: "Templates" -description: "Define sandbox environments programmatically with the Image builder and Snapshots" +description: "Which templates carry over, and which do not" --- -Templates are an alpha, experimental feature. APIs, behavior, and manifest formats may change without notice. +Templates work on v2, 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. -Templates provide a code-first approach to defining sandbox environments. Instead of configuring images manually, you define them programmatically using the SDK. +## How a template is applied here -The system supports two workflows: +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. -1. **Declarative images** — build images with varying dependencies _on demand_ when creating sandboxes -2. **Pre-built snapshots** — create and register _ready-to-use_ snapshots that can be shared across multiple sandboxes - -## Declarative image building - -Build images on-the-fly when creating sandboxes. Ideal for iterating quickly without creating separate snapshots. - -Declarative images are **cached by content hash** — identical manifests produce the same image. Subsequent runs reuse the cached image instantly. - - - -```typescript TypeScript -import { Sandbox } from '@opencomputer/sdk'; -import { Image } from '@opencomputer/sdk/node'; - -// Define an image with packages, env vars, and files -const image = Image.base() - .aptInstall(['curl', 'jq']) - .pipInstall(['requests', 'pandas']) - .env({ PROJECT_ROOT: '/workspace' }) - .workdir('/workspace'); - -// Create a sandbox — the image is built on first run, cached after -const sandbox = await Sandbox.create({ - image, - timeout: 300, - onBuildLog: (log) => console.log(`build: ${log}`), -}); - -const result = await sandbox.exec.run('which curl'); -console.log(result.exitCode); // 0 -``` - -```python Python -from opencomputer import Sandbox, Image - -# Define an image with packages, env vars, and files -image = ( - Image.base() - .apt_install(["curl", "jq"]) - .pip_install(["requests", "pandas"]) - .env({"PROJECT_ROOT": "/workspace"}) - .workdir("/workspace") -) - -# Create a sandbox — the image is built on first run, cached after -sandbox = await Sandbox.create( - image=image, - timeout=300, - on_build_log=lambda log: print(f"build: {log}"), -) - -result = await sandbox.exec.run("which curl") -print(result.exit_code) # 0 -``` - -```bash curl -curl -X POST https://app.opencomputer.dev/api/sandboxes \ - -H "X-API-Key: $OPENCOMPUTER_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "image": { - "steps": [ - {"type": "apt_install", "packages": ["curl", "jq"]}, - {"type": "pip_install", "packages": ["requests", "pandas"]}, - {"type": "env", "vars": {"PROJECT_ROOT": "/workspace"}}, - {"type": "workdir", "path": "/workspace"} - ] - }, - "timeout": 300 - }' -``` - - - -When you pass an `image` to `Sandbox.create()`, the server: - -1. Hashes the image manifest to compute a **cache key** -2. If cached, creates the sandbox from the existing checkpoint instantly -3. If not cached, boots a build sandbox, executes each step, checkpoints the result, then creates your sandbox from it - -### Build memory - -Images build in a **4 GB** sandbox by default. If a build runs out of memory (heavy `apt`/`pip`/`npm`, compiling a large toolchain), raise the build-phase RAM with **`.builderMemory(mb)` / `.builder_memory(mb)`**. - -This only affects the build. The resulting image is unchanged — you size the actual sandbox when you create it, via `memoryMB`: - - - -```typescript TypeScript -// 8 GB to build… -const image = Image.base() - .aptInstall(['build-essential', 'cmake']) - .runCommands('make -j') - .builderMemory(8192); - -// …but the sandbox runs at whatever you ask for -const sandbox = await Sandbox.create({ image, memoryMB: 4096 }); -``` - -```python Python -image = ( - Image.base() - .apt_install(["build-essential", "cmake"]) - .run_commands("make -j") - .builder_memory(8192) -) -sandbox = await Sandbox.create(image=image) # size via the HTTP API's memoryMB -``` - - - -`builderMemory` doesn't change the cache key — it's a build resource, not image content. - -## Creating pre-built snapshots - -Create named snapshots that persist permanently and can be shared across sandboxes. Snapshots are visible in the dashboard and don't need to be rebuilt. - - - -```typescript TypeScript -import { Image, Snapshots } from '@opencomputer/sdk/node'; - -const snapshots = new Snapshots(); - -// Define the image -const image = Image.base() - .aptInstall(['python3-pip']) - .pipInstall(['pandas', 'numpy', 'scikit-learn']) - .workdir('/workspace'); - -// Create a named snapshot with build log streaming -await snapshots.create({ - name: 'data-science', - image, - onBuildLogs: (log) => console.log(`build: ${log}`), -}); - -// Now create sandboxes from the snapshot — instant, no build step -const sandbox = await Sandbox.create({ snapshot: 'data-science' }); -``` - -```python Python -from opencomputer import Image, Snapshots - -snapshots = Snapshots() - -# Define the image -image = ( - Image.base() - .apt_install(["python3-pip"]) - .pip_install(["pandas", "numpy", "scikit-learn"]) - .workdir("/workspace") -) - -# Create a named snapshot with build log streaming -await snapshots.create( - name="data-science", - image=image, - on_build_logs=lambda log: print(f"build: {log}"), -) - -# Now create sandboxes from the snapshot — instant, no build step -sandbox = await Sandbox.create(snapshot="data-science") -``` - -```bash curl -# Create a named snapshot -curl -X POST https://app.opencomputer.dev/api/snapshots \ - -H "X-API-Key: $OPENCOMPUTER_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "data-science", - "image": { - "steps": [ - {"type": "apt_install", "packages": ["python3-pip"]}, - {"type": "pip_install", "packages": ["pandas", "numpy", "scikit-learn"]}, - {"type": "workdir", "path": "/workspace"} - ] - } - }' - -# Create sandbox from the snapshot — instant -curl -X POST https://app.opencomputer.dev/api/sandboxes \ - -H "X-API-Key: $OPENCOMPUTER_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"snapshot": "data-science"}' -``` - - - -### Sizing a fork's memory - -A sandbox created from a snapshot inherits the snapshot's memory by default. Pass `memoryMB` to give it more RAM — useful when one branch needs more headroom than the base. - - - -```typescript TypeScript -// Fork the snapshot with 8 GB instead of its captured size -const sandbox = await Sandbox.create({ snapshot: 'data-science', memoryMB: 8192 }); -``` - -```python Python -# Fork the snapshot with 8 GB instead of its captured size -sandbox = await Sandbox.create(snapshot="data-science", memory_mb=8192) -``` - -```bash curl -curl -X POST https://app.opencomputer.dev/api/sandboxes \ - -H "X-API-Key: $OPENCOMPUTER_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{"snapshot": "data-science", "memoryMB": 8192}' -``` - - - -`memoryMB` is clamped to a valid range, and the response's `memoryMB` reports the effective value: - -- **Floor — the snapshot's own memory.** A smaller request is ignored; a fork can't start smaller than the snapshot it restores. A 4 GB snapshot forked with `memoryMB: 1024` still boots at ~4 GB. -- **Ceiling.** Larger requests are capped to the maximum platform tier. - -The same `memoryMB` field works when [forking from a checkpoint](/api-reference/checkpoints/fork). - -### Managing snapshots - - - -```typescript TypeScript -const snapshots = new Snapshots(); - -// List all snapshots -const list = await snapshots.list(); -for (const s of list) { - console.log(`${s.name} — ${s.status}`); -} - -// Get a specific snapshot -const snapshot = await snapshots.get('data-science'); -console.log(snapshot.status); // "ready" - -// Delete a snapshot -await snapshots.delete('data-science'); +```typescript +const sandbox = await Sandbox.create({ templateID: "my-template" }); ``` -```python Python -snapshots = Snapshots() - -# List all snapshots -snapshot_list = await snapshots.list() -for s in snapshot_list: - print(f"{s['name']} — {s['status']}") - -# Get a specific snapshot -snapshot = await snapshots.get("data-science") -print(snapshot["status"]) # "ready" - -# Delete a snapshot -await snapshots.delete("data-science") -``` - -```bash curl -# List all snapshots -curl https://app.opencomputer.dev/api/snapshots \ - -H "X-API-Key: $OPENCOMPUTER_API_KEY" - -# Get a specific snapshot -curl https://app.opencomputer.dev/api/snapshots/data-science \ - -H "X-API-Key: $OPENCOMPUTER_API_KEY" - -# Delete a snapshot -curl -X DELETE https://app.opencomputer.dev/api/snapshots/data-science \ - -H "X-API-Key: $OPENCOMPUTER_API_KEY" -``` - - - ---- - -## Default template - -Sandboxes use the default template when no image or snapshot is specified. It includes: - -- **Ubuntu 22.04** -- **Python 3** with pip, venv, setuptools -- **Node.js 20 LTS** with npm -- **Build tools:** build-essential, cmake, pkg-config -- **CLI tools:** git, git-lfs, curl, wget, jq, rsync, htop, tree -- **Editors:** nano, vim-tiny -- **Database:** sqlite3 -- **Networking:** openssh-client, iproute2, net-tools, dnsutils -- **Claude Agent SDK** and claude-code (pre-installed for agent sessions) - ---- - -## Image configuration - -The `Image` class provides a fluent, immutable API for defining sandbox environments. Each method returns a **new** `Image` instance — the original is never modified. - -| Method | Description | -| --- | --- | -| `Image.base()` | Start from the default OpenSandbox environment | -| `.aptInstall(packages)` / `.apt_install(packages)` | Install system packages via apt-get | -| `.pipInstall(packages)` / `.pip_install(packages)` | Install Python packages via pip | -| `.runCommands(...cmds)` / `.run_commands(*cmds)` | Run shell commands during build | -| `.env(vars)` | Set environment variables (written to `/etc/environment`) | -| `.workdir(path)` | Set default working directory | -| `.addFile(path, content)` / `.add_file(path, content)` | Embed a file with inline content | -| `.addLocalFile(local, remote)` / `.add_local_file(local, remote)` | Read a local file into the image | -| `.addLocalDir(local, remote)` / `.add_local_dir(local, remote)` | Read a local directory into the image | -| `.builderMemory(mb)` / `.builder_memory(mb)` | RAM for the build phase (default 4 GB; doesn't affect the resulting sandbox) | -| `.toJSON()` / `.to_dict()` | Return the image manifest | -| `.cacheKey()` / `.cache_key()` | Compute SHA-256 content hash | - -## SnapshotInfo - -| Field | Type | Description | -| --- | --- | --- | -| `id` | string | Unique snapshot identifier | -| `name` | string | Snapshot name | -| `status` | string | `"building"`, `"ready"`, or `"failed"` | -| `contentHash` | string | SHA-256 hash of the image manifest | -| `checkpointId` | string | Linked checkpoint ID | -| `manifest` | object | The declarative image manifest | -| `createdAt` | string | ISO 8601 creation timestamp | -| `lastUsedAt` | string | ISO 8601 last usage timestamp | - - - Full SDK reference: [TypeScript SDK](/sdks/typescript/templates) · [Python SDK](/sdks/python/templates) · [HTTP API](/reference/api#snapshots). - +## The catch: rootfs templates are refused + + +A template that carries a **rootfs image** cannot be used on v2. The create fails with +*"template … carries a rootfs image, which v2 cannot restore."* + + +Templates built on v1 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 to do + + + + 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](/sandboxes/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 + + +Declarative image manifests (`image:` on create) and the image build pipeline are **not +supported** on v2. They depend on a build fleet it does not have. + + +If you build images today, that workflow needs an alternative before you migrate: + + + + 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 + environment again. + + diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 000000000..ea1483a82 --- /dev/null +++ b/docs/style.css @@ -0,0 +1,14 @@ +/* Pre-cutover notice for the v2 docs. + * + * The banner takes its background from `bg-primary-dark` (colors.dark, a + * near-black), so it can only be recoloured here — changing colors.dark would + * repaint the whole site. Text is already forced white by the banner's own + * utilities, so only the background needs overriding. + * + * Violet-600 rather than a pastel: white on a genuinely light purple lands + * around 3:1 and fails WCAG AA for 14px text. This reads purple and clears AA + * at ~5.7:1. + */ +#banner { + background-color: #7c3aed !important; +}