Architecture
A backend-agnostic core, between the caller and the sandbox
Enclave takes an untrusted workload, runs it inside a sandbox under quotas and default-deny egress, brokers any external access without placing a secret in the sandbox, streams the run, delivers a structured result, and tears everything down. Here is how the pieces fit together.
Overview#
Enclave is a pnpm/TypeScript monorepo with a single hub — the control plane — and several clients of it. Data flows in one direction: every package depends on shared for the wire format; the control plane is the hub everything else talks to; sdk and mcp are thin clients of its REST API.
The defining design choice is that the control plane talks to a single SessionBackend interface. The orchestrator, audit, streaming, credential, and auth/tenancy logic are backend-agnostic — so the whole system is developable and testable on a laptop against an in-process simulator, then runs unchanged against a gVisor-on-Kubernetes backend.
Agent
SDK · MCP · REST · Console
- run(code, egress, scopes)
- ← stream() · result()
- ← audit()
Fastify + Orchestrator
backend-agnostic core
- • orchestrator — lifecycle
- • credential broker — mints & withholds
- • audit log — immutable
- • SSE stream + webhooks
- • auth + org-scoped tenancy
gVisor sandbox (runsc)
the dangerous side of the boundary
- no service-account token
- non-root · all caps dropped
- read-only rootfs · no host mounts
- egress: default-deny
- credential injected (env)
- CPU / memory / wall-clock quotas
logs & result stream back over SSE · the audit log records every egress decision, quota kill, and teardown · every object is reclaimed on teardown
Data flow#
The lifecycle of a single request:
- run — the caller (SDK, MCP, REST, or console) posts a workload: code, language, an egress policy, any service bindings, and optional limits/webhook.
- validate & stamp — the orchestrator validates the request and stamps
orgId/createdByfrom the authenticated principal. The publicSessionnever carries a token field. - launch — it hands a
LaunchSpecto the backend, which provisions the sandbox. No secret is placed in the workload container: service-binding secrets are injected at the egress proxy, and a private-repo git token is mounted only on the clone init-container. - observe — the backend emits events through a
BackendEventSink: phase changes, stdout/stderr, egress decisions, the result. The orchestrator fans these out to the SSE stream, the immutable audit log, and any webhook. - deliver & teardown — the structured result is available over the API; teardown reclaims every per-session object.
The control plane#
A Fastify REST API in front of an orchestrator. Its responsibilities are cleanly separated:
- Orchestrator — owns the session lifecycle end to end; the single place tenancy is enforced (every accessor funnels through an org-scoped
ownedBy()check). - Credential broker — mints the short-lived scoped git token for a private-repo clone; it is mounted only on the clone init-container and never reaches the workload or the caller.
- Audit log — an immutable, per-session record of every security-relevant event.
- SSE + webhooks — live log/result streaming and optional event delivery.
- Auth + tenancy — an auth hook verifies a user JWT or an API key and sets
req.principal; the orchestrator scopes everything byorgId.
See the control-plane page for the routes, the orchestrator functions, and the backend interface.
Pluggable backends#
Every workload runs behind one SessionBackend. Three implementations share identical orchestration:
- SimulatorBackend — models the sandbox's observable behaviour (egress decisions, host-fs isolation, quota kills, secret withholding) without executing code or touching a cluster. Honest about being a model. Powers tests, the demo, and every dev loop.
- KubernetesBackend — provisions a gVisor Job + per-session NetworkPolicy + code ConfigMap (plus a git-clone init-container and Secret for a private-repo source), streams logs, maps terminal state to a kill-reason, and tears it down.
- DockerBackend — a dev convenience that runs the workload in a local container. Useful on macOS, but it runs on the host kernel (no gVisor) and can't enforce allowlist egress — so it is not a security boundary.
SessionBackend interface so every path — simulator, Docker, and Kubernetes — inherits it. Auth and org-scoping live at the orchestrator/route layer, so all three backends get identical tenancy enforcement.The session pod#
On the Kubernetes path, each session is one Job whose pod is locked down by construction: runtimeClassName=gvisor (runsc), automountServiceAccountToken=false, runAsNonRoot, all capabilities dropped, readOnlyRootFilesystem, and no host path mounts. A per-session NetworkPolicy makes egress default-deny (which also blocks the 169.254.169.254 metadata IP). CPU, memory, and wall-clock limits bound it. No secret is placed in the workload container: service-binding secrets are injected at the egress proxy, and a private-repo git token stays on the clone init-container.
The full enforcement story — and the honest list of what is not yet enforced — is on the containment model page.
Monorepo layout#
enclave/
├── shared/ wire-format types (one source of truth)
├── control-plane/ the hub — Fastify API + orchestrator + backends
│ └── src/backends/ simulator · docker · kubernetes
├── sdk/ typed TS client
├── mcp/ MCP server (agent tools)
├── ui/ React web console
├── runner/ in-sandbox harness image (python + node)
├── deploy/ k8s manifests (RuntimeClass, NetworkPolicy, RBAC, …)
└── demo/ self-contained adversarial demoEach directory has its own component page documenting its role, interfaces, and contracts.