An open source control plane for sandboxes. A manifest in the shape
of a Kubernetes object describes the environment an agent or a workload
runs in: its image, its storage, the credentials it may use, the hosts
it may reach, what it may spawn, and how long it lives. cellad makes
that environment exist on a data plane, keeps it inside the boundary
the manifest declared, and lets you run commands, attach a terminal,
drive a screen, and move files in and out. The data plane is a cluster
or a machine cellad drives directly, or your own infrastructure
running a worker that connects outbound. Identity comes from any OpenID
Connect issuer. Permission comes from an endpoint you write.
Latere runs Cella inside its hosted platform; this repository is the control plane that platform is built on, and anyone can run it.
Design. The specs are written and the repository passes its quality
gate. cellad serves its probes, verifies a caller against the issuers
you list, signs the identities it hands to sandboxes and workers, and
asks your authorizer or its own owner policy what a caller may do. It
creates nothing yet; the build order says
what lands when. The schema below may still change before the first
tagged release, and the CHANGELOG names every change to it.
An agent needs somewhere to run code that is not your machine and not production: a Linux environment with the right image, a working tree and storage that outlives the process, credentials scoped to the task that the code itself never sees, network access to the hosts the task needs and no others, and a lifetime that ends on its own. A training loop needs a thousand such environments, each different, none kept warm. An application an agent built needs one that keeps its state and answers on a port. Sandbox services exist, and each has its own API, its own idea of identity, its own account system, and its own opinion about where the sandbox runs, so a platform that composes one is written against that vendor.
Cella makes the environment a document and the control plane a component you run.
- One manifest, one meaning.
apiVersion: cella.latere.ai/v1beta1,kind: Sandbox, and beside itSecret,Volume,SandboxSet, andEnvironment. Every surface, the API, thecellacommand, and a platform importing the packages, resolves a manifest through one function, and what you read back is what runs, defaults included. - A boundary the workload cannot move. A secret carries its own destination scope; a sandbox holds only a placeholder and an egress gateway swaps it for the value on the way out, toward those hosts and no others. A sandbox may spawn sandboxes, and every child is a subset of its parent. Nothing that happens inside widens what was declared.
- Your data plane or ours. Six drivers behind one contract: Kubernetes, Podman, a virtual machine class, an OS-level sandbox on your own laptop, a bare process for tests, and a remote driver whose worker runs on infrastructure the control plane never dials into.
- Desired state survives the data plane. What you applied lives in the control plane; what runs is read back from the driver's labels. A sandbox a cluster loses is recreated with its volumes reattached.
- Scheduling, not just pools. Start it now, take it from a pool, or
queue it against an environment's capacity with a priority. A
SandboxSetruns a thousand variants of one template and collects the results.
apiVersion: cella.latere.ai/v1beta1
kind: Sandbox
metadata:
name: dev
spec:
image: ghcr.io/example/sandbox:1.4
workspace:
source: git
git: { url: https://github.com/example/repo.git, ref: main, secret: github-token }
secrets:
- { name: github-token, env: GITHUB_TOKEN }
volumes:
- { name: state, path: /data, volume: app-state }
network:
egress: { allowedHosts: ["pypi.org", "*.pythonhosted.org"] }
ports: [{ name: web, port: 8080, expose: public }]
lifecycle: { autoStop: 15m, ttl: 24h }cella secret apply -f github-token.yaml --value-from-env GITHUB_TOKEN
cella volume apply -f app-state.yaml
cella apply -f sandbox.yaml -w
cella exec dev -- make test
cella cp dev:/workspace/out ./outCELLA_OIDC_ISSUERS=<your issuer url> make run # cellad on loopback
make # the quality gateToday make run serves the probes at http://127.0.0.1:8081/readyz and
the key set at http://127.0.0.1:8080/.well-known/jwks.json. It needs
an issuer that answers, because cellad reads its discovery document
and its key set before it listens; it generates the signing key once
under out/run/ and keeps it. Once the stubs of the
test stubs spec land, make run
starts an issuer of its own and needs nothing from you. Once the drivers
and the API land it starts the egress gateway, the authorizer, and the
sink beside the server and prints a token to apply the manifest above
with.
cellad knows who is calling and asks somebody else what they may do.
There is no anonymous access and no API key: a caller that wants a
long-lived credential gets one from its own issuer.
Who. CELLA_OIDC_ISSUERS lists the OpenID Connect issuers you
trust, any of them. At start cellad reads each one's discovery
document and key set and refuses to start when one does not answer,
names another issuer, or publishes no RS256 or ES256 key, so a wrong
issuer is a deployment you fix rather than a log you read later. A
bearer is accepted when a listed issuer signed it, its aud contains
CELLA_OIDC_AUDIENCE, and it has not expired. Nothing else about the
token is interpreted. A subject is the issuer and the sub claim
joined, https://login.example.com|alice, so two issuers that agree on
a sub are two different subjects, and every claim of the token reaches
your authorizer exactly as it arrived. An organisation, role, or group
claim means whatever your authorizer decides it means, and nothing to
cellad.
What. CELLA_AUTHORIZER_URL points at an endpoint you write.
cellad POSTs the subject, its claims, an action, and the object to it,
and reads back an allow or a deny, with optional ceilings and, on a
list, a filter. An allow is cached for the ttl your endpoint chooses,
a deny for five seconds, and anything that is not a decision fails the
request rather than allowing it. Write the endpoint in Go against
latere.ai/x/cella/authorizer, which publishes the
thirty-two actions and the ceilings so you keep no copy of the strings.
With CELLA_AUTHORIZER_URL unset, cellad runs its own owner policy:
you may create anything but an environment, you may act on what you own,
a list returns your own objects, and CELLA_ADMIN_SUBJECTS names the
subjects who may act on everything and who alone make environments. A
sandbox's own identity is least privileged: it reads and execs itself,
reads the tree below it, creates a child, and nothing else.
What cellad signs. CELLA_TOKEN_KEY is one or two PEM RSA keys. The
first signs the identity every sandbox gets at /run/cella/token and
the keys your self-hosted workers register with; every key is published
at /.well-known/jwks.json, so anything can verify a sandbox's identity
offline with a stock JWT library. Rotation is yours: put a new key in
front, and take the old block out once the tokens it signed have
expired.
openssl genrsa -out token.pem 2048
CELLA_OIDC_ISSUERS=https://login.example.com \
CELLA_PUBLIC_URL=https://cella.example.com \
CELLA_TOKEN_KEY="$(cat token.pem)" \
CELLA_ADMIN_SUBJECTS='https://login.example.com|alice' \
cellad| Variable | Required | Default | |
|---|---|---|---|
CELLA_OIDC_ISSUERS |
yes | issuer URLs whose tokens are accepted, comma separated | |
CELLA_PUBLIC_URL |
yes | where callers reach the public listener; the issuer of the tokens cellad signs |
|
CELLA_TOKEN_KEY |
yes | one or two PEM RSA private keys of at least 2048 bits; the first signs, all are published | |
CELLA_OIDC_AUDIENCE |
cella |
the aud a caller's token must contain |
|
CELLA_OIDC_INSECURE_ISSUERS |
issuers from the list that may use http:// off a loopback address; for a local issuer, never for a deployment |
||
CELLA_AUTHORIZER_URL, CELLA_AUTHORIZER_TOKEN |
your authorization endpoint and the bearer cellad sends it; the URL unset selects the owner policy, and the URL without the token is a start-up failure |
||
CELLA_AUTHORIZER_TIMEOUT, CELLA_AUTHORIZER_CACHE |
5s, 60s |
one decision's deadline, and how long an allow that names no ttl is held |
|
CELLA_ADMIN_SUBJECTS |
rendered subjects the owner policy lets act on everything; read and unused with an authorizer set | ||
CELLA_ENVIRONMENT_KEY_TTL |
8760h |
how long a worker's environment key lives |
The repository scaffold spec is the whole table, identity and everything else; the identity spec is why each rule is what it is.
- Five kinds with strict decoding, server-side defaults, and a
statusthe server writes, evolving under written rules. - Six drivers, an isolation class each, and the conformance suite a seventh must pass.
- A lifecycle: create, start, stop, delete, idle auto-stop, TTL, recovery from desired state, cascade over a spawn tree.
- An egress gateway that substitutes credentials by destination and never lets a value into a sandbox.
- Volumes with a life of their own, and a workspace that is one.
- Three scheduling strategies, queues with priority and fair share, and sets for rollouts and evaluations.
- Mesh networking between peers and spawn with a budget.
- Exec, attach, files, logs, ports, screenshot, and input as streams.
- OIDC from any issuer, an authorizer webhook with a built-in owner policy, an admission webhook with built-in defaults and ceilings, and a signed event sink.
- Self-hosted environments through a worker that connects outbound.
- Go packages a platform imports:
manifest,runtime,controller,egress. - The
cellacommand and a skill file that teaches an agent to use it. - Signed images, SBOMs, and provenance on every release.
| Page | |
|---|---|
| Specs | the design, one spec per component, with the build order |
| Architecture | the two planes, the packages, what the control plane owns and what a platform supplies |
| Manifest contract | every field, every rule, every error code |
| Egress and secrets | how a credential reaches a request without reaching the sandbox |
| Data plane workers | running sandboxes on your own infrastructure |
| Building a plane | how a platform composes the packages and the webhooks |
| docs/ | for people who run cellad or build against it |
CONTRIBUTING.md is how to build, the bar, and
where a package belongs. SECURITY.md is where to
report a vulnerability.
Apache-2.0. See LICENSE.