docs/getting-started/introduction.mdx
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>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?;
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();
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(),
)
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>let sb = Sandbox::builder("sandbox")
.image("python")
.secret_env("OPENAI_API_KEY", api_key, "api.openai.com")
.create()
.await?;
await using sb = await Sandbox.builder("sandbox")
.image("python")
.secretEnv("OPENAI_API_KEY", process.env.OPENAI_API_KEY!, "api.openai.com")
.create();
sb = await Sandbox.create(
"sandbox",
image="python",
secrets=[
Secret.env("OPENAI_API_KEY",
value=os.environ["OPENAI_API_KEY"],
allow_hosts=["api.openai.com"],
),
],
)
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>let sb = Sandbox::builder("sandbox")
.image("python")
.network(|n| n
.policy(NetworkPolicy::allowlist(["api.openai.com", "pypi.org"]))
)
.create()
.await?;
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();
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"),
)),
),
)
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>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?;
// 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();
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),
),
},
)
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.
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?;
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
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(),
),
);
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))
)
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.
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.
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>// 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?;
// 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"] })
# 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",
})