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.

Caller

Agent

SDK · MCP · REST · Console

  • run(code, egress, scopes)
  • ← stream() · result()
  • ← audit()
run
Control plane

Fastify + Orchestrator

backend-agnostic core

  • • orchestrator — lifecycle
  • • credential broker — mints & withholds
  • • audit log — immutable
  • • SSE stream + webhooks
  • • auth + org-scoped tenancy
Joblaunch / teardown
Session pod

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/createdBy from the authenticated principal. The public Session never carries a token field.
  • launch — it hands a LaunchSpec to 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 by orgId.

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.
Invariant 2
New behaviour goes through the 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#

repotext
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 demo

Each directory has its own component page documenting its role, interfaces, and contracts.