microsandbox-rb
Lightweight microVM sandboxes for Ruby — run AI agents and untrusted code with hardware-level isolation.
The microsandbox-rb gem provides native bindings to the microsandbox runtime via a Rust extension (magnus). It spins up real microVMs (not containers) in under 100 ms, runs standard OCI (Docker) images, and gives you full control over command execution, the guest filesystem, networking, and metrics — all from an idiomatic, synchronous Ruby API. There is no daemon to install and no server to connect to: the runtime is embedded directly in your process.
This is an unofficial, community-maintained Ruby implementation — not part of the official SDK family (Rust, TypeScript, Python, Go) — though it wraps the same core engine.
Features
- Hardware isolation — each sandbox is a real VM with its own Linux kernel
- Sub-100 ms boot — no daemon, no server setup, embedded directly in your app
- OCI image support — pull and run images from Docker Hub, GHCR, ECR, or any OCI registry
- Command execution — run commands or shell scripts and collect output
- Guest filesystem access — read, write, list, copy, stat files inside a running sandbox
- Metrics & logs — CPU, memory, disk and network I/O; captured stdout/stderr/system logs
- Idiomatic Ruby — keyword arguments, block-scoped lifecycle, a typed error hierarchy
- Thread-friendly — the GVL is released during sandbox calls, so other Ruby threads keep running
Requirements
- Ruby >= 3.1
- Linux with KVM enabled, or macOS on Apple Silicon (M-series)
- Building from source additionally needs a Rust toolchain (stable >= 1.91)
Installation
The gem is published as microsandbox-rb, but you still require "microsandbox"
(the microsandbox package name was already taken on RubyGems):
# Gemfile
gem "microsandbox-rb", require: "microsandbox"
bundle install
# or
gem install microsandbox-rb
The first build downloads the msb runtime and libkrunfw firmware into
~/.microsandbox. You can (re)provision them explicitly at any time:
Microsandbox.install unless Microsandbox.installed?
Quick start
require "microsandbox"
Microsandbox::Sandbox.create("hello", image: "python") do |sb|
output = sb.exec("python", ["-c", "print('Hello, World!')"])
puts output.stdout # => "Hello, World!\n"
puts output.success? # => true
end
# the sandbox is stopped automatically when the block returns
Usage
Lifecycle
# Block form — recommended; stops the sandbox automatically (even on error)
Microsandbox::Sandbox.create("box", image: "alpine") do |sb|
# ...
end
# Manual form — you are responsible for stopping it
sb = Microsandbox::Sandbox.create("box", image: "alpine")
begin
# ...
ensure
sb.stop # graceful (sb.stop(timeout: 5) to bound the wait)
# sb.kill # force (SIGKILL)
end
# Inspect / manage existing sandboxes
Microsandbox::Sandbox.list # => [Microsandbox::SandboxInfo, ...]
Microsandbox::Sandbox.get("box") # => Microsandbox::SandboxInfo
Microsandbox::Sandbox.start("box") # restart a stopped sandbox
Microsandbox::Sandbox.remove("box") # remove a stopped sandbox
Configuration
Microsandbox::Sandbox.create(
"configured",
image: "python",
cpus: 2,
memory: 1024, # MiB
env: { "API_BASE" => "https://example.com" },
workdir: "/app",
labels: { "team" => "research" },
ports: { 8080 => 80 }, # host => guest (TCP)
network: "public_only", # or "none" for airgapped
replace: true # replace an existing sandbox of the same name
) do |sb|
# ...
end
Executing commands
Microsandbox::Sandbox.create("exec-demo", image: "alpine") do |sb|
# Direct command (no shell)
out = sb.exec("ls", ["-la", "/etc"], cwd: "/", timeout: 30)
out.exit_code # => 0
out.success? # => true
out.stdout # => "..." (UTF-8)
out.stderr_bytes # => raw ASCII-8BIT bytes
# Shell script (pipes, redirects, &&)
sb.shell("cat /etc/os-release | grep VERSION").stdout
# Environment, stdin, working directory
sb.exec("cat", [], stdin: "piped data")
sb.exec("sh", ["-c", "echo $GREETING"], env: { "GREETING" => "hi" })
end
A non-zero exit is not an error — inspect exit_code/success?. Spawn-time
failures (e.g. command not found) and timeouts raise typed errors (see below).
Guest filesystem
Microsandbox::Sandbox.create("fs-demo", image: "alpine") do |sb|
sb.fs.write("/tmp/data.txt", "hello")
sb.fs.read_text("/tmp/data.txt") # => "hello" (UTF-8)
sb.fs.read("/tmp/data.txt") # => raw bytes (ASCII-8BIT)
sb.fs.exists?("/tmp/data.txt") # => true
sb.fs.mkdir("/tmp/sub")
sb.fs.copy("/tmp/data.txt", "/tmp/sub/copy.txt")
sb.fs.rename("/tmp/sub/copy.txt", "/tmp/sub/renamed.txt")
sb.fs.list("/tmp/sub") # => [Microsandbox::FsEntry, ...]
sb.fs.stat("/tmp/data.txt") # => Microsandbox::FsMetadata
# Host <-> guest copies
sb.fs.copy_from_host("./local.txt", "/tmp/local.txt")
sb.fs.copy_to_host("/tmp/out.txt", "./out.txt")
end
Metrics & logs
Microsandbox::Sandbox.create("obs", image: "alpine") do |sb|
m = sb.metrics # => Microsandbox::Metrics
m.cpu_percent
m.memory_bytes
m.uptime_secs
sb.logs(tail: 100, sources: ["stdout", "stderr"]).each do |entry|
puts "[#{entry.source}] #{entry.text}"
end
end
Streaming output
For long-running commands, stream events as they arrive instead of waiting:
Microsandbox::Sandbox.create("stream", image: "python") do |sb|
handle = sb.exec_stream("python", ["-u", "-c", "import time\nfor i in range(3): print(i); time.sleep(1)"])
handle.each do |event| # ExecHandle is Enumerable
print event.text if event.stdout?
end
# or: out = handle.collect → ExecOutput (drain to the end)
# interactive stdin:
# sink = handle.stdin; sink.write("data\n"); sink.close
# control: handle.signal(15), handle.kill, handle.resize(rows, cols)
end
Images
Manage the local OCI image cache (images are pulled automatically on create):
Microsandbox::Image.list # => [Microsandbox::ImageInfo, ...]
Microsandbox::Image.get("alpine") # => Microsandbox::ImageInfo
Microsandbox::Image.inspect("alpine").layers # => [{...}, ...]
Microsandbox::Image.remove("alpine", force: true)
report = Microsandbox::Image.prune
report.bytes_reclaimed
Named volumes
Persistent storage that outlives individual sandboxes:
Microsandbox::Volume.create("cache", kind: "disk", size_mib: 512)
Microsandbox::Volume.list # => [Microsandbox::VolumeInfo, ...]
Microsandbox::Sandbox.create("with-vol", image: "alpine",
volumes: { "/data" => { named: "cache" } }) do |sb|
sb.fs.write("/data/state.txt", "persisted")
end
Microsandbox::Volume.remove("cache")
volumes: accepts a host path String (bind mount) or { bind: "/host" } /
{ named: "volume-name" } per guest path. Boot from a snapshot with
Sandbox.create(name, from_snapshot: "snap-name-or-path").
Error handling
All errors descend from Microsandbox::Error and carry a stable #code:
begin
Microsandbox::Sandbox.create("dup", image: "alpine")
Microsandbox::Sandbox.create("dup", image: "alpine") # name clash
rescue Microsandbox::SandboxAlreadyExistsError => e
warn "#{e.code}: #{e.}" # => "sandbox-already-exists: ..."
rescue Microsandbox::Error => e
warn "microsandbox failed: #{e.}"
end
| Class | #code |
|---|---|
InvalidConfigError |
invalid-config |
SandboxNotFoundError |
sandbox-not-found |
SandboxAlreadyExistsError |
sandbox-already-exists |
SandboxStillRunningError |
sandbox-still-running |
ExecTimeoutError |
exec-timeout |
ExecFailedError |
exec-failed |
FilesystemError |
filesystem-error |
ImageNotFoundError |
image-not-found |
MetricsDisabledError / MetricsUnavailableError |
metrics-disabled / metrics-unavailable |
| … | (see lib/microsandbox/errors.rb) |
Runtime configuration
The msb runtime path is resolved in this order: the MSB_PATH environment
variable → an SDK-set override → the config file → ~/.microsandbox/bin/msb →
msb on PATH.
Microsandbox.installed? # => true/false
Microsandbox.install # download + install the runtime (idempotent)
Microsandbox.runtime_path # => "/Users/you/.microsandbox/bin/msb"
Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override
Development
bin/setup # bundle install
bundle exec rake compile # build the native extension (debug)
bundle exec rake compile:release # build optimized
bundle exec rake spec # run unit specs (no runtime needed)
MICROSANDBOX_INTEGRATION=1 bundle exec rake spec:all # + real microVM specs
Unit specs run without a runtime. Integration specs boot real microVMs and are
opt-in via MICROSANDBOX_INTEGRATION=1 (override the test image with
MICROSANDBOX_TEST_IMAGE).
The native extension depends on the microsandbox core crate via a pinned git
tag, so it builds without an adjacent checkout. To develop against a sibling
microsandbox/ checkout instead (faster, reflects local runtime changes):
cp .cargo/config.toml.example .cargo/config.toml # gitignored path override
bundle exec rake compile
Releasing
Releases are automated by .github/workflows/release.yml via RubyGems
Trusted Publishing (OIDC) — there is no API key to store as a secret.
One-time setup (before the first release), create a pending trusted publisher at https://rubygems.org/profile/oidc/pending_trusted_publishers:
| Field | Value |
|---|---|
| RubyGems gem name | microsandbox-rb |
| Repository owner | ya-luotao |
| Repository name | microsandbox-rb |
| Workflow filename | release.yml |
| Environment | (leave blank) |
On the first successful push the pending publisher auto-converts to a permanent one bound to the gem.
Each release:
- Bump
Microsandbox::VERSION(and thetag = "vX.Y.Z"on the core-crate dependency inext/microsandbox/Cargo.toml) to match the upstream runtime, updateCHANGELOG.md. - (Recommended first time) run the workflow's manual
dry_rundispatch to build all platform gems without publishing — confirms thearm64-darwincross-build succeeds. - Push a
vX.Y.Ztag. CI builds precompiled, multi-ABI platform gems (x86_64-linux,aarch64-linux,arm64-darwin) withrake-compiler-dock, plus the source gem, and pushes them to RubyGems. The publish job usesrubygems/configure-rubygems-credentials(OIDC) withid-token: write— noRUBYGEMS_API_KEYsecret required.
See DESIGN.md for the architecture and the implemented-surface
section for what's covered today vs. on the roadmap. Covered: full sandbox
lifecycle (including the async request_stop/request_kill/request_drain/
wait_until_stopped/detach/owns_lifecycle? controls and label-filtered
list_with), exec/shell (collected and streaming), the full guest
filesystem, metrics (per-sandbox, Microsandbox.all_sandbox_metrics, and
streaming metrics_stream/log_stream), logs, OCI image-cache management,
named volumes, and snapshots (create/list/verify/export/import +
boot-from-snapshot). Create options span resources, network policy presets,
log_level/security/rlimits/pull_policy/secrets and more; exec/shell
take per-call rlimits. Still on the roadmap: custom per-rule network policies,
file patches, registry auth, interactive attach, SSH, and the raw agent client.
License
Apache-2.0. See LICENSE.