Skip to main content
hiloop runs agent sandboxes on Kubernetes, in a cluster you operate. A sandbox is a Kubernetes object. Creating one writes a custom resource; a controller turns that into a pod running under gVisor in a dedicated namespace. The sandbox’s /workspace is served from node-local storage by a per-node daemon, and its durability comes from continuous replication to your object storage rather than from a network volume attached to the pod. That last choice is the one that shapes everything else, so it is worth stating plainly: because the filesystem is described by a manifest of content-addressed chunks, forking a prepared workspace copies a manifest instead of copying data. The cost of a fork does not grow with the size of the workspace.

The shape at a glance

  1. One authenticating edge. It resolves your credential to an organization, user, and scope, and stamps that identity. Backend services trust only edge-stamped identity, never a request body.
  2. The control-plane API validates the request, owns the product record, and writes the sandbox resource.
  3. The controller reconciles that resource into exactly one pod.
  4. A status mirror watches the cluster and converges what it observed into the product record. The state you read is evidence of what the runtime is doing, not a record of what someone asked for.
Two consequences worth naming. Sandbox identity stays opaque, so a sandbox id never exposes a pod name, container id, or node name and your callers cannot couple to the runtime. And admission fails closed: a request a deployment cannot enforce exactly is refused with a named unsupported_capability rather than quietly weakened.

Design trade-offs

Every choice below buys something and costs something. The costs are the useful part of this section, so they are stated as plainly as the benefits.

Kubernetes objects, rather than owning the host

Most sandbox platforms run their own orchestrator over VMs or bare metal, and some Kubernetes-based ones run workloads as privileged containers with host paths, driven by a node agent outside the pod model. Either way the platform controls the host outright. We run sandboxes as ordinary Kubernetes objects in a cluster you operate. It costs us the freedom to do whatever is convenient on the node. We have to satisfy your cluster’s admission and pod-security rules rather than working around them, and we inherit Kubernetes pod-start cost: the kubelet, the CNI, and the pod sandbox all sit on the path to a running sandbox, which a platform driving containers directly on the node does not pay. That is a real startup and density tax, and it is the clearest thing we give up. We took it because it puts sandboxes in your cluster under your policy, your tooling, and your observability, with nothing phoning home, and because the isolation controls are then enforced by your API server rather than by our good intentions. It also means you can inspect and debug a sandbox with kubectl rather than through our dashboard.

gVisor for every sandbox, rather than the host kernel

Every sandbox pod runs under a user-space kernel that intercepts system calls. It costs us syscall performance. Independent published measurements put simple syscalls at roughly twice the cost of a conventional container runtime, and metadata-heavy filesystem work considerably worse. Some workloads notice, and a build over a large dependency tree is the usual case. We took it because the isolation boundary then does not depend on our application code being correct. A bug in our control plane, or a stolen control-plane credential, cannot land a customer workload on the host kernel, because the requirement is checked by cluster admission rather than by us. For a platform whose entire job is running untrusted code, that is a better trade than the performance.

Local disk plus blob durability, rather than a network volume per sandbox

Giving each sandbox its own network volume is the obvious design. Its costs are properties of network block storage rather than of any particular implementation: The attachment ceiling is the one that bites: it is a fixed per-instance budget, shared with network interfaces, and it caps durable sandboxes per node low enough to dominate scheduling. Across the providers we surveyed, the field has largely moved away from this shape for that reason. It costs us cold reads and a replication window. Data not yet on the node faults in from object storage, so the first touch of cold data is slower than reading an attached disk, and against a cloud store that gap is substantial rather than marginal. And a write is durable once replicated rather than immediately, so data written inside the replication window is at risk if the node is lost. That window is bounded rather than open-ended; what survives what states what it means. We took it because those three costs are structural rather than fixable, and because the fan-out an agent workload actually performs is cheap in this shape and expensive in the other.

A manifest copy for fork, rather than copying a filesystem

