microsandbox-rb 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +229 -8
  3. data/Cargo.lock +45 -46
  4. data/DESIGN.md +90 -72
  5. data/README.md +178 -669
  6. data/SECURITY.md +65 -0
  7. data/docs/configuration.md +154 -0
  8. data/docs/errors.md +74 -0
  9. data/docs/execution.md +108 -0
  10. data/docs/filesystem.md +45 -0
  11. data/docs/images.md +65 -0
  12. data/docs/lifecycle.md +141 -0
  13. data/docs/observability.md +99 -0
  14. data/docs/releasing.md +117 -0
  15. data/docs/runtime.md +154 -0
  16. data/docs/surface.md +75 -0
  17. data/ext/microsandbox/Cargo.toml +9 -5
  18. data/ext/microsandbox/src/agent.rs +13 -11
  19. data/ext/microsandbox/src/backend.rs +1 -1
  20. data/ext/microsandbox/src/exec.rs +12 -8
  21. data/ext/microsandbox/src/fs_stream.rs +19 -7
  22. data/ext/microsandbox/src/host_path.rs +108 -0
  23. data/ext/microsandbox/src/image.rs +2 -2
  24. data/ext/microsandbox/src/lib.rs +3 -2
  25. data/ext/microsandbox/src/runtime.rs +189 -13
  26. data/ext/microsandbox/src/sandbox.rs +149 -102
  27. data/ext/microsandbox/src/snapshot.rs +17 -11
  28. data/ext/microsandbox/src/ssh.rs +22 -16
  29. data/ext/microsandbox/src/stream.rs +6 -2
  30. data/ext/microsandbox/src/volume.rs +50 -17
  31. data/lib/microsandbox/fs.rb +4 -8
  32. data/lib/microsandbox/image.rb +2 -2
  33. data/lib/microsandbox/log_entry.rb +1 -1
  34. data/lib/microsandbox/metrics.rb +1 -1
  35. data/lib/microsandbox/outbound_proxy.rb +8 -0
  36. data/lib/microsandbox/sandbox.rb +128 -13
  37. data/lib/microsandbox/snapshot.rb +1 -1
  38. data/lib/microsandbox/ssh.rb +2 -10
  39. data/lib/microsandbox/streams.rb +8 -5
  40. data/lib/microsandbox/version.rb +1 -1
  41. data/lib/microsandbox/volume.rb +31 -14
  42. data/lib/microsandbox-rb.rb +7 -0
  43. data/lib/microsandbox.rb +88 -31
  44. data/sig/microsandbox.rbs +24 -0
  45. metadata +14 -1
data/README.md CHANGED
@@ -1,615 +1,225 @@
1
1
  # microsandbox-rb
2
2
 
