# 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](#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.
