microsandbox Documentation

Easy, fast, programmable and local-first microVM runtime and library

8,761 stars Rust Rust 40 files · ~152,272 tokens #agents#docker#golang#linux#local#macos#microvm#nodejs
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy microsandbox on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name microsandbox --restart always superradcompany/microsandbox:latest

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

sh
curl -fsSL https://install.microsandbox.dev | sh        # 🍎 macOS / 🐧 Linux
powershell
irm https://install.microsandbox.dev/windows | iex      # 🪟 Windows

 We also support other package managers →

sh
brew install superradcompany/tap/microsandbox
sh
npm i microsandbox
sh
uv tool install microsandbox
sh
cargo install microsandbox

Start creating sandboxes once installed:

sh
msb run ubuntu

  Install the SDK

sh
npm i microsandbox                                       # 🟦 TypeScript
sh
cargo add microsandbox                                   # 🦀 Rust
sh
uv add microsandbox                                      # 🐍 Python
sh
go 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

sh
msb run python -- python3 -c "print('Hello from a microVM!')"

  Named Sandboxes

sh
# Create and start a named sandbox
msb create --name app python
sh
# Execute commands
msb exec app -- python -c "import this"
msb exec app -- curl https://example.com
sh
# 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 app3

Forking duplicates live execution. Restoring starts from saved state; add --cow-mem to share unchanged memory from a full snapshot. Snapshot generations describe progression, while branches describe distinct paths through snapshot history. The old --forked restore flag remains a deprecated alias. See forking and snapshots.

sh
# Lifecycle
msb stop app
msb start app
msb rm app

  Image Management

sh
msb pull python           # Pull an image
msb image ls              # List cached images
msb image rm python       # Remove an image

  Configuration File

sh
msb run --conf sandbox.yaml -- octocat
yaml
# 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

sh
msb 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 --help for quick help menu.
· msb --tree for complete command hierarchy and descriptions.
· msb <command> --tree for 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

typescript
import { 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 →

rust
use 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 →

python
import 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 →

ruby
require "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.stop

See the Ruby SDK guide for installation, lifecycle,
networking, and backend details.

 Go Example →

go
package 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.

sh
npx 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:

sh
git submodule update --init --recursive

Examples

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 DynFileSystem and 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.

rust
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.

rust
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.

rust
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)
rust
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

rust
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

rust
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.

rust
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.

rust
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.

rust
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 + libkrunfw installed (bundled in the wheel, or at ~/.microsandbox/)
  • For root-bind and root-block: git submodule update --init --recursive

Setup

Build the Python SDK (one-time):

sh
cd sdk/python
uv sync --group dev
maturin develop --release

Running

From the repo root:

sh
uv run --project sdk/python python examples/python/<example>/main.py

For example:

sh
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.py

Configuration-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 + libkrunfw installed (via cargo build with the download-binaries feature, or manually)
  • For root-bind and root-block: git submodule update --init --recursive

Running

Each example is a standalone binary crate in the workspace:

sh
cargo run -p <example-name>

For example:

sh
cargo run -p oci-root
cargo run -p net-basic
cargo run -p fs-read-stream

Configuration-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 + libkrunfw available from the microsandbox platform package installed through optional dependencies, or MSB_PATH pointing at a working msb binary
  • For root-bind and root-block: git submodule update --init --recursive

Setup

Each example is a standalone Node.js project. Install dependencies first:

sh
cd examples/typescript/<example>
npm install

Running

sh
npm start

For example:

sh
cd examples/typescript/root-oci
npm install
npm start

Configuration-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:

text
sdk -> agent-client -> protocol-client -> protocol

The 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

text
packages/agent-client/
├── rust/
└── typescript/
  • rust/ publishes as microsandbox-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

text
packages/microsandbox-types/
├── rust/
└── typescript/
  • rust/ publishes as microsandbox-types (crate name microsandbox_types). It is the source of truth for every shared type.
  • typescript/ publishes as @microsandbox/types. Its src/cloud.ts (entry) and src/domain.ts are 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.