3
- Lightweight microVM sandboxes for Ruby — run AI agents and untrusted code with hardware-level isolation.
4
-
5
- The `microsandbox-rb` gem provides native bindings to the [microsandbox](https://github.com/superradcompany/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.
6
-
7
- This is an **unofficial, community-maintained** Ruby implementation — not part of the official SDK family — though it wraps the same core engine.
8
-
9
- ## Upstream & acknowledgements
10
-
11
- `microsandbox-rb` exists only because of the excellent work by the [Super Rad
12
- Company](https://github.com/superradcompany) team on the upstream
13
- **microsandbox** runtime. All the hard parts — the microVM engine, the guest
14
- `agentd`, the networking stack — are theirs; this gem is a thin Ruby skin over
15
- them. Our deepest thanks to the maintainers and community. 🙏
16
-
17
- - **Website & docs** — <https://microsandbox.dev> · [documentation](https://docs.microsandbox.dev)
18
- - **Official repository** — [superradcompany/microsandbox](https://github.com/superradcompany/microsandbox)
19
- - **Official SDKs** —
20
- [Rust](https://github.com/superradcompany/microsandbox/tree/main/sdk) ·
21
- [Python](https://github.com/superradcompany/microsandbox/tree/main/sdk/python) ·
22
- [TypeScript / Node](https://github.com/superradcompany/microsandbox/tree/main/sdk/node-ts) ·
23
- [Go](https://github.com/superradcompany/microsandbox/tree/main/sdk/go) ·
24
- [Ruby](https://github.com/superradcompany/microsandbox/tree/main/sdk/ruby)
25
- (since upstream `v0.6.9` there is an **official** `microsandbox` gem — a
26
- compact veneer over the same Rust SDK. This gem predates it and covers a
27
- larger surface (snapshots, SSH, streaming, volumes fs, network policy DSL,
28
- RBS types); both define the `Microsandbox` module, so use one or the other,
29
- not both, in a single process.)
30
- - **Agents** — [Agent Skills](https://github.com/superradcompany/skills) · [MCP server](https://github.com/superradcompany/microsandbox-mcp)
31
- - **Community** — [Discord](https://discord.gg/T95Y3XnEAK)
32
-
33
- > **Not affiliated.** This gem is an independent, community-maintained project.
34
- > It is **not** built, endorsed, or supported by Super Rad Company or the
35
- > microsandbox team. Please don't direct questions about this gem to the
36
- > upstream project — open an issue here instead. **Contributions are very
37
- > welcome — PRs welcome!** See [Contributing](#contributing).
38
-
39
- ## Features
40
-
41
- - **Hardware isolation** — each sandbox is a real VM with its own Linux kernel
42
- - **Sub-100 ms boot** — no daemon, no server setup, embedded directly in your app
43
- - **OCI image support** — pull and run images from Docker Hub, GHCR, ECR, or any OCI registry
44
- - **Command execution** — run commands or shell scripts and collect output
45
- - **Guest filesystem access** — read, write, list, copy, stat files inside a running sandbox
46
- - **Metrics & logs** — CPU, memory, disk and network I/O; captured stdout/stderr/system logs
47
- - **Rootfs patches** — inject files, dirs, and symlinks into the image before boot (`Microsandbox::Patch`)
48
- - **Fine-grained networking** — composable profiles (`:public`/`:private`/`:host`) *and* custom CIDR/domain/group allow-deny rules (`Microsandbox::NetworkPolicy`)
49
- - **SSH & SFTP** — native in-process SSH client/server and file transfer (`Sandbox#ssh`)
50
- - **Raw agent client** — byte-level access to the guest `agentd` protocol (`Microsandbox::AgentClient`)
51
- - **Idiomatic Ruby** — keyword arguments, block-scoped lifecycle, a typed error hierarchy
52
- - **Thread-friendly** — the GVL is released during sandbox calls, so _other_ Ruby threads keep running. The _calling_ thread blocks uninterruptibly until the call returns (`Timeout::timeout`/`Thread#kill`/Ctrl-C can't interrupt a blocked native call), so bound long-running work with a real deadline where one exists: `exec(timeout:)` / `shell(timeout:)` kill the guest command after N seconds, and `AgentClient.connect_sandbox`/`connect_path` take a `timeout:` that bounds only the connect handshake. The streaming paths and `AgentClient#request`/`#stream` have no timeout knob and can block indefinitely if the guest wedges — see DESIGN.md
53
-
54
- ## Requirements
55
-
56
- - **Ruby** >= 3.1
57
- - **Linux** with KVM enabled, or **macOS** on Apple Silicon (M-series)
58
- - A **Rust** toolchain (stable >= 1.91) — needed only when installing the source
59
- gem (it compiles the native extension on install). Precompiled per-platform
60
- gems, where available, require no Rust toolchain; see [Releasing](#releasing)
61
- - The **`msb` runtime + `libkrunfw` firmware** — shipped by the optional
62
- companion gem `microsandbox-rb-binaries`, or downloaded into `~/.microsandbox`
63
- on first use; see [The runtime binaries](#the-runtime-binaries). Cloud-only
64
- users (`MSB_BACKEND=cloud`) need no local runtime at all
3
+ [![Gem Version](https://img.shields.io/gem/v/microsandbox-rb)](https://rubygems.org/gems/microsandbox-rb)
4
+ [![CI](https://github.com/ya-luotao/microsandbox-rb/actions/workflows/ci.yml/badge.svg)](https://github.com/ya-luotao/microsandbox-rb/actions/workflows/ci.yml)
5
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-CC342D)](https://www.ruby-lang.org/)
6
+ [![Docs](https://img.shields.io/badge/docs-rubydoc.info-blue)](https://rubydoc.info/gems/microsandbox-rb)
7
+ [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-green)](LICENSE)
65
8
 
66
- ## Installation
67
-
68
- The gem is published as **`microsandbox-rb`**, but you still `require "microsandbox"`
69
- (the `microsandbox` package name was already taken on RubyGems):
9
+ Lightweight microVM sandboxes for Ruby. Run AI agents and untrusted code with hardware-level isolation from an idiomatic, **synchronous** Ruby API. The gem embeds the [microsandbox](https://github.com/superradcompany/microsandbox) runtime through a Rust (magnus) native extension: it boots real microVMs, not containers, in under 100 ms, runs standard OCI images, and needs no daemon or server.
70
10
 
71
- ```ruby
72
- # Gemfile
73
- gem "microsandbox-rb", require: "microsandbox"
74
- ```
11
+ > **Unofficial and community-maintained.** This gem is an independent project, not built, endorsed, or supported by [Super Rad Company](https://github.com/superradcompany) or the microsandbox team. All the hard parts (the microVM engine, the guest `agentd`, the networking stack) are theirs; this gem is a Ruby skin over them, tracked release by release. Please open issues [here](https://github.com/ya-luotao/microsandbox-rb/issues), not upstream. Upstream links: [website](https://microsandbox.dev) · [docs](https://docs.microsandbox.dev) · [official SDKs](https://github.com/superradcompany/microsandbox/tree/main/sdk) · [Agent Skills](https://github.com/superradcompany/skills) · [MCP server](https://github.com/superradcompany/microsandbox-mcp) · [Discord](https://discord.gg/T95Y3XnEAK).
75
12
 
76
- ```bash
77
- bundle install
78
- # or
79
- gem install microsandbox-rb
80
- ```
13
+ ## Highlights
81
14
 
82
- Installing the **source gem** compiles the Rust extension, so the first install
83
- takes a few minutes and needs a Rust toolchain (`rustc >= 1.91`) on `PATH`. When
84
- a **precompiled platform gem** is available for your OS/architecture, RubyGems
85
- picks it automatically and no Rust toolchain is required.
15
+ - **Real VMs, not containers.** Every sandbox has its own Linux kernel under KVM (Linux) or the Hypervisor framework (Apple Silicon). Sub-100 ms boot; the runtime lives in your process.
16
+ - **Synchronous, idiomatic Ruby.** Keyword arguments, block-scoped lifecycle, a flat typed error hierarchy with stable `#code`s, and hand-maintained RBS signatures. The GVL is released during native calls, so other threads keep running.
17
+ - **The whole core surface.** Collected *and* streaming exec, logs, metrics, and filesystem handles; live `modify` with dry-run plans; snapshots; named volumes with host-side access; image cache management; interactive `attach`; in-process SSH/SFTP; a raw `agentd` client.
18
+ - **Network policy as data.** Composable profiles plus per-rule CIDR/domain allow-deny policies, TLS interception, outbound SOCKS4/SOCKS5 proxies, fail-closed strict hostname mode, and secrets injection with host allow-lists.
19
+ - **Convergent lifecycle.** `connect_or_create`, `restart`, `destroy`, and `wait_for_status` take a name to the state you want and refuse to touch a sandbox someone recreated under the same name.
20
+ - **Hermetic runtime.** The optional `microsandbox-rb-binaries` companion gem vendors a checksum-verified `msb` + `libkrunfw` in lockstep with the SDK, so production never downloads on first use. Without it, the runtime is provisioned into `~/.microsandbox` on demand.
21
+ - **Local or cloud.** The same API runs against the local libkrun backend or the microsandbox cloud, selected per process or per block.
86
22
 
87
- ### The runtime binaries
23
+ ## Installation
88
24
 
89
- `microsandbox-rb` is **SDK-only** — it wraps the microVM runtime, it doesn't
90
- carry it. The host-side `msb` runtime and the `libkrunfw` firmware come from an
91
- optional companion gem, **`microsandbox-rb-binaries`**, published as one gem per
92
- platform (`arm64-darwin`, `x86_64-linux-gnu`, `aarch64-linux-gnu`) and
93
- versioned in lockstep with this gem:
25
+ The gem is published as **`microsandbox-rb`** but is required as `microsandbox`
26
+ (the `microsandbox` name was already taken on RubyGems):
94
27
 
95
28
  ```ruby
96
- # Gemfile — install both gems at the same version
29
+ # Gemfile
97
30
  gem "microsandbox-rb", require: "microsandbox"
98
- gem "microsandbox-rb-binaries"
31
+ gem "microsandbox-rb-binaries" # optional: vendored msb runtime + libkrunfw firmware
99
32
  ```
100
33
 
101
- That's all the wiring there is: `require "microsandbox"` finds the companion
102
- gem, checks it is the same version and built for the same upstream runtime, and points the resolver
103
- at its vendored `msb` — so your bundle carries the runtime and nothing is
104
- downloaded at install time or on first call. **Recommended whenever you boot
105
- local microVMs.** Cloud-only users (`MSB_BACKEND=cloud`) should skip it: it is a
106
- separate, optional gem precisely so nobody has to fetch ~50 MB of binaries they
107
- won't run. Neither gem depends on the other.
108
-
109
- > **Availability.** The binaries gems are published on every release tag
110
- > alongside `microsandbox-rb` (first release to ship them: the one after
111
- > 0.13.0). On an older SDK version, or a platform without a bundle, use the
112
- > fallback below (or build them yourself from `binaries/` — see that
113
- > directory's README).
114
-
115
- **Fallback — first-use download.** Without the companion gem, the `msb` runtime
116
- and `libkrunfw` firmware are provisioned into `~/.microsandbox` automatically on
117
- first use (the first `Sandbox.create`/`start` downloads them if missing). To
118
- provision ahead of time — e.g. while baking a container image, or to avoid the
119
- first-call latency — call `install` explicitly:
34
+ Then `bundle install`, or `gem install microsandbox-rb`. Installing the source
35
+ gem compiles the Rust extension, so the first install takes a few minutes.
36
+
37
+ **Prerequisites**
38
+
39
+ - Ruby 3.1 or newer.
40
+ - Linux with KVM enabled, or macOS on Apple Silicon. Cloud-only users
41
+ (`MSB_BACKEND=cloud`) need neither.
42
+ - A stable Rust toolchain (1.91 or newer) on `PATH` to compile the source gem.
43
+ Precompiled extension gems are not published; see
44
+ [docs/releasing.md](docs/releasing.md).
45
+ - The `msb` runtime and `libkrunfw` firmware, from the companion gem above or
46
+ downloaded on first use. Provision ahead of time (a Docker layer, an
47
+ air-gapped host) with:
120
48
 
121
49
  ```ruby
122
50
  Microsandbox.install unless Microsandbox.installed?
123
51
  ```
124
52
 
125
- Set `MICROSANDBOX_NO_AUTO_INSTALL` to disable the automatic first-use download
126
- (e.g. on air-gapped hosts that provision the runtime out of band). None of this
127
- applies when the companion gem supplies the runtime: its binaries are already the
128
- matching version, so `ensure_runtime!` skips the installer entirely and nothing
129
- is written to `~/.microsandbox`.
53
+ The companion gem ships one platform gem each for `arm64-darwin`,
54
+ `x86_64-linux-gnu`, and `aarch64-linux-gnu`, must match the SDK version, and is
55
+ found automatically at `require "microsandbox"`. See
56
+ [docs/runtime.md](docs/runtime.md) for the resolution order, `MSB_PATH`, and
57
+ disabling the first-use download.
130
58
 
131
59
  ## Quick start
132
60
 
133
61
  ```ruby
134
62
  require "microsandbox"
135
63
 
136
- Microsandbox::Sandbox.create("hello", image: "public.ecr.aws/docker/library/python:3-slim") do |sb|
64
+ Microsandbox::Sandbox.create("hello", image: "public.ecr.aws/docker/library/python:3-slim",
65
+ ephemeral: true) do |sb|
137
66
  output = sb.exec("python", ["-c", "print('Hello, World!')"])
138
67
  puts output.stdout # => "Hello, World!\n"
139
68
  puts output.success? # => true
140
69
  end
141
- # the sandbox is stopped automatically when the block returns
70
+ # the block form stops the sandbox when it returns; `ephemeral: true` also
71
+ # removes it, so the name is free the next time this runs
142
72
  ```
143
73
 
144
- > **Why `public.ecr.aws/docker/library/...`?** The examples pull from AWS's
145
- > public mirror of the Docker Library because anonymous **Docker Hub** pulls are
146
- > rate-limited and often fail with `registry error: Not authorized`. Plain short
147
- > names like `image: "python"` work too if you aren't rate-limited. For private
148
- > or authenticated registries (including authenticated Docker Hub), pass
149
- > `registry_auth:` — see [Private & authenticated registries](#private--authenticated-registries).
74
+ Without `ephemeral: true` a stopped sandbox stays in the catalog under its
75
+ name (start it again with `Sandbox.start`, or delete it with `Sandbox.remove`),
76
+ and creating another one with the same name raises `SandboxAlreadyExistsError`.
77
+ See [docs/lifecycle.md](docs/lifecycle.md).
150
78
 
151
- ## Usage
79
+ Outside Bundler, write `require "microsandbox-rb"`: the separate `microsandbox`
80
+ gem on RubyGems also ships a `microsandbox.rb`, and plain
81
+ `require "microsandbox"` loads whichever is found first. Both define a
82
+ top-level `Microsandbox` module, so load only one of them in a process.
152
83
 
153
- ### Lifecycle
84
+ The examples pull from AWS's public Docker Library mirror because anonymous
85
+ Docker Hub pulls are rate-limited; `image: "python"` works too when you aren't.
86
+ See [docs/images.md](docs/images.md) for authenticated and private registries.
154
87
 
155
- ```ruby
156
- # Block form — recommended; stops the sandbox automatically (even on error)
157
- Microsandbox::Sandbox.create("box", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
158
- # ...
159
- end
160
-
161
- # Manual form — you are responsible for stopping it
162
- sb = Microsandbox::Sandbox.create("box", image: "public.ecr.aws/docker/library/alpine:latest")
163
- begin
164
- # ...
165
- ensure
166
- sb.stop # graceful (SIGTERM→SIGKILL escalation, 10s default)
167
- # sb.stop_and_wait # graceful, then wait → ExitStatus(#exit_code, #success?)
168
- # sb.kill # force (SIGKILL); sb.drain for a graceful drain
169
- end
170
-
171
- # Inspect / manage existing sandboxes. `get` returns a controllable
172
- # SandboxHandle (the live `stop`/`kill`/`drain`/`wait` live on the object from
173
- # `create`/`start`; fine-grained control lives on the handle). `list` returns a
174
- # cursor-paginated, Enumerable SandboxPage of handles (runtime v0.6.8).
175
- Microsandbox::Sandbox.list # => Microsandbox::SandboxPage (first page)
176
- Microsandbox::Sandbox.list.map(&:name) # enumerate the page's handles
177
- # next page: Sandbox.list_with(cursor: page.next_cursor, limit: 50)
178
- h = Microsandbox::Sandbox.get("box") # => Microsandbox::SandboxHandle
179
- h.status # :running, :stopped, :created, ...
180
- h.stop_with_timeout(5) # custom escalation timeout
181
- h.request_stop # fire-and-return; pair with #wait_until_stopped
182
- h.request_kill
183
- h.request_drain
184
- h.wait_until_stopped # => Microsandbox::SandboxStopResult
185
- Microsandbox::Sandbox.start("box") # restart a stopped sandbox
186
- Microsandbox::Sandbox.remove("box") # remove a stopped sandbox
187
- ```
188
-
189
- **Convergent lifecycle** (runtime `v0.6.16`) — idempotent operations that take a
190
- name to the state you want, whatever state it is in now:
88
+ ### Lifecycle
191
89
 
192
90
  ```ruby
193
- # Create it, or connect to (and start) the one that is already there. The
194
- # keyword options are `create`'s and apply only when a create actually happens.
91
+ # Create it, or connect to (and start) the one that already exists.
195
92
  sb = Microsandbox::Sandbox.connect_or_create("box", image: "public.ecr.aws/docker/library/alpine:latest")
196
-
197
- sb.id # opaque identity of the *persisted* sandbox — unlike
198
- # the reusable name, it changes on remove+recreate
199
- sb.wait_for_status(:running) # => SandboxHandle (no built-in timeout)
200
- sb = sb.restart # stop + start; => a new live Sandbox
201
- sb.destroy # stop + remove, in one step
202
-
203
- h = Microsandbox::Sandbox.get("box")
204
- h.connect_or_start # connect if running, start if not => Sandbox
205
- h.restart(force: true, timeout: 5)
206
- h.destroy
207
- ```
208
-
209
- Everything that acts on an *existing* receiver — `wait_for_status`, `restart`,
210
- `destroy`, `connect_or_start` — compares the identity it was bound to against
211
- the name's current owner first, raising `Microsandbox::SandboxReplacedError`
212
- rather than touching a sandbox someone else recreated under the same name.
213
- (`connect_or_create` is the entry point, so it has no prior identity to check:
214
- it simply converges on whichever sandbox now owns the name.)
215
-
216
- `connect_or_create`'s block form stops the sandbox only when the call owns its
217
- lifecycle — i.e. when it created it attached. A sandbox it merely connected to,
218
- or started with `detached: true`, is left running. And the stop it does issue is
219
- scoped to that sandbox's `id`, so a name removed and recreated while the block
220
- ran cannot be taken down by the teardown either.
221
-
222
- > **v0.5.8 lifecycle change.** Upstream split the lifecycle into the live
223
- > `Sandbox` and a controllable `SandboxHandle`, and the gem mirrors it. The live
224
- > `Sandbox#stop`/`#kill` no longer take a `timeout:`; `#request_stop`/
225
- > `#request_kill`/`#request_drain`/`#wait_until_stopped` and a custom stop timeout
226
- > now live on the `SandboxHandle` from `Sandbox.get`. `Sandbox.get`/`.list` return
227
- > a `SandboxHandle` (was a read-only `SandboxInfo`, kept as a deprecated alias).
228
-
229
- ### Configuration
230
-
231
- ```ruby
232
- Microsandbox::Sandbox.create(
233
- "configured",
234
- image: "public.ecr.aws/docker/library/python:3-slim",
235
- cpus: 2,
236
- memory: 1024, # MiB
237
- env: { "API_BASE" => "https://example.com" },
238
- workdir: "/app",
239
- labels: { "team" => "research" },
240
- ports: { 8080 => 80 }, # host => guest (TCP)
241
- network: [:public], # composable profiles; :none for airgapped
242
- replace: true # replace an existing sandbox of the same name
243
- ) do |sb|
244
- # ...
245
- end
93
+ sb.status # => :running
94
+ sb = sb.restart # stop + start
95
+ sb.destroy # stop + remove
96
+
97
+ # Handles for existing sandboxes (cursor-paginated list, fine-grained control)
98
+ Microsandbox::Sandbox.list.map(&:name)
99
+ h = Microsandbox::Sandbox.get("box") # => Microsandbox::SandboxHandle
100
+ h.request_stop
101
+ h.wait_until_stopped # => Microsandbox::SandboxStopResult
246
102
  ```
247
103
 
248
- **Outbound proxy** (runtime `v0.6.17`) — route the sandbox's egress through a
249
- SOCKS4 (TCP) or SOCKS5 (TCP + non-DNS UDP) proxy with `proxy:`. The proxy is
250
- dialed by the runtime's host-side network stack, so its address is resolved
251
- from the host (`127.0.0.1` is the host's loopback), and the egress policy
252
- (`network:`) still governs which destinations may be reached. A SOCKS5 password
253
- comes from a host environment variable via `SecretSource.env` — only the
254
- variable's *name* is handed to the runtime. Local backend only.
255
-
256
- ```ruby
257
- Microsandbox::Sandbox.create("worker", image: "python",
258
- proxy: Microsandbox::OutboundProxy.socks5("127.0.0.1:1080"))
259
-
260
- Microsandbox::Sandbox.create("worker", image: "python",
261
- proxy: Microsandbox::OutboundProxy.socks5("10.0.0.5:1080")
262
- .credentials("sandbox", Microsandbox::SecretSource.env("PROXY_PASSWORD")))
263
-
264
- Microsandbox::Sandbox.create("worker", image: "python",
265
- proxy: Microsandbox::OutboundProxy.socks4("127.0.0.1:1080", user_id: "ci"))
104
+ See [docs/lifecycle.md](docs/lifecycle.md) for the live `Sandbox` /
105
+ `SandboxHandle` split, `detach`, and the identity checks behind
106
+ `SandboxReplacedError`.
266
107
 
267
- # The equivalent plain Hash works too:
268
- Microsandbox::Sandbox.create("worker", image: "python",
269
- proxy: { protocol: :socks5, address: "10.0.0.5:1080",
270
- credentials: { username: "sandbox", password: { env: "PROXY_PASSWORD" } } })
271
- ```
272
-
273
- **Strict hostname policy** (runtime `v0.6.18`) — `strict: true` makes a
274
- hostname-rule allow fail closed unless the runtime can actually see the request
275
- authority (plain-HTTP `Host`, or SNI/`:authority` under TLS interception); a
276
- bypassed or non-intercepted HTTPS flow allowed only by a hostname rule is then
277
- denied before the upstream dial. Default `false`; create-only. Independently of
278
- `strict:`, any policy with domain rules now checks plain-HTTP `Host` headers
279
- against it.
108
+ ### Running commands
280
109
 
281
110
  ```ruby
282
- Microsandbox::Sandbox.create("worker", image: "python",
283
- network: Microsandbox::NetworkPolicy.custom(default_egress: :deny,
284
- rules: [{ action: :allow, direction: :egress, protocol: :tcp, port: 443,
285
- destination: Microsandbox::Destination.domain("api.example.com") }]),
286
- strict: true)
287
- ```
288
-
289
- ### Executing commands
290
-
291
- ```ruby
292
- Microsandbox::Sandbox.create("exec-demo", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
293
- # Direct command (no shell)
111
+ Microsandbox::Sandbox.create("work", image: "public.ecr.aws/docker/library/python:3-slim") do |sb|
294
112
  out = sb.exec("ls", ["-la", "/etc"], cwd: "/", timeout: 30)
295
- out.exit_code # => 0
296
- out.success? # => true
297
- out.stdout # => "..." (UTF-8)
298
- out.stderr_bytes # => raw ASCII-8BIT bytes
299
-
300
- # Shell script (pipes, redirects, &&)
113
+ out.exit_code # non-zero exit is data, not an exception
301
114
  sb.shell("cat /etc/os-release | grep VERSION").stdout
302
115
 
303
- # Environment, stdin, working directory
304
- sb.exec("cat", [], stdin: "piped data")
305
- sb.exec("sh", ["-c", "echo $GREETING"], env: { "GREETING" => "hi" })
306
- end
307
- ```
308
-
309
- A non-zero exit is **not** an error — inspect `exit_code`/`success?`. Spawn-time
310
- failures (e.g. command not found) and timeouts raise typed errors (see below).
311
-
312
- **Default workload** (runtime `v0.6.9`): `create` is strictly boot-only — it
313
- never runs the image's `ENTRYPOINT`/`CMD`. Execute the image's own command
314
- explicitly:
315
-
316
- ```ruby
317
- Microsandbox::Sandbox.create("worker", image: "example/worker:latest",
318
- cmd: ["worker.py", "--once"]) do |sb| # cmd: overrides the durable image CMD
319
- out = sb.exec_default(timeout: 300) # buffered; exec-style options
320
- handle = sb.exec_default_stream # or streaming (returns an ExecHandle)
321
- sb.attach_default # or interactive (host TTY)
322
- end
323
- ```
324
-
325
- An image whose entrypoint and CMD resolve to no executable command raises
326
- `Microsandbox::NoDefaultCommandError`.
327
-
328
- ### Guest filesystem
329
-
330
- ```ruby
331
- Microsandbox::Sandbox.create("fs-demo", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
332
- sb.fs.write("/tmp/data.txt", "hello")
333
- sb.fs.read_text("/tmp/data.txt") # => "hello" (UTF-8)
334
- sb.fs.read("/tmp/data.txt") # => raw bytes (ASCII-8BIT)
335
- sb.fs.exists?("/tmp/data.txt") # => true
336
-
337
- sb.fs.mkdir("/tmp/sub")
338
- sb.fs.copy("/tmp/data.txt", "/tmp/sub/copy.txt")
339
- sb.fs.rename("/tmp/sub/copy.txt", "/tmp/sub/renamed.txt")
340
- sb.fs.list("/tmp/sub") # => [Microsandbox::FsEntry, ...]
341
- sb.fs.stat("/tmp/data.txt") # => Microsandbox::FsMetadata
342
-
343
- # Host <-> guest copies
344
- sb.fs.copy_from_host("./local.txt", "/tmp/local.txt")
345
- sb.fs.copy_to_host("/tmp/out.txt", "./out.txt")
346
- end
347
- ```
348
-
349
- ### Metrics & logs
350
-
351
- ```ruby
352
- Microsandbox::Sandbox.create("obs", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
353
- # On v0.6.x runtimes the metrics slot goes live a beat after create returns,
354
- # so `metrics` can briefly raise "no live metrics slot" right after boot —
355
- # retry for a few hundred ms rather than treating the first failure as fatal.
356
- m = sb.metrics # => Microsandbox::Metrics
357
- m.cpu_percent
358
- m.memory_bytes
359
- m.uptime_secs
360
-
361
- sb.logs(tail: 100, sources: ["stdout", "stderr"]).each do |entry|
362
- puts "[#{entry.source}] #{entry.text}"
363
- end
364
- end
365
- ```
366
-
367
- ### Live modification & health
368
-
369
- Resize, reconfigure, or rotate secrets on a sandbox **without recreating it**,
370
- and probe the guest agent's liveness. To leave headroom for a live CPU/memory
371
- resize, reserve a ceiling at create time with `max_cpus:`/`max_memory:`:
372
-
373
- ```ruby
374
- Microsandbox::Sandbox.create("live", image: "public.ecr.aws/docker/library/alpine:latest",
375
- cpus: 1, max_cpus: 4, memory: 512, max_memory: 2048) do |sb|
376
- # Health check — does NOT refresh the idle timer:
377
- ping = sb.ping # => Microsandbox::PingResult
378
- ping.latency_ms # round-trip latency
379
-
380
- # Explicitly refresh the idle-activity timer (resets any idle_timeout:):
381
- sb.touch.activity_seq # => Microsandbox::TouchResult
382
-
383
- # Preview a change without applying it:
384
- plan = sb.modify(cpus: 2, memory: 1024, dry_run: true)
385
- plan.applied? # => false
386
- plan.changes # => [{ kind: "config", field: "cpus", ... }, ...]
387
-
388
- # Live resize — applies to the running VM under the default :no_restart policy:
389
- sb.modify(cpus: 2, memory: 1024)
390
-
391
- # Grow the root disk (managed upper or flat, MiB — runtime v0.6.9). Applied
392
- # while stopped; growth-only:
393
- sb.modify(root_disk_size: 8192, policy: :next_start)
394
-
395
- # env/labels/workdir changes on a *running* sandbox require a restart, so the
396
- # default :no_restart policy rejects the whole apply (it raises rather than
397
- # partially applying). Persist them for the next start — or restart now —
398
- # by saying so explicitly:
399
- sb.modify(env: { "TIER" => "prod" }, remove_env: ["DEBUG"],
400
- labels: { "role" => "worker" }, policy: :next_start) # or policy: :restart
401
-
402
- # Rotating/removing an *existing* secret (or updating its allowed hosts) is
403
- # live; *adding* a new secret is restart-required, like env. Specs are keyed
404
- # by name; env:/store:/value: are mutually exclusive:
405
- sb.modify(
406
- secrets: { "API_KEY" => { env: "HOST_API_KEY", allowed_hosts: ["api.example.com"] } },
407
- remove_secrets: ["OLD_TOKEN"],
408
- )
409
- end
410
- ```
411
-
412
- The apply is **all-or-nothing**: under a given `policy:` every planned change
413
- must be applicable, or the whole `modify` raises — nothing is partially
414
- applied. Use `dry_run: true` to inspect each change's `disposition` (`"live"`,
415
- `"next start"`, `"requires restart"`) before committing.
416
-
417
- `SandboxHandle` (from `Sandbox.get`/`list`) carries the same `#ping`/`#touch`/
418
- `#modify`; on a stopped sandbox `#ping`/`#touch` raise `SandboxNotRunningError`.
419
-
420
- ### Streaming output
421
-
422
- For long-running commands, stream events as they arrive instead of waiting:
423
-
424
- ```ruby
425
- Microsandbox::Sandbox.create("stream", image: "public.ecr.aws/docker/library/python:3-slim") do |sb|
426
- handle = sb.exec_stream("python", ["-u", "-c", "import time\nfor i in range(3): print(i); time.sleep(1)"])
427
- handle.each do |event| # ExecHandle is Enumerable
116
+ # Stream events as they arrive; ExecHandle is a single-pass Enumerable
117
+ sb.exec_stream("python", ["-u", "-c", "for i in range(3): print(i)"]).each do |event|
428
118
  print event.text if event.stdout?
429
119
  end
430
- # or: out = handle.collect → ExecOutput (drain to the end)
431
- # interactive stdin — create the stream with stdin: :pipe to get a writable sink:
432
- # h = sb.exec_stream("cat", [], stdin: :pipe)
433
- # sink = h.stdin; sink.write("data\n"); sink.close # close sends EOF
434
- # control: handle.signal(15), handle.kill, handle.resize(rows, cols)
435
- end
436
- ```
437
-
438
- > **Streams are single-pass.** `ExecHandle`, `LogStream`, `MetricsStream`,
439
- > `FsReadStream`, `PullSession`, and `AgentStream` are `Enumerable`, but `each`
440
- > drains a one-shot native channel — they are forward-only, not rewindable, and
441
- > meant for a single consumer. Iterate (or `collect`/`read`) exactly once: a
442
- > second `each`, or a combinator after a partial drain (`count` then `each`,
443
- > `to_a` twice), silently yields nothing. Don't share one handle across threads.
444
-
445
- ### Images
446
120
 
447
- Manage the local OCI image cache (images are pulled automatically on `create`):
448
-
449
- ```ruby
450
- Microsandbox::Image.list # => [Microsandbox::ImageInfo, ...]
451
- Microsandbox::Image.get("public.ecr.aws/docker/library/alpine:latest") # => Microsandbox::ImageInfo
452
- Microsandbox::Image.inspect("public.ecr.aws/docker/library/alpine:latest").layers # => [{...}, ...]
453
- Microsandbox::Image.remove("public.ecr.aws/docker/library/alpine:latest", force: true)
454
- report = Microsandbox::Image.prune
455
- report.bytes_reclaimed
121
+ sb.fs.write("/tmp/data.txt", "hello")
122
+ sb.fs.read_text("/tmp/data.txt") # => "hello"
123
+ sb.fs.copy_to_host("/tmp/data.txt", "./data.txt")
124
+ end
456
125
  ```
457
126
 
458
- ### Private & authenticated registries
127
+ See [docs/execution.md](docs/execution.md) for stdin pipes, signals, the
128
+ image's default command, SSH, and the threading and timeout caveats, and
129
+ [docs/filesystem.md](docs/filesystem.md) for the guest filesystem API.
459
130
 
460
- Images are pulled automatically on `create`. For a private registry — or to lift
461
- Docker Hub's anonymous rate limit — pass `registry_auth:` with a username and a
462
- password or token:
131
+ ### Networking and secrets
463
132
 
464
133
  ```ruby
465
- Microsandbox::Sandbox.create(
466
- "private",
467
- image: "registry.example.com/team/app:latest",
468
- registry_auth: { username: "ci-bot", password: ENV.fetch("REGISTRY_TOKEN") }
134
+ Microsandbox::Sandbox.create("agent", image: "public.ecr.aws/docker/library/python:3-slim",
135
+ network: Microsandbox::NetworkPolicy.custom(default_egress: :deny,
136
+ rules: [{ action: :allow, direction: :egress, protocol: :tcp, port: 443,
137
+ destination: Microsandbox::Destination.domain("api.example.com") }]),
138
+ strict: true,
139
+ proxy: Microsandbox::OutboundProxy.socks5("127.0.0.1:1080"),
140
+ secrets: [{ env: "API_KEY", value: ENV.fetch("HOST_API_KEY"), host: "api.example.com" }]
469
141
  ) do |sb|
470
142
  # ...
471
143
  end
472
144
  ```
473
145
 
474
- For self-hosted registries you can also reach the registry over plain HTTP and
475
- trust a private CA:
146
+ See [docs/configuration.md](docs/configuration.md) for every create option,
147
+ including resources, ports, volumes, and root-disk layout.
476
148
 
477
- ```ruby
478
- Microsandbox::Sandbox.create(
479
- "internal",
480
- image: "registry.internal:5000/app:latest",
481
- registry_insecure: true, # plain HTTP instead of HTTPS
482
- registry_ca_certs: File.read("/etc/pki/internal-ca.pem") # String or Array of PEMs
483
- )
484
- ```
485
-
486
- Without `registry_auth:`, the core's default credential resolution still applies
487
- (OS keyring, global config, and `~/.docker/config.json`), so an existing
488
- `docker login` is honored automatically.
489
-
490
- ### Named volumes
491
-
492
- Persistent storage that outlives individual sandboxes:
493
-
494
- ```ruby
495
- Microsandbox::Volume.create("cache", kind: "disk", size_mib: 512)
496
- Microsandbox::Volume.list # => [Microsandbox::VolumeInfo, ...]
497
-
498
- Microsandbox::Sandbox.create("with-vol", image: "public.ecr.aws/docker/library/alpine:latest",
499
- volumes: { "/data" => { named: "cache" } }) do |sb|
500
- sb.fs.write("/data/state.txt", "persisted")
501
- end
502
-
503
- Microsandbox::Volume.remove("cache")
504
- ```
505
-
506
- `volumes:` accepts a host path String (bind mount) or `{ bind: "/host" }` /
507
- `{ named: "volume-name" }` per guest path. A bind or named mount may pin the
508
- fallback guest owner for host files with `uid:`/`gid:` (both required together,
509
- runtime `v0.6.15`). Boot from a snapshot with
510
- `Sandbox.create(name, from_snapshot: "snap-name-or-path")`.
511
-
512
- ### Error handling
513
-
514
- All errors descend from `Microsandbox::Error` and carry a stable `#code`:
515
-
516
- ```ruby
517
- begin
518
- Microsandbox::Sandbox.create("dup", image: "public.ecr.aws/docker/library/alpine:latest")
519
- Microsandbox::Sandbox.create("dup", image: "public.ecr.aws/docker/library/alpine:latest") # name clash
520
- rescue Microsandbox::SandboxAlreadyExistsError => e
521
- warn "#{e.code}: #{e.message}" # => "sandbox-already-exists: ..."
522
- rescue Microsandbox::Error => e
523
- warn "microsandbox failed: #{e.message}"
524
- end
525
- ```
526
-
527
- | Class | `#code` |
528
- |-------|---------|
529
- | `InvalidConfigError` | `invalid-config` |
530
- | `SandboxNotFoundError` | `sandbox-not-found` |
531
- | `SandboxAlreadyExistsError` | `sandbox-already-exists` |
532
- | `SandboxStillRunningError` | `sandbox-still-running` |
533
- | `ExecTimeoutError` | `exec-timeout` |
534
- | `ExecFailedError` | `exec-failed` |
535
- | `FilesystemError` | `filesystem-error` |
536
- | `ImageNotFoundError` | `image-not-found` |
537
- | `MetricsDisabledError` / `MetricsUnavailableError` | `metrics-disabled` / `metrics-unavailable` |
538
- | … | (see `lib/microsandbox/errors.rb`) |
539
-
540
- ## Runtime configuration
541
-
542
- The `msb` runtime path is resolved in this order: the `MSB_PATH` environment
543
- variable → the `microsandbox-rb-binaries` gem (or another SDK-set override) →
544
- the config file → `~/.microsandbox/bin/msb` → `msb` on `PATH`.
545
- `Microsandbox.runtime_path` reports the winner.
149
+ ### Observability and live modification
546
150
 
547
151
  ```ruby
548
- Microsandbox.installed? # => true/false
549
- Microsandbox.install # download + install the runtime (idempotent)
550
- Microsandbox.runtime_path # => "/Users/you/.microsandbox/bin/msb"
551
- Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override (set-once)
552
- Microsandbox.libkrunfw_path = "/opt/microsandbox/lib/libkrunfw.dylib" # override (set-once)
152
+ sb.metrics.cpu_percent # metrics slot goes live a beat after boot
153
+ sb.logs(tail: 100).each { |e| puts e.text }
154
+ sb.ping.latency_ms
155
+ sb.modify(cpus: 2, memory: 1024, dry_run: true).changes # preview, then apply
553
156
  ```
554
157
 
555
- When [`microsandbox-rb-binaries`](#the-runtime-binaries) is installed,
556
- `require "microsandbox"` claims that SDK-set slot with the gem's vendored `msb`
557
- (the firmware is found alongside it), and `runtime_path` points into the gem.
558
- The two gems are versioned in lockstep and the companion gem must be the same
559
- version **and** built for the same upstream runtime — a mismatch of either is reported with a warning and
560
- skipped, and the SDK falls back to `~/.microsandbox` rather than driving a
561
- runtime it doesn't match. Because the slot is **set-once**,
562
- `Microsandbox.runtime_path=` is then a no-op: use the `MSB_PATH` environment
563
- variable, which outranks it, to point at a different runtime.
564
-
565
- ### Backend routing
566
-
567
- As of v0.5.8 every operation runs through a backend. The default is the local
568
- libkrun backend; without any configuration nothing changes. A backend can be
569
- selected programmatically or via the environment:
570
-
571
- ```ruby
572
- Microsandbox.default_backend_kind # => :local (or :cloud)
573
- Microsandbox.set_default_backend(:cloud, url: "https://api.example.com", api_key: ENV["MSB_API_KEY"])
574
- # or a named profile from ~/.microsandbox/config.json:
575
- Microsandbox.set_default_backend(:cloud, profile: "prod")
576
-
577
- # Scoped override (restored afterward, even on error):
578
- Microsandbox.with_backend(:local) { Microsandbox::Sandbox.create("box", image: "alpine") { |sb| ... } }
579
- ```
580
-
581
- Resolution order when no backend is set programmatically: `MSB_BACKEND`
582
- (`local`/`cloud`) → `MSB_PROFILE` → the `active_profile` in
583
- `~/.microsandbox/config.json` (path overridable via `MSB_CONFIG_PATH`) →
584
- local. **Cloud intent must be explicit** (since runtime `v0.6.9`): a bare
585
- `MSB_API_KEY` is treated as credential material, not backend intent, and no
586
- longer selects the cloud on its own — pair it with `MSB_BACKEND=cloud` (which
587
- reads `MSB_API_URL`/`MSB_API_KEY`), or select a cloud profile. Invalid cloud
588
- configuration (e.g. `MSB_BACKEND=cloud` without a usable API key or cloud
589
- profile) fails closed with `Microsandbox::InvalidConfigError` instead of
590
- silently running locally. The cloud backend currently supports a subset of
591
- operations (create/start/stop/remove/get/list, one-shot exec, follow log
592
- streaming); unsupported operations raise `Microsandbox::UnsupportedError`.
158
+ See [docs/observability.md](docs/observability.md).
159
+
160
+ ## Documentation
161
+
162
+ | Topic | Guide |
163
+ |-------|-------|
164
+ | Create, stop, handles, convergent lifecycle, identity checks | [docs/lifecycle.md](docs/lifecycle.md) |
165
+ | Create options: resources, network policy, strict mode, proxies, secrets, volumes | [docs/configuration.md](docs/configuration.md) |
166
+ | `exec`/`shell`, default workload, streaming, single-pass streams, threads and timeouts, SSH, raw agent client | [docs/execution.md](docs/execution.md) |
167
+ | Guest filesystem, streaming reads/writes, host copies | [docs/filesystem.md](docs/filesystem.md) |
168
+ | Metrics, logs, `ping`/`touch`, live `modify` plans | [docs/observability.md](docs/observability.md) |
169
+ | Image cache, private registries, snapshots | [docs/images.md](docs/images.md) |
170
+ | Runtime binaries gem, `msb` resolution order, backends, environment variables | [docs/runtime.md](docs/runtime.md) |
171
+ | Error classes and stable codes | [docs/errors.md](docs/errors.md) |
172
+ | Complete supported surface and what is not yet exposed | [docs/surface.md](docs/surface.md) |
173
+ | Release process, release preflight, binaries gems | [docs/releasing.md](docs/releasing.md) |
174
+ | Architecture: native extension, sync bridge, GVL, error mapping | [DESIGN.md](DESIGN.md) |
175
+ | Companion runtime gem internals | [binaries/README.md](https://github.com/ya-luotao/microsandbox-rb/blob/main/binaries/README.md) |
176
+
177
+ API reference: [rubydoc.info/gems/microsandbox-rb](https://rubydoc.info/gems/microsandbox-rb).
178
+ Type signatures: [`sig/microsandbox.rbs`](sig/microsandbox.rbs).
179
+
180
+ ## Comparison with the official Ruby gem
181
+
182
+ Since upstream `v0.6.9` the microsandbox repository ships an official
183
+ `microsandbox` gem built from
184
+ [`sdk/ruby`](https://github.com/superradcompany/microsandbox/tree/main/sdk/ruby),
185
+ a compact veneer over the same Rust SDK. Both gems define the `Microsandbox`
186
+ module, so use one or the other in a process, not both. Differences below are
187
+ taken from the official gem's own README as of upstream `v0.6.18`.
188
+
189
+ | Capability | `microsandbox-rb` (this gem) | official `microsandbox` gem |
190
+ |---|:---:|:---:|
191
+ | Lifecycle, convergent `connect_or_create`, local/cloud backends | ✅ | ✅ |
192
+ | Collected `exec`/`shell`, logs, metrics, guest filesystem | ✅ | ✅ |
193
+ | Image, volume, and snapshot management | ✅ | ✅ |
194
+ | Streaming exec, logs, metrics, filesystem handles | ✅ | not exposed |
195
+ | Interactive SSH/SFTP client and server | ✅ | `ssh_exec` only |
196
+ | Live modification plans (`modify`, dry-run) | ✅ | not exposed |
197
+ | Outbound SOCKS proxy (`proxy:`) | ✅ | ✅ |
198
+ | Full network policy and mount builders, strict hostname mode | ✅ | not exposed |
199
+ | Rootfs patches, structured root disk, raw `agentd` client | ✅ | — |
200
+ | RBS type signatures | ✅ | — |
201
+ | Runtime binaries | companion gem or first-use download | first-use download |
202
+ | Version numbering | own semver; runtime tag via `Microsandbox.runtime_version` | mirrors the upstream tag |
593
203
 
594
204
  ## Versioning
595
205
 
596
- The gem follows its **own** [semantic version](https://semver.org/), **independent
597
- of** the upstream `microsandbox` runtime it embeds. Early releases (`0.5.7`–`0.5.9`)
598
- happened to share the upstream tag, but gem-only revisions and a bundled breaking
599
- change diverged the two numbers — the gem version is **not** a reliable indicator
600
- of the embedded runtime version. To learn which runtime a build wraps, ask it:
206
+ The gem follows its **own** [semantic version](https://semver.org/), independent
207
+ of the upstream runtime it embeds: early releases (`0.5.7`–`0.5.9`) happened to
208
+ share the upstream tag, but the numbers have diverged since and the gem version
209
+ is **not** an indicator of the embedded runtime. Ask the build instead:
601
210
 
602
211
  ```ruby
603
- Microsandbox::VERSION # => "0.17.0" (the gem's own version)
212
+ Microsandbox::VERSION # => "0.17.2" (the gem's own version)
604
213
  Microsandbox.runtime_version # => "v0.6.18" (the embedded upstream runtime tag)
605
214
  ```
606
215
 
607
- The companion [`microsandbox-rb-binaries`](#the-runtime-binaries) gem is
608
- versioned in **lockstep** with this gem (same number, released together) and
609
- pins the same upstream runtime — install both at the same version. The gem's
610
- `Microsandbox::Binaries::VERSION` and `::RUNTIME_VERSION` are asserted against
611
- this gem's constants by the test suite, so a companion gem can't silently go
612
- stale.
216
+ While the gem is `0.x`, a breaking API change bumps the minor and a fix bumps
217
+ the patch. Adopting a new upstream runtime bumps the gem version, the pinned
218
+ git tag, `Microsandbox::RUNTIME_VERSION`, and the table below together. The
219
+ companion `microsandbox-rb-binaries` gem is versioned in lockstep (same number,
220
+ released together) and pins the same runtime; the test suite asserts both
221
+ constants against this gem's. Every release records its runtime in
222
+ [CHANGELOG.md](CHANGELOG.md).
613
223
 
614
224
  | Gem version | Upstream runtime | Notes |
615
225
  |-------------|------------------|-------|
@@ -627,8 +237,7 @@ stale.
627
237
  | `0.9.0` | `v0.6.1` | adopts upstream `v0.6.0`+`v0.6.1` (zombie-runtime wait fix, secret substitution through CONNECT proxies, stale-sandbox cleanup); upstream public API is additive — no Ruby surface change |
628
238
  | `0.9.1` | `v0.6.2` | adopts upstream `v0.6.2` (faster image loads/pulls via early cache gate + zlib-rs); upstream API unchanged — no Ruby surface change |
629
239
  | `0.9.2` | `v0.6.3` | adopts upstream `v0.6.3` (v4-only sandboxes stop advertising AAAA DNS answers — fixes guest gRPC/c-ares preferring unreachable IPv6; scoped upstream TLS verification); glue moves to `*_local` SDK variants — no Ruby surface change |
630
- | `0.9.3` | `v0.6.6` | adopts upstream `v0.6.4`+`v0.6.6` (`v0.6.5` was yanked upstream): snapshot restore by pinned digest — fixes fatal restore-after-tag-republish bug, fragmented-UDP/PMTU relay fixes, exec kills the whole process group, ephemeral stop-wait tolerance, readdir RSS-leak fix; upstream API growth is additive-only — no Ruby surface change |
631
- | `0.10.0` | `v0.6.6` | `v0.6.6` API parity: live `modify`/resize, `ping`/`touch`, create `max_cpus`/`max_memory` |
240
+ | `0.10.0` | `v0.6.6` | adopts upstream `v0.6.4`+`v0.6.6` (`v0.6.5` was yanked upstream): snapshot restore by pinned digest — fixes fatal restore-after-tag-republish bug, fragmented-UDP/PMTU relay fixes, exec kills the whole process group, ephemeral stop-wait tolerance, readdir RSS-leak fix; plus `v0.6.6` API parity: live `modify`/resize, `ping`/`touch`, create `max_cpus`/`max_memory` |
632
241
  | `0.11.0` | `v0.6.7` | adopts upstream `v0.6.7` (**breaking**): network profiles replace `public_only`/`non_local`, structured `root_disk:` replaces `oci_upper_size:` (deprecated alias kept), snapshot descriptor contract (`create` re-keyed by name, `save`/`load` rename, `snapshot_to` removed, on-disk auto-migration), `Image.load`/`Image.save`, `follow_root_symlinks:`; runtime carries the GHSA-4vq3-cjpp-v7fg `msb copy` fix |
633
242
  | `0.12.0` | `v0.6.8` | adopts upstream `v0.6.8` (**breaking**): `Sandbox.list`/`.list_with` return a cursor-paginated `SandboxPage` (`limit:`/`cursor:` keywords), `UnsupportedError` re-keyed by structured operations with `#operation`/`#hint`; runtime adds a shared log registry for followed streams and cloud exec/ssh reconnects |
634
243
  | `0.13.0` | `v0.6.9` | adopts upstream `v0.6.9` (**breaking**): a bare `MSB_API_KEY` no longer selects the cloud backend (explicit `MSB_BACKEND=cloud` or a cloud profile required; invalid cloud config fails closed with `InvalidConfigError`); snapshot payload integrity becomes opt-in (`record_integrity:`, `verify` can report `:not_recorded`). Parity: default-workload execution (`exec_default`/`exec_default_stream`/`attach_default`, `cmd:`), flat root disks (`RootDisk.flat`), `modify(root_disk_size:)`, `rate_limiter:`, `vsock:`, `default_backend_info`, `Volume.get_default` |
@@ -636,146 +245,46 @@ stale.
636
245
  | `0.15.0` | `v0.6.14` | adopts upstream `v0.6.10`–`v0.6.14` step by step: bind-mount correctness, guest bootstrap off the kernel command line, DNS pins for deferred domain allows, Linux glibc 2.28 baseline for the prebuilt runtime, legacy ext4 upper-disk resize, `msb_krun` 0.1.32. Parity: `ssh.open_client`/`prepare_server` accept `inactivity_timeout:` (seconds; `0` disables, `nil` inherits the 600s global default) |
637
246
  | `0.16.0` | `v0.6.16` | adopts upstream `v0.6.15`+`v0.6.16` step by step: mount fallback ownership, readonly-mount write-probe fix, log retrieval rerouted through the SDK backends, config overlaid by field presence, network-slot recycling. Parity: per-mount `uid:`/`gid:`, and the convergent lifecycle — `Sandbox.connect_or_create`, `#id`, `#wait_for_status`, `#restart`, `#destroy`, `SandboxHandle#connect_or_start`, `SandboxReplacedError` |
638
247
  | `0.17.0` | `v0.6.18` | adopts upstream `v0.6.17`+`v0.6.18` step by step: outbound SOCKS4/SOCKS5 proxies (SOCKS5 UDP + credentials), migration-order fix for databases last opened by a `v0.6.15` `msb`, security hardening — plain-HTTP `Host`/`:authority` now checked against domain-rule policies even in non-strict mode, plus fail-closed strict hostname mode. Parity: `proxy:` (`Microsandbox::OutboundProxy` / `SecretSource`), `strict:` |
639
-
640
- **Going forward** — the gem version moves on its own semver track and no longer
641
- mirrors the upstream tag:
642
-
643
- - A **gem-only** change (new bindings, fixes, refactors over the *same* runtime)
644
- bumps the gem version by itself.
645
- - **Adopting a new upstream runtime** bumps the gem version too and updates the
646
- pinned git tag, `Microsandbox::RUNTIME_VERSION`, and the table above together.
647
- - While the gem is `0.x`, a breaking API change bumps the **minor** (`0.5 → 0.6`),
648
- not the patch — the `0.5.9 → 0.5.10` lifecycle split predates this policy and is
649
- the reason for it.
650
-
651
- Every release records its embedded runtime in `CHANGELOG.md`, and
652
- `Microsandbox.runtime_version` reports it at runtime.
248
+ | `0.17.1` | `v0.6.18` | gem-only security and hardening release: relative host paths pinned at create time (GHSA-mcw9-9rf6-p4rm), `Volume.fs` path validation, `Cargo.lock` advisory updates, argument-guard and cleanup fixes, `SECURITY.md` |
249
+ | `0.17.2` | `v0.6.18` | gem-only fix and release-pipeline hardening release: an interrupted native call no longer loses its result (a sandbox the call owns is stopped on `Timeout`/`Thread#kill`), release preflight, SHA-pinned workflow actions and `cargo deny` advisory checks, RBS and docs contract specs |
653
250
 
654
251
  ## Development
655
252
 
656
253
  ```bash
657
- bin/setup # bundle install
658
- bundle exec rake compile # build the native extension (debug)
659
- bundle exec rake compile:release # build optimized
660
- bundle exec rake spec # run unit specs (no runtime needed)
661
- MICROSANDBOX_INTEGRATION=1 bundle exec rake spec:all # + real microVM specs
254
+ bin/setup # bundle install + compile the native extension
255
+ bundle exec rake compile # rebuild the extension (debug); compile:release for optimized
256
+ bundle exec rake spec # unit + fake-runtime specs (no runtime needed)
257
+ MICROSANDBOX_INTEGRATION=1 bundle exec rspec spec/integration # boot real microVMs (KVM / Apple Silicon)
258
+ bundle exec standardrb # Ruby lint
259
+ cargo fmt --check --manifest-path ext/microsandbox/Cargo.toml
260
+ cargo clippy --manifest-path ext/microsandbox/Cargo.toml -- -D warnings
662
261
  ```
663
262
 
664
- Unit specs run without a runtime. Integration specs boot real microVMs and are
665
- opt-in via `MICROSANDBOX_INTEGRATION=1` (override the test image with
666
- `MICROSANDBOX_TEST_IMAGE`).
263
+ CI compiles and runs the unit suite on Ruby 3.1 through 3.4 on Linux and macOS,
264
+ lints Rust and Ruby, packages and installs the built gems, builds every
265
+ `microsandbox-rb-binaries` platform gem, and boots real microVMs in a KVM
266
+ integration job against both the provisioned and the bundled runtime.
667
267
 
668
- The native extension depends on the `microsandbox` core crate via a pinned git
669
- tag, so it builds without an adjacent checkout. To develop against a sibling
670
- `microsandbox/` checkout instead (faster, reflects local runtime changes):
268
+ The native extension pins the upstream core crate by git tag, so it builds
269
+ without an adjacent checkout. To develop against a sibling `microsandbox/`
270
+ checkout instead:
671
271
 
672
272
  ```bash
673
273
  cp .cargo/config.toml.example .cargo/config.toml # gitignored path override
674
274
  bundle exec rake compile
675
275
  ```
676
276
 
677
- ## Releasing
678
-
679
- Releases are automated by `.github/workflows/release.yml` via RubyGems
680
- **Trusted Publishing** (OIDC) — there is no API key to store as a secret. The
681
- trusted publisher is already configured for this gem, so no per-release secret
682
- or credential setup is needed.
683
-
684
- **Each release:**
685
-
686
- 1. Bump the gem's **own** version — `Microsandbox::VERSION` and the matching
687
- `[package] version` in `ext/microsandbox/Cargo.toml` (they must stay equal) —
688
- on its independent semver track (see [Versioning](#versioning)); **don't** pick
689
- the number to mirror the upstream tag. If the release also adopts a new upstream
690
- runtime, bump the `tag = "vX.Y.Z"` on **both** the `microsandbox` and
691
- `microsandbox-network` git deps, update `Microsandbox::RUNTIME_VERSION` to match,
692
- and add a row to the Versioning table. Also bump
693
- `Microsandbox::Binaries::VERSION` (and, on a runtime adoption,
694
- `::RUNTIME_VERSION`) in `binaries/lib/microsandbox/binaries.rb` — the
695
- companion gem ships in lockstep and the specs assert both. Update
696
- `CHANGELOG.md`.
697
- 2. Push a `vX.Y.Z` tag. CI builds the **source gem** and pushes it to RubyGems
698
- via `rubygems/configure-rubygems-credentials` (OIDC, `id-token: write`) — no
699
- `RUBYGEMS_API_KEY` secret required.
700
-
701
- > **Precompiled per-platform gems** are built best-effort by the `cross-gems`
702
- > job, gated to manual `workflow_dispatch` so you can iterate
703
- > (`gh workflow run release.yml`) without failing tag releases. They are not
704
- > auto-published on tags yet: a gem that *compiles but can't boot a microVM*
705
- > would be served to users ahead of the source gem, and CI can't boot a VM to
706
- > prove otherwise — so promotion is manual after validating the artifact on each
707
- > platform. A precompiled gem ships the compiled extension (with the guest
708
- > `agentd` baked in by *target* arch); the host-side `msb` + `libkrunfw` runtime
709
- > is **not** in it — that comes from the companion `microsandbox-rb-binaries`
710
- > gem, or, when that isn't installed, is fetched into `~/.microsandbox` on first
711
- > use by `Microsandbox.ensure_runtime!` (libkrunfw is `dlopen`'d by `msb` at
712
- > runtime, never linked into the gem). The
713
- > real cross-compile work is linking the *target* native libraries — `libcap-ng`
714
- > on Linux (handled via Debian multiarch in the workflow) and the Hypervisor +
715
- > Security frameworks on macOS (via osxcross; the one platform left to confirm).
716
- > Until promoted, users install the source gem (which compiles via `rb_sys`).
717
-
718
- > **Runtime binaries gems** (`microsandbox-rb-binaries`, source in `binaries/`)
719
- > are a *separate* artifact from the precompiled extension gems above: they carry
720
- > no Ruby extension, only the upstream `msb` + `libkrunfw` for one platform,
721
- > verified against the release's published `checksums.sha256` when vendored. CI's
722
- > `binaries` job vendors and builds all three (`arm64-darwin`,
723
- > `x86_64-linux-gnu`, `aarch64-linux-gnu`) on every run and smoke-tests the host
724
- > one; `release.yml`'s `binaries-gems` job does the same on a tag and a separate
725
- > `publish-binaries` job pushes them after the SDK gem is live (a companion
726
- > failure is its own red job and never blocks the SDK release).
727
- > They use their own RubyGems trusted-publisher entry (same repo + workflow,
728
- > gem name `microsandbox-rb-binaries`). To build them by hand:
729
- > `rake -C binaries vendor[<platform>]` then `rake -C binaries build[<platform>]`
730
- > (→ `binaries/pkg/*.gem`).
731
-
732
- See [DESIGN.md](DESIGN.md) for the architecture and the implemented-surface
733
- section. The binding covers the official-SDK surface: sandbox
734
- lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
735
- `status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
736
- `stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
737
- `wait_until_stopped`/`config`/`config_json`/`snapshot` from
738
- `Sandbox.get`, and the cursor-paginated `list`/`list_with` with label
739
- filters),
740
- backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
741
- `exec`/`shell` (collected and streaming), interactive `attach`/
742
- `attach_shell`, the full guest filesystem (incl. streaming `read_stream`/
743
- `write_stream`), metrics (per-sandbox, `Microsandbox.all_sandbox_metrics`, and
744
- streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
745
- named volumes (incl. host-side `Volume.fs`/`VolumeInfo#fs` read/write),
746
- snapshots (create/open/list/list_dir/reindex/verify/save/load +
747
- boot-from-snapshot), image archives (`Image.load`/`Image.save`), streaming
748
- image-pull progress (`Sandbox.create_with_progress` → `PullSession`),
749
- **rootfs patches** (`Microsandbox::Patch`), the structured **root disk**
750
- (`root_disk:` managed/tmpfs/disk via `Microsandbox::RootDisk`), **network
751
- configuration** (composable profiles, custom per-rule
752
- `Microsandbox::NetworkPolicy`/`Rule`/`Destination`, plus DNS, TLS interception,
753
- IPv4/IPv6 pools, `max_connections`, `trust_host_cas`, `strict` hostname mode,
754
- outbound SOCKS4/SOCKS5 `proxy:`), **secrets** (multi-host /
755
- wildcard allow-lists, injection toggles, per-secret + sandbox-level violation
756
- policy), **SSH** (`Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`), and the
757
- **raw agent client** (`Microsandbox::AgentClient`). Create options span
758
- resources, `init`/`ephemeral`, disk-image `fstype`, network policy + config,
759
- `log_level`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches`/`volumes`
760
- (bind/named/tmpfs/disk with mount policies) and more; `exec`/`shell` take
761
- per-call `rlimits`, and `create` accepts
762
- `registry_auth`/`registry_insecure`/`registry_ca_certs` for private and
763
- authenticated registries, plus customizable provisioning via `Microsandbox.setup`.
764
-
765
- A few secondary upstream knobs are not yet exposed: per-published-port host bind
766
- address (ports always bind loopback), network interface overrides, and inline
767
- named-volume create-mode (pre-create the volume with `Volume.create`, then mount
768
- it with `{ named: "…" }`).
769
-
770
277
  ## Contributing
771
278
 
772
- This is a community-maintained gem and **contributions are very welcome** —
773
- bug reports, fixes, docs, and feature work alike. Open an
774
- [issue](https://github.com/ya-luotao/microsandbox-rb/issues) or send a
775
- [pull request](https://github.com/ya-luotao/microsandbox-rb/pulls); PRs target
776
- `main`. Before pushing, run the local gate (Rust `fmt`/`clippy`, `standardrb`,
777
- and unit specs) — see [Development](#development).
279
+ Bug reports and pull requests are welcome on
280
+ [GitHub](https://github.com/ya-luotao/microsandbox-rb/issues). Report security
281
+ problems privately — see [SECURITY.md](SECURITY.md), which also maps upstream
282
+ advisories to the gem versions they affect. Please include a
283
+ failing spec with bug reports where possible and keep pull requests focused on
284
+ one change; PRs target `main`. When the public API changes, update
285
+ `sig/microsandbox.rbs` and `CHANGELOG.md` in the same change, and run the local
286
+ gate (`cargo fmt`/`clippy`, `standardrb`, unit specs) before pushing.
778
287
 
779
288
  ## License
780
289
 
781
- Apache-2.0. See [LICENSE](LICENSE).
290
+ Released under the [Apache License 2.0](LICENSE).