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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +229 -8
- data/Cargo.lock +45 -46
- data/DESIGN.md +90 -72
- data/README.md +178 -669
- data/SECURITY.md +65 -0
- data/docs/configuration.md +154 -0
- data/docs/errors.md +74 -0
- data/docs/execution.md +108 -0
- data/docs/filesystem.md +45 -0
- data/docs/images.md +65 -0
- data/docs/lifecycle.md +141 -0
- data/docs/observability.md +99 -0
- data/docs/releasing.md +117 -0
- data/docs/runtime.md +154 -0
- data/docs/surface.md +75 -0
- data/ext/microsandbox/Cargo.toml +9 -5
- data/ext/microsandbox/src/agent.rs +13 -11
- data/ext/microsandbox/src/backend.rs +1 -1
- data/ext/microsandbox/src/exec.rs +12 -8
- data/ext/microsandbox/src/fs_stream.rs +19 -7
- data/ext/microsandbox/src/host_path.rs +108 -0
- data/ext/microsandbox/src/image.rs +2 -2
- data/ext/microsandbox/src/lib.rs +3 -2
- data/ext/microsandbox/src/runtime.rs +189 -13
- data/ext/microsandbox/src/sandbox.rs +149 -102
- data/ext/microsandbox/src/snapshot.rs +17 -11
- data/ext/microsandbox/src/ssh.rs +22 -16
- data/ext/microsandbox/src/stream.rs +6 -2
- data/ext/microsandbox/src/volume.rs +50 -17
- data/lib/microsandbox/fs.rb +4 -8
- data/lib/microsandbox/image.rb +2 -2
- data/lib/microsandbox/log_entry.rb +1 -1
- data/lib/microsandbox/metrics.rb +1 -1
- data/lib/microsandbox/outbound_proxy.rb +8 -0
- data/lib/microsandbox/sandbox.rb +128 -13
- data/lib/microsandbox/snapshot.rb +1 -1
- data/lib/microsandbox/ssh.rb +2 -10
- data/lib/microsandbox/streams.rb +8 -5
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox/volume.rb +31 -14
- data/lib/microsandbox-rb.rb +7 -0
- data/lib/microsandbox.rb +88 -31
- data/sig/microsandbox.rbs +24 -0
- metadata +14 -1
data/README.md
CHANGED
|
@@ -1,615 +1,225 @@
|
|
|
1
1
|
# microsandbox-rb
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
+
[](https://rubygems.org/gems/microsandbox-rb)
|
|
4
|
+
[](https://github.com/ya-luotao/microsandbox-rb/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
[](https://rubydoc.info/gems/microsandbox-rb)
|
|
7
|
+
[](LICENSE)
|
|
65
8
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
bundle install
|
|
78
|
-
# or
|
|
79
|
-
gem install microsandbox-rb
|
|
80
|
-
```
|
|
13
|
+
## Highlights
|
|
81
14
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
23
|
+
## Installation
|
|
88
24
|
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
-
|
|
102
|
-
gem
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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"
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
h
|
|
204
|
-
h.
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
-
#
|
|
304
|
-
sb.
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
475
|
-
|
|
146
|
+
See [docs/configuration.md](docs/configuration.md) for every create option,
|
|
147
|
+
including resources, ports, volumes, and root-disk layout.
|
|
476
148
|
|
|
477
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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/),
|
|
597
|
-
of
|
|
598
|
-
|
|
599
|
-
|
|
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.
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
`
|
|
611
|
-
|
|
612
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
658
|
-
bundle exec rake compile
|
|
659
|
-
bundle exec rake
|
|
660
|
-
bundle exec
|
|
661
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
`
|
|
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
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
[
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
290
|
+
Released under the [Apache License 2.0](LICENSE).
|