Forking publishes a description of the filesystem under a new identity and shares the underlying data, so a fork’s cost is governed by the size of that description rather than the size of the workspace. It costs us real complexity in the storage engine: content-addressed chunks, a replication log with exactly-one-writer semantics, and a cache with its own eviction behavior. That is harder than calling a volume-clone API, and it is why we copied a hardened open-source implementation of the block layer instead of inventing one. We took it because branching one prepared environment into many is the operation an agent or evaluation workload performs constantly, and this is the difference between fan-out being free and fan-out being the bottleneck.

Continuous replication, rather than periodic snapshots

Writes are captured behind the live device and streamed to object storage in order, instead of the filesystem being quiesced on a timer. It costs us bookkeeping we have to get exactly right, and a bound we have to enforce: there is a backlog of not-yet-replicated data, and at the bound the workload feels backpressure instead of the backlog growing without limit. A workload writing faster than your object store absorbs will be slowed down. We took it because the alternative stops guest I/O to take a copy, and because recovery then reads current state rather than replaying history, so a restore does not get slower the more that was written.

Memory capture as an accelerator, not the correctness path

Filesystem continuity is what the product guarantees. Memory capture sits on top of it: a stop checkpoints the process tree and records that image as the sandbox’s restore point, and a start brings back the filesystem and starts the workload from its image rather than replaying the capture. It costs us the thing people most want to hear, which is that a sandbox resumes exactly where it left off. It does not, and a workload that needs to continue mid-flight has to rebuild that from what it wrote to disk. We took it because a memory image is only valid against the exact filesystem generation it was captured over, and a capture that restores successfully with subtly divergent memory is worse than one that fails outright. Treating the filesystem as the guarantee and memory as an optimization keeps that failure mode visible instead of silent. See memory capture.

What you install

You provide Kubernetes and nodes, PostgreSQL, object storage, a registry mirror, TLS, an identity provider, and a collector for telemetry. See deploy on Kubernetes for the full cluster contract.

Isolation

Sandboxes run arbitrary, untrusted images, so every control sits outside the workload and is pinned by cluster policy rather than by application code.
  • gVisor for every sandbox pod. The runsc runtime class puts a user-space kernel between the workload and the host.
  • Admission pins it. A cluster policy refuses any sandbox whose pod template does not request the gVisor runtime class, or which asks to share the host’s network, process, or IPC namespaces. The check runs at the Kubernetes API server, so neither a control-plane bug nor a stolen control-plane credential can place a workload on the host kernel.
  • A dedicated namespace enforcing the Kubernetes baseline pod security standard as defense in depth, with no Kubernetes service-account token in sandbox pods.
  • An egress deny floor. DNS, the public internet, and the API’s dedicated capture listener are network-reachable. The workload has no capture proof, so that listener rejects it; all other cluster services, private address ranges, and the cloud metadata endpoint are unreachable.
  • The workspace is mounted nosuid and nodev, so an image cannot smuggle setuid-root binaries or device nodes in through its own workspace. noexec is deliberately not set, because building and running your own code in /workspace is the primary flow.
  • No platform-managed provider credential in a workload. The approved gateway target uses an external Envoy data plane plus a Hiloop control service to verify live-workload proof, policy, and budget and select a credential outside the guest. That sandbox path is not deployed; the existing Rust proxy is operator-only. Direct provider access is not a supported fallback, so clean sandbox model access remains unavailable.

The workspace layer