text
rust/lib/*.rs  ──(ts-rs)──▶  typescript/src/{cloud,domain}.ts

To regenerate the bindings after changing a Rust type:

bash
cargo run -p microsandbox-types --features ts --bin microsandbox-types-generate

CI runs the same generator with --check and fails when the checked-in bindings drift:

bash
cargo run -p microsandbox-types --features ts --bin microsandbox-types-generate -- --check

What 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 CloudCreateSandboxRequest union,
    CloudSandbox, snapshot resources and operations, and
    paginated/message/error bodies.

Cloud snapshot contracts distinguish the durable resource from the asynchronous
capture operation:

  • CloudCreateSnapshotRequest is discriminated by kind. Its current disk
    variant captures the writable disk, and the enum can gain other snapshot
    kinds without changing the envelope shape.
  • CloudSnapshot uses the same kind discriminator and includes its canonical
    manifest, byte size, labels, and CloudSnapshotLocation.
  • CloudSnapshotLocation is 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.
  • CloudSnapshotOperation carries the requested kind and tracks capture
    through queued, in_progress, succeeded, or failed; result is
    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:

text
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:

text
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 the WINGET_RELEASE_ENABLED repository variable to true. Keep it unset before then so release workflows skip the update-winget job.
  • Add a WINGET_TOKEN repository secret: a classic PAT with public_repo scope whose owner has a fork of microsoft/winget-pkgs. The submit step is skipped when this secret is absent.
  • If that fork lives under an account other than superradcompany, set a WINGET_FORK_USER repository 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

  1. Fork and clone microsoft/winget-pkgs.
  2. Copy the staged manifests into the fork:
powershell
$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
  1. Validate and smoke-test from the winget-pkgs checkout on Windows:
powershell
winget validate $dst
winget install --manifest $dst --accept-package-agreements --accept-source-agreements
msb doctor
winget uninstall SuperRadCompany.Microsandbox
  1. Commit the copied files in the winget-pkgs fork and open a pull request against microsoft/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

  1. Wait for a release that includes:
    • microsandbox-windows-x86_64.zip
    • microsandbox-windows-aarch64.zip
    • checksums.sha256
  2. Copy the template files into the winget-pkgs path:
powershell
$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
  1. Replace template markers:
    • {{VERSION}} with the semver package version, for example 0.5.9
    • {{TAG}} with the GitHub release tag, for example v0.5.9
    • {{RELEASE_DATE}} with the release date in YYYY-MM-DD
    • {{SHA256_X64}} with the checksum for microsandbox-windows-x86_64.zip
    • {{SHA256_ARM64}} with the checksum for microsandbox-windows-aarch64.zip

The checksums are available from the release asset:

powershell
gh release download $tag --repo superradcompany/microsandbox --pattern checksums.sha256 --dir $env:TEMP --clobber
Select-String -Path "$env:TEMP\checksums.sha256" -Pattern "microsandbox-windows"
  1. Validate and smoke-test from the winget-pkgs checkout:
powershell
winget validate $dst
winget install --manifest $dst --accept-package-agreements --accept-source-agreements
msb doctor
winget uninstall SuperRadCompany.Microsandbox
  1. 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
sh scripts/msb-trust/build.sh

core.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 runs
sh 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

bash
go get github.com/superradcompany/microsandbox/sdk/go

Call 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.

go
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

go
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.

go
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.

go
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

go
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

go
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

go
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

go
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

go
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(&microsandbox.NetworkConfig{
        DenyDomains:        []string{"blocked.example.com"},
        DenyDomainSuffixes: []string{".evil.com"},
        DNS: &microsandbox.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.

go
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

go
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

go
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

go
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

go
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.

go
_, 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:

bash
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:

bash
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:

bash
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/basic

Use libmicrosandbox_go_ffi.so for Linux. Full integration tests require local virtualization support and runtime artifacts:

bash
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:

bash
MSB_STORAGE_NATIVE_LIBRARY="$PWD/../../target/debug/libmicrosandbox_go_ffi.dylib" \
    go test -tags storage_native ./internal/ffi -run '^TestStorageNativeReportsAndPruning$' -count=1

License

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+:

typescript
import { Sandbox } from "microsandbox";
javascript
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

bash
npm install microsandbox

Runtime setup is normally included. To provision msb + libkrunfw separately, use the CLI installer or explicitly install them from your application:

typescript
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

typescript
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.

typescript
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.

typescript
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.

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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.

typescript
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

typescript
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

typescript
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

typescript
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

typescript
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.

typescript
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 microsandbox package depends on platform packages through optional dependencies.
  • The platform package carries the native addon, msb, and libkrunfw.
  • The microsandbox and msb bin shims forward to the resolved msb binary. They do not install runtime files.
  • If no platform package is present, reinstall with optional dependencies enabled, install the matching @superradcompany/microsandbox- package, or set MSB_PATH.

Inspect a runtime version

typescript
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:

bash
npm ci
npm run build
npm run typecheck
npm test

On 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:

bash
cd examples/typescript/root-oci
npm install
npm start

License

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, .pyi stubs, and py.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

bash
pip install microsandbox

Quick Start

python
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.

python
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.

python
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

python
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:
            break

Filesystem Operations

python
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

python
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

python
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.

python
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

python
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

python
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

python
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

python
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.

python
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.

python
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.

python
from microsandbox import ensure_runtime

runtime = await ensure_runtime()
print(runtime.msb_path, runtime.libkrunfw_path)

More Documentation

Development

From sdk/python:

bash
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:

bash
uv run --project sdk/python python examples/python/root-oci/main.py

Runtime integration tests require local virtualization support and runtime artifacts:

bash
cd sdk/python
uv run pytest integration/test_create_kwargs.py integration/test_exec.py

License

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:

sh
gem install microsandbox

[!NOTE]
Platform gems are currently built and validated in CI but not yet published
to RubyGems — every gem install microsandbox still 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 if
gem --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:

sh
gem install microsandbox --platform ruby

With Bundler:

ruby
gem "microsandbox", force_ruby_platform: true

To build a platform gem from a checkout, install every target Ruby, then run
from sdk/ruby:

sh
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:platform

gem:platform refuses to package unless all three ABIs are staged. Set
RUBY_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:

ruby
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:

ruby
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
end

Backends

The local backend is the default. Select a backend explicitly when needed:

ruby
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. Prefer
Sandbox.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.

ruby
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.destroy

Run 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:

ruby
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 with
proxy: 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:

ruby
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:

ruby
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:

ruby
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}"
end

Class 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 explicit
stop_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:

ruby
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"
end

SnapshotSourceRecoveryError 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 refresh
ext/microsandbox/Cargo.lock. To refresh it manually, run
cargo 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

bash
cargo add microsandbox

Cargo 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
bash
# 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,net

For the last option, install the runtime with the CLI installer or call setup::ensure_runtime() at startup. See Runtime setup for details.

Quick Start

rust
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.

rust
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.

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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.

rust
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

rust
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

rust
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.

text
TypedMessage / EncodedMessage / raw envelope / exact packet
                           |
                 Client<AgentProtocol>
                           |
          current or legacy relay handshake
                           |
                       agent relay

Native, encoded, and raw messages

rust,no_run
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

rust,no_run
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.

rust,no_run
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.

text
AgentProtocol (setup, encoder, gates)
                 |
          Client<AgentProtocol>
            /          \
 native / encoded     raw / exact packets
                 |
       Unix socket / named pipe / WebSocket

Installation 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

ts
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

ts
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

ts
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.

text
ControlConnection -- JSON capabilities --+-- advertised CBOR: fresh stream + hello
                                        |                      |
                                        |               ControlClient
                                        |
                                        +-- legacy: JsonControlClient

Automatic 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.

rust,no_run
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.

rust,no_run
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.

rust,no_run
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.

ts
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.

text
connectControl(path)
    |
    +-- JSON capabilities --> valid legacy reply --> fresh JSON exchange per operation
    |
    +-- JSON capabilities --> advertises CBOR -----> fresh hello/welcome --> shared framed client
ts
import { 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:

ts
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()
ts
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 (domain module): 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 canonical SnapshotManifest schema.
  • Networking: NetworkSpec, PublishedPortSpec, PortProtocol.
  • Exec and logs: Rlimit, RlimitResource, SandboxLogLevel, LogSource, HandoffInit.
  • Cloud wire contracts (cloud module): sandbox, snapshot, pagination, message, and error request/response types.
  • Validation (validation module): validate_sandbox_name, validate_hostname, hostname_from_sandbox_name, and the MAX_SANDBOX_NAME_BYTES / MAX_HOSTNAME_BYTES limits.

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

toml
[dependencies]
microsandbox-types = "0.7.0"
rust
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, and MountOptions use #[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.
  • VolumeMount has hand-written Serialize/Deserialize. It tags variants with a type field and accepts a legacy top-level readonly flag, folding it into MountOptions on 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.

bash
# 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 -- --check

A 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

bash
cargo test -p microsandbox-types
cargo test -p microsandbox-types --features ts

Overview (`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:

bash
cargo run -p microsandbox-types --features ts --bin microsandbox-types-generate

Install

bash
npm install @microsandbox/types

Usage

Import the shapes you need with import type:

ts
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 by
kind; 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 type field: CloudRootfsSource is { type: "bind"; … } | { type: "oci"; reference: string } | { type: "disk_image"; … }, and CloudVolumeMount / CloudHostPattern / CloudViolationAction follow the same shape.
  • CloudCreateSandboxRequest is a flat union discriminated by source: "oci" | "bind" | "disk_image" | "disk_snapshot". Common sandbox fields stay flat on every variant; source-specific fields such as reference, path, and disk_snapshot_ref appear only where they apply. Servers also accept the legacy image request shape without source during migration.
  • CloudCreateSnapshotRequest and CloudSnapshot are flat unions discriminated by the current kind: "disk"; common snapshot fields stay flat beside the discriminator.
  • Lowercase domain enums like StatVirtualization are 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.ts and snapshot.ts and are re-exported from the package entry, so a single import from @microsandbox/types sees the whole cloud contract.

Build And Typecheck

bash
npm run build       # tsc -> dist/
npm run typecheck   # tsc --noEmit

Overview (`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.

text
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.

text
Client<P>
          /          |           \
 native payload   encoded      raw frame / exact packet
          \          |           /
          protocol codec + shared router
                       |
                ByteTransport

Implementing 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.

ts
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:

sh
go run ./examples/basic

Examples

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"

microsandbox cloud is in [private beta](/cloud/overview); the API is available to organizations with access.

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:

```bash Shell curl https://api.microsandbox.dev/v1/sandboxes \ -H "Authorization: Bearer $MSB_API_KEY" ```

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

text
https://api.microsandbox.dev

Errors

Errors return an error object with a stable machine-readable code, a human-readable message, and optional structured details:

```json JSON { "error": { "code": "invalid_api_key", "message": "unauthorized", "details": null } } ```

Pagination

List endpoints use cursor pagination. Pass limit to size the page and cursor to continue from a previous response:

```json JSON { "data": [ ... ], "has_more": true, "next_cursor": "eyJv..." } ```

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

```bash CLI (Linux & macOS) curl -fsSL https://install.microsandbox.dev | sh ```
bash homebrew (macos)
brew install superradcompany/tap/microsandbox
powershell cli (windows)
irm https://install.microsandbox.dev/windows | iex

On 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.

Local sandboxes require Linux with glibc 2.28 or newer and KVM, an Apple Silicon Mac, or an x64 or ARM64 machine running Windows 10 or later with Windows Hypervisor Platform enabled. See [Linux troubleshooting](/troubleshooting/linux), [macOS troubleshooting](/troubleshooting/macos), or [Windows troubleshooting](/troubleshooting/windows) for setup help.

Check your local setup with:

```bash Shell msb doctor ```

Quick start

```bash Shell msb create python --name worker msb exec worker -- python -c "print('hello')" msb stop worker msb rm worker ```

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:

```bash Shell msb run alpine -- echo hello msb sandbox run alpine -- echo hello msb sbx run alpine -- echo hello ```

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:

```bash Shell msb snap create ready --sandbox devbox --full msb snap restore devbox:ready --name worker msb snap ls --group devbox msb snap export devbox:ready --output ready.msb msb snap import ready.msb --group received ```

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:

```bash bash # Requires the bash-completion package. Without it, add this line to # ~/.bashrc instead: source <(msb completion bash) mkdir -p ~/.local/share/bash-completion/completions msb completion bash > ~/.local/share/bash-completion/completions/msb ```
zsh zsh
mkdir -p ~/.zsh/completions
msb completion zsh > ~/.zsh/completions/_msb
# Add this line to ~/.zshrc before compinit runs:
#   fpath=(~/.zsh/completions $fpath)
fish fish
mkdir -p ~/.config/fish/completions
msb completion fish > ~/.config/fish/completions/msb.fish
elvish elvish
mkdir -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)
powershell powershell
$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:

```bash Shell msb --tree ```

You can scope it to a specific subcommand:

```bash Shell msb image --tree # Show only image commands msb volume --tree # Show only volume commands msb run --tree # Show all flags for run msb sandbox --tree # Show the canonical sandbox command group msb sbx run --tree # Show run flags through the group alias ```

Limit the depth or hide flags and arguments for a compact command overview:

```bash Shell msb --tree -L 2 msb --tree --commands msb image --tree --commands -L 1 msb --tree --brief --commands -L 2 msb --tree -Cb -L 2 ```

-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 is in **private beta**, and currently requires you to explicitly [request access](https://dashboard.microsandbox.dev/signup). If you already have access, you can get your API key from the [dashboard](https://dashboard.microsandbox.dev/access/api-keys).

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

```bash Shell export MSB_BACKEND=cloud export MSB_API_KEY="msb_..." msb context ```

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:

```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 the cloud!')"]);
console.log(output.stdout());

text
```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?;
python python
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()
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 the cloud!')"})
fmt.Println(output.Stdout())
ruby ruby
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
end
bash cli
msb 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

Create, inspect, list, reconnect, start, stop, and remove sandboxes. Run buffered, streaming, and interactive commands, then move files in or out. Create managed volumes and access their files without starting a sandbox. Apply egress policy and hostname-scoped secrets from the same SDK surface.

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 ssh and 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

The REST API behind the SDKs, authenticated with your API key. Force local, select in code, or switch with profiles. New to microsandbox? Start here; everything carries over.

Getting Started Quickstart (`docs/getting-started/quickstart.mdx`)


title: Quickstart icon: "bolt"

Run your first sandbox

```bash npx npx microsandbox run debian ```
bash macos & linux
curl -fsSL https://install.microsandbox.dev | sh
msb run debian
powershell windows
irm https://install.microsandbox.dev/windows | iex
msb run debian
Prefer short commands such as `msb run debian` for everyday use. `msb sandbox run debian` and `msb sbx run debian` do the same thing, with the same flags. Explore the full group with `msb sandbox --help`.

Having trouble with a local installation? Choose your platform for setup checks and fixes.

text
**Linux**glibc 2.28+ and KVM
text
**macOS**Apple Silicon
text
**Windows**10+ · WHP
For application code, install the SDK for your language. For terminal workflows, use one of the CLI options above. Both run microsandbox locally by default; there is no separate server or daemon to set up. The same installation also drives [microsandbox cloud](/cloud/overview) when an API key is set.
text
<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).
Create a sandbox, execute code inside it, and get the result back.
text
<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:

  1. Pulled the image from Docker Hub, unless it was already cached.
  2. Assembled a copy-on-write filesystem so changes inside the sandbox do not modify the base image.
  3. Booted a microVM as a child process with the resource limits you configured.
  4. 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.

Want the same sandbox on hosted infrastructure? Set `MSB_BACKEND=cloud` and export `MSB_API_KEY`; the code above then runs on [microsandbox cloud](/cloud/overview) unchanged.

Next steps


How To Overview (`docs/how-to/overview.mdx`)


title: How-to guides sidebarTitle: Overview icon: "book-open"

Agents

text
<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

text
<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

text
<li>[Run a browser agent](/how-to/browser-automation/browser-use)</li>
<li>[Automate a browser](/how-to/browser-automation/playwright)</li>

Workloads

text
<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

text
<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

text
<li>[Run microsandbox in Docker](/how-to/docker/docker)</li>
<li>[Run microsandbox in Kubernetes](/how-to/sandboxing/kubernetes)</li>

Storage

text
<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

text
<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

text
<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
```typescript TypeScript import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
.image("python")
.pullPolicy("always")
.create();

text
```rust Rust
let sb = Sandbox::builder("worker")
    .image("python")
    .pull_policy(PullPolicy::Always)
    .create()
    .await?;
python python
from microsandbox import PullPolicy, Sandbox

sb = await Sandbox.create("worker", image="python", pull_policy=PullPolicy.ALWAYS)
go go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithPullPolicy(m.PullPolicyAlways),
)
ruby ruby
sb = Microsandbox::Sandbox.create("worker", image: "python", pull_policy: "always")
Once microsandbox resolves an image reference, it pins the exact layers. At first pull, `python` resolves to a specific set of immutable layers. Subsequent `start()` calls use the pinned layers without re-resolving the mutable tag, so your sandbox is reproducible even if the upstream tag moves.

Private registries

Authenticate to private registries by passing credentials.

```typescript TypeScript await using sb = await Sandbox.builder("worker") .image("registry.corp.io/team/app:latest") .registry((r) => r.auth({ kind: "basic", username: "deploy", password: process.env.REGISTRY_TOKEN!, })) .pullPolicy("always") .create(); ```
rust rust
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?;
python python
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,
)
go go
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

Local-only

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:

```json JSON { "registries": { "hosts": { "localhost:5050": { "insecure": true } } } } ```

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:

```json JSON { "registries": { "ca_certs": "/path/to/ca-bundle.pem" } } ```

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

```json JSON { "registries": { "ca_certs": "/path/to/corporate-ca.pem", "hosts": { "localhost:5050": { "insecure": true, "auth": { "username": "deploy", "password_env": "REGISTRY_TOKEN" } }, "ghcr.io": { "auth": { "username": "user", "store": "keyring" } } } } } ```

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.

```typescript TypeScript await using sb = await Sandbox.builder("worker") .image("localhost:5050/my-app:latest") .registry((r) => r.insecure()) .create(); ```
rust rust
let sb = Sandbox::builder("worker")
    .image("localhost:5050/my-app:latest")
    .registry(|r| r.insecure())
    .create()
    .await?;
python python
from microsandbox import Sandbox

sb = await Sandbox.create(
    "worker",
    image="localhost:5050/my-app:latest",
    registry_insecure=True,
)
go go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("localhost:5050/my-app:latest"),
    m.WithRegistryInsecure(),
)

Image storage

Local-only

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 pull` from the CLI to pre-pull images before creating sandboxes. This avoids blocking on a download during `Sandbox.create`.

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

Local-only

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.

```typescript TypeScript import { Sandbox } from "microsandbox";

// 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();

text
```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?;
python python
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)
go go
// 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),
)
ruby ruby
# 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.
The filesystem type (`ext4`, `xfs`, etc.) must match what's actually on the disk image. The guest kernel mounts it using the specified filesystem driver.

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:

```typescript TypeScript import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("isolated")
.image("python")
.disableNetwork()
.create();

text
```rust Rust
use microsandbox::Sandbox;

let sb = Sandbox::builder("isolated")
    .image("python")
    .network(|n| n.enabled(false))
    .create()
    .await?;
python python
from microsandbox import Network, Sandbox

sb = await Sandbox.create("isolated", image="python", network=Network.none())
go go
sb, err := m.CreateSandbox(ctx, "isolated",
    m.WithImage("python"),
    m.WithNetwork(m.NetworkPolicy.None()),
)
ruby ruby
sb = Microsandbox::Sandbox.create("isolated", image: "python", network: :none)
bash cli
msb create python --name isolated --no-net

The 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.

```typescript TypeScript import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("shared-worker")
.image("python")
.deploymentProfile("multi-tenant")
.create();

text
```rust Rust
use microsandbox::sandbox::DeploymentProfile;
use microsandbox::Sandbox;

let sb = Sandbox::builder("shared-worker")
    .image("python")
    .deployment_profile(DeploymentProfile::MultiTenant)
    .create()
    .await?;
python python
from microsandbox import DeploymentProfile, Sandbox

sb = await Sandbox.create(
    "shared-worker",
    image="python",
    deployment_profile=DeploymentProfile.MULTI_TENANT,
)
go go
sb, err := m.CreateSandbox(ctx, "shared-worker",
    m.WithImage("python"),
    m.WithDeploymentProfile(m.DeploymentProfileMultiTenant),
)
bash cli
msb create python --name shared-worker --deployment-profile multi-tenant

In 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:

```typescript TypeScript import { NetworkPolicy, Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
.image("python")
.network((n) => n.policy(NetworkPolicy.fromProfiles(["public", "private"])))
.create();

text
```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?;
python python
from microsandbox import Network, NetworkProfile, Sandbox

sb = await Sandbox.create(
    "worker",
    image="python",
    network=Network.from_profiles(NetworkProfile.PUBLIC, NetworkProfile.PRIVATE),
)
go go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithNetwork(m.NetworkPolicy.FromProfiles(m.NetworkProfilePublic, m.NetworkProfilePrivate)),
)
bash cli
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:

```typescript TypeScript import { NetworkPolicy, Sandbox } from "microsandbox";

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();

text
```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?;
python python
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(),
        ),
    )),
)
go go
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(),
        },
    }),
)
bash cli
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:

```typescript TypeScript import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("translated")
.image("python")
.network((n) => n.nat64Prefix("64:ff9b:1::/96"))
.create();

text
```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?;
python python
from microsandbox import Network, Sandbox

sb = await Sandbox.create(
    "translated",
    image="python",
    network=Network(nat64_prefixes=("64:ff9b::/96", "64:ff9b:1::/96")),
)
go go
sb, err := m.CreateSandbox(ctx, "translated",
    m.WithImage("python"),
    m.WithNetwork(&m.NetworkConfig{
        NAT64Prefixes: []string{"64:ff9b::/96", "64:ff9b:1::/96"},
    }),
)
bash cli
msb create python --name translated --net-nat64-prefix 64:ff9b:1::/96

This 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:

```typescript TypeScript import { NetworkPolicy, Sandbox } from "microsandbox";

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();

text
```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?;
python python
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,
)
go go
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(),
)
ruby ruby
sb = Microsandbox::Sandbox.create(
  "github-only",
  image: "python",
  network: { allowed_hosts: ["github.com"], allowed_ports: [443] },
  intercept_tls: true
)
bash cli
msb create python --name github-only \
  --net-default-egress deny \
  --net-rule "[email protected]:tcp:443,allow@dns" \
  --tls-intercept

Domain 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.

```typescript TypeScript import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("api")
.image("python")
.port(8080, 80)
.create();

text
```rust Rust
use microsandbox::Sandbox;

let sb = Sandbox::builder("api")
    .image("python")
    .port(8080, 80)
    .create()
    .await?;
python python
from microsandbox import PortBinding, Sandbox

sb = await Sandbox.create("api", image="python", ports=[PortBinding.tcp(8080, 80)])
go go
sb, err := m.CreateSandbox(ctx, "api",
    m.WithImage("python"),
    m.WithPorts(map[uint16]uint16{8080: 80}),
)
bash cli
msb create python --name api -p 8080:80

Use 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:

```typescript TypeScript import { Sandbox } from "microsandbox";

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();

text
```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?;
python python
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),
            ),
        ),
    ),
)
go go
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},
            },
        },
    }),
)
bash cli
msb create python --name throttled \
  --net-egress-bandwidth 1M/1s \
  --net-egress-bandwidth-burst 512K \
  --net-egress-ops 1000/1s

Network 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.

```typescript TypeScript await using sb = await Sandbox.builder("agent") .image("python") .network((n) => n .policy(policy) .tls((t) => t) .http((h) => h .denyResponse(true) .denyResponseFormat("json") .networkDenyMessage("Ask the user for network access.") .secretDenyMessage("Ask the user to review secret access."))) .create(); ```
rust rust
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?;
python python
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.",
        ),
    ),
)
go go
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.",
        },
    }),
)
ruby ruby
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."
  }
)
yaml sandbox.yml
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:

json
{
  "code": "network_policy_denied",
  "message": "Request blocked by network policy.",
  "domain": "example.com"
}

Secret-policy denial:

json
{
  "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:

```typescript TypeScript import { NetworkPolicy, Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("devbox")
.image("python")
.network((n) => n.policy(NetworkPolicy.fromProfiles(["public", "host"])))
.create();

text
```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?;
python python
from microsandbox import Network, NetworkProfile, Sandbox

sb = await Sandbox.create(
    "devbox",
    image="python",
    network=Network.from_profiles(NetworkProfile.PUBLIC, NetworkProfile.HOST),
)
go go
sb, err := m.CreateSandbox(ctx, "devbox",
    m.WithImage("python"),
    m.WithNetwork(m.NetworkPolicy.FromProfiles(m.NetworkProfilePublic, m.NetworkProfileHost)),
)
bash cli
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


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.

```typescript TypeScript await using sb = await Sandbox.builder("worker") .image("python") .create(); ```
rust rust
let sb = Sandbox::builder("worker")
    .image("python")
    .create()
    .await?;
python python
sb = await Sandbox.create("worker", image="python")
go go
sb, err := m.CreateSandbox(ctx, "worker", m.WithImage("python"))
ruby ruby
sb = Microsandbox::Sandbox.create("worker", image: "python")
bash cli
msb create python --name worker

Sandbox names must be non-empty and no longer than 128 UTF-8 bytes.

Common configuration

Limited on cloud

```typescript TypeScript await using sb = await Sandbox.builder("worker") .image("python") .memory(1024) .cpus(2) .env("DEBUG", "true") .label("user.id", "alice") .volume("/tmp/scratch", (m) => m.tmpfs().size(100)) .create(); ```
rust rust
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?;
python python
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)},
)
go go
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}),
    }),
)
ruby ruby
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.
bash cli
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.

`cpus` and `memory` are limits, not reservations. Guest memory is allocated as the VM touches pages.

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:

Local-only

```typescript TypeScript const image = "python"; // Or "./my-rootfs" or "./alpine.qcow2". await using sb = await Sandbox.builder("worker").image(image).create(); ```
rust rust
use microsandbox::Sandbox;

let image = "python"; // Or "./my-rootfs" or "./alpine.qcow2".
let sb = Sandbox::builder("worker").image(image).create().await?;
python python
from microsandbox import Sandbox

image = "python"  # Or "./my-rootfs" or "./alpine.qcow2".
sb = await Sandbox.create("worker", image=image)
go go
image := "python" // Or "./my-rootfs" or "./alpine.qcow2".
sb, err := m.CreateSandbox(ctx, "worker", m.WithImage(image))
if err != nil {
    return err
}
ruby ruby
image = "python" # Or "./my-rootfs" or "./alpine.qcow2".
sb = Microsandbox::Sandbox.create("worker", image: image)
bash cli
# Choose one source:
msb create python --name worker
msb create ./my-rootfs --name worker
msb create ./alpine.qcow2 --name worker

OCI 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

Local-only

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:

```typescript TypeScript await using sb = await Sandbox.builder("worker") .image("python") .replace() .create(); ```
rust rust
let sb = Sandbox::builder("worker")
    .image("python")
    .replace()
    .create()
    .await?;
python python
sb = await Sandbox.create("worker", image="python", replace=True)
go go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithReplace(),
)
ruby ruby
sb = Microsandbox::Sandbox.create("worker", image: "python", replace: true)
bash cli
msb create --replace python --name worker

When 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

```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

All 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.

```typescript TypeScript import { Sandbox } from "microsandbox";

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());

text
```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(())
}
python python
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())
go go
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())
}
ruby ruby
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}"
end

Reference

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.
One default surprises people: workloads run as **root inside the guest**. That is in-guest root, contained by the VM boundary. It is not a privilege on your host, and you can drop it. See [Isolation boundary](/security/isolation#in-guest-privilege) and [Hardening](/security/hardening).

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.com and 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.

The microVM, the host-guest channel, and in-guest privilege. Private root, mounts, snapshots, and image supply chain. Egress filtering, SSRF, DNS rebinding, and metadata. How credentials stay on the host, and the exact guarantee. Dial the controls to match your threat level.

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:

```json JSON { "sandbox_defaults": { "cpus": 2, "memory_mib": 1024 } } ```

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.

``` /* Detailed source-code truncated for AI context efficiency. */ ```

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`

