sandboxes
A sandbox is a hosted VPS instance with a template and a lifetime: `create` admits it after one IAM decision, then claims a warm instance of its template or creates one through the instance lifecycle, and the `vps.sandboxes` reconciler, which runs every 15 seconds, deletes it once `expiresAt` passes. A sandbox's `state` follows its instance: `starting`, `ready` while the instance runs, `failed` when it is in `error` or `lost`, `deleting` once deletion was asked, and `deleted` when the instance is gone. The instance belongs to the sandbox: `instances.start`, `stop`, `resize` and `delete` refuse it with `INVALID_STATE`, and `instances.get` still returns it. See [Sandboxes](/docs/guides/vps/sandboxes).
A sandbox is a hosted VPS instance with a template and a lifetime: create admits it after one IAM decision, then claims a warm instance of its template or creates one through the instance lifecycle, and the vps.sandboxes reconciler, which runs every 15 seconds, deletes it once expiresAt passes. A sandbox's state follows its instance: starting, ready while the instance runs, failed when it is in error or lost, deleting once deletion was asked, and deleted when the instance is gone. The instance belongs to the sandbox: instances.start, stop, resize and delete refuse it with INVALID_STATE, and instances.get still returns it. See Sandboxes.
All 7 methods are served by this release.
| Method | Kind | Authorization | Phase | Status |
|---|---|---|---|---|
create | mutation | compute:sandboxes:create @ compute-sandbox | v0.5 | registered |
list | read | compute:sandboxes:read @ compute-tenant + items | v0.5 | registered |
get | read | compute:sandboxes:read @ compute-sandbox | v0.5 | registered |
extend | mutation | compute:sandboxes:update @ compute-sandbox | v0.5 | registered |
snapshot | mutation | compute:sandboxes:update @ compute-sandbox | v0.5 | registered |
exec | mutation | compute:sandboxes:exec @ compute-sandbox | v0.5 | registered |
delete | mutation | compute:sandboxes:delete @ compute-sandbox | v0.5 | registered |
create
POST /api/compute/sandboxes/create
Starts a sandbox from a template after one IAM decision on the draft, claiming a warm instance when the template holds one and creating a new instance otherwise; the lifetime starts at once.
- Idempotency.
idempotencyKeyis required. The sandbox id derives from the tenant, the caller's identity and the key: a retry of an admitted request returns the stored sandbox without a second decision, and a different request under the same key isCONFLICTwithdetails.sandboxId, as is the key replayed by another agent or person.waitis not part of the compared request. - IAM.
compute:sandboxes:createis decided oncompute-sandbox/plus the draft id, whose parent is the template's pool. Policies can testtemplateId,vcpu,memoryGiB,gpus,ttlSecondsandwarm(true when the template keeps a warm pool). A refusal deletes the draft; a delegated session can getCONFIRMATION_REQUIRED. After an allow, the pool's budget check can answerSPEND_LIMIT_REACHEDand keep the draft for a retry with the same key. - Start. The oldest stopped or paused warm instance of the template is claimed: it changes owner to the caller, is set to run, and publishes
instance.starting; the claim places nothing new. Without one, a new instance is created and placed asinstances.createwould, publishinginstance.createdand, once placed,instance.scheduled; when no host fits it waits in scheduling and the sandbox staysstarting.warmin the answer tells which path was taken. - Restore.
fromSnapshotstarts the sandbox from one of the tenant's ownreadysnapshots of this template, taken on its runtime (another tenant's isNOT_FOUND, one not ready isINVALID_STATE, one of another template isINVALID_INPUT). An agent restores only snapshots of sandboxes it created itself; any other isNOT_FOUNDto it. A restore never claims a warm instance, and a snapshot that includes memory places only on nodes with the samecpu.model. Policies see it as thefromSnapshotattribute. - Lifetime.
ttlSecondsdefaults to the template'sdefaultTtlSecondsand may not exceed itsmaxTtlSeconds(INVALID_INPUT).expiresAtis set when the sandbox is admitted. - Errors.
NOT_FOUNDfor a template missing or deleted in the tenant.INVALID_STATEwhen the template's flavor has been archived or removed. On the new-instance path the template's pool, flavor and image are checked again, with the errors ofsandboxTemplates.create;QUOTA_EXCEEDEDwhen the shape does not fit the tenant's quota andCAPACITY_UNAVAILABLEwhen the pool has no free IP address, both keeping the draft. - Waiting.
wait.timeoutMs(at most 30 seconds) holds the answer until the sandbox leavesstarting; a timeout returns it stillstarting, not an error.
Publishes sandbox.created with warm and expiresAt. sandbox.ready follows from the reconciler once the instance first runs.
- Kind: mutation
- Authorization:
compute:sandboxes:create@compute-sandbox - Module:
vps - Phase: v0.5 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
templateId | string | yes | at most 57 characters; matches ^sbxt_[0-9a-hjkmnp-tv-z]{26,52}$ |
ttlSeconds | integer | 60-2592000 | |
fromSnapshot | string | at most 57 characters; matches ^snap_[0-9a-hjkmnp-tv-z]{26,52}$ | |
idempotencyKey | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
wait | object | ||
wait.timeoutMs | integer | yes | 0-30000 |
list
POST /api/compute/sandboxes/list
Returns a page of the tenant's sandboxes, newest first, with a cursor when more remain.
scope defaults to mine, the caller's own sandboxes, which needs no per-item check. all lists every sandbox of the tenant, each one checked for compute:sandboxes:read; those the caller may not read are left out, so a page can hold fewer than limit items. Deleted sandboxes are left out unless deleted is true, and templateId keeps one template's sandboxes. limit is 1 to 200 (default 50); cursor is the last sandbox id of the previous page. Drafts are never listed. Each item carries identityRebound as get describes.
- Kind: read
- Authorization:
compute:sandboxes:read@compute-tenant+ items - Module:
vps - Phase: v0.5 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
scope | one of mine, all | default "mine" | |
deleted | boolean | default false | |
templateId | string | at most 57 characters; matches ^sbxt_[0-9a-hjkmnp-tv-z]{26,52}$ | |
limit | integer | 1-200 | |
cursor | string | at most 56 characters; matches ^sbx_[0-9a-hjkmnp-tv-z]{26,52}$ |
get
POST /api/compute/sandboxes/get
Returns one sandbox of the tenant with its state, its instance's state, addresses and node, and its expiresAt.
readyAt is set once the reconciler saw the instance run, and expiredAt when the sandbox ended by reaching expiresAt. snapshots lists the sandbox's snapshots with their state. Drafts and sandboxes of other tenants are NOT_FOUND.
identityRebound appears only on a memory restore that was given its own guest identity. It is false until the host has installed that identity; until then the guest still answers to its source's identity, and exec is refused with restore-shares-identity while another guest shares it. It is true once the identity is installed, from which point exec signs for this sandbox alone. It is absent when there is nothing to install: the sandbox is not a memory restore, or exec is not configured.
- Kind: read
- Authorization:
compute:sandboxes:read@compute-sandbox - Module:
vps - Phase: v0.5 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
sandboxId | string | yes | at most 56 characters; matches ^sbx_[0-9a-hjkmnp-tv-z]{26,52}$ |
extend
POST /api/compute/sandboxes/extend
Moves a sandbox's expiresAt later after one IAM decision, within its template's maxTtlSeconds counted from creation; returns the sandbox.
- Give exactly one of
until(the new expiry, epoch milliseconds) orby(added to the current expiry:30m,8h,7d, or milliseconds, at most 30 days); both or neither isINVALID_INPUT. A lifetime past the template'smaxTtlSecondsisINVALID_INPUT, and a sandbox whose deletion has started, bydeleteor by expiry, isINVALID_STATE. - IAM.
compute:sandboxes:updateis decided on the sandbox with a pendingttlSecondsequal to the whole lifetime after the change, so a policy that capsttlSecondsoncreatealso caps extensions. A refusal isACCESS_DENIEDand clears the pending change. - Idempotency. An
untilthat is not later than the current expiry returns the sandbox unchanged without a decision, so a retrieduntilis safe. There is noidempotencyKey: a repeatedbyextends again. - While another extend of the same sandbox is being decided, the call is
CONFLICTwithdetails.reason: 'extend-in-flight'andretryAfterSeconds. A spent budget isSPEND_LIMIT_REACHED.
The stored ttlSeconds becomes the new whole lifetime. Publishes sandbox.extended with the new expiresAt.
- Kind: mutation
- Authorization:
compute:sandboxes:update@compute-sandbox - Module:
vps - Phase: v0.5 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
sandboxId | string | yes | at most 56 characters; matches ^sbx_[0-9a-hjkmnp-tv-z]{26,52}$ |
until | integer | 0-140737488355328 | |
by | string,integer |
snapshot
POST /api/compute/sandboxes/snapshot
Saves a running or paused sandbox's state (disk, and memory unless includeMemory is false) as a tenant-private snapshot; returns it pending until the host's instance.snapshot command settles.
-
Where. The deployment's snapshot store (
vps({ snapshots })) presigns an https upload URL, and the host uploads straight to it. Without a store the call isINVALID_CONFIG. A sandbox that is not running or paused, or whose deletion has started, isINVALID_STATE. -
Settling. The
vps.sandboxesreconciler, woken when the command settles, makes the snapshotready(with its sha256, size, runtime and CPU model) and publishessandbox.snapshot.ready, or makes itfailedand publishessandbox.snapshot.failed. -
Idempotency. With
idempotencyKey, a retry returns the same snapshot; the key used for another sandbox isCONFLICT. Without one, every call takes a new snapshot. -
Lifetime. A snapshot is deleted with its sandbox unless
keepis true. A tenant teardown deletes all of them, kept ones included. Snapshots are never promoted to shared images. -
Kind: mutation
-
Authorization:
compute:sandboxes:update@compute-sandbox -
Module:
vps -
Phase: v0.5 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
sandboxId | string | yes | at most 56 characters; matches ^sbx_[0-9a-hjkmnp-tv-z]{26,52}$ |
includeMemory | boolean | default true | |
keep | boolean | default false | |
idempotencyKey | string | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
exec
POST /api/compute/sandboxes/exec
Opens SSH into a running sandbox: returns a single-use ssh tunnel ticket for its instance and a short OpenSSH user certificate from the deployment's sandbox SSH CA for the principal <account>@<instanceId>.
- How. A sandbox has no in-guest agent: the node's daemon dials the instance's own address, and the instance's sshd trusts the sandbox CA and accepts only its own principal, so a certificate for one sandbox opens no other. Open
GETon the returnedpathwith the ticket beforeexpiresAt, then speak SSH over it.better-compute sandbox execdoes both. - Host key. The answer tells whether the guest can be pinned.
hostCertificateisvalidwhen the guest presents a host certificate from the sandbox CA, still good for at least a minute (then pin it with@cert-authorityforhostPrincipalandHostKeyAlias). It isexpiredwhen that certificate ended, andnonewhen the instance was never certified.hostCertifiedis true only whenvalid. - Certificate.
publicKeymust be an Ed25519 or ECDSA P-256 key.ttlSecondsis 60 to 900 (default 600) and never past the caller's session. Guests receive no revocation list, so the short lifetime is the revocation control, and tunnel re-authorization closes the transport. - Errors.
UNSUPPORTEDwhen the deployment has no sandbox SSH CA (tunnels.sandboxSsh).ACCESS_DENIEDfor an API key (reasonsession-required) or a session ending within a minute.INVALID_STATEwhen the sandbox is not running or is being deleted, and withdetails.reason: 'restore-shares-identity'while another running guest shares its SSH identity (a memory restore of the same snapshot, or the sandbox it was taken from): a certificate for one would open the others, so exec waits until only one of them runs. The node must be connected and advertisetunnel.instance-dial. Exec shares the per-identity tunnel rate limit withtunnels.open(RATE_LIMITED).
Hosted by the tunnels module. Publishes tunnel.issued.
- Kind: mutation
- Authorization:
compute:sandboxes:exec@compute-sandbox - Module:
tunnels - Phase: v0.5 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
sandboxId | string | yes | at most 56 characters; matches ^sbx_[0-9a-z]{26,52}$ |
publicKey | string | yes | at most 4096 characters |
ttlSeconds | integer | 60-900; default 600 |
delete
POST /api/compute/sandboxes/delete
Asks for the sandbox's instance to be deleted and marks the sandbox deleting; returns the sandbox at once.
The instance's desiredState becomes deleted and the vps.instances reconciler releases its allocation, IP lease and quota as for any instance, publishing instance.deleting with the sandboxId. The call is idempotent: a sandbox already deleting is returned in its current state. Drafts and missing sandboxes are NOT_FOUND.
When the instance is gone, the vps.sandboxes reconciler marks the sandbox deleted, removes it from the authorization projection and publishes sandbox.deleted with expired: false. Expiry takes the same path after publishing sandbox.expired, and its sandbox.deleted carries expired: true.
- Kind: mutation
- Authorization:
compute:sandboxes:delete@compute-sandbox - Module:
vps - Phase: v0.5 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
sandboxId | string | yes | at most 56 characters; matches ^sbx_[0-9a-hjkmnp-tv-z]{26,52}$ |
Better Compute is created by Sean Filimon
Last updated
sandboxVolumes
Sandbox volumes are persistent disks on one host that a tenant's sandboxes attach across runs, so an agent keeps its memory, caches and checkouts. A volume is placed when it is created: its size is booked as `diskGiB` on one host of the pool, charged to the tenant's quota, and the host keeps it as a sparse raw file. A sandbox template names volumes with a mount path. Sandboxes of that template then run on the volume's host only, attach it as an extra virtio disk, and mount it by serial (`/dev/disk/by-id/virtio-<serial>`) from their user data. The first mount formats it as ext4, and no later one ever does. A volume takes any number of read-only attachments or one read-write attachment, never both. Volumes are metered as `vps.volume.gib.hours` from creation until the disk is released. See [Sandboxes](/docs/guides/vps/sandboxes).
secrets
Secrets are write-only: `put` hands a value to the configured secret store and returns its `secret://` reference and version, and no method ever reads a value back. Events and audit rows carry only the reference, name and version; values reach agents through the secret broker, never through the API. Methods are decided on `compute-tenant/{tenantId}`, or on `compute-fleet/global` for platform secrets with `scope: 'fleet'` in the root tenant. See [Security](/docs/operations/security) and [BMC credentials](/docs/guides/bare-metal/bmc).