Capability: runtime

Implement a sandbox runtime — boot a box that runs Spunto's agent, keep its /workspace across stops, and say what state it is in.

runtime is where sandboxes run. Spunto Cloud, Google Cloud and the machines an organization brings itself are each a runtime app; so is anything you implement here. A member picks among the runtimes their organization installed when they create a sandbox.

The contract is small on purpose. A provider owns the lifecycle of a box: boot it, stop it, start it again, delete it, and say what state it's in.

It does not carry the terminal, command execution, the IDE or port forwarding. Every box runs Spunto's agent, which dials back to the platform over an outbound connection and carries all of that itself — whatever the provider. Your boxes need no inbound port open to the internet.

What a box must do:

  1. Run the image it is given, with the environment it is given.
  2. Run the bootstrap command as its main process. It fetches and starts the agent (curl or wget, and sh, must be in the image).
  3. Reach SPUNTO_API_URL over HTTPS, outbound.
  4. Keep /workspace across stop and start.

OperationRequiredUnlocks
createYesCreating a sandbox
startYesStarting a stopped sandbox
stopYesStopping one, /workspace kept
destroyYesDeleting one
describeYesKnowing its state
listMachineTypesA size picker, with prices
listSizesA simpler size picker, when there is one kind of machine
listLocationsChoosing where a box runs (a region, a machine)
featuresTelling members what a box here can and can't do
pausePause: a stop that keeps the box's memory, processes included
forkFork: a copy of a running box, memory included
growGrowing a box's disks
logs, build, buildStatus, snapshot…Starting boxes from a prebuilt project image instead of building in place

Every operation's input carries a handle — what create returned — except create itself and the listings.

// input
{
  "sandboxId": "sbx_8f2c…",
  "name": "fix-login",
  "image": "mcr.microsoft.com/devcontainers/typescript-node:22",
  "size": "small",
  "machineType": "small",
  "location": "europe-west1-b",
  "needs": { "dind": false },
  "volumes": [{ "name": "workspace", "path": "/workspace" }],
  "bootstrap": "if command -v curl …; fi",
  "env": {
    "SPUNTO_API_URL": "https://spunto.net",
    "SPUNTO_SANDBOX_ID": "sbx_8f2c…",
    "SPUNTO_SANDBOX_TOKEN": "…"
  },
  "labels": {
    "spunto.sandboxId": "sbx_8f2c…",
    "spunto.organizationId": "org_…",
    "spunto.projectId": "prj_…",
    "spunto.userId": "usr_…"
  }
}
 
// result
{ "handle": "vm-8f2c" }
  • Answer within 10 seconds. Accept the request, start booting in the background, and return the handle. The agent's connection is how Spunto learns the box is up; describe is how it learns anything else.
  • Be idempotent on sandboxId. If create is called for a sandbox you already have a box for, return that box's handle. Spunto retries create when your app was unreachable or timed out, and a box booted twice is a box billed twice.
  • env contains a secret (SPUNTO_SANDBOX_TOKEN): the box's identity towards the platform. Pass it to the box and nowhere else — not in logs, not in labels.
  • labels are ids and links, never secrets: keep them with the box (instance labels, container labels) for whoever operates the machines. Ignore them if you have nowhere to put them.
  • size, machineType and location are ids your app gave in its own listings. machineType wins over size.
  • needs.dind: the project needs Docker inside the box. Refuse if you can't provide it rather than boot a box that will fail later.
  • Optional in the result: idePort, if the IDE cannot listen on 8080 in your boxes.

// input
{ "handle": "vm-8f2c", "sandboxId": "sbx_8f2c…" }
 
// result
{ "ok": true }
  • stop keeps /workspace and nothing else: the next start boots the box again. Billing should stop with it.
  • stop may answer before the box is fully down — describe then says "transition": "stopping" until it is.
  • destroy deletes the box and its storage, and is idempotent: destroying a box that is already gone succeeds.

// input
{ "handle": "vm-8f2c", "sandboxId": "sbx_8f2c…" }
 
// result
{ "state": "running" }
state
creatingBooting
runningUp — the agent may not have connected yet
stoppedStopped, /workspace kept
missingYou have no box for this handle
errorIt failed — say why in detail
unreachableYou can't tell right now (the machine is offline). Nothing should be concluded from it

Optional fields: detail (one line, shown to the member), transition (stopping, pausing) while a stop or a pause is under way, ram (saved / none) for a provider that implements pause.

What a member picks a box as — a size on a kind of machine.

// result
{
  "machineTypes": [
    { "id": "small", "label": "Small", "size": "small", "cpus": 2, "memoryGb": 4, "diskGb": 30, "pricePerHour": 0.04 }
  ]
}

The id comes back to create as machineType. pricePerHour is shown as is; leave it out if you don't bill by the hour.

What your boxes guarantee, shown to members so they know what they're picking.

// result
{
  "dind": true,
  "suspendKeepsDisk": true,
  "suspendKeepsProcesses": false,
  "location": "Your own machines, in Frankfurt",
  "billing": "A stopped box costs nothing"
}

Most runtimes need a credential — an API key, a cluster token. Declare "auth": "apiToken" and serve a form on your installPath where an OAuth app would put its consent screen: verify the launch token, ask for the credential, check it against your backend, then stage and redirect exactly as in Installing. A runtime that needs nothing ("auth": "none") can stage and redirect straight away.

Prebuilt images

Building a project's image ahead of time, and starting boxes from it in seconds, goes through logs, build or snapshot — operations still moving as we add providers. If your runtime should start from prebuilt images, talk to us before implementing them.