Local-only

Set deployment_profile when the local host must enforce one deployment policy for every sandbox:

```json JSON { "deployment_profile": "multi-tenant" } ```

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`

Local-only

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`

Local-only

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`

Limited on cloud

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`

Local-only

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:

```json JSON { "runtime": { "block_writeback": { "mode": "fixed", "per_disk_mib": 2048, "pool_mib": 12288 } } } ```

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`

Local-only

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.

```json JSON { "registries": { "hosts": { "ghcr.io": { "auth": { "username": "octocat", "store": "keyring" } }, "localhost:5050": { "insecure": true, "auth": { "username": "dev", "password_env": "LOCAL_REGISTRY_TOKEN" } } } } } ```

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
Exactly one of `store`, `password_env`, or `secret_name` must be set per entry. Setting none or more than one is an error.

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:

  1. Explicit SDK auth via .registry(|r| r.auth(...)) on the sandbox builder
  2. OS keyring entries created by msb registry login
  3. Config file registries.hosts.<host>.auth entries in config.json
  4. Docker config ~/.docker/config.json credential helpers
  5. Anonymous (no authentication)

`metrics`

Local-only

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:

text
Running sandbox
      |
      +-- disk snapshot ----> new VM boots from the saved disk
      |
      +-- full snapshot ----> new VM resumes saved RAM, CPUs, devices, and disk

Disk 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.

text
~/.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 snapshot

IDs 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

bash
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.

text
worker:cp01 ---- worker:cp02 ---- worker:cp03  <- head
                     \
                      +-------- worker:experiment

The 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.
bash
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 rewind

There 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?

text
+---- 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

bash
# 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-mem

The 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:

bash
msb snap import checkpoints/cp02.msb checkpoints/cp01.msb --group received

Loading 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:

bash
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:

bash
msb snap import checkpoints/cp01.msb --group received --set-head

For 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": {