Getting Started
AgentENV (abbreviated as AENV) is a self-hosted sandbox runtime for AI agents. It runs isolated Firecracker microVMs and exposes an E2B-compatible HTTP API — so existing E2B SDK code works against it without modification. The repository is available at https://github.com/kvcache-ai/AgentENV.
Why AgentENV
- Scale across diverse environments: AENV runs massive numbers of Firecracker environments across machines and diverse OCI-compatible images, loaded on demand via overlaybd. Local disk acts as a bounded cache, retaining hot data and evicting cold, so images can exceed disk capacity while startup stays fast cluster-wide, without pre-warming every host.
- Make idle environments inexpensive: Snapshot-backed environments boot or resume in under 50 ms and pause in under 100 ms. Idle environments can quickly release CPU and memory, then return when new work arrives.
- Native snapshot and fork support: AENV snapshots memory and filesystem changes incrementally, completing in under 100 ms even under heavy disk modification. A running environment can fork into multiple independent sandboxes for parallel agent workflows. Snapshots persist to S3-compatible object storage or a shared distributed filesystem to prevent data loss.
- Preserve performance and density over time: AENV delivers high-performance I/O via ublk while sharing the host page cache across storage and memory-snapshot data. Memory ballooning returns reclaimable guest memory to the host, sustaining high overcommit as environments run longer and diverge.
Features
- Firecracker microVMs with full Linux kernel isolation per sandbox
- Pause and resume with memory + disk snapshots for instant cold start
- Layered block devices via overlaybd + ublk for copy-on-write image sharing
- Snapshot-backed template builder for publishing reusable, pre-configured sandbox runtimes
- E2B-compatible API so existing E2B SDKs and CLIs work out of the box
- Reverse proxy to reach services running inside sandboxes via HTTP and WebSocket
- Multi-node scaling with a gateway + scheduler control plane (prototype)
Who Is This For
AgentENV is built for teams running AI agents that need isolated execution environments: code interpreters, tool-use agents, autonomous coding agents, or any workload where you want a fresh (or cached) Linux environment per task.
Interacting with the Server
AgentENV exposes an HTTP API. There are four ways to use it:
| Method | Best for |
|---|---|
| aenv CLI | Interactive use, scripting, local development |
| E2B | Application code — existing E2B-based applications work with AgentENV without modification |
| HTTP API | Direct control, other languages, automation |
Where to Go Next
- Quick Start — Install the server, run your first sandbox. Takes ~5 minutes on a supported Linux host.
- Deployment — Build from source, Docker Compose multi-node, or Kubernetes.
Quick Start
Prerequisites
- Linux kernel 6.8+; the install script additionally requires Ubuntu 24.04
/dev/kvmaccess for Firecracker microVM execution
The install script attempts to install missing download and checksum commands,
provisions KVM permissions, loads the ublk_drv kernel module, and downloads
all runtime assets on first run.
Installation requires root, but the installed service does not run as root. It
uses a dedicated aenv system account with CAP_NET_ADMIN and
CAP_SYS_ADMIN, plus group access to /dev/kvm and the ublk devices.
Setup
1. Install and start the server
Option A — Install Script (Linux x86_64)
The script installs both the server and the aenv CLI. Set AENV_HOME_PATH to
choose the data directory; if it is not set, AENV stores runtime dependencies
and data in /var/lib/aenv. After installation, start the server as a systemd
service:
curl -fsSL https://raw.githubusercontent.com/kvcache-ai/AgentENV/main/scripts/install.sh | sudo AENV_HOME_PATH=/path/to/aenv/data bash
sudo systemctl start aenv
To customize the server configuration, edit config.toml and restart the service:
sudo vim /var/lib/aenv/config/config.toml # Or <AENV_HOME_PATH>/config/config.toml if AENV_HOME_PATH is set.
sudo systemctl restart aenv
To change the port, edit API_ADDR in /etc/default/aenv and restart the service.
The service’s persistent state is owned by aenv under /var/lib/aenv by
default. Transient namespace and daemon-socket state lives under /run/aenv.
Option B — Docker
curl -fsSL https://raw.githubusercontent.com/kvcache-ai/AgentENV/main/scripts/docker-setup.sh | sudo bash
docker pull ghcr.io/kvcache-ai/aenv-server:latest
docker run --rm -it \
--device /dev/kvm --privileged -v /dev:/dev \
-p 8000:8000 \
ghcr.io/kvcache-ai/aenv-server:latest
To customize the server configuration, download and edit the configuration file, then mount it at the path used by the container:
curl -fsSL https://raw.githubusercontent.com/kvcache-ai/AgentENV/main/config/default.toml -o config.toml
vim config.toml
docker run --rm -it \
--device /dev/kvm --privileged -v /dev:/dev \
-v "$PWD/config.toml:/workspace/config/default.toml:ro" \
-p 8000:8000 \
ghcr.io/kvcache-ai/aenv-server:latest
To change the port, add the -e API_ADDR and -p flags.
The server is accessible at http://127.0.0.1:8000 by default. Verify it is running:
curl http://127.0.0.1:8000/health
2. Install the aenv CLI
Skip this step if you used Option A — the install script already includes aenv.
Supports Linux and macOS on x86_64 and arm64:
curl -fsSL https://raw.githubusercontent.com/kvcache-ai/AgentENV/main/scripts/install-cli.sh | bash
3. Authenticate
aenv auth
# AENV server URL [http://localhost:8000]: http://127.0.0.1:8000
# API key: dummy
For local development, any non-empty string works as the API key.
4. Pull a template and run a sandbox
aenv pull ubuntu:22.04 --name ubuntu
aenv start ubuntu # starts a sandbox and attaches an interactive shell
Next Steps
- Deployment — build from source, multi-node options
- Core Concepts — how sandboxes, templates, and snapshots work
- E2B — SDK and CLI compatibility
- API Reference — full HTTP API
On-Demand Loading from Shared Storage
AgentENV loads images on demand via overlaybd. Local disk acts as a bounded cache, retaining hot data and evicting cold data so nodes do not need to pre-warm every image or keep a complete copy of every snapshot.
Prerequisites
- Storage connectivity should be as fast as possible. Use at least a 1 Gbps network; 10 Gbps or faster is strongly recommended.
1. Open the configuration file
If AgentENV is running as a systemd service, open the installed configuration:
sudo vim /var/lib/aenv/config/config.toml # Or use the path to your config file.
If AgentENV is running in Docker, download and edit the default configuration:
curl -fsSL https://raw.githubusercontent.com/kvcache-ai/AgentENV/main/config/default.toml -o config.toml
vim config.toml
2. Configure shared storage
AgentENV supports two shared storage backends: POSIXFS and OSS. Choose one of the following options.
Option A: POSIXFS
Enable the POSIXFS backend, set the shared snapshot location, and increase the remote-block cache:
[snapshot]
repository_backend = "posix_fs"
[backend.posix_fs]
snapshot_store = "/mnt/aenv-snapshots"
[image.cache.remote_blocks]
max_size_gb = 100
Option B: OSS
Enable the OSS backend, configure the OSS connection, and increase the remote-block cache:
[snapshot]
repository_backend = "oss"
[backend.oss]
endpoint = "YOUR_ENDPOINT"
bucket = "YOUR_BUCKET"
region = "YOUR_REGION"
prefix = "YOUR_PREFIX"
cache_max_size_gb = 100
access_key_id = "YOUR_ACCESS_KEY_ID"
access_key_secret = "YOUR_ACCESS_KEY_SECRET"
[image.cache.remote_blocks]
max_size_gb = 100
3. Apply the configuration
If AgentENV is running as a systemd service, restart it:
sudo systemctl restart aenv
If AgentENV is running in Docker, stop the current container and recreate it with the updated configuration and shared snapshot directory:
# The /mnt/aenv-snapshots mount is required only when using POSIXFS.
docker stop CONTAINER_ID_OR_NAME
docker run --rm -it --name aenv-server \
--device /dev/kvm --privileged -v /dev:/dev \
-v "$PWD/config.toml:/workspace/config/default.toml:ro" \
-v /mnt/aenv-snapshots:/mnt/aenv-snapshots \
-p 8000:8000 \
ghcr.io/kvcache-ai/aenv-server:latest
Once configured, AgentENV loads templates and snapshots from shared storage by default.
aenv CLI Reference
aenv is the native CLI for AgentENV. It wraps the HTTP API and envd gRPC endpoints into a developer-friendly interface for managing templates, sandboxes, and snapshots.
Installation
curl -fsSL https://raw.githubusercontent.com/kvcache-ai/AgentENV/main/scripts/install-cli.sh | bash
Or build from source (requires Rust):
git clone https://github.com/kvcache-ai/AgentENV.git
cd AgentENV
make install-aenv
Authentication
aenv auth
Save the server URL and API key. Credentials are stored at ~/.config/aenv/credentials (mode 0600).
aenv auth
# AENV server URL [http://localhost:8000]: The address of the AgentENV server
# API key: dummy (Any non-empty string works for local development.)
Templates
aenv pull <image>
Create a template from an OCI image. Waits for the build to complete by default.
aenv pull ubuntu:22.04
aenv pull ubuntu:22.04 --name my-ubuntu
| Flag | Description |
|---|---|
--name <name> | Override the template name |
--start-cmd <cmd> | Shell command to run inside the sandbox before capturing the template snapshot |
--ready-cmd <cmd> | Shell command used to wait until the sandbox is ready (polled until it exits 0) |
--probe <PORT> | Wait until localhost:<PORT> accepts TCP connections |
-d, --detach | Submit the build and return immediately without waiting |
--timeout <SECS> | Maximum seconds to wait for the build to complete |
aenv build <dockerfile>
Create a template from a local Dockerfile.
aenv build ./Dockerfile
aenv build ./Dockerfile -t my-app
aenv build ./Dockerfile --image ghcr.io/myorg/base:latest
| Flag | Description |
|---|---|
-t, --tag <name> | Template name. Defaults to the parent directory name |
--image <image> | Override the FROM image used as the rootfs base |
aenv template list
List all templates. Alias: aenv template ls, aenv templates list.
aenv template list
aenv template list --output json
aenv template watch <template>
Watch a template build until it succeeds or fails. Accepts either a template name/alias or a template UUID.
aenv template watch my-ubuntu
aenv template watch <template-id>
aenv template delete <template>
Delete a template by name or ID. Alias: aenv template rm.
aenv template delete my-ubuntu
aenv template delete <template-id>
Sandboxes
aenv start <target>
Start a sandbox and attach an interactive shell. <target> is a template name or template UUID.
aenv start my-ubuntu
aenv start --cold ubuntu:24.04 # start directly from an OCI image
| Flag | Description |
|---|---|
--cold | Start directly from an external OCI image instead of a template |
--timeout <secs> | Sandbox TTL in seconds (default: 300) |
--cpu-count <n> / --cpu | CPU cores — only valid with --cold |
--memory-mb <n> / --mem | Memory in MiB — only valid with --cold |
-d, --detach | Print the sandbox ID and exit without attaching a shell |
<target> accepts a template UUID, template name, or (with --cold) an OCI image reference.
aenv pause <sandbox-id>
Pause a running sandbox. The sandbox state is preserved and can be resumed later.
aenv pause <sandbox-id>
aenv resume <sandbox-id>
Resume a paused sandbox.
aenv resume <sandbox-id>
| Flag | Description |
|---|---|
--timeout <secs> | TTL in seconds from now (default: 300) |
aenv timeout <sandbox-id> <seconds>
Set or extend the sandbox expiration to <seconds> from now.
aenv timeout <sandbox-id> 600
aenv connect <sandbox-id>
Attach an interactive shell to a running or paused sandbox. Alias: aenv cn.
aenv connect <sandbox-id>
Resumes the sandbox if paused before attaching.
aenv exec <sandbox-id> <command> [args...]
Run a one-shot command in a sandbox and stream its output.
aenv exec <sandbox-id> ls -la /
aenv list
List all sandboxes. Alias: aenv ls.
aenv list
Outputs a table on a TTY and JSON when piped. Override with --output table|json.
aenv delete <sandbox-id>
Kill and delete a sandbox. Alias: aenv rm.
aenv delete <sandbox-id>
aenv rm <sandbox-id>
Snapshots
aenv snapshot create <sandbox-id>
Capture a persistent snapshot from a running sandbox. The snapshot can be used as a template to start new sandboxes with aenv start.
aenv snapshot create <sandbox-id>
aenv snapshot create <sandbox-id> --name my-base
| Flag | Description |
|---|---|
--name <name> | Snapshot name or alias |
aenv snapshot list
List persistent snapshots. Alias: aenv snapshot ls, aenv snap ls.
aenv snapshot list
aenv snapshot list --sandbox-id <sandbox-id>
| Flag | Description |
|---|---|
--sandbox-id <id> | Filter snapshots by source sandbox ID |
--output <format> | Output format: table (default on TTY) or json |
To delete a snapshot, use aenv template delete <snapshot-id> or aenv template delete <name> — snapshots share the same underlying store as templates and are deleted through the same command.
Docker (Single Node)
Run a single AgentENV node in a Docker container. This avoids installing the Rust toolchain on the host but still requires /dev/kvm.
Prerequisites
- Linux kernel 6.8+
/dev/kvmaccess for Firecracker microVM execution- Docker
Build
Option A — Pre-built Image
docker pull ghcr.io/kvcache-ai/aenv-server:latest
curl -fsSL https://raw.githubusercontent.com/kvcache-ai/AgentENV/main/scripts/docker-setup.sh | sudo bash
Option B — Build from Source
git clone https://github.com/kvcache-ai/AgentENV.git
cd AgentENV
sudo bash scripts/docker-setup.sh
docker build -f deploy/docker/Dockerfile.agentenv -t aenv:latest .
To use a regional apt mirror for both build and runtime stages, pass a base URL
that contains debian, debian-security, and ubuntu mirror paths:
docker build \
--build-arg APT_MIRROR_BASE=https://mirrors.example.com \
-f deploy/docker/Dockerfile.agentenv \
-t aenv:latest .
Run
docker run --rm -it \
--device /dev/kvm --privileged -v /dev:/dev \
-p 8000:8000 \
ghcr.io/kvcache-ai/aenv-server:latest # or aenv:latest if built from source
The --privileged flag is required for Firecracker’s network namespace operations (veth pairs, iptables). The server auto-downloads runtime assets on first start and is accessible at http://127.0.0.1:8000 once ready.
Verify
curl http://127.0.0.1:8000/health
Docker Compose (Multi-Node Simulation)
Run a full multi-node stack on a single host using Docker Compose. This simulates a production-like topology with a gateway, scheduler, and multiple AgentENV backend nodes.
What Gets Started
| Service | Port | Description |
|---|---|---|
| Gateway | :8080 | HTTP/WebSocket reverse proxy |
| Scheduler | :9090 | gRPC node selection and sandbox binding |
| agentenv-a | :8001 | AgentENV runtime node A |
| agentenv-b | :8002 | AgentENV runtime node B |
Prerequisites
- Linux kernel 6.8+
/dev/kvmaccess (passed into the runtime containers)- Docker and Docker Compose
build-essential(sudo apt install -y build-essential)
Clone the Repository
git clone https://github.com/kvcache-ai/AgentENV.git
cd AgentENV
Start the Cluster
sudo bash scripts/docker-setup.sh
make deploy-up
To enable host-based sandbox data-plane URLs, set the shared sandbox proxy domain variable when starting the stack:
SANDBOX_PROXY_DOMAINS=sandbox.example.com \
make deploy-up
Compose passes this value to both the gateway routing allowlist and runtime
nodes’ sandbox response metadata. The domain must resolve to the gateway,
usually through wildcard DNS for *.sandbox.example.com.
Verify
# Health check via gateway
curl http://127.0.0.1:8080/health
# Cluster node snapshots via gateway
curl http://127.0.0.1:8080/nodes
# Direct health check on a backend node
curl http://127.0.0.1:8001/health
Management Commands
make deploy-ps # Show container status
make deploy-logs # Stream logs from all services
make deploy-down # Tear down the cluster
Configuration
Container deployments use deploy/docker/config/default.json. Scheduler and backend node endpoints are configured for the Docker network.
The runtime image includes uvm-ublk at /usr/local/bin/uvm-ublk. Compose uses that path instead of a host-built env/ublk/uvm-ublk binary.
The compose manifest also wires node heartbeat reporting from runtime nodes to scheduler:
AENV_NODE_IDis set explicitly per node container (node-a,node-b).AENV_OBSERVABILITY_SCHEDULER_REPORT_ENABLED=trueenables scheduler heartbeat reporting.AENV_OBSERVABILITY_SCHEDULER_ENDPOINTis set tohttp://scheduler:9090.SANDBOX_PROXY_DOMAINS, when set, is passed through as bothGATEWAY_SANDBOX_PROXY_DOMAINSandAENV_SANDBOX_PROXY_DOMAINS.
Kubernetes (Multi-Node)
Deploy AgentENV across a Kubernetes cluster with a gateway, scheduler, and runtime nodes on every worker.
Architecture
| Workload | Kind | Description |
|---|---|---|
agentenv-gateway | Deployment + ClusterIP Service | HTTP reverse proxy for client traffic |
agentenv-scheduler | Deployment (single replica) + ClusterIP Service | gRPC node selection and sandbox binding |
agentenv-node | DaemonSet (privileged) | One runtime Pod per Kubernetes node |
agentenv-nodes | Headless Service | Used by the scheduler for EndpointSlice discovery |
Why a DaemonSet for Runtime Nodes
- Each Pod needs host-local access to
/dev/kvm - Sandbox networking uses host iptables and network namespaces
- Runtime assets and committed snapshot state are cached per-host at
/var/lib/aenv
Prerequisites
- Kubernetes worker nodes with Linux kernel 6.8+ and
/dev/kvmaccess - Runtime Pods run privileged
- Docker
build-essential(sudo apt install -y build-essential)kubectlwith Kustomize support
Clone the Repository
git clone https://github.com/kvcache-ai/AgentENV.git
cd AgentENV
Build Container Images
make k8s-build
This builds three images: agentenv-runtime:latest, agentenv-gateway:latest, and agentenv-scheduler:latest.
Deploy
# Run on each worker node before deploying
sudo bash scripts/docker-setup.sh
# Render manifests (preview)
make k8s-render
# Apply to cluster
make k8s-apply
To enable host-based sandbox data-plane URLs, set the shared sandbox proxy domain variable when rendering or applying manifests:
SANDBOX_PROXY_DOMAINS=sandbox.example.com make k8s-apply
The helper writes this value into the generated ConfigMap and applies it to
both the gateway routing allowlist and runtime nodes’ sandbox response metadata.
The domain must resolve to the gateway Ingress or LoadBalancer, usually through
wildcard DNS for *.sandbox.example.com.
The default overlay is deploy/k8s/overlays/default, targeting the agentenv-system namespace. The gateway is exposed as ClusterIP by default. Add your own Ingress or LoadBalancer for external access.
The make targets build a temporary Kustomize context so runtime Pods mount the repository’s config/default.toml rather than a separate checked-in copy.
The runtime DaemonSet injects scheduler-report wiring for each node Pod:
AENV_UBLK_DAEMON_BINARY_PATH=/usr/local/bin/uvm-ublk-daemonso the Pod uses theuvm-ublk-daemonbinary included in the runtime imageAENV_NODE_IDfrom Pod metadata name (metadata.name)AENV_OBSERVABILITY_SCHEDULER_REPORT_ENABLED=trueAENV_OBSERVABILITY_SCHEDULER_ENDPOINT=http://agentenv-scheduler:9090AENV_SANDBOX_PROXY_DOMAINSfrom the shared sandbox proxy ConfigMap
The P2P listen address must be reachable Pod-to-Pod; use a concrete container port or a Pod-reachable address if your cluster policy does not allow dialing ephemeral ports.
Operations
# Rollout restart all workloads
make k8s-redeploy
# Delete all resources
make k8s-delete
Local Development (k3s)
A dedicated local-dev overlay mounts the repository’s env/ directory directly into the DaemonSet at /workspace/env, avoiding runtime asset copies:
make k8s-build
make k8s-load-dev # Import images into k3s/containerd
make k8s-render-dev # Preview manifests
make k8s-apply-dev # Apply to cluster
make k8s-refresh-dev # Build + load + rollout restart (all-in-one)
Service Discovery
The scheduler watches EndpointSlices for the headless agentenv-nodes Service and watches Pods for optional label-based discovery policy. It schedules only serving, non-terminating DaemonSet Pods. Pods matching scheduler.discovery.kubernetes.no_schedule_pod_selector stay discoverable as lingering/no-schedule nodes, while Pods matching scheduler.discovery.kubernetes.ignore_pod_selector are excluded. Both IPv4 and IPv6 endpoint addresses are supported.
Sandbox bindings remain in-memory, so the scheduler should run as a single replica. Bindings are lost on restart.
Manual Compile (Single Node)
Run AgentENV directly from source on a single Linux host, useful for development and testing.
If you want to skip building from source, see Quick Start.
Prerequisites
- Ubuntu 24.04 with Linux kernel 6.8+
/dev/kvmaccess for Firecracker microVM execution- Rust toolchain (stable) — install via rustup
sudoaccess
Clone the Repository
git clone https://github.com/kvcache-ai/AgentENV.git
cd AgentENV
Build
# Debug build
make
# Release build (recommended for production)
make release
Start the Server
# Debug build
API_ADDR=0.0.0.0:8000 make start-server
# Release build
API_ADDR=0.0.0.0:8000 make start-server-release
The server auto-downloads runtime assets (Firecracker binary, kernel, rootfs) on first start. Once ready, it listens at http://127.0.0.1:8000.
Verify
curl http://127.0.0.1:8000/health
Configuration
The server reads config/default.toml by default. Override with:
AENV_CONFIG_PATH=/path/to/config.toml make start-server
See Configuration Reference for all settings.
Configuration Reference
AgentENV reads configuration from a TOML file. The default path is config/default.toml. Override it with:
export AENV_CONFIG_PATH=/path/to/config.toml
# or
cargo run --bin server -- --config /path/to/config.toml
Global Settings
| Key | Type | Default | Description |
|---|---|---|---|
home_path | string | "/var/lib/aenv" | Base directory for local AgentENV state. Overridden by AENV_HOME_PATH |
runtime_path | string | "/run/aenv" | Base directory for transient namespace and daemon-socket state. Overridden by AENV_RUNTIME_PATH |
deps_path | string | "$AENV_HOME/deps" | Root directory for auto-downloaded runtime assets. Overridden by AENV_DEPS_PATH |
$AENV_HOME is a literal placeholder in state-path values, not a shell
environment variable. AgentENV replaces it with the resolved home_path after
applying AENV_HOME_PATH; ublk.daemon_socket_path additionally supports
$AENV_RUNTIME, which resolves to runtime_path. Relative paths without these
placeholders are resolved against the directory containing the configuration
file.
Packaged runtime dependency versions and download URLs live in
config/deps_manifest.toml. config.toml should contain runtime behavior
and explicit local path overrides, not the default dependency catalog.
[firecracker]
Firecracker VM binary and boot configuration.
| Key | Type | Default | Description |
|---|---|---|---|
version | string | manifest value | Optional Firecracker release override for auto-download |
url | string | manifest value | Optional download URL template override with {version} and {arch} placeholders |
binary_path | string | derived from manifest/config version | Explicit path to a local firecracker binary. Setup skips the Firecracker download and requires this to be a readable, non-empty, executable regular file |
boot_args | string | "console=ttyS0 reboot=k panic=1 pci=off init=/init …" | Kernel command line arguments. The shipped default also includes DAMON memory-reclaim parameters; see config/default.toml for the full value. |
allowed_extra_boot_args_prefixes | array of strings | [] | Allowed prefixes for extraBootArgs on cold-start sandboxes. If empty, no request-provided extra boot args are appended |
socket_timeout_secs | integer | 3 | Max seconds to wait for the Firecracker API socket |
socket_poll_ms | integer | 1 | Poll interval (ms) for checking socket availability |
work_dir | string | "$AENV_HOME/firecracker-work" | Parent directory for per-sandbox Firecracker work directories. These dirs contain runtime sockets, symlinks, local logs, and writable OverlayBD upper layer data such as overlaybd/upper.data and overlaybd/upper.index |
serial_dir | string | "$AENV_HOME/logs/serial" | Directory for persistent Firecracker serial output (per-sandbox subdirectories) |
log_level | string | unset (disabled) | Optional Firecracker log level (Error, Warning, Info, Debug, Trace, case-insensitive). When set to a non-empty value, Firecracker’s own logging is enabled and written to a firecracker.log file in each sandbox’s log directory (alongside the serial output). Empty/unset disables it |
[kernel]
Linux kernel image for microVMs.
| Key | Type | Default | Description |
|---|---|---|---|
version | string | manifest value | Optional kernel version override for auto-download |
url | string | manifest value | Optional download URL template override with {version} placeholder |
image_path | string | derived from manifest/config version | Explicit path to a local vmlinux.bin. Setup skips the kernel download and requires this to be a readable, non-empty regular file |
[tools]
Tools drive image used to boot the AgentENV control plane inside each microVM.
| Key | Type | Default | Description |
|---|---|---|---|
version | string | manifest value | Immutable SemVer release of the complete tools drive; custom distributions should use a unique prerelease such as 0.1.0-custom.1 |
url | string | manifest value | Optional OCI image URL template override with a {version} placeholder; requires an explicit version when set |
drive_path | string | unset | Local tools ext4 source imported into the versioned dependency directory; requires an explicit version |
control_plane_port | integer | 49983 | Port used by envd inside the guest |
Snapshots and paused sandboxes keep using the tools drive version they were
created with. Launch does not download missing releases: operators must install
the recorded version under <deps_path>/tools/<version>/tools.ext4 before
restore. Setup retains previously installed versions until they are removed
manually.
Template Rootfs Images
User-visible rootfs images are selected at the template API layer.
POST /v2/templates/{templateID}/builds/{buildID} accepts an optional
fromImage field:
- omitted: use
[image.resolver].default_image - full OCI reference: use the supplied image
- short name: normalize standard Docker Hub forms such as
ubuntu:24.04andnode:20
[image.resolver]
| Key | Type | Default | Description |
|---|---|---|---|
default_image | string | ubuntu:24.04 | Image used when template builds omit fromImage |
search_registries | array of strings | ["docker.io", "ghcr.io"] | Registries tried when resolving short image references |
allowed_registries | array of strings | unset (no restriction) | Whitelist of registry hosts (e.g. docker.io, registry.example.com:5000). Omitting the key imposes no restriction; an explicit empty list [] denies every registry. When set to a non-empty list, only references whose registry host is in the list resolve; any other host is rejected as a client (4xx, ImageReferenceError) error. See How the three registry settings interact below. |
try_referrers_overlaybd_prefixes | array of strings | [] | Image reference prefixes for which AgentENV tries OCI Referrers API via regctl for an overlaybd-native artifact before converting a standard OCI image locally. Prefixes are matched with simple starts_with; include the trailing slash yourself, for example registry.example.com/ or registry.example.com/team/. Requires regctl on PATH; lookup failures fall back to the source image. |
How the three registry settings interact
Image resolution runs in two phases, and the three keys act at different points:
search_registries— completion. Only used for short / unqualified references (e.g.ubuntu). Each entry is prefixed to the name to build a list of fully-qualified candidates (docker.io/library/ubuntu:latest,ghcr.io/ubuntu:latest, …). Fully-qualified references skip this step.allowed_registries— gating. Applied right after candidates are built, to both fully-qualified references and the candidates expanded fromsearch_registries. Candidates whose registry host is not whitelisted are dropped; if none remain, the reference is rejected with a 4xx error. In effect the resolvable set of short-name hosts is the intersection ofsearch_registriesandallowed_registries— e.g. searchingdocker.ioandghcr.iowhile only allowingghcr.ioresolves short names toghcr.ioonly.try_referrers_overlaybd_prefixes— per-candidate optimization. Runs later, while resolving an already-permitted candidate: after its manifest is fetched, AgentENV may query the OCI Referrers API on the same registry/repository for an overlaybd-native artifact. Because referrer lookups never leave the source image’s own host, they are implicitly covered byallowed_registries— no separate whitelist entry is needed for referrers.
[image.cache]
Node-local cache root for resolved and converted user images.
| Key | Type | Default | Description |
|---|---|---|---|
root_dir | string | "$AENV_HOME/image-cache" | Root directory for AgentENV image-cache artifacts |
capacity_gb | integer | 100 | Budget for capacity-driven eviction of local commit bytes. Enforced only when [image.cache.gc].enabled is true: the background GC evicts least-recently-used source configs once usage crosses the high watermark, down to the low watermark. Unset = no capacity cap. |
[image.cache.gc]
Background hard-commit garbage collection for the image cache. When enabled,
each pass reconciles metadata from the on-disk source configs and then deletes
hard-commit objects that are no longer rooted by source configs, held by
image-cache leases, or referenced by the in-process running set. Committed
snapshots are durable SnapshotRepository state and do not pin ImageCache
commits. With capacity_gb set, GC first evicts least-recently-used source
configs over the high watermark so hard-commit GC can reclaim what they unrooted.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable the background image-cache GC task |
interval_secs | integer | 1800 | Seconds between GC passes (a value <= 0 falls back to the default) |
min_age_secs | integer | 600 | Minimum time since last use before a source config is eligible for capacity eviction (the LRU floor) |
high_watermark_ratio | float | 0.95 | Begin capacity eviction once local commit bytes exceed capacity_gb × this ratio. Clamped to (0, 1] |
low_watermark_ratio | float | 0.70 | Evict down to capacity_gb × this ratio once the high watermark trips. Clamped to (0, high_watermark_ratio] |
Capacity-driven eviction runs only when [image.cache].capacity_gb is set;
otherwise the GC still reclaims unreachable commits but performs no watermark
eviction.
[image.cache.remote_blocks]
Overlaybd registryfs_v2 remote block cache settings. The directory is always
<image.cache.root_dir>/remote-blocks.
| Key | Type | Default | Description |
|---|---|---|---|
max_size_gb | integer | 10 | Maximum size of the overlaybd remote block cache in GiB. This value is written to generated overlaybd cacheConfig.cacheSizeGB |
Resolved image data is cached under:
<image.cache.root_dir>/
commits/
<sha256-commit-digest>/
overlaybd.commit
# full overlaybd commit store shared by OCI conversion and download
indexes/ # OCI layer + conversion context -> overlaybd commit descriptor
remote-blocks/ # overlaybd-native remote block cache
configs/ # resolved image configs
<slug>-<hash>-image.json
[sandbox_proxy]
Optional host-based data-plane routing for sandbox services.
| Key | Type | Default | Description |
|---|---|---|---|
domains | array of strings | [] | DNS domains accepted by the server for host-based proxy URLs shaped like {port}-{sandboxID}.{domain}. The first configured domain is returned in sandbox create/detail responses as domain. |
When domains is empty, the server still supports /proxy and routing-header
proxy requests, but does not classify requests by Host. Domains are normalized
to lowercase, deduplicated, and must be valid DNS names. The configured order is
preserved because domains[0] is the advertised sandbox domain.
Environment variable override:
AENV_SANDBOX_PROXY_DOMAINS
[network.egress]
Node-level sandbox egress guardrails. These rules are installed before
per-sandbox allowOut / denyOut rules, so sandbox API requests cannot
override them.
| Key | Type | Default | Description |
|---|---|---|---|
always_denied_cidrs | array of IPv4 CIDR strings | ["10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.168.0.0/16"] | Destination CIDRs that are always rejected from sandboxes before user egress policy is evaluated. Deployments can remove selected RFC1918 ranges when sandbox egress to those destinations is required. |
[network.internal]
AgentENV-internal sandbox address plan. Change these only when the defaults overlap with host or deployment network ranges.
| Key | Type | Default | Description |
|---|---|---|---|
host_interaction_cidr | IPv4 CIDR | 10.11.0.0/16 | Per-slot host interaction address pool. Must contain at least 32768 addresses. |
veth_cidr | IPv4 CIDR | 10.12.0.0/16 | Per-slot namespace veth pair pool. Must contain at least 65536 addresses. |
The two configured CIDRs must not overlap each other or AgentENV’s fixed VM tap
link 169.254.0.20/30. These networks are also treated as reserved sandbox
egress destinations regardless of always_denied_cidrs.
[machine]
Default VM resources for sandboxes.
| Key | Type | Default | Description |
|---|---|---|---|
mem_size_mib | integer | 1024 | Guest RAM in MiB |
vcpu_count | integer | 2 | Number of virtual CPUs |
[envd]
In-guest envd daemon settings.
| Key | Type | Default | Description |
|---|---|---|---|
version | string | "0.5.15" | Expected envd version baked into the tools drive image |
init_timeout_secs | integer | 60 | Max seconds to wait for envd to become ready after VM start |
poll_ms | integer | 3 | Poll interval (ms) for envd health check retries |
[orchestrator]
Sandbox lifecycle management.
| Key | Type | Default | Description |
|---|---|---|---|
auto_evict_interval_ms | integer | 1000 | Poll interval (ms) for background timeout eviction |
default_sandbox_timeout_secs | integer | 15 | Default keep-alive timeout for sandboxes |
auto_resume_min_sandbox_timeout_secs | integer | 300 | When a data-plane request targets a non-running sandbox, automatically resume it (if auto-resume is enabled) and refresh its timeout for no-less than this duration |
persisted_sandbox_store_path | string | "$AENV_HOME/persisted-sandboxes" | Directory for persisted sandbox state |
[pool]
Shared process-wide warm-pool defaults used by network slots, block devices, and pre-spawned Firecracker processes. Pools prewarm to the low watermark, then grow the refill target geometrically toward the high watermark when real acquisitions drain the pool.
| Key | Type | Default | Description |
|---|---|---|---|
low_watermark | integer | 2 | Initial lower bound for all enabled warm-resource pools |
high_watermark | integer | 64 | Maximum idle target for all enabled warm-resource pools |
Component sections:
| Section | Key | Type | Default | Description |
|---|---|---|---|---|
[pool.network] | maintenance_enabled | boolean | true | Enable the background network-slot maintenance worker |
[pool.block] | enabled | boolean | true | Enable the ublk overlaybd warm-device pool |
[pool.block] | startup_prewarm | boolean | true | Prewarm block devices after the first reusable image shape is known |
[pool.firecracker] | enabled | boolean | true | Enable pre-spawned Firecracker processes for snapshot resume |
[pool.firecracker] | maintenance_enabled | boolean | true | Enable the background Firecracker process maintenance worker |
[pool.firecracker] | startup_prewarm | boolean | true | Spawn warm Firecracker entries up to the low watermark during server startup |
[pool.firecracker] | fill_concurrency | integer | 4 | Maximum number of warm Firecracker processes created concurrently by one maintenance refill batch |
Validation rules:
low_watermark <= high_watermark[pool.firecracker].fill_concurrency > 0
[node_identity]
Stable identity fields for this node. These values appear in node API responses and scheduler heartbeats.
| Key | Type | Default | Description |
|---|---|---|---|
node_id | string | hostname-derived | Stable node identifier returned by the admin/node APIs |
cluster_id | string (UUID) | nil UUID | Logical cluster identifier included in node snapshots |
service_instance_id | string | generated UUID | Unique process/service instance identifier for the current node runtime |
[observability]
Node-level observability and host metrics collection.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable the node/admin observability service. When disabled, /nodes returns an empty list and /nodes/{nodeID} returns 404 |
When observability is enabled, host CPU/memory/disk metrics are collected at request time. CPU percent is computed from two samples; the first node metrics request waits about 100ms to return a measured value.
[observability.scheduler_report]
Optional scheduler heartbeat reporting for multi-node control plane integration.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable periodic scheduler heartbeat reporting. Requires [cluster].scheduler_endpoint |
interval_secs | integer | 5 | Heartbeat report interval in seconds |
Environment variable overrides:
AENV_OBSERVABILITY_SCHEDULER_REPORT_ENABLEDAENV_OBSERVABILITY_SCHEDULER_ENDPOINTAENV_OBSERVABILITY_REPORT_INTERVAL_SECS
AENV_OBSERVABILITY_SCHEDULER_ENDPOINT overrides [cluster].scheduler_endpoint for the reporter process only.
[cluster]
Shared cluster-level service endpoints.
| Key | Type | Default | Description |
|---|---|---|---|
scheduler_endpoint | string | unset | gRPC endpoint for the scheduler, for example "http://127.0.0.1:9090". Used by scheduler heartbeat reporting and P2P peer discovery. |
[p2p]
Project-wide artifact transport configuration. The transport is disabled by default. When enabled, it is used by the overlaybd P2P HTTP facade and by snapshot publication/runtime resolution as an optional artifact visibility and acceleration path.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable the P2P artifact transport. When false, AgentENV uses DisabledP2pTransport, so lookups miss and publishes are no-ops. |
transport | string | "iroh" | Transport backend. Supported values are "disabled" and "iroh". Ignored while enabled = false. |
store_dir | string | "$AENV_HOME/p2p/store" | Local store used by the transport backend. Relative explicit paths are resolved against the config file directory. |
listen_addr | string | "0.0.0.0:0" | Optional local listen address for the embedded transport endpoint. Port 0 lets the OS choose a free port. |
lookup_timeout_ms | integer | 5000 | Timeout for one artifact catalog lookup against a peer. |
fetch_timeout_ms | integer | 30000 | Timeout for fetching one artifact from a peer. |
peer_discovery_refresh_interval_secs | integer | 5 | Interval for refreshing peer endpoints from scheduler. Values below one second are clamped to one second. |
[custom_extension]
Custom extension service configuration. When url is unset, the integration is fully disabled. See Custom Extension.
| Key | Type | Default | Description |
|---|---|---|---|
url | string | unset | HTTP base URL of the custom extension service. When set, AgentENV invokes sandbox lifecycle hooks under POST {url}/sandbox-hook/*. |
timeout_ms | integer | 5000 | Timeout for each custom extension HTTP call, in milliseconds. |
[snapshot]
Snapshot storage/build configuration.
| Key | Type | Default | Description |
|---|---|---|---|
local_cache_path | string | "$AENV_HOME/snapshot-local-cache" | Manager-owned node-local snapshot artifact/cache root. Relative explicit paths are resolved against the config file directory. |
repository_backend | string | "posix_fs" | Snapshot repository backend. Supported values: "posix_fs" and "oss" |
p2p_enabled | boolean | true | When enabled, the snapshot manager publishes committed snapshots to the P2P transport and attempts to resolve from it before falling back to the repository backend. |
Environment variable overrides:
AENV_SNAPSHOT_LOCAL_CACHE_PATH
[backend.posix_fs]
POSIX filesystem-backed snapshot repository configuration. This section is used when snapshot.repository_backend = "posix_fs".
| Key | Type | Default | Description |
|---|---|---|---|
snapshot_store | string | "$AENV_HOME/snapshot-store" | Root directory for durable committed snapshot repository state. Relative explicit paths are resolved against the config file directory. |
Environment variable overrides:
AENV_SNAPSHOT_STORE
[backend.oss]
OSS-backed snapshot repository configuration. This section is required when snapshot.repository_backend = "oss".
| Key | Type | Default | Description |
|---|---|---|---|
endpoint | string | none | OSS endpoint URL, for example "https://oss-cn-hangzhou.aliyuncs.com" |
bucket | string | none | OSS bucket name used for committed snapshot state |
prefix | string | empty | Optional object key prefix under the bucket |
credential_process | string | unset | External command used to fetch OSS credentials. Use a plain executable-plus-args form without shell expansion, pipes, or command substitution so it behaves consistently across AgentENV and overlaybd credential consumers |
access_key_id | string | unset | Static OSS access key ID. Required when credential_process is not set |
access_key_secret | string | unset | Static OSS access key secret. Required when credential_process is not set |
security_token | string | unset | Optional session token paired with static access key credentials |
region | string | none | Region passed to the S3-compatible object-store client; required for current OSS backend |
cache_max_size_gb | integer | 10 | Maximum size of the node-local OSS artifact cache in GiB |
Notes:
credential_processand static access key settings are mutually exclusive in practice; whencredential_processis set, the backend ignores static credential fields.credential_processshould be written as a portable argv-style command line. Avoid$VAR, backticks,$(...), pipes, and shell builtins.- Although the config section is still named
oss, the runtime path is implemented via a shared S3-compatible client, soregionmust be configured.
Other path override:
AENV_DEPS_PATH
Setup sysctl tuning is host-level setup. It is skipped before reading /proc/sys
when the server detects that it is running inside a container. Set
AENV_FORCE_SYSCTL_TUNING=1 only for a privileged container with writable
host sysctls; otherwise configure these kernel parameters on the host.
[protoc]
Protobuf compiler metadata for code generation lives in
config/deps_manifest.toml, not config.toml.
| Key | Type | Default | Description |
|---|---|---|---|
version | string | "33.4" | protoc release version |
url | string | GitHub release URL | Download URL template with {version} and {platform} placeholders |
[ublk]
Optional userspace block device configuration. When enabled, rootfs is served through a ublk device instead of a plain file, managed by uvm-ublk-daemon.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable ublk-backed rootfs |
daemon_binary_path | string | "$AENV_HOME/ublk/uvm-ublk-daemon" | Path to the uvm-ublk-daemon binary |
daemon_socket_path | string | "$AENV_RUNTIME/ublk-daemon.sock" | Unix socket path used by the daemon |
daemon_log_path | string | "$AENV_HOME/logs/ublk-daemon.log" | File path for daemon logs; deployments are responsible for rotation and retention |
daemon_metrics_listen_addr | string | "0.0.0.0:9103" | HTTP listen address for daemon Prometheus metrics; empty string disables it |
device_type | string | "overlaybd" | "cow" (copy-on-write) or "overlaybd" (layered image) |
Environment variable override:
AENV_UBLK_DAEMON_BINARY_PATHAENV_UBLK_DAEMON_METRICS_LISTEN_ADDR
[ublk.overlaybd]
Overlaybd-specific configuration used when ublk.device_type = "overlaybd".
| Key | Type | Default | Description |
|---|---|---|---|
global_config_path | string | "$AENV_HOME/overlaybd/overlaybd-global.json" | Path to overlaybd global config JSON (see note below). Relative explicit paths are resolved against the config file directory. |
read_only | boolean | false | When set to true, materializes the rootfs without a writable upper |
runtime_upper_mode | string | "hybridLogStructured" | Runtime upper format for newly materialized writable rootfs OverlayBD images. Supported values are "logStructured", "hybridLogStructured", and "sparse". Existing source uppers keep their own mode |
allow_shrink | boolean | false | Allows an explicit cold-start diskSizeMB smaller than the source rootfs. Explicit sizes use MiB and must be divisible by 1024. Growth is always allowed; snapshot resume never resizes. |
resize_timeout_secs | integer | 120 | Timeout in seconds for the cold-start OverlayBD resize tool. Must be greater than zero. |
download_enable | boolean | false | Enables overlaybd layer-level background download for remote layers |
p2p_lookup_timeout_ms | integer | 300 | Timeout for one foreground Overlaybd descriptor lookup through the localhost P2P HTTP facade. Timeout is treated as a cacheable miss. |
p2p_fetch_range_timeout_ms | integer | 2000 | Timeout for one foreground Overlaybd range fetch through the localhost P2P HTTP facade before falling back to the origin registry. |
global_config_path and auto-generated config
The file at the configured default path
$AENV_HOME/overlaybd/overlaybd-global.json is auto-generated by the server
at startup. The generated JSON incorporates several TOML settings —
[image.cache].root_dir, [image.cache.remote_blocks].max_size_gb,
download_enable, [backend.oss] credentials, and Docker registry credentials
detected from ~/.docker/config.json — into a single overlaybd runtime config
file.
The server regenerates the file at global_config_path on every startup, so
these TOML settings always take effect automatically — any manual edits to the
generated file are overwritten on the next startup. To keep customizations,
make them through the TOML settings, not by editing the generated JSON.
[memory_snapshot]
Memory snapshot overlaybd configuration. The server auto-generates the file at the default path on every startup.
| Key | Type | Default | Description |
|---|---|---|---|
overlaybd_global_config_path | string | "$AENV_HOME/overlaybd/mem-overlaybd-global.json" | Path to the overlaybd global config used for the memory-snapshot ublk backend. Regenerated at startup (manual edits are overwritten); change only to relocate the generated file. |
direct_overlaybd | bool | true | Create memory overlaybd layers directly from Firecracker dirty memory ranges via process_vm_readv, skipping the intermediate mem.bin file. Set AGENTENV_MEMORY_SNAPSHOT_DIRECT_OVERLAYBD=false to force the legacy mem.bin conversion path. |
[memory_snapshot.background_download]
Background download settings dedicated to remote memory-snapshot OverlayBD layers.
They do not change the general rootfs or attached-drive defaults. All fields are
serialized into the generated memory OverlayBD global config. Each remote layer
is downloaded block by block: a sequential sparse-file scan collects the pending
blocks, then at most concurrency block tasks fetch non-overlapping ranges in
parallel on a dedicated multi-thread runtime. Layer files are still processed
one at a time. Downloads of a sandbox-bound device start only after envd is
ready (plus delay), with a 20s fallback if the ready signal is lost; while
foreground remote reads are in flight, background block reads yield to a small
guaranteed floor instead of competing at full speed. The generated memory
config leaves throttling off (maxMBps = 0); image configs that carry a positive
maxMBps keep their historical shared rate limit across the block tasks.
Completed layers are switched to the local file only after a full-file digest
check; a failed or canceled block never switches.
| Key | Type | Default | Description |
|---|---|---|---|
enable | boolean | true | Enables background download for remote memory-snapshot layers. |
delay | integer | 0 | Delay in seconds after envd is ready before background download begins (downloads never start before envd readiness; a 20s fallback applies if the ready signal is lost). |
delay_extra | integer | 1 | Exclusive upper bound for random extra delay. The default 1 ensures delay = 0 adds no jitter. |
try_cnt | integer | 5 | Retry count, with the same semantics as OverlayBD DownloadConfig.tryCnt. |
block_size | integer | 16777216 | Download block size in bytes (16 MiB). Peak scratch memory per active layer download is block_size * concurrency. |
concurrency | integer | 4 | Maximum number of in-flight block remote reads within a single remote layer. 1 keeps the historical serial behavior. Must be greater than zero. |
max_inflight_blocks | integer | 16 | Process-wide cap on in-flight download blocks shared by every concurrent layer download; bounds total scratch memory to max_inflight_blocks * block_size. Must be greater than zero. |
Environment Variables
Deployment Helpers
These variables are consumed by the repository’s Docker Compose and Kubernetes helpers, then passed to the server or gateway-specific variables listed below.
| Variable | Default | Description |
|---|---|---|
SANDBOX_PROXY_DOMAINS | empty | Comma-separated DNS domains for host-based sandbox data-plane URLs. In multi-node deployments this single value is applied to both gateway routing and runtime sandbox response metadata. |
Server
| Variable | Default | Description |
|---|---|---|
API_ADDR | 0.0.0.0:8000 | Address and port the API server listens on |
AENV_CONFIG_PATH | config/default.toml | Path to the TOML configuration file |
AENV_LOG_FORMAT | compact | Server log output format: compact, pretty, or json |
AENV_LOG_SPAN_EVENTS | off | Tracing span lifecycle events to emit: off, new, enter, exit, close, active, or full |
AENV_NODE_ID | hostname-derived | Override the runtime node identifier used in observability/admin snapshots |
AENV_CLUSTER_ID | nil UUID | Override the cluster UUID used for P2P peer discovery and scheduler grouping |
AENV_SERVICE_INSTANCE_ID | random UUIDv7 | Override the per-process service instance UUID included in heartbeats |
AENV_OBSERVABILITY_SCHEDULER_REPORT_ENABLED | from config | Enable scheduler heartbeat reporting |
AENV_OBSERVABILITY_SCHEDULER_ENDPOINT | unset | Override scheduler heartbeat reporting endpoint |
AENV_OBSERVABILITY_REPORT_INTERVAL_SECS | 5 | Override heartbeat reporting interval in seconds |
AENV_CUSTOM_EXTENSION_URL | unset | Override [custom_extension].url, the HTTP base URL of the custom extension service |
AENV_SANDBOX_PROXY_DOMAINS | from config | Comma-separated DNS domains that enable server-side host-based sandbox proxy URLs like {port}-{sandboxID}.{domain} and populate the sandbox response domain field. Empty or unset keeps [sandbox_proxy].domains. |
AENV_HOME_PATH | /var/lib/aenv | Override the base directory from which AgentENV derives local state, caches, logs, generated configs, and downloaded dependencies. Component-specific path settings remain available as advanced overrides. |
AENV_RUNTIME_PATH | /run/aenv | Override the transient runtime directory used for network namespace mount points and the default ublk daemon socket. |
AENV_DEPS_PATH | $AENV_HOME/deps | Override root directory for auto-downloaded runtime assets (Firecracker, kernel, tools drive). |
AENV_SNAPSHOT_LOCAL_CACHE_PATH | $AENV_HOME/snapshot-local-cache | Override the snapshot manager’s node-local artifact/cache root |
AENV_SNAPSHOT_STORE | $AENV_HOME/snapshot-store | Override the posix_fs snapshot repository root directory |
AENV_UBLK_DAEMON_BINARY_PATH | $AENV_HOME/ublk/uvm-ublk-daemon | Override path to the uvm-ublk-daemon binary |
AENV_UBLK_DAEMON_METRICS_LISTEN_ADDR | 0.0.0.0:9103 | Override ublk daemon Prometheus metrics listen address; empty string disables it |
AENV_FORCE_SYSCTL_TUNING | unset | Set to 1 to force sysctl tuning in a privileged container with writable host sysctls. Normally skipped automatically inside containers. |
AENV_FIRECRACKER_WORK_DIR | $AENV_HOME/firecracker-work | Override the parent directory for per-sandbox Firecracker work directories. |
AENV_FIRECRACKER_SERIAL_DIR | $AENV_HOME/logs/serial | Override the directory for persistent Firecracker serial output. Files are grouped under {serial_dir}/{sandbox_id}/. |
AENV_PERSISTED_SANDBOX_STORE_PATH | $AENV_HOME/persisted-sandboxes | Override the directory where paused sandbox state is persisted across server restarts. |
E2B SDK / CLI
These variables configure the E2B SDK and CLI to point at an AgentENV server. Values depend on your deployment mode.
| Variable | Description |
|---|---|
E2B_API_URL | AgentENV server API base URL |
E2B_SANDBOX_URL | Sandbox proxy URL (for WebSocket and process interaction) |
E2B_API_KEY | API key for authentication |
E2B_ACCESS_TOKEN | Access token (used by e2b template commands) |
Values by Deployment Mode
Manual compile (single node):
export E2B_API_URL=http://127.0.0.1:8000
export E2B_SANDBOX_URL=${E2B_API_URL}
export E2B_API_KEY=e2b_000000
export E2B_ACCESS_TOKEN=dummy
Docker Compose / Kubernetes (multi-node):
export E2B_API_URL=http://127.0.0.1:8080
export E2B_SANDBOX_URL=${E2B_API_URL}
export E2B_API_KEY=e2b_000000
export E2B_ACCESS_TOKEN=dummy
In both modes, sandbox data-plane requests can use routing headers with
E2B_SANDBOX_URL=${E2B_API_URL}. The explicit/proxyprefix (${E2B_API_URL}/proxy) is still accepted for back-compat.
For local development, any non-empty value works for
E2B_API_KEYandE2B_ACCESS_TOKENbecause the server only checks that the auth header is present.
Gateway and Scheduler
These variables apply to both the gateway and scheduler processes.
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL | info | Log level: debug, info, warn, or error |
LOG_FORMAT | auto | Log output format: auto, console, or json |
Gateway
| Variable | Default | Description |
|---|---|---|
GATEWAY_HTTP_LISTEN_ADDR | :8080 | HTTP listen address |
GATEWAY_METRICS_LISTEN_ADDR | :9102 | Prometheus metrics listen address |
GATEWAY_SCHEDULER_ADDR | 127.0.0.1:9090 | Scheduler gRPC address for routing and node lookup |
GATEWAY_QUERY_ONLY_SCHEDULER_ADDR | unset | Optional secondary scheduler gRPC address used only for sandbox data-plane LookupNode queries. When set, creation and control-plane calls still go to GATEWAY_SCHEDULER_ADDR. |
GATEWAY_REQUEST_TIMEOUT | 30s | Override the gateway’s HTTP request timeout (for example, 1m30s) |
GATEWAY_SANDBOX_PROXY_DOMAINS | from config | Comma-separated DNS domains that enable gateway host-based sandbox proxy URLs like {port}-{sandboxID}.{domain}. Empty or unset keeps gateway.sandbox_proxy_domains. |
GATEWAY_DEBUG_MODE | false | Enable gateway debug mode |
Scheduler
| Variable | Default | Description |
|---|---|---|
SCHEDULER_GRPC_LISTEN_ADDR | :9090 | gRPC listen address |
SCHEDULER_METRICS_LISTEN_ADDR | :9101 | Prometheus metrics listen address |
SCHEDULER_STRATEGY | round_robin | Node selection strategy for new sandboxes: round_robin or random |
SCHEDULER_REDIS_ADDR | unset | Redis address for persistent sandbox-to-node bindings (for example, redis:6379). Unset = in-memory bindings, lost on scheduler restart. |
SCHEDULER_BINDING_TTL | 30s | How long a sandbox-to-node binding is kept without a confirming heartbeat. Accepts Go duration strings (for example, 1m). |
SCHEDULER_ARTIFACT_STORE_CAPACITY | 1000000 | Maximum number of P2P artifact entries held in the scheduler’s in-memory index |
SCHEDULER_ARTIFACT_LOOKUP_NODE_LIMIT | 0 | Maximum number of nodes checked per P2P artifact lookup. 0 means no limit. |
How AgentENV Works
AgentENV runs AI agents inside isolated Firecracker microVMs. Each sandbox is a lightweight Linux VM with its own kernel, filesystem, and network namespace.
System Overview
┌──────────────────────────────────────────────────┐
│ AgentENV Node │
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ API │──>│ Orchestrator │ │
│ │ (Axum) │ │ (lifecycle) │ │
│ └──────────┘ └──────┬───────┘ │
│ │ │
│ ┌─────────▼───────────┐ │
│ │ Firecracker VM │ │
│ │ │ │
│ │ /dev/vda (rootfs) │ │
│ │ /dev/vdb (extra) │ │
│ │ │ │
│ └─────────────────────┘ │
│ │ │
│ ┌─────────▼────────────┐ │
│ │ Block Device Layer │ │
│ │ (overlaybd + ublk) │ │
│ └──────────────────────┘ │
└──────────────────────────────────────────────────┘
Request Flow
- A client sends an HTTP request to the AgentENV API (for example,
POST /sandboxes). - The API layer validates the request, checks authentication, and forwards it to the orchestrator.
- The orchestrator manages the sandbox lifecycle: it creates a Firecracker VM, sets up networking, and attaches block devices.
- The VM boots with a layered block device (overlaybd) that stacks read-only base image layers with a writable upper layer. Multiple sandboxes share the same base layers.
- Inside the VM, an envd daemon handles command execution, file operations, and health reporting.
- Clients interact with running sandboxes via the reverse proxy (
/proxy, routing headers, or configured sandbox proxy domains), which forwards HTTP and WebSocket traffic to services inside the VM.
Key Components
| Component | What It Does |
|---|---|
| API Server | HTTP server exposing E2B-compatible endpoints for sandbox and template management |
| Orchestrator | State machine managing sandbox lifecycle transitions (create, pause, resume, delete) |
| Firecracker VM | Lightweight microVM providing kernel-level isolation per sandbox |
| Block Device Layer | overlaybd (layered images) + ublk (userspace block devices) for efficient storage |
| envd | In-guest daemon for executing commands, streaming output, and managing processes |
| Reverse Proxy | Routes HTTP/WebSocket traffic from clients to services running inside sandboxes |
| Snapshot Manager | Manages committed snapshots for efficient sandbox creation and reuse |
| Template Builder | User-facing build layer that declaratively produces committed snapshots with pre-installed software |
Multi-Node Architecture
For multi-node deployments, a gateway and scheduler sit in front of multiple AgentENV nodes:
Client ──HTTP──> Gateway (:8080) ──gRPC──> Scheduler (:9090)
│ │
│ ┌─────────────────────┘
│ │ node selection / lookup
▼ ▼
Node A (:8000) Node B (:8000)
The gateway routes requests by sandbox ID. For new sandboxes, the scheduler picks a node. For existing sandboxes, the scheduler looks up the node that owns it. See Deployment for setup instructions.
Templates
A template is the user-facing wrapper around a committed snapshot. Instead of booting a fresh VM and installing software every time, you build a template once and later create sandboxes from it in milliseconds.
How Templates Work
- Define: specify an overlaybd-backed base rootfs and ordered steps such as
run,env, andworkdir. - Build: AgentENV boots a temporary sandbox and executes those steps inside it.
- Finalize: optional startup commands can be started and checked for readiness before the snapshot is captured.
- Publish: the result is committed as a snapshot in the snapshot repository.
- Launch: creating a sandbox from a template resolves the committed snapshot and resumes from it.
Create Your Template
There are two ways to create a template: aenv pull imports an OCI image directly, and aenv build runs Dockerfile instructions inside a temporary build sandbox.
aenv pull
Pulling an existing OCI image as a template:
aenv pull ubuntu:24.04
| Flag | Description |
|---|---|
--name <name> | Template name. Defaults to the repository segment of the image |
--start-cmd <cmd> | Command to run before capturing the snapshot |
--ready-cmd <cmd> | Shell command polled every 2 s until exit 0; gates snapshot capture |
--probe <port> | Readiness check: wait for TCP on localhost:<port> |
-d, --detach | Submit the build and return without waiting |
--timeout <secs> | Maximum time to wait for the build to complete |
Env, WorkingDir, and User are automatically inherited from the OCI image config. See Runtime Configuration for the full field list.
aenv build
⚠️ Experimental — Not recommended for production use.
Build a template by running Dockerfile instructions inside a temporary sandbox:
aenv build ./Dockerfile
| Flag | Description |
|---|---|
-t, --tag <name> | Template name. Defaults to the parent directory name of the Dockerfile |
--image <ref> | Override the FROM image |
Supported Dockerfile instructions:
| Instruction | Behavior |
|---|---|
FROM | Base image (overridable with --image) |
RUN | Shell command executed inside the build sandbox |
ENV | Set an environment variable |
ARG | Set a build-time variable |
WORKDIR | Set the working directory |
USER | Set the default user |
ENTRYPOINT | Becomes the template startCmd |
CMD | Becomes startCmd if no ENTRYPOINT is present |
EXPOSE / VOLUME / LABEL | Accepted but stored as metadata only |
Runtime Configuration
Both methods read the same set of OCI image config fields. For aenv pull, these come from the image config or flags; for aenv build, they are set by the
corresponding Dockerfile instructions executed during the build. The following fields from the OCI image-spec config object are recognised:
| OCI field | Dockerfile instruction | Runtime effect |
|---|---|---|
Env | ENV | Environment variables injected into every sandbox process |
WorkingDir | WORKDIR | Default working directory |
User | USER | Default user |
Entrypoint / Cmd | ENTRYPOINT / CMD | Mapped to startCmd for aenv build; use --start-cmd explicitly for aenv pull |
ExposedPorts | EXPOSE | Stored as metadata only |
Volumes | VOLUME | Stored as metadata only |
Labels | LABEL | Stored as metadata only |
Aliases
Each template is identified by a UUID. Pass --name or -t at creation time
to assign a human-readable alias:
aenv pull ubuntu:24.04 --name my-base
aenv build ./Dockerfile -t my-service
The alias can be used wherever a template ID is accepted:
aenv start my-base
aenv template delete my-service
Manage Templates
List templates
aenv template list # alias: aenv template ls
Displays all templates with their ID, name, build status, CPU, memory, disk size, and last-updated timestamp.
Delete a template
aenv template delete <template-id-or-name> # alias: aenv template rm
Watch a build
aenv build always detaches immediately after submitting. Use watch to follow the build status until it succeeds or fails:
aenv template watch <template-id-or-name>
Start a sandbox from a template
aenv start <template-id-or-name> # start and attach an interactive shell
aenv start -d <template-id-or-name> # detach: print sandbox ID and exit
Relationship to Snapshots
Templates are the API and UX layer. Snapshots are the durable runtime layer.
- A template build publishes one committed snapshot.
- A template ID or alias resolves to one committed snapshot.
- A sandbox created from a template resumes from that snapshot.
If you want the storage and runtime model underneath templates, see Snapshots.
Sandboxes
A sandbox is an isolated Firecracker microVM with its own Linux kernel, filesystem, and network stack. Each sandbox runs independently and can be paused, resumed, or deleted.
Lifecycle
Creating ──> Running ──> Pausing ──> Paused
│ │
├──> Snapshotting └──> Resuming ──> Running
├──> Forking
└──> Killing
| State | Description |
|---|---|
| Creating | VM is booting, block devices are being attached, networking is being configured |
| Running | VM is ready. Commands can be executed, proxy traffic is routed, timeout is ticking |
| Pausing | Memory and disk snapshots are being captured |
| Paused | VM is stopped. Snapshot artifacts are stored. No resources consumed |
| Resuming | Sandbox is being restored from its paused snapshot |
| Snapshotting | A persistent snapshot is being captured; sandbox returns to Running after |
| Forking | Sandbox is being cloned into child sandboxes; source returns to Running after |
| Killing | VM is being torn down and resources released |
Starting a Sandbox
There are two ways to start a sandbox:
Warm Start (from a template)
Starting from a pre-built template restores a snapshot of a known filesystem state.
aenv start <template-id>
See Templates for how templates are created.
Cold Start (from an OCI image)
A cold start pulls an OCI image directly and converts it into a block device at runtime.
aenv start --cold ubuntu:24.04
The cold-start API accepts an optional diskSizeMB field to set the root filesystem’s virtual size in MiB. Explicit values must be at least 1024 MiB and divisible by 1024 because the current resize tool operates at 1 GiB granularity. Growth is allowed by default; shrinking below the source image size requires ublk.overlaybd.allow_shrink = true. If omitted, the image’s built-in virtual size is used. Resizing applies only when creating a fresh writable root filesystem, not to read-only images, images with an existing upper, or snapshot resume. Sandbox responses also report disk size as diskSizeMB.
Working with Sandboxes
Shell and command execution
# Attach to a sandbox
aenv connect <sandbox-id>
# Run a one-shot command and stream its output
aenv exec <sandbox-id> ls -la /
Pause and Resume
Pausing a sandbox captures:
- Memory snapshot of the running VM state
- Disk snapshot of the writable filesystem layer
Resuming restores the VM from these snapshots in milliseconds. The sandbox picks up exactly where it left off, including running processes and open network connections.
aenv pause <sandbox-id>
aenv resume <sandbox-id>
Persistent Snapshots
A snapshot captures the state of a running sandbox into a template that can be used to start new sandboxes.
aenv snapshot create <sandbox-id>
aenv snapshot create <sandbox-id> --name my-base
The resulting snapshot appears in aenv snapshot list and can be started with aenv start <name>. See Snapshots for details.
Fork
Forking clones a running sandbox into up to 16 child sandboxes on the same node. The source sandbox is briefly paused while the clone is captured, then resumes. All children inherit the source’s filesystem, memory, and resource configuration.
curl -X POST \
-H 'X-API-Key: test-key' \
-H 'Content-Type: application/json' \
-d '{"count": 3}' \
http://127.0.0.1:8000/sandboxes/<sandbox-id>/fork
Managing sandboxes
# List all sandboxes
aenv list
# Delete a sandbox
aenv delete <sandbox-id>
Auto-Eviction
Every sandbox has a TTL (time-to-live). When it expires, one of two actions is taken:
- pause (default) — the sandbox is paused and its state is preserved
- kill — the sandbox is deleted permanently
Set TTL with --timeout <secs>:
# Start a sandbox with TTL of 600s
aenv start <template-id> -d --timeout 600
# Set the sandbox expiration for 600 seconds from now
aenv timeout <sandbox-id> 600
To delete instead of pause on expiry, use the API directly with autoPause: false:
curl -X POST \
-H 'X-API-Key: test-key' \
-H 'Content-Type: application/json' \
-d '{"templateID": "<template-id>", "timeout": 600, "autoPause": false}' \
http://127.0.0.1:8000/sandboxes
The default timeout is configured in config/default.toml under [orchestrator].default_sandbox_timeout_secs.
Networking
Each sandbox runs in its own network namespace. By default, outbound internet access is enabled. To disable it at creation time:
curl -X POST \
-H 'X-API-Key: test-key' \
-H 'Content-Type: application/json' \
-d '{"templateID": "my-ubuntu", "allowInternetAccess": false}' \
http://127.0.0.1:8000/sandboxes
For fine-grained egress control, pass a network object when creating the sandbox. Allowed entries take precedence over denied entries:
allowOut— CIDR, IP, or domain patternsdenyOut— CIDR or IP
curl -X POST \
-H 'X-API-Key: test-key' \
-H 'Content-Type: application/json' \
-d '{
"templateID": "my-ubuntu",
"network": {
"allowOut": ["8.8.8.8/32", "example.com", "*.example.com"],
"denyOut": ["0.0.0.0/0"]
}
}' \
http://127.0.0.1:8000/sandboxes
Egress rules can also be updated on a running sandbox:
curl -X PUT \
-H 'X-API-Key: test-key' \
-H 'Content-Type: application/json' \
-d '{"allowOut": ["8.8.8.8/32"], "denyOut": ["0.0.0.0/0"]}' \
http://127.0.0.1:8000/sandboxes/<sandbox-id>/network
Omitting both fields clears all egress rules.
Data Storage
| Path | Contents | Config |
|---|---|---|
$AENV_HOME/snapshot-store/ | Committed snapshot and template artifacts (rootfs layers, memory snapshots, metadata) | [backend.posix_fs].snapshot_store |
$AENV_HOME/persisted-sandboxes/ | Paused sandbox state persisted across server restarts | [orchestrator].persisted_sandbox_store_path |
$AENV_HOME/image-cache/ | Converted OCI image layers (overlaybd format) cached after first cold start or template build | [image.cache].root_dir |
Snapshots
Snapshots are the durable runtime primitive in AgentENV. Everything else is built on top of them:
- Templates are stored as snapshots. A template build commits one snapshot; the template ID is an alias that resolves to it.
- Sandboxes launch by resuming from a snapshot.
- Running sandboxes can produce new snapshots, capturing their current state for later reuse or branching.
Create a Snapshot from a Running Sandbox
Capture the current state of a live sandbox:
aenv snapshot create <sandbox-id>
aenv snapshot create <sandbox-id> --name my-checkpoint
Pass --name to assign a human-readable alias. The alias can then be used with
aenv start or any command that accepts a snapshot ID.
The sandbox transitions through Running → Snapshotting → Running during
capture; the sandbox continues running after the snapshot is committed.
Recoverable failures leave the sandbox running and surface the error to the
caller. Terminal failures — where the runtime was mutated past safe resume —
tear down the sandbox.
Manage Snapshots
List Snapshots
List all snapshots, optionally filtered by source sandbox:
aenv snapshot list # alias: aenv snapshot ls
aenv snapshot ls --sandbox-id <sandbox-id>
The --sandbox-id filter works even after the source sandbox has been deleted.
Start Sandboxes from Snapshots
Start a new sandbox from a snapshot:
aenv start <snapshot-id-or-name>
Delete Snapshots
Snapshots share the same catalog as templates. To delete a snapshot, use the template delete command with the snapshot ID or alias:
aenv template delete <snapshot-id-or-name>
Optional P2P Visibility
When [p2p].enabled = true, published snapshot artifacts are advertised
through the node-to-node P2P transport after the repository commit succeeds.
This makes new artifacts discoverable to peer nodes before slow backend reads
become the bottleneck:
vm_state.binandfirecracker-manifest.jsonare advertised under snapshot-scoped keys.- Overlaybd commit layers are advertised under the shared overlaybd layer keys
(
overlaybd-layer/v1/sha256:<digest>), so overlaybd runtime reads can find them through the overlaybd P2P facade.
P2P does not change the committed snapshot model. The snapshot repository remains the source of truth, and failed P2P publication does not roll back a successful snapshot publish.
Storage
Three-layer model
It helps to treat these as three different layers rather than one combined store:
1. Builder staging
This is a manager-owned temporary workspace used while executing one build. It contains local snapshot artifacts captured from the build sandbox, for example:
<local_cache_path>/
snapshots/<id>/
vm_state.bin
mem_image.json
rootfs/
image.json
drives/<drive-id>/
image.json
This directory is a TemplateBuilder implementation detail. It is not the
durable committed record.
2. Committed snapshot repository
Once a build is published, the durable state lives in the snapshot repository:
<snapshot_store>/
repository/
catalog/
aliases/
snapshots/<id>/
snapshot.json
firecracker-manifest.json
vm_state.bin
managed-layers/
<digest>.overlaybd.commit
This repository is the source of truth wrapped by the template API.
3. Node-local runtime cache
Before a sandbox is launched, AgentENV derives runnable overlaybd configs from the committed snapshot and materializes them in a node-local runtime cache:
<node_local_runtime_cache>/
runtime/<id>/
memory/
image.json
rootfs/
image.json
drives/<drive-id>/
image.json
These are launch-time derived runtime inputs, not committed metadata.
What a committed snapshot contains
Each committed snapshot is split across two durable files:
snapshot.json contains:
context— the runtime configuration baked in at build time: environment variables, working directory, user, exposed ports, volumes, and labels. Sandboxes launched from this snapshot inherit these values.rootfs_layers,attached_drives[].layers,memory_layers— overlaybd layer references, either as managed local layers (deduplicated by content digest) or as external OCI registry references.- Startup configuration.
firecracker-manifest.json contains launch-time metadata not expressed as
overlay layer lists, including memory.virtual_size, rootfs.virtual_size,
and per-drive metadata.
The following files exist only as local build artifacts or node-local derived configs and are not committed:
rootfs/image.jsondrives/<id>/image.jsonmem_image.jsonupper.dataupper.index
Runtime resolution
Before launch, the committed state is resolved into node-local paths:
memory_layersbecome runtimememory/image.jsonrootfs.layersbecome runtimerootfs/image.jsonattached_drives[].layersbecome runtimedrives/<id>/image.jsonfirecracker-manifest.jsonis loaded and hydrated with node-local absolute paths- committed
vm_state.binbecomes the runnable vm-state path
Custom Extension
The custom extension is an optional external HTTP service that AgentENV calls during the sandbox lifecycle. It lets you implement deployment-specific behavior — for example, connecting sandboxes into a VPN, custom firewall rules, or extra mounts — without changing AgentENV itself.
The extension implements a small set of HTTP endpoints (“hooks”); AgentENV is the client. The interface is defined in src/custom_extension_api/openapi.yml.
Configuration
Enable the integration by pointing AgentENV at your extension service:
# config/default.toml (or your AENV_CONFIG_PATH)
[custom_extension]
url = "http://127.0.0.1:9090"
# timeout_ms = 5000 # optional, per-call timeout in milliseconds
AENV_CUSTOM_EXTENSION_URL works as well. When url is unset, the integration is fully disabled: no hooks are called and customExtensionParams must be empty.
Lifecycle hooks
All hooks are POST {url}/sandbox-hook/* with a JSON body. Any connection error, timeout, or non-2xx response fails the corresponding sandbox operation (except stop, which is best-effort).
| Hook | When | Request | Response |
|---|---|---|---|
start-fresh | Before a fresh sandbox boots, after its network slot is allocated | sandboxId, sandboxInstanceId, networkNamespacePath, customExtensionParams | optional extraBootArgs appended to the kernel cmdline |
start-resume | Before a sandbox resumes from a snapshot (template launch, resume after pause, fork child) | same as above | none |
patch-params | When a user PATCHes the sandbox’s params | sandboxId, patch (verbatim user body) | updated full customExtensionParams |
stop | When the sandbox runtime is torn down, before the network slot is released | sandboxId, sandboxInstanceId | none |
Notes:
- Instance identity. A
sandboxIdis reused across pause/resume cycles. Everystart-fresh/start-resumecarries a freshsandboxInstanceIdidentifying that runtime instance, and the subsequentstopcarries the same value. Becausestopis best-effort and may be reordered (e.g. a pause’sstoparriving after the resume’sstart-resume), treat(sandboxId, sandboxInstanceId)as the identity of a running instance and ignorestopnotifications whosesandboxInstanceIdis not the latest started instance for that sandbox. stopalso fires on pause. Pausing persists the sandbox state and then stops the VM process and releases the network namespace; the subsequent resume creates a fresh runtime and firesstart-resume. In-place pause+resume during snapshot capture does not fire any hook (and keeps the samesandboxInstanceId).stopis best-effort: delivery failures are only logged, and it is also fired fire-and-forget if a started sandbox is dropped without an explicit stop.networkNamespacePathis the host path of the sandbox’s netns file (e.g./var/run/netns/agentenv-ns-*), so the extension can enter the namespace (e.g.nsenter --net=...) to set up firewall rules or VPN interfaces.- Concurrent
patch-paramscalls to the same sandbox are not serialized; if your patch semantics are not commutative, handle concurrency in the extension.
customExtensionParams
An opaque JSON object per sandbox, interpreted only by the extension. An absent value and an empty object are equivalent (empty params).
Set at creation — POST /sandboxes and POST /sandboxes-cold accept customExtensionParams:
{
"templateID": "my-template",
"customExtensionParams": { "vpn": { "network": "team-a" } }
}
Non-empty params are rejected with 400 when no extension is configured on the server.
Read — GET /sandboxes/{sandboxID}/custom-extension-params returns the current value ({} when empty).
Patch — PATCH /sandboxes/{sandboxID}/custom-extension-params (running sandboxes only). The request body is passed through verbatim to the extension’s patch-params hook; its semantics are defined entirely by the extension. The hook returns the updated full params, which AgentENV stores and returns:
curl -X PATCH .../sandboxes/{id}/custom-extension-params \
-d '{"vpn": {"network": "team-a", "peers": ["10.8.0.2", "10.8.0.3"]}}'
Persistence — params survive pause/resume and are stored into snapshots created from the sandbox. When starting from a template, a customExtensionParams provided at creation overrides the one stored in the snapshot; otherwise the snapshot’s value is inherited.
Minimal extension example
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
# Latest started runtime instance per sandbox: (sandboxId, sandboxInstanceId)
# is the identity of a running instance; a stop for a superseded instance
# (e.g. arriving after a newer start) is ignored.
latest_instance: dict[str, str] = {}
@app.post("/sandbox-hook/start-fresh")
async def start_fresh(req: Request):
body = await req.json()
latest_instance[body["sandboxId"]] = body["sandboxInstanceId"]
# e.g. nsenter --net={body["networkNamespacePath"]} wg-quick up ...
return {"extraBootArgs": None}
@app.post("/sandbox-hook/start-resume")
async def start_resume(req: Request):
body = await req.json()
latest_instance[body["sandboxId"]] = body["sandboxInstanceId"]
return {}
@app.post("/sandbox-hook/patch-params")
async def patch_params(req: Request):
body = await req.json()
# apply body["patch"] however you like, then return the full new params
return {"customExtensionParams": body["patch"]}
@app.post("/sandbox-hook/stop")
async def stop(req: Request):
body = await req.json()
if latest_instance.get(body["sandboxId"]) == body["sandboxInstanceId"]:
latest_instance.pop(body["sandboxId"], None)
# tear down resources for this instance
return {}
Any non-2xx response (or timeout) fails the corresponding sandbox operation, except for stop, which is always tolerated.
Proxy
The reverse proxy lets you reach services running inside a sandbox from outside. It supports HTTP requests, SSE streams, and WebSocket connections.
Endpoints
ANY /proxyforwards to/inside the sandboxANY /proxy/{path}forwards to/{path}inside the sandbox- Header-routed requests on otherwise unmatched paths forward the original path unchanged. This lets clients use the same base URL for API and sandbox data traffic when they send routing headers.
- When sandbox proxy domains are configured, host-based URLs shaped like
{port}-{sandboxID}.{domain}forward the original path without routing headers.
Query strings are forwarded unchanged.
Required Headers
Each proxied request must identify the target sandbox and port:
| Header | Description |
|---|---|
x-agentenv-sandbox-id | Sandbox UUID to route to |
x-agentenv-target-port | Port of the service inside the sandbox |
E2B-compatible aliases are also accepted:
| Header | Alias for |
|---|---|
e2b-sandbox-id | x-agentenv-sandbox-id |
e2b-sandbox-port | x-agentenv-target-port |
These routing headers are stripped before the request is forwarded to the sandbox.
Host-based proxy requests derive both values from Host, for example
http://8080-<sandbox-uuid>.sandbox.example.com/health targets port 8080.
The configured domain must route to the AgentENV server in single-node mode or
to the gateway in multi-node mode. Host-based proxy traffic is always treated as
data-plane traffic; lifecycle and other control-plane APIs should use the base
API host.
API Reference
E2B
E2B SDK
AgentENV exposes an E2B-compatible API, so the official E2B SDK works out of the box.
General Settings
Set environment variables to point at your AgentENV server. See Environment Variables for values per deployment mode.
# Single-node example
export E2B_API_URL=http://127.0.0.1:8000
export E2B_SANDBOX_URL=${E2B_API_URL}
export E2B_API_KEY=e2b_000000
export E2B_ACCESS_TOKEN=dummy
TypeScript SDK
Setup
Install the SDK:
npm install e2b
Usage
import { Sandbox } from "e2b";
// Create a sandbox from a template
const sandbox = await Sandbox.create("<template-id>", {
apiKey: process.env.E2B_API_KEY,
});
// List running sandboxes
const running = Sandbox.list({
apiKey: process.env.E2B_API_KEY,
limit: 20,
query: { state: ["running"] },
});
console.log(await running.nextItems());
// Run a command inside the sandbox
sandbox.commands.run("echo hello world");
// Pause the sandbox
await Sandbox.Pause(sandbox.sandboxId, {
apiKey: process.env.E2B_API_KEY,
});
// Kill the sandbox
await sandbox.kill();
Replace <template-id> with a template that exists in your local template store. Use e2b template list or GET /v2/templates to see available templates.
Python SDK
Setup
Install the SDK:
pip install e2b
Usage
from e2b import Sandbox, SandboxQuery, SandboxState
# Reuse the environment variables set in your shell:
# E2B_API_URL / E2B_SANDBOX_URL / E2B_API_KEY
# Create a sandbox from a template
sandbox = Sandbox.create("<template-id>")
# List running sandboxes
running = Sandbox.list(
limit=20,
query=SandboxQuery(state=[SandboxState.RUNNING]),
)
print(running.next_items())
# Run a command inside the sandbox
result = sandbox.commands.run("echo hello world")
print(result.stdout, end="")
# Pause the sandbox
sandbox.beta_pause()
# Kill the sandbox
sandbox.kill()
E2B CLI
AgentENV is compatible with the E2B CLI, but we recommend using the aenv CLI for AgentENV workflows.
Common Issues
/dev/kvm not accessible
Symptom: Server fails to start with a KVM-related error.
Solution: Ensure your host has hardware virtualization enabled (Intel VT-x or AMD-V) and that /dev/kvm is readable by the current user. On most systems:
sudo usermod -aG kvm $USER
# Log out and back in for the group change to take effect
Permission denied for network operations
Symptom: Sandbox creation fails with network namespace or iptables errors.
Solution: The server requires both CAP_NET_ADMIN and CAP_SYS_ADMIN in
its effective, permitted, and inheritable sets. The installed systemd unit
configures these automatically. For a source checkout, use make start-server
or scripts/run-with-capabilities.sh <server-binary>; do not run the whole
server as root. Also verify that the runtime account belongs to the kvm group
and can open /dev/ublk-control.
Sandbox namespaces are missing from ip netns list
Symptom: Sandboxes are running, but ip netns list does not show their
network namespaces and ip netns exec <name> cannot find them.
Solution: AgentENV stores namespace mount points under
$AENV_RUNTIME_PATH/netns instead of /var/run/netns; the installed service
uses /run/aenv/netns. Inspect that directory directly and enter a namespace
by path when needed:
sudo nsenter --net=/run/aenv/netns/agentenv-ns-<slot> ip addr
Config file not found
Symptom: Error: config file not found
Solution: The server looks for config/default.toml by default. Either run from the repository root or set AENV_CONFIG_PATH:
export AENV_CONFIG_PATH=/path/to/your/config.toml
Port already in use
Symptom: Address already in use when starting the server.
Solution: Another process is using port 8000. Either stop it or change the listen address:
API_ADDR=0.0.0.0:8001 make start-server
Sandbox creation timeout
Symptom: POST /sandboxes returns a timeout error.
Solution: Check that runtime assets (Firecracker binary, kernel, rootfs) have been downloaded. The server auto-provisions them on first start, but network issues can cause failures. Run cargo run --bin server -- --setup-only to provision manually and see detailed errors.
Also check [envd].init_timeout_secs in your config. The default is 60 seconds. If the rootfs image is large, the in-guest envd daemon may need more time to initialize.
TODO: Expand with more common issues as they are reported.
AgentENV Architecture
AgentENV runs AI agents inside isolated, snapshot-capable Firecracker microVMs. Its core is a storage subsystem that provides layered block devices mountable into VMs and ublk-backed memory snapshot restore. The system also includes a per-node orchestrator managing sandbox lifecycle, and a prototype distributed control plane (gateway + scheduler) for multi-node routing.
System Overview
┌───────────────────────────────────────────────────────────┐
│ AgentENV Node │
│ │
│ ┌──────────┐ ┌──────────────┐ │
│ │ API │──>│ Orchestrator │ │
│ │ (Axum) │ │ (lifecycle) │ │
│ └──────────┘ └──────┬───────┘ │
│ │ │
│ ┌─────────▼───────────┐ │
│ │ Firecracker VM │ │
│ │ │ │
│ │ /dev/vda (rootfs) │ │
│ │ /dev/vdb (extra)───┼───┐ │
│ │ VM memory ─────────┼───┼──┐ │
│ │ │ │ │ │
│ └─────────────────────┘ │ │ │
│ │ │ │
│ Block device path: │ │ │
│ ┌────────────────────────▼──┐│ │
│ │ ublk (/dev/ublkbN) ││ │
│ │ userspace block device ││ │
│ └────────────┬──────────────┘│ │
│ │ │ │
│ ┌─────────────▼─────────────┐│ │
│ │ overlaybd ││ │
│ │ ┌───────┐ ┌───────┐ ││ │
│ │ │ upper │ │layer 0│ ... ││ │
│ │ │ (r/w) │ │(r/o) │ ││ │
│ │ └───────┘ └───────┘ ││ │
│ └───────────────────────────┘│ │
│ │ │
│ Memory restore path: │ │
│ ┌────────────────────────────▼───┐ │
│ │ ublk (/dev/ublkbM) │ │
│ │ read-only memory block device │ │
│ │ (shared across same-snapshot │ │
│ │ sandboxes via refcounting) │ │
│ └────────────┬───────────────────┘ │
│ │ │
│ ┌─────────────▼──────────────┐ │
│ │ overlaybd (mem layers) │ │
│ │ ┌───────┐ ┌───────┐ │ │
│ │ │snap N │ │snap 0 │ ... │ │
│ │ │(r/o) │ │(r/o) │ │ │
│ │ └───────┘ └───────┘ │ │
│ └────────────────────────────┘ │
└───────────────────────────────────────────────────────────┘
Storage
The storage subsystem turns layered image files into block devices mountable by VMs, and provides ublk-backed memory snapshot restore for snapshot resume. Four crates compose the active subsystem:
overlaybd (storage/overlaybd/)
LSMT (Log Structured Merge Tree) based layered image format.
Image structure: Each layer file has a HeaderTrailer (magic LSMT\0\1\2, UUID, flags, index/data offsets) and an array of DiskSegmentMapping entries (16 bytes each, bit-packed: 50-bit offset, 14-bit length, 55-bit physical offset, zeroed flag, layer tag). Layers are stacked: immutable compressed read-only layers at the bottom, a single writable upper layer on top.
Read path: ImageFile resolves a read request by searching layers top-down via the segment index. The first layer containing a mapping for the requested block range serves the data. Unmapped ranges in upper layers fall through to lower layers.
Write path: All writes append to the upper layer. The upper layer’s index is updated in memory and flushed on sync.
Backends (pluggable via VirtualFile trait):
LocalFile: io_uring pread/pwrite with optional O_DIRECTregistryfs_v2: OCI registry (remote layer download)tar: tar archive reading- Optional cache layer for decompressed block caching
Compression: zstd (level 3) with random-access jump tables and CRC32C checksums.
Snapshot: snapshot_runtime() copies upper layer data/index to a snapshot directory using reflink (CoW) where the filesystem supports it.
Key files: image_file.rs (high-level image), index_file.rs (LSMT stacking: LSMTReadOnlyFile, LSMTFile), format.rs (binary format), index.rs (segment mapping), zfile.rs (compression), snapshot.rs.
ublk (storage/ublk/)
Async userspace block device server using Linux’s ublk kernel driver. Exposes overlaybd images (or raw CoW files) as /dev/ublkbN block devices.
Device lifecycle:
UVMUblkCtrlBuildersendsADDto/dev/ublk-controlvia io_uringUringCmd- Kernel allocates device ID, creates
/dev/ublkcN(control) and/dev/ublkbN(block) - Per-queue worker threads start, each with a thread-local
AsyncIoRingand slab-allocated I/O slots - Kernel dispatches block I/O to mmap’d
ublksrv_io_descarrays; userspace processes them asynchronously delete_dev()tears down the device
Target implementations (UVMUblkTarget trait):
OverlaybdTarget: wrapsImageFilefor full layered image I/OBasicCowTarget: chunk-based copy-on-write over a read-only origin file. Per-chunkAtomicU8state tracking (Origin -> Copying -> Cow) prevents duplicate copies. Coalesces adjacent reads.
I/O buffers: AutoRegBuffer (zero-copy via sparse buffer table, kernel 6.8+) or UserBuffer (traditional allocation).
Key files: lib.rs (public API), ctrl.rs (device controller), dev.rs (device + queue management), queue.rs (I/O descriptor handling), io_buffer.rs, impls/cow.rs, impls/overlaybd_target.rs.
ublk-daemon (storage/ublk-daemon/)
Long-running daemon process (uvm-ublk-daemon) that manages all ublk devices in one process and communicates with the AgentENV node over a Unix domain socket.
- Supports RPCs for OverlayBD runtime creation for sandbox rootfs/extra drives, raw OverlayBD/COW device creation for non-runtime callers, warm-pool acquire/release, resize capability queries, restack snapshot, delete, and shutdown.
UblkDaemonClientspawns and monitors the daemon process from the node runtime.UblkDeviceManager(src/sandbox/ublk/device.rs) is the node-facing singleton that delegates lifecycle operations to the daemon client; device IDs are allocated in the daemon.
This separation keeps ublk device ownership and io_uring control in a dedicated process while the node server orchestrates lifecycle state.
storage-util (storage/util/)
Shared io_uring abstractions used by both ublk and overlaybd.
AsyncIoRing<S>: generic async io_uring wrapper with slab-basedRingFuturefor CQE delivery. Supports standard (64B) and extended (128B) SQE types.IoRingWorker: spawns dedicated worker threads with thread-local io_uring instances. MPSC channel submission eliminates cross-thread locking.ReloadableIDAllocator: O(1) bitmap-based ID allocation/recycling with free list. Supports reloading pre-occupied IDs on restart.
Sandbox integration (src/sandbox/ublk/ + src/sandbox/extra_drive.rs)
device.rs: owns the process-wideUblkDeviceManager, which talks touvm-ublk-daemonand creates / deletes / snapshots all runtime ublk devices.overlaybd.rs: materializes runtime configs (rewrites paths, creates symlinks to layer files) for rootfs and attached drives.extra_drive.rs: prepares user-specified extra block drives with rollback on failure. Read-only and writable drives now follow the same per-sandbox device lifecycle; the only semantic difference is whether overlaybd materializes a writable upper.
Memory Snapshot Restore
Memory snapshot restore uses ublk-backed overlaybd devices rather than userfaultfd. On resume, a read-only ublk device is created from the stacked memory overlaybd layers and passed to Firecracker as a BackendType::File memory backend. Firecracker mmaps the block device and COWs pages into anonymous memory on first write, so the underlying device is never modified.
Sharing: Multiple sandboxes booting from the same snapshot template share a single memory ublk device via reference counting. This allows the Linux page cache to be reused across all sandboxes using the same memory image, significantly reducing I/O for concurrent launches from the same template.
Memory snapshot creation: On pause, Firecracker’s native diff snapshot (SnapshotType::Diff) produces a sparse mem.bin (using mincore internally to find present pages). This sparse file is then packaged into an overlaybd layer via convert_sparse_mem_to_overlaybd. Parent layers from previous snapshots are stacked, forming the full layered memory image.
Note:
storage/uffd-core/contains an alternative userfaultfd-based memory restore implementation that is retained for reference but excluded from the workspace build.
Per-Node Subsystems
Each node is an AgentENV server binary (src/bin/server.rs) on a Linux host with /dev/kvm.
| Subsystem | Location | Responsibility |
|---|---|---|
| API layer | src/api/ | Axum HTTP server, OpenAPI endpoints, reverse proxy to sandbox services, node/admin APIs |
| Orchestrator | src/orchestrator/ | Sandbox lifecycle state machine (Creating, Running, Pausing, Paused, Resuming, Killing), auto-eviction, incremental runtime metrics, paused-sandbox persistence across restarts |
| Observability | src/observability/ | Node identity, machine info, request-time host metrics collection, node snapshot projection for admin APIs, optional scheduler heartbeat reporting |
| Sandbox | src/sandbox/ | Firecracker VM management, network namespaces, rootfs, envd communication, ublk devices (rootfs + memory), warm network/block/Firecracker pools |
| Snapshot + Template Builder | src/snapshot/, src/template/ | src/snapshot/ owns committed snapshot storage/runtime resolution; src/template/ provides the user-facing builder that publishes snapshots |
| P2P artifact transport | src/p2p/ | Optional project-wide artifact lookup, publish, and fetch layer with disabled and iroh-backed transports |
| Config | src/cfg.rs | TOML config for firecracker paths, machine specs, timeouts, shared pool tuning, observability metadata, P2P, and scheduler-report settings |
Sandbox Networking
Sandbox networking is managed by a process-wide NetworkManager (src/sandbox/network/manager.rs) plus per-slot Slot objects (src/sandbox/network/slot.rs).
- Each slot owns a stable index-derived address bundle from
[network.internal](defaulting to10.11.0.0/16and10.12.0.0/16) plus the fixed VM tap link169.254.0.20/30, together with the host veth name, namespace path, and iptables rules for one sandbox network namespace. - Network policy supports base allow/deny plus explicit egress rules. The
/sandboxes/{sandboxID}/networkendpoint replaces per-sandboxallowOut(CIDR/IP/domain patterns) anddenyOut(CIDR/IP only) rules at runtime; allow rules always take precedence. allocate_any()first tries a warm-slot pool and falls back to creating a new namespace/veth/tap/iptables setup on demand.- Warm-pool maintenance uses a single Condvar-driven background worker with low/high watermarks.
release()enqueues slots back to the warm pool; when maintenance is enabled, even releases above high watermark are first enqueued and then drained asynchronously by the worker.[pool]provides shared watermarks and[pool.network].maintenance_enabledcontrols network worker behavior.- Because the manager is a process-wide singleton, orchestrator shutdown explicitly calls
NetworkManager::shutdown()after deleting remaining sandboxes so cached slots are drained and no new allocations race with teardown. - Although calling
NetworkManager::shutdown()on exit is recommended for clean teardown, the manager also has aDropandlibc::atexithandler to best-effort cleanup of any remaining namespaces and veth interfaces on unexpected shutdown and during testing.
Snapshot resume can also use [pool.firecracker] to pre-spawn (network slot, Firecracker process) pairs. A warm entry transfers its network slot, process, and Firecracker CWD to the resumed sandbox, which avoids the spawn and API-socket wait in the resume critical path. [pool.block] controls the ublk daemon’s overlaybd warm-device pool; it shares the same top-level watermarks but performs async refill from request paths because reusable block devices are image/size-specific.
Observability Data Flow
The node observability path combines request-time host collection with request-time projection:
src/orchestrator/metrics.rsmaintains incremental runtime counters during lifecycle operations, including running sandbox count, starting sandbox count, allocated CPU/memory, and create success/failure totals.src/orchestrator/service.rspublishes those counters through atokio::sync::watchchannel whenever lifecycle state changes affect the node’s runtime accounting.src/observability/identity.rsresolves stable node identity fields such as node ID, cluster ID, service instance ID, package version, and build-time commit.src/observability/machine.rscaptures static machine descriptors from/proc/cpuinfo.src/observability/host.rscollects host CPU, memory, and disk usage each time a node snapshot is requested. CPU percent is derived from two/proc/statsamples; on the first request it takes both samples with a 100ms window to avoid returning a synthetic zero.src/observability/service.rsmerges the latest orchestrator counters, identity, machine info, request-time host metrics, and current sandbox ID roster into aNodeSnapshotreturned by the admin endpoints and reused by heartbeat reporting.src/observability/reporter.rsoptionally sends periodic heartbeat reports to scheduler over gRPC (Heartbeat) and performs best-effortUnregisterNodeon shutdown.- Scheduler report config can be provided from TOML (
[observability.scheduler_report]) and uses[cluster].scheduler_endpointas the shared scheduler address. The reporter enable flag, address, and interval can be overridden by env vars (AENV_OBSERVABILITY_SCHEDULER_REPORT_ENABLED,AENV_OBSERVABILITY_SCHEDULER_ENDPOINT,AENV_OBSERVABILITY_REPORT_INTERVAL_SECS). - If a P2P transport exposes a local endpoint, the reporter includes it in the scheduler heartbeat so other nodes can discover it.
This keeps node requests lightweight on orchestrator data: they avoid re-listing and sorting all sandboxes on every API call while still returning fresh host metrics.
The observability subsystem has two configuration-controlled scopes:
observability.enabled: controls whether the node observability service is constructed at all. When disabled, node/admin observability endpoints degrade rather than trying to synthesize partial snapshots.observability.scheduler_report.enabled: controls optional scheduler heartbeat reporting. It can be overridden byAENV_OBSERVABILITY_SCHEDULER_REPORT_ENABLED. When enabled, reporting requires[cluster].scheduler_endpointorAENV_OBSERVABILITY_SCHEDULER_ENDPOINT.
P2P Artifact Transport
src/p2p/ provides a project-wide artifact transport abstraction for modules that need to exchange validated files between runtime nodes. Consumers depend on the P2pTransport trait, whose main operations are lookup, lookup_with_hints, fetch, publish, unpublish, local_endpoint, and shutdown.
The default DisabledP2pTransport keeps the feature inert: lookups return no descriptor, publish is a no-op, and fetch fails with TransportDisabled. The IrohBlobsP2pTransport backend starts an embedded iroh endpoint, serves artifact bytes through iroh-blobs, and serves a small AgentENV catalog protocol over the same endpoint to map stable artifact keys to transport-neutral descriptors.
One P2P artifact key represents one logical artifact. Lookup returns at most one descriptor, selected from the local catalog first and then from discovered peers in order. Artifact descriptors contain the stable key, provider node ID, optional provider endpoint, backend-specific locator string, and module-defined JSON metadata. Backend locators stay opaque to callers; for iroh the locator is the iroh-blobs hash used for content-addressed fetch.
A successful remote fetch also best-effort advertises the fetched blob from the local node. This makes the fetching node a provider for later peers and lets artifacts spread through the cluster.
Peer discovery is decoupled behind P2pPeerDiscovery. In normal multi-node deployments, SchedulerPeerDiscovery periodically calls scheduler ListP2pPeers, filters by backend and cluster, and excludes the local node. StaticP2pPeerDiscovery and NoopP2pPeerDiscovery cover tests and disabled/local-only operation.
Snapshot publishing also uses the P2P layer as a best-effort acceleration path. After a snapshot repository commit succeeds, SnapshotManager advertises the fixed Firecracker artifacts and overlaybd layers. OSS-backed snapshot resolution tries P2P before object storage for fixed artifacts; POSIX-backed resolution does not consume P2P because the POSIX repository path is already the committed artifact source. Overlaybd layer reads are accelerated by the overlaybd P2P HTTP facade rather than by snapshot resolvers.
See P2P Artifact Transport for the detailed design.
Node API endpoints (E2B-compatible):
POST /sandboxescreate a sandboxGET /sandboxeslist sandboxesGET /sandboxes/{id}get sandbox metadataDELETE /sandboxes/{id}delete a sandboxPOST /sandboxes/{id}/pausepause (snapshot) a sandboxPOST /sandboxes/{id}/resumeresume from snapshotGET /nodesreturn node-level observability snapshotsGET /nodes/{id}return node details plus currently running sandboxesANY /proxy,ANY /proxy/{path}, routing-header fallback, and configured sandbox proxy hosts reverse proxy to sandbox services
Distributed Control Plane (prototype)
The multi-node control plane in services/ is a prototype. It routes client traffic across multiple AgentENV backend nodes.
Client ──HTTP──> Gateway (:8080) ──gRPC──> Scheduler (:9090)
│ │
│ ┌─────────────────────┘
│ │ node selection / lookup
▼ ▼
Node A (:8000) Node B (:8000)
Gateway (services/gateway/): HTTP reverse proxy. Extracts sandbox data-plane routes from headers (x-agentenv-sandbox-id / e2b-sandbox-id) or configured host-based proxy domains ({port}-{sandboxID}.{domain}). Host-based routes are only enabled for explicit gateway.sandbox_proxy_domains entries, require RFC 952/1123 DNS-label-compatible sandbox IDs, and require the full {port}-{sandboxID} label to fit the 63-character DNS label limit. Runtime nodes have their own [sandbox_proxy].domains setting for the same host-based URL shape and return the first configured domain in sandbox metadata. In multi-node deployments, repository helpers can apply one SANDBOX_PROXY_DOMAINS value to both gateway and runtime node configuration. Sandbox control-plane routes such as /sandboxes/{id}/pause are routed by sandbox ID from the URL path; sandbox data-plane traffic is not inferred from URL path alone. For new sandboxes, calls Schedule() to pick a node. For existing sandboxes, calls LookupNode(). After sandbox creation, calls RecordAssignment() to seed a sandbox-to-node binding. Without explicit routing headers, it also handles cluster aggregation of GET /sandboxes, GET /v2/sandboxes, GET /nodes, and resolves GET /nodes/{id} via scheduler before proxying to the resolved node.
Scheduler (services/scheduler/): gRPC service with pluggable node discovery and in-memory sandbox-to-node bindings, plus observed-node snapshots reported by runtime nodes. RPCs include Schedule, LookupNode, RecordAssignment, Heartbeat, ListObservedNodes, ListP2pPeers, GetNode, and UnregisterNode. Strategies: round_robin (default), random. Proto contract: services/api/proto/scheduler.proto. For P2P, scheduler stores and returns opaque peer endpoints from heartbeat records; artifact catalog lookup and byte transfer stay node-to-node.
Binding lifecycle:
RecordAssignmentcreates the initial binding immediately after sandbox creation succeeds.- Runtime heartbeats include the node’s full sandbox ID roster. Scheduler treats that roster as the source of truth for that node and removes bindings missing from the latest heartbeat.
binding_ttlis a freshness TTL for routing information, not a copy of sandbox timeout. If a binding stops being refreshed by gateway or heartbeats, scheduler drops it on the next lookup or roster reconcile.UnregisterNoderemoves the observed node record and proactively clears bindings owned by that node.
Discovery modes:
static: explicitscheduler.nodeslist from configkubernetes: EndpointSlice watch over the headlessagentenv-nodesService, using ready DaemonSet Pod IPs as backend endpoints
Limitations: All bindings are in-memory (lost on scheduler restart). After a scheduler restart, bindings are rebuilt from new sandbox creations plus the next heartbeat roster from each runtime node. Kubernetes discovery updates the schedulable node set dynamically, but binding persistence is still not replicated.
Deployment:
# local dev (single node)
make start-server && make -C services run-scheduler && make -C services run-gateway
# docker compose (multi-node)
make deploy-up # gateway + scheduler + 2 backend nodes
make deploy-down # teardown
# kubernetes (gateway + scheduler + daemonset runtime nodes)
make k8s-render
make k8s-apply
In Kubernetes deployments, AgentENV runtime nodes run as a privileged DaemonSet
so each host gets exactly one runtime Pod with access to /dev/kvm,
iptables/network-namespace operations, and a hostPath-backed workspace cache.
The deployment helpers materialize the DaemonSet ConfigMap from config/default.toml
at render/apply time so AgentENV runtime config remains single-sourced.
Directory Structure
storage/
├── overlaybd/src/ # layered image format (core)
│ ├── image_file.rs # high-level image abstraction
│ ├── index_file.rs # LSMT layer stacking
│ ├── format.rs # binary format (HeaderTrailer, DiskSegmentMapping)
│ ├── index.rs # segment mapping
│ ├── zfile.rs # zstd compression + jump tables
│ ├── snapshot.rs # snapshot capture
│ ├── local.rs # LocalFile backend (io_uring)
│ └── registryfs_v2.rs # OCI registry backend
├── ublk/src/ # userspace block device server
│ ├── lib.rs # public API
│ ├── ctrl.rs # /dev/ublk-control interface
│ ├── dev.rs # device + queue management
│ ├── queue.rs # I/O descriptor handling
│ ├── io_buffer.rs # zero-copy + traditional buffers
│ └── impls/ # target implementations
│ ├── cow.rs # BasicCowTarget
│ └── overlaybd_target.rs # OverlaybdTarget
├── ublk-daemon/src/ # ublk daemon (unix socket RPC)
│ ├── client.rs # daemon client used by node runtime
│ ├── server.rs # daemon server + request loop
│ └── protocol.rs # RPC message types
├── util/src/ # shared io_uring abstractions
│ ├── io_ring/ # AsyncIoRing, IoRingWorker
│ └── id_allocator.rs # bitmap-based ID allocation
└── uffd-core/src/ # userfaultfd memory restore (excluded from workspace, retained for reference)
├── handler.rs # UffdHandle, page fault event loop
├── backend.rs # MemoryImageBackend trait
├── overlaybd.rs # OverlaybdMemoryImage backend
├── process_vm_reader.rs # ProcessVmReader (process_vm_readv)
└── scm.rs # SCM_RIGHTS fd passing
src/
├── bin/server.rs # node binary entrypoint
├── api/ # HTTP API layer
├── orchestrator/ # sandbox lifecycle
├── observability/ # node identity + host/runtime metrics projection
├── sandbox/ # Firecracker VM management
│ ├── extra_drive.rs # extra drive preparation
│ └── ublk/ # storage integration
│ ├── device.rs # daemon-backed ublk device lifecycle
│ └── overlaybd.rs # runtime config materialization
├── snapshot/ # committed snapshot model, repository backends, runtime resolution
├── template/ # user-facing template builder over snapshots
└── cfg.rs # TOML config
services/ # prototype distributed control plane (Go)
├── gateway/ # HTTP reverse proxy
├── scheduler/ # gRPC node selection + binding
├── api/proto/ # protobuf contracts
└── shared/ # config, logging
Sandbox and Test Flow Guide
This document describes the public sandbox API, the global ConfigManager under
AgentENV/src/cfg.rs, and what AgentENV/tests/integration/fc.rs,
AgentENV/tests/integration/process.rs, AgentENV/tests/integration/snapshot.rs, and
AgentENV/tests/integration/orchestrator.rs validate.
1) Sandbox Public Interface
Types and responsibilities
-
FirecrackerSandboxConfig(AgentENV/src/sandbox/firecracker/config.rs)- Purpose: describes a fresh VM boot (kernel + tools drive + user image).
- Key fields
firecracker_binary: path to thefirecrackerexecutable.kernel_image: path tovmlinux.bin.tools_drive_version: immutable tools drive identity. The node resolves it to<deps_path>/tools/<version>/tools.ext4before mounting it as/dev/vda.user_image_config: overlaybd config for the writable user image (/dev/vdb).boot_args: kernel boot args (e.g.console=ttyS0 ... init=/init).vcpu_count,mem_size_mib: VM size.runtime_policy: socket/envd timeouts and poll intervals.firecracker_stdout_path,firecracker_stderr_path: capture FC logs. If unset, logs default towork_dir/logs/firecracker-stdout.logandwork_dir/logs/firecracker-stderr.log.envd_version: expected envd version for the guest image.env_vars: optional default environment variables injected after envd init.ublk_config: optional ublk-backed rootfs configuration.
- Convenience
from_global_config()builds a config fromConfigManagerusing the internal default template rootfs image. The image must already be resolved under<image-cache-root>/configs.from_global_config_with_user_image(...)builds a config from an already resolved overlaybd image config.- Runtime launches from committed snapshots are built through
FirecrackerSandbox::from_snapshot(&RunnableSnapshot, &SandboxLaunchConfig). uid/gid: File ownership (default0).
-
ProcessOpts(AgentENV/src/sandbox/process.rs)- Purpose: Options for starting a process inside the sandbox.
- Key fields
envs:HashMap<String, String>for process environment variables.cwd: Optional working directory.timeout: Optional max time to wait for completion.
- Builder methods:
with_envs(),with_cwd(),with_timeout().
-
ProcessOutput(AgentENV/src/sandbox/process.rs)- Purpose: Result of a completed process execution.
- Fields:
stdout,stderr,exit_code.
-
ProcessHandle(AgentENV/src/sandbox/process.rs)- Purpose: Handle to a running process inside the sandbox.
- Key methods
pid(): Returns the PID inside the guest VM.wait().await: Waits for exit and collects all output.send_stdin(data).await: Sends bytes to stdin.send_signal(signal).await: Sends a signal (e.g. SIGTERM).kill().await: Kills the process withSIGKILL.
-
FirecrackerSnapshotConfig(AgentENV/src/sandbox/firecracker/config.rs)- Purpose: describes how to resume a VM from snapshot + memory + base disk.
- Key fields
vm_state_path: Firecracker VM state file.mem_overlaybd_config: runtime memory overlay image config.base_rootfs_path: writable disk image or overlaybd rootfs image config paired with that snapshot.firecracker_binary: FC binary to use for resuming.runtime_policy: socket/envd timeouts and poll intervals.envd_version: guest envd version metadata.env_vars: optional default environment variables restored on start.
- Notes
FirecrackerSnapshotConfigreturned bypause()owns a tempdir (keeps files alive).get_rootfs_path()resolves the snapshot rootfs path used by runtime tests.
-
SandboxBackendandSandboxExecutor(AgentENV/src/sandbox/backend.rs)- Purpose: public traits for sandbox lifecycle and process execution.
- Key methods
SandboxBackend:start,start_nowait,wait_for_ready,pause,resume,stop.SandboxExecutor:run_command,run_command_with_opts,start_process.
-
FirecrackerSandbox(AgentENV/src/sandbox/firecracker/sandbox.rs)- Purpose: lifecycle controller for a single Firecracker instance.
- Main methods (detailed)
FirecrackerSandbox::new(FirecrackerSandboxConfig)- Creates a sandbox handle for a fresh boot.
- Does not start Firecracker; it only prepares the object.
- Each sandbox gets its own temporary work directory.
FirecrackerSandbox::resume_from_snapshot_config(&FirecrackerSnapshotConfig)- Creates a new sandbox handle and immediately starts it from a snapshot config.
- Uses
vm_state_path, the memory overlay image config, andbase_rootfs_pathfrom the config. - Returns a running sandbox after Firecracker and envd are ready.
FirecrackerSandbox::from_snapshot(&RunnableSnapshot, &SandboxLaunchConfig)- Creates a sandbox handle from committed snapshot state resolved for a node.
- Uses snapshot
vm_state.binplus node-local materializedmemory/image.jsonandrootfs/image.json. - Does not start Firecracker; call
start().awaitafterward.
start().await- Spawns the Firecracker process, waits for the API socket, starts the VM, then waits for guest-level envd readiness.
pause().await- Sends
PATCH /vmto pause the VM. - Creates snapshot artifacts including
vm_state.bin,mem.bin, the packaged memory image config, and rootfs snapshot state. - Returns a
FirecrackerSnapshotConfigthat owns the tempdir holding these files.
- Sends
resume().await- Resumes a paused VM in-place by sending
PATCH /vmwithResumed. - Use this when you want to keep the same sandbox instance.
- Resumes a paused VM in-place by sending
stop().await- Stops Firecracker, releases network resources, and cleans up optional ublk state.
run_command(cmd, args).await- Runs a command inside the sandbox via envd gRPC and waits for it to
complete. Returns
ProcessOutputwith stdout, stderr, and exit code.
- Runs a command inside the sandbox via envd gRPC and waits for it to
complete. Returns
run_command_with_opts(cmd, args, opts).await- Same as
run_commandbut acceptsProcessOptsfor setting env vars, working directory, and execution timeout.
- Same as
start_process(cmd, args, opts).await- Starts a long-running process inside the sandbox. Returns a
ProcessHandlethat can stream output, send stdin, or kill the process.
- Starts a long-running process inside the sandbox. Returns a
firecracker_stdout_path()/firecracker_stderr_path()- Resolved log paths used by this sandbox (including defaults).
work_rootfs_path()- Returns the path to the per-instance writable rootfs inside the work directory. Useful for inspecting or copying disk state after a run.
What happens under the hood (important for correct usage)
- Each
FirecrackerSandboxuses its own temp work directory. - Kernel is used via the configured host path.
- When resuming, the writable rootfs and memory image are materialized into the work dir so each instance can mutate independently. For overlaybd-backed snapshot-backed template launches, this means recreating runtime image configs and fresh writable uppers from the committed layer stack.
- Rootfs copies use copy-on-write helpers where available.
- Firecracker runs with
current_dir = work_dir, so relative paths work. - Firecracker logs default to
work_dir/logs/*unless explicit log paths are configured. start()waits for both the API socket and envd readiness.- Although the Firecracker process will be killed on drop, explicit
stop()is recommended.
Minimal usage example
#![allow(unused)]
fn main() {
use agentenv::sandbox::{FirecrackerSandbox, FirecrackerSandboxConfig, SandboxExecutor};
let mut cfg = FirecrackerSandboxConfig::new(
"/path/to/firecracker".into(),
"/path/to/vmlinux.bin".into(),
"/path/to/rootfs.ext4".into(),
);
cfg.boot_args = Some("console=ttyS0 reboot=k panic=1 pci=off init=/init".into());
let mut sandbox = FirecrackerSandbox::new(cfg)?;
sandbox.start().await?;
// Run a command inside the VM
let output = sandbox.run_command("echo", &["hello"]).await?;
assert_eq!(output.exit_code, 0);
println!("{}", output.stdout);
let snapshot = sandbox.pause().await?;
sandbox.stop().await?;
let mut resumed_sandbox = FirecrackerSandbox::resume_from_snapshot_config(&snapshot).await?;
resumed_sandbox.stop().await?;
}
Runtime prerequisites
- Linux host
/dev/kvmaccessible by current userdebugfs(e2fsprogs) if you use init injection or disk-inspection helpers
2) Global Config Manager (src/cfg.rs)
The tests and benchmarks use a shared global config manager based on
config/default.toml.
Config file format
AgentENV/config/default.toml (override via AENV_CONFIG_PATH or --config)
[firecracker]
boot_args = "your-kernel-boot-args"
binary_path = "/path/to/firecracker"
socket_timeout_secs = 3
socket_poll_ms = 20
# Optional override; defaults to $AENV_HOME/firecracker-work.
# work_dir = "/path/to/firecracker-work"
# Optional override; defaults to $AENV_HOME/logs/serial.
# serial_dir = "/path/to/serial"
# Optional; enables Firecracker's own logging when set to a non-empty level.
# log_level = "Info"
[kernel]
image_path = "/path/to/vmlinux.bin"
[tools]
version = "0.1.0-custom.1"
drive_path = "/path/to/tools.ext4"
[machine]
mem_size_mib = 128
vcpu_count = 1
[envd]
version = "0.5.15"
init_timeout_secs = 30
poll_ms = 10
[orchestrator]
auto_evict_interval_ms = 1000
default_sandbox_timeout_secs = 15
[snapshot]
repository_backend = "posix_fs"
local_cache_path = "$AENV_HOME/snapshot-local-cache"
[backend.posix_fs]
snapshot_store = "$AENV_HOME/snapshot-store"
[ublk]
enabled = false
TOML parameter reference
-
[firecracker]boot_args: Kernel command line. The runtime appendsinit=/initif not already present (tools drive provides the init script).binary_path: Absolute or relative path tofirecracker.socket_timeout_secs: Max time to wait for the Firecracker API socket.socket_poll_ms: Poll interval for checking socket existence.work_dir: Optional override for the per-sandbox Firecracker working directories. These directories include runtime sockets, symlinks, local logs, and writable OverlayBD upper layer data (overlaybd/upper.dataandoverlaybd/upper.index). Defaults to$AENV_HOME/firecracker-work.serial_dir: Optional override for the persistent output directory. Serial output is written under a per-sandbox subdirectory and not removed by the sandbox. Defaults to$AENV_HOME/logs/serial.log_level: Optional Firecracker log level (Error,Warning,Info,Debug,Trace, case-insensitive). When set to a non-empty value, Firecracker’s own logging is enabled and written to afirecracker.logfile in the sandbox’s log directory (the same directory used for serial output). If omitted or empty, Firecracker logging is disabled.
-
[kernel]image_path: Optional local kernel image path (vmlinux.bin). If omitted, AgentENV derives it fromdeps_pathand the bundled dependency manifest.
-
[tools]drive_path: Optional local tools drive source. When set, an explicitversionis required and setup imports it into the immutable version directory.
-
Template rootfs image
- User-visible rootfs images are selected when starting template builds through the optional
fromImagefield. - If
fromImageis omitted, AgentENV uses[image.resolver].default_image. - Standard Docker Hub shortnames such as
ubuntu:24.04andnode:20are normalized before image resolution. - Resolved image configs are cached under
<image-cache-root>/configs/<slug>-<hash>-image.json; local layer products are cached under<image-cache-root>/layers/; overlaybd-native remote block reads are cached under<image-cache-root>/remote-blocks/. - Registry authentication uses docker credentials from
~/.docker/config.jsonor$DOCKER_CONFIG/config.json.
- User-visible rootfs images are selected when starting template builds through the optional
-
[machine]mem_size_mib: Guest RAM size (default: 128).vcpu_count: Number of vCPUs (default: 1).
-
[envd]version: Expected envd version baked into the runtime image.init_timeout_secs: Max time to wait for the in-guest envd daemon to become ready after VM start (default: 30).poll_ms: Poll interval for envd health-check retries (default: 10).
-
[orchestrator]auto_evict_interval_ms: Poll interval for background timeout eviction.default_sandbox_timeout_secs: Default keep-alive timeout used by the orchestrator.auto_resume_min_sandbox_timeout_secs: When a data-plane request targets a non-running sandbox, automatically resume it (if auto-resume is enabled) and refresh its timeout for no-less than this duration.
-
[pool]low_watermark: Shared lower bound for warm resources. Defaults to2.high_watermark: Shared upper bound target for warm resources. Defaults to64.[pool.network].maintenance_enabled: Enables the background network-slot maintenance worker. Defaults totrue.[pool.block].enabled: Enables the ublk overlaybd warm-device pool.[pool.firecracker].enabled: Enables pre-spawned Firecracker processes for snapshot resume.
-
[snapshot]local_cache_path: Manager-owned node-local snapshot artifact/cache root. Defaults to$AENV_HOME/snapshot-local-cache.repository_backend: Snapshot repository backend. Defaults toposix_fs.
-
[backend.posix_fs]snapshot_store: Committed snapshot store root. Defaults to$AENV_HOME/snapshot-store.
-
[ublk]enabled: Enables ublk-backed rootfs handling.daemon_binary_path: Optional path touvm-ublk-daemon.daemon_socket_path: Optional unix socket path for daemon RPCs.daemon_log_path: Optional log file path for the daemon process.device_type:coworoverlaybd.
-
[ublk.overlaybd]global_config_path: path to the generated overlaybd runtime config. Required foroverlaybddevice_type. Per-image configs are derived from template buildfromImagevalues and live under<image.cache.root_dir>/configs.
Defaults and optional fields
[firecracker].binary_path: Optional. If omitted, AgentENV derives it fromdeps_pathand the bundled dependency manifest.[firecracker].boot_args: Optional. Defaults toconsole=ttyS0 reboot=k panic=1 pci=off.[firecracker].socket_timeout_secs: Optional. Defaults to 3 seconds.[firecracker].socket_poll_ms: Optional. Defaults to 20 ms.[firecracker].work_dir: Optional override. Defaults to$AENV_HOME/firecracker-work.[firecracker].serial_dir: Optional override. Defaults to$AENV_HOME/logs/serial.[kernel].image_path: Optional. If omitted, AgentENV derives it fromdeps_pathand the bundled dependency manifest.[tools].drive_path: Optional local source override; requires an explicit[tools].versionwhen set.[tools].url: Optional registry override; requires an explicit[tools].versionwhen set.[machine]: Optional. If omitted, defaults are 128 MiB RAM and 1 vCPU.[envd]: Optional, but template and sandbox flows expect a validversion.[envd].init_timeout_secs: Optional. Defaults to 30 seconds.[envd].poll_ms: Optional. Defaults to 10 ms.[orchestrator]: Optional. If omitted, orchestrator runtime defaults are used.[pool]: Optional. If omitted, network pool maintenance is enabled with low watermark 2 and high watermark 64; block and Firecracker process pools are disabled unless enabled in their component subsections.[snapshot]: Optional. If omitted,repository_backenddefaults toposix_fsand the local cache root derives fromAENV_HOME.[backend.posix_fs]: Optional. If omitted, the POSIX snapshot store defaults to$AENV_HOME/snapshot-store.[ublk]: Optional. If omitted, ublk is disabled.
Environment-based configuration
The config loader reads the full TOML file from the following sources:
AENV_CONFIG_PATH--config <path>config/default.toml
Runtime image and binary paths are configured in the TOML file itself.
Required fields
[kernel].image_path[tools].versionwhen[tools].drive_pathor[tools].urlis overridden[envd].version
If the [orchestrator] section is present, it must include
default_sandbox_timeout_secs.
ConfigManager (recommended entry point)
The helper is strict. If requirements are not met (missing files, invalid config), it returns an error and the test fails.
Path resolution order
AENV_CONFIG_PATHenvironment variable--configCLI flagconfig/default.toml(default)
Helper methods
ConfigManager::global()->Result<&'static ConfigManager>- Lazily initializes and returns a process-wide singleton.
ConfigManager::new()->Result<ConfigManager>- Loads config using the default resolution order.
ConfigManager::new_from_path(path)->Result<ConfigManager>- Loads a specific TOML file.
ConfigManager::config()->&AppConfig- Returns the parsed application config.
ConfigManager::global_config()->Result<&'static AppConfig>- Returns the global parsed config directly.
ConfigManager::get_orchestrator_config()->Option<OrchestratorConfig>- Returns orchestrator settings when configured.
AppConfig::resolved_snapshot_store()->PathBuf- Resolves the effective committed snapshot store root.
AppConfig::resolved_snapshot_local_cache_path()->PathBuf- Resolves the effective manager-owned node-local snapshot cache root.
Typical test setup flow
#![allow(unused)]
fn main() {
use agentenv::sandbox::{FirecrackerSandbox, FirecrackerSandboxConfig, SandboxExecutor};
let sandbox_config = FirecrackerSandboxConfig::from_global_config()?;
let mut sandbox = FirecrackerSandbox::new(sandbox_config)?;
sandbox.start().await?;
// Run commands via envd gRPC
let output = sandbox.run_command("echo", &["hello"]).await?;
assert_eq!(output.exit_code, 0);
let snapshot = sandbox.pause().await?;
sandbox.stop().await?;
let mut resumed_sandbox = FirecrackerSandbox::resume_from_snapshot_config(&snapshot).await?;
resumed_sandbox.stop().await?;
}
3) What the Integration Tests Validate
integration/fc.rs
-
microvm_startup_moves_to_runningBoots a fresh VM and verifies that:- Firecracker starts and accepts API calls.
- Kernel + rootfs can be configured without error.
- The VM can be cleanly shut down afterward.
-
microvm_pause_transitions_to_pausedValidates pause + snapshot creation by:- Pausing a running VM via FC API.
- Creating
vm_state.binandmem.bin. - Ensuring those files exist on disk.
-
microvm_resume_from_pause_transitions_to_runningValidates in-place resume by:- Pausing a VM.
- Resuming it in the same sandbox instance.
- Confirming the control flow completes without error.
-
microvm_resume_transitions_to_runningValidates resume into a new sandbox by:- Pausing and snapshotting a VM.
- Shutting down the original sandbox.
- Resuming from the snapshot in a new sandbox instance.
-
snapshot_preserves_disk_state_on_resumeConfirms disk state is preserved by:- Writing a marker file via
run_command. - Pausing and resuming from the snapshot.
- Reading the marker file after resume and asserting it persists.
- Writing a marker file via
-
snapshot_chain_survives_after_parent_snapshot_handle_is_droppedValidates multi-level snapshot chains by:- Creating a snapshot, resuming, creating a second snapshot.
- Dropping the first snapshot handle.
- Resuming from the second snapshot and verifying disk state.
- Also tests
pause_to_dirwith snapshot-owned inherited layer adoption.
-
microvm_can_access_internetConfirms guest networking works before and after snapshot resume. -
multiple_resumes_have_independent_disk_stateConfirms multiple resumptions are isolated by:- Resuming multiple VMs from a single snapshot sequentially.
- Each resumed VM verifies the shared marker and writes its own file.
- Verifying each snapshot has independent state.
integration/process.rs
-
run_command_captures_stdoutRunsecho hello worldand verifies stdout contains the expected string. -
run_command_captures_stderrRuns a shell command that writes to stderr and verifies stderr capture. -
run_command_reports_exit_codeRunsexit 42and verifies the exit code is reported correctly. -
run_command_with_opts_sets_env_and_cwdVerifies that environment variables and working directory options are honoured. -
run_command_handles_timeoutVerifies long-running commands fail when a timeout is configured. -
run_command_with_unbounded_outputVerifies excessive output is rejected instead of buffering forever. -
start_process_interactive_stdinStartscat, sends data via stdin, kills the process, and verifies the echoed output. -
start_process_send_signalStartssleep 300, sends SIGTERM, and verifies the process terminates with a non-zero exit code. -
run_command_after_snapshot_resumePauses a sandbox, resumes from a snapshot config, and verifiesrun_commandworks on the resumed instance.
integration/snapshot.rs
-
built_snapshot_can_be_loaded_by_alias_and_launchedBuilds a template-backed snapshot, loads it by alias, launches a sandbox from the resolved runnable snapshot, and verifies captured runtime state. -
listed_committed_snapshot_can_be_loaded_and_resolvedVerifiesSnapshotManager::list_committed()returns usable committed snapshots that can be loaded and resolved into runnable paths. -
loaded_runnable_snapshot_by_alias_can_be_resolvedVerifiesSnapshotManager::load_runnable()resolves alias-based lookups into runnable snapshot state in one step. -
delete_committed_snapshot_by_alias_removes_it_from_repositoryVerifies deleting a committed snapshot by alias removes the durable record. -
snapshot_rebuild_from_committed_snapshot_preserves_base_stateVerifies rebuilding from a committed snapshot preserves the base filesystem state while adding new changes. -
rebuild_merges_env_metadata_and_list_filter_can_select_by_idVerifies rebuilds merge environment metadata correctly and that committed snapshot listing can filter by snapshot id.
integration/orchestrator.rs
orchestrator_lifecycleVerifies the orchestrator can create, fetch, list, filter, keep alive, and delete template-backed sandboxes while keeping proxy lookup state in sync.
4) End-to-End Execution Checklist
- Provide Firecracker binary, kernel, tools drive, overlaybd runtime, and ublk daemon. These are automatically downloaded when the server starts, or you can run
cargo run --bin server -- --setup-onlyto provision runtime dependencies independently. - Provision host access once as root with
server --setup-host --runtime-user <user> --runtime-group <group>. The group is the runtime service group: it owns AgentENV state and receives ublk device access. Normal server startup performs validation only and never invokessudo. - Host setup installs a udev rule for
/dev/ublk-control,/dev/ublkc*, and/dev/ublkb*, so the runtime group can access the control and dynamic device nodes. - Update
config/default.tomlpaths, or pointAENV_CONFIG_PATHto a custom config file. - Ensure
/dev/kvmis accessible by the runtime user. - If you run template tests, ensure the host can run
regctl(server setup installs it automatically from the[regclient]manifest entry) and access the registry for templatefromImageresolution. - Run
scripts/tests/e2e/run_e2e.shfor API-level E2E coverage. The runner exportsE2E_TEMPLATE_USER_IMAGE. Suite05_template_lifecycle.shalso creates a template build withE2E_SHORT_USER_IMAGEto verify short-name image resolution. - Run
make test-agent-integrationto run theagentenvintegration test modules intests/integration/as a non-root user with the required capabilities, plus the Docker/MinIO-backed OSS snapshot repository test (tests/snapshot_oss_e2e_test.rs). - Run
make test-ublkto run theuvm-ublkandoverlaybdstorage tests, including the Docker/MinIO-backed OSS backend test (storage/overlaybd/tests/oss_backend_minio.rs).
4.1) Firecracker Client and Runtime Upgrade Checklist
When bumping AgentENV to a patched Firecracker runtime, update both the generated client API and the binary that setup downloads:
-
Replace
thirdparty/firecracker-client/firecracker.yamlwith the target Firecracker OpenAPI spec. -
Run
make firecracker-clientfrom the repository root and review generated changes underthirdparty/firecracker-client/. -
Build the patched Firecracker release binary on a Linux host. Package it as a gzip tar archive named
firecracker-{version}-{arch}.tgzcontaining afirecrackerexecutable. -
Update the
[firecracker]entry inconfig/deps_manifest.tomlwith the new version and a download URL template that supports{version}and{arch}. -
Before relying on the URL in tests, verify it is a direct download:
curl -L "<url>" -o /tmp/firecracker.tgz tar -tzf /tmp/firecracker.tgzThe archive listing should include
firecracker. -
Run setup against a clean or temporary Firecracker dependency directory so it really downloads the archive:
cargo run --bin server -- --config /path/to/config.toml --setup-only -
Validate the Rust side after code generation:
cargo fmt --check -p firecracker_client -p agentenv cargo check -p firecracker_client cargo check -p agentenv cargo test -p agentenv --lib
5) Running Custom Scripts
Run commands via envd
Once the sandbox is started (with a rootfs that includes the envd daemon), you can execute commands inside the VM using the process API:
#![allow(unused)]
fn main() {
// Simple command
let output = sandbox.run_command("ls", &["-la", "/tmp"]).await?;
println!("exit={} stdout={}", output.exit_code, output.stdout);
// Command with options
use agentenv::sandbox::ProcessOpts;
let opts = ProcessOpts::new()
.with_cwd("/tmp")
.with_envs([("KEY".into(), "value".into())].into());
let output = sandbox.run_command_with_opts("env", &[], opts).await?;
// Interactive / long-running process
let mut handle = sandbox.start_process("cat", &[], ProcessOpts::default()).await?;
handle.send_stdin(b"hello\n").await?;
handle.kill().await?;
let output = handle.wait().await?;
}
The process API uses envd’s gRPC ProcessClient under the hood. It requires
the envd daemon to be running inside the guest VM.
Run different scripts for multiple snapshot resumes
/init is not re-executed after resume, so per-instance behavior must come
from per-instance disk contents.
A typical pattern:
- Start a base VM and
pause()to create a snapshot. - For each instance, copy the snapshot rootfs and write a per-instance file
into it (for example using
debugfs). - Create a
FirecrackerSnapshotConfigthat points to that per-instance rootfs and resume.
The guest must keep a long-running dispatcher (started during the initial boot) that watches for these per-instance files and executes them after resume.
Template Builder and Testing
This document explains how the current template-facing build API maps onto the snapshot-first internals, how committed snapshots move through the repository/runtime boundary, and what the relevant tests validate.
1) Public Surface vs Internal Model
User-facing builder API
-
TemplateBuildSpec(src/template/build_spec.rs)- Describes a declarative build request.
- Builder helpers include:
from_existing_rootfs(path)from_overlaybd_configs(global_config_path, image_config_path)run(cmd)env(key, value)workdir(path)apt(packages)alias(alias)resources(cpu_count, memory_mib)
- The current builder implementation accepts overlaybd-backed fresh builds and
rebuilds from committed snapshots. Fresh ext4-rootfs builds are not wired
through
TemplateBuilder.
-
TemplateBuilder(src/template/builder.rs)- Preserves the external template semantics for build / rebuild flows, while
committed snapshot lifecycle operations live in
SnapshotManager. - Main methods:
new()with_local_store_root(path)build_and_publish(snapshot_manager, config).awaitrebuild_and_publish(snapshot_manager, config, snapshot_id, base_snapshot).await
- Preserves the external template semantics for build / rebuild flows, while
committed snapshot lifecycle operations live in
Snapshot-first internals
-
StoredSnapshot/RunnableSnapshot(src/snapshot/types/)StoredSnapshotis the durable committed manifest state.- It stores snapshot metadata plus logical rootfs / attached-drive layer descriptions and memory layers.
- Snapshot publication also persists
firecracker-manifest.jsonfor launch-time metadata such as rootfs/memory virtual size and attached-driveread_onlyflags. - It does not store fixed local artifact locations such as
mem_image.jsonor runtime-derivedrootfs/image.json. RunnableSnapshotis a node-local resolved view with concrete artifact paths and runtime-ready overlaybd image configs.
-
SnapshotRepository/SnapshotRuntimeResolver(src/snapshot/repository/interfaces.rs)SnapshotRepositoryowns committed durable state.SnapshotRuntimeResolverturns committed state into node-local runnable paths.
2) Build and Publish Flow
TemplateBuilder::build_and_publish(...) does the following:
- Prepare the build base from either:
- a committed snapshot (
rebuild_and_publish) - overlaybd configs (
from_overlaybd_configs)
- a committed snapshot (
- Start a temporary Firecracker sandbox with the requested CPU/memory.
- Execute template steps in order.
- Probe
envd, kernel, and firecracker versions from the running guest and executable. - Pause the sandbox and export a
FirecrackerSnapshotManifestcarrying metadata for repository publication - Publish those local artifacts into the configured snapshot repository.
The important boundary is:
- the builder API describes what to build
- local build artifacts are manager-owned temporary outputs
- committed snapshot records store logical durable state
- backend layout conventions determine where committed files and shared layers live
- runtime resolution turns committed snapshot state into node-local runnable paths
3) Repository Layout
The default backend is the POSIX filesystem backend. Given a backend root like
/path/to/store/repository:
- committed snapshot manifest:
/path/to/store/repository/snapshots/<id>/snapshot.json
- committed fixed-layout files:
/path/to/store/repository/snapshots/<id>/vm_state.bin/path/to/store/repository/snapshots/<id>/firecracker-manifest.json
- shared managed layers:
/path/to/store/repository/managed-layers/<digest>.overlaybd.commit
- alias bindings:
/path/to/store/repository/catalog/aliases/<alias>/path/to/store/repository/catalog/aliases/<alias>.lock
Alias locking is per-alias, not a single global aliases.lock file.
snapshot.json intentionally does not repeat local build-artifact paths such
as mem_image.json or rootfs/image.json. Runtime code derives node-local
image configs from:
- repository root
- snapshot id
- attached drive id
- managed layer digest
firecracker-manifest.json complements this by persisting launch-time virtual
size and attached-drive mode metadata without embedding node-local runtime
paths.
4) Load and Launch Flow
Typical runtime usage is:
SnapshotManager::load_committed(id_or_alias).awaitSnapshotManager::resolve_runnable(stored).awaitFirecrackerSandbox::from_snapshot(&runnable, &SandboxLaunchConfig::default())sandbox.start().await
If you want the two snapshot-manager calls combined, use
SnapshotManager::load_runnable(id_or_alias).await.
resolve_runnable(...) is where backend-neutral committed state becomes
node-local launch inputs:
memory_layers-> runtimememory/image.jsonrootfs.layers-> runtimerootfs/image.jsonattached_drives[].layers-> runtimedrives/<id>/image.jsonattached_drives[].read_only-> runtime mount mode fordrives/<id>firecracker-manifest.json-> runtime manifest hydrated with node-local artifact paths- committed
vm_state.bin-> runnable vm-state path
5) Minimal Example
#![allow(unused)]
fn main() {
use agentenv::cfg::ConfigManager;
use agentenv::image::ImageResolver;
use agentenv::sandbox::{FirecrackerSandbox, SandboxExecutor, SandboxLaunchConfig};
use agentenv::snapshot::SnapshotManager;
use agentenv::template::{TemplateBuildSpec, TemplateBuilder};
let builder = TemplateBuilder::new();
let snapshot_manager = SnapshotManager::new()?;
let config = ConfigManager::global()?.config();
let image_resolver = ImageResolver::new(config);
let image_config = image_resolver
.resolve(image_resolver.default_image())
.await?
.overlaybd_config_path;
let alias = "my-template-v1";
builder
.build_and_publish(
&snapshot_manager,
TemplateBuildSpec::new()
.from_overlaybd_config(image_config)
.alias(alias)
.resources(1, 128)
.run("mkdir -p /workspace")
.workdir("/workspace")
.env("MARK", "ready")
.run("printf '%s' \"$MARK\" > mark.txt"),
)
.await?;
let runnable = snapshot_manager
.load_runnable(alias)
.await?
.expect("template should exist");
let mut sandbox =
FirecrackerSandbox::from_snapshot(&runnable, &SandboxLaunchConfig::default())?;
sandbox.start().await?;
let out = sandbox.run_command("cat", &["/workspace/mark.txt"]).await?;
assert_eq!(out.exit_code, 0);
assert_eq!(out.stdout.trim(), "ready");
sandbox.stop().await?;
}
6) State Transition
The template-facing state transition is:
- local build artifacts
- committed
snapshot.json+firecracker-manifest.json+ fixed files + managed layers - node-local runtime-derived image configs
- Firecracker resume inputs
7) What the Tests Validate
tests/integration/snapshot.rs covers:
- building and publishing from overlaybd configs
- loading committed snapshots by alias / id through the snapshot manager
- resolving committed snapshots into runnable snapshots
- launching Firecracker sandboxes from resolved snapshots
- rebuilding from committed snapshots while preserving base state
- listing and deleting committed snapshots through the template API surface
tests/integration/snapshot_attached_drive.rs covers overlaybd attached-drive handling
across sandbox capture, publish, resolve, launch, and rebuild flows, including:
- committed metadata preserving
readOnly - runtime mount mode for readonly and writable drives
- rebuild keeping attached-drive metadata stable
tests/snapshot_oss_e2e_test.rs covers OSS-backed snapshot publication,
resolution, alias cleanup, missing managed-layer failure modes, and deletion.
Repository/backend unit tests under src/snapshot/repository/backends/ cover:
- artifact import
- alias handling
- committed snapshot deletion
- runtime cache behavior
8) Common Failure Areas
- overlaybd base config paths are missing or invalid
ublkis disabled while using overlaybd build bases- Linux host prerequisites for Firecracker / KVM / namespaces are missing
- repository alias conflicts during publish
- runtime resolver cannot materialize local paths from committed artifacts
Persistence Artifact Inventory
This document lists AgentENV artifacts that can remain on disk or in object storage beyond a single function call. It is organized by the module that owns each artifact’s lifecycle.
Path Roots
| Root | Default | Owner | Notes |
|---|---|---|---|
home_path | /var/lib/aenv | src/cfg.rs | Base for paths containing the literal $AENV_HOME placeholder. AENV_HOME_PATH overrides it before placeholder expansion. |
runtime_path | /run/aenv | src/cfg.rs, src/sandbox/network/* | Base for transient namespace mount points and daemon sockets. AENV_RUNTIME_PATH overrides it. |
deps_path | $AENV_HOME/deps | src/cfg.rs, src/setup/* | Base for downloaded runtime dependencies. AENV_DEPS_PATH can place these rebuildable assets outside home_path. |
| Firecracker sandbox work dirs | $AENV_HOME/firecracker-work with agentenv-fc- children | src/sandbox/firecracker/* | Per-sandbox runtime directories for sockets, symlinks, ublk runtime dirs, local logs, and writable OverlayBD upper layer data (overlaybd/upper.data, overlaybd/upper.index). An explicit [firecracker].work_dir overrides the root. |
firecracker.serial_dir | $AENV_HOME/logs/serial | src/sandbox/firecracker/* | Durable Firecracker stdout/stderr root, grouped by sandbox ID. An explicit [firecracker].serial_dir overrides the root. |
managed_snapshot_root | <firecracker-work-base>/managed-snapshots | src/sandbox/firecracker/* | In-process live snapshot artifact root used to keep captured snapshots alive until publish or drop. |
persisted_sandbox_store_path | $AENV_HOME/persisted-sandboxes | src/orchestrator/persistence/* | Durable paused sandbox records and artifacts. |
snapshot_store | $AENV_HOME/snapshot-store | src/snapshot/repository/* | Durable committed snapshot repository root. The configured backend uses <snapshot_store>/repository. Relative explicit paths are resolved against the config file directory. |
snapshot.local_cache_path | $AENV_HOME/snapshot-local-cache | src/snapshot/artifact_cache.rs, runtime resolvers | Node-local cache for materialized runtime artifacts. Relative explicit paths are resolved against the config file directory. |
image.cache.root_dir | $AENV_HOME/image-cache | src/image/*, overlaybd runtime | Node-local image cache root. Contains configs/, indexes/, commits/, and remote-blocks/. |
p2p.store_dir | $AENV_HOME/p2p/store | src/p2p/*, src/cfg.rs | Local store for P2P artifact transport backends. Relative explicit paths are resolved against the config file directory. |
image.cache.remote_blocks | <image.cache.root_dir>/remote-blocks | overlaybd runtime config | Remote block cache root. Overlaybd also stores premerged-index/ under this cache dir. Its size limit comes from image.cache.remote_blocks.max_size_gb. |
ublk.daemon_socket_path | $AENV_RUNTIME/ublk-daemon.sock | src/sandbox/ublk/*, storage/ublk-daemon/* | Unix socket used for server-to-daemon IPC. |
ublk.daemon_log_path | $AENV_HOME/logs/ublk-daemon.log | storage/ublk-daemon/* | Daemon log file supplied during config normalization. An explicit path overrides the default. |
Setup And Config
Owned by src/setup/* and src/cfg.rs.
| Artifact | Location | Contents | Purpose | Lifecycle |
|---|---|---|---|---|
| Firecracker binary | <deps_path>/firecracker/{version}/firecracker | Firecracker executable | VM process runtime | Created during setup when missing. Old versions are retained until manually removed. |
| CPU template helper | <deps_path>/firecracker/{version}/cpu-template-helper | Optional Firecracker helper executable | Detects host CPU config for cluster-wide CPU intersection | Extracted from Firecracker package when present. |
| Kernel image | <deps_path>/kernel/{version}/vmlinux.bin | Guest kernel | VM boot source | Downloaded during setup. Old versions are retained until manually removed. |
| Tools drive | <deps_path>/tools/{version}/tools.ext4 | Read-only ext4 image with envd/tools | Firecracker root drive shared by sandboxes and pinned by snapshots through its immutable release version | Extracted from OCI or imported from tools.drive_path during setup. Old versions are retained until manually removed. |
| Overlaybd tools | <deps_path>/overlaybd/bin/*, <deps_path>/overlaybd/lib/* | overlaybd-create, overlaybd-apply, overlaybd-commit, overlaybd-resize, libraries | OCI-to-overlaybd conversion and packaging | Installed during setup when release metadata does not match. |
| Overlaybd release metadata | <deps_path>/overlaybd/tools-release.json | Installed overlaybd release identifier | Detects whether tools need reinstalling | Rewritten on setup when release changes. |
| Overlaybd package downloads | <deps_path>/overlaybd/downloads/* | Downloaded package archives | Setup cache for overlaybd release packages | Kept after install; no automatic GC. |
| Generated overlaybd config | $AENV_HOME/overlaybd/overlaybd-global.json, $AENV_HOME/overlaybd/mem-overlaybd-global.json | Runtime global config, cache path, credentials config | Configures overlaybd runtime and memory snapshot overlaybd access | Rewritten during setup/startup. |
| Overlaybd runtime log | <deps_path>/overlaybd/overlaybd.log | Overlaybd runtime logs | Debugging | Appended by overlaybd runtime; no automatic GC. |
Firecracker Sandbox
Runtime
Owned by src/sandbox/firecracker/*.
| Artifact | Location | Contents | Purpose | Lifecycle | Rebuildable |
|---|---|---|---|---|---|
| Managed live snapshot root | <firecracker-work-base>/managed-snapshots/{sandbox_id}/{uuid}/... | Firecracker snapshot artifacts kept alive in-process | Holds captured running-sandbox snapshots until publish, and supports in-process pause state | Created by FirecrackerSandbox::pause() or snapshot(). Removed when PersistentSnapshotRootGuard drops. | No. |
| Snapshot VM state | snapshot artifact dir vm_state.bin | Firecracker VM state | Pause/resume and snapshot publish input | Created by Firecracker create_snapshot. Owner depends on caller: persister, managed root, or repository publish input. | No. |
| Raw memory diff | snapshot artifact dir mem.bin | Sparse Firecracker diff memory dump | Input to memory overlaybd conversion and source for virtual memory size | Created by Firecracker create_snapshot. Removed best-effort after the memory overlaybd layer is committed. | Temporary input only; not reconstructed after cleanup. |
| Memory overlaybd layer | snapshot artifact dir mem_overlaybd/overlaybd.commit | Sealed overlaybd layer for memory pages | Runtime memory restore and publish input | Created by packaging mem.bin. Imported into repository managed layers during publish. | No, unless raw mem.bin is still present. |
| Memory image config | snapshot artifact dir mem_image.json | Overlaybd image config stacking memory layers | Resume paused sandbox and publish memory layers | Written after memory conversion. Later repository resolvers regenerate runtime memory configs from committed layers. | Yes after publish; no for paused sandbox unless layers are known. |
| Rootfs snapshot config | snapshot artifact dir rootfs/image.json | Overlaybd image config for captured rootfs state | Resume paused sandbox and publish rootfs layers | Staged from live runtime config after restack/seal. | Yes only from associated layers and metadata. |
| Rootfs snapshot layer | snapshot artifact dir rootfs/snapshot.commit | Sealed writable upper from live rootfs | Captures disk writes since previous lower stack | Created by ublk daemon restack for writable overlaybd rootfs. | No. |
| Inherited runtime layers | snapshot artifact dir rootfs/inherited-layers/{index}/{source-file} | Snapshot-owned hard links or copies of inherited runtime-created lower suffixes | Removes dependence on previous managed snapshot or persisted sandbox artifact roots | Created during pause when inherited lowers come from the managed snapshot root or another sandbox/generation under the same persisted artifacts root. | No. |
| Attached-drive snapshot dirs | snapshot artifact dir drives/{drive_id}/... | Per-drive image config and snapshot layer | Captures writable attached-drive state | Created alongside rootfs snapshot for each drive. | No. |
| Firecracker work dir | configured work root, or consumed pool tempfile dir | API socket, symlinks, runtime dirs, local logs | Firecracker CWD for a sandbox | Created when sandbox handle is built, or moved from a warm pool entry; removed by owning TempDir. | Yes, except live state. |
| Firecracker serial logs | firecracker.serial_dir/{sandbox_id}/*, or warm pool work dir logs | Firecracker stdout/stderr | Debugging | Opened on spawn and appended. Warm logs are relocated when a warm process is consumed. Configured serial output is not automatically GC’d. | No, but disposable. |
| Firecracker logger output | firecracker.log in the same per-sandbox log directory as the serial logs | Firecracker internal logger (PUT /logger) | Debugging | Only created when firecracker.log_level is set to a non-empty level. Written by Firecracker itself; not automatically GC’d. | No, but disposable. |
Firecracker Pool
Owned by src/sandbox/firecracker/pool.rs.
| Artifact | Location | Contents | Purpose | Lifecycle |
|---|---|---|---|---|
| Warm pool work dir | system tempfile dir | Firecracker socket and warm process logs | Pre-spawned Firecracker process CWD | Created by pool maintenance as a TempDir. Removed when warm entry is cleaned up, or moved into the consuming sandbox and later dropped there. |
| Warm pool logs | warm pool work dir firecracker-stdout.log, firecracker-stderr.log | Firecracker output before the warm process is consumed | Debugging warm startup | Created on warm spawn. Relocated into sandbox log path when consumed if no explicit stdout/stderr override. |
Extra Drives
Owned by src/sandbox/extra_drive.rs and Firecracker snapshot code.
| Artifact | Location | Contents | Purpose | Lifecycle | Rebuildable |
|---|---|---|---|---|---|
| Extra-drive runtime dir | sandbox work dir extra-drive-runtime-{drive_id}/ | Runtime image.json, upper files, result file | ublk runtime for attached drive | Created when preparing extra drives. Released on rollback/stop; work dir cleanup removes files. | Not while running; otherwise unnecessary. |
| Extra-drive symlink | sandbox work dir extra-drive-{drive_id} | Symlink to /dev/ublkbN device path | Firecracker drive attachment path | Created after ublk runtime device creation. Removed on rollback or work dir cleanup. | Yes. |
| Extra-drive snapshot artifact | snapshot artifact dir drives/{drive_id}/... | Captured drive overlaybd config and commit layer | Preserve attached-drive writable state across pause/resume/publish | Created during sandbox snapshot/pause. Later owned by persister, managed root, or repository publish flow. | No. |
Image Resolver
Owned by src/image/*.
| Artifact | Location | Contents | Purpose | Lifecycle |
|---|---|---|---|---|
| Image config cache | <image.cache.root_dir>/configs/*-image.json | Digest-qualified overlaybd image configs for resolved user images | Avoids repeating OCI manifest classification and config generation | Written on image resolve. Regenerated if invalid or if referenced local lowers are missing. No unified GC today. |
| Image metadata sidecar | <image.cache.root_dir>/configs/*.metadata.json | Base env/workdir metadata from OCI image config | Preserves image launch context beside cached image config | Written after image config. Rebuilt if missing while image config is usable. |
| Overlaybd commit cache | <image.cache.root_dir>/commits/{digest-slug}/overlaybd.commit | Content-addressed overlaybd commit layers from standard OCI conversion, and target dirs for remote overlaybd-native layers | Node-local reusable layer store for user image layers | Standard OCI conversion writes commits. Remote overlaybd-native configs point dir here for runtime population. No unified GC today. |
| OCI conversion index | <image.cache.root_dir>/indexes/{source-digest}/...json | Mapping from OCI source layer/context to overlaybd commit digest and size | Skips repeated layer conversion when converted commits exist | Written after successful standard OCI conversion. No unified GC today. |
| Temporary OCI pull/conversion work | process temp dir | OCI layout and per-layer conversion workspace | Intermediate input for standard OCI conversion | Owned by TempDir; removed after conversion scope exits. |
Snapshot Repository
Owned by src/snapshot/repository/* and src/snapshot/types/*.
| Artifact | Location | Contents | Purpose | Lifecycle | Rebuildable |
|---|---|---|---|---|---|
| Snapshot records | POSIX: <snapshot_store>/repository/catalog/records/{id}.json; OSS: catalog/records/{id}.json | Snapshot ID, alias, source, resources, build status, committed logical metadata | Durable user-visible snapshot/template metadata | Created before template build or at publish. Updated when committed or errored. Deleted by snapshot delete. | No. |
| Snapshot aliases | POSIX: <snapshot_store>/repository/catalog/aliases/{alias}; OSS: catalog/aliases/{alias}.json | Alias-to-snapshot binding | Name lookup for templates/snapshots | Bound during create/publish with conflict checks. Deleted with record cleanup. | Partially, from records if aliases are still recorded. |
| Firecracker manifest | POSIX: <snapshot_store>/repository/snapshots/{id}/firecracker-manifest.json; OSS: artifacts/{id}/firecracker-manifest.json | Firecracker snapshot shape, virtual sizes, attached-drive metadata; path fields are hydrated at runtime | Runtime manifest template for launching committed snapshots | Persisted during publish. Removed with per-snapshot artifacts. | Partially, but kept as durable artifact. |
| VM state | POSIX: <snapshot_store>/repository/snapshots/{id}/vm_state.bin; OSS: artifacts/{id}/vm_state.bin | Firecracker VM state snapshot | Required to resume committed snapshots | Copied/uploaded during publish. Removed with per-snapshot artifacts. | No. |
| Managed snapshot layers | POSIX: <snapshot_store>/repository/managed-layers/{digest}.overlaybd.commit; OSS: managed-layers/{digest} | Content-addressed rootfs, attached-drive, and memory overlaybd commit layers | Shared immutable layer storage for committed snapshots | Imported during publish by descriptor or by hashing local descriptor-less layers. Usually not removed with a single snapshot. | Not from metadata alone; can be re-fetched only if source still exists. |
| Source-registry publications | SnapshotRecord.committed.disk_publications plus remote OCI registry objects | Published rootfs/attached-drive image refs and manifest digests | Lets compatible snapshot deltas live in their source registry | Created during OSS publish when image publishing is enabled. Rolled back on failure or delete when possible. | Registry-owned; record is required to locate. |
Repository records are the logical source of truth for committed snapshots. Build-time and runtime image.json files should not become committed truth unless they contain data that cannot be derived from committed metadata.
Snapshot Runtime Resolution
Owned by src/snapshot/artifact_cache.rs, src/snapshot/runtime_support.rs, and backend runtime resolvers.
| Artifact | Location | Contents | Purpose | Lifecycle | Rebuildable |
|---|---|---|---|---|---|
| Runtime rootfs config | <snapshot_local_cache_path>/runtime/{snapshot_id}/rootfs/image.json | Node-local overlaybd config for committed rootfs layers | Launch committed snapshot on the current node | Materialized during snapshot resolve and pinned by RunnableSnapshot lease. LRU-evictable after unpinned. | Yes. |
| Runtime memory config | <snapshot_local_cache_path>/runtime/{snapshot_id}/memory/image.json | Node-local overlaybd config for committed memory layers | Provides memory backend image to Firecracker resume | Materialized during snapshot resolve and pinned by lease. LRU-evictable after unpinned. | Yes. |
| Runtime attached-drive configs | <snapshot_local_cache_path>/runtime/{snapshot_id}/drives/{drive_id}/image.json | Node-local overlaybd configs for attached drives | Launch committed snapshot with attached drives | Materialized during snapshot resolve and pinned by lease. LRU-evictable after unpinned. | Yes. |
| OSS cached VM state | <snapshot_local_cache_path>/artifacts/{id}/vm_state.bin | Downloaded VM state object | Avoids repeated object-store download while pinned/cached | Downloaded by OSS resolver through LocalArtifactCache. LRU-evictable after unpinned. | Yes, by downloading again. |
| POSIX VM state reference | Repository vm_state.bin path | Direct path into POSIX repository | Avoids copying VM state into local runtime cache | Checked during resolve. Lifetime follows repository artifact. | No; repository artifact is source. |
LocalArtifactCache owns pinning, in-flight materialization deduplication, and LRU eviction for files it manages. It does not own the durable snapshot repository.
Orchestrator Persistence
Owned by src/orchestrator/persistence/*.
| Artifact | Location | Contents | Purpose | Lifecycle |
|---|---|---|---|---|
| Paused sandbox record DB | <persisted_sandbox_store_path>/records.db | RocksDB keyed by sandbox ID; values are compact JSON records containing version, lifecycle, sandbox metadata, artifact root, and backend state | Restores paused sandboxes after server restart | Record written after pause succeeds. Marked resuming before resume. Rolled back on resume failure. Deleted on resume/delete. |
| Paused sandbox artifact generation | <persisted_sandbox_store_path>/artifacts/{sandbox_id}/{uuid}/... | Firecracker snapshot artifacts generated by pause_to_dir | Durable artifacts for one paused sandbox generation | Allocated before pause, populated by backend, referenced by paused record. Removed during explicit delete or load_all orphan cleanup. |
Paused sandbox artifacts are not committed snapshot repository artifacts. They are owned by the sandbox persister and should not be shared as snapshot truth.
P2P Artifact Transport
Owned by src/p2p/*.
| Artifact | Location | Contents | Purpose | Lifecycle |
|---|---|---|---|---|
| Iroh blob store | <p2p.store_dir>/iroh | iroh-blobs content-addressed data and internal metadata | Local store for published artifacts and cached fetches | Created on P2P transport init, grows on publish and successful remote fetch. These blobs are collected by GC after unpublish removes those tags. |
| P2P catalog DB | <p2p.store_dir>/iroh/catalog.db | RocksDB keyed by artifact key; values are compact JSON artifact descriptors | Lets peers resolve stable keys into descriptors for this node | Loaded on startup. Individual entries are upserted on publish and after successful remote fetch, and deleted on unpublish. |
Snapshot publication writes best-effort P2P catalog entries after repository commit:
snapshot/v1/artifacts/{snapshot_id}/vm_state.binsnapshot/v1/artifacts/{snapshot_id}/firecracker-manifest.jsonoverlaybd-layer/v1/sha256:<digest>for referenced rootfs, memory, and attached-drive overlaybd commit layers
Those P2P entries are optional copies, not committed snapshot truth. Clearing <p2p.store_dir> can reduce peer-to-peer availability and force peers back to OSS or origin-registry reads, but it must not make a committed snapshot invalid by itself.
Ublk Daemon Runtime
Owned by storage/ublk-daemon/* and src/sandbox/ublk/*.
| Artifact | Location | Contents | Purpose | Lifecycle | Rebuildable |
|---|---|---|---|---|---|
| Overlaybd runtime image config | caller-provided runtime dir image.json | Rewritten overlaybd config with runtime-relative lower and upper paths | Opens ublk overlaybd target | Materialized during CreateOverlaybdRuntimeDevice. Removed during rollback or work dir cleanup. | Yes from source image config while not running. |
| Runtime upper data | runtime dir upper.data | Writable overlaybd upper data file | Stores live writes for writable devices | Created for writable runtime if source config has no existing upper. Restacked into snapshot layer on pause/snapshot. | No while running. |
| Runtime upper index | runtime dir upper.index | Log-structured upper index | Resolves writes in upper.data | Created with log-structured writable upper. Restacked into sealed snapshot layer. | No while running. |
| Runtime result file | runtime dir result.txt | Overlaybd result file path | Overlaybd runtime convention | Created/used by overlaybd runtime. Removed during cleanup. | Yes. |
| Ublk daemon socket | default $AENV_RUNTIME/ublk-daemon.sock | Unix socket | IPC between AgentENV server and ublk daemon | Created when daemon starts; removed/replaced by process lifecycle. | Yes. |
| Ublk daemon log | configured ublk.daemon_log_path, default $AENV_HOME/logs/ublk-daemon.log | Daemon logs | Debugging | Appended by the daemon; the deployment owns rotation and retention. | No, but disposable. |
Overlaybd Storage
Owned by storage/overlaybd/*.
| Artifact | Location | Contents | Purpose | Lifecycle | Rebuildable |
|---|---|---|---|---|---|
| Remote block cache | configured cacheConfig.cacheDir, derived from <image.cache.root_dir>/remote-blocks | Cached registryfs_v2 block ranges | Speeds remote overlaybd-native layer reads | Managed by overlaybd cache settings. | Yes. |
| Premerged index cache | cacheConfig.cacheDir/premerged-index/*.pmidx | Serialized merged read-only lower index | Speeds opening repeated lower stacks | Written asynchronously on read-only open. Pruned by size limit derived from cache size. | Yes. |
| Sealed overlaybd commit files | Various owner paths: image cache, snapshot dirs, repository managed layers | Overlaybd layer data and index trailer | Immutable lower layers for block devices and memory images | Lifecycle is owned by the module that stores the file. Overlaybd only defines the format and open/merge behavior. | Depends on owner. |
Ownership Rules
- Snapshot repository artifacts are durable user-visible state. Do not delete them from node-local GC code.
- Paused sandbox artifacts are durable only for the paused sandbox that owns them. Do not treat them as committed snapshots.
- Runtime
image.jsonfiles under snapshot local cache or ublk runtime dirs are derived artifacts. They should be rebuildable from committed metadata or source image configs. - Node-local
image-cacheartifacts are disposable cache, but content-addressed commits may be expensive to regenerate. The metadata-backed GC only reclaims commits no longer rooted by on-disk source configs, held by image-cache leases, or referenced by the in-process running set. Committed snapshots are durable SnapshotRepository state and do not pin ImageCache commits. See[image.cache.gc]in the configuration reference for scheduling. - P2P store contents are node-local and optional. Unpublish removes the local catalog entry and related blobs; clearing the store only affects peer-to-peer sharing and may require republishing or refetching artifacts.
- Logs and dependency downloads are operational artifacts. They are not part of sandbox or snapshot correctness, but may require separate retention policy outside image-cache GC.
AgentENV Proxy Design and Usage
This document describes the per-node reverse proxy that forwards requests into individual sandboxes. For the distributed routing layer (gateway to scheduler to node), see System Architecture.
Scope
Each AgentENV node runs a reverse proxy that accepts requests on its API server and forwards them to sandbox services over the sandbox interaction network. In a multi-node deployment, the gateway resolves sandbox ownership through the scheduler and forwards data-plane requests to the owning node’s proxy surface.
Current entrypoints:
ANY /proxyANY /proxy/{*proxy_path}- Any otherwise unmatched path that carries sandbox routing headers
- Host-based sandbox proxy requests when
[sandbox_proxy].domainsis configured:{port}-{sandboxID}.{domain}
Implementation references:
src/api/proxy.rssrc/orchestrator/service.rssrc/orchestrator/proxy.rs
Routing Contract
Each proxied request must identify:
- Sandbox ID
- Target port inside the sandbox service plane
Accepted headers:
x-agentenv-sandbox-idx-agentenv-target-port- E2B-compatible aliases:
e2b-sandbox-ide2b-sandbox-port
Validation:
- Sandbox ID must be a valid UUID format.
- Target port must parse as
u16and be greater than0.
Host-based routing derives both fields from Host. The configured domain must
match exactly after lowercase normalization and optional trailing-dot removal.
Sandbox IDs in host routes must be valid UUIDs and the target port must fit in
u16.
Runtime Route Model
AgentENV keeps an in-memory runtime route table in orchestrator.
- Route key:
SandboxId - Route value:
ProxyTarget(currently host interaction IP) plus route metadata (version,updated_at)
Design rule:
- Only
Runningsandboxes publish runtime routes. - Non-running states rely on metadata fallback (not route-table state).
Lookup behavior (proxy_lookup_for):
- If runtime route exists:
Ready(target) - Else read metadata:
- no metadata:
NotFound - metadata state is
Running:RouteMissing - metadata state is
Paused:Paused { auto_resume } - other states:
Unavailable(state)
- no metadata:
This keeps hot-path reads lock-light and avoids reading sandbox instance internals in API request paths.
Paused Sandbox Auto-Resume
/proxy can auto-resume paused sandboxes when lifecycle policy enables it.
- Proxy never reads sandbox metadata directly.
- Orchestrator lookup returns
Paused { auto_resume }, and proxy decides behavior from that signal. - Auto-resume is attempted once per request.
- Resume timeout update uses
EnsureMinimum(5 minutes):- effective sandbox timeout is
max(existing_timeout, 5 minutes)
- effective sandbox timeout is
- Proxy waits up to:
- test builds: short unit-test timeout
- non-test runtime:
60s
Request outcomes for paused sandboxes:
auto_resume = false:410 Goneauto_resume = trueand resume succeeds, then route becomes ready: request is forwarded normallyauto_resume = truebut resume/lookup fails:502 Bad Gatewayauto_resume = truebut resume wait times out:504 Gateway Timeout
Lifecycle Hooks and Race Hardening
Route publication/removal is tied to lifecycle transitions.
- Create/Resume success path:
- Persist metadata to
Running - Publish runtime route only if the launching sandbox handle is still the current handle
- Persist metadata to
- Pause/Delete/Rollback paths:
- Atomically detach sandbox handle and runtime route before stop/finalization
Race protections:
- Late route publication from stale handles is blocked via pointer identity check.
- Handle detachment and route removal happen in one critical section to reduce stale-route visibility windows.
- Launch rollback supports both transitional-state rollback and running-state rollback paths.
HTTP Forwarding Semantics
Path and Query
/proxyforwards to upstream//proxy/{*proxy_path}forwards raw URI path suffix after/proxy- Header-routed fallback requests forward the original request path
- Host-based requests forward the original request path
- Query string is forwarded unchanged
Important details:
- Percent-encoded path segments are preserved.
- Repeated leading slashes are preserved.
- Example:
/proxy//apiforwards as//api
- Example:
- Host-based proxy routing runs before Axum route matching. When a configured sandbox proxy host is used, the request is data-plane traffic even if its path resembles a control-plane API.
Header Handling
Control-plane routing headers are stripped before forwarding upstream:
x-agentenv-sandbox-idx-agentenv-target-porte2b-sandbox-ide2b-sandbox-port
Hop-by-hop headers are stripped on both request and response paths, including:
- Standard hop-by-hop headers (
Connection,Upgrade,TE,Trailer,Transfer-Encoding,Proxy-Authenticate,Proxy-Authorization,Keep-Alive) - Any extra headers nominated by
Connection
Forwarded headers are injected:
x-forwarded-hostx-forwarded-protox-forwarded-methodx-forwarded-uri
Streaming
HTTP bodies are proxied as streams (request and response), including SSE and large uploads/downloads.
WebSocket Semantics
WebSocket upgrade is supported through the same /proxy endpoints.
- Client upgrade request is validated and forwarded upstream.
- Bidirectional frame bridging is established after successful upstream handshake.
- Selected subprotocol from upstream is propagated to the client.
Handshake failure behavior:
- If upstream rejects with an HTTP response (for example
401,403,404), status/body are forwarded as-is. - Transport or connection failures return
502 Bad Gateway. - Handshake timeout returns
504 Gateway Timeout.
Error Mapping
400 Bad Request- Missing/invalid sandbox routing header
- Missing/invalid target port header
- Invalid upstream URI construction
404 Not Found- Sandbox not found
410 Gone- Sandbox is paused and auto-resume is disabled
- Sandbox exists but is not proxyable in current state
502 Bad Gateway- Upstream transport/connect failure
- Sandbox is
Runningbut runtime route is missing (RouteMissing) - Paused sandbox auto-resume failed
504 Gateway Timeout- Paused sandbox auto-resume timed out
- Upstream response header timeout
- Upstream websocket handshake timeout
Usage Examples
HTTP
curl -i \
-H 'X-API-Key: test-key' \
-H 'x-agentenv-sandbox-id: <sandbox-uuid>' \
-H 'x-agentenv-target-port: 8080' \
'http://127.0.0.1:8000/proxy/health?full=true'
E2B-compatible headers
curl -i \
-H 'X-API-Key: test-key' \
-H 'e2b-sandbox-id: <sandbox-uuid>' \
-H 'e2b-sandbox-port: 8080' \
'http://127.0.0.1:8000/proxy/status'
WebSocket
Example (generic):
- URL:
ws://127.0.0.1:8000/proxy/ws/echo - Headers:
x-agentenv-sandbox-id: <sandbox-uuid>x-agentenv-target-port: <port>- API auth headers as required by your deployment
Host-based
curl -i \
-H 'X-API-Key: test-key' \
'http://8080-<sandbox-uuid>.sandbox.example.com/status'
Testing Notes
Relevant test coverage exists in:
src/api/proxy.rsunit tests (HTTP, SSE, large body, websocket, headers, path preservation, error mapping)src/orchestrator/service.rsunit tests (route publication/removal behavior and stale-handle guard)- Integration lifecycle tests in
tests/integration/orchestrator.rs - E2E proxy suite in
scripts/tests/e2e/suites/06_proxy.sh(header compatibility and paused auto-resume behavior)
For environment-backed integration validation, use repository-prescribed integration targets.
Distributed Control Plane
The multi-node control plane lives in services/ as a separate Go module. It routes client traffic across multiple AgentENV backend nodes.
Components
- Gateway (
services/gateway/): HTTP reverse proxy that routes by sandbox ID - Scheduler (
services/scheduler/): gRPC service for node selection, sandbox-to-node binding, observed node snapshots, and P2P peer endpoint discovery
Build and Test
Prerequisites: Go 1.21+
# From services/
make build # builds both gateway and scheduler
make test # tests both services
make tidy # go mod tidy + formatting
make proto # regenerate protobuf
Run Locally
# Start scheduler (default: 127.0.0.1:9090)
make -C services run-scheduler
# Start gateway
make -C services run-gateway
Discovery Modes
The scheduler supports two node discovery modes:
- static (default): explicit node list from config
- kubernetes: watches EndpointSlices for a headless Service, using ready Pod IPs as backends
Deployment
Docker Compose
make deploy-up # gateway + scheduler + 2 backend nodes
make deploy-ps # status
make deploy-logs # logs
make deploy-down # teardown
Kubernetes
make k8s-render # render manifests
make k8s-apply # apply to cluster
Deployment model:
gateway: Deployment + ClusterIP Servicescheduler: single-replica Deployment + ClusterIP Serviceagentenv-node: privileged DaemonSet with/dev/kvmand hostPathagentenv-nodes: headless Service for scheduler EndpointSlice discovery
gRPC API
Proto contract: services/api/proto/scheduler.proto
RPCs: Schedule, ListNodes, LookupNode, RecordAssignment, Heartbeat, ListObservedNodes, ListP2pPeers, GetNode, UnregisterNode
Runtime node heartbeats may include an opaque P2pEndpoint containing a backend name and backend-specific address. The scheduler stores that endpoint with the observed-node record and returns ready peers through ListP2pPeers(cluster_id, backend, exclude_node_id). The scheduler does not query artifact catalogs and never forwards artifact data.
For full configuration details (header compatibility, timeouts, logging), see the services README.
P2P Artifact Transport
AgentENV has a project-wide P2P artifact transport layer in src/p2p/. It lets runtime modules publish, discover, and fetch validated files from peer nodes without depending on a concrete transport implementation.
The first concrete backend is embedded in the AgentENV server process and uses iroh plus iroh-blobs. The public API stays behind traits and serializable types so future backends can be selected by configuration.
Goals
- Provide one reusable node-to-node artifact transport for multiple AgentENV modules.
- Keep module code independent from concrete backends.
- Use scheduler for endpoint discovery and a lightweight in-memory artifact-to-node index, not artifact metadata storage or data proxying.
- Let transport backends use their native content-addressing or integrity checks while keeping module code backend-neutral.
- Keep disabled mode cheap and safe for single-node deployments.
Module Layout
| Path | Responsibility |
|---|---|
src/p2p/mod.rs | Public exports and transport_from_config factory |
src/p2p/config.rs | Resolved P2P config and transport kind selection |
src/p2p/types.rs | Transport-neutral artifact, endpoint, peer, and publish types |
src/p2p/transport.rs | P2pTransport trait and disabled implementation |
src/p2p/discovery/ | Peer discovery and artifact-index hints via scheduler/static/no-op implementations |
src/p2p/iroh/ | Embedded iroh + iroh-blobs backend and catalog protocol |
Public API
Consumers use Arc<dyn P2pTransport>:
#![allow(unused)]
fn main() {
#[async_trait]
pub trait P2pTransport: Send + Sync {
async fn lookup(&self, key: &P2pArtifactKey) -> Result<Option<P2pArtifactDescriptor>>;
async fn lookup_with_hints(
&self,
key: &P2pArtifactKey,
hints: &[P2pArtifactProviderHint],
) -> Result<Option<P2pArtifactDescriptor>>;
async fn fetch(&self, descriptor: &P2pArtifactDescriptor, destination: &Path) -> Result<u64>;
async fn fetch_bytes(&self, descriptor: &P2pArtifactDescriptor) -> Result<Bytes>;
async fn fetch_byte_range(
&self,
descriptor: &P2pArtifactDescriptor,
offset: u64,
len: usize,
) -> Result<Bytes>;
async fn publish(&self, request: &P2pPublishRequest) -> Result<()>;
async fn unpublish(&self, key: &P2pArtifactKey) -> Result<bool>;
fn local_endpoint(&self) -> Option<P2pEndpoint>;
async fn shutdown(&self) -> Result<()>;
}
}
P2pArtifactKey is a stable string chosen by the consuming module. One key represents one logical artifact. Callers should encode the complete cache context into that key and use descriptor metadata for module-specific validation.
P2pArtifactDescriptor contains:
key: stable lookup key.providers: nodes that can serve the artifact, represented asP2pPeervalues.backend_locator: optional transport-specific string used only by the matching backend. For iroh this is theiroh-blobshash.metadata: module-defined JSON used by callers to interpret the file.
P2pPublishRequest wraps a stable key, a local source path or in-memory bytes, optional metadata, and a P2pPublishMode. P2pPublishRequest::file(key, source) defaults metadata to null and publish mode to Copy; P2pPublishRequest::bytes(key, bytes) publishes already-buffered bytes. Copy allows the transport to copy bytes into its own store; Reference lets backends retain or index the existing file when supported.
Disabled Transport
DisabledP2pTransport is selected when [p2p].enabled = false or transport = "disabled".
lookupreturnsOk(None).publishsucceeds as a no-op.unpublishsucceeds as a no-op and returnsOk(false).fetchreturnsTransportDisabled.local_endpointreturnsNone.
This lets consumers call the P2P layer unconditionally while keeping default deployments unchanged.
Iroh Backend
IrohBlobsP2pTransport is selected with [p2p].enabled = true and [p2p].transport = "iroh".
At startup it:
- Creates the configured blob store directory.
- Opens an
iroh_blobs::store::fs::FsStorewith gated background GC. - Opens a RocksDB-backed local catalog at
<p2p.store_dir>/iroh/catalog.db. - Binds an
irohendpoint, optionally using[p2p].listen_addr. - Starts one router that serves both
iroh-blobsdata and AgentENV’s catalog protocol. - Publishes the local endpoint through
local_endpoint()so observability heartbeat can advertise it.
The backend uses one local catalog:
- Published catalog: artifact key to the descriptor this node can serve. The in-memory map is loaded from RocksDB on startup; each RocksDB value is a compact JSON descriptor.
Lookup first checks the local published catalog, then asks scheduler for nodes indexed under the artifact key, and finally falls back to discovered peers over the AgentENV catalog ALPN in hint/discovery order. Scheduler-indexed and fallback candidates are still verified by querying each node’s catalog; scheduler never stores locators or metadata. The descriptor carries the iroh blob hash in its backend locator, so fetch does not depend on prior in-process lookup state.
Fetch parses the descriptor’s backend locator as an iroh blob hash, passes all descriptor providers to iroh-blobs, downloads that blob into the local FsStore, and exports it to the requested destination. Iroh’s content-addressed blob transfer validates bytes against that hash. If any descriptor provider is local node, fetch exports directly from the local store.
After a remote fetch downloads the blob into the local store, the iroh backend best-effort advertises the local copy. It applies the same deterministic retention tag used by publish, builds a descriptor with this node as the only local catalog provider, upserts that descriptor into the local catalog, and records the key in scheduler. This makes the fetching node a provider for later peers and lets artifacts spread through the cluster. Advertisement or scheduler-index failures are logged and do not fail fetch; only the download and destination export are part of the required fetch result.
Publish imports the local file into FsStore, applies a deterministic named tag derived from the artifact key (agentenv:p2p:v1:{sha256(key)}), builds a descriptor with this node as the only local catalog provider, local endpoint, iroh blob hash locator, and request metadata, upserts that descriptor into the local catalog, and best-effort records the key in scheduler.
Unpublish removes the key from the local catalog, deletes the deterministic named tag, forgets the key in scheduler, persists the catalog, and returns whether a local publication was removed. Deleting the tag stops future catalog lookup immediately, but it does not synchronously delete the blob bytes. The iroh store reclaims untagged blobs through its GC.
GC is gated to avoid periodic full-store scans when there is no known deletion work:
- The transport starts with one pending GC pass, so a restart can clean up blobs whose tags were removed before a previous process exited.
unpublishmarks GC as pending after deleting the retention tag.- Each GC interval wakes up the iroh-blobs GC task, but the configured
add_protectedcallback aborts the mark/sweep pass unless pending GC is set. - When pending GC is set, one full mark/sweep pass runs. It preserves tagged and temp-tagged blobs and deletes blobs that are no longer protected.
Scheduler Discovery And Artifact Index
The scheduler is a directory for node endpoints and artifact-to-node hints only.
- AgentENV starts the configured P2P transport in
src/bin/server.rs. - If the transport exposes
local_endpoint(),ObservabilityReporterincludes it in heartbeat requests. - Scheduler stores the endpoint with the observed-node record.
SchedulerPeerDiscoveryperiodically callsListP2pPeers(cluster_id, backend, exclude_node_id).- Scheduler returns ready nodes with non-empty endpoints for the requested backend.
- The transport dials peers directly for catalog lookup and byte transfer.
For artifact lookup acceleration, the iroh backend also calls:
RecordP2pArtifact(cluster_id, backend, key, node_id)after publish and after successful remote fetch advertisement.ForgetP2pArtifact(cluster_id, backend, key, node_id)after local unpublish.LookupP2pArtifact(cluster_id, backend, key, exclude_node_id)before broad peer polling.
Scheduler keeps this artifact index in memory and stores only key-to-node mappings. Lookup responses are filtered through the same ready-node and backend endpoint checks as ListP2pPeers. Node unregister removes all artifact mappings for that node. Scheduler does not persist artifact catalogs, inspect artifact metadata or locators, or proxy bytes.
Snapshot And Overlaybd Consumers
src/snapshot/p2p.rs is the snapshot-store integration layer. It is intentionally small: the snapshot manager publishes artifacts after the configured repository commit succeeds, and runtime resolvers treat P2P as an optional acceleration path rather than committed truth.
Snapshot publication advertises:
- Fixed Firecracker artifacts under
snapshot/v1/artifacts/{snapshot_id}/{relative_path}:vm_state.binis published from its local file path.firecracker-manifest.jsonis serialized from the committed manifest and published as bytes.
- Overlaybd layers referenced by the snapshot’s rootfs, memory, and attached-drive image configs. These are not published under a snapshot-specific key. They reuse the overlaybd layer artifact protocol owned by
src/overlaybd/p2p/artifact.rs, with keys likeoverlaybd-layer/v1/sha256:<digest>andLayerMetadataunderstood by the overlaybd HTTP facade.
That split is important. Snapshot fixed artifacts are scoped to one snapshot ID and are only consumed by snapshot runtime resolvers. Overlaybd commit layers are content-addressed and may be consumed by any overlaybd runtime path, including foreground range reads through /p2p-http/{origin}. Do not add a second snapshot-specific key format for overlaybd layers; doing so publishes bytes that overlaybd cannot discover.
OSS snapshot resolution consumes fixed artifacts through P2P first:
vm_state.binis fetched into the node-localLocalArtifactCachefrom P2P, falling back to OSS on miss or fetch failure.firecracker-manifest.jsonis fetched withfetch_bytesfrom P2P, parsed in memory, and falls back to OSS on miss, fetch failure, or parse failure.
POSIX snapshot resolution does not consume P2P. A POSIX repository is already a directly accessible committed artifact store; if a required file is missing, the resolver returns ArtifactNotFound instead of repairing the repository from peers. Overlaybd layer acceleration for runtime block reads remains the responsibility of the overlaybd P2P facade, not the POSIX snapshot resolver.
Snapshot P2P publish is best-effort. A failed P2P publish logs a warning and does not roll back the committed snapshot record. This preserves the repository as the source of truth while making newly published artifacts visible to peers before slower backend downloads or uploads become the bottleneck.
Integrity And Ownership
The P2P transport does not define what an artifact means. Each consuming module owns:
- Constructing stable keys.
- Writing metadata into publish requests.
- Validating metadata before trusting a fetched artifact.
- Deciding whether and when to publish local artifacts.
This boundary keeps the transport reusable and avoids baking module-specific cache semantics into src/p2p.