Documentation
¶
Overview ¶
runtime.go — the Kubernetes half: what a sandbox IS in the cluster, and the one channel into it.
A sandbox is a Pod, and its isolation boundary is the RUNTIME that pod names — one field, `SANDBOX_RUNTIME_CLASS`, holding `gvisor` or `kata-fc` or `kata-clh` or nothing. It is the ONLY boundary claimed here: no uid juggling, no process groups, no daemon inside the pod deciding what it is allowed to run. Everything the predecessor built to approximate that boundary in Go is deleted rather than kept "for defence in depth", because a second half-boundary is a second thing to keep true.
Empty is honest, not a hole: it means the node's default runtime, which is the containment a normal pod gets. It ships that way because runsc has to be installed on the nodes first and that restarts containerd under 204 running pods — maintenance, not a release step. The field flips afterwards with no rebuild, and that is the whole reason the runtime is a string.
There is no os/exec in this file and there must never be. It creates Kubernetes objects and streams bytes to the apiserver.
Package sandbox is the ONE compute primitive: a sandbox is a gVisor pod that runs somebody else's code, and every lifetime is the same object.
a function invoke = a sandbox with a seconds-long lease a code-exec call = a sandbox with a session lease an agentic coding run = a sandbox with a project volume and a long lease
Not three subsystems, not three schedulers. One record, one pod spec, one way in. What differs between them is `ttlSec` and whether a volume is attached.
POST /v1/sandboxes {kind:"sandbox", class, project?, ttlSec?} -> Sandbox
GET /v1/sandboxes ?kind=&project=&status=
GET /v1/sandboxes/:id
DELETE /v1/sandboxes/:id ?purge=1 drops the volume too
POST /v1/sandboxes/:id/exec {argv|command, stdin?, timeoutSec?} -> {exitCode,stdout,stderr}
GET /v1/sandboxes/:id/fs ?path= read a file, or list a directory
POST /v1/sandboxes/:id/fs ?path= write a file
THERE IS EXACTLY ONE WAY INTO A SANDBOX, and it is the Kubernetes exec subresource. fs read/list/write are not a second channel — they are `cat`, `ls` and `tee` over that one channel, which is also how `kubectl cp` has always worked. The predecessor shipped an in-pod HTTP daemon with eleven endpoints, a shared pool-wide API key and a pod-IP address book; all three are gone, because Kubernetes already had the channel and we were re-implementing it badly.
A SANDBOX IS ADDRESSED BY POD NAME THROUGH THE APISERVER, NEVER BY IP. That is not a style preference, it is the fix for a real cross-tenant read: a pod that dies on its own — evicted, OOM-killed, drained — never runs a release path, so a row keeps a Host the CNI has since handed to another tenant's replacement pod, and a call to that address is served by a stranger with every credential checking out. A pod NAME is minted per sandbox and never reused, so the same mistake cannot be spelled. The header-stamping protocol that was invented to defend the IP scheme is deleted along with the scheme.
NOTHING RUNS IN THIS PACKAGE. There is no os/exec here and there must never be. It creates Kubernetes objects and streams bytes to the apiserver; the work happens inside the pod, under runsc, on the far side of a runtime boundary.
THERE IS NO POOL. A sandbox is created for a lease and deleted at its end. Claiming from a warm pool is what forced the recycle, the pod-IP address book and the label-patch race arbitration; it also grows, inevitably, into a scheduler beside Kubernetes' own. If a warm pool is ever wanted, it is a Deployment an operator sizes — not a manager in this process.
AUTH is the ordinary one: IAM terminates identity at the edge and this package reads principal.Org. It never mints a credential, and no caller credential reaches a sandbox — the pod carries no service-account token at all.
Index ¶
- Constants
- func End(s *Service, ctx context.Context, org, id string, purge bool) error
- func Mount(app cloud.Router, deps cloud.Deps) error
- func Ours(id string) bool
- func Routes(app cloud.Router, s *cloud.Service[state])
- func Write(s *Service, ctx context.Context, org, id, path string, data []byte) (string, int, error)
- type Bound
- type Cmd
- type Entry
- type ExecResult
- type Sandbox
- type Service
- type Spec
- type Store
- func (s *Store) Close() error
- func (s *Store) Delete(ctx context.Context, org, id string) error
- func (s *Store) Expired(ctx context.Context, org string, now int64) ([]Sandbox, error)
- func (s *Store) Get(ctx context.Context, org, id string) (Sandbox, error)
- func (s *Store) IDs(ctx context.Context, org string) (map[string]bool, error)
- func (s *Store) List(ctx context.Context, org, project, status string) ([]Sandbox, error)
- func (s *Store) Live(ctx context.Context, org, project string) (Sandbox, error)
- func (s *Store) LiveOfClass(ctx context.Context, org, class string) (int, error)
- func (s *Store) Put(ctx context.Context, m Sandbox) error
Constants ¶
const IDPrefix = "m_"
IDPrefix is how a sandbox of ours is told apart from one the compute control plane owns, on a resource both answer for. It is a prefix on the id rather than a lookup, so dispatch costs nothing and cannot go stale.
const KindSandbox = "sandbox"
KindSandbox is the sandbox this package provisions: a gVisor pod in our own cluster. It is a VALUE on the shared /v1/sandboxes resource, beside the kinds the compute control plane provisions (droplets, GPUs, bot sandbox), because "a sandbox the org has" is one noun and splitting it by who provisions it would publish two.
Variables ¶
This section is empty.
Functions ¶
func End ¶
End ends the lease: the pod goes, the volume stays unless purge.
purge drops the VOLUME as well, and it is opt-in because the volume holds the only copy of the checkout and the caches. Ending a lease is cheap and reversible; deleting someone's uncommitted work is neither.
func Mount ¶
Mount registers the sandbox-sandbox half of /v1/sandboxes.
It is composed INTO the app that already owns the /v1/sandboxes prefix rather than claiming a manifest row of its own: zip refuses two owners for one prefix, the compute surface has held that prefix in production for months, and a second `sandbox` noun is exactly the duplication this package exists to remove. See apps/visor's mount, which is the only caller.
func Routes ¶
Routes registers everything this package serves.
The collection and member routes used to be left out, on the reasoning that they were "shared with the compute surface" — true while this served /v1/machines, which visor owns and where a second registration of one resource is a conflict. It is /v1/sandboxes now, owned outright, and leaving them out meant Create, List, Get and Delete existed as exported functions that no request could ever reach: a caller could exec in a sandbox it had no way to create. The handlers were there, the routes were not, and nothing said so — the same shape as the policy that selected no pod and the installer that installed nothing.
Types ¶
type Bound ¶
Bound is the ONE selector anything sweeping sandboxes may use, and the check that it is safe, in one value that cannot be had without the other.
It is a type and not a pair of strings so that a caller cannot end up holding half of it. `hanzo.ai/sandbox` with no namespace is the cluster-wide selector that caused the incident; there is no way to spell that here, because the only constructor refuses a system namespace and the only field that names a label is written by this file.
type Cmd ¶
Cmd is one command to run inside a sandbox. Argv is the honest form; Command is the convenience for a caller holding a shell line. Exactly one is required.
type Entry ¶
type Entry struct {
Path string `json:"path"`
Dir bool `json:"dir,omitempty"`
Data []byte `json:"data,omitempty"`
Entries []string `json:"entries,omitempty"`
}
Entry is what a path IS: a file's bytes, or a directory's entries. One read answers both because one shell command answers both, and a caller that had to stat first would pay two round trips through the apiserver to learn something the same command already knew.
type ExecResult ¶
type ExecResult struct {
ExitCode int `json:"exitCode"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
}
ExecResult is what running a command produced. A non-zero ExitCode is data, not an error: the call succeeded and the program failed.
type Sandbox ¶
type Sandbox struct {
ID string `json:"id"`
Org string `json:"org"`
Kind string `json:"kind"`
Class string `json:"class"`
Project string `json:"project,omitempty"`
Status string `json:"status"` // pending | running | error
Image string `json:"image"`
Pod string `json:"-"`
Volume string `json:"volume,omitempty"`
Error string `json:"error,omitempty"`
CreatedAt int64 `json:"createdAt"`
LastUsedAt int64 `json:"lastUsedAt"`
ExpiresAt int64 `json:"expiresAt,omitempty"`
}
Sandbox is one leased execution sandbox. It is the SAME record whether the lease is seconds long (a function invoke) or a week (a suspended coding session) — status and expiresAt carry the difference, not a second table.
Pod is the sandbox's ADDRESS, and it is `json:"-"` on purpose. It is a pod name in a namespace a caller has no business knowing, and handing it out both maps the cluster for whatever runs inside a sandbox and invites a client to try reaching the sandbox directly, which would mean terminating auth somewhere other than the IAM edge. The predecessor returned a pod IP in every create, get and list response.
func Get ¶
Get answers one sandbox THIS org owns. An id belonging to another org is 404 and not 403, because a 403 would confirm the id exists and whether a given sandbox exists is itself a cross-tenant fact.
type Service ¶
Service is the mounted subsystem, handed back to the app that owns the /v1/sandboxes prefix so its collection handlers can dispatch into this one.
type Spec ¶
Spec is what a lease asks for.
ID is the RESUME: name a sandbox you already hold and Lease returns it instead of minting one. That is what makes "a session" expressible without a session table — the caller's session id IS the sandbox id, and the only store either needs is the one this package already keeps.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is one org's sandbox registry — ONE SQLite file per org at {DataDir}/orgs/{orgSlug}/sandbox.db (cloud.OrgDB, HIP-0302). Org isolation is PHYSICAL (a different file), with the org column kept as defence in depth so a query that somehow reached the wrong file still returns nothing.
func (*Store) Get ¶
Get is org-scoped in the WHERE clause, always. The file is already per-org; this is the second lock on the same door.
func (*Store) IDs ¶
IDs is every sandbox this org still claims, with NO LIMIT — which is the whole reason it is not just List.
The orphan sweep subtracts this set from the pods in the cluster, so a truncated answer here does not mean "fewer rows": it means every sandbox past the cutoff is reported as unclaimed and its pod is deleted while its owner is working in it. List's `LIMIT 200` is right for a page a human reads and catastrophic for a set a sweep differences against, so the sweep gets its own query rather than a bigger limit somebody has to keep ahead of the fleet.
func (*Store) Live ¶
Live is the single-attach check: the sandbox, if any, currently holding this project's volume.
func (*Store) LiveOfClass ¶
Live counts the org's sandboxes of one class that are still holding a pod.
A COUNT and not a len(List): List is LIMIT 200, so a cap read through it would stop counting at 200 and stop refusing at exactly the point refusing matters.