Each sandbox’s /workspace is a block device served by the per-node daemon, with a manifest describing where every block lives. Node-local storage is the read-write path. A copy-on-write cache on the node holds fetched chunks and dirty blocks. Cold blocks fault in from your object storage in the background while the workload runs. Reads are verified before they are served. Blocks are 4 KiB; data is fetched in larger compressed, checksummed chunks, and the checksum is verified before a single byte reaches the kernel. A corrupt or truncated fetch leaves no partial state to serve later. Chunk boundaries follow file boundaries, not fixed offsets. This is what makes sharing real. Recompiling a project shifts symbol addresses throughout a filesystem image, so fixed-offset chunking finds almost nothing in common between two builds of the same image, while file-aligned chunking finds most of it. Deduplication is what makes fork and fan-out cheap, so the layout is chosen for it. Durability is continuous replication, not a periodic snapshot. Writes are captured point-in-time behind the live device, so guest I/O is never stopped to take a copy. Batches land on node disk, then upload in strict order. Each payload is confirmed in storage before its log record publishes, and that record publishes through a create-only conditional write, so exactly one writer can ever win a sequence number. A lost race is treated as split brain and stops the lane rather than retrying into corruption. Restore is a read, not a replay. Current state is the newest consolidated manifest plus its log tail. A restore composes those and starts serving immediately, faulting blocks in lazily. There is no byte replay on the start path, so starting does not get slower the more that was written. Fork is a manifest copy. Forking publishes the source’s effective manifest under the child’s own identity. The child then diverges through its own log and its own write cache. Chunks stay shared because the objects the parent’s history wrote are immutable and uniquely keyed: nothing is rewritten and nothing is re-keyed. Forking one source into many children never serializes on the source. Backpressure is bounded and explicit. The un-replicated backlog is bounded by how much can drain inside a spot interruption grace window. Below that bound writes never block; at it, the lane applies backpressure rather than growing an unbounded local backlog.

Portability

One policy layer of our own, holding a single timeout ladder, retry policy, verify-before-serve rule, and error taxonomy, sits over maintained blob drivers for S3, Google Cloud Storage, and local files. Any S3-compatible endpoint works, and the S3 lane is tested continuously against MinIO. The conditional-write primitive the replication log depends on is implemented natively by each driver, with no vendor escape hatches, which is why the same code path is correct on your object store as on ours.

What survives what

What a durability receipt proves. A receipt reports exactly what has been proven and nothing more: local means the capture exists on the node, replicated means its upload to your object storage is confirmed. The API does not report replicated on the strength of a local write, and it does not hold your request open waiting for replication. If you need the stronger guarantee before proceeding, ask for it and wait. Control-plane records, including sandbox identity, lineage, metadata, and telemetry, live in your replicated stores independent of any node.

Memory capture

Filesystem continuity is the correctness path. Memory capture sits on top of it, for a long-lived session you would rather not warm up again. A stop captures: while the pod is still running, the per-node daemon pauses the process tree and checkpoints the sandbox against the container runtime’s state directory, then uploads the image to your object storage. The captured image is recorded as the sandbox’s restore point. A restore point is a record, not something a start consumes. Starting a stopped sandbox brings back its /workspace and starts the workload from its image. It does not replay the captured memory, so treat process state as something to rebuild from what you wrote to disk, and treat the restore point as the artifact a resume path would read rather than as a resume that already happens. Two properties of the capture are treated as load-bearing:
  • Restorability is verified, not assumed. A checkpoint can report success and still be unrestorable, so only an actual restore proves an image is good.
  • A memory image is bound to the exact filesystem generation it was captured over. A small drift in a library underneath a checkpoint would restore successfully with divergent memory, which is worse than failing outright.

Where your data lives

Control-plane metadata, every workspace byte and manifest, event payloads, sandbox compute, and your credentials stay in your environment. The platform can run with no outbound internet route at all: mirror the images and charts into your registry, host the identity provider inside the boundary, and point telemetry at your own collector. One thing to be clear about, because it is a property of the product rather than of your configuration: a sandbox’s default egress posture allows the public internet, with private ranges and the metadata endpoint denied. A workload cannot reach your internal infrastructure, but a workload that wants to send data to a public endpoint can. If your requirement is that nothing leaves the network at all, that is a policy you impose at your own perimeter.
Parts of the workspace block storage layer derive from E2B’s Apache-2.0 licensed implementation, with provenance and license recorded in our source tree.