Technical Documentation: superradcompany/microsandbox
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
Microsandbox runs untrusted workloads inside fast, local microVMs: AI agents, user code, plugins, CI jobs, dev environments, scrapers, and automation.
- Hardware Isolation: Hardware-level isolation with tiny virtual machines.
- Fork & Snapshot: Fork live sandboxes[^demo]. Save running sandbox state and restore later.
- Cross Platform: Runs on Linux, macOS, and Windows.
- OCI Compatible: Runs standard container images from Docker Hub, GHCR, or any OCI registry.
- Docker-Like Workflows: Familiar image, command, shell, and volume workflows.
- Instant Startup: Average boot times[^boot-time] under 100 milliseconds.
- Embeddable: Spawn VMs right within your code. No setup server. No long-running daemon.
- Secrets That Can't Leak: Unexploitable secret keys that never enter the VM.
- Long-Running: Sandboxes can run in detached mode. Great for long-lived sessions.
- Agent-Ready: Your agents can create their own sandboxes with our Agent Skills and MCP server.
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) Getting Started
Install the CLI
shcurl -fsSL https://install.microsandbox.dev | sh # 🍎 macOS / 🐧 Linuxpowershellirm https://install.microsandbox.dev/windows | iex # 🪟 WindowsWe also support other package managers →
shbrew install superradcompany/tap/microsandboxshnpm i microsandboxshuv tool install microsandboxshcargo install microsandbox
Start creating sandboxes once installed:
shmsb run ubuntu
Install the SDK
shnpm i microsandbox # 🟦 TypeScriptshcargo add microsandbox # 🦀 Rustshuv add microsandbox # 🐍 Pythonshgo get github.com/superradcompany/microsandbox/sdk/go # 🐹 Go
Requirements:
- macOS: Apple Silicon.
- Linux: KVM enabled.
- Windows: WHP enabled.
Warning: Microsandbox is still beta software. Expect breaking changes, missing features, and rough edges.
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) CLI
The msb CLI provides a complete interface for managing sandboxes, snapshots, images, and volumes.
Run a Command
shmsb run python -- python3 -c "print('Hello from a microVM!')"
Named Sandboxes
sh# Create and start a named sandbox msb create --name app pythonsh# Execute commands msb exec app -- python -c "import this" msb exec app -- curl https://example.comsh# Fork a running sandbox. msb fork app --name app2 msb exec app2 -- python -c "print('An independent copy!')"sh# Snapshot now, resume later. msb snap create --sandbox app --full -o snapshot.msb msb snap restore snapshot.msb --name app3Forking duplicates live execution. Restoring starts from saved state; add
--cow-memto share unchanged memory from a full snapshot. Snapshot generations describe progression, while branches describe distinct paths through snapshot history. The old--forkedrestore flag remains a deprecated alias. See forking and snapshots.sh# Lifecycle msb stop app msb start app msb rm app
Image Management
shmsb pull python # Pull an image msb image ls # List cached images msb image rm python # Remove an image
Configuration File
shmsb run --conf sandbox.yaml -- octocatyaml# sandbox.yaml image: python:3.12 memory: 64M network: allow: - api.github.com scripts: octocat: | python - <<'PY' import urllib.request request = urllib.request.Request( "https://api.github.com/octocat", headers={"User-Agent": "microsandbox-example"}, ) with urllib.request.urlopen(request) as response: print(response.read().decode()) PY
Status & Inspection
shmsb ls # List all sandboxes msb ps app # Show sandbox status msb inspect app # Detailed sandbox info msb metrics app # Live CPU/memory/network stats
[!TIP]
Run:
·msb --helpfor quick help menu.
·msb --treefor complete command hierarchy and descriptions.
·msb <command> --treefor a specific command tree.
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) SDK
The SDK lets you create and control sandboxes directly from your application. Sandbox.builder("...").create() boots a microVM as a child process. No infrastructure required.
Run Code in a Sandbox
typescriptimport { Sandbox } from "microsandbox"; await using sandbox = await Sandbox.builder("my-sandbox") .image("python") .cpus(1) .memory(512) .create(); const output = await sandbox.exec("python", [ "-c", "print('Hello from a microVM!')", ]); console.log(output.stdout());Rust Example →
rustuse microsandbox::Sandbox; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let sandbox = Sandbox::builder("my-sandbox") .image("python") .cpus(1) .memory(512) .create() .await?; let output = sandbox .exec("python", ["-c", "print('Hello from a microVM!')"]) .await?; println!("{}", output.stdout()?); sandbox.stop().await?; Ok(()) }Python Example →
pythonimport asyncio from microsandbox import Sandbox async def main(): sandbox = await Sandbox.create( "my-sandbox", image="python", cpus=1, memory=512, ) output = await sandbox.exec("python", ["-c", "print('Hello from a microVM!')"]) print(output.stdout_text) await sandbox.stop() asyncio.run(main())Ruby Example →
rubyrequire "microsandbox" sandbox = Microsandbox::Sandbox.create( "my-sandbox", image: "python", cpus: 1, memory: 512, network: { allowed_hosts: ["api.openai.com"], allowed_ports: [443] }, secrets: [{ env: "OPENAI_API_KEY", value: ENV.fetch("OPENAI_API_KEY"), allowed_host: "api.openai.com" }] ) output = sandbox.exec("python", ["-c", "print('Hello from a microVM!')"]) puts output.stdout sandbox.stopSee the Ruby SDK guide for installation, lifecycle,
networking, and backend details.Go Example →
gopackage main import ( "context" "fmt" "log" microsandbox "github.com/superradcompany/microsandbox/sdk/go" ) func main() { ctx := context.Background() // Downloads the microsandbox runtime to ~/.microsandbox/ on first run. if _, err := microsandbox.EnsureRuntime(ctx, microsandbox.RuntimeConfig{}, microsandbox.InstallOptions{}); err != nil { log.Fatal(err) } sandbox, err := microsandbox.CreateSandbox(ctx, "my-sandbox", microsandbox.WithImage("python"), microsandbox.WithCPUs(1), microsandbox.WithMemory(512), ) if err != nil { log.Fatal(err) } defer sandbox.Stop(ctx) output, err := sandbox.Exec(ctx, "python", []string{"-c", "print('Hello from a microVM!')"}) if err != nil { log.Fatal(err) } fmt.Println(output.Stdout()) }
The first call to
create()pulls the image if it isn't cached locally, so it may take longer depending on your connection. Subsequent runs reuse the cache.
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) Guides
Practical ways to put microsandbox to work:
• Docker in a Sandbox: Run Docker without touching the host daemon.
• OpenCode: Give a coding agent an isolated project workspace.
• Browser Use: Run an AI browser agent inside a microVM.
• Playwright: Run headless browser jobs inside a microVM.
• Warm Workers: Snapshot a toolchain and launch clean workers.
• Migration Rehearsal: Test a database migration, then restore the baseline.
• GitHub Actions Runner: Run each self-hosted job in a disposable microVM.
• Documents to PDF: Convert untrusted documents in a fresh offline worker.
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) Community Showcase
Agent frameworks & runtimes
• Eve by Vercel: Agent framework that ships microsandbox as a sandbox backend.
• Agentic Coding Quickstart by U.S. GSA: From zero to a running AI coding agent with USAi in minutes.
• Condukt and Once by Tuist: Elixir agentic engine, and cacheable actions that run in fresh sandboxes.
• langchain-microsandbox by kenwoodjw: Microsandbox integration for LangChain Deep Agents.
• Smithers by Smithers: Agent workflows with full observability, rewind, fork, and replay.
• AgentConnect: Bring multiple AI agents into your team's chats, issues, and pull requests.
• wrap by Tobi Lütke: Run coding agents and project commands in isolated Arch Linux microVMs.
• Agent VM by Wiren Board: Run AI agents in safe VMs scoped to a local folder.
Tools & infrastructure
• h5i by h5i: Secure, auditable browser for AI agents, written in pure Rust.
• Devsy by Devsy: Deploy devcontainers onto any cloud, Kubernetes cluster, or Docker host.
• OpenWork by Different AI: Open-source Claude Cowork alternative with a microsandbox image.
Guides & showcases
• Awesome Microsandbox by ya-luotao: Curated list of SDKs, integrations, tools, and resources.
• msb-omarchy by ya-luotao: Omarchy desktop with graphics inside a microVM on Apple Silicon.
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) AI Agents
Agent Skills
Teach any AI coding agent how to use microsandbox by installing the Agent Skills. Works with Claude Code, Cursor, Codex, Gemini CLI, GitHub Copilot, and more.
shnpx skills add superradcompany/skills
MCP Server
Connect any MCP-compatible agent to microsandbox with the MCP server. Provides structured tool calls for sandbox lifecycle, command execution, filesystem access, volumes, and monitoring.
sh# Claude Code claude mcp add --transport stdio microsandbox -- npx -y microsandbox-mcp
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) Documentation
For guides, API references, and examples, visit the microsandbox documentation.
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) Contributing
Interested in contributing to microsandbox? Check out our CONTRIBUTING.md for guidelines and DEVELOPMENT.md for build, test, and release instructions.
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) License
This project is licensed under the Apache License 2.0.
[](./#gh-dark-mode-only)[](./#gh-light-mode-only) Acknowledgements
Special thanks to all our contributors, testers, and community members who help make microsandbox better every day! We'd like to thank the following projects and communities that made microsandbox possible: libkrun and smoltcp
[^demo]: We have cool demos showcasing vm pause/resume, snapshotting, and forking: Mario Never Dies and Multiverse of Madness
[^boot-time]: Boot time refers to guest boot on an M1 machine.
Overview (`examples/README.md`)
Examples
Examples showing how to use the microsandbox SDK in TypeScript, Rust, Python, Go, and Ruby.
Each language directory has its own README with setup and run instructions.
Most configuration-focused examples intentionally replace their fixed-name sandbox so every run uses the image, resources, mounts, networking, and secrets shown in the source. The interactive shell-attach example instead uses connect-or-create because preserving its workspace across runs is useful. Examples that generate a unique name use strict creation.
| Language | Directory | README |
|---|---|---|
| TypeScript | typescript/ |
typescript/README.md |
| Rust | rust/ |
rust/README.md |
| Python | python/ |
python/README.md |
| Go | ../sdk/go/examples/ |
../sdk/go/examples/README.md |
| Ruby | ../sdk/ruby/examples/ |
../sdk/ruby/README.md |
Prerequisites
Some examples use git submodules for sample Alpine rootfs assets:
git submodule update --init --recursiveExamples
| Example | Description |
|---|---|
lifecycle-convergence |
Live convergence, restart, destroy, and stale-handle identity-safety checks |
root-oci |
Create a sandbox from an OCI image (e.g. alpine) |
root-bind |
Create a sandbox from a local bind-mounted directory |
root-block |
Create a sandbox from a qcow2 disk image |
rootfs-patch |
Pre-boot filesystem modifications (files, dirs, appends) |
init-handoff |
Hand PID 1 inside the guest off to systemd |
volume-named |
Persistent named volume shared between sandboxes |
volume-disk |
Mount raw / qcow2 disk images at arbitrary guest paths |
fs-read-stream |
Stream a large file from the sandbox in chunks |
metrics-stream |
Subscribe to streaming resource metrics |
shell-attach |
Reusable interactive terminal session using connect-or-create |
net-basic |
DNS resolution, HTTP fetch, interface status |
net-dns |
DNS filtering — block domains and suffixes |
net-policy |
Network policies — public-only, allow-all, no-network |
net-ports |
Port publishing — expose guest services on host ports |
net-secrets |
Secret injection with TLS placeholder substitution |
net-tls |
TLS interception with per-domain cert generation |
Overview (`crates/filesystem/README.md`)
microsandbox-filesystem
Filesystem backends for microsandbox virtual machines. This crate now exposes the three backends the runtime still uses: PassthroughFs, MemFs, and DualFs.
- No root required — runs unprivileged; guest-visible ownership and permissions are virtualized without touching the host filesystem
- Runs on macOS too — the guest sees a full Linux filesystem even when the host runs macOS
- Zero-copy I/O — file reads and writes flow directly between host and guest with no intermediate allocation
- Composable — all backends implement
DynFileSystemand can be nested freely
Backends
PassthroughFs
Exposes a single host directory to the guest VM. The guest sees standard Linux ownership, permissions, and file types even when the host runs macOS.
let fs = PassthroughFs::builder()
.root_dir("/path/to/host/dir")
.build()?;Metadata like uid, gid, and mode are stored in an extended attribute on the host, so the actual host file permissions stay untouched. Special files (devices, sockets, FIFOs) are stored as regular files with their type bits in the xattr. Path confinement prevents the guest from escaping the root directory.
MemFs
A pure in-memory filesystem — no host I/O at all. Good for scratch space and ephemeral workloads.
use microsandbox_filesystem::SizeExt;
let fs = MemFs::builder()
.capacity(64.mib())
.max_inodes(10_000)
.build()?;DualFs
Combines two backends under a single mount point with a dispatch policy that routes each operation to the right backend.
let fs = DualFs::builder()
.backend_a(mem_fs)
.backend_b(passthrough_fs)
.policy(ReadBackendBWriteBackendA)
.build()?;Built-in policies:
| Policy | Behavior |
|---|---|
ReadBackendBWriteBackendA |
Reads from B, writes to A (default) |
BackendAOnly |
Everything goes to A |
BackendAFallbackToBackendBRead |
Reads try A first, fall back to B |
MergeReadsBackendAPrecedence |
Merge directory listings, A wins on conflicts |
When a write targets a file that lives on the other backend, DualFs copies it over automatically.
Cache Policies
All backends support configurable kernel caching:
| Policy | Behavior |
|---|---|
Never |
No caching — every read hits the backend (DIRECT_IO) |
Auto |
Kernel decides (default for PassthroughFs and DualFs) |
Always |
Aggressive caching (default for MemFs, since memory is authoritative) |
let fs = PassthroughFs::builder()
.root_dir("/path/to/dir")
.cache_policy(CachePolicy::Always)
.build()?;Overview (`crates/image/README.md`)
microsandbox-image
Pull OCI container images and cache ready-to-mount filesystem artifacts locally. This crate handles the full image lifecycle for microsandbox — from resolving a multi-platform manifest to producing shared per-layer EROFS inputs, stitched layered roots, and flat ext4 roots.
- Multi-platform resolution — automatically picks the right manifest for your OS and architecture
- Parallel materialization — layers download, verify, ingest, and materialize concurrently
- Content-addressable caching — duplicate layers across images are stored once, with cross-process safety via
flock() - Progress streaming — real-time events for download, layer materialization, and fsmeta/VMDK stitching stages
The target-aware layer reuse and publication contract is specified in MATERIALIZATION.md.
Quick Start
use std::path::Path;
use microsandbox_image::{GlobalCache, Platform, PullOptions, Registry};
let cache = GlobalCache::new(Path::new("/path/to/cache"))?;
let platform = Platform::host_linux();
let registry = Registry::new(platform, cache)?;
let reference = "docker.io/library/alpine".parse()?;
let result = registry.pull(&reference, &PullOptions::default()).await?;
// Materialized layer diff_ids, bottom-to-top
for diff_id in &result.layer_diff_ids {
println!("{diff_id}");
}
// Parsed image config (env, cmd, entrypoint, user, etc.)
println!("entrypoint: {:?}", result.config.entrypoint);
println!("env: {:?}", result.config.env);Authentication
use microsandbox_image::RegistryAuth;
// Public registries — no credentials needed
let registry = Registry::new(platform, cache)?;
// Private registries
let auth = RegistryAuth::Basic {
username: "user".into(),
password: "token".into(),
};
let registry = Registry::builder(platform, cache).auth(auth).build()?;Pull Policies
Control when the crate contacts the registry vs. uses the local cache.
use microsandbox_image::{PullOptions, PullPolicy, RootfsMaterialization};
// Use cache if available, download otherwise (default)
let opts = PullOptions { pull_policy: PullPolicy::IfMissing, ..Default::default() };
// Always fetch a fresh manifest, but reuse cached layers by digest
let opts = PullOptions { pull_policy: PullPolicy::Always, ..Default::default() };
// Cache-only — error if the image isn't already cached
let opts = PullOptions { pull_policy: PullPolicy::Never, ..Default::default() };
// Reuse shared EROFS layers, then produce flat ext4 without fsmeta/VMDK
let opts = PullOptions {
materialization: RootfsMaterialization::Flat,
..Default::default()
};Progress Reporting
Stream progress events while the download runs in the background.
use microsandbox_image::PullProgress;
let (mut progress, task) = registry.pull_with_progress(&reference, &PullOptions::default());
tokio::spawn(async move {
while let Some(event) = progress.recv().await {
match event {
PullProgress::Resolved { layer_count, .. } => {
println!("Pulling {layer_count} layers");
}
PullProgress::LayerDownloadProgress { downloaded_bytes, total_bytes, .. } => {
println!("Downloaded {downloaded_bytes}/{total_bytes:?} bytes");
}
PullProgress::Complete { .. } => {
println!("Done!");
}
_ => {}
}
}
});
let result = task.await??;Multi-Platform Images
Fat manifests (OCI Image Index) are automatically resolved to your target platform. Platform::host_linux() detects the current architecture by default.
use microsandbox_image::Platform;
// Auto-detect (e.g. linux/amd64 or linux/arm64)
let platform = Platform::host_linux();
// Explicit platform
let platform = Platform::new("linux", "arm64");
// With variant (e.g. armv7)
let platform = Platform::with_variant("linux", "arm", "v7");Overview (`examples/python/README.md`)
Python Examples
Prerequisites
- Python 3.10+
- uv and maturin (
uv tool install maturin) msb+libkrunfwinstalled (bundled in the wheel, or at~/.microsandbox/)- For
root-bindandroot-block:git submodule update --init --recursive
Setup
Build the Python SDK (one-time):
cd sdk/python
uv sync --group dev
maturin develop --releaseRunning
From the repo root:
uv run --project sdk/python python examples/python/<example>/main.pyFor example:
uv run --project sdk/python python examples/python/root-oci/main.py
uv run --project sdk/python python examples/python/net-basic/main.py
uv run --project sdk/python python examples/python/fs-read-stream/main.pyConfiguration-focused examples intentionally replace their fixed-name sandbox so reruns match the source. shell-attach uses connect_or_create instead, preserving its interactive workspace and configuration across runs.
Examples
| Example | Description |
|---|---|
lifecycle-convergence |
Live convergence and stale-handle identity-safety checks with timing metrics |
cloud-backend |
Cloud backend lifecycle and live logs |
root-oci |
OCI image rootfs |
root-bind |
Bind-mounted local directory |
root-block |
qcow2 disk image |
rootfs-patch |
Pre-boot filesystem patches |
init-handoff |
Hand PID 1 off to systemd |
volume-named |
Named volumes shared across sandboxes |
volume-disk |
Disk image volumes (raw / qcow2) at guest paths |
snapshot-fork |
Snapshot a stopped sandbox and boot a fresh one from it |
fs-read-stream |
Streaming file read |
metrics-stream |
Streaming resource metrics |
shell-attach |
Reusable interactive shell using connect_or_create |
net-basic |
Basic networking |
net-dns |
DNS filtering |
net-policy |
Network policies |
net-ports |
Port publishing |
net-secrets |
Secret injection |
net-tls |
TLS interception |
The snapshot-fork example keeps its historical directory name; it demonstrates restoring a saved disk snapshot. Live forking uses the fork API.
Overview (`examples/rust/README.md`)
Rust Examples
Prerequisites
- Rust (2024 edition)
msb+libkrunfwinstalled (viacargo buildwith thedownload-binariesfeature, or manually)- For
root-bindandroot-block:git submodule update --init --recursive
Running
Each example is a standalone binary crate in the workspace:
cargo run -p <example-name>For example:
cargo run -p oci-root
cargo run -p net-basic
cargo run -p fs-read-streamConfiguration-focused examples intentionally replace their fixed-name sandbox so reruns match the source. shell-attach uses connect_or_create instead, preserving its interactive workspace and configuration across runs.
Examples
| Example | Command | Description |
|---|---|---|
lifecycle-convergence |
cargo run -p lifecycle-convergence |
Live convergence and stale-handle identity-safety checks with timing metrics |
cloud-backend |
cargo run -p cloud-backend |
Cloud backend lifecycle and live logs |
root-oci |
cargo run -p root-oci |
OCI image rootfs |
root-bind |
cargo run -p root-bind |
Bind-mounted local directory |
root-block |
cargo run -p root-block |
qcow2 disk image |
rootfs-patch |
cargo run -p rootfs-patch |
Pre-boot filesystem patches |
init-handoff |
cargo run -p init-handoff |
Hand PID 1 off to systemd |
volume-named |
cargo run -p volume-named |
Named volumes shared across sandboxes |
volume-disk |
cargo run -p volume-disk |
Disk image volumes (raw / qcow2) at guest paths |
snapshot-fork |
cargo run -p snapshot-fork |
Snapshot a stopped sandbox and boot a fresh one from it |
fs-read-stream |
cargo run -p fs-read-stream |
Streaming file read |
metrics-stream |
cargo run -p metrics-stream |
Streaming resource metrics |
shell-attach |
cargo run -p shell-attach |
Reusable interactive shell using connect_or_create |
net-basic |
cargo run -p net-basic |
Basic networking |
net-dns |
cargo run -p net-dns |
DNS filtering |
net-policy |
cargo run -p net-policy |
Network policies |
net-ports |
cargo run -p net-ports |
Port publishing |
net-secrets |
cargo run -p net-secrets |
Secret injection |
net-tls |
cargo run -p net-tls |
TLS interception |
The snapshot-fork example keeps its historical directory name; it demonstrates restoring a saved disk snapshot. Live forking uses the fork API.
Overview (`examples/typescript/README.md`)
TypeScript Examples
Prerequisites
- Node.js >= 22
msb+libkrunfwavailable from themicrosandboxplatform package installed through optional dependencies, orMSB_PATHpointing at a workingmsbbinary- For
root-bindandroot-block:git submodule update --init --recursive
Setup
Each example is a standalone Node.js project. Install dependencies first:
cd examples/typescript/<example>
npm installRunning
npm startFor example:
cd examples/typescript/root-oci
npm install
npm startConfiguration-focused examples intentionally replace their fixed-name sandbox so reruns match the source. shell-attach uses connectOrCreate instead, preserving its interactive workspace and configuration across runs.
Examples
| Example | Description |
|---|---|
lifecycle-convergence |
Live convergence and stale-handle identity-safety checks with timing metrics |
cloud-backend |
Cloud backend lifecycle and live logs |
root-oci |
OCI image rootfs |
root-bind |
Bind-mounted local directory |
root-block |
qcow2 disk image |
rootfs-patch |
Pre-boot filesystem patches |
init-handoff |
Hand PID 1 off to systemd |
volume-named |
Named volumes shared across sandboxes |
volume-disk |
Disk image volumes (raw / qcow2) at guest paths |
snapshot-fork |
Snapshot a stopped sandbox and boot a fresh one from it |
fs-read-stream |
Streaming file read |
metrics-stream |
Streaming resource metrics |
shell-attach |
Reusable interactive shell using connectOrCreate |
net-basic |
Basic networking |
net-dns |
DNS filtering |
net-policy |
Network policies |
net-ports |
Port publishing |
net-secrets |
Secret injection |
net-tls |
TLS interception |
The snapshot-fork example keeps its historical directory name; it demonstrates restoring a saved disk snapshot. Live forking uses the fork API.
Overview (`packages/agent-client/README.md`)
microsandbox agent client
Transport-agnostic clients for speaking the microsandbox agent protocol.
This layer sits between the protocol package and the high-level SDKs:
sdk -> agent-client -> protocol-client -> protocolThe agent package supplies connection handshakes, its existing message codec, and protocol-generation gates. Its public client is a specialization of the shared generic router, which owns IDs, framing, transport lifetime, and request/stream routing. SDK packages own sandbox lifecycle, name resolution, image management, volumes, and metrics.
Layout
packages/agent-client/
├── rust/
└── typescript/rust/publishes asmicrosandbox-agent-client.typescript/publishes as@microsandbox/agent-client.
The Rust crate has no default transport feature. Local SDK connections opt into uds on Unix or named-pipe on Windows. An owned AsyncRead + AsyncWrite transport can also be supplied, including a caller-authenticated WebSocket adapted to bytes.
The TypeScript package exposes a browser-safe default entry and keeps Node-only Unix sockets behind @microsandbox/agent-client/node; browser/front-end callers use WebSocketTransport.
Message Model
The public API names protocol boundaries by structure:
TypedMessage: message type plus a native payload object.EncodedMessage: message type plus already-CBOR-encoded payload bytes.RawFrame: routing fields and an opaque envelope body.Message(Rust) /InboundFrame(TypeScript): an inspectable envelope retaining its original raw frame.TransportPacket: exact transport bytes, including length prefix.
Unchecked packet writes are an explicit escape hatch for relays, tests, and specialized tooling.
Rust and TypeScript preserve native, encoded, raw, exact-packet, explicit-ID, and owned split-stream access. See each package README for the API migration and validated examples. Shared TypeScript packages build together through the private npm workspace in packages.
Overview (`packages/microsandbox-types/README.md`)
microsandbox types
Shared task and wire contract types for microsandbox.
These packages define the backend-neutral shapes that describe what a sandbox should be: its rootfs source, resources, mounts, patches, network, lifecycle, and the cloud HTTP request/response bodies that carry those specs over the wire. Everything that has to agree on a sandbox's shape (the Rust SDK, the CLI, the cloud API, and the TypeScript front end) depends on these contracts instead of redefining them.
This is a contract layer, not a runtime. It does not create, start, or talk to sandboxes. It only models the data those operations exchange.
Layout
packages/microsandbox-types/
├── rust/
└── typescript/rust/publishes asmicrosandbox-types(crate namemicrosandbox_types). It is the source of truth for every shared type.typescript/publishes as@microsandbox/types. Itssrc/cloud.ts(entry) andsrc/domain.tsare generated from the Rust types, never hand-edited; only the cloud contract and the domain types it references are emitted.
Source Of Truth
The Rust crate owns the definitions. The TypeScript bindings are derived from them with ts-rs behind the crate's ts feature, so the two stay byte-for-byte aligned.
rust/lib/*.rs ──(ts-rs)──▶ typescript/src/{cloud,domain}.tsTo regenerate the bindings after changing a Rust type:
cargo run -p microsandbox-types --features ts --bin microsandbox-types-generateCI runs the same generator with --check and fails when the checked-in bindings drift:
cargo run -p microsandbox-types --features ts --bin microsandbox-types-generate -- --checkWhat Lives Here
- Sandbox specs:
SandboxSpec,SandboxResources,SandboxRuntimeOptions, rootfs sources, mounts, patches, init, lifecycle policy. - Networking intent:
NetworkSpec, published ports, protocols. - Volumes and snapshot manifests:
VolumeSpec,SnapshotSpec, and their kinds. - Exec and logging:
Rlimit,RlimitResource,LogSource,SandboxLogLevel. - Cloud wire contracts: the source-tagged
CloudCreateSandboxRequestunion,CloudSandbox, snapshot resources and operations, and
paginated/message/error bodies.
Cloud snapshot contracts distinguish the durable resource from the asynchronous
capture operation:
CloudCreateSnapshotRequestis discriminated bykind. Its currentdisk
variant captures the writable disk, and the enum can gain other snapshot
kinds without changing the envelope shape.CloudSnapshotuses the same kind discriminator and includes its canonical
manifest, byte size, labels, andCloudSnapshotLocation.CloudSnapshotLocationis either a managed artifact ID or a host-volume
path. The same type is used when restoring a sandbox so location semantics
are not duplicated.CloudSnapshotOperationcarries the requested kind and tracks capture
throughqueued,in_progress,succeeded, orfailed;resultis
populated on success.- Validation helpers: sandbox-name and hostname rules shared across SDK, CLI, and cloud.
Backend-private materialized state (registry credentials, local cache paths, DB rows, resolved manifest digests, process handles) deliberately stays out of these packages. See each language's README for details.
Cloud Request Compatibility
Cloud request objects ignore unknown fields, including fields inside network and runtime options. This allows newer SDKs to send additive options before the receiving Cloud release understands them. Unknown settings have no effect until that server supports them; request success does not confirm that every supplied setting was applied.
Missing fields retain their documented defaults. Required fields, recognized field types and values, and enum variants still undergo validation. Ignoring unknown object keys does not make new enum variants, renamed fields, or changed field types compatible.
This policy applies to public Cloud wire types, not host or operator configuration. SDK checks for runtime-only options remain separate from the server's treatment of future Cloud fields.
Overview (`packaging/winget/README.md`)
Windows Package Manager draft
This directory keeps the source draft for publishing microsandbox to the Windows Package Manager Community Repository.
The official winget package does not live in this repository. The ready-to-submit 0.6.0 manifests are staged under:
packaging/winget/manifests/s/SuperRadCompany/Microsandbox/0.6.0/Copy that directory into a fork of microsoft/winget-pkgs under the matching community repository path:
manifests/s/SuperRadCompany/Microsandbox/<version>/The package uses winget's zip + portable installer shape because the microsandbox Windows release bundle contains msb.exe next to libkrunfw.dll. The manifest sets ArchiveBinariesDependOnPath: true so the archive directory is placed on PATH and msb.exe can load its adjacent DLL.
How submission works
The first version must be submitted to microsoft/winget-pkgs by hand using the staged 0.6.0 manifests (see Submit 0.6.0 below). Komac, which the release automation uses, refuses to open a PR for a package that does not yet exist in the community repository.
After that first version lands, set the WINGET_RELEASE_ENABLED repository variable to true to automate subsequent releases. The update-winget job in .github/workflows/release.yml then runs on every v* tag: it invokes winget-releaser (Komac), which copies the previously published manifests, bumps the version, rewrites the installer URLs and SHA256 hashes from the release's microsandbox-windows-*.zip assets, and opens a PR against microsoft/winget-pkgs.
One-time setup for the automation:
- After the first version is accepted into
microsoft/winget-pkgs, set theWINGET_RELEASE_ENABLEDrepository variable totrue. Keep it unset before then so release workflows skip theupdate-wingetjob. - Add a
WINGET_TOKENrepository secret: a classic PAT withpublic_reposcope whose owner has a fork ofmicrosoft/winget-pkgs. The submit step is skipped when this secret is absent. - If that fork lives under an account other than
superradcompany, set aWINGET_FORK_USERrepository variable to the fork owner's username.
Prerelease tags (-rc, -alpha, -beta, -dev) are skipped, matching the release job's prerelease detection. The manual Render a release manifest flow below remains as a fallback if the automation is ever unavailable.
Submit 0.6.0
- Fork and clone
microsoft/winget-pkgs. - Copy the staged manifests into the fork:
$version = "0.6.0"
$src = "C:\src\microsandbox\packaging\winget\manifests\s\SuperRadCompany\Microsandbox\$version"
$dst = "C:\src\winget-pkgs\manifests\s\SuperRadCompany\Microsandbox\$version"
New-Item -ItemType Directory -Force -Path $dst | Out-Null
Copy-Item "$src\*.yaml" $dst- Validate and smoke-test from the
winget-pkgscheckout on Windows:
winget validate $dst
winget install --manifest $dst --accept-package-agreements --accept-source-agreements
msb doctor
winget uninstall SuperRadCompany.Microsandbox- Commit the copied files in the
winget-pkgsfork and open a pull request againstmicrosoft/winget-pkgs.
The local staged manifests are based on GitHub release v0.6.0, published on 2026-06-27, with SHA256 values from the release checksums.sha256 asset.
Render a release manifest
- Wait for a release that includes:
microsandbox-windows-x86_64.zipmicrosandbox-windows-aarch64.zipchecksums.sha256
- Copy the template files into the winget-pkgs path:
$version = "0.5.9"
$tag = "v$version"
$dst = "C:\src\winget-pkgs\manifests\s\SuperRadCompany\Microsandbox\$version"
New-Item -ItemType Directory -Force -Path $dst | Out-Null
Copy-Item packaging\winget\SuperRadCompany.Microsandbox\*.yaml $dst- Replace template markers:
{{VERSION}}with the semver package version, for example0.5.9{{TAG}}with the GitHub release tag, for examplev0.5.9{{RELEASE_DATE}}with the release date inYYYY-MM-DD{{SHA256_X64}}with the checksum formicrosandbox-windows-x86_64.zip{{SHA256_ARM64}}with the checksum formicrosandbox-windows-aarch64.zip
The checksums are available from the release asset:
gh release download $tag --repo superradcompany/microsandbox --pattern checksums.sha256 --dir $env:TEMP --clobber
Select-String -Path "$env:TEMP\checksums.sha256" -Pattern "microsandbox-windows"- Validate and smoke-test from the winget-pkgs checkout:
winget validate $dst
winget install --manifest $dst --accept-package-agreements --accept-source-agreements
msb doctor
winget uninstall SuperRadCompany.Microsandbox- Submit the rendered files as a PR to
microsoft/winget-pkgs.
Do not submit these templates directly. The community repository requires concrete release URLs and SHA256 hashes.
Overview (`scripts/msb-trust/README.md`)
Application trust helper
Users download scripts/msb-trust/msb-trust.sh. Edit the sources here, then rebuild:
sh scripts/msb-trust/build.shcore.sh handles application selection, dependency reporting, and summaries.
Each file in apps/ contains one application's setup and help. The build discovers
these files alphabetically and produces a standalone POSIX shell script. No source
files or build tools are needed in the sandbox.
Add an application
Create apps/<name>.sh using lowercase letters, digits, and underscores, starting
with a letter. Use an existing handler as a starting point. Define these functions:
| Function | Purpose |
|---|---|
<name>_tools |
Print required command names, separated by whitespace. |
<name>_packages |
Receive a package manager (apt-get, dnf, or empty), followed by missing tools. Print the corresponding package names, or nothing for manual setup. |
<name>_help |
Print requirements and application-specific instructions. |
<name>_configure |
Import the CA at $CA_CERT, preserving existing certificates. |
Optional functions are <name>_description for the help listing,<name>_available for detection beyond commands on PATH, and<name>_install_hint for additional prerequisite instructions.
Keep files limited to function definitions. Handlers run in separate subshells;
explicitly guard fallible operations with || fail "useful explanation" because
batch dispatch disables shell errexit. Use progress for success messages so
batch runs can print one combined summary. Never install packages automatically.
Rebuild and include both the source and generated script in your change. CI runssh scripts/msb-trust/build.sh --check to reject stale output or missing required
functions. Validate setup, repeat runs, and failures against the real application.
Overview (`sdk/go/README.md`)
microsandbox
Lightweight VM sandboxes for Go applications that need hardware-level isolation for AI agents, tools, tests, and untrusted code.
The github.com/superradcompany/microsandbox/sdk/go module provides Go bindings to the microsandbox runtime. It creates microVM-backed sandboxes from OCI images or other rootfs sources, then exposes command execution, guest filesystem access, networking, secrets, volumes, metrics, logs, snapshots, and SSH/SFTP through an idiomatic Go API.
For the full API reference and longer guides, use Go docs and the microsandbox docs site:
EnsureRuntime reuses any complete msb and libkrunfw pair in MSB_HOME (or ~/.microsandbox by default), without executing the binary or requiring its version to equal the SDK version. It refuses partial installations. The SDK version selects newly downloaded artifacts; it does not trigger replacement of an existing pair.
Features
- Hardware VM isolation with a guest Linux kernel
- Go API over an embedded CGO FFI library
- Collected and streaming command execution
- Guest filesystem read, write, list, copy, stat, and stream operations
- Named volumes, bind mounts, tmpfs mounts, and disk-image mounts
- Network policies, DNS filtering, TLS interception, secrets, and port publishing
- Rootfs patches before boot
- Detached sandboxes that can outlive the Go process
- Metrics, logs, snapshots, SSH/SFTP, image cache, and local/cloud backend helpers
Requirements
- Go 1.22+
- CGO enabled and a C compiler/toolchain available
- Linux with KVM, macOS with Apple Silicon, or Windows 11 with WHP enabled
Supported Platforms
| Platform | Architecture | Notes |
|---|---|---|
| macOS | ARM64 / Apple Silicon | Embedded FFI library |
| Linux | x86_64 | Embedded FFI library |
| Linux | ARM64 | Embedded FFI library |
| Windows | x86_64, ARM64 | Embedded FFI library; runtime support is in preview |
The Go binary embeds the SDK FFI library and extracts it on first use. The msb runtime and libkrunfw are installed separately into MSB_HOME (default ~/.microsandbox/) by EnsureRuntime (%USERPROFILE%\.microsandbox on Windows).
Installation
go get github.com/superradcompany/microsandbox/sdk/goCall EnsureRuntime at process startup when your program will create local sandboxes. It is idempotent and surfaces runtime download/setup failures before the first sandbox operation.
if _, err := microsandbox.EnsureRuntime(ctx, microsandbox.RuntimeConfig{}, microsandbox.InstallOptions{}); err != nil {
log.Fatalf("microsandbox setup: %v", err)
}Use IsRuntimeInstalled(RuntimeConfig{}) for a boolean availability check or ResolveRuntime(RuntimeConfig{}) to require an existing pair and obtain its paths without installing runtime binaries. InstallRuntime explicitly provisions the selected source. All setup calls use the shared Rust resolver and installer; the first call may also materialize the embedded SDK FFI library.
Quick Start
package main
import (
"context"
"fmt"
"log"
"time"
microsandbox "github.com/superradcompany/microsandbox/sdk/go"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
if _, err := microsandbox.EnsureRuntime(ctx, microsandbox.RuntimeConfig{}, microsandbox.InstallOptions{}); err != nil {
log.Fatal(err)
}
name := "go-readme"
sb, err := microsandbox.CreateSandbox(ctx, name,
microsandbox.WithImage("alpine:3.19"),
microsandbox.WithMemory(512),
microsandbox.WithCPUs(1),
microsandbox.WithReplace(),
)
if err != nil {
log.Fatal(err)
}
defer func() {
stopCtx, stopCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer stopCancel()
_ = sb.Stop(stopCtx)
_ = sb.Close()
_ = microsandbox.RemoveSandbox(context.Background(), name)
}()
out, err := sb.Shell(ctx, "echo 'Hello from microsandbox!'")
if err != nil {
log.Fatal(err)
}
fmt.Println(out.Stdout())
}Reusable Lifecycle Convergence
Use ConnectOrCreateSandbox when a stable name should converge on one persisted sandbox. Existing configuration wins; options are used only if creation is necessary. Handles retain a stable ID, so lifecycle calls on stale receivers refuse to act on a replacement that reused the name.
sb, err := microsandbox.ConnectOrCreateSandbox(ctx, "worker",
microsandbox.WithImage("python"),
microsandbox.WithMemory(1024),
)
fmt.Printf("%s: %s\n", sb.Name(), sb.ID())
handle, err := microsandbox.GetSandbox(ctx, "worker")
running, err := handle.ConnectOrStart(ctx)
err = running.RequestStop(ctx)
stopped, err := running.WaitForStatus(ctx, microsandbox.SandboxStatusStopped)
restarted, err := stopped.Restart(ctx)
err = restarted.Destroy(ctx)Common Examples
These snippets assume you already have a live sb *microsandbox.Sandbox and ctx context.Context. See sdk/go/examples for complete runnable programs.
Fork a Live Sandbox
Forking copies a running or paused local sandbox's disk and execution state into an independent child. Memory uses copy-on-write automatically. The source keeps its previous running or paused state. Host resources require explicit bindings; see forking and resource bindings.
child, err := sb.Fork(ctx, "experiment")
if err != nil { return err }
defer child.Close()
if err := child.Stop(ctx); err != nil { return err }Use ForkMany(ctx, []string{"alice", "bob"}) to capture once for several children. Inspect every returned outcome: one child's startup failure does not remove successful siblings. See the fork API reference.
Restoring starts from a saved snapshot instead. Use WithCowMemory() to request copy-on-write memory for a full-snapshot restore. A generation describes snapshot-history progression; a branch describes a distinct path through that history. The former live branch APIs and old CoW restore names remain deprecated aliases. See restore migration notes for the old-to-new names and language-specific deprecation notices.
Command Execution
out, err := sb.Exec(ctx, "python3", []string{"-c", "print(1 + 1)"})
if err != nil {
log.Fatal(err)
}
fmt.Println(out.Stdout())
fmt.Println(out.ExitCode())
out, err = sb.Shell(ctx, "echo hello && pwd")
out, err = sb.Exec(ctx, "make", []string{"build"},
microsandbox.WithExecCwd("/app"),
microsandbox.WithExecTimeout(30*time.Second),
)Non-zero process exits are represented on ExecOutput; they are not Go errors by themselves.
Streaming Execution
handle, err := sb.ShellStream(ctx, "tail -f /var/log/app.log")
if err != nil {
log.Fatal(err)
}
defer handle.Close()
for {
event, err := handle.Recv(ctx)
if err != nil {
break
}
switch event.Kind {
case microsandbox.ExecEventStdout:
_, _ = os.Stdout.Write(event.Data)
case microsandbox.ExecEventStderr:
_, _ = os.Stderr.Write(event.Data)
case microsandbox.ExecEventExited, microsandbox.ExecEventDone:
return
}
}Filesystem Operations
fs := sb.FS()
if err := fs.WriteString(ctx, "/tmp/config.json", `{"debug": true}`); err != nil {
log.Fatal(err)
}
content, err := fs.ReadString(ctx, "/tmp/config.json")
if err != nil {
log.Fatal(err)
}
fmt.Println(content)
entries, err := fs.List(ctx, "/etc")
if err != nil {
log.Fatal(err)
}
for _, entry := range entries {
fmt.Printf("%s (%s)\n", entry.Path, entry.Kind)
}Named Volumes
volume, err := microsandbox.CreateVolume(ctx, "go-readme-data",
microsandbox.WithVolumeQuota(100),
microsandbox.WithVolumeLabels(map[string]string{"purpose": "readme"}),
)
if err != nil {
log.Fatal(err)
}
defer volume.Remove(ctx)
writer, err := microsandbox.CreateSandbox(ctx, "go-readme-writer",
microsandbox.WithImage("alpine:3.19"),
microsandbox.WithMounts(map[string]microsandbox.MountConfig{
"/data": microsandbox.Mount.Named("go-readme-data", microsandbox.MountOptions{}),
}),
microsandbox.WithReplace(),
)
if err != nil {
log.Fatal(err)
}
defer writer.Close()
_, err = writer.Shell(ctx, "echo 'hello' > /data/message.txt")Network, DNS, and Ports
isolated, err := microsandbox.CreateSandbox(ctx, "go-readme-isolated",
microsandbox.WithImage("alpine:3.19"),
microsandbox.WithNetwork(microsandbox.NetworkPolicy.None()),
microsandbox.WithReplace(),
)
if err != nil {
log.Fatal(err)
}
defer isolated.Close()
filtered, err := microsandbox.CreateSandbox(ctx, "go-readme-filtered",
microsandbox.WithImage("alpine:3.19"),
microsandbox.WithNetwork(µsandbox.NetworkConfig{
DenyDomains: []string{"blocked.example.com"},
DenyDomainSuffixes: []string{".evil.com"},
DNS: µsandbox.DNSConfig{
Nameservers: []string{"1.1.1.1:53"},
},
}),
microsandbox.WithReplace(),
)
if err != nil {
log.Fatal(err)
}
defer filtered.Close()
web, err := microsandbox.CreateSandbox(ctx, "go-readme-web",
microsandbox.WithImage("python:3.12"),
microsandbox.WithPorts(map[uint16]uint16{8080: 80}),
microsandbox.WithReplace(),
)Secrets
Secrets use placeholder substitution. The real value stays on the host and is substituted only for allowed network destinations.
sb, err := microsandbox.CreateSandbox(ctx, "go-readme-agent",
microsandbox.WithImage("python:3.12"),
microsandbox.WithSecrets(microsandbox.Secret.Env(
"OPENAI_API_KEY",
os.Getenv("OPENAI_API_KEY"),
microsandbox.SecretEnvOptions{Allow: []string{"api.openai.com"}},
)),
microsandbox.WithReplace(),
)Rootfs Patches
sb, err := microsandbox.CreateSandbox(ctx, "go-readme-patched",
microsandbox.WithImage("alpine:3.19"),
microsandbox.WithPatches(
microsandbox.Patch.Text("/etc/greeting.txt", "Hello!\n", microsandbox.PatchOptions{}),
microsandbox.Patch.Mkdir("/app", microsandbox.PatchOptions{}),
microsandbox.Patch.Text("/app/config.json", `{"debug": true}`, microsandbox.PatchOptions{}),
microsandbox.Patch.Append("/etc/hosts", "127.0.0.1 myapp.local\n"),
),
microsandbox.WithReplace(),
)Detached Mode
sb, err := microsandbox.CreateSandbox(ctx, "go-readme-background",
microsandbox.WithImage("python:3.12"),
microsandbox.WithDetached(),
microsandbox.WithReplace(),
)
if err != nil {
log.Fatal(err)
}
_ = sb.Detach(ctx)
handle, err := microsandbox.GetSandbox(ctx, "go-readme-background")
if err != nil {
log.Fatal(err)
}
reconnected, err := handle.Connect(ctx)
if err != nil {
log.Fatal(err)
}
out, err := reconnected.Shell(ctx, "echo reconnected")Metrics
metrics, err := sb.Metrics(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("CPU: %.1f%%\n", metrics.CPUPercent)
fmt.Printf("Memory: %.1f MiB\n", float64(metrics.MemoryBytes)/1024/1024)
stream, err := sb.MetricsStream(ctx, 500*time.Millisecond)
if err != nil {
log.Fatal(err)
}
defer stream.Close()
sample, err := stream.Recv(ctx)
if err == nil && sample != nil {
fmt.Printf("CPU: %.1f%%\n", sample.CPUPercent)
}Storage Usage and Runtime Cache Cleanup
usage, err := microsandbox.StorageUsage(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(usage.BranchMemory.LogicalBytes)
item, err := sb.StorageUsage(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(item.Name, item.LogicalBytes)
preview, err := microsandbox.PruneStorage(ctx, microsandbox.StoragePruneOptions{
DryRun: true,
OlderThanSeconds: 600,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(preview.Entries)Reports contain integer byte counts; optional pointer fields are nil when unknown. Logical bytes are file lengths, and allocated bytes may count shared CoW blocks repeatedly. They do not measure exclusive disk ownership. sb.StorageUsage(ctx) observes the live sandbox's managed directory through its retained backend and stable identity; host bind mounts and shared runtime-memory caches are excluded.
Go's metadata-only SandboxHandle, SnapshotArtifact, and SnapshotHandle do not yet expose per-object storage methods because they do not retain a native backend-bound receiver. Use the detailed Sandboxes.Items and Snapshots.Items fields in StorageUsage(ctx) for observations through the selected backend.
PruneStorage is an explicit cleanup operation and does not prompt. DryRun: true only previews; calling with DryRun: false rechecks ownership and removes unused runtime RAM. It retains durable snapshots, named volumes, sandbox disks, and handoff locks. Check each entry for partial errors. Logical bytes removed are reported separately from physical bytes reclaimed, which remain unknown. Remote backends and native bundles predating storage support return ErrUnsupportedOperation.
Typed Errors
Go SDK errors can be checked with IsKind and unwrapped with errors.As.
_, err := microsandbox.GetSandbox(ctx, "missing")
if microsandbox.IsKind(err, microsandbox.ErrSandboxNotFound) {
fmt.Println("sandbox does not exist")
}
var msbErr *microsandbox.Error
if errors.As(err, &msbErr) {
fmt.Printf("microsandbox error kind: %s\n", msbErr.Kind)
}Runnable Examples
Each example is a self-contained main.go under sdk/go/examples. Run them from sdk/go:
go run ./examples/basic
go run ./examples/network
go run ./examples/snapshot-fork| Example | Covers |
|---|---|
basic |
Create, exec, filesystem, metrics, stop, close, remove |
cloud-backend |
Cloud backend lifecycle and live logs |
detached |
Detached lifecycle, reattach, stop, and remove |
disk |
Build and mount a raw ext4 disk image |
errors |
Error categories with IsKind and errors.As |
filesystem |
Guest filesystem operations and streaming |
image-cache |
Cached OCI image list, inspect, remove, and prune |
metrics |
Point-in-time and streaming metrics |
network |
Presets, DNS, TLS, and custom network settings |
patches |
Pre-boot rootfs patches |
ports |
Guest TCP port publishing |
secrets |
Secret placeholder injection |
snapshot-fork |
Snapshot a stopped sandbox and restore a fresh sandbox |
streaming |
Streaming exec, signals, and cancellation |
tls |
TLS interception configuration |
volumes |
Named volume lifecycle |
The snapshot-fork example keeps its historical directory name; it demonstrates restoring a saved disk snapshot. Live forking uses the Fork API.
More Documentation
Development
The SDK embeds the FFI library at build time, so normal unit tests do not require a Rust toolchain:
cd sdk/go
go test -count=1 ./...To iterate on the FFI shim itself, build the native library from the repository root, then run Go commands from sdk/go with MICROSANDBOX_FFI_PATH and the microsandbox_ffi_path build tag:
cargo build -p microsandbox-go
cd sdk/go
export MICROSANDBOX_FFI_PATH=../../target/debug/libmicrosandbox_go_ffi.dylib
go run -tags microsandbox_ffi_path ./examples/basicUse libmicrosandbox_go_ffi.so for Linux. Full integration tests require local virtualization support and runtime artifacts:
go test -tags "smoke microsandbox_ffi_path" -count=1 .
go test -tags "integration microsandbox_ffi_path" -v -count=1 ./integration/...The storage smoke test uses a fresh process and temporary home, requires no VM, and only prunes its own synthetic RAM files:
MSB_STORAGE_NATIVE_LIBRARY="$PWD/../../target/debug/libmicrosandbox_go_ffi.dylib" \
go test -tags storage_native ./internal/ffi -run '^TestStorageNativeReportsAndPruning$' -count=1License
Apache-2.0
Overview (`sdk/node-ts/README.md`)
microsandbox
Lightweight VM sandboxes for Node.js and TypeScript applications that need hardware-level isolation for AI agents, tools, tests, and untrusted code.
The microsandbox npm package provides TypeScript bindings to the microsandbox runtime through a native addon. It creates microVM-backed sandboxes from OCI images or other rootfs sources, then exposes command execution, guest filesystem access, networking, secrets, volumes, metrics, logs, snapshots, and SSH/SFTP through an async TypeScript API.
For the full API reference and longer guides, use the docs site:
A complete runtime in the configured home (MSB_HOME, or ~/.microsandbox by default) takes precedence over platform-package binaries. Explicit binary paths still win. A partial home installation errors instead of falling back to the package. This also applies to the msb and microsandbox CLI entry points.
Features
- Hardware VM isolation with a guest Linux kernel
- ESM-first API with CommonJS
require()support and generated native bindings - Collected and streaming command execution
- Guest filesystem read, write, list, copy, stat, and stream operations
- Named volumes, bind mounts, tmpfs mounts, and disk-image mounts
- Network policies, DNS filtering, TLS interception, secrets, and port publishing
- Rootfs patches before boot
- Detached sandboxes that can outlive the Node.js process
- Metrics, logs, snapshots, SSH/SFTP, image cache, and local/cloud backend helpers
Requirements
- Node.js 22+
- Linux with KVM, macOS with Apple Silicon, or Windows 11 with WHP enabled
- Windows support is currently preview; see the Windows troubleshooting guide for WHP and runtime setup notes.
The package root supports both ESM imports and CommonJS require() on Node.js 22+:
import { Sandbox } from "microsandbox";const { Sandbox } = require("microsandbox");Supported Platforms
| Platform | Architecture | Platform package |
|---|---|---|
| macOS | ARM64 / Apple Silicon | @superradcompany/microsandbox-darwin-arm64 |
| Linux | x86_64 | @superradcompany/microsandbox-linux-x64-gnu |
| Linux | ARM64 | @superradcompany/microsandbox-linux-arm64-gnu |
| Windows | x86_64 | @superradcompany/microsandbox-win32-x64-msvc |
| Windows | ARM64 | @superradcompany/microsandbox-win32-arm64-msvc |
The matching platform package carries the native addon plus runtime binaries. Keep npm optional dependencies enabled: omitting them also removes the required addon, which MSB_PATH cannot replace.
Installation
npm install microsandboxRuntime setup is normally included. To provision msb + libkrunfw separately, use the CLI installer or explicitly install them from your application:
import { ensureRuntime } from "microsandbox";
const runtime = await ensureRuntime();
console.log(runtime.msbPath, runtime.libkrunfwPath);Use MSB_PATH and MSB_LIBKRUNFW_PATH to select an external runtime; this does not remove the npm package's bundled files. See Runtime setup for custom paths and versions.
Quick Start
import { MiB, Sandbox } from "microsandbox";
await using sandbox = await Sandbox.builder("ts-readme")
.image("alpine")
.cpus(1)
.memory(MiB(512))
.replace()
.create();
const output = await sandbox.shell("echo 'Hello from microsandbox!'");
console.log(output.stdout().trim());await using calls Sandbox.stop() when the handle leaves scope. Use a plain const sandbox = ... and call lifecycle methods yourself when you need finer control.
Reusable Lifecycle Convergence
Use connectOrCreate when a stable name should converge on one persisted sandbox. Existing configuration wins; the builder is used only if creation is necessary. Handles retain a stable id, so lifecycle calls on stale receivers refuse to act on a replacement that reused the name.
const sandbox = await Sandbox.builder("worker")
.image("python")
.memory(MiB(1024))
.connectOrCreate();
console.log(`${sandbox.name}: ${sandbox.id}`);
const running = await (await Sandbox.get("worker")).connectOrStart();
await running.requestStop();
const stopped = await running.waitForStatus("stopped");
const restarted = await stopped.restart();
await restarted.destroy();Local Storage Usage And Cleanup
Storage.usage() reports images, saved snapshots, sandboxes, volumes, branch RAM, and rebuildable snapshot RAM for the selected local backend. Unknown measurements are null. Logical byte counts and allocated blocks do not measure exclusive physical ownership because files can share disk blocks. All byte fields are bigint, preserving the full unsigned 64-bit range; object counts are numbers. To serialize a report with JSON.stringify, supply a replacer that converts bigints to decimal strings.
import { Storage } from "microsandbox";
const usage = await Storage.usage();
console.log(usage.branchMemory.logicalBytes); // bigint | null
const preview = await Storage.prune({ dryRun: true, olderThanSeconds: 3600 });
console.log(preview.entries);
const result = await Storage.prune({ olderThanSeconds: 3600 });
console.log(result.logicalBytesRemoved);
for (const entry of result.entries) {
if (entry.error !== null) console.error(entry.path, entry.error);
}Pruning removes only unused published runtime RAM. Live mappings, retained baselines, and pending handoffs remain protected. Saved snapshots, images, sandbox disks, volumes, and stable lock files are retained. olderThanSeconds must be a whole non-negative safe integer; the default is zero. dryRun defaults to false. The SDK does not prompt for confirmation, and each apply call rechecks ownership. Individual failures remain in entries alongside completed removals; physicalBytesReclaimed is unknown (null). Cloud backends reject these local-only operations. Each call captures its backend before returning the promise; changing the default while it runs cannot redirect it. An older native addon without these methods raises UnsupportedOperationError when they are called.
SandboxHandle.storageUsage(), Snapshot.storageUsage(), and SnapshotHandle.storageUsage() report one object using its captured backend, with the same bigint byte fields and null unknowns. Snapshot records returned by Snapshot.list() contain metadata only; fetch a native handle with Snapshot.get() before inspecting its storage.
Common Examples
These snippets assume you already have a live sandbox: Sandbox.
Fork a Live Sandbox
Forking copies a running or paused local sandbox's disk and execution state into an independent child. Memory uses copy-on-write automatically. The source keeps its previous running or paused state. Host resources require explicit bindings; see forking and resource bindings.
const child = await sandbox.fork("experiment");
await child.stop();Use forkMany(["alice", "bob"]) to capture once for several children. Inspect every returned outcome: one child's startup failure does not remove successful siblings. See the fork API reference.
Restoring starts from a saved snapshot instead. Use .cowMemory() to request copy-on-write memory for a full-snapshot restore. A generation describes snapshot-history progression; a branch describes a distinct path through that history. The former live branch APIs and old CoW restore names remain deprecated aliases. See restore migration notes for the old-to-new names and language-specific deprecation notices.
Command Execution
const result = await sandbox.exec("python3", ["-c", "print(1 + 1)"]);
console.log(result.stdout());
console.log(result.code);
const output = await sandbox.shell("echo hello && pwd");
console.log(output.stdout());
const configured = await sandbox.execWith("python3", (exec) =>
exec
.args(["script.py"])
.cwd("/app")
.env("PYTHONPATH", "/app/lib")
.timeout(30_000),
);
const handle = await sandbox.execStream("tail", ["-f", "/var/log/app.log"]);
for await (const event of handle) {
if (event.kind === "stdout") process.stdout.write(event.data);
if (event.kind === "stderr") process.stderr.write(event.data);
if (event.kind === "exited") break;
}Filesystem Operations
const fs = sandbox.fs();
await fs.write("/tmp/config.json", '{"debug": true}');
console.log(await fs.readToString("/tmp/config.json"));
for (const entry of await fs.list("/etc")) {
console.log(`${entry.path} (${entry.kind})`);
}
await fs.copyFromHost("./local-file.txt", "/tmp/file.txt");
await fs.copyToHost("/tmp/output.txt", "./output.txt");
if (await fs.exists("/tmp/config.json")) {
const meta = await fs.stat("/tmp/config.json");
console.log(`size: ${meta.size}, kind: ${meta.kind}`);
}Named Volumes
import { MiB, Sandbox, Volume } from "microsandbox";
const data = await Volume.builder("ts-readme-data").quota(MiB(100)).create();
{
await using writer = await Sandbox.builder("ts-readme-writer")
.image("alpine")
.volume("/data", (mount) => mount.named(data.name))
.replace()
.create();
await writer.shell("echo 'hello' > /data/message.txt");
}
{
await using reader = await Sandbox.builder("ts-readme-reader")
.image("alpine")
.volume("/data", (mount) => mount.named(data.name).readonly())
.replace()
.create();
console.log((await reader.shell("cat /data/message.txt")).stdout().trim());
}
const entries = await data.fs().list("");
for (const entry of entries) {
console.log(`${entry.path} (${entry.kind})`);
}Network, DNS, and Ports
import { NetworkPolicy, Rule, Destination, Sandbox } from "microsandbox";
await using isolated = await Sandbox.builder("ts-readme-isolated")
.image("alpine")
.network((network) => network.policy(NetworkPolicy.none()))
.replace()
.create();
const filteredPolicy = NetworkPolicy.builder()
.defaultAllow()
.egress((egress) =>
egress
.denyDomain("blocked.example.com")
.denyDomainSuffix(".evil.com"),
)
.build();
await using filtered = await Sandbox.builder("ts-readme-filtered")
.image("alpine")
.network((network) => network.policy(filteredPolicy))
.replace()
.create();
const allowlisted = {
defaultEgress: "deny",
defaultIngress: "allow",
rules: [
Rule.allowEgress(Destination.domain("api.openai.com")),
],
} satisfies NetworkPolicy;
await using web = await Sandbox.builder("ts-readme-web")
.image("python")
.network((network) => network.policy(allowlisted))
.port(8080, 80)
.create();Domain blocking is network policy behavior. DnsBuilder configures DNS resolver behavior such as rebinding protection, nameservers, and query timeouts.
Secrets
Secrets use placeholder substitution. The real value stays on the host and is substituted only for allowed network destinations.
import { Sandbox } from "microsandbox";
await using sandbox = await Sandbox.builder("ts-readme-agent")
.image("python")
.secretEnv("OPENAI_API_KEY", process.env.OPENAI_API_KEY!, "api.openai.com")
.replace()
.create();Rootfs Patches
await using sandbox = await Sandbox.builder("ts-readme-patched")
.image("alpine")
.patch((patch) =>
patch
.text("/etc/greeting.txt", "Hello!\n")
.mkdir("/app", { mode: 0o755 })
.text("/app/config.json", '{"debug": true}', { mode: 0o644 })
.append("/etc/hosts", "127.0.0.1 myapp.local\n"),
)
.replace()
.create();Detached Mode
const sandbox = await Sandbox.builder("ts-readme-background")
.image("python")
.detached(true)
.replace()
.create();
await sandbox.detach();
const handle = await Sandbox.get("ts-readme-background");
const reconnected = await handle.connect();
const output = await reconnected.shell("echo reconnected");
console.log(output.stdout().trim());TLS Interception
await using sandbox = await Sandbox.builder("tls-inspect")
.image("python")
.network((n) => n.tls((t) =>
t.bypass("*.googleapis.com")
.verifyUpstream(true)
.interceptedPorts([443])
.upstreamCaCert("/etc/ssl/corp-root.pem")
.upstreamCaCertFor("api.internal", "./certs/api-ca.pem")
.verifyUpstreamFor("*.preview.internal", false),
))
.create();Metrics
import { allSandboxMetrics } from "microsandbox";
const metrics = await sandbox.metrics();
console.log(`CPU: ${metrics.cpuPercent.toFixed(1)}%`);
console.log(`Memory: ${(metrics.memoryBytes / 1024 / 1024).toFixed(1)} MiB`);
for await (const sample of await sandbox.metricsStream(1000)) {
console.log(sample.timestamp.toISOString(), sample.cpuPercent);
break;
}
for (const [name, sample] of Object.entries(await allSandboxMetrics())) {
console.log(`${name}: ${sample.cpuPercent.toFixed(1)}%`);
}Typed Errors
TypeScript exports typed errors for the common SDK categories and falls back to MicrosandboxError for unmapped runtime variants. Catch specific errors when you need category-specific handling, and catch MicrosandboxError as the broad SDK base class.
import {
MicrosandboxError,
Sandbox,
SandboxAlreadyExistsError,
} from "microsandbox";
try {
await Sandbox.builder("worker").image("alpine").create();
} catch (error) {
if (error instanceof SandboxAlreadyExistsError) {
console.log("already exists; resume it or pass replace()");
} else if (error instanceof MicrosandboxError) {
console.log(`microsandbox error: ${error.message}`);
} else {
throw error;
}
}Runtime Notes
- The
microsandboxpackage depends on platform packages through optional dependencies. - The platform package carries the native addon,
msb, andlibkrunfw. - The
microsandboxandmsbbin shims forward to the resolvedmsbbinary. They do not install runtime files. - If no platform package is present, reinstall with optional dependencies enabled, install the matching
@superradcompany/microsandbox-package, or setMSB_PATH.
Inspect a runtime version
import { resolveRuntimeVersion } from "microsandbox";
const version = await resolveRuntimeVersion("/path/to/msb");This reads the embedded semantic version without executing the file or requiring firmware. It returns a version string, or null for older executables without the section. File access failures, malformed executable metadata and invalid version sections reject the promise. There is no implicit --version subprocess fallback, and normal sandbox launch does not perform this optional inspection.
More Documentation
Development
From sdk/node-ts:
npm ci
npm run build
npm run typecheck
npm testOn a release branch whose native packages are not published yet, replace npm ci with node ../../scripts/ci/build-unpublished-node-sdk.mjs. This installs the locked build tools and builds TypeScript while restoring the package manifests afterward. Continue with npm run build to build the native addon locally. Standard npm ci works again after the post-release lockfile refresh.
Run repository examples from the specific example directory:
cd examples/typescript/root-oci
npm install
npm startLicense
Apache-2.0
The size helpers and Mebibytes type are re-exported from @microsandbox/types/size, so values can be passed directly between this SDK and the standalone control client. The local npm workspace links that shared package with a versioned dependency for publishing. npm ci, npm run build:ts, and npm run typecheck work from this directory; the build and typecheck scripts build the shared types first.
Overview (`sdk/python/README.md`)
microsandbox
Lightweight VM sandboxes for Python applications that need hardware-level isolation for AI agents, tools, tests, and untrusted code.
The microsandbox Python package provides async Python bindings to the microsandbox runtime. It creates microVM-backed sandboxes from OCI images or other rootfs sources, then exposes command execution, guest filesystem access, networking, secrets, volumes, metrics, logs, snapshots, and SSH/SFTP through Python-friendly classes and dataclasses.
For the full API reference and longer guides, use the docs site:
A complete runtime in the configured home (MSB_HOME, or ~/.microsandbox by default) takes precedence over wheel binaries. Explicit binary paths still win. A partial home installation errors instead of falling back to the wheel. This also applies to the packaged CLI entry points.
Features
- Hardware VM isolation with a guest Linux kernel
- Async sandbox lifecycle, execution, filesystem, metrics, and logs APIs
- OCI image, bind-rootfs, disk-image, and snapshot-based sandboxes
- Named volumes, bind mounts, tmpfs mounts, and disk-image mounts
- Network policies, DNS filtering, TLS interception, secrets, and port publishing
- Rootfs patches before boot
- Detached sandboxes that can outlive the Python process
- Typed Python surface with
StrEnums, frozen dataclasses, event objects,.pyistubs, andpy.typed
Requirements
- Python 3.10+
- Linux with KVM, macOS with Apple Silicon, or Windows 11 with WHP enabled
- Windows support is currently preview; see the Windows troubleshooting guide for WHP and runtime setup notes.
Supported Platforms
| Platform | Architecture | Notes |
|---|---|---|
| macOS | ARM64 / Apple Silicon | Wheel bundles msb and libkrunfw |
| Linux | x86_64 | Wheel bundles msb and libkrunfw |
| Linux | ARM64 | Wheel bundles msb and libkrunfw |
| Windows | x86_64, ARM64 | Preview; requires WHP |
Python wheels bundle the matching msb runtime and libkrunfw library. Source checkouts and unreleased local builds can override runtime paths with MSB_PATH, MSB_LIBKRUNFW_PATH, or microsandbox.set_libkrunfw_path(...).
Installation
pip install microsandboxQuick Start
import asyncio
from microsandbox import Sandbox
async def main() -> None:
async with await Sandbox.create("python-readme", image="alpine", replace=True) as sandbox:
output = await sandbox.shell("echo 'Hello from microsandbox!'")
print(output.stdout_text.strip())
asyncio.run(main())async with stops and removes the sandbox when the block exits. Use Sandbox.create(...) without a context manager when you want to control stop(), kill(), or remove() yourself.
Reusable Lifecycle Convergence
Use connect_or_create when a stable name should converge on one persisted sandbox. Existing configuration wins; creation arguments are used only if creation is necessary. Handles retain a stable id, so lifecycle calls on stale receivers refuse to act on a replacement that reused the name.
from microsandbox import SandboxStatus
sandbox = await Sandbox.connect_or_create("worker", image="python", memory=1024)
print(f"{await sandbox.name}: {await sandbox.id}")
running = await (await Sandbox.get("worker")).connect_or_start()
await running.request_stop()
stopped = await running.wait_for_status(SandboxStatus.STOPPED)
restarted = await stopped.restart()
await restarted.destroy()Common Examples
These snippets assume you already have a live sandbox: Sandbox.
Fork a Live Sandbox
Forking copies a running or paused local sandbox's disk and execution state into an independent child. Memory uses copy-on-write automatically. The source keeps its previous running or paused state. Host resources require explicit bindings; see forking and resource bindings.
child = await sandbox.fork("experiment")
await child.stop()Use await sandbox.fork_many(["alice", "bob"]) to capture once for several children. Inspect every returned outcome: one child's startup failure does not remove successful siblings. See the fork API reference.
Restoring starts from a saved snapshot instead. Use cow_memory=True to request copy-on-write memory for a full-snapshot restore. A generation describes snapshot-history progression; a branch describes a distinct path through that history. The former live branch APIs and old CoW restore names remain deprecated aliases. See restore migration notes for the old-to-new names and language-specific deprecation notices.
Command Execution
import sys
from microsandbox import ExecEventType
output = await sandbox.exec("python3", ["-c", "print(1 + 1)"])
print(output.stdout_text)
print(output.exit_code)
output = await sandbox.shell("echo hello && pwd")
print(output.stdout_text)
output = await sandbox.exec(
"python3",
["script.py"],
cwd="/app",
env={"PYTHONPATH": "/app/lib"},
timeout=30.0,
)
handle = await sandbox.exec_stream("tail", ["-f", "/var/log/app.log"])
async for event in handle:
match event.event_type:
case ExecEventType.STDOUT:
sys.stdout.buffer.write(event.data)
case ExecEventType.STDERR:
sys.stderr.buffer.write(event.data)
case ExecEventType.EXITED:
breakFilesystem Operations
fs = sandbox.fs
await fs.write("/tmp/config.json", b'{"debug": true}')
print(await fs.read_text("/tmp/config.json"))
for entry in await fs.list("/etc"):
print(f"{entry.path} ({entry.kind})")
await fs.copy_from_host("./local-file.txt", "/tmp/file.txt")
await fs.copy_to_host("/tmp/output.txt", "./output.txt")
if await fs.exists("/tmp/config.json"):
meta = await fs.stat("/tmp/config.json")
print(f"size: {meta.size}, kind: {meta.kind}")Named Volumes
from microsandbox import Sandbox, Volume
data = await Volume.create("python-readme-data", quota_mib=100)
writer = await Sandbox.create(
"python-readme-writer",
image="alpine",
volumes={"/data": Volume.named(data.name)},
replace=True,
)
await writer.shell("echo 'hello' > /data/message.txt")
await writer.stop()
reader = await Sandbox.create(
"python-readme-reader",
image="alpine",
volumes={"/data": Volume.named(data.name, readonly=True)},
replace=True,
)
output = await reader.shell("cat /data/message.txt")
print(output.stdout_text.strip())
await reader.stop()Network, DNS, and Ports
from microsandbox import Network, NetworkProfile, Sandbox
from microsandbox.types import DnsConfig
isolated = await Sandbox.create(
"python-readme-isolated",
image="alpine",
network=Network.none(),
replace=True,
)
filtered = await Sandbox.create(
"python-readme-filtered",
image="alpine",
network=Network(
deny_domains=("blocked.example.com",),
deny_domain_suffixes=(".evil.com",),
dns=DnsConfig(nameservers=("1.1.1.1:53",)),
),
replace=True,
)
web = await Sandbox.create(
"python-readme-web",
image="python",
ports={8080: 80},
network=Network.from_profiles(NetworkProfile.PUBLIC),
replace=True,
)Secrets
Secrets use placeholder substitution. The real value stays on the host and is substituted only for allowed network destinations.
import os
from microsandbox import Sandbox, Secret
sandbox = await Sandbox.create(
"python-readme-agent",
image="python",
secrets=[
Secret.env(
"OPENAI_API_KEY",
value=os.environ["OPENAI_API_KEY"],
allow=["api.openai.com"],
),
],
replace=True,
)Rootfs Patches
from microsandbox import Patch, Sandbox
sandbox = await Sandbox.create(
"python-readme-patched",
image="alpine",
patches=[
Patch.text("/etc/greeting.txt", "Hello!\n"),
Patch.mkdir("/app", mode=0o755),
Patch.text("/app/config.json", '{"debug": true}', mode=0o644),
Patch.append("/etc/hosts", "127.0.0.1 myapp.local\n"),
],
replace=True,
)Detached Mode
sandbox = await Sandbox.create(
"python-readme-background",
image="python",
detached=True,
replace=True,
)
handle = await Sandbox.get("python-readme-background")
reconnected = await handle.connect()
output = await reconnected.shell("echo reconnected")
print(output.stdout_text.strip())TLS Interception
from microsandbox import (
Network,
Sandbox,
ScopedUpstreamCACert,
ScopedVerifyUpstream,
TlsConfig,
)
sandbox = await Sandbox.create(
"tls-inspect",
image="python",
network=Network(
tls=TlsConfig(
bypass=("*.googleapis.com",),
verify_upstream=True,
intercepted_ports=(443,),
upstream_ca_certs=("/etc/ssl/corp-root.pem",),
scoped_upstream_ca_certs=(
ScopedUpstreamCACert("api.internal", "./certs/api-ca.pem"),
),
scoped_verify_upstream=(
ScopedVerifyUpstream("*.preview.internal", False),
),
),
),
)Metrics
from microsandbox import MiB, all_sandbox_metrics
metrics = await sandbox.metrics()
print(f"CPU: {metrics.cpu_percent:.1f}%")
print(f"Memory: {metrics.memory_bytes // MiB} MiB")
async for sample in sandbox.metrics_stream(interval=1.0):
print(f"CPU: {sample.cpu_percent:.1f}%")
break
for name, sample in (await all_sandbox_metrics()).items():
print(f"{name}: {sample.cpu_percent:.1f}%")Storage Usage and Runtime Cache Cleanup
Storage.usage() reports the selected local backend's aggregate storage. Sandbox and snapshot handles also provide storage_usage() for their managed files. Reports contain raw integer byte counts; None means unknown. Logical sizes and allocated-block observations do not measure exclusive physical ownership on filesystems that share copy-on-write blocks.
from microsandbox import Storage
usage = await Storage.usage()
print(usage.branch_memory.logical_bytes)
preview = await Storage.prune(dry_run=True, older_than_seconds=600)
for entry in preview.entries:
print(entry.path, entry.state, entry.logical_bytes)
# Explicitly remove currently unused runtime RAM after rechecking ownership.
result = await Storage.prune(older_than_seconds=600)
print(result.logical_bytes_removed)Pruning preserves durable snapshots, sandbox disks, named volumes, and stable lock files. Pending handoffs, live or paused VMs, and retained baselines protect their RAM. The report lists skipped entries and per-file errors, including partial success; physical_bytes_reclaimed remains None. Remote backend storage operations raise UnsupportedError. Static storage calls retain the backend selected when called, and handle methods retain the backend that created the handle.
Typed Errors
Python exports typed errors for the common SDK categories and falls back to MicrosandboxError for unmapped runtime variants. Catch specific errors when you need category-specific handling, and catch MicrosandboxError as the broad SDK base class.
from microsandbox import MicrosandboxError, Sandbox, SandboxAlreadyExistsError
try:
await Sandbox.create("worker", image="alpine")
except SandboxAlreadyExistsError:
print("already exists; resume it or pass replace=True")
except MicrosandboxError as exc:
print(f"microsandbox error: {exc}")Runtime Setup
Installed wheels bundle the runtime files. The setup helpers are useful for source checkouts, shared runtime installs, and surfacing setup failures at process startup.
from microsandbox import ensure_runtime
runtime = await ensure_runtime()
print(runtime.msb_path, runtime.libkrunfw_path)More Documentation
Development
From sdk/python:
uv sync --group dev
uv run maturin develop --release
uv run pytest tests
uv run ruff check .From the repository root, run an example against the SDK project:
uv run --project sdk/python python examples/python/root-oci/main.pyRuntime integration tests require local virtualization support and runtime artifacts:
cd sdk/python
uv run pytest integration/test_create_kwargs.py integration/test_exec.pyLicense
Apache-2.0
Overview (`sdk/ruby/README.md`)
Ruby SDK
The microsandbox Ruby SDK provides Ruby 3.3+ bindings for creating and
controlling local or cloud sandboxes.
Installation
Install the gem:
gem install microsandbox[!NOTE]
Platform gems are currently built and validated in CI but not yet published
to RubyGems — everygem install microsandboxstill compiles the source gem
and needs a Rust toolchain. This section describes the state once the release
wiring ships them.
Precompiled platform gems carry the native extension, so these combinations
install without a Rust toolchain:
| Gem platform | Ruby | Notes |
|---|---|---|
x86_64-linux-gnu |
3.3–4.0 | glibc 2.35+ |
aarch64-linux-gnu |
3.3–4.0 | glibc 2.35+ |
arm64-darwin |
3.3–4.0 | Apple Silicon |
x64-mingw-ucrt |
3.3–4.0 | RubyInstaller 3.3+ |
The Linux gems require glibc 2.35 or newer (Ubuntu 22.04, Debian 12, and
later). The platform name carries no glibc version, so on an older glibc host
the platform gem still installs but fails to load — force the source gem there
(see below).
The Linux gems need libcap-ng0 at runtime. Debian and Ubuntu ship it in the
base system — the stock ruby:slim images load the gem with no extra
packages, which CI verifies — so this only matters on stripped-down
environments such as distroless images.
Installing a platform gem requires RubyGems 3.3.11 or newer; earlier releases
mismatch -linux gems against glibc hosts. Run gem update --system first ifgem --version reports anything older.
musl (Alpine), Windows on ARM, and any other platform or Ruby version fall
back to the source gem automatically. The source gem compiles the extension
during install and therefore needs a Rust toolchain (1.85 or newer).
To compile from source even where a platform gem exists:
gem install microsandbox --platform rubyWith Bundler:
gem "microsandbox", force_ruby_platform: trueTo build a platform gem from a checkout, install every target Ruby, then run
from sdk/ruby:
rake cargo:patch_workspace version_check
rake gem:stage # Once each under Ruby 3.3, 3.4, and 4.0
GEM_PLATFORM=arm64-darwin rake gem:platformgem:platform refuses to package unless all three ABIs are staged. SetRUBY_ABIS (for example RUBY_ABIS=3.4) to relax that when testing against a
single local Ruby; CI never sets it.
cargo:patch_workspace builds against the Rust SDK in this checkout and saves
the standalone lockfile for restoration. Run it before version_check so you
can build an unpublished release. The check still requires matching gem and
extension versions and an exact Rust SDK pin, but skips the registry lockfile
check. When finished, run rake cargo:unpatch_workspace to restore the lockfile
and switch back to the published SDK.
To use the local backend, install the microsandbox runtime and firmware once:
require "microsandbox"
Microsandbox.install unless Microsandbox.installed?Local sandboxes require Apple Silicon virtualization on macOS or KVM on Linux. On Windows, use Windows 11 on x64 or ARM64 and enable WHP. Ruby CI currently covers Linux x86_64.
Quick start
Sandbox.with stops the sandbox when the block exits, including when the block
raises an exception:
require "microsandbox"
Microsandbox.install unless Microsandbox.installed?
Microsandbox::Sandbox.with(
"my-sandbox",
image: "python",
cpus: 1,
memory: 512
) do |sandbox|
output = sandbox.exec("python", ["-c", "print('Hello from a microVM!')"])
puts output.stdout
endBackends
The local backend is the default. Select a backend explicitly when needed:
Microsandbox.use_local_backend! # Default
# Or:
Microsandbox.use_cloud_backend!(ENV.fetch("MSB_API_KEY"))
# Or:
Microsandbox.use_cloud_profile!("production")Lifecycle
A lifecycle-owning Sandbox stops when Ruby garbage-collects it. PreferSandbox.with for scoped work. Call sandbox.detach when the VM must outlive
the Ruby object, then manage it through Sandbox.get.
If both a Sandbox.with block and its cleanup fail, the block's original
exception is preserved.
Blocking calls do not prevent other Ruby threads from running. Forked child
processes recreate the native runtime before use.
Use connect_or_create when a stable name should converge on one persisted sandbox. Existing configuration wins; options are used only if creation is necessary. Handles retain a stable id, so lifecycle calls on stale receivers refuse to act on a replacement that reused the name.
sandbox = Microsandbox::Sandbox.connect_or_create(
"worker",
image: "python",
memory: 1024
)
puts "#{sandbox.name}: #{sandbox.id}"
running = Microsandbox::Sandbox.get("worker").connect_or_start
running.request_stop
stopped = running.wait_for_status("stopped")
restarted = stopped.restart
restarted.destroyRun ruby examples/lifecycle_convergence.rb from sdk/ruby to exercise the complete local lifecycle against a live microVM. The example verifies convergence, restart, destroy, and stale-handle identity safety, then emits machine-readable timing metrics.
Networking and secrets
network: :none disables networking. An allowlist creates a default-deny
egress policy:
sandbox = Microsandbox::Sandbox.create(
"secure-sandbox",
image: "python",
network: { allowed_hosts: ["api.example.com"], allowed_ports: [443] },
secrets: [{
env: "API_KEY",
value: ENV.fetch("API_KEY"),
allowed_host: "api.example.com"
}]
)The guest receives a placeholder for each secret. The host proxy substitutes
the real value only for the allowed TLS hostname. Secret values persist in
host-side sandbox configuration, so load them from a secret manager, never log
them, and rotate them after suspected host compromise.
Route outbound TCP through an HTTP CONNECT proxy withproxy: Microsandbox::OutboundProxy.http_connect("127.0.0.1:3128"). The
microsandbox host resolves and checks each destination before opening the
tunnel. HTTP CONNECT proxy authentication and UDP are not supported.
Snapshots
Snapshot operations use the selected backend and preserve whether a snapshot
reference is an ID or a path. Existing static save calls remain available, and
opened snapshots and live handles can save through the backend they retain:
snapshot = Microsandbox::Snapshot.open("after-pip-install")
snapshot.save_to("/tmp/after-pip-install.tar.zst", with_image: true)
handle = Microsandbox::Snapshot.get("after-pip-install")
handle.save_to("/tmp/after-pip-install.tar.zst")
Microsandbox::Snapshot.save(
"after-pip-install",
"/tmp/after-pip-install.tar.zst",
plain_tar: false
)Snapshot archive operations are currently local-only. With the cloud backend, save, save_to, direct directory enumeration, reindexing, archive loading, and payload verification raise an unsupported-operation error. Capture, lookup, listing, open, and removal remain backend-neutral. The Ruby SDK does not yet expose dedicated sandbox restoration; use another SDK or the CLI for that operation.
Supported surface
The gem supports sandbox lifecycle operations, collected exec and shell
output, SSH exec, logs, metrics, guest filesystem operations, local image,
volume, and snapshot management, local or cloud backend selection, and typed
error classes (see Errors).
SSH exec inherits the global inactivity timeout by default. Override it for a
single command in seconds, or use 0 to disable it:
output = sandbox.ssh_exec("long-running-agent", inactivity_timeout: 1_800)
persistent = sandbox.ssh_exec("long-running-agent", inactivity_timeout: 0)Streaming exec, logs, metrics, and filesystem handles; interactive SSH/SFTP;
live modification plans; and the complete Rust network and mount builders are
not currently exposed. Use the Rust SDK when those APIs are required.
Errors
Every error reported by a sandbox, image, volume, snapshot, or backend
operation is a Microsandbox::Error, so rescue Microsandbox::Error catches
all of them. The native layer raises the subclass matching the core error,
which lets callers branch on the failure without matching message text:
begin
sandbox.exec("sleep", ["30"], timeout: 1)
rescue Microsandbox::ExecTimeoutError => error
puts "timed out: #{error.message}"
rescue Microsandbox::Error => error
puts "#{error.code}: #{error.message}"
endClass names and #code strings mirror the Python SDK; the snapshot,
exec-failed, and volume-already-exists classes follow the Go SDK's finer
coverage. All classes are direct subclasses of Microsandbox::Error
(code microsandbox-error):
| Group | Classes |
|---|---|
| Runtime bootstrap | RuntimeNotInstalledError, RuntimeIncompleteError |
| Configuration | InvalidConfigError, NoDefaultCommandError |
| Lifecycle | SandboxNotFoundError, SandboxNotRunningError, SandboxAlreadyExistsError, SandboxReplacedError, SandboxStillRunningError, SandboxStopTimedOutError, StopTimeoutError |
| Execution | ExecTimeoutError, ExecFailedError |
| Filesystem | FilesystemError, PathNotFoundError |
| Volumes and images | VolumeNotFoundError, VolumeAlreadyExistsError, ImageNotFoundError, ImageInUseError, ImagePullFailedError |
| Snapshots | SnapshotNotFoundError, SnapshotAlreadyExistsError, SnapshotSandboxRunningError, SnapshotImageMissingError, SnapshotIntegrityError, SnapshotSourceRecoveryError, SnapshotMigrationError |
| Networking | NetworkPolicyError, SecretViolationError, TlsError |
| I/O | IoError |
| Metrics | MetricsDisabledError, MetricsUnavailableError |
| Runtime compatibility | UnsupportedOperationError |
| Backend routing | CloudHttpError, UnsupportedError |
Each class exposes its stable, machine-readable code through .code and#code (for example Microsandbox::ExecTimeoutError.code == "exec-timeout").
Core errors without a dedicated class raise Microsandbox::Error itself.PathNotFoundError, ImagePullFailedError, SecretViolationError,TlsError, and SandboxStopTimedOutError are defined for parity with the
Python SDK but are not raised by the current core; an explicitstop_with_timeout that runs out of time raises StopTimeoutError.
UnsupportedError is raised when the selected backend does not implement an
operation. Its message names the Ruby API and the remedy, both also available
as attributes:
Microsandbox.use_cloud_backend!(ENV.fetch("MSB_API_KEY"))
begin
Microsandbox::Sandbox.create("my-sandbox", image: "python", replace: true)
rescue Microsandbox::UnsupportedError => error
error.message # => "sandbox.create is not supported by this backend: the replace option is not accepted here"
error.operation # => "sandbox.create"
error.hint # => "the replace option is not accepted here"
endSnapshotSourceRecoveryError is raised when a snapshot was captured but the
source sandbox failed to recover its prior execution state. It carries the
recovery locator as attributes, so there is no need to parse the message:source_sandbox, checkpoint_id, checkpoint_root, checkpoint_path,detail, publication_error, and artifact. artifact is a Hash with"kind" ("installed" or "archive"), "path", "snapshot_id", and"digest" keys, set only when the requested snapshot was published;
otherwise checkpoint_path names the retained runtime-local checkpoint. The
error does not imply that the source is running or safe to resume.
Argument validation is not covered by that guarantee: unknown keywords and
wrongly typed values keep raising Ruby's ArgumentError and TypeError
before any operation runs.
Development
The gem, native extension, and published Rust SDK must use the same version.rake version_check verifies their versions, the exact SDK pin, and the
standalone lockfile's registry entry.
After publishing the Rust SDK, the release workflow opens a PR to refreshext/microsandbox/Cargo.lock. To refresh it manually, runcargo update -p microsandbox --precise <version> from ext/microsandbox,
using the version pinned in Cargo.toml with no workspace patch active.
Overview (`sdk/rust/README.md`)
microsandbox
Lightweight VM sandboxes for Rust applications that need hardware-level isolation for AI agents, tools, tests, and untrusted code.
microsandbox is the Rust SDK for the microsandbox runtime. It exposes an async API for creating microVM-backed sandboxes, running commands, reading and writing guest files, managing volumes, configuring network policy, and working with secrets.
For full API documentation, use the docs site and generated Rust docs:
Features
- Hardware VM isolation with a guest Linux kernel
- OCI image, bind-rootfs, disk-image, and snapshot-based sandboxes
- Collected and streaming command execution
- Guest filesystem read, write, list, copy, stat, and stream operations
- Named volumes, bind mounts, tmpfs mounts, and disk-image mounts
- Network policies, DNS filtering, TLS interception, secrets, and port publishing
- Rootfs patches before boot
- Detached sandboxes that can outlive the Rust process
- Metrics, logs, snapshots, SSH/SFTP, image cache, and local/cloud backend helpers
Requirements
- Rust toolchain with Rust 2024 edition support
- Linux with KVM, macOS with Apple Silicon, or Windows 11 (x64 or ARM64) with WHP enabled
- Windows support is currently preview; see the Windows troubleshooting guide for WHP and runtime setup notes.
Installation
cargo add microsandboxCargo Features
| Feature | Default | Description |
|---|---|---|
local |
yes | Local runtime, setup, image cache, snapshots, metrics, and filesystem-backed volume APIs |
cloud |
yes | Cloud API backend and remote sandbox/volume operations |
keyring |
yes | Registry credential lookup through the platform keyring |
net |
yes | Network configuration, port publishing, policies, TLS interception, and secrets; the SDK uses the type/builder surface without compiling the host network engine |
download-binaries |
yes | Install a matching official msb + libkrunfw pair during Cargo builds; implies local |
embed-binaries |
no | Embed a compressed msb + libkrunfw archive for offline runtime installation; implies local |
ssh |
no | SSH, SFTP, and interactive SSH helpers |
# Cloud only
cargo add microsandbox --no-default-features --features cloud,net
# Local, with automatic runtime installation at build time
cargo add microsandbox --no-default-features --features local,net,download-binaries,keyring
# Local, without downloading or embedding runtime binaries
cargo add microsandbox --no-default-features --features local,netFor the last option, install the runtime with the CLI installer or call setup::ensure_runtime() at startup. See Runtime setup for details.
Quick Start
use microsandbox::Sandbox;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let sandbox = Sandbox::builder("rust-readme")
.image("alpine")
.cpus(1)
.memory(512)
.replace()
.create()
.await?;
let output = sandbox.shell("echo 'Hello from microsandbox!'").await?;
println!("{}", output.stdout()?.trim());
sandbox.stop().await?;
Sandbox::remove("rust-readme").await?;
Ok(())
}Reusable Lifecycle Convergence
Use connect_or_create when a stable name should converge on one persisted sandbox. Existing configuration wins; the builder is used only if creation is necessary. Handles retain a stable id, so lifecycle calls on stale receivers refuse to act on a replacement that reused the name.
let sandbox = Sandbox::builder("worker")
.image("python")
.memory(1024)
.connect_or_create()
.await?;
println!("{}: {}", sandbox.name(), sandbox.id());
let running = Sandbox::get("worker").await?.connect_or_start().await?;
running.request_stop().await?;
let stopped = running
.wait_for_status(microsandbox::sandbox::SandboxStatus::Stopped)
.await?;
let restarted = stopped.restart().await?;
restarted.destroy().await?;Common Examples
These snippets assume you already have a live sandbox: Sandbox. See examples/rust for complete runnable crates.
Fork a Live Sandbox
Forking copies a running or paused local sandbox's disk and execution state into an independent child. Memory uses copy-on-write automatically. The source keeps its previous running or paused state. Host resources require explicit bindings; see forking and resource bindings.
let child = sandbox.fork("experiment").fork().await?;
child.stop().await?;Use fork_many(["alice", "bob"]).fork().await? to capture once for several children. Inspect every returned outcome: one child's startup failure does not remove successful siblings. See the fork API reference.
Restoring starts from a saved snapshot instead. Use .cow_memory() to request copy-on-write memory for a full-snapshot restore. A generation describes snapshot-history progression; a branch describes a distinct path through that history. The former live branch APIs and old CoW restore names remain deprecated aliases. See restore migration notes for the old-to-new names and language-specific deprecation notices.
Command Execution
use microsandbox::ExecEvent;
let output = sandbox.exec("python3", ["-c", "print(1 + 1)"]).await?;
println!("stdout: {}", output.stdout()?);
println!("exit code: {}", output.status().code);
let mut handle = sandbox.exec_stream("tail", ["-f", "/var/log/app.log"]).await?;
while let Some(event) = handle.recv().await {
match event {
ExecEvent::Stdout(data) => print!("{}", String::from_utf8_lossy(&data)),
ExecEvent::Stderr(data) => eprint!("{}", String::from_utf8_lossy(&data)),
ExecEvent::Exited { code } => {
println!("exited with {code}");
break;
}
ExecEvent::Started { .. } => {}
ExecEvent::Failed(err) => {
eprintln!("exec failed: {err:?}");
break;
}
ExecEvent::StdinError(err) => eprintln!("stdin error: {err:?}"),
}
}Filesystem Operations
let fs = sandbox.fs();
fs.write("/tmp/config.json", br#"{"debug": true}"#).await?;
let data = fs.read("/tmp/config.json").await?;
println!("{}", String::from_utf8_lossy(&data));
for entry in fs.list("/etc").await? {
println!("{} ({:?})", entry.path, entry.kind);
}Named Volumes
use microsandbox::{Sandbox, Volume, size::SizeExt};
let data = Volume::builder("readme-data").quota(100.mib()).create().await?;
let writer = Sandbox::builder("readme-writer")
.image("alpine")
.volume("/data", |v| v.named(data.name()))
.replace()
.create()
.await?;
writer.shell("echo 'hello' > /data/message.txt").await?;
writer.stop().await?;
let reader = Sandbox::builder("readme-reader")
.image("alpine")
.volume("/data", |v| v.named(data.name()).readonly())
.replace()
.create()
.await?;
let output = reader.shell("cat /data/message.txt").await?;
println!("{}", output.stdout()?.trim());Network Policies
use microsandbox::{NetworkPolicy, Sandbox};
let isolated = Sandbox::builder("readme-isolated")
.image("alpine")
.network(|n| n.policy(NetworkPolicy::none()))
.replace()
.create()
.await?;
let filtered_policy = NetworkPolicy::builder()
.default_allow()
.rule(|r| r.egress().deny().domain("blocked.example.com"))
.rule(|r| r.egress().deny().domain_suffix(".evil.com"))
.build()?;
let filtered = Sandbox::builder("readme-filtered")
.image("alpine")
.network(|n| n.policy(filtered_policy))
.replace()
.create()
.await?;Use .disable_network() when the sandbox should not receive a network interface at all. Use NetworkPolicy::none() when the interface should exist but policy should deny traffic.
Port Publishing
let sandbox = Sandbox::builder("readme-web")
.image("python")
.port(8080, 80)
.replace()
.create()
.await?;Secrets
Secrets use placeholder substitution. The real value stays on the host and is substituted only for allowed network destinations.
let sandbox = Sandbox::builder("readme-agent")
.image("python")
.secret_env("OPENAI_API_KEY", "sk-real-secret-123", "api.openai.com")
.replace()
.create()
.await?;Rootfs Patches
let sandbox = Sandbox::builder("readme-patched")
.image("alpine")
.patch(|p| {
p.text("/etc/greeting.txt", "Hello!\n", None, false)
.mkdir("/app", Some(0o755))
.append("/etc/hosts", "127.0.0.1 myapp.local\n")
})
.replace()
.create()
.await?;Detached Mode
let sandbox = Sandbox::builder("readme-background")
.image("python")
.detached(true)
.replace()
.create()
.await?;
sandbox.detach().await;
let handle = Sandbox::get("readme-background").await?;
let reconnected = handle.connect().await?;
let output = reconnected.shell("echo reconnected").await?;
println!("{}", output.stdout()?.trim());More Documentation
License
Apache-2.0
Overview (`packages/agent-client/rust/README.md`)
microsandbox-agent-client
AgentClient is Client<AgentProtocol> from microsandbox-protocol-client. The agent package supplies relay setup and generation metadata; the shared engine supplies one reader, writer, allocator, and ownership registry. SDK services such as execution collection and filesystem handles remain in microsandbox.
TypedMessage / EncodedMessage / raw envelope / exact packet
|
Client<AgentProtocol>
|
current or legacy relay handshake
|
agent relayNative, encoded, and raw messages
use microsandbox_agent_client::{AgentClient, TypedMessage, EncodedMessage};
use microsandbox_protocol::{fs::{FsOp, FsRequest, FsResponse}, message::MessageType};
async fn inspect(client: &AgentClient) -> Result<(), Box<dyn std::error::Error>> {
let request = FsRequest {
bulk: None,
op: FsOp::Stat { path: "/etc/os-release".into(), follow_symlink: true },
};
let reply = client.request(TypedMessage::new(MessageType::FsRequest, &request)).await?;
let response: FsResponse = reply.payload()?;
println!("{}", response.ok);
// The caller's payload bytes are not parsed or normalized by this path.
let payload = vec![0xa0];
let _reply = client.request(EncodedMessage::new(MessageType::Ping, payload)).await?;
Ok(())
}Message.t is the actual wire string. Unknown names and unknown envelope fields remain inspectable: Message::raw() borrows the original frame and into_raw() returns it. Ordinary requests surface peer error messages as messages. Optional Request<AgentProtocol> implementations add checked unary decoding without narrowing the generic API.
Owned streams and explicit IDs
use microsandbox_agent_client::{AgentClient, TypedMessage};
use microsandbox_protocol::{exec::{ExecRequest, ExecStdin}, message::MessageType};
async fn exchange(client: &AgentClient, request: &ExecRequest) -> Result<(), Box<dyn std::error::Error>> {
let stream = client.stream(TypedMessage::new(MessageType::ExecRequest, request)).await?;
let id = stream.id();
let (sender, mut receiver) = stream.into_parts();
client.send(id, TypedMessage::new(MessageType::ExecStdin, ExecStdin { data: vec![] })).await?;
sender.send(TypedMessage::new(MessageType::ExecStdin, ExecStdin { data: b"input".to_vec() })).await?;
while let Some(message) = receiver.recv().await? {
println!("{}", message.t);
}
Ok(())
}Both split parts retain the connection and ID lease; sender handles may be cloned and there is one consuming receiver. Terminal receipt disables sends and delivers the terminal frame once. Closing or dropping the receiver disables sends and retains bounded drain state until terminal completion. It sends no process signal or filesystem EOF. Transport loss before a terminal frame is an error, distinct from clean exhaustion.
request_raw(flags, body), stream_raw(flags, body), and send_raw(id, flags, body) exchange opaque envelopes without CBOR decoding. write_unchecked(packet_bytes) serializes exact caller bytes without allocating an ID or subscribing for replies. TransportPacket remains an optional standalone framing helper; packet.into_bytes() feeds the exact write API. Standalone packet readers require exclusive transport ownership.
Setup and metadata
Enable uds for native Unix sockets or named-pipe for Windows pipes. connect_stream accepts an owned AsyncRead + AsyncWrite + Unpin + Send transport, including UdsTransport and NamedPipeTransport. The stream feature name remains accepted for existing manifests; generic owned transports are available without additional dependencies.
use microsandbox_agent_client::AgentClient;
use std::time::Duration;
async fn connect(path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
let client = AgentClient::connect_with(path, |o| o.setup_timeout(Duration::from_secs(5))).await?;
println!("{}", client.ready().agent_version());
println!("{}", client.ready().negotiated_version);
println!("{} ready bytes", client.ready().ready_bytes().len());
client.close().await;
Ok(())
}Setup has one deadline across dial and handshake. The current [id_min,id_max] relay prologue and the supported pre-0.5 [id_offset] prologue retain their existing parsing, ID ranges, and ready payload behavior. Known operations use the smaller host/peer generation as their availability gate. Current connections continue emitting the host's existing envelope generation; legacy connections emit generation one. Source-level API changes do not retire the legacy wire path.
Migration from the previous active client
| Previous API | Shared-client API |
|---|---|
connect(path) / connect_stream(stream) |
Same entry points; owned streams no longer require the stream feature |
request(type, &payload) |
request(TypedMessage::new(type, &payload)) |
stream(type, &payload) -> (id, receiver) |
stream(TypedMessage::new(type, &payload)), then id() / into_parts() |
send(id, type, &payload) |
send(id, TypedMessage::new(type, &payload)) |
request_raw(flags, body) |
Same call; retains drain state after a nonterminal first reply |
stream_raw(flags, body) -> (id, receiver) |
Owned raw stream, then id() / into_parts() |
send_raw(id, flags, body) |
Same call, requiring a live owned ID |
connect_with_timeout(path, duration) |
`connect_with(path, |
connect_stream_with_timeout(stream, duration) |
`connect_stream_with(stream, |
connect_with_deadline(path, deadline) |
`connect_with(path, |
connect_stream_with_deadline(stream, deadline) |
`connect_stream_with(stream, |
ready() |
ready().agent |
ready_bytes() |
ready().ready_bytes() |
negotiated_version() / supports(type) |
ready().negotiated_version / ready().supports(type) |
protocol() / is_legacy_protocol() |
ready().wire_format / ready().is_legacy_protocol() |
agent_version() |
ready().agent_version() |
ensure_version_compat(type) |
AgentProtocol::ensure_version_compat_for(type, client.ready().negotiated_version) |
AgentClient::ensure_version_compat_for(type, generation) |
AgentProtocol::ensure_version_compat_for(type, generation) |
Unwired AgentStream<T> / packet-only AgentTransport |
Active AgentStream / owned byte transports accepted directly by connect_stream |
| Connection close by ownership transfer | Shared close(&self); last client/stream owner also closes the transport |
ClientError.delivery distinguishes NotSent from Unknown after writer admission. Requests are never automatically replayed. Raw stream access, known-ID sends, custom transports, ready bytes, dynamic names, and encoded payloads remain available without adopting SDK domain objects.
Deadline conversions use tokio::time::Instant, matching the original absolute-deadline API. Keep the conversion inside the options closure so it uses the remaining duration when setup begins. Known native and encoded sends perform the same availability check automatically; the public AgentProtocol helper supports callers retaining only the generation. Unsupported operations now use the shared ClientError category rather than the previous agent-specific error fields.
Validation: cargo test -p microsandbox-agent-client --all-features --locked. Unix-socket tests require permission to bind temporary local sockets. The migrated historical handshake cases are source-level regression tests; they do not replace live historical runtime/SDK validation.
The SDK retains an internal optimized client for generation-eight bulk transfers and Unix shared arenas. Its existing bulk APIs remain available; the generic client is the public transport-independent framed API.
Overview (`packages/agent-client/typescript/README.md`)
@microsandbox/agent-client
Low-level TypeScript access to the microsandbox agent protocol. AgentClient is Client<AgentProtocol> from @microsandbox/protocol-client; the agent package supplies relay setup, its existing encoder, and generation gates. The shared engine owns framing, IDs, queues, cancellation, and transport lifetime. Sandbox lifecycle and process/filesystem convenience APIs belong to the SDK.
AgentProtocol (setup, encoder, gates)
|
Client<AgentProtocol>
/ \
native / encoded raw / exact packets
|
Unix socket / named pipe / WebSocketInstallation and entry points
Install with npm install @microsandbox/agent-client. Node.js 22 or newer is required for native transports. The root entry is browser-safe. @microsandbox/agent-client/node imports Node's net module and accepts native endpoint paths verbatim, including Windows named-pipe names.
Native messages
import { connectUnix } from "@microsandbox/agent-client/node";
import { typedMessage } from "@microsandbox/agent-client";
const client = await connectUnix("/tmp/msb-agent.sock", { setupTimeoutMs: 10_000 });
try {
const response = await client.request(typedMessage("core.fs.request", {
op: { Stat: { path: "/etc/os-release", follow_symlink: true } },
}), { requestTimeoutMs: 5_000 });
console.log(response.type, response.decodePayload());
console.log(client.ready.agentVersion, client.ready.negotiatedVersion);
} finally {
await client.close();
}typedMessage(name, value) asks the agent codec to encode a native payload. encodedMessage(name, bytes) supplies only the application payload; it does not supply an envelope or framed packet. Both paths apply known message gates and flags. Names are open strings, so extensions remain accessible without editing an enum. These methods do not validate an application schema. requestTyped() accepts an optional checked request with its own result decoder and requires a terminal response.
The current TypeScript agent encoder continues to emit generation-five envelopes, including its existing CBOR map and byte-string representations. The ready generation controls feature gates. The supported pre-0.5 relay prologue selects generation-one envelopes and rejects unavailable filesystem/TCP operations before sending. The captured ready frame and unknown fields remain available through client.ready.frame and client.ready.readyBytes.
Browser streams
import { AgentClient, WebSocketConnector, typedMessage } from "@microsandbox/agent-client";
const client = await AgentClient.connectConnector(
new WebSocketConnector("wss://relay.example.com/agent"),
{ setupTimeoutMs: 10_000 },
);
try {
const stream = await client.openStream(typedMessage("core.exec.request", {
cmd: "sh", args: ["-lc", "echo hello"],
}));
try {
for await (const frame of stream) {
console.log(frame.type, frame.payload);
}
} finally {
stream.close();
}
} finally {
await client.close();
}The relay must forward the agent's binary byte stream, including its prologue. WebSocketTransport also wraps a caller-constructed socket for custom authentication. Its incoming queue defaults to 8 MiB and 4096 messages; overflow closes the connection because browser WebSockets cannot pause incoming messages. Outgoing writes wait for the browser's send queue to drain. The connector and handshake share one setup deadline.
Raw and exact access
import { type AgentClient, TransportPacket } from "@microsandbox/agent-client";
async function exchangeOpaque(client: AgentClient, envelope: Uint8Array) {
const stream = await client.openStreamRaw(0, envelope);
const { sender, receiver } = stream.split();
try {
await sender.send(0, envelope);
await client.sendRaw(sender.id, 0, envelope);
return await receiver.next({ requestTimeoutMs: 5_000 });
} finally {
receiver.close();
sender.close();
}
}
async function forwardExactPacket(client: AgentClient, bytes: Uint8Array) {
await client.writeUnchecked(TransportPacket.fromBytes(bytes));
}Raw APIs leave envelope and payload bytes opaque. requestRaw(flags, body) returns the first raw reply. openStreamRaw() receives through the terminal reply. sendRaw(id, flags, body) requires an active ID owned by this connection. A split sender retains its original lease and cannot regain permission if the numeric ID is reused. writeUnchecked(bytes) can write even deliberately malformed transport bytes; TransportPacket.fromBytes() optionally validates a single packet first.
Native replies retain the complete original raw frame as frame.raw, including unknown envelope fields. core.error is an ordinary terminal agent response; interpreting it belongs to the caller or a checked request decoder.
Ownership and failures
Streams own their send and receive halves. split() transfers those halves and invalidates the original stream's send/receive methods. Closing a receiver or leaving async iteration stops local delivery; it sends no remote cancel, signal, or EOF. An abandoned ID remains draining until its terminal frame arrives. A timed-out next() does not consume the next arriving frame.
client.clone() shares the connection. client.close() closes every shared handle and wakes pending operations. Explicitly close clients and streams: JavaScript garbage collection only offers best-effort cleanup. A request deadline covers writer admission and the local response wait; it does not cancel remote execution. ClientError.delivery is not_sent before admission and unknown afterward. Nothing automatically retries an admitted operation.
Migration from the previous package API
| Previous surface | Shared-engine surface |
|---|---|
Independent AgentClient class/router |
AgentClient = Client<AgentProtocol> plus convenience constructors |
handshakeTimeoutMs |
setupTimeoutMs, covering dial and handshake together |
client.negotiatedVersion() |
client.ready.negotiatedVersion |
Closed MessageType parameter |
Open string names; MessageType still lists known names |
Packet-oriented custom AgentTransport |
Ordered read(maxBytes), write(bytes), close() byte transport |
InboundFrame.fromRawFrame(frame) |
new AgentEnvelopeCodec().decode(frame) |
new AgentStream(...) |
Obtain owned streams from client.openStream() or openStreamRaw() |
| Request, encoded payload, stream, exact packet methods | Retained on the generic client, with additional raw and split interfaces |
These are public source API changes. Existing agent wire behavior is preserved; source and byte tests do not replace the required historical runtime/SDK launch tests.
Development
From the repository's packages directory, run npm ci, npm run build, npm run typecheck, and npm test. The private workspace links the shared package locally; published dependency metadata uses the release version. The README's TypeScript examples are checked with the public package declarations.
Overview (`packages/control-client/rust/README.md`)
Rust control client
ControlConnection discovers whether an existing control endpoint supports framed CBOR or legacy JSON. ControlClient is the explicitly framed Client<ControlProtocol> entry point, and JsonControlClient is an explicit legacy unary adapter. Their checked helpers share the same operation records.
ControlConnection -- JSON capabilities --+-- advertised CBOR: fresh stream + hello
| |
| ControlClient
|
+-- legacy: JsonControlClientAutomatic discovery has one deadline across the probe, redial, and hello. It selects CBOR only after a valid affirmative capability response. Malformed replies, arbitrary peer errors, empty EOF, and failed framed setup are errors. There is no error-string downgrade or mutation replay. ControlClient sends its hello directly when callers already know the peer supports framed control.
use microsandbox_control_client::{
ControlConnection, ControlMessageType, ControlReply, Empty, GetMemoryState, TypedMessage,
};
async fn inspect(path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
let connection = ControlConnection::connect(path).await?;
let reply = connection.request(TypedMessage::new(ControlMessageType::MemoryState, Empty {})).await?;
match reply {
ControlReply::Framed(message) => println!("{}", message.t),
ControlReply::Json(reply) => println!("{} original JSON bytes", reply.raw().len()),
}
println!("{} MiB", connection.request_typed(&GetMemoryState).await?.target_mib);
connection.close().await;
Ok(())
}In framed mode, clones reuse one connection. Automatic JSON mode rediscovers before each fresh exchange when its connector cannot prove runtime identity. An owner implementing VerifiedControlConnector verifies the connected peer and the runtime's OS birth token on every dial, then rechecks the active run before each operation; connect_verified_connector can reuse that owner's JSON selection. A path or PID alone is insufficient. A detected change invalidates the handle before sending instead of silently retargeting the request. The runtime owner, such as an SDK backend, supplies that identity implementation; the protocol package does not claim to verify it itself.
JsonControlClient::new(path) and from_connector(...) are inert constructors for callers explicitly choosing legacy mode. Its request translates only known native requests and returns the actual JsonReply, including ok:false replies. Checked helpers report LegacyRemote with the original reply and unknown batch progress. JSON numbers retain their original tokens; checked memory fields use u64 without a floating-point intermediate. connection.framed() fails locally in JSON mode, and encoded-payload input is rejected there before dialing. Raw streams and exact packets remain available on the explicitly framed client.
connection.capabilities() borrows the released generation-1 discovery projection without network I/O, while connection.runtime_capabilities() exposes the complete discovered facility set. GetCapabilities remains available for a fresh generation-1-compatible observation and GetRuntimeCapabilities requests the complete record on generation 2. closed().await observes shared closure, including an idle framed disconnect; a successful JSON exchange ending in EOF does not close the session.
Generation 1 remains the released capabilities, memory, CPU, and secrets surface. Generation 2 adds CreateCheckpoint, CreateDiskCheckpoint, CreateBranch, PauseRuntime, ResumeRuntime, GetPauseState, GrowRootDisk, and CompactDisks. ControlConnection talking to a framed generation-1 runtime selects the historical JSON representation for these checked operations before sending anything; it never retries a mutation after a framed failure. The explicit ControlClient instead returns a not-sent unsupported-operation error. Linux descriptor-backed branch creation remains an SDK-level JSON/SCM_RIGHTS exception because ordinary framed messages cannot transfer file descriptors.
Enable uds for Unix endpoint connections or named-pipe for Windows endpoint connections. Caller-owned transports and connectors can be used without either native feature. Endpoint paths are accepted verbatim.
use microsandbox_control_client::{
ControlClient, GetMemoryState, SetMemoryTarget, size::SizeExt,
};
async fn resize(path: &std::path::Path) -> Result<(), Box<dyn std::error::Error>> {
let client = ControlClient::connect(path).await?;
let before = client.request_typed(&GetMemoryState).await?;
let accepted = client.request_typed(&SetMemoryTarget::new(2048.mib())).await?;
println!("{} -> {} MiB", before.target_mib, accepted.target_mib);
client.close().await;
Ok(())
}Memory wire records retain u64 values. The existing size helpers retain their conversion rules; direct SetMemoryTarget { total_mib } construction provides full-width wire input. Target responses report acceptance and current observations, not guest convergence. Ordered secret batches preserve partial completion and the index of the first failed entry.
The client keeps request, stream, request_raw, stream_raw, explicit-ID send/send_raw, exact write_unchecked, owned into_parts, and checked request_typed operations. Generic requests return peer error frames as messages; checked helpers interpret them and retain the original response. No sandbox, execution, filesystem, or convergence service is required to use any of these paths.
use microsandbox_control_client::{ControlClient, EncodedMessage};
async fn inspect(client: &ControlClient, payload: Vec<u8>) -> Result<(), Box<dyn std::error::Error>> {
let response = client.request(EncodedMessage::new("extension.inspect", payload)).await?;
println!("{}: {} payload bytes", response.t, response.p.len());
Ok(())
}Setup defaults to one ten-second deadline and requests to a thirty-second local wait. Request expiry does not cancel remote work or trigger replay. ClientError retains NotSent versus Unknown delivery. Clones and owned streams share a connection; close closes it for all owners.
Run cargo test -p microsandbox-control-client --all-features --locked. For live tests, set MSB_CONTROL_TEST_SOCKET and MSB_CONTROL_TEST_MODE=cbor or json. The explicitly framed test requires CBOR; run only live_automatic_discovery_and_explicit_json -- --ignored --exact against a JSON-only runtime. Use bounded disposable fixtures (512 MiB and two CPUs), and run live tests serially. The automatic test changes targets and restores them through explicit JSON. Optional MSB_CONTROL_TEST_SECRET names a dummy fixture initially set to before and allowed for example.invalid; the test exercises legacy partial-failure reporting and restores that fixture. Skipped live tests are not runtime or platform validation.
Overview (`packages/control-client/typescript/README.md`)
TypeScript control client
ControlClient is Client<ControlProtocol> over the shared framed engine. It sends a bounded control.hello directly, negotiates generation and limits, and exposes the original welcome frame. Use it when the peer is known to support framed control. It does not probe JSON or downgrade after failure.
The root entry is browser-safe and accepts a caller-owned byte transport or reconnectable connector. The /node entry accepts an existing Unix socket or Windows named-pipe path without renaming it.
import { GetMemoryState, MiB, SetMemoryTarget, typedMessage } from "@microsandbox/control-client";
import { connectFramedControl } from "@microsandbox/control-client/node";
async function inspectAndResize(path: string) {
const client = await connectFramedControl(path, { setupTimeoutMs: 10_000 });
try {
const response = await client.request(typedMessage("control.memory.state", {}));
console.log(response.type, response.raw.body);
const before = await client.requestTyped(new GetMemoryState());
const accepted = await client.requestTyped(new SetMemoryTarget(MiB(2048)));
console.log(before.target_mib, accepted.target_mib); // bigint, even for small values
} finally {
await client.close();
}
}Generation-1 checked helpers cover capabilities, memory state/target, CPU state/target, and ordered secret updates. Generation 2 adds GetRuntimeCapabilities, CreateCheckpoint, CreateDiskCheckpoint, CreateBranch, PauseRuntime, ResumeRuntime, GetPauseState, GrowRootDisk, and CompactDisks. They validate replies and retain the actual response on ControlClientError. Generic message requests return peer error frames directly. A target reply reports acceptance and observation; it does not establish guest convergence. Secret batches stop at the first failure and retain earlier completed changes.
Use connectControl for automatic discovery on the same endpoint. It sends the existing read-only JSON capabilities request, closes that exchange, and opens a fresh framed connection only after an affirmative CBOR advertisement. Setup has one total deadline across both connections. Malformed discovery, timeout, EOF without a reply, or a failed welcome never trigger fallback or replay.
connectControl(path)
|
+-- JSON capabilities --> valid legacy reply --> fresh JSON exchange per operation
|
+-- JSON capabilities --> advertises CBOR -----> fresh hello/welcome --> shared framed clientimport { GetMemoryState, MiB, SetMemoryTarget, typedMessage } from "@microsandbox/control-client";
import { connectControl } from "@microsandbox/control-client/node";
async function compatibleResize(path: string) {
const connection = await connectControl(path);
try {
const reply = await connection.request(typedMessage("control.memory.state", {}));
if (reply.kind === "cbor") console.log(reply.frame.raw.body);
else console.log(reply.reply.raw, reply.reply.value.get("memory"));
const before = await connection.requestTyped(new GetMemoryState());
const accepted = await connection.requestTyped(new SetMemoryTarget(MiB(2048)));
return { before, accepted };
} finally {
await connection.close();
}
}ControlConnection.connectConnector accepts a repeatable caller connector from the browser-safe root. The connection exposes the read-only generation-1 capabilities projection, the complete runtimeCapabilities discovery snapshot, mode, clone(), isClosed(), close(), request(), and requestTyped(). framed() returns the complete generic surface in CBOR mode and fails locally with unsupported_mode in JSON mode. Encoded messages likewise fail locally in JSON mode; the adapter translates only known, validated native operations.
When a framed peer selects generation 1, ControlConnection.requestTyped() sends generation-2 checked operations through their historical JSON form before any mutation is admitted. An unverified connector rediscovers the endpoint format first. This path is never a retry after a CBOR failure. The explicit ControlClient rejects those operations locally against a generation-1 welcome. Linux descriptor-backed branch creation remains outside ordinary CBOR because it requires SCM_RIGHTS; generation 2 covers direct local branching without descriptor transfer.
JsonReply contains the original response line, including its delimiter and whitespace, and a lossless Map of fields. JsonNumber.token retains each original numeric token; checked memory observations become bigint without passing through a JavaScript number. Ordinary requests return ok:false replies. Checked helpers raise legacy_remote with the original reply and unknown batch progress. Neither arbitrary diagnostic strings nor legacy errors are converted into structured CBOR errors.
For explicit legacy access, construction is inert and performs no discovery:
import { GetCpuState } from "@microsandbox/control-client";
import { jsonControl } from "@microsandbox/control-client/node";
async function inspectLegacy(path: string) {
const client = jsonControl(path);
try {
return await client.requestTyped(new GetCpuState());
} finally {
await client.close();
}
}The equivalent root API is JsonControlClient.fromConnector(connector, options?). Ordinary legacy request and response lines keep their existing size contract; only automatic discovery replies have a 64 KiB limit. Preparing a secret batch snapshots its entries without imposing the framed 4 MiB ceiling on JSON. JavaScript strings remain subject to garbage collection; explicit buffer clearing is not a guarantee that all copies have been erased.
A standalone automatic JSON connection rediscovers before each fresh operation because an endpoint path cannot establish process identity. ControlConnection.connectVerifiedConnector permits discovery reuse when the caller supplies a VerifiedControlConnector: connect must verify every connected peer against the saved OS process birth identity, and verifySession must check the active run and process before each operation. Both receive a deadline and cancellation context. Runtime replacement raises runtime_changed before mutation admission and closes the shared handle. This extension point does not implement an SDK database or OS identity verifier by itself.
MiB, GiB, KiB, TiB, and Mebibytes are exported from @microsandbox/types/size. Their arithmetic matches the SDK helpers. SetMemoryTarget validates integer precision and range before converting to the wire quantity; it also accepts bigint for full-width values.
All generic operations remain available:
| Need | Method |
|---|---|
| Native or encoded payload | request(typedMessage(...)), request(encodedMessage(...)) |
| Exact opaque envelope | requestRaw(flags, body) |
| Message or raw subscription | openStream(...), openStreamRaw(...) |
| Owned sender and receiver | stream.split() |
| Follow-up using an owned ID | sendOnStream(id, ...), sendRaw(id, ...) |
| Exact packet without a subscription | writeUnchecked(bytes) |
| Optional checked unary operation | requestTyped(request) |
| Shared connection ownership | clone(), close() |
import { ControlClient, encodedMessage, type ByteTransport } from "@microsandbox/control-client";
async function inspectExtension(transport: ByteTransport, payload: Uint8Array) {
const client = await ControlClient.connectTransport(transport);
try {
const reply = await client.request(encodedMessage("extension.inspect", payload));
return { name: reply.type, payload: reply.payload, original: reply.raw };
} finally {
await client.close();
}
}The default control request deadline is 30 seconds. Expiry abandons the local wait; it does not cancel remote work or retry it. ClientError.delivery distinguishes requests that were not admitted from unknown outcomes. Complete idle connections do not expire automatically. Explicit close closes every clone; JavaScript finalization is only best-effort cleanup.
From packages, run npm run build, npm run typecheck, and npm test. Live validation is deliberately opt-in: set MSB_CONTROL_TEST_SOCKET to a disposable framed runtime's socket and run npm test -w @microsandbox/control-client. Add MSB_CONTROL_TEST_MODE=cbor to run automatic discovery as well. The package serializes test files because live files mutate and restore the same VM's targets. A skipped live test is not platform or runtime compatibility evidence.
For a historical JSON-only runtime, set MSB_CONTROL_TEST_MODE=json and run only the compatibility live file with npm exec -w @microsandbox/control-client -- vitest run tests/compatibility-live.test.ts. It checks automatic and explicit JSON access, concurrent reads, full-width result types, and accepted memory/CPU targets, then restores the original targets. These running-runtime tests do not establish SDK launch compatibility.
The additional secret test requires MSB_CONTROL_TEST_SECRET naming a dummy fixture initially set to before and allowed for example.invalid. It exercises partial completion, malformed batches, and legacy JSON failures, then restores those fixture values.
Overview (`packages/microsandbox-types/rust/README.md`)
microsandbox-types
Shared task and wire contract types for microsandbox.
This crate is the source of truth for the backend-neutral shapes that describe a sandbox and the cloud HTTP bodies that carry them. The Rust SDK, the CLI, and the cloud backend all import these types so they agree on one definition instead of duplicating wire shapes. The generated @microsandbox/types TypeScript package is derived from this crate.
It is a leaf dependency by design. It pulls in serde, serde_json, chrono, sha2, and thiserror, and nothing from the local VM machinery (no runtime, image, network, or database crates). That keeps it cheap enough for front-end generation, cloud API models, and SDK wrappers to all depend on.
What This Crate Owns
The crate models durable user and wire intent: what the user wants to exist, not how a backend fulfills it.
- Sandbox spec (
domainmodule):SandboxSpec,SandboxResources,SandboxRuntimeOptions,EnvVar,SandboxPolicy. - Rootfs sources:
RootfsSource(bind, OCI, disk image),OciRootfsSource,DiskImageFormat,PullPolicy. - Mounts and patches:
VolumeMount,MountOptions,StatVirtualization,HostPermissions,Patch,SecurityProfile. - Volumes and snapshots:
VolumeSpec,VolumeKind,NamedVolumeCreate,NamedVolumeMode,SnapshotSpec, and the canonicalSnapshotManifestschema. - Networking:
NetworkSpec,PublishedPortSpec,PortProtocol. - Exec and logs:
Rlimit,RlimitResource,SandboxLogLevel,LogSource,HandoffInit. - Cloud wire contracts (
cloudmodule): sandbox, snapshot, pagination, message, and error request/response types. - Validation (
validationmodule):validate_sandbox_name,validate_hostname,hostname_from_sandbox_name, and theMAX_SANDBOX_NAME_BYTES/MAX_HOSTNAME_BYTESlimits.
Backend-private materialized state stays out: registry credentials, local CA paths, replace flags, pull-discovered manifest digests, snapshot upper paths, process handles, and DB rows belong to the SDK and backends, not the contract.
Usage
[dependencies]
microsandbox-types = "0.7.0"use microsandbox_types::{RootfsSource, SandboxResources, SandboxSpec};
let spec = SandboxSpec {
name: "worker".into(),
image: RootfsSource::oci("python"),
resources: SandboxResources { cpus: 2, memory_mib: 1024 },
..Default::default()
};The Rust SDK re-exports the contract types it accepts, so most SDK users get these through microsandbox::* and do not depend on this crate directly.
Serialization Notes
SandboxSpec,SandboxResources,SandboxRuntimeOptions,NetworkSpec, andMountOptionsuse#[serde(default)], so partial JSON fills missing fields from static defaults.- Lowercase-on-the-wire enums (
StatVirtualization,HostPermissions,SecurityProfile,SandboxLogLevel,LogSource,PortProtocol) match the CLI grammar and the TypeScript string unions. VolumeMounthas hand-writtenSerialize/Deserialize. It tags variants with atypefield and accepts a legacy top-levelreadonlyflag, folding it intoMountOptionson read.- Static defaults only. Nothing here reads process-global or profile config; the SDK and backends apply environment defaults before execution.
TypeScript Generation
The ts feature derives ts_rs::TS on exported wire types and builds the microsandbox-types-generate binary, which writes domain.ts, snapshot.ts, and cloud.ts beneath ../typescript/src.
# Regenerate the checked-in bindings.
cargo run -p microsandbox-types --features ts --bin microsandbox-types-generate
# Verify they are current (used in CI; exits non-zero when stale).
cargo run -p microsandbox-types --features ts --bin microsandbox-types-generate -- --checkA unit test (checked_in_bindings_match_generated_output) also fails when any generated TypeScript file drifts, so cargo test --features ts catches stale bindings too.
Testing
cargo test -p microsandbox-types
cargo test -p microsandbox-types --features tsOverview (`packages/microsandbox-types/typescript/README.md`)
@microsandbox/types
Shared task and wire contract types for microsandbox, as TypeScript.
This package is the type-only mirror of the cloud contract in the microsandbox-types Rust crate. It gives TypeScript consumers (the cloud front end and anything talking to the cloud API) the exact cloud wire shapes microsandbox uses, so a CloudSandboxSpec means the same thing on both sides of the wire. Only the domain types the cloud contract references are generated; the broader SDK/domain surface is not.
It contains no runtime code. Every export is a type; importing it adds nothing to your bundle.
Generated, Not Hand-Written
Three files are generated from the Rust crate with ts-rs, each carrying a // @generated header. Do not edit them:
src/cloud.ts— the package entry: the cloud wire types, which import and re-export their domain and snapshot dependencies.src/domain.ts— only the domain types the cloud twins transitively reference (EnvVar,Rlimit,NetworkPolicy, …).src/snapshot.ts— the canonical snapshot manifest schema embedded by cloud snapshot resources.
To change a shape, edit the Rust type and regenerate from the repo root:
cargo run -p microsandbox-types --features ts --bin microsandbox-types-generateInstall
npm install @microsandbox/typesUsage
Import the shapes you need with import type:
import type {
CloudCreateSandboxRequest,
CloudSnapshot,
CloudSnapshotOperation,
} from "@microsandbox/types";
function createSandbox(request: CloudCreateSandboxRequest) {
// POST the source-tagged request to the cloud API
}CloudCreateSnapshotRequest and CloudSnapshot are unions discriminated bykind; the current public variant is "disk". CloudSnapshotOperation carries
that same kind and exposes only the public statuses queued, in_progress,succeeded, and failed. Both snapshot creation results and disk-snapshot
restore requests use CloudSnapshotLocation to distinguish a managed artifact
ID from a host-volume path.
Generated Shape Notes
The bindings follow ts-rs conventions, which mirror the Rust serde representation:
- Cloud enums are internally tagged with a
typefield:CloudRootfsSourceis{ type: "bind"; … } | { type: "oci"; reference: string } | { type: "disk_image"; … }, andCloudVolumeMount/CloudHostPattern/CloudViolationActionfollow the same shape. CloudCreateSandboxRequestis a flat union discriminated bysource: "oci" | "bind" | "disk_image" | "disk_snapshot". Common sandbox fields stay flat on every variant; source-specific fields such asreference,path, anddisk_snapshot_refappear only where they apply. Servers also accept the legacyimagerequest shape withoutsourceduring migration.CloudCreateSnapshotRequestandCloudSnapshotare flat unions discriminated by the currentkind: "disk"; common snapshot fields stay flat beside the discriminator.- Lowercase domain enums like
StatVirtualizationare string-literal unions ("strict" | "relaxed" | "off"). - Optional Rust fields are
T | null; fields skipped when absent are?:optional. - Referenced domain and snapshot types live in
domain.tsandsnapshot.tsand are re-exported from the package entry, so a single import from@microsandbox/typessees the whole cloud contract.
Build And Typecheck
npm run build # tsc -> dist/
npm run typecheck # tsc --noEmitOverview (`packages/protocol-client/rust/README.md`)
Generic framed client
Client owns one byte connection, its correlation IDs, and all routed subscriptions. P: Protocol establishes the transport and selects message metadata and an EnvelopeCodec. Protocol consumers can implement both traits outside this crate. tests/engine.rs exercises external protocol setup and routing, while tests/external_codec.rs supplies an external connector and a non-CBOR envelope codec across native, encoded, raw, checked, stream, explicit-ID, and exact-packet calls. Rust native payload serialization remains CBOR; the custom codec controls its enclosing envelope, and encoded/raw paths retain caller-supplied bytes.
native payload ---- TypedMessage ---+
encoded payload --- EncodedMessage -+--> codec --> shared writer
raw envelope -----------------------+-----------> shared writer
exact packet -----------------------------------> shared writer
shared reader --> ID routing --> RawStream --> Stream (decode on recv)request returns the first response. If it is not terminal, the router retains the ID and discards later replies until terminal completion. request_typed additionally requires a terminal response and invokes a borrowed Request implementation's checked decoder. Application errors remain that implementation's responsibility; the generic engine does not equate a terminal response with success.
stream and stream_raw return owned subscriptions. into_parts() moves them into a cloneable sender and one receiver. Both retain the connection. Dropping or closing the receiver disables sends and drains any admitted operation until terminal completion; it does not send a process signal, file EOF, TCP close, or cancellation packet. Old sender handles cannot gain permission when an ID is reused.
send and send_raw support explicit IDs owned by the connection. write_unchecked serializes caller-owned packet bytes without creating an ID or response subscription. Native and encoded-payload paths apply protocol availability gates; raw paths preserve opaque bytes. A decoded Message retains its original RawFrame, including unknown envelope fields.
Connections are shared handles. Explicit close(&self) closes every handle and wakes waiters. closed().await observes closure, including idle peer disconnection, without polling; canceling that wait leaves the connection alone. Last-owner drop closes the transport; the reader and writer do not keep the owner alive. A stream remains an owner after a temporary client handle is dropped.
Queues have item and combined byte limits. A retained slow receiver can backpressure the whole connection; fair per-stream flow control is not provided by the wire protocol. Dropping that receiver releases its queued frames and wakes blocked routing. Active and draining IDs both consume in-flight capacity.
ClientError.delivery distinguishes NotSent before writer admission from Unknown afterward. Timeouts and dropped futures stop local waiting, without retrying or undoing peer work. An EOF before terminal completion is an error; truncation inside a frame is reported separately. Optional incomplete-frame deadlines start at the first byte and do not expire idle connections.
The uds and named-pipe features enable LocalConnector on their platforms. It uses the endpoint supplied by the caller without inventing names. A custom Connector receives an absolute setup deadline and returns an exclusively owned byte stream. connect_stream accepts a transport that the caller has already dialed and authenticated.
Validation: cargo test -p microsandbox-protocol-client --all-features --locked. The Unix connector test binds a temporary socket and therefore needs an execution environment that permits local socket creation.
Protocols choose whether completed correlation IDs may be reused. Agent connections retire IDs for the lifetime of the connection, matching the relay; control connections may reuse an ID after its terminal response. Exhaustion is a local error and does not reconnect or replay a request.
Overview (`packages/protocol-client/typescript/README.md`)
@microsandbox/protocol-client
A protocol-neutral framed client for TypeScript. Client owns one byte transport, correlation IDs, bounded queues, a serialized writer, and response routing. P owns setup, its envelope codec, and message gates. Agent and control operations stay in their protocol packages; sandbox lifecycle stays in the SDK.
Client<P>
/ | \
native payload encoded raw frame / exact packet
\ | /
protocol codec + shared router
|
ByteTransportImplementing a protocol
An external package can implement setup and metadata using only public declarations. This example protocol reads one ready byte and uses the shared CBOR envelope. Its server must implement that same handshake.
import {
Client, CborEnvelopeCodec, readExactly,
type Protocol, type ByteTransport, type EstablishContext, type Established, type SendMetadata,
} from "@microsandbox/protocol-client";
class ExampleProtocol implements Protocol<number> {
async establish(transport: ByteTransport, context: EstablishContext): Promise<Established<number>> {
const ready = (await readExactly(transport, 1, context.signal))[0]!;
return {
transport, ready, codec: new CborEnvelopeCodec(), limits: context.limits,
ids: { start: 1, endExclusive: 2 ** 32 },
};
}
prepare(ready: number, _wireName: string): SendMetadata {
return { generation: ready, flags: 0 };
}
}
async function connect(transport: ByteTransport) {
return Client.connectTransport(transport, new ExampleProtocol(), { setupTimeoutMs: 10_000 });
}ByteTransport.read(maxBytes) returns between one and maxBytes ordered bytes, or null for clean EOF. The engine makes one read at a time and independently serializes writes. close() must wake both directions. Passing a transport transfers ownership, including when setup fails. Connector.connect(context) supplies repeatable dialing with a deadline and abort signal; it does no discovery itself. LocalConnector and NodeTransport live at @microsandbox/protocol-client/node. Browser-safe WebSocketConnector and WebSocketTransport are root exports.
EnvelopeCodec.encodePayload() supports protocol-specific native encoders. encode() wraps supplied payload bytes; decode() returns an inspectable InboundFrame retaining its raw frame. The standard codec uses { v, t, p }, with p an actual CBOR byte string, strict unsigned integers, and open message names. Raw subscriptions never invoke that codec.
Public access levels
| API | Input and result |
|---|---|
request(typedMessage(name, value)) |
Native payload; first InboundFrame reply |
request(encodedMessage(name, bytes)) |
Pre-encoded application payload; first message reply |
requestTyped(request) |
Optional checked request and result decoder; requires terminal reply |
openStream(message) |
Owned native stream through terminal |
requestRaw(flags, body) / openStreamRaw(flags, body) |
Opaque envelope bodies and raw replies |
sendOnStream(id, message) / sendRaw(id, flags, body) |
Explicit ID owned by this connection |
writeUnchecked(bytes) |
Exact transport bytes without framing or semantic checks |
stream.split() |
Separate owned sender and single receiver |
client.ready / clone() / close() |
Protocol metadata and shared connection lifetime |
Checked helpers are optional; neither native messages nor raw responses are narrowed to a generated message enum. Application errors remain distinct from ClientError. Unknown message names, payload bytes, and envelope fields remain inspectable.
Limits, ownership, and cancellation
Default engine limits are 4 MiB per frame, 1024 in-flight exchanges, 256 queued writes, 1024 queued responses per exchange, and 8 MiB of combined engine-owned packet buffering. Protocol setup can select tighter limits. Counts exclude caller-owned inputs, returned frames, and operating-system or browser socket buffers. Native transports read in paused mode; the WebSocket adapter separately caps its input queue and closes on overflow.
A setup deadline covers dialing and protocol establishment. Optional request deadlines cover writer admission and local response waiting; incomplete-frame deadlines start only when a frame begins. Idle connections have no implicit incomplete-frame deadline. AbortSignal cancels local waiting. delivery: "not_sent" means an attempt never entered the writer; "unknown" means it may have reached the peer. The engine never retries an admitted operation.
Closing a stream only stops local delivery. Nonterminal abandoned exchanges retain their IDs until terminal; stale split senders remain invalid after ID reuse. A receive timeout leaves future frames available. Shared client.close() wakes every handle and closes the transport. Close explicitly in finally: JavaScript finalization is best-effort, while explicit close is deterministic.
Development
From packages, run npm ci, npm run build, npm run typecheck, and npm test. Tests exercise an externally defined protocol, full-width IDs, byte preservation, routing, cancellation and admission, ownership, backpressure, transport truncation, WebSocket bounds, and public README examples. Live runtime and historical SDK compatibility tests are additional release requirements.
To check package exports independently of workspace resolution, install locally packed client packages into a separate consumer directory, then run node protocol-fixtures/check-packed-protocol-consumer.mjs /path/to/consumer from packages. The consumer lockfile must identify the protocol-client tarball and integrity, and the installed package must not be a workspace symlink. This compiles and executes a custom connector, non-CBOR envelope codec, native/encoded/raw/checked calls, full-width IDs, split streams, explicit sends, and exact packet writes against the installed declarations.
The command prints a retained output directory. With the packed agent and control packages also installed in that consumer, pass that output directory to node protocol-fixtures/check-browser-package-roots.mjs /path/to/consumer/protocol-consumer-output. This bundles all three root exports for the browser, rejects Node builtin imports, and executes the same protocol consumer without Node globals. It verifies browser package loading; actual browser/WebSocket integration remains a separate check.
Protocols choose whether completed correlation IDs may be reused. Agent connections retire IDs for the lifetime of the connection, matching the relay; control connections may reuse an ID after its terminal response. Exhaustion is a local error and does not reconnect or replay a request.
Overview (`sdk/go/examples/README.md`)
Go Examples
Run from sdk/go:
go run ./examples/basicExamples
| Example | Description |
|---|---|
basic |
Create a sandbox, run commands, use filesystem and metrics |
cloud-backend |
Cloud backend lifecycle and live logs |
detached |
Detached lifecycle, reattach, stop, and remove |
disk |
Build and mount a raw ext4 disk image |
errors |
Typed error handling with IsKind and errors.As |
filesystem |
Filesystem read/write/list/stat/copy/streaming operations |
image-cache |
List, get, inspect, and garbage-collect cached OCI images |
lifecycle-convergence |
Live convergence and stale-handle identity-safety checks with timing metrics |
metrics |
Point-in-time and streaming metrics |
network |
Presets, DNS, TLS, and custom network settings |
patches |
Pre-boot rootfs patches |
ports |
Publish guest TCP ports on host ports |
secrets |
Secret placeholder injection |
snapshot-fork |
Create a stopped-sandbox snapshot and restore a fresh sandbox from it |
streaming |
Streaming exec, signals, and cancellation |
tls |
TLS interception configuration |
volumes |
Named volume lifecycle |
The snapshot-fork example keeps its historical directory name; it demonstrates restoring a saved disk snapshot. Live forking uses the Fork API.
Api Reference Overview (`docs/api-reference/overview.mdx`)
title: Overview "og:title": Microsandbox cloud REST API reference keywords: ["cloud API", "REST API", "API keys", "personal access tokens", "endpoint reference"] icon: "book-open"
The microsandbox cloud REST API manages sandboxes and the accounts around them. Use it directly when an SDK does not cover your workflow, or when you need account, billing, or audit endpoints.
For running code in sandboxes (exec, streaming, PTY, file transfer, SSH), use the SDKs or CLI. They speak the sandbox command channel, which is not part of this REST surface.
Credentials
The sidebar separates endpoints by who the request represents:
| Credential | Format | Represents | Use for |
|---|---|---|---|
| API key | msb_... |
An organization | Services, SDKs, CLI automation, and organization resources |
| Personal token | msb_pat_... |
A user | Account and organization administration |
API keys and personal tokens are both sent as bearer tokens:
API keys are created in the dashboard. API-key paths infer the organization from the key. Personal-token paths include the organization slug when the request needs an organization.
Base URL
https://api.microsandbox.devErrors
Errors return an error object with a stable machine-readable code, a human-readable message, and optional structured details:
Pagination
List endpoints use cursor pagination. Pass limit to size the page and cursor to continue from a previous response:
Request the next page with ?cursor=<next_cursor>; has_more: false means you have everything.
Usage and billing
The usage endpoints report metered consumption for the current period. Reported cost figures are estimates; the dashboard is the source of truth for billing.
Cli Overview (`docs/cli/overview.mdx`)
title: CLI overview "og:title": Microsandbox CLI installation and command reference sidebarTitle: "Overview" icon: "book"
Interactive sessions are detected automatically; no -it flags are needed.
Set an API key to use microsandbox cloud. Each reference page marks local-only commands and cloud limitations.
Install
brew install superradcompany/tap/microsandboxirm https://install.microsandbox.dev/windows | iexOn Linux and macOS, the install script writes the runtime to ~/.microsandbox/ and creates command links in ~/.local/bin/. It does not edit shell startup files. If ~/.local/bin is not on your PATH, the installer prints the command to add it. On macOS, Homebrew provides the same CLI through the official tap. On Windows, use PowerShell. Windows support is currently in preview.
Check your local setup with:
Quick start
msb run without --name creates an ephemeral sandbox, removed when its command finishes. Named sandboxes persist. Names must contain 1–128 UTF-8 bytes.
Command forms
Use short commands for everyday work. These forms are equivalent:
Aliases such as ls, rm, cp, and mod also work inside sandbox and sbx.
Saved-state operations are grouped under msb snap. The equivalent long form, msb snapshot, is also supported; this documentation uses msb snap consistently:
Omit --full to save disk state for a fresh boot. The group defaults to the source sandbox's name, so the first command creates devbox:ready. Full captures, live forks, groups, and archive operations are local-only. Use msb fork devbox --names worker-a worker-b to create children directly from the same live moment. See Snapshots for backend limits and resource bindings. Existing top-level restore and snapshot save/load commands remain supported.
Quick reference
| Commands | Purpose |
|---|---|
run, create, start, stop, restart |
Manage sandbox lifecycles |
exec, copy, logs |
Run commands and access files or output |
ls, ps, inspect, metrics |
Inspect sandboxes |
modify, pause, resume, fork, rm |
Change, fork, or remove sandboxes |
snap |
Save and restore state |
ssh |
SSH sessions, keys, and listeners |
volume, volumes |
Manage named storage |
df, prune |
Inspect local storage and reclaim unused runtime RAM |
pull, load, save, images, rmi, image |
Manage cached images |
registry, registries |
Manage registry credentials |
context |
Show the active backend |
install, uninstall |
Turn sandboxes into shell commands |
doctor, update, downgrade, self |
Manage the CLI installation |
completion |
Generate shell completions |
Global options
| Flag | Description |
|---|---|
--tree |
Display the complete command tree with descriptions |
-L, --levels <N> |
Limit tree depth, counting the selected root as level 0 (requires --tree) |
-C, --commands |
Hide flags and arguments in the command tree (requires --tree) |
-b, --brief |
Omit descriptions from the command tree (requires --tree) |
--error |
Show only errors |
--warn |
Show warnings and errors |
--info |
Show info, warnings, and errors |
--debug |
Show debug output |
--trace |
Show all output including trace |
Output formatting
Command results normally go to stdout; progress and diagnostics go to stderr. Use JSON output when a command supports it for scripting.
Progress, styled action confirmations, warnings, and errors go to stderr. Tables, inspection results, and JSON go to stdout. Empty-list messages retain their command-specific behavior: the registry empty-list message goes to stdout, while listing installed aliases produces no output when none exist. The plain snapshot reindex completion message also remains on stdout.
Indented output uses two spaces, with nested content at four spaces. Human-readable image, snapshot, and volume sizes use binary units (KiB, MiB, GiB, TiB); JSON retains its documented numeric fields.
Shell completion
msb completion <shell> prints a completion script for bash, zsh, fish, elvish, or powershell. Write it to the location your shell loads completions from:
mkdir -p ~/.zsh/completions
msb completion zsh > ~/.zsh/completions/_msb
# Add this line to ~/.zshrc before compinit runs:
# fpath=(~/.zsh/completions $fpath)mkdir -p ~/.config/fish/completions
msb completion fish > ~/.config/fish/completions/msb.fishmkdir -p ~/.config/elvish/completions
msb completion elvish > ~/.config/elvish/completions/msb.elv
# Add this line to ~/.config/elvish/rc.elv:
# eval (slurp < ~/.config/elvish/completions/msb.elv)$completions = Join-Path (Split-Path $PROFILE) "completions"
New-Item -Path $completions -ItemType Directory -Force | Out-Null
msb completion powershell > (Join-Path $completions "msb.ps1")
# Add this line to your profile ($PROFILE):
# . "$PSScriptRoot/completions/msb.ps1"Restart your shell. Regenerate saved completions after upgrading.
Command tree
Use --tree as an alternative to --help to see every command, subcommand, and flag at once:
You can scope it to a specific subcommand:
Limit the depth or hide flags and arguments for a compact command overview:
-L N (or --levels N) counts the selected root as level 0, so -L 1 shows its immediate children and -L 0 shows only the root. Omitting the limit shows all levels. At the depth limit, an inline marker such as … 4 subcommands, 2 arguments counts the immediate children that were collapsed; arguments include both flags and positional arguments. Hidden commands and options are excluded, and --commands excludes arguments from both the tree and these counts.
--brief omits command and argument descriptions and their alignment padding, while retaining names, aliases, value placeholders, and collapse counts. It works with both -L and --commands.
Cloud Overview (`docs/cloud/overview.mdx`)
title: Cloud keywords: ["microsandbox cloud", "hosted sandboxes", "cloud API keys"] icon: "cloud"
microsandbox cloud runs your sandboxes on hosted infrastructure. The SDKs and the msb CLI are the same ones you use locally: the same builders, the same commands, the same code. Select the cloud backend and provide an API key; there is no daemon or separate client.
Connect
msb context confirms that subsequent commands will run on cloud and shows what selected it without exposing the API key. For profiles, SDK selection, and precedence, see Local or cloud.
Sandbox identity
Looking up a sandbox by name selects the sandbox currently using that name.
The returned object retains that sandbox's ID for lifecycle operations, commands,
and file access. If the sandbox is deleted and its name reused, operations on
an old object fail instead of reaching the replacement. Fetch the sandbox by
name again to select the replacement; you do not need to manage IDs yourself.
Same code, different backend
Everything from the quickstart works unchanged. With cloud selected and the API key exported, this creates a sandbox on cloud instead of your machine:
await using sb = await Sandbox.builder("hello")
.image("python")
.memory(512)
.create();
const output = await sb.exec("python", ["-c", "print('Hello from the cloud!')"]);
console.log(output.stdout());
```rust Rust
use microsandbox::Sandbox;
let sb = Sandbox::builder("hello")
.image("python")
.memory(512)
.create()
.await?;
let output = sb.exec("python", ["-c", "print('Hello from the cloud!')"]).await?;
println!("{}", output.stdout()?);
sb.stop().await?;from microsandbox import Sandbox
sb = await Sandbox.create("hello", image="python", memory=512)
output = await sb.exec("python", ["-c", "print('Hello from the cloud!')"])
print(output.stdout_text)
await sb.stop()sb, err := m.CreateSandbox(ctx, "hello",
m.WithImage("python"),
m.WithMemory(512),
)
if err != nil {
return err
}
defer sb.Stop(ctx)
output, err := sb.Exec(ctx, "python", []string{"-c", "print('Hello from the cloud!')"})
fmt.Println(output.Stdout())require "microsandbox"
Microsandbox::Sandbox.with("hello", image: "python", memory: 512) do |sb|
output = sb.exec("python", ["-c", "print('Hello from the cloud!')"])
puts output.stdout
endmsb run python -- python3 -c "print('Hello from the cloud!')"Hardware virtualization support on your machine is not needed for cloud sandboxes; msb doctor checks apply to the local runtime only.
What works on cloud
Compatibility
The everyday workflow is shared across local and cloud: sandbox lifecycle, command execution, common guest-filesystem operations, network policy, secrets, SSH sessions, and managed storage. The differences below are the ones to plan for.
Across the docs, no badge means a feature works everywhere. Exceptions use only three badges: Local-only, Limited on cloud, and Cloud-only.
| Area | Cloud status | Difference or alternative |
|---|---|---|
| Sandbox lifecycle and execution | Works everywhere | Create, start, inspect, list, stop, remove, exec, shell, streaming, and interactive PTY workflows use the same SDK and CLI surface. |
| Guest filesystem | Limited on cloud | Common path operations and host-to-guest copies work. Low-level open-handle operations are local-only. |
| Managed volumes | Limited on cloud | Default and named directory volumes support lifecycle and filesystem access. Create a named volume before mounting it; disk-kind volumes and create-on-mount are local-only. |
| Volume and host paths | Limited on cloud | Bind and disk-image source paths resolve against the organization's host volume, not the machine running the client. |
| Network policy and secrets | Works everywhere | Egress policies and hostname-scoped secret substitution carry across backends. |
| Custom networking | Local-only | Published ports, custom nameservers, interface overrides, rate limiters, TLS interception, and host-CA trust are not accepted by cloud create. |
| Logs | Limited on cloud | SDK live-follow and msb logs -f work. Bounded or historical reads are local-only; persist followed output externally when retention matters. |
| SSH | Limited on cloud | Native SDK sessions and msb ssh work. msb ssh serve is local-only; reusable SDK servers require explicit key material. |
| Images | Limited on cloud | OCI image references and registry credentials work. Local cache commands, host-directory or disk-image roots, plain-HTTP registries, and custom registry CAs are local-only. |
| Reconfiguration | Local-only | Recreate the sandbox with the new CPU, memory, mounts, or other settings instead of using modify. |
| Snapshots | Limited on cloud | Capture disk snapshots from stopped persistent sandboxes and restore them into new sandboxes. Live capture, full checkpoints, forking, groups, and local artifact archives remain local-only. |
| Resource metrics | Local-only | Instrument the workload with an external monitoring system. |
| Process controls | Limited on cloud | Graceful stop plus maximum-duration and idle-timeout policies work. Force kill, drain, ping, and touch are local-only. |
REST API
The same key also authenticates the REST API directly, so you can manage sandboxes, volumes, and usage over plain HTTPS without an SDK. Command execution, file transfer, and SSH ride the sandbox command channel, which the SDKs and CLI handle for you.
What to know
- Local or cloud. Cloud is explicit: set
MSB_BACKEND=cloud, select a cloud profile, or choose it in code. See Local or cloud. - Networking. Cloud sandboxes get a platform-assigned hostname. SSH works through the same
msb sshand SDK SSH clients you use locally. - Images. Specify an OCI image when you create the sandbox; the cloud pulls it for you.
- Managed settings. Device policy applies to create requests and CLI/SDK SSH sessions. Unsupported create settings fail before the request is sent. Existing cloud sandboxes and the hosted SSH gateway keep their settings.
- Keys and billing. API keys, usage, and invoices live in the dashboard.
Next steps
Getting Started Quickstart (`docs/getting-started/quickstart.mdx`)
title: Quickstart icon: "bolt"
Run your first sandbox
curl -fsSL https://install.microsandbox.dev | sh
msb run debianirm https://install.microsandbox.dev/windows | iex
msb run debianHaving trouble with a local installation? Choose your platform for setup checks and fixes.
**Linux**glibc 2.28+ and KVM**macOS**Apple Silicon**Windows**10+ · WHP<CodeGroup>
```bash TypeScript
npm install microsandbox
```
```bash Rust
cargo add microsandbox
```
```bash Python
pip install microsandbox
```
```bash Go
go get github.com/superradcompany/microsandbox/sdk/go
```
```bash Ruby
gem install microsandbox
```
</CodeGroup>
You can also provision the runtime with the CLI installer above or explicit SDK setup helpers. See [Runtime setup](/sdk/setup#bring-your-own-runtime) for Node's bundled binaries and Rust's opt-out options.
Check local virtualization support with:
<CodeGroup>
```bash Shell
msb doctor
```
</CodeGroup>
For platform-specific setup notes, see [Linux troubleshooting](/troubleshooting/linux), [macOS troubleshooting](/troubleshooting/macos), or [Windows troubleshooting](/troubleshooting/windows).<CodeGroup>
```typescript TypeScript
import { Sandbox } from "microsandbox";
await using sb = await Sandbox.builder("hello")
.image("python")
.memory(512)
.create();
const output = await sb.exec("python", ["-c", "print('Hello from a microVM!')"]);
console.log(output.stdout()); // Hello from a microVM!
```
```rust Rust
use std::error::Error;
use microsandbox::Sandbox;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let sb = Sandbox::builder("hello")
.image("python")
.memory(512)
.create()
.await?;
let output = sb.exec("python", ["-c", "print('Hello from a microVM!')"]).await?;
println!("{}", output.stdout()?); // Hello from a microVM!
sb.stop().await?;
Ok(())
}
```
```python Python
import asyncio
from microsandbox import Sandbox
async def main():
sb = await Sandbox.create(
"hello",
image="python",
memory=512,
)
output = await sb.exec("python", ["-c", "print('Hello from a microVM!')"])
print(output.stdout_text) # Hello from a microVM!
await sb.stop()
asyncio.run(main())
```
```go Go
sb, err := m.CreateSandbox(ctx, "hello",
m.WithImage("python"),
m.WithMemory(512),
)
if err != nil {
return err
}
defer sb.Stop(ctx)
output, err := sb.Exec(ctx, "python", []string{"-c", "print('Hello from a microVM!')"})
if err != nil {
return err
}
fmt.Println(output.Stdout()) // Hello from a microVM!
```
```ruby Ruby
require "microsandbox"
Microsandbox.install unless Microsandbox.installed?
Microsandbox::Sandbox.with("hello", image: "python", memory: 512) do |sandbox|
output = sandbox.exec("python", ["-c", "print('Hello from a microVM!')"])
puts output.stdout
end
```
```bash CLI
msb run python -- python3 -c "print('Hello from a microVM!')"
```
</CodeGroup>What just happened?
Here's what happened behind that Sandbox.builder(...).create() call:
- Pulled the image from Docker Hub, unless it was already cached.
- Assembled a copy-on-write filesystem so changes inside the sandbox do not modify the base image.
- Booted a microVM as a child process with the resource limits you configured.
- Started the guest agent so the SDK can run commands and move data in and out.
The exec call uses the host-guest command channel, not SSH and not the sandbox network.
Next steps
- SDK references: choose TypeScript, Rust, Python, Go, or Ruby for sandbox lifecycle and API details
- Protect API credentials: let sandboxed code use an API without receiving the credential value
- Browse guides: set up agents, CI/CD, Docker, and more
- Run on microsandbox cloud: the same code on hosted infrastructure
- Modify live sandbox settings: resize headroom, labels, secrets, and storage
- Troubleshoot Linux setup: KVM,
/dev/kvm, and permissions - Troubleshoot macOS setup: Apple Silicon and local runtime checks
- Troubleshoot Windows setup: WHP,
msb doctor, and runtime setup - Run commands and stream output:
exec,shell,attach, and streaming - Control network access: policies, DNS interception, and secret protection
- Manage sandboxes with the CLI: create, inspect, and manage sandboxes from the terminal
How To Overview (`docs/how-to/overview.mdx`)
title: How-to guides sidebarTitle: Overview icon: "book-open"
Agents
<li>[Run Claude Code](/how-to/agents/claude-code)</li>
<li>[Run Codex CLI](/how-to/agents/codex)</li>
<li>[Run OpenCode](/how-to/agents/opencode)</li>
<li>[Set up OpenClaw](/how-to/agents/openclaw)</li>
<li>[Run Goose](/how-to/agents/goose)</li>
<li>[Run Gemini CLI](/how-to/agents/gemini-cli)</li>
<li>[Set up Hermes Agent](/how-to/agents/hermes-agent)</li>
<li>[Run Pi](/how-to/agents/pi)</li>Development
<li>[Connect VS Code](/how-to/development/vscode)</li>
<li>[Connect JetBrains via ACP](/how-to/development/jetbrains-acp)</li>
<li>[Run code-server](/how-to/development/code-server)</li>
<li>[Run JupyterLab](/how-to/development/jupyterlab)</li>
<li>[Open a VNC desktop](/how-to/development/vnc-desktop)</li>Browser automation
<li>[Run a browser agent](/how-to/browser-automation/browser-use)</li>
<li>[Automate a browser](/how-to/browser-automation/playwright)</li>Workloads
<li>[Transcode media with FFmpeg](/how-to/file-processing/ffmpeg)</li>
<li>[Convert documents to PDF](/how-to/file-processing/libreoffice-pdf)</li>
<li>[Vet a Terraform provider offline](/how-to/plugins/terraform)</li>
<li>[Rehearse a database migration](/how-to/data/migration-rehearsal)</li>
<li>[Crawl a site with Scrapy](/how-to/web-automation/scrapy)</li>
<li>[Process a batch in parallel](/how-to/automation/parallel-batch-jobs)</li>Sandbox setup
<li>[Run Docker in a sandbox](/how-to/docker/docker-in-sandbox)</li>
<li>[Use local images](/how-to/docker/local-images)</li>
<li>[Boot with systemd](/how-to/guest/systemd-services)</li>
<li>[Start warm workers from snapshots](/how-to/sandboxing/warm-workers)</li>Deployment
<li>[Run microsandbox in Docker](/how-to/docker/docker)</li>
<li>[Run microsandbox in Kubernetes](/how-to/sandboxing/kubernetes)</li>Storage
<li>[Run PostgreSQL](/how-to/data/postgresql)</li>
<li>[Run Redis](/how-to/data/redis)</li>
<li>[Mount S3 with JuiceFS](/how-to/data/juicefs-s3)</li>CI/CD
<li>[Run a GitHub Actions job](/how-to/ci-cd/github-actions-runner)</li>
<li>[Run isolated PR checks](/how-to/ci-cd/pr-checks)</li>
<li>[Run Docker Compose tests](/how-to/ci-cd/compose-tests)</li>
<li>[Serve a preview deployment](/how-to/ci-cd/preview-deploys)</li>
<li>[Schedule dependency audits](/how-to/automation/dependency-audits)</li>Observability
<li>[Send metrics to Grafana Cloud](/how-to/metrics-backends/grafana-cloud)</li>
<li>[Forward metrics with Grafana Alloy](/how-to/metrics-backends/grafana-alloy)</li>
<li>[Inspect metrics with OpenTelemetry](/how-to/metrics-backends/otel-collector)</li>
<li>[Send metrics to Prometheus](/how-to/metrics-backends/prometheus)</li>
<li>[Send metrics to Datadog](/how-to/metrics-backends/datadog)</li>Images Overview (`docs/images/overview.mdx`)
title: Images icon: "layer-group"
OCI images
microsandbox uses standard OCI container images as root filesystems. Docker Hub, GHCR, ECR, GCR, any OCI-compatible registry works. Existing images run as-is.
When you specify an image like python, microsandbox pulls the manifest, downloads the layers in parallel, and stacks them as a copy-on-write filesystem. Changes inside the sandbox don't modify the base image. Two sandboxes using the same image share the same cached layers on disk.
Pull policies
By default, microsandbox pulls an image only if it isn't already cached. You can change this behavior.
| Policy | Behavior |
|---|---|
"if-missing" |
Pull only if not cached (default) |
"always" |
Always check the registry for updates |
"never" |
Use local cache only, fail if missing |
await using sb = await Sandbox.builder("worker")
.image("python")
.pullPolicy("always")
.create();
```rust Rust
let sb = Sandbox::builder("worker")
.image("python")
.pull_policy(PullPolicy::Always)
.create()
.await?;from microsandbox import PullPolicy, Sandbox
sb = await Sandbox.create("worker", image="python", pull_policy=PullPolicy.ALWAYS)sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("python"),
m.WithPullPolicy(m.PullPolicyAlways),
)sb = Microsandbox::Sandbox.create("worker", image: "python", pull_policy: "always")Private registries
Authenticate to private registries by passing credentials.
let sb = Sandbox::builder("worker")
.image("registry.corp.io/team/app:latest")
.registry(|r| r.auth(RegistryAuth::Basic {
username: "deploy".into(),
password: std::env::var("REGISTRY_PASSWORD")?,
}))
.pull_policy(PullPolicy::Always)
.create()
.await?;import os
from microsandbox import PullPolicy, RegistryAuth, Sandbox
sb = await Sandbox.create(
"worker",
image="registry.corp.io/team/app:latest",
registry_auth=RegistryAuth.basic("deploy", os.environ["REGISTRY_PASSWORD"]),
pull_policy=PullPolicy.ALWAYS,
)sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("registry.corp.io/team/app:latest"),
m.WithRegistryAuth(m.RegistryAuth{
Username: "deploy",
Password: os.Getenv("REGISTRY_PASSWORD"),
}),
m.WithPullPolicy(m.PullPolicyAlways),
)Registry TLS
By default, microsandbox connects to registries over HTTPS using system CA roots. You can customize this per-registry in ~/.microsandbox/config.json.
Plain HTTP registries
Local registries often run without TLS. Mark them as insecure to connect over plain HTTP:
Custom CA certificates
For registries using self-signed or internal CA certificates, point ca_certs to a PEM file. This applies globally to all registry connections:
microsandbox adds these certificates to the default system roots, so public registries continue to work normally. You can also supply the same roots per-sandbox through the SDK: caCerts in TypeScript, ca_certs in Rust, registry_ca_certs in Python, and WithRegistryCACerts / WithRegistryCACertsPath in Go.
Combined configuration
Per-sandbox SDK options override user registry settings. Managed registry settings take precedence over both. Managed insecure: false requires TLS without changing credentials; managed auth: null forces anonymous access, and managed CA settings replace per-call custom certificates.
let sb = Sandbox::builder("worker")
.image("localhost:5050/my-app:latest")
.registry(|r| r.insecure())
.create()
.await?;from microsandbox import Sandbox
sb = await Sandbox.create(
"worker",
image="localhost:5050/my-app:latest",
registry_insecure=True,
)sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("localhost:5050/my-app:latest"),
m.WithRegistryInsecure(),
)Image storage
Images are cached in the global microsandbox home directory:
| Path | Description |
|---|---|
~/.microsandbox/cache/layers/ |
Materialized EROFS image layers |
~/.microsandbox/cache/fsmeta/ |
Merged metadata images for cached manifests |
~/.microsandbox/cache/vmdk/ |
VMDK descriptors used to boot cached images |
~/.microsandbox/db/ |
Database tracking image metadata and digests |
Layers are content-addressable and deduplicated. If python:3.12 and python:3.11 share a base layer, it's stored once.
Use msb image prune to remove cached images that are not used by any sandbox or indexed snapshot and reclaim dangling image artifacts. Prune keeps images referenced by existing sandboxes or snapshots. It cleans up image metadata, unreachable manifests, orphaned layers, layer EROFS artifacts, fsmeta EROFS artifacts, and VMDK descriptor artifacts.
msb load accepts Docker image archives and OCI Image Layout archives. By default, msb save exports from the materialized EROFS cache as a Docker archive. Use --format oci to export as an OCI Image Layout archive instead. The expanded forms are msb image load and msb image save. The exported image should run the same way after msb load, but it is a regenerated archive: manifest digest and layer digests can differ from the original registry image.
Disk images
In addition to OCI container images, microsandbox can boot a sandbox directly from a disk image file. The guest gets raw block device access, and its kernel mounts the filesystem from the device directly.
This is a fundamentally different path from OCI images. With OCI, microsandbox stacks image layers as a copy-on-write filesystem. With disk images, the guest owns the block device. No overlay, no copy-on-write between sandboxes.
Use disk images when you need a pre-built VM template, a custom kernel configuration, or an OS that isn't available as a container image.
Supported formats
| Format | Description |
|---|---|
| QCOW2 | QEMU Copy-On-Write v2. Supports thin provisioning and backing files. The most common format. |
| Raw | Uncompressed raw disk image. No overhead, but no thin provisioning. |
| VMDK | VMware virtual disk format. |
Usage
When you pass a file path ending in .qcow2, .raw, or .vmdk, microsandbox auto-detects the format. For disk-image roots, rename ambiguous files with one of those extensions and set the filesystem type when auto-detection needs a hint.
// Auto-detect format from the file extension
await using sb = await Sandbox.builder("custom-vm")
.image("./ubuntu-22.04.qcow2")
.cpus(2)
.memory(2048)
.create();
// Explicit filesystem type via the ImageBuilder
await using sb2 = await Sandbox.builder("custom-vm")
.imageWith((i) => i.disk("./alpine.raw").fstype("ext4"))
.cpus(1)
.memory(512)
.create();
```rust Rust
use microsandbox::size::SizeExt;
// Auto-detect
let sb = Sandbox::builder("custom-vm")
.image("./ubuntu-22.04.qcow2")
.cpus(2)
.memory(2.gib())
.create()
.await?;
// Explicit filesystem type
let sb2 = Sandbox::builder("custom-vm")
.image_with(|i| i.disk("./alpine.raw").fstype("ext4"))
.cpus(1)
.memory(512)
.create()
.await?;from microsandbox import Image, Sandbox
# Auto-detect format from file extension
sb = await Sandbox.create("custom-vm", image="./ubuntu-22.04.qcow2", cpus=2, memory=2048)
# Explicit filesystem type
sb2 = await Sandbox.create("custom-vm", image=Image.disk("./alpine.raw", fstype="ext4"), cpus=1, memory=512)// Auto-detect format from the file extension
sb, err := m.CreateSandbox(ctx, "custom-vm",
m.WithImage("./ubuntu-22.04.qcow2"),
m.WithCPUs(2),
m.WithMemory(2048),
)
// Explicit filesystem type
sb2, err := m.CreateSandbox(ctx, "custom-vm-2",
m.WithImageDisk("./alpine.raw", "ext4"),
m.WithCPUs(1),
m.WithMemory(512),
)# Auto-detect the format from the file extension.
sb = Microsandbox::Sandbox.create(
"custom-vm", image: "./ubuntu-22.04.qcow2", cpus: 2, memory: 2048
)
# Ruby accepts image paths but does not expose a filesystem-type override.Reference
For exact image APIs, see TypeScript, Rust, Python, Go, or Ruby. For local image management, see Image commands.
Networking Overview (`docs/networking/overview.mdx`)
title: Overview keywords: ["sandbox networking", "network policy", "port forwarding", "egress"] icon: "network-wired"
Microsandbox checks sandbox traffic against a host-controlled network policy. Start with the defaults, then allow only the destinations and ports your workload needs.
Defaults
Sandboxes can reach the public internet by default. Private networks, the host, loopback, link-local addresses, and cloud metadata endpoints are blocked. Inbound traffic needs a published port.
To isolate a sandbox from the network:
await using sb = await Sandbox.builder("isolated")
.image("python")
.disableNetwork()
.create();
```rust Rust
use microsandbox::Sandbox;
let sb = Sandbox::builder("isolated")
.image("python")
.network(|n| n.enabled(false))
.create()
.await?;from microsandbox import Network, Sandbox
sb = await Sandbox.create("isolated", image="python", network=Network.none())sb, err := m.CreateSandbox(ctx, "isolated",
m.WithImage("python"),
m.WithNetwork(m.NetworkPolicy.None()),
)sb = Microsandbox::Sandbox.create("isolated", image: "python", network: :none)msb create python --name isolated --no-netThe TypeScript and Rust examples disable the network device. Python Network.none(), Go NetworkPolicy.None(), and CLI --no-net retain the device but deny traffic in both directions through policy.
Ruby exposes network disablement, host/port allowlists, proxies, and basic TLS interception. Deployment profiles, policy profiles, NAT64, port publishing, and rate limits require TypeScript, Rust, Python, Go, or the CLI; see the Ruby networking reference for its available options.
Deployment profiles
Local sandboxes use single-tenant by default. Use multi-tenant for shared hosts: it enforces public-network access, DNS rebinding protection, and stricter limits. Sandbox policies can restrict access further. Cloud chooses its own profile.
await using sb = await Sandbox.builder("shared-worker")
.image("python")
.deploymentProfile("multi-tenant")
.create();
```rust Rust
use microsandbox::sandbox::DeploymentProfile;
use microsandbox::Sandbox;
let sb = Sandbox::builder("shared-worker")
.image("python")
.deployment_profile(DeploymentProfile::MultiTenant)
.create()
.await?;from microsandbox import DeploymentProfile, Sandbox
sb = await Sandbox.create(
"shared-worker",
image="python",
deployment_profile=DeploymentProfile.MULTI_TENANT,
)sb, err := m.CreateSandbox(ctx, "shared-worker",
m.WithImage("python"),
m.WithDeploymentProfile(m.DeploymentProfileMultiTenant),
)msb create python --name shared-worker --deployment-profile multi-tenantIn multi-tenant mode, traffic must pass both the platform's public-network policy and the sandbox's policy. Custom DNS servers and interface overrides are removed; host CA import and published ports are disabled. The sandbox cannot relax these restrictions.
An operator can set deployment_profile in ~/.microsandbox/config.json or on LocalBackend. This overrides the sandbox's choice on create and restart. Managed cloud requests do not carry a deployment profile; the hosting driver selects it.
The per-sandbox maxTcpConnections setting optionally limits concurrent guest-initiated TCP connections. maxConnections remains a deprecated TCP-only alias. When omitted, single-tenant has no connection-count cap and multi-tenant defaults to 1,024. An explicit positive value overrides either default; zero explicitly selects unlimited. These are runtime defaults, not operator-enforced ceilings. Services accepting tenant configuration must enforce their own policy on overrides.
Legacy v0.6.x launch contracts keep their historical default of 256 TCP connections when the limit is omitted. The current SDK rejects explicit unlimited TCP connections, values above 4,096, or multi-tenant values above 256 when launching an older runtime because those peers cannot preserve the requested meaning. A current runtime also rejects legacy zero TCP limits: old zero meant deny all connections, not unlimited. Current-contract defaults and explicit limits are unchanged.
maxUdpConnections independently limits host-side UDP relay sessions. Omission is unlimited in single-tenant mode and defaults to 1,024 in multi-tenant mode; zero explicitly selects unlimited. At a finite limit, a new session evicts the least recently active session. Sessions expire after 60 seconds of inactivity. Historical runtime launch contracts do not support an explicit UDP limit, including zero; the SDK rejects that override with an upgrade-required error instead of silently ignoring it.
maxInboundTcpConnections limits active inbound TCP connections across published ports; 0 means unlimited. Omission uses the runtime default: unlimited in 0.7.9+, or 256 in older versions. Explicit overrides require runtime support or fail with an upgrade-required error. This local-only limit is separate from the listener queue (tcpAcceptQueueSize).
On 0.7.9+, finished connections leave the active budget after response delivery. The runtime retains up to 256 finished sockets in TIME-WAIT, evicting the oldest under pressure and shortening protection against delayed packets. Host writes stalled for 30 seconds are closed; successful writes restart the timer.
Each TCP socket allocates 128 KiB of buffers. An outbound cap of 1,024 allows up to 128 MiB; an inbound cap of 256 plus 256 retained sockets allows up to 64 MiB. The retention pool can use 32 MiB even with a small active cap. These figures exclude relay channels, host socket buffers, and other allocations. Unlimited connections still face memory, file-descriptor, and source-port limits.
High-level profiles
Combine public, private, and host to allow those destination groups. Use none to deny all traffic or all to allow all traffic. This example allows public and private networks:
await using sb = await Sandbox.builder("worker")
.image("python")
.network((n) => n.policy(NetworkPolicy.fromProfiles(["public", "private"])))
.create();
```rust Rust
use microsandbox::{NetworkPolicy, NetworkProfile, Sandbox};
let policy = NetworkPolicy::from_profiles([NetworkProfile::Public, NetworkProfile::Private]);
let sb = Sandbox::builder("worker")
.image("python")
.network(|n| n.policy(policy))
.create()
.await?;from microsandbox import Network, NetworkProfile, Sandbox
sb = await Sandbox.create(
"worker",
image="python",
network=Network.from_profiles(NetworkProfile.PUBLIC, NetworkProfile.PRIVATE),
)sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("python"),
m.WithNetwork(m.NetworkPolicy.FromProfiles(m.NetworkProfilePublic, m.NetworkProfilePrivate)),
)msb create python --name worker --net "public,private"Add host for services on your machine; see Reaching the host. Duplicate profiles are ignored. Explicit rules placed before profile rules take precedence.
Low-level custom policies
For finer control, set default actions and add rules. The first matching rule wins. This example allows public HTTPS and gateway DNS while denying other outbound traffic:
await using sb = await Sandbox.builder("restricted-worker")
.image("alpine")
.network((n) => n.policy(
NetworkPolicy.builder()
.defaultDeny()
.egress((e) => e.tcp().port(443).allowPublic())
.egress((e) => e.udp().tcp().port(53).allowHost())
.build(),
))
.create();
```rust Rust
use microsandbox::{NetworkPolicy, Sandbox};
let policy = NetworkPolicy::builder()
.default_deny()
.egress(|e| e.tcp().port(443).allow_public())
.egress(|e| e.udp().tcp().port(53).allow_host())
.build()?;
let sb = Sandbox::builder("restricted-worker")
.image("alpine")
.network(|n| n.policy(policy))
.create()
.await?;from microsandbox import Action, DestGroup, Destination, Network, NetworkPolicy, Protocol, Rule, Sandbox
sb = await Sandbox.create(
"restricted-worker",
image="alpine",
network=Network(policy=NetworkPolicy(
default_egress=Action.DENY,
rules=(
Rule.allow(destination=Destination.group(DestGroup.PUBLIC), protocol=Protocol.TCP, port=443),
*Rule.allow_dns(),
),
)),
)sb, err := m.CreateSandbox(ctx, "restricted-worker",
m.WithImage("alpine"),
m.WithNetwork(&m.NetworkConfig{
DefaultEgress: m.PolicyActionDeny,
Rules: []m.PolicyRule{
{Action: m.PolicyActionAllow, Direction: m.PolicyDirectionEgress, Destination: "public", Protocol: m.PolicyProtocolTCP, Port: "443"},
m.Rule.AllowDNS(),
},
}),
)msb create alpine --name restricted-worker \
--net-default-egress deny \
--net-rule "allow@public:tcp:443,allow@dns"Rules can match destination groups, IPs, CIDRs, domains, and ports. See the CLI rule syntax or an SDK reference for exact options.
NAT64 prefixes
NAT64 lets IPv6 networks reach IPv4 destinations. Microsandbox checks both addresses against policy, so translated private and metadata addresses stay blocked. The standard 64:ff9b::/96 prefix is included by default.
If your local network routes another /96 prefix, add it like this:
await using sb = await Sandbox.builder("translated")
.image("python")
.network((n) => n.nat64Prefix("64:ff9b:1::/96"))
.create();
```rust Rust
use microsandbox::Sandbox;
let prefix = "64:ff9b:1::/96".parse()?;
let sb = Sandbox::builder("translated")
.image("python")
.network(|n| n.nat64_prefix(prefix))
.create()
.await?;from microsandbox import Network, Sandbox
sb = await Sandbox.create(
"translated",
image="python",
network=Network(nat64_prefixes=("64:ff9b::/96", "64:ff9b:1::/96")),
)sb, err := m.CreateSandbox(ctx, "translated",
m.WithImage("python"),
m.WithNetwork(&m.NetworkConfig{
NAT64Prefixes: []string{"64:ff9b::/96", "64:ff9b:1::/96"},
}),
)msb create python --name translated --net-nat64-prefix 64:ff9b:1::/96This setting classifies traffic; it does not set up NAT64 routing. Cloud rejects custom or empty prefix lists. See CLI options for details.
Hostname rules and HTTPS
Allow specific domains with hostname rules. By default, HTTPS allowed only by a hostname rule requires TLS inspection, so microsandbox can check the hostname inside the request:
const policy = NetworkPolicy.builder()
.defaultDeny()
.egress((e) => e.tcp().port(443).allowDomain("github.com"))
.egress((e) => e.udp().tcp().port(53).allowHost())
.build();
await using sb = await Sandbox.builder("github-only")
.image("python")
.network((n) => n.policy(policy))
.interceptTls()
.create();
```rust Rust
use microsandbox::{NetworkPolicy, Sandbox};
let policy = NetworkPolicy::builder()
.default_deny()
.egress(|e| e.tcp().port(443).allow_domains(["github.com"]))
.egress(|e| e.udp().tcp().port(53).allow_host())
.build()?;
let sb = Sandbox::builder("github-only")
.image("python")
.network(|n| n.policy(policy))
.intercept_tls()
.create()
.await?;from microsandbox import Action, Destination, Network, NetworkPolicy, Protocol, Rule, Sandbox
sb = await Sandbox.create(
"github-only",
image="python",
network=Network(policy=NetworkPolicy(
default_egress=Action.DENY,
rules=(
Rule.allow(destination=Destination.domain("github.com"), protocol=Protocol.TCP, port=443),
*Rule.allow_dns(),
),
)),
intercept_tls=True,
)sb, err := m.CreateSandbox(ctx, "github-only",
m.WithImage("python"),
m.WithNetwork(&m.NetworkConfig{
DefaultEgress: m.PolicyActionDeny,
Rules: []m.PolicyRule{
{Action: m.PolicyActionAllow, Direction: m.PolicyDirectionEgress, Destination: "github.com", Protocol: m.PolicyProtocolTCP, Port: "443"},
m.Rule.AllowDNS(),
},
}),
m.WithInterceptTLS(),
)sb = Microsandbox::Sandbox.create(
"github-only",
image: "python",
network: { allowed_hosts: ["github.com"], allowed_ports: [443] },
intercept_tls: true
)msb create python --name github-only \
--net-default-egress deny \
--net-rule "[email protected]:tcp:443,allow@dns" \
--tls-interceptDomain and domain-suffix rules start with the hostname microsandbox can observe. DNS rules see the query name, and TLS connections usually expose SNI before the encrypted session starts. That is enough to decide whether a connection may be opened, but it is not the same as seeing the HTTP request authority.
For plaintext HTTP, microsandbox can inspect the Host header and require it to match the hostname that policy allowed. For HTTPS without TLS interception, or for a host that is configured to bypass interception, the HTTP Host header and HTTP/2 :authority value are encrypted. In that case microsandbox cannot prove which application hostname the request is using after the TLS session starts.
Strict hostname policy mode is enabled by default: hostname allowlists fail closed unless microsandbox can inspect the request authority. In strict mode, hostname-based allow rules still work for plaintext HTTP and TLS-intercepted HTTPS. Non-intercepted HTTPS is denied when it would otherwise be allowed only by a hostname rule. IP, CIDR, group, and default allow rules are not treated as hostname-based allows. Set --net-strict=false or network.strict: false in sandbox configuration to opt out.
Port mapping
Publish a guest port to your host machine. This example forwards host port 8080 to guest port 80, listening on 127.0.0.1 by default. Port publishing is local-only.
await using sb = await Sandbox.builder("api")
.image("python")
.port(8080, 80)
.create();
```rust Rust
use microsandbox::Sandbox;
let sb = Sandbox::builder("api")
.image("python")
.port(8080, 80)
.create()
.await?;from microsandbox import PortBinding, Sandbox
sb = await Sandbox.create("api", image="python", ports=[PortBinding.tcp(8080, 80)])sb, err := m.CreateSandbox(ctx, "api",
m.WithImage("python"),
m.WithPorts(map[uint16]uint16{8080: 80}),
)msb create python --name api -p 8080:80Use an explicit bind address, such as 0.0.0.0, only when you intentionally want to listen beyond localhost. -p 8080:80 and SDK helpers like .port(8080, 80) bind to 127.0.0.1; -p 127.0.0.1:8080:80 is the same loopback-only shape. -p 0.0.0.0:8080:80 or a specific LAN interface address makes the host listener reachable outside the machine, subject to your OS firewall and network policy.
If the host starts without IPv4 or IPv6 routes, microsandbox assigns private guest addresses for published ports unless guest addresses are already configured. A specific host bind address must still exist at startup.
CLI -p and YAML ports lists also accept inclusive, equal-length port ranges: -p 8000-8002:80-82 forwards three TCP ports, while -p 0.0.0.0:10240-11264:10240-11264/udp publishes 1,025 UDP ports. Ranges expand into the same individual mappings used by the SDKs, with one host socket per port. See Published ports for validation and host resource requirements.
On Windows, the first published port may trigger a Windows Defender Firewall prompt for msb.exe because the runtime opens a host listening socket. For local development, keep the bind address on 127.0.0.1. Only allow private/public network access in the firewall prompt when you intentionally bind a published port beyond loopback.
Rate limits
Limit inbound or outbound bandwidth and packet rate on local sandboxes. Omitted limits leave traffic unthrottled; changes take effect on the next start. Cloud does not support this setting.
This example allows 1 MiB/s outbound, a one-time 512 KiB burst, and 1,000 packets per second:
await using sb = await Sandbox.builder("throttled")
.image("python")
.network((n) => n.rateLimiter((r) => r.egress((r) => r
.bandwidth(1_048_576, 1_000)
.bandwidthBurst(524_288)
.ops(1_000, 1_000))))
.create();
```rust Rust
use std::time::Duration;
use microsandbox::size::SizeExt;
use microsandbox::Sandbox;
let sb = Sandbox::builder("throttled")
.image("python")
.network(|n| n.rate_limiter(|r| r.egress(|r| r
.bandwidth(1.mib(), Duration::from_secs(1))
.bandwidth_burst(512.kib())
.ops(1_000, Duration::from_secs(1)))))
.create()
.await?;from microsandbox import Network, NetworkRateLimiter, RateLimiter, Sandbox, TokenBucket
sb = await Sandbox.create(
"throttled",
image="python",
network=Network(
rate_limiter=NetworkRateLimiter(
egress=RateLimiter(
bandwidth=TokenBucket(size=1_048_576, refill_time_ms=1_000, one_time_burst=524_288),
ops=TokenBucket(size=1_000, refill_time_ms=1_000),
),
),
),
)sb, err := m.CreateSandbox(ctx, "throttled",
m.WithImage("python"),
m.WithNetwork(&m.NetworkConfig{
RateLimiter: &m.NetworkRateLimiterConfig{
Egress: &m.RateLimiterConfig{
Bandwidth: &m.TokenBucketConfig{Size: 1 << 20, RefillTime: time.Second, OneTimeBurst: 512 << 10},
Ops: &m.TokenBucketConfig{Size: 1000, RefillTime: time.Second},
},
},
}),
)msb create python --name throttled \
--net-egress-bandwidth 1M/1s \
--net-egress-bandwidth-burst 512K \
--net-egress-ops 1000/1sNetwork rate limits apply only to traffic crossing a sandbox's virtual network device. They do not limit CPU, memory, disk I/O, API requests, or how many sandboxes can run. If you do not configure a limiter, microsandbox does not throttle the sandbox's network bandwidth or packet rate. Once configured, the limiter is enforced independently for outbound (egress) and inbound (ingress) traffic.
For each direction, you can configure a bandwidth bucket measured in bytes per refill interval, a packet-rate bucket measured in packets per interval, or both. An omitted direction or bucket remains unthrottled. Buckets start full, refill continuously, and can include a one-time startup burst that does not refill. Limits are set at creation and take effect on the next sandbox start.
The --net-ingress-* flags mirror the egress flags for inbound traffic (--net-ingress-bandwidth, --net-ingress-bandwidth-burst, --net-ingress-ops, --net-ingress-ops-burst). Sizes accept raw bytes plus K, M, and G suffixes; the interval defaults to one second when omitted. A frame larger than the bandwidth bucket is delivered once and the limiter then pauses long enough to pay it off, so oversized packets are throttled instead of stuck.
HTTP denial responses
New local sandboxes return JSON 403 Forbidden responses for supported denied requests by default, with code, message, and domain fields. Set deny_response: false to close denied connections without a response. This requires a supporting runtime; cloud behavior is unchanged.
In JSON mode, network_deny_message and secret_deny_message customize the respective messages. Messages are used as-is; the destination is in domain. Changing a message or format does not override an explicit deny_response: false.
Select deny_response_format: text for legacy behavior: network denials return plain text, deny_message substitutes {host}, and secret violations close without a response. The old message setting remains supported but is deprecated for new integrations. Each mode uses only its own message settings. Omitted settings use the runtime defaults: responses enabled with JSON format. Explicit settings take precedence. Cloud accepts the default settings without applying them; enabled custom settings are local-only.
use microsandbox::sandbox::HttpDenyResponseFormat;
let sb = Sandbox::builder("agent")
.image("python")
.network(|n| n
.policy(policy)
.tls(|t| t.enabled(true))
.http(|h| h
.deny_response(true)
.deny_response_format(HttpDenyResponseFormat::Json)
.network_deny_message("Ask the user for network access.")
.secret_deny_message("Ask the user to review secret access.")))
.create()
.await?;from microsandbox import HttpConfig, Network, Sandbox, TlsConfig
sb = await Sandbox.create(
"agent",
image="python",
network=Network(
policy=policy,
tls=TlsConfig(),
http=HttpConfig(
deny_response=True,
deny_response_format="json",
network_deny_message="Ask the user for network access.",
secret_deny_message="Ask the user to review secret access.",
),
),
)sb, err := m.CreateSandbox(ctx, "agent",
m.WithImage("python"),
m.WithNetwork(&m.NetworkConfig{
Rules: rules,
TLS: &m.TLSConfig{},
HTTP: &m.HTTPConfig{
DenyResponse: true,
DenyResponseFormat: m.HTTPDenyResponseJSON,
NetworkDenyMessage: "Ask the user for network access.",
SecretDenyMessage: "Ask the user to review secret access.",
},
}),
)sb = Microsandbox::Sandbox.create(
"agent",
image: "python",
network: { allowed_hosts: ["example.com"], allowed_ports: [80, 443] },
http: {
deny_response: true,
deny_response_format: "json",
network_deny_message: "Ask the user for network access.",
secret_deny_message: "Ask the user to review secret access."
}
)network:
policy: none
allow: [github.com]
http:
deny_response: true
deny_response_format: json
network_deny_message: "Ask the user for network access."
secret_deny_message: "Ask the user to review secret access."Check the response status, or use curl --fail to make a 403 return a nonzero exit code.
Responses work for HTTP/1.0 and HTTP/1.1. HTTPS requires TLS interception on the destination port, with no bypass for the host. Network-policy denials never reach the upstream server.
In JSON mode, secret-policy denials can also return a response for the first request on a connection, before any upstream response bytes have reached the guest. The connection may already be open when a placeholder is detected; forwarding stops at the violation.
Secret-policy violations still close the connection for later or pipelined requests, after an upstream response (including 100 Continue), and for HEAD, CONNECT, block-and-terminate, or unsupported body substitution.
DNS denies still return NXDOMAIN. HTTP/2, other protocols, malformed or incomplete requests, and silent clients close without an HTTP response.
JSON responses use Content-Type: application/json. Match code to identify the error and display message to the user. domain is the requested hostname, an IP address fallback for network denials, or null when unknown.
Network-policy denial:
{
"code": "network_policy_denied",
"message": "Request blocked by network policy.",
"domain": "example.com"
}Secret-policy denial:
{
"code": "secret_policy_denied",
"message": "Request blocked by secret policy.",
"domain": "example.com"
}Both custom messages are JSON-escaped. An omitted message uses the default; an empty string leaves message empty (Go treats an empty string as omitted). Text mode works with older runtimes that support denial responses. JSON mode requires the new runtime capability; the SDK returns an upgrade-required error if it is missing.
Reaching the host
host.microsandbox.internal points to the host machine from inside a local sandbox. The default policy blocks it, so allow the host group when the guest needs a local service:
await using sb = await Sandbox.builder("devbox")
.image("python")
.network((n) => n.policy(NetworkPolicy.fromProfiles(["public", "host"])))
.create();
```rust Rust
use microsandbox::{NetworkPolicy, NetworkProfile, Sandbox};
let policy = NetworkPolicy::from_profiles([NetworkProfile::Public, NetworkProfile::Host]);
let sb = Sandbox::builder("devbox")
.image("python")
.network(|n| n.policy(policy))
.create()
.await?;from microsandbox import Network, NetworkProfile, Sandbox
sb = await Sandbox.create(
"devbox",
image="python",
network=Network.from_profiles(NetworkProfile.PUBLIC, NetworkProfile.HOST),
)sb, err := m.CreateSandbox(ctx, "devbox",
m.WithImage("python"),
m.WithNetwork(m.NetworkPolicy.FromProfiles(m.NetworkProfilePublic, m.NetworkProfileHost)),
)msb create python --name devbox --net "public,host"Guest 127.0.0.1 is inside the sandbox, not the host. On cloud, the host group refers to the cloud worker, not the machine running your SDK.
Reference
For exact APIs, see TypeScript, Rust, Python, Go, or Ruby. For flags and configuration, see Sandbox commands and Sandbox configuration.
Next
- DNS: nameservers and domain blocking
- Outbound proxies: route traffic through a proxy
- TLS inspection: inspect HTTPS traffic
- Host sockets: connect to host IPC without a TCP port
- Network security: understand the policy boundary and defenses
Sandboxes Overview (`docs/sandboxes/overview.mdx`)
title: Overview keywords: ["microVM sandboxes", "sandbox configuration", "sandbox lifecycle"] icon: "circle-info"
A sandbox is a microVM with its own Linux kernel, filesystem, and network stack. Your application or the msb CLI starts it as a child process on your machine or on microsandbox cloud, then talks to the guest agent to run commands, move files, and control lifecycle.
The security boundary is hardware virtualization, not Linux namespaces. That makes sandboxes a good fit for untrusted workloads: user-submitted code, AI agent actions, plugins, dependency installs, CI jobs, scrapers, and tools that should not inherit the host process's full privileges.
Create a sandbox
At minimum, a sandbox needs a name and an image. Everything else has defaults: 1 vCPU, 512 MiB memory, public-only networking, and /bin/sh as the default shell.
let sb = Sandbox::builder("worker")
.image("python")
.create()
.await?;sb = await Sandbox.create("worker", image="python")sb, err := m.CreateSandbox(ctx, "worker", m.WithImage("python"))sb = Microsandbox::Sandbox.create("worker", image: "python")msb create python --name workerSandbox names must be non-empty and no longer than 128 UTF-8 bytes.
Common configuration
let sb = Sandbox::builder("worker")
.image("python")
.memory(1024)
.cpus(2)
.env("DEBUG", "true")
.label("user.id", "alice")
.volume("/tmp/scratch", |v| v.tmpfs().size(100))
.create()
.await?;sb = await Sandbox.create(
"worker",
image="python",
memory=1024,
cpus=2,
env={"DEBUG": "true"},
labels={"user.id": "alice"},
volumes={"/tmp/scratch": Volume.tmpfs(size_mib=100)},
)sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("python"),
m.WithMemory(1024),
m.WithCPUs(2),
m.WithEnv(map[string]string{"DEBUG": "true"}),
m.WithLabels(map[string]string{"user.id": "alice"}),
m.WithMounts(map[string]m.MountConfig{
"/tmp/scratch": m.Mount.Tmpfs(m.TmpfsOptions{SizeMiB: 100}),
}),
)sb = Microsandbox::Sandbox.create(
"worker",
image: "python",
memory: 1024,
cpus: 2,
env: { "DEBUG" => "true" },
labels: { "user.id" => "alice" }
)
# Ruby does not expose the tmpfs mount option shown in the other tabs.msb create python --name worker \
-c 2 \
-m 1G \
-e DEBUG=true \
--label user.id=alice \
--tmpfs /tmp/scratch:100| Option | Default | Description |
|---|---|---|
image |
required | OCI image, local rootfs path, or disk image |
cpus |
1 |
Virtual CPU limit |
max_cpus |
same as cpus |
Boot-time maximum possible virtual CPUs |
memory |
512 |
Guest memory limit in MiB |
max_memory |
same as memory |
Boot-time maximum hotpluggable memory in MiB |
thp |
madvise |
Guest transparent huge-page policy selected at boot |
guest_clock |
sync |
Follow host time (sync) or stop host clock updates after boot (off). Local Rust SDK and CLI only |
workdir |
image default | Default working directory for commands |
shell |
/bin/sh |
Shell used by shell() calls |
env |
empty | Environment variables |
labels |
empty | Key/value metadata for metric attribution |
volumes |
empty | Bind, named, tmpfs, or disk-image mounts |
network |
public-only | Network policy and published ports |
scripts |
empty | Named scripts mounted at /.msb/scripts/ |
max_cpus and max_memory reserve live resize headroom. They default to the
starting cpus and memory values, so set them higher when a sandbox may need
to grow later. See Live Modify for the full change
model.
Labels are arbitrary key=value metadata attached to a sandbox. They can be set
at create time or changed while the sandbox is running, and they also select
sandboxes in bulk. See Labels for naming guidance, bulk
actions, metric attribution, and OCI image labels.
Image sources
Most sandboxes start from an OCI image. You can also use a host directory as the root filesystem, or boot from a disk image. Select one source:
use microsandbox::Sandbox;
let image = "python"; // Or "./my-rootfs" or "./alpine.qcow2".
let sb = Sandbox::builder("worker").image(image).create().await?;from microsandbox import Sandbox
image = "python" # Or "./my-rootfs" or "./alpine.qcow2".
sb = await Sandbox.create("worker", image=image)image := "python" // Or "./my-rootfs" or "./alpine.qcow2".
sb, err := m.CreateSandbox(ctx, "worker", m.WithImage(image))
if err != nil {
return err
}image = "python" # Or "./my-rootfs" or "./alpine.qcow2".
sb = Microsandbox::Sandbox.create("worker", image: image)# Choose one source:
msb create python --name worker
msb create ./my-rootfs --name worker
msb create ./alpine.qcow2 --name workerOCI images use a copy-on-write overlay so sandboxes can share cached base layers. Disk images are attached as block devices, so each sandbox should use its own disk image copy unless the image format handles its own snapshotting.
See Images for the full OCI and disk image model.
Naming conflicts
Creating a sandbox fails if another sandbox already has the same name. Use connect_or_create when you want to reuse the current persisted identity and its configuration. Use replace when you explicitly want a fresh sandbox with that name:
let sb = Sandbox::builder("worker")
.image("python")
.replace()
.create()
.await?;sb = await Sandbox.create("worker", image="python", replace=True)sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("python"),
m.WithReplace(),
)sb = Microsandbox::Sandbox.create("worker", image: "python", replace: true)msb create --replace python --name workerWhen replacing a running sandbox, microsandbox attempts graceful shutdown before force-killing it. Use replace_with_timeout or --replace-with-timeout when the workload needs a longer grace period.
Reference
For the complete sandbox API, see TypeScript, Rust, Python, Go, or Ruby. For terminal and REST workflows, see Sandbox commands and the Cloud API.
Where to go next
- Lifecycle: start, stop, detach, wait, and remove sandboxes
- Commands: run commands and stream output
- Filesystem: read, write, and transfer files
- Secrets: inject credentials without exposing values
- Images: choose OCI, directory, or disk-image roots
- Volumes: persist or share data
- SSH: connect with SSH and SFTP clients
- Snapshots: capture and reuse writable sandbox state
- Live Modify: change settings after create
- Labels: organize, select, and attribute sandboxes
- Logs: inspect workload and runtime output
- Metrics: monitor sandbox resource usage
- Bootstrap: prepare scripts, patches, and PID 1
- Networking: control egress, ingress, DNS, and ports
Sdk Overview (`docs/sdk/overview.mdx`)
title: Overview "og:title": Microsandbox SDKs for TypeScript, Rust, Python, Go, and Ruby keywords: ["microsandbox SDK", "Python SDK", "TypeScript SDK", "Rust SDK", "Go SDK"] icon: "play"
The SDK runs microsandbox directly from your application. Running locally, creating a sandbox spawns a VM as a child process; there is no daemon to install and no server to connect to. With an API key set, the same SDK runs sandboxes on microsandbox cloud instead.
Installation
cargo add microsandboxpip install microsandboxgo get github.com/superradcompany/microsandbox/sdk/gogem install microsandboxAll five SDKs can use a separately provisioned runtime. See Runtime setup for code tabs covering each SDK’s packaging and setup requirements.
Small example
Create a sandbox from a container image, run a command, print the output, and stop it.
await using sb = await Sandbox.builder("hello")
.image("python")
.create();
const output = await sb.exec("python", ["-c", "print('Hello, World!')"]);
console.log("Output:", output.stdout());
```rust Rust
use std::error::Error;
use microsandbox::Sandbox;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let sb = Sandbox::builder("hello")
.image("python")
.create()
.await?;
let output = sb.exec("python", ["-c", "print('Hello, World!')"]).await?;
println!("{}", output.stdout()?);
sb.stop().await?;
Ok(())
}import asyncio
from microsandbox import Sandbox
async def main():
sb = await Sandbox.create(
"hello",
image="python",
)
output = await sb.exec("python", ["-c", "print('Hello, World!')"])
print("Output:", output.stdout_text)
await sb.stop()
asyncio.run(main())package main
import (
"context"
"fmt"
"log"
m "github.com/superradcompany/microsandbox/sdk/go"
)
func main() {
ctx := context.Background()
if _, err := m.EnsureRuntime(ctx, m.RuntimeConfig{}, m.InstallOptions{}); err != nil {
log.Fatal(err)
}
sb, err := m.CreateSandbox(ctx, "hello", m.WithImage("python"))
if err != nil {
log.Fatal(err)
}
defer func() {
_ = sb.Stop(context.Background())
_ = sb.Close()
}()
out, err := sb.Exec(ctx, "python", []string{"-c", "print('Hello, World!')"})
if err != nil {
log.Fatal(err)
}
fmt.Println("Output:", out.Stdout())
}require "microsandbox"
Microsandbox.install unless Microsandbox.installed?
Microsandbox::Sandbox.with("hello", image: "python") do |sandbox|
output = sandbox.exec("python", ["-c", "print('Hello, World!')"])
puts "Output: #{output.stdout}"
endReference
Choose the SDK for your language:
For low-level protocol integrations, see the Agent Client references for TypeScript, Rust, Python, and Go. See also error handling.
Security Overview (`docs/security/overview.mdx`)
title: "Security model" icon: "shield-halved"
microsandbox runs code you don't trust, like AI agents, user submissions, plugins, scrapers, and build jobs, without handing any of it your host. This page describes the security model end to end: the boundary it draws, the guarantees that boundary gives you by default, and the things it deliberately leaves to you.
The whole model rests on one idea. The guest is untrusted, the host is trusted, and a hardware hypervisor sits between them. Every interaction a workload has with the outside world, whether files, network, secrets, or the control channel, is brokered by host-side code on the trusted side of that boundary.
The trust boundary
A sandbox is a microVM: a real virtual machine with its own Linux kernel, scheduled by a hardware hypervisor (KVM on Linux, Apple's Hypervisor.framework on macOS). It is not a container sharing your host kernel.
Two actors sit on either side of one boundary:
| Side | What lives there | How it's treated |
|---|---|---|
| Guest | Your workload and its processes, the guest Linux kernel, and agentd (the in-guest agent) |
Fully untrusted. Assume it is adversarial. |
| Host | Your application or the msb CLI, plus the per-sandbox process that embeds the VMM, network stack, filesystem broker, and secret injection |
Trusted. It enforces the boundary. |
The guest reaches the host only through a fixed, small set of paravirtual (virtio) devices. It cannot make host syscalls, read host memory, or open host connections on its own. Everything it asks for is mediated by the trusted side.
What you get by default
Before any configuration, a sandbox already gives you:
- Hardware-isolated execution. Each sandbox has its own kernel and memory, scheduled by the hypervisor. A guest kernel compromise stays in the guest.
- A private filesystem. The guest sees only its image plus whatever you explicitly mount. Your host filesystem stays invisible.
- An isolated writable root. Writes land in a per-sandbox layer that never touches the shared image cache or another sandbox.
- A network that can't pivot inward. The public internet is reachable, but private ranges, loopback, link-local, cloud metadata, and your host are all denied. Inbound traffic only reaches the ports you publish.
- Secrets that stay on the host. Credentials you bind never enter the VM. The guest only ever sees a meaningless placeholder.
Scope: what the boundary covers
Being candid about the edges is more useful than a long list of features. Here is where the model defends you, where it does not, and what it expects you to own.
Defended by the model
- Guest-to-host escape. The VM and hypervisor boundary is what keeps a compromised guest off your host.
- Sandbox-to-sandbox isolation. Separate VMs, separate processes, separate writable layers, separate network gateways.
- Egress filtering and SSRF. Workloads cannot reach your private network, host, or cloud metadata service by default.
- Secret leakage to unintended hosts. A bound credential is only revealed at the host you allow it for.
- Host filesystem privacy. The guest sees nothing of your host disk unless you mount it.
Out of scope, by design
- A compromised host. If an attacker already controls your host, or the process that launches sandboxes, they are on the trusted side. Secret values live in host memory, and host configuration defines the policy. microsandbox protects the host from the guest, not a guest from a hostile host.
- The hypervisor and CPU. The model trusts KVM, Hypervisor.framework, and the silicon to enforce VM isolation. A hypervisor or hardware vulnerability sits below this boundary.
- What an allowed destination does with your data. If you allow egress to
api.example.comand inject a secret there, that endpoint receives the real credential. The model controls where data can go, not what the far side does with it. - Image provenance. Pulled content is verified against its declared digest, but signatures and attestations are not checked. The registry you pull from is part of your trusted base. See Filesystem & images.
- Self-inflicted denial of service. A workload can exhaust its own CPU and memory within the limits you set. That doesn't affect other sandboxes or the host.
Your responsibility
- Pick a network policy that matches your threat model. The default is sensible, but high-stakes workloads often want deny-by-default.
- Drop in-guest privileges when the workload doesn't need root.
- Pin images you trust, and mount only what's needed, read-only where possible.
- Keep the host that runs sandboxes secure. It is the trusted anchor for everything above.
How the model is enforced
Each subsystem carries part of the boundary. The pages below go deep on each, candidly including the limits.
Reporting a vulnerability
If you find a way across any of these boundaries, like a guest-to-host escape, a cross-sandbox leak, an egress-filter bypass, or a secret revealed outside its intended host, we want to hear about it. Use GitHub's private vulnerability reporting or email [email protected]. The full process and scope are in the project's security policy.
Configuration (`docs/configuration.mdx`)
title: Global configuration "og:title": Microsandbox global configuration reference icon: "gear"
For guidance on choosing between global configuration, sandbox YAML, CLI
arguments, and SDK inputs, start with Configuration.
microsandbox reads user configuration from ~/.microsandbox/config.json, or MSB_CONFIG_PATH when set. All fields are optional. A missing file or empty JSON object adds no user overrides; defaults and managed settings still apply.
Reference example
Set only the values you want to override. For example, give new sandboxes two CPUs and 1 GiB of memory:
Relative host paths stored in this file resolve from the file's directory, including home, paths.*, and registries.ca_certs. For example, /etc/microsandbox/config.json containing "ca_certs": "./company.pem" selects /etc/microsandbox/company.pem, regardless of the caller's working directory. This intentionally replaces the previous working-directory-relative interpretation.
Use an absolute path to keep a location elsewhere. Programmatic SDK paths and environment defaults are captured relative to the caller's working directory when the backend is constructed. Guest paths, such as sandbox_defaults.workdir, are unaffected.
Newly saved user files include "version": 1. An omitted version defaults to 1; invalid or unsupported versions are rejected.
For reading and saving configuration programmatically, see the Rust configuration reference.
Managed configuration
Administrators can enforce these same fields using a protected managed.json file with the shape { "version": 1, "overrides": { ... } }. Supplied managed values take precedence over ordinary user settings and CLI/SDK options. The user and managed files have independent schema versions, each defaulting to 1 when omitted. Managed values are not automatically saved into the user file.
See Managed deployment for deployment, precedence, clearing, and updates. This page remains the reference for individual fields and accepted values.
Top-level fields
| Field | Default | Description |
|---|---|---|
home |
~/.microsandbox |
Root directory for all microsandbox data |
log_level |
null (silent) |
Log level for sandbox processes: error, warn, info, debug, trace |
deployment_profile |
null |
Authoritative local host-runtime isolation profile: single-tenant or multi-tenant |
database |
reference | Database connection settings |
paths |
reference | Path overrides for binaries and directories |
sandbox_defaults |
reference | Defaults applied to new sandboxes, subject to backend support |
runtime |
reference | Host runtime performance policy |
registries |
reference | Container registry authentication |
ssh |
reference | Host-side SSH session defaults |
metrics |
reference | Live metrics shared-memory registry settings |
snapshots |
reference | Full snapshot capture and restore limits |
active_profile |
null |
Default backend profile name |
profiles |
reference | Named backend profiles |
`deployment_profile`
Set deployment_profile when the local host must enforce one deployment policy for every sandbox:
single-tenant preserves the network configuration requested by each sandbox. multi-tenant applies the host-runtime isolation floor for shared infrastructure. The configured value is authoritative on create and restart, so a per-sandbox CLI or SDK option cannot weaken or replace it. A programmatic backend override takes precedence over the user file. When the field is absent or null, each sandbox selects its own profile and defaults to single-tenant.
The parser also accepts the internal wire spellings single_tenant and multi_tenant for compatibility, but the kebab-case values above are canonical in this human-edited file.
`database`
| Field | Default | Description |
|---|---|---|
url |
null |
Database URL. Uses SQLite under home when null |
max_connections |
5 |
Maximum connection pool size |
connect_timeout_secs |
30 |
Timeout when acquiring a database connection from the pool |
busy_timeout_secs |
30 |
SQLite busy timeout before a contended write surfaces as an error |
`paths`
All path fields are optional. Runtime pairs resolve from environment overrides, SDK-provided paths, explicit configuration, then the install under home. Other unset paths resolve relative to home; runtime resolution does not search PATH or debug build directories.
On Windows, the default home is %USERPROFILE%\.microsandbox. JSON strings can use escaped backslashes such as "C:\\Users\\you\\.microsandbox\\lib\\libkrunfw.dll" or forward slashes such as "C:/Users/you/.microsandbox/lib/libkrunfw.dll".
| Field | Default | Description |
|---|---|---|
msb |
{home}/bin/msb |
msb binary. Configure a matching libkrunfw path; when omitted, resolution can use a library adjacent to this executable. An incomplete pair fails closed. |
libkrunfw |
{home}/lib/libkrunfw |
Matching libkrunfw shared library (.so on Linux, .dylib on macOS, .dll on Windows). Set this together with msb. |
agentd |
embedded when enabled | Linux guest Agentd executable. Normal release builds embed it in msb; custom builds without embed-binaries must provide this field or MSB_AGENTD_PATH. The environment variable takes precedence, and an explicitly configured missing or invalid file fails closed. |
cache |
{home}/cache |
Image layer cache |
sandboxes |
{home}/sandboxes |
Per-sandbox state |
volumes |
{home}/volumes |
Named volumes |
snapshots |
{home}/snapshots |
Snapshot artifacts |
logs |
{home}/logs |
Sandbox logs |
secrets |
{home}/secrets |
Secrets. Registry secrets live under secrets/registries/ |
Runtime path settings are captured during local backend construction, with managed paths taking precedence. Existing backend handles keep their settings until replaced.
For programmatic access, see the Rust configuration reference.
`ssh`
| Field | Default | Description |
|---|---|---|
inactivity_timeout_secs |
600 |
Disconnect an SSH session after this many seconds without SSH traffic. Set to 0 to disable. |
The timeout applies to host-side SSH sessions created by msb ssh, msb ssh serve, and the SDK, for both local and cloud sandboxes. SSH traffic resets the timer. This setting is separate from a sandbox lifecycle idle timeout: disconnecting SSH does not stop the sandbox, and sandbox activity policy does not change the SSH timeout.
Explicit CLI or SDK options override the user-configured value; a managed timeout takes precedence over both. The setting is resolved when a client or server endpoint is prepared using the backend's retained configuration, so file changes do not alter existing connections. Programmatic local backends can override the persisted timeout.
`sandbox_defaults`
Defaults applied to sandboxes unless overridden per-sandbox.
For workload effects, host requirements, filesystem expectations, and verification steps for these settings, see Performance.
| Field | Default | Description |
|---|---|---|
cpus |
1 |
Number of vCPUs |
memory_mib |
512 |
Guest memory in MiB |
cpu_placement |
"inherit" |
Host vCPU placement: inherit, auto, spread, or compact |
placement_profile |
null |
Name of a host-defined entry in runtime.placement_profiles |
thp |
"madvise" |
Guest transparent huge-page policy: always, madvise, or never |
oci |
reference | Defaults for OCI-rooted sandboxes |
shell |
"/bin/sh" |
Shell for interactive sessions and scripts |
workdir |
null |
Working directory inside the sandbox |
outbound_proxy |
null |
Default outbound SOCKS4/SOCKS5 proxy for local sandboxes. Uses the existing proxy object with protocol, address (IP:port), and optional authentication. See managed proxy configuration. |
metrics_sample_interval_ms |
1000 |
Runtime metrics sampling interval in milliseconds. Set to 0 to disable sampling. |
disable_metrics_sample |
false |
Force-disable metrics sampling regardless of metrics_sample_interval_ms. |
Explicit CLI or SDK options win over config.json, and managed overrides take precedence over both. microsandbox resolves the effective values before creating a local sandbox or sending a cloud create request. Cloud creation rejects sandbox options it cannot represent. File changes apply after constructing a new backend or starting a new CLI invocation; they do not rewrite existing sandboxes.
Local creation resolves built-in defaults < image defaults < config.json < CLI/SDK patches < managed overrides after image metadata is available. Cloud creation applies user settings, request options, and managed overrides on the client; the cloud worker resolves image metadata.
For local workdir, omission inherits the lower layer and explicit null clears it, including an image working directory. Local CPU and memory maxima remain explicit when supplied; otherwise they follow the final size. Cloud supports no separate hotplug maximum: a supplied maximum equal to the requested size follows the final size. A maximum that differs from the final size is rejected.
An ordinary outbound_proxy default can be overridden per sandbox. A managed value takes precedence over CLI/SDK proxy options; managed null clears them.
The whole proxy object is replaced, including credentials. SOCKS4 accepts an optional user_id; SOCKS5 accepts credentials containing username and password: { "kind": "env", "var": "PROXY_PASSWORD" }. Passwords are resolved at sandbox startup.
Proxy defaults do not enable disabled networking or route host downloads. Cloud creation fails if the final configuration contains a proxy, including one supplied by managed policy.
`sandbox_defaults.oci`
Defaults applied only when the sandbox rootfs is an OCI image.
User OCI defaults apply to local creation. Managed root-disk settings also apply to cloud create requests, which accept only kind: "managed".
| Field | Default | Description |
|---|---|---|
root_disk |
null |
Default OCI root disk using the same tagged shape as the SDK: managed, tmpfs, or flat. A missing value preserves the managed layered root. User-owned disk-image defaults are rejected because they would share one writable image across sandboxes. |
upper_size_mib |
null |
Deprecated managed-layered size shorthand. When both this field and root_disk are absent, the resolved managed size is 4096 MiB. |
root_disk and upper_size_mib are mutually exclusive. For a portable flat default, use clone: "auto"; it attempts a native reflink and safely falls back to a sparse copy. Use clone: "reflink" only when failure is preferable to copying on a host filesystem without reflink support.
When the root-disk default is flat, plain msb pull IMAGE also prepares the reusable flat artifact. An explicit msb pull IMAGE --materialize layered|flat|all always wins.
`runtime`
| Field | Default | Description |
|---|---|---|
block_writeback |
{ "mode": "auto" } |
Buffered host writeback containment and live pressure sharing on Linux. Pool pressure never rejects sandbox creation; unconfigured auto is a no-op on other hosts. |
placement_profiles |
{} |
Host-owned named CPU and NUMA placement profiles selectable by sandboxes |
`runtime.placement_profiles`
Placement profiles let an operator define safe host topology policy once while callers select it by name. numa.mode is prefer_single, strict_single, or inherit; memory.mode is follow_cpu or inherit.
This release does not enable multi-node guest placement. prefer_single falls back to inherited host NUMA behavior when one node cannot fit. strict_single fails clearly because it is an explicit guarantee.
follow_cpu requires managed CPU placement; it cannot use inherited placement. Linux prefers the selected node for ordinary profiles and allows memory to spill elsewhere under pressure; strict_single uses a required binding instead. If CPUs span nodes, capacity is already insufficient, or the kernel rejects a best-effort affinity or memory-policy syscall, an ordinary profile starts with inherited memory placement.
Windows keeps ordinary memory inherited because its preferred-node allocation cannot be undone if a later vCPU affinity attempt falls back; strict_single can still request required preferred-node allocation. Windows checks current boot-time availability, but does not expose equivalent per-node total capacity for a hard future-growth promise.
macOS inherits ordinary CPU and memory scheduling for non-strict profiles because it has no equivalent hard-affinity API.
`runtime.block_writeback`
| Field | Modes | Default | Description |
|---|---|---|---|
mode |
all | "auto" |
auto gives each eligible disk a measured 1536 MiB maximum and shares the aggregate pool when the host is busy, fixed requires an explicit maximum, and off disables both the VMM bound and host-global pressure coordination. |
per_disk_mib |
fixed |
required | Explicit maximum for each eligible writable raw disk. The boot-time maximum is at least 128 MiB; the live fair share may fall below it under aggregate pressure. This field is rejected in auto and off modes. |
pool_mib |
auto, fixed |
null |
Optional aggregate dirty-credit pressure-pool override. null derives the pool as the lower of 10% of physical RAM and a conservative estimate of Linux's dirty-background threshold, using the exact byte threshold when configured or the ratio applied to current MemAvailable. This field is rejected in off mode. |
Use a fixed policy only when representative measurements justify a different per-disk window:
pool_mib is a live pressure budget, not eagerly allocated RAM. Every eligible writable disk in the same MSB_HOME receives a weighted max-min fair share. Disks with a smaller configured maximum keep that smaller value, and the remaining pool is divided equally across the rest. Creating another sandbox never fails merely because this pool is full.
Existing VMMs observe membership changes within 250 ms. If a disk already owns more dirty data than its new share, libkrun retires accounted ranges and pauses later writes until it converges below the target. A guest write already reserved before the target changed completes safely. fixed and an explicitly pooled auto policy are unsupported on other hosts.
See the performance guide for choosing and measuring runtime settings.
`registries`
| Field | Default | Description |
|---|---|---|
ca_certs |
null |
Path to a PEM file with additional CA root certificates trusted for registry pulls |
hosts |
{} |
Per-registry settings keyed by registry hostname |
`registries.hosts`
A map of registry hostnames to settings. Each host entry can mark the registry as insecure (plain HTTP) and can include an auth entry. Each auth entry specifies a username and exactly one credential source.
Host entry fields
| Field | Required | Description |
|---|---|---|
insecure |
No | Use plain HTTP instead of HTTPS for this registry |
auth |
No | Authentication entry for this registry |
Auth entry fields
| Field | Required | Description |
|---|---|---|
username |
Yes | Registry username |
store |
No | Credential store. Only "keyring" is supported (macOS Keychain, Windows Credential Manager, Linux Secret Service) |
password_env |
No | Environment variable containing the password or token |
secret_name |
No | Filename under {home}/secrets/registries/ containing the password or token |
Auth resolution order
Managed registry hosts merge by field. Setting insecure: false enforces TLS while preserving employee credentials. Omitted auth keeps the normal credential lookup and CLI/SDK credentials; explicit auth: null forces anonymous access. A managed auth object replaces credentials, including explicit CLI/SDK credentials. A managed registries.ca_certs path replaces user and per-call certificates; null clears them. Without a managed certificate setting, per-call certificates append to user certificates.
For local pulls without managed auth, microsandbox resolves credentials in this order, including when a managed host entry only sets TLS options:
- Explicit SDK auth via
.registry(|r| r.auth(...))on the sandbox builder - OS keyring entries created by
msb registry login - Config file
registries.hosts.<host>.authentries inconfig.json - Docker config
~/.docker/config.jsoncredential helpers - Anonymous (no authentication)
`metrics`
| Field | Default | Description |
|---|---|---|
capacity |
0 |
Number of slots reserved in the live metrics shared-memory registry. 0 uses the built-in default. Stop all sandboxes for the same home before changing this value. |
`snapshots`
| Field | Default | Description |
|---|---|---|
max_filesystem_state_mib |
4 |
Largest virtio-fs backend state, in MiB, that one filesystem device can carry through a full snapshot. Valid values are 1 through 4095. |
This limit applies per filesystem device during capture, verification, export, import, restore, and live forks.
After changing it, restart running sandboxes to update their capture limit. Host snapshot checks use the new limit immediately, so lowering it can reject captures from sandboxes still using a larger limit.
When reading snapshots, allow enough space for each device's saved state. The reader does not need to match the capture host's maximum. A smaller limit rejects oversized state before making changes.
Before raising it, consider memory use, especially when restoring imported snapshots. Raise it only for snapshots you trust.
`profiles`
Named backend profiles keyed by profile name; active_profile selects the default. How profiles participate in local or cloud selection, including the full precedence order, is documented in Local or cloud.
| Field | Default | Description |
|---|---|---|
backend |
required | local or cloud |
api_key_ref |
none | Credential reference; required for cloud profiles |
url |
https://api.microsandbox.dev |
Cloud endpoint override for development, self-hosted, or on-prem control planes |
Supported credential references for api_key_ref:
| Prefix | Description |
|---|---|
env:<VAR_NAME> |
Read the API key from an environment variable |
inline:<API_KEY> |
Store the API key directly in config.json; use only for development or CI |
Docs.Json (`docs/docs.json`)
{
"$schema": "https://mintlify.com/docs.json",
"theme": "maple",
"name": "microsandbox",
"logo": {
"light": "/images/microsandbox-banner-light.svg",
"dark": "/images/microsandbox-banner-dark.svg",
"href": "https://microsandbox.dev"
},
"favicon": "/logo/favicon.png",
"colors": {
"primary": "#BF84FE",
"light": "#BF84FE",
"dark": "#BF84FE"
},
"thumbnails": {
"appearance": "dark",
"background": "/images/og-background.png"
},
"navbar": {
"primary": {
"type": "github",
"href": "https://github.com/superradcompany/microsandbox"
}
},
"navigation": {
"tabs": [
{
"tab": "Documentation",
"groups": [
{
"group": "Getting Started",
"icon": "rocket",
"pages": [
"getting-started/introduction",
"getting-started/quickstart",
"cloud/overview",
"getting-started/agents"
]
},
{
"group": "Sandboxes",
"icon": "box",
"pages": [
"sandboxes/overview",
"sandboxes/lifecycle",
"sandboxes/commands",
"sandboxes/filesystem",
"sandboxes/secrets",
"images/overview",
"sandboxes/volumes",
"sandboxes/ssh",
"sandboxes/snapshots",
"sandboxes/tuning",
"sandboxes/labels",
"sandboxes/logs",
"sandboxes/metrics",
"sandboxes/bootstrap"
]
},
{
"group": "Networking",
"icon": "network-wired",
"pages": [
"networking/overview",
"networking/tls",
"networking/dns",
"networking/outbound-proxy",
"networking/host-sockets"
]
},
{
"group": "Deep dive",
"icon": "gauge",
"pages": [
"operations/backends",
"operations/configuration",
"observability/msb-metrics",
"sandboxes/optimization"
]
},
{
"group": "Troubleshooting",
"icon": "screwdriver-wrench",
"pages": [
"troubleshooting/linux",
"troubleshooting/macos",
"troubleshooting/windows"
]
}
]
},
{
"tab": "SDKs & CLI",
"groups": [
{
"group": "Getting Started",
"icon": "flag",
"pages": [
"sdk/overview",
"sdk/setup",
"sdk/errors",
"configuration"
]
},
{
"group": "SDKs",
"icon": "code",
"pages": [
{
"group": "TypeScript",
"icon": "js",
"expanded": false,
"pages": [
"sdk/typescript/sandbox",
"sdk/typescript/execution",
"sdk/typescript/ssh",
"sdk/typescript/filesystem",
"sdk/typescript/volumes",
"sdk/typescript/networking",
"sdk/typescript/proxies",
"sdk/typescript/vsock",
"sdk/typescript/secrets",
"sdk/typescript/snapshots",
"sdk/typescript/images",
"sdk/typescript/agent-client"
]
},
{
"group": "Rust",
"icon": "rust",
"expanded": false,
"pages": [
"sdk/rust/sandbox",
"sdk/rust/configuration",
"sdk/rust/execution",
"sdk/rust/ssh",
"sdk/rust/filesystem",
"sdk/rust/volumes",
"sdk/rust/networking",
"sdk/rust/proxies",
"sdk/rust/vsock",
"sdk/rust/secrets",
"sdk/rust/snapshots",
"sdk/rust/images",
"sdk/rust/agent-client"
]
},
{
"group": "Python",
"icon": "python",
"expanded": false,
"pages": [
"sdk/python/sandbox",
"sdk/python/execution",
"sdk/python/ssh",
"sdk/python/filesystem",
"sdk/python/volumes",
"sdk/python/networking",
"sdk/python/proxies",
"sdk/python/vsock",
"sdk/python/secrets",
"sdk/python/snapshots",
"sdk/python/images",
"sdk/python/agent-client"
]
},
{
"group": "Go",
"icon": "golang",
"expanded": false,
"pages": [
"sdk/go/sandbox",
"sdk/go/execution",
"sdk/go/ssh",
"sdk/go/filesystem",
"sdk/go/volumes",
"sdk/go/images",
"sdk/go/networking",
"sdk/go/proxies",
"sdk/go/vsock",
"sdk/go/secrets",
"sdk/go/snapshots",
"sdk/go/agent-client"
]
},
{
"group": "Ruby",
"icon": "gem",
"expanded": false,
"pages": [
"sdk/ruby/sandbox",
"sdk/ruby/configuration",
"sdk/ruby/execution",
"sdk/ruby/ssh",
"sdk/ruby/filesystem",
"sdk/ruby/volumes",
"sdk/ruby/networking",
"sdk/ruby/vsock",
"sdk/ruby/snapshots",
"sdk/ruby/images"
]
}
]
},
{
"group": "CLI",
"icon": "terminal",
"pages": [
"cli/overview",
"cli/configuration",
"cli/sandbox-commands",
"cli/snapshot-commands",
"cli/ssh-commands",
"cli/volume-commands",
"cli/storage-commands",
"cli/image-commands",
"cli/registry-commands",
"cli/management"
]
}
]
},
{
"tab": "API Reference",
"groups": [
{
"group": "Getting Started",
"icon": "book-open",
"pages": [
"api-reference/overview"
]
},
{
"group": "API keys",
"icon": "key",
"openapi": {
"source": "/api-reference/openapi.json",
"directory": "api-reference/endpoints"
}
},
{
"group": "Personal tokens",
"icon": "user",
"openapi": {
"source": "/api-reference/openapi.personal.json",
"directory": "api-reference/personal"
}
}
]
},
{
"tab": "How-to guides",
"groups": [
{
"group": "How-to guides",
"pages": [
"how-to/overview",
{
"group": "Agents",
"expanded": false,
"icon": "robot",
"pages": [
"how-to/agents/claude-code",
"how-to/agents/codex",
"how-to/agents/opencode",
"how-to/agents/openclaw",
"how-to/agents/goose",
"how-to/agents/gemini-cli",
"how-to/agents/hermes-agent",
"how-to/agents/pi"
]
},
{
"group": "Development",
"expanded": false,
"icon": "code",
"pages": [
"how-to/development/vscode",
"how-to/development/jetbrains-acp",
"how-to/development/code-server",
"how-to/development/jupyterlab",
"how-to/development/vnc-desktop"
]
},
{
"group": "Browser automation",
"expanded": false,
"icon": "globe",
"pages": [
"how-to/browser-automation/browser-use",
"how-to/browser-automation/playwright"
]
},
{
"group": "Workloads",
"expanded": false,
"icon": "gears",
"pages": [
"how-to/file-processing/ffmpeg",
"how-to/file-processing/libreoffice-pdf",
"how-to/plugins/terraform",
"how-to/data/migration-rehearsal",
"how-to/web-automation/scrapy",
"how-to/automation/parallel-batch-jobs"
]
},
{
"group": "Sandbox setup",
"expanded": false,
"icon": "box",
"pages": [
"how-to/docker/docker-in-sandbox",
"how-to/docker/local-images",
"how-to/guest/systemd-services",
"how-to/sandboxing/warm-workers"
]
},
{
"group": "Deployment",
"expanded": false,
"icon": "server",
"pages": [
"how-to/docker/docker",
"how-to/sandboxing/kubernetes"
]
},
{
"group": "Storage",
"expanded": false,
"icon": "database",
"pages": [
"how-to/data/postgresql",
"how-to/data/redis",
"how-to/data/juicefs-s3"
]
},
{
"group": "CI/CD",
"expanded": false,
"icon": "code-pull-request",
"pages": [
"how-to/ci-cd/github-actions-runner",
"how-to/ci-cd/pr-checks",
"how-to/ci-cd/compose-tests",
"how-to/ci-cd/preview-deploys",
"how-to/automation/dependency-audits"
]
},
{
"group": "Observability",
"expanded": false,
"icon": "chart-line",
"pages": [
"how-to/metrics-backends/grafana-cloud",
"how-to/metrics-backends/grafana-alloy",
"how-to/metrics-backends/otel-collector",
"how-to/metrics-backends/prometheus",
"how-to/metrics-backends/datadog"
]
}
]
}
]
},
{
"tab": "Enterprise",
"groups": [
{
"group": "Enterprise",
"pages": ["enterprise"]
},
{
"group": "Employee devices",
"pages": [
"enterprise/prepare-environment",
"enterprise/corporate-networking",
"enterprise/managed-configuration",
"enterprise/deploy-and-verify"
]
},
{
"group": "Talk to us",
"pages": ["enterprise/contact"]
}
]
},
{
"tab": "Security",
"pages": [
"security/overview",
"security/isolation",
"security/filesystem",
"security/network",
"security/secrets",
"security/hardening"
]
},
{
"tab": "Changelog",
"groups": [
{
"group": "Migration guides",
"icon": "arrow-right-arrow-left",
"pages": [
"migrations/v0.7"
]
},
{
"group": "Releases",
"icon": "tag",
"pages": [
"changelog/v0.7.8",
"changelog/v0.7.7",
"changelog/v0.7.6",
"changelog/v0.7.5",
"changelog/v0.7.4",
"changelog/v0.7.3",
"changelog/v0.7.2",
"changelog/v0.7.1",
"changelog/v0.7.0"
]
},
{
"group": "Past updates",
"icon": "clock-rotate-left",
"pages": [
{
"group": "September 2026",
"expanded": false,
"pages": [
"changelog/2026-09-25",
"changelog/2026-09-11",
"changelog/2026-09-04"
]
},
{
"group": "August 2026",
"expanded": false,
"pages": [
"changelog/2026-08-28",
"changelog/2026-08-26",
"changelog/2026-08-21",
"changelog/2026-08-14",
"changelog/2026-08-07"
]
},
{
"group": "July 2026",
"expanded": false,
"pages": [
"changelog/2026-07-31",
"changelog/2026-07-24",
"changelog/2026-07-17",
"changelog/2026-07-10",
"changelog/2026-07-05"
]
},
{
"group": "June 2026",
"expanded": false,
"pages": [
"changelog/2026-06-26",
"changelog/2026-06-19",
"changelog/2026-06-12",
"changelog/2026-06-05"
]
},
{
"group": "May 2026",
"expanded": false,
"pages": [
"changelog/2026-05-29",
"changelog/2026-05-22",
"changelog/2026-05-15",
"changelog/2026-05-01"
]
},
{
"group": "April 2026",
"expanded": false,
"pages": [
"changelog/2026-04-24",
"changelog/2026-04-17",
"changelog/2026-04-10",
"changelog/2026-04-03"
]
}
],
"expanded": false
}
]
}
]
},
"redirects": [
{
"source": "/networking/security-model",
"destination": "/security/network"
},
{
"source": "/getting-started/windows-troubleshooting",
"destination": "/troubleshooting/windows"
},
{
"source": "/examples/kubernetes",
"destination": "/how-to/sandboxing/kubernetes"
},
{
"source": "/examples/automation/parallel-batch-jobs",
"destination": "/how-to/automation/parallel-batch-jobs"
},
{
"source": "/examples/browser-automation/browser-use",
"destination": "/how-to/browser-automation/browser-use"
},
{
"source": "/examples/web-automation/scrapy",
"destination": "/how-to/web-automation/scrapy"
},
{
"source": "/examples/overview",
"destination": "/how-to/overview"
},
{
"source": "/getting-started/backends",
"destination": "/operations/backends"
},
{
"source": "/operations/backend-selection",
"destination": "/operations/backends"
},
{
"source": "/observability/deep-dive",
"destination": "/observability/msb-metrics#metrics"
},
{
"source": "/optimization/overview",
"destination": "/sandboxes/optimization"
},
{
"source": "/sandboxes/cpu-placement",
"destination": "/sandboxes/optimization#cpu"
},
{
"source": "/images/disk-images",
"destination": "/images/overview#disk-images"
},
{
"source": "/networking/vsock",
"destination": "/networking/host-sockets"
},
{
"source": "/sandbox-config",
"destination": "/cli/configuration"
},
{
"source": "/recipes/use-cases/ai-coding-agent",
"destination": "/getting-started/agents"
},
{
"source": "/examples/use-cases/ai-coding-agent",
"destination": "/getting-started/agents"
},
{
"source": "/recipes",
"destination": "/how-to/overview"
},
{
"source": "/examples/data/juicefs-r2",
"destination": "/how-to/data/juicefs-s3"
},
{
"source": "/cloud/compatibility",
"destination": "/cloud/overview#compatibility"
},
{
"source": "/enterprise/offline-deployment",
"destination": "/enterprise/prepare-environment#prepare-offline-devices"
},
{
"source": "/recipes/overview",
"destination": "/how-to/overview"
},
{
"source": "/examples/agents/claude-code",
"destination": "/how-to/agents/claude-code"
},
{
"source": "/examples/agents/codex",
"destination": "/how-to/agents/codex"
},
{
"source": "/examples/agents/opencode",
"destination": "/how-to/agents/opencode"
},
{
"source": "/examples/agents/openclaw",
"destination": "/how-to/agents/openclaw"
},
{
"source": "/examples/agents/goose",
"destination": "/how-to/agents/goose"
},
{
"source": "/examples/agents/gemini-cli",
"destination": "/how-to/agents/gemini-cli"
},
{
"source": "/examples/agents/hermes-agent",
"destination": "/how-to/agents/hermes-agent"
},
{
"source": "/examples/agents/pi",
"destination": "/how-to/agents/pi"
},
{
"source": "/examples/development/vscode",
"destination": "/how-to/development/vscode"
},
{
"source": "/examples/development/jetbrains-acp",
"destination": "/how-to/development/jetbrains-acp"
},
{
"source": "/examples/development/code-server",
"destination": "/how-to/development/code-server"
},
{
"source": "/examples/development/jupyterlab",
"destination": "/how-to/development/jupyterlab"
},
{
"source": "/examples/development/vnc-desktop",
"destination": "/how-to/development/vnc-desktop"
},
{
"source": "/examples/ci-cd/github-actions-runner",
"destination": "/how-to/ci-cd/github-actions-runner"
},
{
"source": "/examples/ci-cd/pr-checks",
"destination": "/how-to/ci-cd/pr-checks"
},
{
"source": "/examples/ci-cd/compose-tests",
"destination": "/how-to/ci-cd/compose-tests"
},
{
"source": "/examples/ci-cd/preview-deploys",
"destination": "/how-to/ci-cd/preview-deploys"
},
{
"source": "/examples/automation/dependency-audits",
"destination": "/how-to/automation/dependency-audits"
},
{
"source": "/examples/sandboxing/warm-workers",
"destination": "/how-to/sandboxing/warm-workers"
},
{
"source": "/examples/browser-automation/playwright",
"destination": "/how-to/browser-automation/playwright"
},
{
"source": "/examples/file-processing/libreoffice-pdf",
"destination": "/how-to/file-processing/libreoffice-pdf"
},
{
"source": "/examples/file-processing/ffmpeg",
"destination": "/how-to/file-processing/ffmpeg"
},
{
"source": "/examples/docker/docker-in-sandbox",
"destination": "/how-to/docker/docker-in-sandbox"
},
{
"source": "/examples/docker/docker",
"destination": "/how-to/docker/docker"
},
{
"source": "/examples/docker/local-images",
"destination": "/how-to/docker/local-images"
},
{
"source": "/examples/guest/systemd-services",
"destination": "/how-to/guest/systemd-services"
},
{
"source": "/examples/plugins/terraform",
"destination": "/how-to/plugins/terraform"
},
{
"source": "/examples/data/juicefs-s3",
"destination": "/how-to/data/juicefs-s3"
},
{
"source": "/examples/data/postgresql",
"destination": "/how-to/data/postgresql"
},
{
"source": "/examples/data/migration-rehearsal",
"destination": "/how-to/data/migration-rehearsal"
},
{
"source": "/examples/data/redis",
"destination": "/how-to/data/redis"
},
{
"source": "/examples/metrics-backends/grafana-cloud",
"destination": "/how-to/metrics-backends/grafana-cloud"
},
{
"source": "/examples/metrics-backends/grafana-alloy",
"destination": "/how-to/metrics-backends/grafana-alloy"
},
{
"source": "/examples/metrics-backends/otel-collector",
"destination": "/how-to/metrics-backends/otel-collector"
},
{
"source": "/examples/metrics-backends/prometheus",
"destination": "/how-to/metrics-backends/prometheus"
},
{
"source": "/examples/metrics-backends/datadog",
"destination": "/how-to/metrics-backends/datadog"
},
{
"source": "/recipes/:slug*",
"destination": "/examples/:slug*"
}
],
"integrations": {
"posthog": {
"apiKey": "phc_mMJPTEQbarHl4JMTdFBcgw5v612dmQrvKv5mcK3KJ3O",
"apiHost": "https://g.microsandbox.dev"
}
},
"appearance": {
"default": "system"
},
"description": "Easy, fast microVMs for untrusted workloads, on your machine or in the cloud",
"seo": {
"organization": {
"id": "https://microsandbox.dev/#organization",
"name": "microsandbox",
"url": "https://microsandbox.dev",
"sameAs": [
"https://github.com/superradcompany/microsandbox",
"https://www.linkedin.com/company/superradcompany",
"https://x.com/microsandbox"
]
}
},
"footer": {
"socials": {
"github": "https://github.com/superradcompany/microsandbox",
"discord": "https://discord.gg/T95Y3XnEAK",
"linkedin": "https://www.linkedin.com/company/superradcompany",
"x": "https://x.com/microsandbox"
}
}
}
Enterprise (`docs/enterprise.mdx`)
title: Enterprise deployment sidebarTitle: Overview icon: "building"
Use microsandbox on employee devices or in your own infrastructure. Choose the deployment approach that fits your workloads and operational requirements.
Choose your deployment
| Deployment | Purpose | Next step |
|---|---|---|
| Employee devices | Standardize local development and team settings | Rollout guides |
| Runtime on your infrastructure | Run sandboxes on your servers or clusters | Discuss your deployment |
| Private cloud deployment | Run the microsandbox cloud platform in your infrastructure | Discuss requirements |
For our hosted service, see microsandbox Cloud.
Roll out to employee devices
Start with one working installation, connect it to company services, then apply policy and roll it out to a pilot group.
| Step | Outcome |
|---|---|
| Prepare your environment | Approved SDKs, runtime files, and images work on the target devices, including offline installations. |
| Configure company networking | Sandboxes can reach approved services through company routing, DNS, and certificate trust. |
| Enforce team settings | A protected policy defines which supported settings employees cannot override. |
| Deploy and verify | A tested pilot, repeatable device deployment, and an update and rollback process. |
Deployment responsibilities
| microsandbox provides | Your organization manages |
|---|---|
| Local sandbox isolation and network controls | Device security, host permissions, and approved workloads |
| Managed overrides for supported CLI and SDK settings | Policy distribution, file protection, and supported client versions |
| Runtime and image installation tools | Package approval, distribution, and updates |
| Configuration for company network access | VPN/proxy enrollment, credentials, DNS, and corporate certificates |
Managed settings apply through supported CLI and SDK clients. They are not a replacement for device security: local administrators can remove policy or replace the runtime.
These guides focus on local sandboxes on company-managed devices. Some managed settings also apply to outgoing cloud requests, as described in Enforce team settings. They do not reconfigure the cloud service.
Discuss your deployment
Planning a runtime rollout or private cloud deployment? Contact our team with a short description of your workloads and deployment needs.
Snapshot Groups Explained (`docs/snapshot-groups-explained.md`)
Microsandbox snapshots: groups, checkpoints, and the head
This describes the snapshot-group implementation on the development stack, not an already released CLI.
Start with the snapshot
A snapshot is a saved point you can use to create another sandbox:
Running sandbox
|
+-- disk snapshot ----> new VM boots from the saved disk
|
+-- full snapshot ----> new VM resumes saved RAM, CPUs, devices, and diskDisk snapshots also work when the source is paused or stopped. Full snapshots require resident execution state: a running or user-paused VM. Each capture produces a new immutable snapshot, even when unchanged disk layers or RAM objects are reused. Exporting a snapshot packages it as a .msb archive; loading an archive installs it without starting a VM.
A group gives those saved points a local home
A group is a namespace containing snapshots and a selected head. Member names such as cp01 are meaningful inside their group. Each snapshot also keeps its portable snap_... ID. A new group imported from competing branches can temporarily have no selected head; choose one explicitly before restoring by the bare group name.
~/.microsandbox/snapshots/
|
+-- worker/
| +-- group.json head = snap_B
| +-- snap_A/
| | +-- snapshot.json ID, parent, disk/state references
| | +-- group-member.json name = cp01
| | +-- layers/... disk-only payload
| +-- snap_B/
| +-- snapshot.json parent = snap_A
| +-- group-member.json name = cp02
| +-- checkpoint/... full checkpoint payload, when captured full
|
+-- imported/
+-- group.json
+-- snap_A/... a separate local copy of the same snapshotIDs above are shortened for readability. A disk-only member uses layers/; a full member uses checkpoint/ with its disk layers, RAM objects, and execution/device state. Optional metadata.json stores labels.
Groups do not magically make random IDs collision-proof. They keep local addresses separate. Within one group, the same ID with the same descriptor is reusable; the same ID with different descriptor bytes is rejected. A name already used by another member is also rejected. Nothing is silently overwritten. If a global ID resolves to multiple local copies, use the group-qualified address instead.
Create and restore
msb create alpine --name worker --memory 512M
# Group defaults to the source sandbox's name: worker.
msb snap create cp01 --sandbox worker --full
msb snap create cp02 --sandbox worker --full
# A bare group selects its head, currently cp02.
msb snap restore worker --name latest --cow-mem
# A qualified name selects an exact checkpoint.
msb snap restore worker:cp01 --name earlier --cow-mem
# You can choose a different group, or let a member name be generated.
msb snap create --sandbox worker --group experiments --full--cow-mem shares clean restored RAM pages using copy-on-write; child writes remain private. It does not change which snapshot is selected. Omit --full at capture for a disk-only snapshot, and omit --cow-mem when cold-booting disk state.
The head moves forward, not sideways by surprise
Snapshots record their actual source ancestry. Neither timestamps, import order, nor an export's --since base defines that ancestry.
worker:cp01 ---- worker:cp02 ---- worker:cp03 <- head
\
+-------- worker:experimentThe rules are small:
- Empty group: a single capture or import initializes its head. A batch selects its one provably newest tip, if there is one.
- Known descendant of the current head: advance automatically.
- Same member, older member, sibling, unrelated history, or missing ancestry: keep the current head. The capture/import still succeeds.
- Explicit selection: choose any complete installed member, including an older one.
msb snap head worker # Read the current head ID
msb snap head worker:experiment # Explicitly choose the other branch
msb snap head worker:cp01 # Explicitly rewindThere is no special main branch. The head is a selected snapshot, not a rule for guessing which future branch is preferred.
What if two separate operations publish siblings concurrently?
+---- snapshot A
head: cp02 ---------+
+---- snapshot B
A publishes first: head cp02 -> A
B publishes next: B is A's sibling, so head stays A
Result: both snapshots exist. Only the first head update wins.Publication checks and head replacement share a per-group lock. The losing sibling is not discarded or reported as a failed capture. If you want B, select it explicitly. Two captures of the same source are serialized and record a parent chain; they are not treated as sibling captures.
A single batch containing both siblings is different: neither argument order nor which file finishes first chooses the head. An existing group retains its head; a new group imports both members with no selected head. Then use msb snap head worker:<member> to choose.
Move a history to another machine
# On the source machine:
mkdir -p checkpoints
msb snap export worker:cp01 --output checkpoints/cp01.msb
msb snap export worker:cp02 --output checkpoints/cp02.msb --since worker:cp01
# On the destination machine:
msb snap import checkpoints/*.msb --group received
msb snap restore received --name restored --cow-memThe shell expands *.msb into archive paths. Their order and filenames do not determine ancestry or load order. You can also list them explicitly, in any order:
msb snap import checkpoints/cp02.msb checkpoints/cp01.msb --group receivedLoading unpacks each supplied archive once, matches omitted disk layers and RAM objects to the available payloads, and validates the reconstructed snapshots before publishing members. It looks in the supplied batch first, then the explicitly selected destination group. This works for disk-only and full incremental archives. No intermediate VM runs.
The same automatic lookup works when archives arrive separately:
msb snap import checkpoints/cp01.msb --group received
msb snap import checkpoints/cp02.msb --group received--base is only needed when the missing data is elsewhere, such as --base another-group:cp01 or --base /path/to/baseline.msb. It supplies data; it does not define ancestry or select the group head. An external archive supplied as --base must be standalone; include dependent archives in the batch instead. Missing dependencies and conflicting IDs, names, or duplicate labels fail before publishing any incoming members.
Current development limitation: disk-only captures reassign layer IDs, so --since between successive disk-only captures can reject the base. Use standalone disk-only exports for that workflow until capture identity preservation is fixed. Full-checkpoint incremental imports were live-tested successfully; the batch loader also handles dependency-correct disk-only archives.
--since omits disk layers and reusable RAM objects supplied by the explicit base. Loading reconstructs a complete owned snapshot; the target does not depend on replaying earlier VMs. Each archive still includes the target's complete memory map and CPU/device state. The .msb archive does not contain a local group's mutable head file: its declared archive head is the import candidate, and the receiving group applies the rules above.
Loading without --group creates one fresh generated group for the whole batch. The CLI prints a digest and installed artifact path for each input archive head, in input order. With one archive, the final line remains its installed path. With several archives, the final path is not necessarily the selected group head; use the group selector or msb snap head received instead. Repeating the same snapshot installs it only once.
The destination directory is now an explicit --dest DIR option, leaving positional arguments for archive paths. Existing snapshot/archive formats are unchanged, including legacy readers.
Importing an old checkpoint does not rewind an existing group. To deliberately select the imported archive's head:
msb snap import checkpoints/cp01.msb --group received --set-headFor a batch, --set-head requires one unambiguous tip; it refuses competing tips rather than picking the last argument. Load those members without --set-head, then select the one you want.
Missing historical checkpoints are okay when payload dependencies are complete. But a missing parent may prevent proving a fast-forward. Filling a history hole does not retrospectively select some other retained tip; select that tip explicitly or import it again once its ancestry is known.
Direct archive capture (msb snap create --output PATH, or -o PATH) and direct archive restore still skip installed snapshot directories. msb fork still creates a local child without publishing a durable snapshot. Neither operation implicitly moves a group's head; a later explicit capture can join a group using the child's recorded ancestry.
Api Reference Openapi.Json (`docs/api-reference/openapi.json`)
{
"openapi": "3.1.0",
"info": {
"title": "microsandbox cloud API",
"description": "REST API for microsandbox cloud operations authenticated with an organization API key.",
"version": "0.1.0"
},
"servers": [
{
"url": "https://api.microsandbox.dev"
}
],
"security": [
{
"api_key": []
}
],
"paths": {
"/v1/sandboxes": {
"get": {
"tags": [
"Sandboxes"
],
"summary": "List sandboxes",
"description": "List the organization's sandboxes with cursor pagination and optional label filtering.",
"operationId": "list_sandboxes",
"parameters": [
{
"name": "cursor",
"in": "query",
"description": "Pagination cursor",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "Page size (default 20, max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "labels",
"in": "query",
"description": "JSON object of labels to AND-match",
"required": false,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Paginated list of sandboxes",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedSandboxResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"post": {
"tags": [
"Sandboxes"
],
"summary": "Create a sandbox",
"description": "Create a sandbox, optionally start it immediately, and optionally wait until it is ready.",
"operationId": "create_sandbox",
"parameters": [
{
"name": "start",
"in": "query",
"description": "If true, durably queue the sandbox for immediate orchestrator dispatch.",
"required": false,
"schema": {
"type": "boolean"
}
},
{
"name": "wait_for",
"in": "query",
"description": "Optional lifecycle state to wait for after durable start admission.",
"required": false,
"schema": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/SandboxWaitFor"
}
]
}
},
{
"name": "wait_timeout",
"in": "query",
"description": "Server-side wait budget in seconds; valid only with wait_for.",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateSandboxRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Sandbox created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SandboxResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"409": {
"description": "Name already exists",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "name_already_exists",
"message": "'name' already exists",
"details": null
}
}
}
}
},
"429": {
"description": "Readiness wait capacity exhausted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "rate_limited",
"message": "too many requests",
"details": null
}
}
}
}
},
"502": {
"description": "Orchestrator unreachable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/sandboxes/by-name/{name}": {
"get": {
"tags": [
"Sandboxes"
],
"summary": "Get a sandbox by name",
"description": "Retrieve one sandbox by its organization-scoped name.",
"operationId": "get_sandbox_by_name",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Sandbox name (unique within the org)",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Sandbox details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SandboxResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"delete": {
"tags": [
"Sandboxes"
],
"summary": "Delete a sandbox by name",
"description": "Delete a sandbox selected by its organization-scoped name. Resource cleanup continues asynchronously.",
"operationId": "delete_sandbox_by_name",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Sandbox name (unique within the org)",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Sandbox scheduled for deletion",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/sandboxes/by-name/{name}/start": {
"post": {
"tags": [
"Sandboxes"
],
"summary": "Start a sandbox by name",
"description": "Start a sandbox selected by its organization-scoped name.",
"operationId": "start_by_name",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Sandbox name (unique within the org)",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "wait_for",
"in": "query",
"description": "Optional lifecycle state to wait for after durable start admission.",
"required": false,
"schema": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/SandboxWaitFor"
}
]
}
},
{
"name": "wait_timeout",
"in": "query",
"description": "Server-side wait budget in seconds; valid only with wait_for.",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
}
],
"responses": {
"200": {
"description": "Sandbox start admitted or requested wait completed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SandboxResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"409": {
"description": "Invalid state transition",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_state_transition",
"message": "invalid state transition",
"details": null
}
}
}
}
},
"429": {
"description": "Readiness wait capacity exhausted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "rate_limited",
"message": "too many requests",
"details": null
}
}
}
}
},
"502": {
"description": "Orchestrator unreachable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/sandboxes/by-name/{name}/stop": {
"post": {
"tags": [
"Sandboxes"
],
"summary": "Stop a sandbox by name",
"description": "Stop a running sandbox selected by its organization-scoped name.",
"operationId": "stop_by_name",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Sandbox name (unique within the org)",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Sandbox stopping",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SandboxResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"409": {
"description": "Invalid state transition",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_state_transition",
"message": "invalid state transition",
"details": null
}
}
}
}
},
"502": {
"description": "Orchestrator unreachable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/sandboxes/{sandbox_id}": {
"get": {
"tags": [
"Sandboxes"
],
"summary": "Get a sandbox",
"description": "Retrieve one sandbox by its unique identifier.",
"operationId": "get_sandbox",
"parameters": [
{
"name": "sandbox_id",
"in": "path",
"description": "Sandbox ID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Sandbox details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SandboxResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"delete": {
"tags": [
"Sandboxes"
],
"summary": "Delete a sandbox",
"description": "Delete a sandbox. Resource cleanup continues asynchronously.",
"operationId": "delete_sandbox",
"parameters": [
{
"name": "sandbox_id",
"in": "path",
"description": "Sandbox ID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Sandbox scheduled for deletion",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"patch": {
"tags": [
"Sandboxes"
],
"summary": "Update a sandbox",
"description": "Rename a sandbox by updating its display name or slug.",
"operationId": "update_sandbox",
"parameters": [
{
"name": "sandbox_id",
"in": "path",
"description": "Sandbox ID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateSandboxRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Updated sandbox",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SandboxResponse"
}
}
}
},
"400": {
"description": "Invalid name/slug",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"409": {
"description": "Name or slug already taken",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "name_already_exists",
"message": "'name' already exists",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/sandboxes/{sandbox_id}/metrics": {
"get": {
"tags": [
"Sandboxes"
],
"summary": "Get sandbox metrics",
"operationId": "get_sandbox_metrics",
"parameters": [
{
"name": "sandbox_id",
"in": "path",
"description": "Sandbox ID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "range_secs",
"in": "query",
"description": "Look-back window in seconds (default 3600 = 1h, max 604800 = 7d).",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
},
{
"name": "step_secs",
"in": "query",
"description": "Sample resolution in seconds (default 30, min 5).",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
}
],
"responses": {
"200": {
"description": "Sandbox metrics",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SandboxMetricsResponse"
}
}
}
},
"400": {
"description": "Invalid range/step",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Metrics backend unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/sandboxes/{sandbox_id}/start": {
"post": {
"tags": [
"Sandboxes"
],
"summary": "Start a sandbox",
"description": "Start a sandbox and optionally wait until it is running.",
"operationId": "start_sandbox",
"parameters": [
{
"name": "sandbox_id",
"in": "path",
"description": "Sandbox ID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "wait_for",
"in": "query",
"description": "Optional lifecycle state to wait for after durable start admission.",
"required": false,
"schema": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/SandboxWaitFor"
}
]
}
},
{
"name": "wait_timeout",
"in": "query",
"description": "Server-side wait budget in seconds; valid only with wait_for.",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
}
],
"responses": {
"200": {
"description": "Sandbox start admitted or requested wait completed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SandboxResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"409": {
"description": "Invalid state transition",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_state_transition",
"message": "invalid state transition",
"details": null
}
}
}
}
},
"429": {
"description": "Readiness wait capacity exhausted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "rate_limited",
"message": "too many requests",
"details": null
}
}
}
}
},
"502": {
"description": "Orchestrator unreachable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/sandboxes/{sandbox_id}/stop": {
"post": {
"tags": [
"Sandboxes"
],
"summary": "Stop a sandbox",
"description": "Stop a running sandbox.",
"operationId": "stop_sandbox",
"parameters": [
{
"name": "sandbox_id",
"in": "path",
"description": "Sandbox ID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Sandbox stopping",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SandboxResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"409": {
"description": "Invalid state transition",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_state_transition",
"message": "invalid state transition",
"details": null
}
}
}
}
},
"502": {
"description": "Sandbox orchestrator unreachable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/sandboxes/{sandbox_id}/volumes": {
"get": {
"tags": [
"Sandboxes"
],
"summary": "List mounted volumes",
"operationId": "list_sandbox_volumes",
"parameters": [
{
"name": "sandbox_id",
"in": "path",
"description": "Sandbox ID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Mounted volumes",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SandboxVolumeMount"
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/sandboxes/snapshot-candidates": {
"get": {
"tags": [
"Snapshots"
],
"summary": "List snapshot candidates",
"description": "List persistent, stopped sandboxes supported by the current disk snapshot policy.",
"operationId": "list_snapshot_candidates",
"parameters": [
{
"name": "cursor",
"in": "query",
"description": "Pagination cursor",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "Page size (default 20, max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Paginated snapshot candidates",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedSnapshotCandidateResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/snapshot-operations": {
"get": {
"tags": [
"Snapshots"
],
"summary": "List snapshot operations",
"description": "List asynchronous snapshot operations requested by this organization.",
"operationId": "list_operations",
"parameters": [
{
"name": "cursor",
"in": "query",
"description": "Pagination cursor",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "Page size (default 20, max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "exclude_redundant",
"in": "query",
"description": "Hide successful managed operations and superseded host-volume operations represented in snapshot inventory",
"required": false,
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "Explicit snapshot operations",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedSnapshotOperationResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/snapshot-operations/{operation_id}": {
"get": {
"tags": [
"Snapshots"
],
"summary": "Get a snapshot operation",
"description": "Poll an asynchronous snapshot operation.",
"operationId": "get_operation",
"parameters": [
{
"name": "operation_id",
"in": "path",
"description": "Snapshot operation id",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Snapshot operation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CloudSnapshotOperation"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Snapshot operation not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/snapshots": {
"get": {
"tags": [
"Snapshots"
],
"summary": "List snapshots",
"description": "List the organization's managed disk snapshots.",
"operationId": "list_snapshots",
"parameters": [
{
"name": "cursor",
"in": "query",
"description": "Pagination cursor",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "Page size (default 20, max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Managed snapshots",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedSnapshotResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"post": {
"tags": [
"Snapshots"
],
"summary": "Create a snapshot",
"description": "Create a managed or host-volume disk snapshot asynchronously.",
"operationId": "create",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CloudCreateSnapshotRequest"
}
}
},
"required": true
},
"responses": {
"202": {
"description": "Snapshot capture queued",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CloudSnapshotOperation"
}
}
}
},
"400": {
"description": "Invalid snapshot request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Source sandbox not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"409": {
"description": "Snapshot destination already exists",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "name_already_exists",
"message": "'name' already exists",
"details": null
}
}
}
}
},
"422": {
"description": "Unsupported or ineligible snapshot request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/snapshots/by-name/{name}": {
"get": {
"tags": [
"Snapshots"
],
"summary": "Get a snapshot by name",
"description": "Retrieve a managed disk snapshot by its organization-unique name.",
"operationId": "get_snapshot_by_name",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Managed snapshot name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Managed snapshot",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CloudSnapshot"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Snapshot not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"delete": {
"tags": [
"Snapshots"
],
"summary": "Delete a snapshot by name",
"description": "Delete a managed disk snapshot by its organization-unique name.",
"operationId": "delete_snapshot_by_name",
"parameters": [
{
"name": "name",
"in": "path",
"description": "Managed snapshot name",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Snapshot deleted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Snapshot not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/snapshots/{snapshot_id}": {
"get": {
"tags": [
"Snapshots"
],
"summary": "Get a snapshot",
"description": "Retrieve a managed disk snapshot by its immutable identifier.",
"operationId": "get",
"parameters": [
{
"name": "snapshot_id",
"in": "path",
"description": "Managed snapshot id",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Managed snapshot",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CloudSnapshot"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Snapshot not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"delete": {
"tags": [
"Snapshots"
],
"summary": "Delete a snapshot",
"description": "Delete a managed disk snapshot by its immutable identifier.",
"operationId": "delete",
"parameters": [
{
"name": "snapshot_id",
"in": "path",
"description": "Managed snapshot id",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Snapshot deleted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Snapshot not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes": {
"get": {
"tags": [
"Volumes"
],
"summary": "List volumes",
"description": "List persistent volumes available to the organization, including current usage.",
"operationId": "list_volumes",
"responses": {
"200": {
"description": "The org's volumes",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/VolumeResponse"
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"post": {
"tags": [
"Volumes"
],
"summary": "Create a volume",
"description": "Create a named persistent volume with optional capacity and labels.",
"operationId": "create_volume",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateVolumeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Volume created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"409": {
"description": "Volume name already in use",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "name_already_exists",
"message": "'name' already exists",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/default": {
"get": {
"tags": [
"Volumes"
],
"summary": "Get the default volume",
"operationId": "get_default",
"responses": {
"200": {
"description": "Get the default volume",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/{id}": {
"delete": {
"tags": [
"Volumes"
],
"summary": "Delete a volume",
"description": "Schedule an unused named volume for deletion.",
"operationId": "delete_volume",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume id",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Volume scheduled for deletion",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/MessageResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Volume not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"409": {
"description": "The default volume cannot be deleted",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "the host volume cannot be deleted",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"patch": {
"tags": [
"Volumes"
],
"summary": "Update a volume",
"description": "Update a named volume's capacity limit or labels.",
"operationId": "update_volume",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume id",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateVolumeRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Volume updated",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeResponse"
}
}
}
},
"400": {
"description": "Invalid request (e.g. cap exceeds the org cap)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Volume not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"409": {
"description": "The host volume cannot be capped",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "the host volume cannot be capped",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/{id}/files": {
"get": {
"tags": [
"Volumes"
],
"summary": "List directory contents",
"operationId": "list_directory_contents",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume UUID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "x-msb-file-path",
"in": "header",
"description": "URL-safe base64 encoded volume-relative UTF-8 path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "List directory contents",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeFileListResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Volume or path not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Volume filesystem unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"delete": {
"tags": [
"Volumes"
],
"summary": "Delete a file or directory",
"operationId": "remove",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume UUID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "x-msb-file-path",
"in": "header",
"description": "URL-safe base64 encoded volume-relative UTF-8 path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "x-msb-file-recursive",
"in": "header",
"description": "Recursively remove a directory; defaults to false",
"required": false,
"schema": {
"type": [
"boolean",
"null"
]
}
}
],
"responses": {
"200": {
"description": "Delete a file or directory",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeFileMutationResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Volume or path not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Volume filesystem unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/{id}/files/content": {
"get": {
"tags": [
"Volumes"
],
"summary": "Read a file",
"operationId": "read",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume UUID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "x-msb-file-path",
"in": "header",
"description": "URL-safe base64 encoded volume-relative UTF-8 path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Read a file",
"content": {
"application/octet-stream": {
"schema": {
"$ref": "#/components/schemas/VolumeFileContent"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Volume or path not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Volume filesystem unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
},
"put": {
"tags": [
"Volumes"
],
"summary": "Write a file",
"operationId": "write",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume UUID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "x-msb-file-path",
"in": "header",
"description": "URL-safe base64 encoded volume-relative UTF-8 path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/octet-stream": {
"schema": {
"$ref": "#/components/schemas/VolumeFileContent"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Write a file",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeFileMutationResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Volume or path not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Volume filesystem unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/{id}/files/copy": {
"post": {
"tags": [
"Volumes"
],
"summary": "Copy a file or directory",
"operationId": "copy",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume UUID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileMoveRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Copy a file or directory",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeFileMutationResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Volume or path not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Volume filesystem unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/{id}/files/exists": {
"get": {
"tags": [
"Volumes"
],
"summary": "Check if a file exists",
"operationId": "exists",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume UUID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "x-msb-file-path",
"in": "header",
"description": "URL-safe base64 encoded volume-relative UTF-8 path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Check if a file exists",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeFileExistsResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Volume or path not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Volume filesystem unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/{id}/files/mkdir": {
"post": {
"tags": [
"Volumes"
],
"summary": "Create a directory",
"operationId": "mkdir",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume UUID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FilePathRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Create a directory",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeFileMutationResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Volume or path not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Volume filesystem unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/{id}/files/rename": {
"post": {
"tags": [
"Volumes"
],
"summary": "Move a file or directory",
"operationId": "rename",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume UUID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileMoveRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Move a file or directory",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeFileMutationResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Volume or path not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Volume filesystem unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/{id}/files/stat": {
"get": {
"tags": [
"Volumes"
],
"summary": "Get file details",
"operationId": "stat",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume UUID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "x-msb-file-path",
"in": "header",
"description": "URL-safe base64 encoded volume-relative UTF-8 path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Get file details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeFileStatResponse"
}
}
}
},
"400": {
"description": "Invalid request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"403": {
"description": "Insufficient permissions",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"404": {
"description": "Volume or path not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Volume filesystem unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/volumes/{id}/metrics": {
"get": {
"tags": [
"Volumes"
],
"summary": "Get volume metrics",
"operationId": "get_volume_metrics",
"parameters": [
{
"name": "id",
"in": "path",
"description": "Volume id",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
},
{
"name": "range_secs",
"in": "query",
"description": "Look-back window in seconds (default 3600 = 1h, max 604800 = 7d).",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
},
{
"name": "step_secs",
"in": "query",
"description": "Sample resolution in seconds (default 30, min 5).",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64",
"minimum": 0
}
}
],
"responses": {
"200": {
"description": "Volume performance metrics",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/VolumeMetricsResponse"
}
}
}
},
"400": {
"description": "Invalid time range or resolution",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_request",
"message": "invalid request",
"details": null
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Volume not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
},
"502": {
"description": "Metrics backend unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "orchestrator_unreachable",
"message": "orchestrator unreachable",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/quotas": {
"get": {
"tags": [
"Quotas"
],
"summary": "Get quota usage",
"description": "Return current resource usage, plan limits, and organization-specific quota overrides.",
"operationId": "get_quotas",
"responses": {
"200": {
"description": "Quota usage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/QuotaUsage"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Org not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "org_not_found",
"message": "org not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/billing": {
"get": {
"tags": [
"Billing"
],
"summary": "Get billing summary",
"operationId": "get_billing",
"responses": {
"200": {
"description": "Billing summary",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BillingSummaryResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/billing/invoices": {
"get": {
"tags": [
"Billing"
],
"summary": "List invoices",
"operationId": "list_invoices",
"responses": {
"200": {
"description": "Invoice list",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Invoice"
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/billing/invoices/{invoice_id}": {
"get": {
"tags": [
"Billing"
],
"summary": "Get an invoice",
"operationId": "get_invoice",
"parameters": [
{
"name": "invoice_id",
"in": "path",
"description": "Invoice ID",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Invoice detail",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Invoice"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Invoice not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/billing/plans": {
"get": {
"tags": [
"Billing"
],
"summary": "List billing plans",
"operationId": "list_plans",
"responses": {
"200": {
"description": "Available plans",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/BillingPlan"
}
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/billing/usage": {
"get": {
"tags": [
"Billing"
],
"summary": "Get a usage summary",
"description": "Return billable usage for the organization.",
"operationId": "get_usage",
"responses": {
"200": {
"description": "Usage summary",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UsageSummary"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/billing/usage/sandbox": {
"get": {
"tags": [
"Billing"
],
"summary": "Get per-sandbox usage",
"description": "Return billable usage grouped by sandbox for a selected UTC time range.",
"operationId": "get_sandbox_breakdown",
"responses": {
"200": {
"description": "Per-sandbox / per-day usage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UsageBreakdown"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/billing/usage/storage": {
"get": {
"tags": [
"Billing"
],
"summary": "Get per-volume storage usage",
"description": "Return persistent storage usage grouped by volume for a selected UTC time range.",
"operationId": "get_storage_breakdown",
"responses": {
"200": {
"description": "Per-volume / per-day storage usage",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StorageBreakdown"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/events": {
"get": {
"tags": [
"Audit events"
],
"summary": "List audit events",
"description": "List audit events for the organization with cursor pagination and optional filters.",
"operationId": "list_events",
"parameters": [
{
"name": "event_type",
"in": "query",
"description": "Filter by event type (e.g. "api_key_created").",
"required": false,
"schema": {
"type": [
"string",
"null"
]
}
},
{
"name": "since",
"in": "query",
"description": "Only return events at or after this timestamp.",
"required": false,
"schema": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
{
"name": "until",
"in": "query",
"description": "Only return events at or before this timestamp.",
"required": false,
"schema": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
{
"name": "cursor",
"in": "query",
"description": "Opaque cursor from a previous response.",
"required": false,
"schema": {
"type": [
"string",
"null"
]
}
},
{
"name": "limit",
"in": "query",
"description": "Number of items to return (default: 20, max: 100).",
"required": false,
"schema": {
"type": [
"integer",
"null"
],
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Paginated list of audit events",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedEventResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/events/{event_id}": {
"get": {
"tags": [
"Audit events"
],
"summary": "Get an audit event",
"description": "Retrieve one audit event by its identifier.",
"operationId": "get_event",
"parameters": [
{
"name": "event_id",
"in": "path",
"description": "Event ID",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Event details",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EventResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
},
"404": {
"description": "Event not found",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "sandbox_not_found",
"message": "resource not found",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/me/org": {
"get": {
"tags": [
"Organization"
],
"summary": "Get the current organization",
"description": "Returns the organization associated with the current credential.",
"operationId": "get_org",
"responses": {
"200": {
"description": "Current organization",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OrgWithRole"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
},
"/v1/members": {
"get": {
"tags": [
"Members"
],
"summary": "List organization members",
"description": "List organization members and their roles with cursor pagination.",
"operationId": "list_members",
"parameters": [
{
"name": "cursor",
"in": "query",
"description": "Pagination cursor",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "limit",
"in": "query",
"description": "Page size (default 20, max 100)",
"required": false,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Paginated list of members",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PaginatedMemberResponse"
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
},
"example": {
"error": {
"code": "invalid_api_key",
"message": "unauthorized",
"details": null
}
}
}
}
}
},
"security": [
{
"api_key": []
}
]
}
}
},
"components": {
"schemas": {
"Action": {
"type": "string",
"description": "Action to take on traffic matched by a [Rule] (or a policy default).",
"enum": [
"allow",
"deny"
]
},
"BillingEffectiveState": {
"type": "string",
"description": "Effective billing state derived from plan, period end, and suspension.",
"enum": [
"free",
"active",
"past_due",
"suspended"
]
},
"BillingPlan": {
"type": "object",
"description": "Billing plan definition with resource quotas.\n\nname is the plan's identity, an arbitrary catalog string (no fixed enum) -\n"free"/"builder"/… are just the names we ship. Every quota is Option:\nNone = unlimited (counts/budgets) or the platform default (per-sandbox\nvCPU/memory/disk). There is no magic 0.",
"required": [
"id",
"name",
"display_name",
"self_serve",
"is_default",
"created_at",
"updated_at"
],
"properties": {
"created_at": {
"type": "string",
"format": "date-time"
},
"description": {
"type": [
"string",
"null"
]
},
"display_name": {
"type": "string"
},
"id": {
"type": "string",
"format": "uuid"
},
"included_disk_gib_hours": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"included_memory_gib_hours": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"included_storage_gib": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"included_vcpu_hours": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Monthly included allowances bundled into the flat fee (pool of vCPU-hours /\nmemory GiB-hours / writable-disk GiB-hours / durable-storage GiB). Usage\nbeyond the pool is metered overage on priced plans; on an unpriced (free)\nplan the compute pool is a hard wall."
},
"is_default": {
"type": "boolean",
"description": "The plan new orgs are assigned. Exactly one row is the default."
},
"max_concurrent_ephemeral_running": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Max simultaneously-running ephemeral sandboxes (cheap, self-cleaning)."
},
"max_concurrent_persistent_running": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Max simultaneously-running persistent sandboxes (snapshot/restore cost)."
},
"max_disk_size_mib_per_sandbox": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Maximum writable disk size (MiB) per sandbox."
},
"max_memory_mib_per_sandbox": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"max_sandboxes": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"max_snapshot_storage_gib": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Per-org managed-snapshot storage hard cap (GiB), compared against the\nsummed compressed archive size of the org's managed snapshots."
},
"max_total_storage_gib": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Per-org durable-storage hard cap (GiB). Used storage is billed at a flat\nper-GB-month rate (no free allotment)."
},
"max_uptime_minutes_per_sandbox": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"max_vcpus_per_sandbox": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"monthly_sandbox_minutes": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Deprecated wall-clock minutes budget (NULL). The compute wall is now\nincluded_vcpu_hours on unpriced plans."
},
"name": {
"type": "string"
},
"self_serve": {
"type": "boolean",
"description": "May a user self-subscribe to this plan via the API? Fail-closed; the\nchange-plan check keys on this flag, not the plan name."
},
"subscription_price_cents": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Flat monthly subscription price (cents); None = unpriced (free). Source\nof upgrade/downgrade ordering - compare prices, not a fixed tier rank."
},
"trial_duration_days": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"updated_at": {
"type": "string",
"format": "date-time"
},
"usage_disk_gib_hour_usd": {
"type": [
"number",
"null"
],
"format": "double"
},
"usage_memory_gib_hour_usd": {
"type": [
"number",
"null"
],
"format": "double"
},
"usage_snapshot_storage_gib_month_usd": {
"type": [
"number",
"null"
],
"format": "double",
"description": "Managed-snapshot storage rate (USD/GiB-month), separate from volumes."
},
"usage_storage_gib_month_usd": {
"type": [
"number",
"null"
],
"format": "double"
},
"usage_vcpu_hour_usd": {
"type": [
"number",
"null"
],
"format": "double",
"description": "Per-unit overage rates (USD) charged on usage beyond the included pool;\nNone on the free/unpriced plan (no overage path). Drive the live cost\nestimate subscription + Σ overage; Autumn's invoice stays authoritative."
}
}
},
"BillingPlanSummary": {
"type": "object",
"description": "Summary of a billing plan (subset of full plan).",
"required": [
"name",
"display_name",
"has_custom_billing_terms"
],
"properties": {
"display_name": {
"type": "string"
},
"has_custom_billing_terms": {
"type": "boolean"
},
"name": {
"type": "string"
}
}
},
"BillingPricingTerms": {
"type": "object",
"description": "Pricing fields which may be inherited from a plan or negotiated per customer.",
"properties": {
"included_disk_gib_hours": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"included_memory_gib_hours": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"included_storage_gib": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"included_vcpu_hours": {
"type": [
"integer",
"null"
],
"format": "int32"
},
"subscription_price_cents": {
"type": [
"integer",
"null"
],
"format": "int64"
},
"usage_disk_gib_hour_usd": {
"type": [
"number",
"null"
],
"format": "double"
},
"usage_memory_gib_hour_usd": {
"type": [
"number",
"null"
],
"format": "double"
},
"usage_snapshot_storage_gib_month_usd": {
"type": [
"number",
"null"
],
"format": "double"
},
"usage_storage_gib_month_usd": {
"type": [
"number",
"null"
],
"format": "double"
},
"usage_vcpu_hour_usd": {
"type": [
"number",
"null"
],
"format": "double"
}
}
},
"BillingSummaryResponse": {
"type": "object",
"description": "Billing summary response for an organization.",
"required": [
"plan",
"effective_pricing",
"effective_state",
"is_suspended"
],
"properties": {
"available_credit_cents": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Positive USD credit Stripe will apply automatically to future invoices.",
"minimum": 0
},
"billing_terms_override": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/BillingTermsOverrideSummary",
"description": "Current agreement metadata, including setup and payment-pending states."
}
]
},
"checkout_url": {
"type": [
"string",
"null"
],
"description": "Hosted-checkout URL to complete payment for a just-changed plan (Autumn).\nSet only on a plan change that needs payment; the client redirects to it."
},
"current_period_ends_at": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"effective_pricing": {
"$ref": "#/components/schemas/BillingPricingTerms",
"description": "Effective commercial terms after applying any org-specific agreement."
},
"effective_state": {
"$ref": "#/components/schemas/BillingEffectiveState"
},
"is_suspended": {
"type": "boolean"
},
"pending_plan": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/BillingPlanSummary",
"description": "Plan the org will switch to at current_period_ends_at, if a downgrade is scheduled."
}
]
},
"plan": {
"$ref": "#/components/schemas/BillingPlanSummary"
}
}
},
"BillingTermsOverrideSummary": {
"type": "object",
"description": "Effective window for the org's active negotiated billing terms.",
"required": [
"status",
"plan",
"effective_from"
],
"properties": {
"effective_from": {
"type": "string",
"format": "date-time"
},
"effective_until": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"plan": {
"$ref": "#/components/schemas/BillingPlanSummary"
},
"post_expiry_plan": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/BillingPlanSummary",
"description": "Explicit plan assigned at expiry; absent means target-plan retail pricing."
}
]
},
"status": {
"$ref": "#/components/schemas/OrgBillingTermsOverrideStatus"
}
}
},
"CertCacheConfig": {
"type": "object",
"description": "Per-domain certificate cache configuration.",
"properties": {
"capacity": {
"type": "integer",
"description": "Maximum number of cached certificates. Default: 1000.",
"minimum": 0
},
"validity_hours": {
"type": "integer",
"format": "int64",
"description": "Certificate validity duration in hours. Default: 24.",
"minimum": 0
}
}
},
"CheckpointSnapshotState": {
"type": "object",
"description": "Immutable checkpoint-manifest-backed snapshot state.",
"required": [
"checkpoint_id",
"manifest"
],
"properties": {
"checkpoint_id": {
"type": "string",
"description": "Stable identifier for the captured cut."
},
"manifest": {
"type": "string",
"description": "SHA-256 identity of the disk or composite checkpoint manifest."
}
},
"additionalProperties": false
},
"CloudCreateSandboxRequest": {
"oneOf": [
{
"allOf": [
{
"$ref": "#/components/schemas/CloudSandboxSpec",
"description": "Settings shared by every sandbox source."
},
{
"type": "object",
"required": [
"reference"
],
"properties": {
"patches": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CloudPatch"
},
"description": "Rootfs patches applied before VM start."
},
"pull_policy": {
"$ref": "#/components/schemas/CloudPullPolicy",
"description": "OCI image pull policy."
},
"reference": {
"type": "string",
"description": "OCI image reference."
},
"resources": {
"$ref": "#/components/schemas/CloudSandboxResources",
"description": "CPU, memory, and writable-disk resources."
}
}
},
{
"type": "object",
"description": "Create a sandbox from an OCI image.",
"required": [
"source"
],
"properties": {
"source": {
"type": "string",
"enum": [
"oci"
]
}
}
}
],
"description": "Create a sandbox from an OCI image."
},
{
"allOf": [
{
"$ref": "#/components/schemas/CloudSandboxSpec",
"description": "Settings shared by every sandbox source."
},
{
"type": "object",
"required": [
"path"
],
"properties": {
"patches": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CloudPatch"
},
"description": "Rootfs patches applied before VM start."
},
"path": {
"type": "string",
"description": "Host directory used as the root filesystem."
},
"resources": {
"$ref": "#/components/schemas/CloudSandboxComputeResources",
"description": "CPU and memory resources."
}
}
},
{
"type": "object",
"description": "Create a sandbox from a host directory.",
"required": [
"source"
],
"properties": {
"source": {
"type": "string",
"enum": [
"bind"
]
}
}
}
],
"description": "Create a sandbox from a host directory."
},
{
"allOf": [
{
"$ref": "#/components/schemas/CloudSandboxSpec",
"description": "Settings shared by every sandbox source."
},
{
"type": "object",
"required": [
"path",
"format"
],
"properties": {
"format": {
"$ref": "#/components/schemas/CloudDiskImageFormat",
"description": "Disk image format."
},
"fstype": {
"type": [
"string",
"null"
],
"description": "Inner filesystem type, when it cannot be detected automatically."
},
"patches": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CloudPatch"
},
"description": "Rootfs patches applied before VM start."
},
"path": {
"type": "string",
"description": "Host path to the disk image."
},
"resources": {
"$ref": "#/components/schemas/CloudSandboxComputeResources",
"description": "CPU and memory resources."
}
}
},
{
"type": "object",
"description": "Create a sandbox from a disk image file.",
"required": [
"source"
],
"properties": {
"source": {
"type": "string",
"enum": [
"disk_image"
]
}
}
}
],
"description": "Create a sandbox from a disk image file."
},
{
"allOf": [
{
"$ref": "#/components/schemas/CloudSandboxSpec",
"description": "Settings shared by every sandbox source."
},
{
"type": "object",
"required": [
"disk_snapshot_ref"
],
"properties": {
"disk_snapshot_ref": {
"$ref": "#/components/schemas/CloudSnapshotLocation",
"description": "Disk snapshot to restore."
},
"pull_policy": {
"$ref": "#/components/schemas/CloudPullPolicy",
"description": "Pull policy used if the snapshot's pinned base image must be fetched."
},
"resources": {
"$ref": "#/components/schemas/CloudSandboxComputeResources",
"description": "CPU and memory resources."
}
}
},
{
"type": "object",
"description": "Create a fresh-booted sandbox from a disk snapshot.",
"required": [
"source"
],
"properties": {
"source": {
"type": "string",
"enum": [
"disk_snapshot"
]
}
}
}
],
"description": "Create a fresh-booted sandbox from a disk snapshot."
}
],
"description": "Wire shape of a cloud sandbox create request body.\n\nEach root filesystem origin is a distinct source variant. The common\nsandbox settings remain flat beside the source-specific fields. Legacy\nrequests carrying an image object are accepted during migration."
},
"CloudCreateSnapshotRequest": {
"oneOf": [
{
"allOf": [
{
"$ref": "#/components/schemas/CloudSnapshotSpec",
"description": "Settings shared by every snapshot kind."
},
{
"type": "object",
"description": "Capture disk state only.",
"required": [
"kind"
],
"properties": {
"kind": {
"type": "string",
"enum": [
"disk"
]
}
}
}
],
"description": "Capture disk state only."
}
],
"description": "Wire shape of a cloud snapshot create request body."
},
"CloudDiskImageFormat": {
"type": "string",
"description": "Disk image format for cloud disk-image sources. Twin of [DiskImageFormat]\nwith a snake_case wire.",
"enum": [
"qcow2",
"raw",
"vmdk"
]
},
"CloudErrorDetails": {
"type": "object",
"description": "Nested cloud API error details.",
"properties": {
"code": {
"type": [
"string",
"null"
],
"description": "Machine-readable error code."
},
"message": {
"type": [
"string",
"null"
],
"description": "Human-readable error message."
}
}
},
"CloudHostPattern": {
"oneOf": [
{
"type": "object",
"description": "Exact hostname match.",
"required": [
"value",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"exact"
]
},
"value": {
"type": "string",
"description": "Hostname to match exactly."
}
}
},
{
"type": "object",
"description": "Wildcard match (e.g. *.openai.com).",
"required": [
"value",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"wildcard"
]
},
"value": {
"type": "string",
"description": "Wildcard pattern."
}
}
},
{
"type": "object",
"description": "Any host (dangerous - the secret can be exfiltrated).",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"any"
]
}
}
}
],
"description": "Host allowlist pattern for cloud secrets. Twin of [HostPattern], with the\ndomain's scalar variants normalized to { value } for a uniform union."
},
"CloudNetworkSpec": {
"type": "object",
"description": "Cloud network specification: a subset of the domain [crate::domain::NetworkSpec].\nInterface overrides, host port mapping, DNS, TLS interception, rate limits,\nand host-CA trust are not part of this type. Unknown fields are ignored for\ncompatibility with newer clients; accepting them does not enable their behavior.",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether networking is enabled for this sandbox.",
"default": true
},
"max_connections": {
"type": [
"integer",
"null"
],
"description": "Max concurrent guest connections.",
"default": null,
"minimum": 0
},
"policy": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/NetworkPolicy",
"description": "Egress/ingress policy."
}
],
"default": null
},
"secrets": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/CloudSecretsConfig",
"description": "Secret-injection config."
}
],
"default": null
},
"strict": {
"type": "boolean",
"description": "Require hostname-based policy allows to use inspectable application authority.",
"default": true
}
}
},
"CloudPatch": {
"oneOf": [
{
"type": "object",
"description": "Write text content to a file.",
"required": [
"path",
"content",
"replace",
"type"
],
"properties": {
"content": {
"type": "string",
"description": "Text content to write."
},
"mode": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "File permissions, such as 0o644. None uses the default.",
"minimum": 0
},
"path": {
"type": "string",
"description": "Absolute guest path, such as /etc/app.conf."
},
"replace": {
"type": "boolean",
"description": "Allow replacing a file that already exists in the rootfs."
},
"type": {
"type": "string",
"enum": [
"text"
]
}
}
},
{
"type": "object",
"description": "Write raw bytes to a file.",
"required": [
"path",
"content",
"replace",
"type"
],
"properties": {
"content": {
"type": "array",
"items": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"description": "Raw byte content to write."
},
"mode": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "File permissions, such as 0o644. None uses the default.",
"minimum": 0
},
"path": {
"type": "string",
"description": "Absolute guest path."
},
"replace": {
"type": "boolean",
"description": "Allow replacing a file that already exists in the rootfs."
},
"type": {
"type": "string",
"enum": [
"file"
]
}
}
},
{
"type": "object",
"description": "Copy a file from the host into the rootfs.",
"required": [
"src",
"dst",
"replace",
"type"
],
"properties": {
"dst": {
"type": "string",
"description": "Absolute guest destination path."
},
"mode": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "File permissions. None preserves source permissions.",
"minimum": 0
},
"replace": {
"type": "boolean",
"description": "Allow replacing a file that already exists in the rootfs."
},
"src": {
"type": "string",
"description": "Host path to copy from."
},
"type": {
"type": "string",
"enum": [
"copy_file"
]
}
}
},
{
"type": "object",
"description": "Copy a directory from the host into the rootfs.",
"required": [
"src",
"dst",
"replace",
"type"
],
"properties": {
"dst": {
"type": "string",
"description": "Absolute guest destination path."
},
"replace": {
"type": "boolean",
"description": "Allow replacing files that already exist in the rootfs."
},
"src": {
"type": "string",
"description": "Host directory to copy from."
},
"type": {
"type": "string",
"enum": [
"copy_dir"
]
}
}
},
{
"type": "object",
"description": "Create a symlink.",
"required": [
"target",
"link",
"replace",
"type"
],
"properties": {
"link": {
"type": "string",
"description": "Absolute guest path where the symlink is created."
},
"replace": {
"type": "boolean",
"description": "Allow replacing a path that already exists in the rootfs."
},
"target": {
"type": "string",
"description": "Symlink target path."
},
"type": {
"type": "string",
"enum": [
"symlink"
]
}
}
},
{
"type": "object",
"description": "Create a directory.",
"required": [
"path",
"type"
],
"properties": {
"mode": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Directory permissions, such as 0o755. None uses the default.",
"minimum": 0
},
"path": {
"type": "string",
"description": "Absolute guest path."
},
"type": {
"type": "string",
"enum": [
"mkdir"
]
}
}
},
{
"type": "object",
"description": "Remove a file or directory.",
"required": [
"path",
"type"
],
"properties": {
"path": {
"type": "string",
"description": "Absolute guest path to remove."
},
"type": {
"type": "string",
"enum": [
"remove"
]
}
}
},
{
"type": "object",
"description": "Append content to an existing file.",
"required": [
"path",
"content",
"type"
],
"properties": {
"content": {
"type": "string",
"description": "Content to append."
},
"path": {
"type": "string",
"description": "Absolute guest path of the file to append to."
},
"type": {
"type": "string",
"enum": [
"append"
]
}
}
}
],
"description": "Filesystem change applied before the sandbox starts. The type field selects the operation."
},
"CloudPullPolicy": {
"type": "string",
"description": "Cloud pull policy. Twin of domain [PullPolicy] with a snake_case wire.",
"enum": [
"if_missing",
"always",
"never"
]
},
"CloudRlimit": {
"type": "object",
"description": "A POSIX resource limit. Twin of [Rlimit] using [CloudRlimitResource].",
"required": [
"resource",
"soft",
"hard"
],
"properties": {
"hard": {
"type": "integer",
"format": "int64",
"description": "Hard limit (ceiling, requires privileges to raise).",
"minimum": 0
},
"resource": {
"$ref": "#/components/schemas/CloudRlimitResource",
"description": "Resource type."
},
"soft": {
"type": "integer",
"format": "int64",
"description": "Soft limit (can be raised up to the hard limit by the process).",
"minimum": 0
}
}
},
"CloudRlimitResource": {
"type": "string",
"description": "POSIX resource-limit identifiers. Twin of [RlimitResource] with a\nsnake_case wire.",
"enum": [
"cpu",
"fsize",
"data",
"stack",
"core",
"rss",
"nproc",
"nofile",
"memlock",
"as",
"locks",
"sigpending",
"msgqueue",
"nice",
"rtprio",
"rttime"
]
},
"CloudSandboxComputeResources": {
"type": "object",
"description": "CPU and memory request shared by sources without a managed writable disk.",
"properties": {
"memory_mib": {
"type": "integer",
"format": "int32",
"description": "Guest memory in MiB.",
"default": 512,
"minimum": 0
},
"vcpus": {
"type": "integer",
"format": "int32",
"description": "Number of virtual CPUs.",
"default": 1,
"minimum": 0
}
}
},
"CloudSandboxResources": {
"type": "object",
"description": "Cloud resource request.",
"properties": {
"disk_size_mib": {
"type": [
"integer",
"null"
],
"format": "int32",
"description": "Writable disk size in MiB. Applies only to OCI root filesystems.",
"default": null,
"minimum": 0
},
"memory_mib": {
"type": "integer",
"format": "int32",
"description": "Guest memory in MiB.",
"default": 512,
"minimum": 0
},
"vcpus": {
"type": "integer",
"format": "int32",
"description": "Number of virtual CPUs.",
"default": 1,
"minimum": 0
}
}
},
"CloudSandboxRuntimeOptions": {
"type": "object",
"description": "Cloud guest runtime options: a subset of [crate::domain::SandboxRuntimeOptions]. The\nhostname and the metrics-sampling knobs are not part of this type.\nUnknown fields are ignored for compatibility with newer clients.",
"properties": {
"cmd": {
"type": [
"array",
"null"
],
"items": {
"type": "string"
},
"description": "Command override.",
"default": null
},
"entrypoint": {
"type": [
"array",
"null"
],
"items": {
"type": "string"
},
"description": "Entrypoint override.",
"default": null
},
"log_level": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/SandboxLogLevel",
"description": "Runtime log level."
}
],
"default": null
},
"scripts": {
"type": "object",
"description": "Named in-guest scripts.",
"default": {},
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"type": "string"
}
},
"shell": {
"type": [
"string",
"null"
],
"description": "Default shell.",
"default": null
},
"user": {
"type": [
"string",
"null"
],
"description": "Guest user.",
"default": null
},
"workdir": {
"type": [
"string",
"null"
],
"description": "Working directory for guest commands.",
"default": null
}
}
},
"CloudSandboxSpec": {
"type": "object",
"description": "Settings shared by every cloud sandbox creation source.",
"required": [
"name"
],
"properties": {
"env": {
"type": "array",
"items": {
"$ref": "#/components/schemas/EnvVar"
},
"description": "Environment variables visible to commands in the sandbox.",
"default": []
},
"init": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/HandoffInit",
"description": "Hand off PID 1 to a guest init binary after agentd setup."
}
],
"default": null
},
"labels": {
"type": "object",
"description": "User-defined labels attached to the sandbox.",
"default": {},
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"type": "string"
}
},
"lifecycle": {
"oneOf": [
{
"$ref": "#/components/schemas/SandboxPolicy",
"description": "Sandbox lifecycle policy."
}
],
"default": {
"ephemeral": false,
"idle_timeout_secs": null,
"max_duration_secs": null
}
},
"mounts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CloudVolumeMount"
},
"description": "Volume mounts.",
"default": []
},
"name": {
"type": "string",
"description": "Unique sandbox name.",
"default": ""
},
"network": {
"oneOf": [
{
"$ref": "#/components/schemas/CloudNetworkSpec",
"description": "Network specification."
}
],
"default": {
"enabled": true,
"strict": true
}
},
"rlimits": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CloudRlimit"
},
"description": "Sandbox-wide resource limits inherited by guest processes.",
"default": []
},
"runtime": {
"oneOf": [
{
"$ref": "#/components/schemas/CloudSandboxRuntimeOptions",
"description": "Guest runtime options."
}
],
"default": {
"cmd": null,
"entrypoint": null,
"log_level": null,
"scripts": {},
"shell": null,
"user": null,
"workdir": null
}
},
"security_profile": {
"oneOf": [
{
"$ref": "#/components/schemas/SecurityProfile",
"description": "In-guest security profile."
}
],
"default": "default"
}
}
},
"CloudSecretEntry": {
"type": "object",
"description": "A single cloud secret entry. Twin of domain [SecretEntry].",
"required": [
"env_var",
"placeholder"
],
"properties": {
"allowed_hosts": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CloudHostPattern"
},
"description": "Hosts allowed to receive this secret."
},
"env_var": {
"type": "string",
"description": "Environment variable name exposed to the sandbox."
},
"injection": {
"$ref": "#/components/schemas/SecretInjection",
"description": "Where the secret may be injected."
},
"on_violation": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/CloudViolationAction",
"description": "Per-secret violation action overriding the config default."
}
]
},
"placeholder": {
"type": "string",
"description": "Placeholder the sandbox sees instead of the real value."
},
"require_tls_identity": {
"type": "boolean",
"description": "Require verified TLS identity before substituting (default: true)."
},
"source": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/CloudSecretSource",
"description": "Host-side source resolved into value at spawn time."
}
]
},
"value": {
"type": "string",
"description": "The secret value (empty when source carries a reference instead)."
}
}
},
"CloudSecretSource": {
"oneOf": [
{
"type": "object",
"description": "Read from a host environment variable at apply time.",
"required": [
"var",
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"env"
]
},
"var": {
"type": "string",
"description": "Host environment variable name."
}
}
},
{
"type": "object",
"description": "Read from a host-side secret store reference.",
"required": [
"reference",
"type"
],
"properties": {
"reference": {
"type": "string",
"description": "Store-specific secret reference."
},
"type": {
"type": "string",
"enum": [
"store"
]
}
}
}
],
"description": "Host-side source for a cloud secret. Twin of [SecretSource]."
},
"CloudSecretsConfig": {
"type": "object",
"description": "Secret-injection config for the cloud API. Twin of domain [SecretsConfig].",
"properties": {
"entries": {
"type": "array",
"items": {
"$ref": "#/components/schemas/CloudSecretEntry"
},
"description": "Secrets to inject."
},
"on_violation": {
"$ref": "#/components/schemas/CloudViolationAction",
"description": "Default action when a placeholder leaks to a disallowed host."
}
}
},
"CloudSnapshot": {
"oneOf": [
{
"allOf": [
{
"$ref": "#/components/schemas/CloudSnapshotDetails",
"description": "Fields shared by every snapshot kind."
},
{
"type": "object",
"description": "A disk-only snapshot.",
"required": [
"kind"
],
"properties": {
"kind": {
"type": "string",
"enum": [
"disk"
]
}
}
}
],
"description": "A disk-only snapshot."
}
],
"description": "Wire shape of a completed cloud snapshot."
},
"CloudSnapshotDetails": {
"type": "object",
"description": "Fields shared by every completed cloud snapshot kind.",
"required": [
"name",
"location",
"digest",
"size_bytes",
"manifest",
"labels",
"created_at"
],
"properties": {
"created_at": {
"type": "string",
"format": "date-time",
"description": "Creation timestamp."
},
"digest": {
"type": "string",
"description": "Snapshot identity: the sha256: digest of the canonical descriptor."
},
"labels": {
"type": "object",
"description": "User-defined labels stored on the snapshot.",
"additionalProperties": {
"type": "string"
},
"propertyNames": {
"type": "string"
}
},
"location": {
"$ref": "#/components/schemas/CloudSnapshotLocation",
"description": "Where the snapshot artifact resides."
},
"manifest": {
"$ref": "#/components/schemas/SnapshotManifest",
"description": "Canonical snapshot descriptor."
},
"name": {
"type": "string",
"description": "Snapshot name."
},
"sandbox_id": {
"type": [
"string",
"null"
],
"description": "Identifier of the sandbox the snapshot was captured from, when known."
},
"size_bytes": {
"type": "integer",
"format": "int64",
"description": "Stored payload size in bytes.",
"minimum": 0
}
}
},
"CloudSnapshotKind": {
"type": "string",
"description": "Kind of cloud snapshot artifact or capture operation.",
"enum": [
"disk"
]
},
"CloudSnapshotLocation": {