Back to Microsandbox

Introduction

docs/getting-started/introduction.mdx

0.4.412.0 KB
Original Source

AI agents run with whatever privileges you give them, and most of the time that's too many. They can see API keys sitting in the environment, reach the network freely (including cloud metadata at 169.254.169.254), and nothing stops a prompt injection from running rm -rf / on the host filesystem. Containers help with some of this, but they share the host kernel, and namespace escapes are a well-documented class of vulnerability.

microsandbox takes a different approach: every sandbox is a real VM with its own Linux kernel. It provides security primitives for preventing exploits like secret exfiltration. And it runs locally on your machine.

<CodeGroup>
rust
use microsandbox::{Sandbox, NetworkPolicy};

let sb = Sandbox::builder("sandbox")
    .image("python")
    .memory(512)
    .cpus(2)
    .secret(|s| s
        .env("OPENAI_API_KEY")
        .value(std::env::var("OPENAI_API_KEY")?)
        .allow_host("api.openai.com")
    )
    .network(|n| n.policy(NetworkPolicy::public_only()))
    .create()
    .await?;
typescript
import { NetworkPolicy, Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("sandbox")
    .image("python")
    .memory(512)
    .cpus(2)
    .secret((s) =>
        s.env("OPENAI_API_KEY")
            .value(process.env.OPENAI_API_KEY!)
            .allowHost("api.openai.com"),
    )
    .network((n) => n.policy(NetworkPolicy.publicOnly()))
    .create();
python
import os
from microsandbox import Network, Sandbox, Secret

sb = await Sandbox.create(
    "sandbox",
    image="python",
    memory=512,
    cpus=2,
    secrets=[
        Secret.env("OPENAI_API_KEY",
            value=os.environ["OPENAI_API_KEY"],
            allow_hosts=["api.openai.com"],
        ),
    ],
    network=Network.public_only(),
)
</CodeGroup> <Callout icon="bolt" color="#7C3AED"> microsandbox spins up lightweight VMs in **under a second**, right from your code on your machine. **No servers, no daemons**, no infrastructure to manage. </Callout>

The basics

  • Under 100ms boot. Sandboxes boot in under 100 milliseconds.
  • No daemon. The runtime spawns directly as a child process of whatever application creates the sandbox. No root, no server, no background service.
  • Any OCI image. Docker Hub, GHCR, ECR, GCR, or any OCI-compatible registry. Shared layers are deduplicated and only stored once.
  • Cross-platform. Native on macOS (Apple Silicon) and Linux (KVM). Same CLI, same SDKs.
  • Multi-language SDKs. Rust, TypeScript, and Python, all with consistent APIs.
  • Programmable. Network policies, filesystem hooks, secret bindings, and TLS interception are all configured from the host side through the SDK.

What makes it different

Secrets that can't leak

Most sandboxes protect secrets by restricting what the guest can do. microsandbox goes further: the guest never has the secret at all.

Instead of injecting real credentials into the sandbox, microsandbox generates random placeholders. The actual value never enters the VM. The only way it reaches the outside world is when a request goes to an allowed host, at which point microsandbox swaps the placeholder for the real value. Everywhere else, the placeholder is just a meaningless string.

So even with full code execution inside the sandbox, there's nothing to exfiltrate. The credential was never there. DNS rebinding protection and cloud metadata blocking kick in automatically when secrets are configured, closing the remaining side channels.

<CodeGroup>
rust
let sb = Sandbox::builder("sandbox")
    .image("python")
    .secret_env("OPENAI_API_KEY", api_key, "api.openai.com")
    .create()
    .await?;
typescript
await using sb = await Sandbox.builder("sandbox")
    .image("python")
    .secretEnv("OPENAI_API_KEY", process.env.OPENAI_API_KEY!, "api.openai.com")
    .create();
python
sb = await Sandbox.create(
    "sandbox",
    image="python",
    secrets=[
        Secret.env("OPENAI_API_KEY",
            value=os.environ["OPENAI_API_KEY"],
            allow_hosts=["api.openai.com"],
        ),
    ],
)
</CodeGroup>

Programmable network layer <sup><sup>coming soon</sup></sup>

Every packet leaving a sandbox passes through a networking stack controlled entirely from the host side, where policy is enforced at the IP, DNS, or HTTP level. From inside the sandbox it looks like a normal network interface, but on the host side you decide what gets through.

There's no underlying host network to bridge to, no rules to circumvent, and no way for the guest to reach around the filter.

<CodeGroup>
rust
let sb = Sandbox::builder("sandbox")
    .image("python")
    .network(|n| n
        .policy(NetworkPolicy::allowlist(["api.openai.com", "pypi.org"]))
    )
    .create()
    .await?;
typescript
import { Destination, NetworkPolicy, Rule, Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("sandbox")
    .image("python")
    .network((n) => n.policy({
        defaultEgress: "deny",
        defaultIngress: "allow",
        rules: [
            Rule.allowEgress(Destination.domain("api.openai.com")),
            Rule.allowEgress(Destination.domain("pypi.org")),
        ],
    }))
    .create();
python
from microsandbox import Network, NetworkPolicy, Rule, Sandbox

sb = await Sandbox.create(
    "sandbox",
    image="python",
    network=Network(
        policy=NetworkPolicy(rules=(
            Rule.allow(destination="api.openai.com"),
            Rule.allow(destination="pypi.org"),
        )),
    ),
)
</CodeGroup>

Extensible filesystem backends <sup><sup>coming soon</sup></sup>

The host controls what the guest sees at the filesystem level. Mount host directories, use managed volumes, or attach custom filesystem backends entirely. Hooks let you intercept reads and writes to do things like encrypt everything going to disk or proxy to remote storage, and the guest never knows the difference.

<CodeGroup>
rust
let sb = Sandbox::builder("encrypted")
    .image("python")
    .volume("/secrets", |v| v
        .bind("/data/secrets")
        .on_read(move |_path, data| decrypt(data, &key))
        .on_write(move |_path, data| encrypt(data, &key))
    )
    .create().await?;
typescript
// Filesystem hooks (onRead / onWrite) are coming soon for Node. For now,
// you can mount the host directory directly:
await using sb = await Sandbox.builder("encrypted")
    .image("python")
    .volume("/secrets", (m) => m.bind("/data/secrets"))
    .create();
python
from microsandbox import Sandbox, Volume

key = get_encryption_key()
sb = await Sandbox.create(
    "encrypted",
    image="python",
    volumes={
        "/secrets": Volume.bind("/data/secrets",
            on_read=lambda path, data: decrypt(data, key),
            on_write=lambda path, data: encrypt(data, key),
        ),
    },
)
</CodeGroup>

Snapshot and reuse setup state

Capture a stopped sandbox's writable upper layer as a portable artifact, then boot fresh sandboxes from it. Install dependencies once, snapshot, and msb run --snapshot ... workers that skip the setup phase. The artifact is a directory you can scp between machines or bundle into a .tar.zst. Snapshots today are disk-only and stopped-only; live VM-state snapshots are tracked as future work.

<CodeGroup>
rust
let sb = Sandbox::builder("baseline")
    .image("python")
    .create()
    .await?;

sb.exec("pip", ["install", "-r", "requirements.txt"]).await?;
sb.stop_and_wait().await?;

// Snapshot the stopped sandbox under a bare name
let h = Sandbox::get("baseline").await?;
let _snap = h.snapshot("after-pip-install").await?;

// Boot 10 workers from the same snapshot (each gets its own upper layer)
let workers: Vec<Sandbox> = futures::future::try_join_all(
    (0..10).map(|i| async move {
        Sandbox::builder(format!("worker-{i}"))
            .from_snapshot("after-pip-install")
            .create()
            .await
    })
).await?;
bash
msb run --name baseline --detach python -- sleep 3600
msb exec baseline -- pip install -r requirements.txt
msb stop baseline

msb snapshot create after-pip-install --from baseline

# Boot 10 workers from the snapshot
for i in $(seq 1 10); do
    msb run --name worker-$i --detach --snapshot after-pip-install -- sleep 3600
done
typescript
import { Sandbox } from "microsandbox";

const sb = await Sandbox.builder("baseline").image("python").create();
await sb.exec("pip", ["install", "-r", "requirements.txt"]);
await sb.stopAndWait();

const h = await Sandbox.get("baseline");
await h.snapshot("after-pip-install");

// Boot 10 workers from the same snapshot
const workers = await Promise.all(
    Array.from({ length: 10 }, (_, i) =>
        Sandbox.builder(`worker-${i}`).fromSnapshot("after-pip-install").create(),
    ),
);
python
import asyncio
from microsandbox import Sandbox

sb = await Sandbox.create("baseline", image="python")
await sb.exec("pip", ["install", "-r", "requirements.txt"])
await sb.stop_and_wait()

h = await Sandbox.get("baseline")
await h.snapshot("after-pip-install")

# Boot 10 workers from the same snapshot
workers = await asyncio.gather(
    *(Sandbox.create(f"worker-{i}", snapshot="after-pip-install") for i in range(10))
)
</CodeGroup>

Spawn sandboxes from sandboxes <sup><sup>coming soon</sup></sup>

Code running inside a sandbox can spawn peer sandboxes alongside itself. This is designed for multi-agent systems where agents need to create sub-environments. Peer sandboxes inherit nothing by default (their own network, filesystem, secrets), and the orchestrator coordinates via the SDK, not shared state. If the code isn't running inside a microsandbox, the spawn call fails safely.

Composable plugin system <sup><sup>coming soon</sup></sup>

Extend microsandbox with in-process Rust plugins or out-of-process plugins in any language. Plugins hook into lifecycle events, exec calls, filesystem operations, or network traffic. They compose naturally, so you can stack an audit logger, a rate limiter, and a network monitor without them knowing about each other.

Bidirectional events <sup><sup>coming soon</sup></sup>

Guest processes can emit typed events to the host, and the host can send events back. No special libraries required in the guest since any language can write JSON to a socket. This enables structured communication beyond stdin/stdout, like progress reporting, task completion signals, or custom RPC.

<CodeGroup>
rust
// Receive events from guest
let _sub = sb.on_event("task.progress", |event: EventData| {
    if let Ok(p) = event.data.unwrap().deserialized::<Progress>() {
        println!("[{}%] {}", p.percent, p.message);
    }
});

// Send events to guest
sb.emit("task.start", TaskConfig {
    input_file: "/data/input.txt".into(),
    output_dir: "/data/output".into(),
}).await?;
typescript
// Receive events from guest
const sub = sb.onEvent("task.progress", (event: EventData) => {
    const { percent, message } = event.data as { percent: number; message: string }
    console.log(`[${percent}%] ${message}`)
})

// Send events to guest
await sb.emit("task.start", { inputFile: "/data/input.txt", options: ["--verbose"] })
python
# Receive events from guest
sub = sb.on_event("task.progress", lambda event: (
    print(f"[{event.data['percent']}%] {event.data['message']}")
))

# Send events to guest
await sb.emit("task.start", {
    "input_file": "/data/input.txt",
    "output_dir": "/data/output",
})
</CodeGroup>

Next steps

<CardGroup cols={2}> <Card title="Quickstart" icon="bolt" href="/getting-started/quickstart"> Get a sandbox running in under 5 minutes </Card> <Card title="Sandbox overview" icon="box" href="/sandboxes/overview"> Configuration, rootfs sources, and lifecycle </Card> <Card title="CLI reference" icon="square-terminal" href="/cli/overview"> Manage sandboxes from the terminal </Card> <Card title="SDK reference" icon="code" href="/sdk/rust"> Full API documentation for all SDKs </Card> </CardGroup>