microsandbox-rb 0.16.0 → 0.17.1

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.
data/README.md CHANGED
@@ -1,574 +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
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.
67
10
 
68
- The gem is published as **`microsandbox-rb`**, but you still `require "microsandbox"`
69
- (the `microsandbox` package name was already taken on RubyGems):
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).
70
12
 
71
- ```ruby
72
- # Gemfile
73
- gem "microsandbox-rb", require: "microsandbox"
74
- ```
75
-
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 require no Rust but are not yet auto-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
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
207
102
  ```
208
103
 
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).
104
+ See [docs/lifecycle.md](docs/lifecycle.md) for the live `Sandbox` /
105
+ `SandboxHandle` split, `detach`, and the identity checks behind
106
+ `SandboxReplacedError`.
228
107
 
229
- ### Configuration
108
+ ### Running commands
230
109
 
231
110
  ```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
246
- ```
247
-
248
- ### Executing commands
249
-
250
- ```ruby
251
- Microsandbox::Sandbox.create("exec-demo", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
252
- # Direct command (no shell)
111
+ Microsandbox::Sandbox.create("work", image: "public.ecr.aws/docker/library/python:3-slim") do |sb|
253
112
  out = sb.exec("ls", ["-la", "/etc"], cwd: "/", timeout: 30)
254
- out.exit_code # => 0
255
- out.success? # => true
256
- out.stdout # => "..." (UTF-8)
257
- out.stderr_bytes # => raw ASCII-8BIT bytes
258
-
259
- # Shell script (pipes, redirects, &&)
113
+ out.exit_code # non-zero exit is data, not an exception
260
114
  sb.shell("cat /etc/os-release | grep VERSION").stdout
261
115
 
262
- # Environment, stdin, working directory
263
- sb.exec("cat", [], stdin: "piped data")
264
- sb.exec("sh", ["-c", "echo $GREETING"], env: { "GREETING" => "hi" })
265
- end
266
- ```
267
-
268
- A non-zero exit is **not** an error — inspect `exit_code`/`success?`. Spawn-time
269
- failures (e.g. command not found) and timeouts raise typed errors (see below).
270
-
271
- **Default workload** (runtime `v0.6.9`): `create` is strictly boot-only — it
272
- never runs the image's `ENTRYPOINT`/`CMD`. Execute the image's own command
273
- explicitly:
274
-
275
- ```ruby
276
- Microsandbox::Sandbox.create("worker", image: "example/worker:latest",
277
- cmd: ["worker.py", "--once"]) do |sb| # cmd: overrides the durable image CMD
278
- out = sb.exec_default(timeout: 300) # buffered; exec-style options
279
- handle = sb.exec_default_stream # or streaming (returns an ExecHandle)
280
- sb.attach_default # or interactive (host TTY)
281
- end
282
- ```
283
-
284
- An image whose entrypoint and CMD resolve to no executable command raises
285
- `Microsandbox::NoDefaultCommandError`.
286
-
287
- ### Guest filesystem
288
-
289
- ```ruby
290
- Microsandbox::Sandbox.create("fs-demo", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
291
- sb.fs.write("/tmp/data.txt", "hello")
292
- sb.fs.read_text("/tmp/data.txt") # => "hello" (UTF-8)
293
- sb.fs.read("/tmp/data.txt") # => raw bytes (ASCII-8BIT)
294
- sb.fs.exists?("/tmp/data.txt") # => true
295
-
296
- sb.fs.mkdir("/tmp/sub")
297
- sb.fs.copy("/tmp/data.txt", "/tmp/sub/copy.txt")
298
- sb.fs.rename("/tmp/sub/copy.txt", "/tmp/sub/renamed.txt")
299
- sb.fs.list("/tmp/sub") # => [Microsandbox::FsEntry, ...]
300
- sb.fs.stat("/tmp/data.txt") # => Microsandbox::FsMetadata
301
-
302
- # Host <-> guest copies
303
- sb.fs.copy_from_host("./local.txt", "/tmp/local.txt")
304
- sb.fs.copy_to_host("/tmp/out.txt", "./out.txt")
305
- end
306
- ```
307
-
308
- ### Metrics & logs
309
-
310
- ```ruby
311
- Microsandbox::Sandbox.create("obs", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
312
- # On v0.6.x runtimes the metrics slot goes live a beat after create returns,
313
- # so `metrics` can briefly raise "no live metrics slot" right after boot —
314
- # retry for a few hundred ms rather than treating the first failure as fatal.
315
- m = sb.metrics # => Microsandbox::Metrics
316
- m.cpu_percent
317
- m.memory_bytes
318
- m.uptime_secs
319
-
320
- sb.logs(tail: 100, sources: ["stdout", "stderr"]).each do |entry|
321
- puts "[#{entry.source}] #{entry.text}"
322
- end
323
- end
324
- ```
325
-
326
- ### Live modification & health
327
-
328
- Resize, reconfigure, or rotate secrets on a sandbox **without recreating it**,
329
- and probe the guest agent's liveness. To leave headroom for a live CPU/memory
330
- resize, reserve a ceiling at create time with `max_cpus:`/`max_memory:`:
331
-
332
- ```ruby
333
- Microsandbox::Sandbox.create("live", image: "public.ecr.aws/docker/library/alpine:latest",
334
- cpus: 1, max_cpus: 4, memory: 512, max_memory: 2048) do |sb|
335
- # Health check — does NOT refresh the idle timer:
336
- ping = sb.ping # => Microsandbox::PingResult
337
- ping.latency_ms # round-trip latency
338
-
339
- # Explicitly refresh the idle-activity timer (resets any idle_timeout:):
340
- sb.touch.activity_seq # => Microsandbox::TouchResult
341
-
342
- # Preview a change without applying it:
343
- plan = sb.modify(cpus: 2, memory: 1024, dry_run: true)
344
- plan.applied? # => false
345
- plan.changes # => [{ kind: "config", field: "cpus", ... }, ...]
346
-
347
- # Live resize — applies to the running VM under the default :no_restart policy:
348
- sb.modify(cpus: 2, memory: 1024)
349
-
350
- # Grow the root disk (managed upper or flat, MiB — runtime v0.6.9). Applied
351
- # while stopped; growth-only:
352
- sb.modify(root_disk_size: 8192, policy: :next_start)
353
-
354
- # env/labels/workdir changes on a *running* sandbox require a restart, so the
355
- # default :no_restart policy rejects the whole apply (it raises rather than
356
- # partially applying). Persist them for the next start — or restart now —
357
- # by saying so explicitly:
358
- sb.modify(env: { "TIER" => "prod" }, remove_env: ["DEBUG"],
359
- labels: { "role" => "worker" }, policy: :next_start) # or policy: :restart
360
-
361
- # Rotating/removing an *existing* secret (or updating its allowed hosts) is
362
- # live; *adding* a new secret is restart-required, like env. Specs are keyed
363
- # by name; env:/store:/value: are mutually exclusive:
364
- sb.modify(
365
- secrets: { "API_KEY" => { env: "HOST_API_KEY", allowed_hosts: ["api.example.com"] } },
366
- remove_secrets: ["OLD_TOKEN"],
367
- )
368
- end
369
- ```
370
-
371
- The apply is **all-or-nothing**: under a given `policy:` every planned change
372
- must be applicable, or the whole `modify` raises — nothing is partially
373
- applied. Use `dry_run: true` to inspect each change's `disposition` (`"live"`,
374
- `"next start"`, `"requires restart"`) before committing.
375
-
376
- `SandboxHandle` (from `Sandbox.get`/`list`) carries the same `#ping`/`#touch`/
377
- `#modify`; on a stopped sandbox `#ping`/`#touch` raise `SandboxNotRunningError`.
378
-
379
- ### Streaming output
380
-
381
- For long-running commands, stream events as they arrive instead of waiting:
382
-
383
- ```ruby
384
- Microsandbox::Sandbox.create("stream", image: "public.ecr.aws/docker/library/python:3-slim") do |sb|
385
- handle = sb.exec_stream("python", ["-u", "-c", "import time\nfor i in range(3): print(i); time.sleep(1)"])
386
- 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|
387
118
  print event.text if event.stdout?
388
119
  end
389
- # or: out = handle.collect → ExecOutput (drain to the end)
390
- # interactive stdin — create the stream with stdin: :pipe to get a writable sink:
391
- # h = sb.exec_stream("cat", [], stdin: :pipe)
392
- # sink = h.stdin; sink.write("data\n"); sink.close # close sends EOF
393
- # control: handle.signal(15), handle.kill, handle.resize(rows, cols)
394
- end
395
- ```
396
-
397
- > **Streams are single-pass.** `ExecHandle`, `LogStream`, `MetricsStream`,
398
- > `FsReadStream`, `PullSession`, and `AgentStream` are `Enumerable`, but `each`
399
- > drains a one-shot native channel — they are forward-only, not rewindable, and
400
- > meant for a single consumer. Iterate (or `collect`/`read`) exactly once: a
401
- > second `each`, or a combinator after a partial drain (`count` then `each`,
402
- > `to_a` twice), silently yields nothing. Don't share one handle across threads.
403
120
 
404
- ### Images
405
-
406
- Manage the local OCI image cache (images are pulled automatically on `create`):
407
-
408
- ```ruby
409
- Microsandbox::Image.list # => [Microsandbox::ImageInfo, ...]
410
- Microsandbox::Image.get("public.ecr.aws/docker/library/alpine:latest") # => Microsandbox::ImageInfo
411
- Microsandbox::Image.inspect("public.ecr.aws/docker/library/alpine:latest").layers # => [{...}, ...]
412
- Microsandbox::Image.remove("public.ecr.aws/docker/library/alpine:latest", force: true)
413
- report = Microsandbox::Image.prune
414
- 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
415
125
  ```
416
126
 
417
- ### 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.
418
130
 
419
- Images are pulled automatically on `create`. For a private registry — or to lift
420
- Docker Hub's anonymous rate limit — pass `registry_auth:` with a username and a
421
- password or token:
131
+ ### Networking and secrets
422
132
 
423
133
  ```ruby
424
- Microsandbox::Sandbox.create(
425
- "private",
426
- image: "registry.example.com/team/app:latest",
427
- 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" }]
428
141
  ) do |sb|
429
142
  # ...
430
143
  end
431
144
  ```
432
145
 
433
- For self-hosted registries you can also reach the registry over plain HTTP and
434
- trust a private CA:
435
-
436
- ```ruby
437
- Microsandbox::Sandbox.create(
438
- "internal",
439
- image: "registry.internal:5000/app:latest",
440
- registry_insecure: true, # plain HTTP instead of HTTPS
441
- registry_ca_certs: File.read("/etc/pki/internal-ca.pem") # String or Array of PEMs
442
- )
443
- ```
444
-
445
- Without `registry_auth:`, the core's default credential resolution still applies
446
- (OS keyring, global config, and `~/.docker/config.json`), so an existing
447
- `docker login` is honored automatically.
146
+ See [docs/configuration.md](docs/configuration.md) for every create option,
147
+ including resources, ports, volumes, and root-disk layout.
448
148
 
449
- ### Named volumes
450
-
451
- Persistent storage that outlives individual sandboxes:
452
-
453
- ```ruby
454
- Microsandbox::Volume.create("cache", kind: "disk", size_mib: 512)
455
- Microsandbox::Volume.list # => [Microsandbox::VolumeInfo, ...]
456
-
457
- Microsandbox::Sandbox.create("with-vol", image: "public.ecr.aws/docker/library/alpine:latest",
458
- volumes: { "/data" => { named: "cache" } }) do |sb|
459
- sb.fs.write("/data/state.txt", "persisted")
460
- end
461
-
462
- Microsandbox::Volume.remove("cache")
463
- ```
464
-
465
- `volumes:` accepts a host path String (bind mount) or `{ bind: "/host" }` /
466
- `{ named: "volume-name" }` per guest path. A bind or named mount may pin the
467
- fallback guest owner for host files with `uid:`/`gid:` (both required together,
468
- runtime `v0.6.15`). Boot from a snapshot with
469
- `Sandbox.create(name, from_snapshot: "snap-name-or-path")`.
470
-
471
- ### Error handling
472
-
473
- All errors descend from `Microsandbox::Error` and carry a stable `#code`:
474
-
475
- ```ruby
476
- begin
477
- Microsandbox::Sandbox.create("dup", image: "public.ecr.aws/docker/library/alpine:latest")
478
- Microsandbox::Sandbox.create("dup", image: "public.ecr.aws/docker/library/alpine:latest") # name clash
479
- rescue Microsandbox::SandboxAlreadyExistsError => e
480
- warn "#{e.code}: #{e.message}" # => "sandbox-already-exists: ..."
481
- rescue Microsandbox::Error => e
482
- warn "microsandbox failed: #{e.message}"
483
- end
484
- ```
485
-
486
- | Class | `#code` |
487
- |-------|---------|
488
- | `InvalidConfigError` | `invalid-config` |
489
- | `SandboxNotFoundError` | `sandbox-not-found` |
490
- | `SandboxAlreadyExistsError` | `sandbox-already-exists` |
491
- | `SandboxStillRunningError` | `sandbox-still-running` |
492
- | `ExecTimeoutError` | `exec-timeout` |
493
- | `ExecFailedError` | `exec-failed` |
494
- | `FilesystemError` | `filesystem-error` |
495
- | `ImageNotFoundError` | `image-not-found` |
496
- | `MetricsDisabledError` / `MetricsUnavailableError` | `metrics-disabled` / `metrics-unavailable` |
497
- | … | (see `lib/microsandbox/errors.rb`) |
498
-
499
- ## Runtime configuration
500
-
501
- The `msb` runtime path is resolved in this order: the `MSB_PATH` environment
502
- variable → the `microsandbox-rb-binaries` gem (or another SDK-set override) →
503
- the config file → `~/.microsandbox/bin/msb` → `msb` on `PATH`.
504
- `Microsandbox.runtime_path` reports the winner.
149
+ ### Observability and live modification
505
150
 
506
151
  ```ruby
507
- Microsandbox.installed? # => true/false
508
- Microsandbox.install # download + install the runtime (idempotent)
509
- Microsandbox.runtime_path # => "/Users/you/.microsandbox/bin/msb"
510
- Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override (set-once)
511
- 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
512
156
  ```
513
157
 
514
- When [`microsandbox-rb-binaries`](#the-runtime-binaries) is installed,
515
- `require "microsandbox"` claims that SDK-set slot with the gem's vendored `msb`
516
- (the firmware is found alongside it), and `runtime_path` points into the gem.
517
- The two gems are versioned in lockstep and the companion gem must be the same
518
- version **and** built for the same upstream runtime — a mismatch of either is reported with a warning and
519
- skipped, and the SDK falls back to `~/.microsandbox` rather than driving a
520
- runtime it doesn't match. Because the slot is **set-once**,
521
- `Microsandbox.runtime_path=` is then a no-op: use the `MSB_PATH` environment
522
- variable, which outranks it, to point at a different runtime.
523
-
524
- ### Backend routing
525
-
526
- As of v0.5.8 every operation runs through a backend. The default is the local
527
- libkrun backend; without any configuration nothing changes. A backend can be
528
- selected programmatically or via the environment:
529
-
530
- ```ruby
531
- Microsandbox.default_backend_kind # => :local (or :cloud)
532
- Microsandbox.set_default_backend(:cloud, url: "https://api.example.com", api_key: ENV["MSB_API_KEY"])
533
- # or a named profile from ~/.microsandbox/config.json:
534
- Microsandbox.set_default_backend(:cloud, profile: "prod")
535
-
536
- # Scoped override (restored afterward, even on error):
537
- Microsandbox.with_backend(:local) { Microsandbox::Sandbox.create("box", image: "alpine") { |sb| ... } }
538
- ```
539
-
540
- Resolution order when no backend is set programmatically: `MSB_BACKEND`
541
- (`local`/`cloud`) → `MSB_PROFILE` → the `active_profile` in
542
- `~/.microsandbox/config.json` (path overridable via `MSB_CONFIG_PATH`) →
543
- local. **Cloud intent must be explicit** (since runtime `v0.6.9`): a bare
544
- `MSB_API_KEY` is treated as credential material, not backend intent, and no
545
- longer selects the cloud on its own — pair it with `MSB_BACKEND=cloud` (which
546
- reads `MSB_API_URL`/`MSB_API_KEY`), or select a cloud profile. Invalid cloud
547
- configuration (e.g. `MSB_BACKEND=cloud` without a usable API key or cloud
548
- profile) fails closed with `Microsandbox::InvalidConfigError` instead of
549
- silently running locally. The cloud backend currently supports a subset of
550
- operations (create/start/stop/remove/get/list, one-shot exec, follow log
551
- 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, precompiled and 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 |
552
203
 
553
204
  ## Versioning
554
205
 
555
- The gem follows its **own** [semantic version](https://semver.org/), **independent
556
- of** the upstream `microsandbox` runtime it embeds. Early releases (`0.5.7`–`0.5.9`)
557
- happened to share the upstream tag, but gem-only revisions and a bundled breaking
558
- change diverged the two numbers — the gem version is **not** a reliable indicator
559
- 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:
560
210
 
561
211
  ```ruby
562
- Microsandbox::VERSION # => "0.16.0" (the gem's own version)
563
- Microsandbox.runtime_version # => "v0.6.16" (the embedded upstream runtime tag)
212
+ Microsandbox::VERSION # => "0.17.1" (the gem's own version)
213
+ Microsandbox.runtime_version # => "v0.6.18" (the embedded upstream runtime tag)
564
214
  ```
565
215
 
566
- The companion [`microsandbox-rb-binaries`](#the-runtime-binaries) gem is
567
- versioned in **lockstep** with this gem (same number, released together) and
568
- pins the same upstream runtime — install both at the same version. The gem's
569
- `Microsandbox::Binaries::VERSION` and `::RUNTIME_VERSION` are asserted against
570
- this gem's constants by the test suite, so a companion gem can't silently go
571
- 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).
572
223
 
573
224
  | Gem version | Upstream runtime | Notes |
574
225
  |-------------|------------------|-------|
@@ -594,145 +245,46 @@ stale.
594
245
  | `0.14.0` | `v0.6.9` | two-gem split: SDK-only gem (no build-time runtime download) + companion `microsandbox-rb-binaries` platform gems |
595
246
  | `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) |
596
247
  | `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` |
597
-
598
- **Going forward** — the gem version moves on its own semver track and no longer
599
- mirrors the upstream tag:
600
-
601
- - A **gem-only** change (new bindings, fixes, refactors over the *same* runtime)
602
- bumps the gem version by itself.
603
- - **Adopting a new upstream runtime** bumps the gem version too and updates the
604
- pinned git tag, `Microsandbox::RUNTIME_VERSION`, and the table above together.
605
- - While the gem is `0.x`, a breaking API change bumps the **minor** (`0.5 → 0.6`),
606
- not the patch — the `0.5.9 → 0.5.10` lifecycle split predates this policy and is
607
- the reason for it.
608
-
609
- Every release records its embedded runtime in `CHANGELOG.md`, and
610
- `Microsandbox.runtime_version` reports it at runtime.
248
+ | `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:` |
249
+ | `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` |
611
250
 
612
251
  ## Development
613
252
 
614
253
  ```bash
615
- bin/setup # bundle install
616
- bundle exec rake compile # build the native extension (debug)
617
- bundle exec rake compile:release # build optimized
618
- bundle exec rake spec # run unit specs (no runtime needed)
619
- 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 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
620
261
  ```
621
262
 
622
- Unit specs run without a runtime. Integration specs boot real microVMs and are
623
- opt-in via `MICROSANDBOX_INTEGRATION=1` (override the test image with
624
- `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.
625
267
 
626
- The native extension depends on the `microsandbox` core crate via a pinned git
627
- tag, so it builds without an adjacent checkout. To develop against a sibling
628
- `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:
629
271
 
630
272
  ```bash
631
273
  cp .cargo/config.toml.example .cargo/config.toml # gitignored path override
632
274
  bundle exec rake compile
633
275
  ```
634
276
 
635
- ## Releasing
636
-
637
- Releases are automated by `.github/workflows/release.yml` via RubyGems
638
- **Trusted Publishing** (OIDC) — there is no API key to store as a secret. The
639
- trusted publisher is already configured for this gem, so no per-release secret
640
- or credential setup is needed.
641
-
642
- **Each release:**
643
-
644
- 1. Bump the gem's **own** version — `Microsandbox::VERSION` and the matching
645
- `[package] version` in `ext/microsandbox/Cargo.toml` (they must stay equal) —
646
- on its independent semver track (see [Versioning](#versioning)); **don't** pick
647
- the number to mirror the upstream tag. If the release also adopts a new upstream
648
- runtime, bump the `tag = "vX.Y.Z"` on **both** the `microsandbox` and
649
- `microsandbox-network` git deps, update `Microsandbox::RUNTIME_VERSION` to match,
650
- and add a row to the Versioning table. Also bump
651
- `Microsandbox::Binaries::VERSION` (and, on a runtime adoption,
652
- `::RUNTIME_VERSION`) in `binaries/lib/microsandbox/binaries.rb` — the
653
- companion gem ships in lockstep and the specs assert both. Update
654
- `CHANGELOG.md`.
655
- 2. Push a `vX.Y.Z` tag. CI builds the **source gem** and pushes it to RubyGems
656
- via `rubygems/configure-rubygems-credentials` (OIDC, `id-token: write`) — no
657
- `RUBYGEMS_API_KEY` secret required.
658
-
659
- > **Precompiled per-platform gems** are built best-effort by the `cross-gems`
660
- > job, gated to manual `workflow_dispatch` so you can iterate
661
- > (`gh workflow run release.yml`) without failing tag releases. They are not
662
- > auto-published on tags yet: a gem that *compiles but can't boot a microVM*
663
- > would be served to users ahead of the source gem, and CI can't boot a VM to
664
- > prove otherwise — so promotion is manual after validating the artifact on each
665
- > platform. A precompiled gem ships the compiled extension (with the guest
666
- > `agentd` baked in by *target* arch); the host-side `msb` + `libkrunfw` runtime
667
- > is **not** in it — that comes from the companion `microsandbox-rb-binaries`
668
- > gem, or, when that isn't installed, is fetched into `~/.microsandbox` on first
669
- > use by `Microsandbox.ensure_runtime!` (libkrunfw is `dlopen`'d by `msb` at
670
- > runtime, never linked into the gem). The
671
- > real cross-compile work is linking the *target* native libraries — `libcap-ng`
672
- > on Linux (handled via Debian multiarch in the workflow) and the Hypervisor +
673
- > Security frameworks on macOS (via osxcross; the one platform left to confirm).
674
- > Until promoted, users install the source gem (which compiles via `rb_sys`).
675
-
676
- > **Runtime binaries gems** (`microsandbox-rb-binaries`, source in `binaries/`)
677
- > are a *separate* artifact from the precompiled extension gems above: they carry
678
- > no Ruby extension, only the upstream `msb` + `libkrunfw` for one platform,
679
- > verified against the release's published `checksums.sha256` when vendored. CI's
680
- > `binaries` job vendors and builds all three (`arm64-darwin`,
681
- > `x86_64-linux-gnu`, `aarch64-linux-gnu`) on every run and smoke-tests the host
682
- > one; `release.yml`'s `binaries-gems` job does the same on a tag and a separate
683
- > `publish-binaries` job pushes them after the SDK gem is live (a companion
684
- > failure is its own red job and never blocks the SDK release).
685
- > They use their own RubyGems trusted-publisher entry (same repo + workflow,
686
- > gem name `microsandbox-rb-binaries`). To build them by hand:
687
- > `rake -C binaries vendor[<platform>]` then `rake -C binaries build[<platform>]`
688
- > (→ `binaries/pkg/*.gem`).
689
-
690
- See [DESIGN.md](DESIGN.md) for the architecture and the implemented-surface
691
- section. The binding covers the official-SDK surface: sandbox
692
- lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
693
- `status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
694
- `stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
695
- `wait_until_stopped`/`config`/`config_json`/`snapshot` from
696
- `Sandbox.get`, and the cursor-paginated `list`/`list_with` with label
697
- filters),
698
- backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
699
- `exec`/`shell` (collected and streaming), interactive `attach`/
700
- `attach_shell`, the full guest filesystem (incl. streaming `read_stream`/
701
- `write_stream`), metrics (per-sandbox, `Microsandbox.all_sandbox_metrics`, and
702
- streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
703
- named volumes (incl. host-side `Volume.fs`/`VolumeInfo#fs` read/write),
704
- snapshots (create/open/list/list_dir/reindex/verify/save/load +
705
- boot-from-snapshot), image archives (`Image.load`/`Image.save`), streaming
706
- image-pull progress (`Sandbox.create_with_progress` → `PullSession`),
707
- **rootfs patches** (`Microsandbox::Patch`), the structured **root disk**
708
- (`root_disk:` managed/tmpfs/disk via `Microsandbox::RootDisk`), **network
709
- configuration** (composable profiles, custom per-rule
710
- `Microsandbox::NetworkPolicy`/`Rule`/`Destination`, plus DNS, TLS interception,
711
- IPv4/IPv6 pools, `max_connections`, `trust_host_cas`), **secrets** (multi-host /
712
- wildcard allow-lists, injection toggles, per-secret + sandbox-level violation
713
- policy), **SSH** (`Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`), and the
714
- **raw agent client** (`Microsandbox::AgentClient`). Create options span
715
- resources, `init`/`ephemeral`, disk-image `fstype`, network policy + config,
716
- `log_level`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches`/`volumes`
717
- (bind/named/tmpfs/disk with mount policies) and more; `exec`/`shell` take
718
- per-call `rlimits`, and `create` accepts
719
- `registry_auth`/`registry_insecure`/`registry_ca_certs` for private and
720
- authenticated registries, plus customizable provisioning via `Microsandbox.setup`.
721
-
722
- A few secondary upstream knobs are not yet exposed: per-published-port host bind
723
- address (ports always bind loopback), network interface overrides, and inline
724
- named-volume create-mode (pre-create the volume with `Volume.create`, then mount
725
- it with `{ named: "…" }`).
726
-
727
277
  ## Contributing
728
278
 
729
- This is a community-maintained gem and **contributions are very welcome** —
730
- bug reports, fixes, docs, and feature work alike. Open an
731
- [issue](https://github.com/ya-luotao/microsandbox-rb/issues) or send a
732
- [pull request](https://github.com/ya-luotao/microsandbox-rb/pulls); PRs target
733
- `main`. Before pushing, run the local gate (Rust `fmt`/`clippy`, `standardrb`,
734
- 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.
735
287
 
736
288
  ## License
737
289
 
738
- Apache-2.0. See [LICENSE](LICENSE).
290
+ Released under the [Apache License 2.0](LICENSE).