microsandbox-rb 0.5.7 → 0.5.8
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 +27 -1
- data/Cargo.lock +1 -1
- data/DESIGN.md +28 -9
- data/README.md +89 -26
- data/ext/microsandbox/Cargo.toml +4 -1
- data/ext/microsandbox/extconf.rb +35 -0
- data/ext/microsandbox/src/sandbox.rs +68 -0
- data/lib/microsandbox/sandbox.rb +32 -1
- data/lib/microsandbox/version.rb +5 -3
- data/lib/microsandbox.rb +43 -2
- data/sig/microsandbox.rbs +3 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56e1f366f8e18a95965bc917c65b739426499a147f1da4a2defb090dba4c375d
|
|
4
|
+
data.tar.gz: 689f3c5518ddc52e6fd851d290e2629d3a3a9a043ba09653a6ce1041039a9a37
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 503d0a3460f47855742e5179ccafab4e7b65132e71af5d3c39ed77b14542ac231e6bdbb9ebe2021e06ff2da912f702c8ad44f5a0dd882343879954d529581729
|
|
7
|
+
data.tar.gz: '08880c49fc0803d36ba7d1ee3b457c130528155a0ede5d3360c34fc805bc94feb4f6b88f7ae16287b97bdfefc32bbb8c1fdb7a7c89cfee3d46606c6a071efd6d'
|
data/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,12 @@ upstream microsandbox runtime.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## [0.5.8] - 2026-06-17
|
|
10
|
+
|
|
11
|
+
Closes the `Sandbox`-class lifecycle gap with the official Python/Node/Go SDKs
|
|
12
|
+
and adds private/authenticated registry support plus first-use runtime
|
|
13
|
+
auto-provisioning (the keystone for precompiled gems). Wraps the same upstream
|
|
14
|
+
core (`v0.5.7`); this is a gem-only revision atop it.
|
|
10
15
|
|
|
11
16
|
### Added
|
|
12
17
|
|
|
@@ -42,6 +47,25 @@ Closes the `Sandbox`-class lifecycle gap with the official Python/Node/Go SDKs.
|
|
|
42
47
|
- CI now runs the real-microVM integration suite (`spec/integration`) on a
|
|
43
48
|
KVM-enabled runner, so the Rust↔core round-trip is exercised in automation —
|
|
44
49
|
not just compilation and unit tests.
|
|
50
|
+
- Registry authentication for `Sandbox.create`: `registry_auth: { username:,
|
|
51
|
+
password: }` (the password may be a token) for private/authenticated
|
|
52
|
+
registries and to lift Docker Hub's anonymous rate limit, plus
|
|
53
|
+
`registry_insecure:` (plain HTTP) and `registry_ca_certs:` (a PEM String or
|
|
54
|
+
Array) for self-hosted registries. Mirrors the Python/Node `registry_auth`
|
|
55
|
+
surface; without it the core's default resolution (OS keyring, global config,
|
|
56
|
+
`~/.docker/config.json`) still applies.
|
|
57
|
+
- `Microsandbox.ensure_runtime!` — provisions the `msb` runtime + `libkrunfw` on
|
|
58
|
+
first use, called automatically by `Sandbox.create`/`start`. This makes
|
|
59
|
+
**precompiled platform gems** usable without a manual `install` step (a
|
|
60
|
+
precompiled-gem user never ran the source build, so the runtime is fetched
|
|
61
|
+
lazily by the running host's arch). Opt out with `MICROSANDBOX_NO_AUTO_INSTALL`.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- The `cross-gems` release job now installs `libcap-ng-dev:arm64` via Debian
|
|
66
|
+
multiarch for the `aarch64-linux` cross-build (the extension links `-lcap-ng`
|
|
67
|
+
for the target arch). Precompiled gems remain `workflow_dispatch`-only and are
|
|
68
|
+
promoted to the publish path manually after per-platform validation.
|
|
45
69
|
|
|
46
70
|
## [0.5.7] - 2026-06-17
|
|
47
71
|
|
|
@@ -91,4 +115,6 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
|
|
|
91
115
|
core crate has Apple-native deps). Until precompiled gems are published,
|
|
92
116
|
installing from source requires a Rust toolchain (stable >= 1.91).
|
|
93
117
|
|
|
118
|
+
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.8...HEAD
|
|
119
|
+
[0.5.8]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.7...v0.5.8
|
|
94
120
|
[0.5.7]: https://github.com/superradcompany/microsandbox/releases/tag/v0.5.7
|
data/Cargo.lock
CHANGED
data/DESIGN.md
CHANGED
|
@@ -76,6 +76,15 @@ SDK-set path (`Microsandbox.runtime_path=`) → config file → workspace build
|
|
|
76
76
|
expose the core `setup::install`/`is_installed` for explicit, idempotent
|
|
77
77
|
provisioning (mirrors the Python `install()`/`is_installed()`).
|
|
78
78
|
|
|
79
|
+
Build-time provisioning only helps the **source gem**, where `build.rs` runs on
|
|
80
|
+
the user's own machine. A **precompiled gem** is built in CI, so its build-time
|
|
81
|
+
download lands on the CI host, not the user's — the user's `~/.microsandbox` is
|
|
82
|
+
empty. `Microsandbox.ensure_runtime!` closes that gap: `Sandbox.create`/`start`
|
|
83
|
+
call it to fetch the runtime on first use (by the *running* host's arch, which is
|
|
84
|
+
always correct), at most once per process. `MICROSANDBOX_NO_AUTO_INSTALL` opts
|
|
85
|
+
out (air-gapped hosts that provision out of band). libkrunfw is `dlopen`'d by
|
|
86
|
+
`msb` at runtime and is never linked into the extension.
|
|
87
|
+
|
|
79
88
|
## Core-crate dependency (self-contained)
|
|
80
89
|
|
|
81
90
|
`ext/microsandbox/Cargo.toml` depends on the core crate via a **pinned git tag**
|
|
@@ -90,14 +99,22 @@ git. The override must never be committed — it would break container builds.
|
|
|
90
99
|
|
|
91
100
|
* **Source gem**: compiles the extension via `extconf.rb` (rb-sys
|
|
92
101
|
`create_rust_makefile`); requires a Rust toolchain (MSRV below).
|
|
93
|
-
* **Precompiled platform gems**: built
|
|
102
|
+
* **Precompiled platform gems**: built best-effort by the `cross-gems` job
|
|
94
103
|
(`.github/workflows/release.yml`) with `oxidize-rb/cross-gem-action`
|
|
95
104
|
(`rake-compiler-dock`) per `Gem::Platform`, shipping multi-ABI
|
|
96
105
|
`lib/microsandbox/<ruby_abi>/` native artifacts — the same model Node uses with
|
|
97
|
-
per-platform packages. End users then install with no Rust toolchain
|
|
106
|
+
per-platform packages. End users then install with no Rust toolchain, and the
|
|
107
|
+
runtime is fetched on first use (see above). The guest `agentd` is baked into
|
|
108
|
+
the extension by *target* arch (`filesystem/build.rs` uses
|
|
109
|
+
`CARGO_CFG_TARGET_ARCH` + `include_bytes!`), so it cross-compiles correctly;
|
|
110
|
+
the real cross work is linking the *target* native libs — `libcap-ng` on Linux
|
|
111
|
+
(via Debian multiarch for `aarch64-linux`) and the Hypervisor + Security
|
|
112
|
+
frameworks on macOS (via osxcross; `arm64-darwin` is the platform still to
|
|
113
|
+
confirm — if osxcross can't link them, move it to a native `macos-14` runner).
|
|
114
|
+
The job is gated to `workflow_dispatch` and **not** auto-published on tags:
|
|
115
|
+
since CI can't boot a microVM to prove a built gem actually works, gems are
|
|
116
|
+
promoted to the publish path manually after per-platform validation. Published
|
|
98
117
|
to RubyGems via Trusted Publishing (OIDC). See [Releasing](README.md#releasing).
|
|
99
|
-
Whether the heavy core cross-builds for `arm64-darwin` under osxcross is
|
|
100
|
-
confirmed on first run; if not, that platform moves to a native macOS runner.
|
|
101
118
|
|
|
102
119
|
## Build requirements
|
|
103
120
|
|
|
@@ -120,14 +137,16 @@ API (`fs.read`/`write`/`list`/`mkdir`/`remove`/`stat`/…), `metrics`,
|
|
|
120
137
|
**OCI image-cache management** (`Image.get`/`list`/`inspect`/`remove`/`prune`),
|
|
121
138
|
**named volumes** (`Volume.create`/`get`/`list`/`remove` + `volumes:` mounts),
|
|
122
139
|
**snapshots** (`Snapshot.create`/`get`/`list`/`remove`/`verify`/`export`/`import`
|
|
123
|
-
+ `from_snapshot:` boot), `version`/`install`/`installed
|
|
124
|
-
|
|
140
|
+
+ `from_snapshot:` boot), `version`/`install`/`installed?`/`ensure_runtime!`,
|
|
141
|
+
**registry auth** (`registry_auth`/`registry_insecure`/`registry_ca_certs` on
|
|
142
|
+
`create`, for private/authenticated registries), and the typed error hierarchy.
|
|
125
143
|
|
|
126
144
|
Create options now cover `image`, `cpus`, `memory`, `oci_upper_size`, `env`,
|
|
127
145
|
`workdir`, `shell`, `user`, `hostname`, `labels`, `scripts`, `entrypoint`,
|
|
128
146
|
`ports`/`ports_udp`, `volumes`, `network` policy presets
|
|
129
147
|
(`public_only`/`none`/`allow_all`/`non_local`), `log_level`, `quiet_logs`,
|
|
130
|
-
`security`, `max_duration`, `idle_timeout`, `rlimits`, `pull_policy`,
|
|
148
|
+
`security`, `max_duration`, `idle_timeout`, `rlimits`, `pull_policy`,
|
|
149
|
+
`registry_auth`/`registry_insecure`/`registry_ca_certs`, `secrets`,
|
|
131
150
|
`from_snapshot`, `detached`, and `replace`/`replace_with_timeout`. `exec`/`shell`
|
|
132
151
|
add per-call `rlimits`.
|
|
133
152
|
|
|
@@ -135,7 +154,7 @@ add per-call `rlimits`.
|
|
|
135
154
|
|
|
136
155
|
The binding is verified at four levels:
|
|
137
156
|
|
|
138
|
-
1. **Unit** (
|
|
157
|
+
1. **Unit** (140 examples) — the Ruby layer's option normalization and value
|
|
139
158
|
objects, with the native layer stubbed.
|
|
140
159
|
2. **Real-microVM integration** (`spec/integration`, opt-in via
|
|
141
160
|
`MICROSANDBOX_INTEGRATION=1`) — boots actual sandboxes and round-trips
|
|
@@ -153,7 +172,7 @@ The binding is verified at four levels:
|
|
|
153
172
|
|
|
154
173
|
**Roadmap:** custom per-rule network policies (CIDR/domain/group allow-deny
|
|
155
174
|
rules — the presets and secret-host allowances are covered), file patches,
|
|
156
|
-
|
|
175
|
+
interactive `attach`/`attach_shell` (host-TTY coupled — raw mode,
|
|
157
176
|
SIGWINCH), SSH (`SshClient`/`SftpClient`/`SshServer`), and the raw agent client.
|
|
158
177
|
The native layer is structured so these slot in module-by-module, exactly as in
|
|
159
178
|
the Python binding.
|
data/README.md
CHANGED
|
@@ -21,7 +21,9 @@ This is an **unofficial, community-maintained** Ruby implementation — not part
|
|
|
21
21
|
|
|
22
22
|
- **Ruby** >= 3.1
|
|
23
23
|
- **Linux** with KVM enabled, or **macOS** on Apple Silicon (M-series)
|
|
24
|
-
-
|
|
24
|
+
- A **Rust** toolchain (stable >= 1.91) — needed only when installing the source
|
|
25
|
+
gem (it compiles the native extension on install). Precompiled per-platform
|
|
26
|
+
gems, where available, require no Rust toolchain; see [Releasing](#releasing)
|
|
25
27
|
|
|
26
28
|
## Installation
|
|
27
29
|
|
|
@@ -39,19 +41,29 @@ bundle install
|
|
|
39
41
|
gem install microsandbox-rb
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
Installing the **source gem** compiles the Rust extension, so the first install
|
|
45
|
+
takes a few minutes and needs a Rust toolchain (`rustc >= 1.91`) on `PATH`. When
|
|
46
|
+
a **precompiled platform gem** is available for your OS/architecture, RubyGems
|
|
47
|
+
picks it automatically and no Rust toolchain is required.
|
|
48
|
+
|
|
49
|
+
Either way the `msb` runtime and `libkrunfw` firmware are provisioned into
|
|
50
|
+
`~/.microsandbox` automatically on first use (the first `Sandbox.create`/`start`
|
|
51
|
+
downloads them if missing). To provision ahead of time — e.g. while baking a
|
|
52
|
+
container image, or to avoid the first-call latency — call `install` explicitly:
|
|
44
53
|
|
|
45
54
|
```ruby
|
|
46
55
|
Microsandbox.install unless Microsandbox.installed?
|
|
47
56
|
```
|
|
48
57
|
|
|
58
|
+
Set `MICROSANDBOX_NO_AUTO_INSTALL` to disable the automatic first-use download
|
|
59
|
+
(e.g. on air-gapped hosts that provision the runtime out of band).
|
|
60
|
+
|
|
49
61
|
## Quick start
|
|
50
62
|
|
|
51
63
|
```ruby
|
|
52
64
|
require "microsandbox"
|
|
53
65
|
|
|
54
|
-
Microsandbox::Sandbox.create("hello", image: "python") do |sb|
|
|
66
|
+
Microsandbox::Sandbox.create("hello", image: "public.ecr.aws/docker/library/python:3-slim") do |sb|
|
|
55
67
|
output = sb.exec("python", ["-c", "print('Hello, World!')"])
|
|
56
68
|
puts output.stdout # => "Hello, World!\n"
|
|
57
69
|
puts output.success? # => true
|
|
@@ -59,18 +71,25 @@ end
|
|
|
59
71
|
# the sandbox is stopped automatically when the block returns
|
|
60
72
|
```
|
|
61
73
|
|
|
74
|
+
> **Why `public.ecr.aws/docker/library/...`?** The examples pull from AWS's
|
|
75
|
+
> public mirror of the Docker Library because anonymous **Docker Hub** pulls are
|
|
76
|
+
> rate-limited and often fail with `registry error: Not authorized`. Plain short
|
|
77
|
+
> names like `image: "python"` work too if you aren't rate-limited. For private
|
|
78
|
+
> or authenticated registries (including authenticated Docker Hub), pass
|
|
79
|
+
> `registry_auth:` — see [Private & authenticated registries](#private--authenticated-registries).
|
|
80
|
+
|
|
62
81
|
## Usage
|
|
63
82
|
|
|
64
83
|
### Lifecycle
|
|
65
84
|
|
|
66
85
|
```ruby
|
|
67
86
|
# Block form — recommended; stops the sandbox automatically (even on error)
|
|
68
|
-
Microsandbox::Sandbox.create("box", image: "alpine") do |sb|
|
|
87
|
+
Microsandbox::Sandbox.create("box", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
|
|
69
88
|
# ...
|
|
70
89
|
end
|
|
71
90
|
|
|
72
91
|
# Manual form — you are responsible for stopping it
|
|
73
|
-
sb = Microsandbox::Sandbox.create("box", image: "alpine")
|
|
92
|
+
sb = Microsandbox::Sandbox.create("box", image: "public.ecr.aws/docker/library/alpine:latest")
|
|
74
93
|
begin
|
|
75
94
|
# ...
|
|
76
95
|
ensure
|
|
@@ -90,7 +109,7 @@ Microsandbox::Sandbox.remove("box") # remove a stopped sandbox
|
|
|
90
109
|
```ruby
|
|
91
110
|
Microsandbox::Sandbox.create(
|
|
92
111
|
"configured",
|
|
93
|
-
image: "python",
|
|
112
|
+
image: "public.ecr.aws/docker/library/python:3-slim",
|
|
94
113
|
cpus: 2,
|
|
95
114
|
memory: 1024, # MiB
|
|
96
115
|
env: { "API_BASE" => "https://example.com" },
|
|
@@ -107,7 +126,7 @@ end
|
|
|
107
126
|
### Executing commands
|
|
108
127
|
|
|
109
128
|
```ruby
|
|
110
|
-
Microsandbox::Sandbox.create("exec-demo", image: "alpine") do |sb|
|
|
129
|
+
Microsandbox::Sandbox.create("exec-demo", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
|
|
111
130
|
# Direct command (no shell)
|
|
112
131
|
out = sb.exec("ls", ["-la", "/etc"], cwd: "/", timeout: 30)
|
|
113
132
|
out.exit_code # => 0
|
|
@@ -130,7 +149,7 @@ failures (e.g. command not found) and timeouts raise typed errors (see below).
|
|
|
130
149
|
### Guest filesystem
|
|
131
150
|
|
|
132
151
|
```ruby
|
|
133
|
-
Microsandbox::Sandbox.create("fs-demo", image: "alpine") do |sb|
|
|
152
|
+
Microsandbox::Sandbox.create("fs-demo", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
|
|
134
153
|
sb.fs.write("/tmp/data.txt", "hello")
|
|
135
154
|
sb.fs.read_text("/tmp/data.txt") # => "hello" (UTF-8)
|
|
136
155
|
sb.fs.read("/tmp/data.txt") # => raw bytes (ASCII-8BIT)
|
|
@@ -151,7 +170,7 @@ end
|
|
|
151
170
|
### Metrics & logs
|
|
152
171
|
|
|
153
172
|
```ruby
|
|
154
|
-
Microsandbox::Sandbox.create("obs", image: "alpine") do |sb|
|
|
173
|
+
Microsandbox::Sandbox.create("obs", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
|
|
155
174
|
m = sb.metrics # => Microsandbox::Metrics
|
|
156
175
|
m.cpu_percent
|
|
157
176
|
m.memory_bytes
|
|
@@ -168,7 +187,7 @@ end
|
|
|
168
187
|
For long-running commands, stream events as they arrive instead of waiting:
|
|
169
188
|
|
|
170
189
|
```ruby
|
|
171
|
-
Microsandbox::Sandbox.create("stream", image: "python") do |sb|
|
|
190
|
+
Microsandbox::Sandbox.create("stream", image: "public.ecr.aws/docker/library/python:3-slim") do |sb|
|
|
172
191
|
handle = sb.exec_stream("python", ["-u", "-c", "import time\nfor i in range(3): print(i); time.sleep(1)"])
|
|
173
192
|
handle.each do |event| # ExecHandle is Enumerable
|
|
174
193
|
print event.text if event.stdout?
|
|
@@ -186,13 +205,45 @@ Manage the local OCI image cache (images are pulled automatically on `create`):
|
|
|
186
205
|
|
|
187
206
|
```ruby
|
|
188
207
|
Microsandbox::Image.list # => [Microsandbox::ImageInfo, ...]
|
|
189
|
-
Microsandbox::Image.get("alpine") # => Microsandbox::ImageInfo
|
|
190
|
-
Microsandbox::Image.inspect("alpine").layers # => [{...}, ...]
|
|
191
|
-
Microsandbox::Image.remove("alpine", force: true)
|
|
208
|
+
Microsandbox::Image.get("public.ecr.aws/docker/library/alpine:latest") # => Microsandbox::ImageInfo
|
|
209
|
+
Microsandbox::Image.inspect("public.ecr.aws/docker/library/alpine:latest").layers # => [{...}, ...]
|
|
210
|
+
Microsandbox::Image.remove("public.ecr.aws/docker/library/alpine:latest", force: true)
|
|
192
211
|
report = Microsandbox::Image.prune
|
|
193
212
|
report.bytes_reclaimed
|
|
194
213
|
```
|
|
195
214
|
|
|
215
|
+
### Private & authenticated registries
|
|
216
|
+
|
|
217
|
+
Images are pulled automatically on `create`. For a private registry — or to lift
|
|
218
|
+
Docker Hub's anonymous rate limit — pass `registry_auth:` with a username and a
|
|
219
|
+
password or token:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
Microsandbox::Sandbox.create(
|
|
223
|
+
"private",
|
|
224
|
+
image: "registry.example.com/team/app:latest",
|
|
225
|
+
registry_auth: { username: "ci-bot", password: ENV.fetch("REGISTRY_TOKEN") }
|
|
226
|
+
) do |sb|
|
|
227
|
+
# ...
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
For self-hosted registries you can also reach the registry over plain HTTP and
|
|
232
|
+
trust a private CA:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
Microsandbox::Sandbox.create(
|
|
236
|
+
"internal",
|
|
237
|
+
image: "registry.internal:5000/app:latest",
|
|
238
|
+
registry_insecure: true, # plain HTTP instead of HTTPS
|
|
239
|
+
registry_ca_certs: File.read("/etc/pki/internal-ca.pem") # String or Array of PEMs
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Without `registry_auth:`, the core's default credential resolution still applies
|
|
244
|
+
(OS keyring, global config, and `~/.docker/config.json`), so an existing
|
|
245
|
+
`docker login` is honored automatically.
|
|
246
|
+
|
|
196
247
|
### Named volumes
|
|
197
248
|
|
|
198
249
|
Persistent storage that outlives individual sandboxes:
|
|
@@ -201,7 +252,7 @@ Persistent storage that outlives individual sandboxes:
|
|
|
201
252
|
Microsandbox::Volume.create("cache", kind: "disk", size_mib: 512)
|
|
202
253
|
Microsandbox::Volume.list # => [Microsandbox::VolumeInfo, ...]
|
|
203
254
|
|
|
204
|
-
Microsandbox::Sandbox.create("with-vol", image: "alpine",
|
|
255
|
+
Microsandbox::Sandbox.create("with-vol", image: "public.ecr.aws/docker/library/alpine:latest",
|
|
205
256
|
volumes: { "/data" => { named: "cache" } }) do |sb|
|
|
206
257
|
sb.fs.write("/data/state.txt", "persisted")
|
|
207
258
|
end
|
|
@@ -219,8 +270,8 @@ All errors descend from `Microsandbox::Error` and carry a stable `#code`:
|
|
|
219
270
|
|
|
220
271
|
```ruby
|
|
221
272
|
begin
|
|
222
|
-
Microsandbox::Sandbox.create("dup", image: "alpine")
|
|
223
|
-
Microsandbox::Sandbox.create("dup", image: "alpine") # name clash
|
|
273
|
+
Microsandbox::Sandbox.create("dup", image: "public.ecr.aws/docker/library/alpine:latest")
|
|
274
|
+
Microsandbox::Sandbox.create("dup", image: "public.ecr.aws/docker/library/alpine:latest") # name clash
|
|
224
275
|
rescue Microsandbox::SandboxAlreadyExistsError => e
|
|
225
276
|
warn "#{e.code}: #{e.message}" # => "sandbox-already-exists: ..."
|
|
226
277
|
rescue Microsandbox::Error => e
|
|
@@ -301,15 +352,25 @@ one bound to the gem.
|
|
|
301
352
|
1. Bump `Microsandbox::VERSION` (and the `tag = "vX.Y.Z"` on the core-crate
|
|
302
353
|
dependency in `ext/microsandbox/Cargo.toml`) to match the upstream runtime,
|
|
303
354
|
update `CHANGELOG.md`.
|
|
304
|
-
2.
|
|
305
|
-
|
|
306
|
-
cross-build succeeds.
|
|
307
|
-
3. Push a `vX.Y.Z` tag. CI builds precompiled, multi-ABI platform gems
|
|
308
|
-
(`x86_64-linux`, `aarch64-linux`, `arm64-darwin`) with `rake-compiler-dock`,
|
|
309
|
-
plus the source gem, and pushes them to RubyGems. The publish job uses
|
|
310
|
-
`rubygems/configure-rubygems-credentials` (OIDC) with `id-token: write` — no
|
|
355
|
+
2. Push a `vX.Y.Z` tag. CI builds the **source gem** and pushes it to RubyGems
|
|
356
|
+
via `rubygems/configure-rubygems-credentials` (OIDC, `id-token: write`) — no
|
|
311
357
|
`RUBYGEMS_API_KEY` secret required.
|
|
312
358
|
|
|
359
|
+
> **Precompiled per-platform gems** are built best-effort by the `cross-gems`
|
|
360
|
+
> job, gated to manual `workflow_dispatch` so you can iterate
|
|
361
|
+
> (`gh workflow run release.yml`) without failing tag releases. They are not
|
|
362
|
+
> auto-published on tags yet: a gem that *compiles but can't boot a microVM*
|
|
363
|
+
> would be served to users ahead of the source gem, and CI can't boot a VM to
|
|
364
|
+
> prove otherwise — so promotion is manual after validating the artifact on each
|
|
365
|
+
> platform. A precompiled gem ships the compiled extension (with the guest
|
|
366
|
+
> `agentd` baked in by *target* arch); the host-side `msb` + `libkrunfw` runtime
|
|
367
|
+
> is fetched into `~/.microsandbox` on first use by `Microsandbox.ensure_runtime!`
|
|
368
|
+
> (libkrunfw is `dlopen`'d by `msb` at runtime, never linked into the gem). The
|
|
369
|
+
> real cross-compile work is linking the *target* native libraries — `libcap-ng`
|
|
370
|
+
> on Linux (handled via Debian multiarch in the workflow) and the Hypervisor +
|
|
371
|
+
> Security frameworks on macOS (via osxcross; the one platform left to confirm).
|
|
372
|
+
> Until promoted, users install the source gem (which compiles via `rb_sys`).
|
|
373
|
+
|
|
313
374
|
See [DESIGN.md](DESIGN.md) for the architecture and the implemented-surface
|
|
314
375
|
section for what's covered today vs. on the roadmap. Covered: full sandbox
|
|
315
376
|
lifecycle (including the async `request_stop`/`request_kill`/`request_drain`/
|
|
@@ -320,8 +381,10 @@ streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
|
|
|
320
381
|
named volumes, and snapshots (create/list/verify/export/import +
|
|
321
382
|
boot-from-snapshot). Create options span resources, network policy presets,
|
|
322
383
|
`log_level`/`security`/`rlimits`/`pull_policy`/`secrets` and more; `exec`/`shell`
|
|
323
|
-
take per-call `rlimits
|
|
324
|
-
|
|
384
|
+
take per-call `rlimits`, and `create` accepts `registry_auth`/`registry_insecure`/
|
|
385
|
+
`registry_ca_certs` for private and authenticated registries. Still on the
|
|
386
|
+
roadmap: custom per-rule network policies, file patches, interactive `attach`,
|
|
387
|
+
SSH, and the raw agent client.
|
|
325
388
|
|
|
326
389
|
## License
|
|
327
390
|
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
# `microsandbox/microsandbox_rb` and stays hidden behind `require "microsandbox"`.
|
|
5
5
|
name = "microsandbox_rb"
|
|
6
6
|
description = "Ruby SDK native extension for microsandbox — secure, fast microVM-based sandboxing."
|
|
7
|
-
version
|
|
7
|
+
# Must equal Microsandbox::VERSION (lib/microsandbox/version.rb) — Native.version
|
|
8
|
+
# returns this via env!("CARGO_PKG_VERSION") and version_spec.rb asserts equality.
|
|
9
|
+
# The core-crate dependency below stays pinned at its own tag (v0.5.7).
|
|
10
|
+
version = "0.5.8"
|
|
8
11
|
authors = ["Super Rad Company <development@superrad.company>"]
|
|
9
12
|
repository = "https://github.com/superradcompany/microsandbox"
|
|
10
13
|
license = "Apache-2.0"
|
data/ext/microsandbox/extconf.rb
CHANGED
|
@@ -3,6 +3,41 @@
|
|
|
3
3
|
require "mkmf"
|
|
4
4
|
require "rb_sys/mkmf"
|
|
5
5
|
|
|
6
|
+
# Preflight: the embedded microsandbox core is edition 2024 and pulls smoltcp,
|
|
7
|
+
# which sets a Minimum Supported Rust Version of 1.91. A `cargo` from an older
|
|
8
|
+
# toolchain (commonly Homebrew's rustc, which shadows a newer rustup on PATH and
|
|
9
|
+
# ignores this gem's rust-toolchain.toml) fails deep in the build with a cryptic
|
|
10
|
+
# "rustc X is not supported by smoltcp" error. Detect it up front and explain
|
|
11
|
+
# the fix instead.
|
|
12
|
+
MSRV = Gem::Version.new("1.91")
|
|
13
|
+
rustc_version = begin
|
|
14
|
+
out = `rustc --version 2>/dev/null`
|
|
15
|
+
out[/\d+\.\d+(\.\d+)?/] && Gem::Version.new(out[/\d+\.\d+(\.\d+)?/])
|
|
16
|
+
rescue StandardError
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
if rustc_version && rustc_version < MSRV
|
|
21
|
+
which_rustc = (`which rustc 2>/dev/null`.strip rescue "")
|
|
22
|
+
abort(<<~MSG)
|
|
23
|
+
|
|
24
|
+
[microsandbox-rb] Rust #{rustc_version} is too old — the embedded core requires rustc >= #{MSRV}.
|
|
25
|
+
Found: #{which_rustc.empty? ? "rustc" : which_rustc} (#{rustc_version})
|
|
26
|
+
|
|
27
|
+
This usually means an older rustc (e.g. Homebrew's) is ahead of a newer
|
|
28
|
+
rustup toolchain on your PATH. Fixes:
|
|
29
|
+
|
|
30
|
+
• Put rustup's toolchain first for the install:
|
|
31
|
+
PATH="$HOME/.cargo/bin:$PATH" gem install microsandbox-rb
|
|
32
|
+
• Or make a recent stable the default and ensure no other rustc shadows it:
|
|
33
|
+
rustup install stable && rustup default stable
|
|
34
|
+
• Or upgrade your system Rust to >= #{MSRV}.
|
|
35
|
+
|
|
36
|
+
(This gem ships a rust-toolchain.toml pinning `stable`; the rustup `cargo`
|
|
37
|
+
shim honors it, but a non-rustup `cargo` does not.)
|
|
38
|
+
MSG
|
|
39
|
+
end
|
|
40
|
+
|
|
6
41
|
# Builds the Rust cdylib and installs it as lib/microsandbox/microsandbox.{bundle,so}.
|
|
7
42
|
# The Cargo profile is selected via the RB_SYS_CARGO_PROFILE env var (defaults to
|
|
8
43
|
# release for installed gems, dev for `rake compile`).
|
|
@@ -19,6 +19,7 @@ use microsandbox::sandbox::{
|
|
|
19
19
|
SandboxMetrics, SandboxStatus, SandboxStopResult, SecurityProfile,
|
|
20
20
|
};
|
|
21
21
|
use microsandbox::LogLevel;
|
|
22
|
+
use microsandbox::RegistryAuth;
|
|
22
23
|
use microsandbox_network::policy::NetworkPolicy;
|
|
23
24
|
|
|
24
25
|
use crate::conv;
|
|
@@ -137,6 +138,14 @@ impl Sandbox {
|
|
|
137
138
|
if let Some(mib) = conv::opt_u32(opts, "oci_upper_size")? {
|
|
138
139
|
b = b.oci_upper_size(mib);
|
|
139
140
|
}
|
|
141
|
+
// Registry connection settings, for private / non-default registries:
|
|
142
|
+
// Basic auth (username + password/token), plain-HTTP `insecure`, and
|
|
143
|
+
// extra PEM CA roots. The Ruby layer flattens `registry_auth: {...}`
|
|
144
|
+
// into these keys; mirrors the Python `registry_auth=` and Node
|
|
145
|
+
// `.registry(r => r.auth(...))` surfaces.
|
|
146
|
+
if let Some(rc) = parse_registry_config(opts)? {
|
|
147
|
+
b = b.registry(move |r| rc.apply(r));
|
|
148
|
+
}
|
|
140
149
|
if let Some(secs) = conv::opt::<u64>(opts, "max_duration")? {
|
|
141
150
|
b = b.max_duration(secs);
|
|
142
151
|
}
|
|
@@ -502,6 +511,65 @@ fn parse_rlimits(opts: RHash) -> Result<Vec<(RlimitResource, u64, u64)>, Error>
|
|
|
502
511
|
Ok(out)
|
|
503
512
|
}
|
|
504
513
|
|
|
514
|
+
//--------------------------------------------------------------------------------------------------
|
|
515
|
+
// Registry option parsing
|
|
516
|
+
//--------------------------------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
/// Parsed registry connection settings (auth + transport). Built from the flat
|
|
519
|
+
/// `registry_*` option keys the Ruby layer normalizes `registry_auth:` into.
|
|
520
|
+
struct RegistryConfig {
|
|
521
|
+
auth: Option<RegistryAuth>,
|
|
522
|
+
insecure: bool,
|
|
523
|
+
ca_certs: Vec<String>,
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
impl RegistryConfig {
|
|
527
|
+
fn apply(
|
|
528
|
+
self,
|
|
529
|
+
mut r: microsandbox::sandbox::RegistryConfigBuilder,
|
|
530
|
+
) -> microsandbox::sandbox::RegistryConfigBuilder {
|
|
531
|
+
if let Some(auth) = self.auth {
|
|
532
|
+
r = r.auth(auth);
|
|
533
|
+
}
|
|
534
|
+
if self.insecure {
|
|
535
|
+
r = r.insecure();
|
|
536
|
+
}
|
|
537
|
+
for pem in self.ca_certs {
|
|
538
|
+
r = r.ca_certs(pem.into_bytes());
|
|
539
|
+
}
|
|
540
|
+
r
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/// Read the `registry_*` options, returning `None` when none are set (so the
|
|
545
|
+
/// default credential-resolution chain in the core is left untouched).
|
|
546
|
+
fn parse_registry_config(opts: RHash) -> Result<Option<RegistryConfig>, Error> {
|
|
547
|
+
let username = conv::opt_string(opts, "registry_username")?;
|
|
548
|
+
let password = conv::opt_string(opts, "registry_password")?;
|
|
549
|
+
let insecure = conv::opt_bool(opts, "registry_insecure")?;
|
|
550
|
+
let ca_certs = conv::opt_string_vec(opts, "registry_ca_certs")?;
|
|
551
|
+
|
|
552
|
+
let auth = match (username, password) {
|
|
553
|
+
(Some(username), Some(password)) => Some(RegistryAuth::Basic { username, password }),
|
|
554
|
+
(None, None) => None,
|
|
555
|
+
// A half-specified credential is a caller bug, not a silent anonymous pull.
|
|
556
|
+
_ => {
|
|
557
|
+
return Err(error::base_error(
|
|
558
|
+
"registry_auth requires both :username and :password",
|
|
559
|
+
))
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
if auth.is_none() && !insecure && ca_certs.is_empty() {
|
|
564
|
+
return Ok(None);
|
|
565
|
+
}
|
|
566
|
+
Ok(Some(RegistryConfig {
|
|
567
|
+
auth,
|
|
568
|
+
insecure,
|
|
569
|
+
ca_certs,
|
|
570
|
+
}))
|
|
571
|
+
}
|
|
572
|
+
|
|
505
573
|
//--------------------------------------------------------------------------------------------------
|
|
506
574
|
// Exec option parsing
|
|
507
575
|
//--------------------------------------------------------------------------------------------------
|
data/lib/microsandbox/sandbox.rb
CHANGED
|
@@ -119,6 +119,14 @@ module Microsandbox
|
|
|
119
119
|
# @param rlimits [Hash, nil] resource limits: { resource => limit } or
|
|
120
120
|
# { resource => [soft, hard] } (e.g. { nofile: 65_535 })
|
|
121
121
|
# @param pull_policy ["always","if-missing","never", nil] image pull behavior
|
|
122
|
+
# @param registry_auth [Hash, nil] credentials for a private/authenticated
|
|
123
|
+
# registry: { username:, password: } (the password may be a token).
|
|
124
|
+
# Without this the core's default resolution chain still applies (OS
|
|
125
|
+
# keyring, global config, `~/.docker/config.json`).
|
|
126
|
+
# @param registry_insecure [Boolean] reach the registry over plain HTTP
|
|
127
|
+
# instead of HTTPS (for local/self-hosted registries)
|
|
128
|
+
# @param registry_ca_certs [String, Array<String>, nil] extra PEM-encoded CA
|
|
129
|
+
# root certificate(s) to trust (for a registry with a private CA)
|
|
122
130
|
# @param secrets [Array<Hash>, nil] placeholder-protected secrets, each
|
|
123
131
|
# { env:, value:, host: } — the value is substituted by the TLS proxy only
|
|
124
132
|
# for the allowed host (auto-enables TLS interception)
|
|
@@ -133,8 +141,10 @@ module Microsandbox
|
|
|
133
141
|
entrypoint: nil, ports: nil, ports_udp: nil, volumes: nil, network: nil,
|
|
134
142
|
from_snapshot: nil, log_level: nil, quiet_logs: false, security: nil,
|
|
135
143
|
oci_upper_size: nil, max_duration: nil, idle_timeout: nil, rlimits: nil,
|
|
136
|
-
pull_policy: nil,
|
|
144
|
+
pull_policy: nil, registry_auth: nil, registry_insecure: false,
|
|
145
|
+
registry_ca_certs: nil, secrets: nil,
|
|
137
146
|
detached: false, replace: false, replace_with_timeout: nil)
|
|
147
|
+
Microsandbox.ensure_runtime!
|
|
138
148
|
opts = {}
|
|
139
149
|
opts["image"] = image.to_s if image
|
|
140
150
|
opts["from_snapshot"] = from_snapshot.to_s if from_snapshot
|
|
@@ -160,6 +170,7 @@ module Microsandbox
|
|
|
160
170
|
opts["idle_timeout"] = Integer(idle_timeout) if idle_timeout
|
|
161
171
|
opts["rlimits"] = normalize_rlimits(rlimits) if rlimits
|
|
162
172
|
opts["pull_policy"] = pull_policy.to_s if pull_policy
|
|
173
|
+
apply_registry_opts(opts, registry_auth, registry_insecure, registry_ca_certs)
|
|
163
174
|
opts["secrets"] = normalize_secrets(secrets) if secrets
|
|
164
175
|
opts["detached"] = true if detached
|
|
165
176
|
if replace_with_timeout
|
|
@@ -185,6 +196,7 @@ module Microsandbox
|
|
|
185
196
|
# Restart a previously-defined sandbox by name.
|
|
186
197
|
# @return [Sandbox]
|
|
187
198
|
def start(name, detached: false)
|
|
199
|
+
Microsandbox.ensure_runtime!
|
|
188
200
|
new(Native::Sandbox.start(name.to_s, { "detached" => detached }))
|
|
189
201
|
end
|
|
190
202
|
|
|
@@ -225,6 +237,25 @@ module Microsandbox
|
|
|
225
237
|
ports.each_with_object({}) { |(k, v), acc| acc[Integer(k)] = Integer(v) }
|
|
226
238
|
end
|
|
227
239
|
|
|
240
|
+
# Flatten the registry options into the native layer's `registry_*` keys.
|
|
241
|
+
# `auth` is a Hash { username:, password: } (string or symbol keys); both
|
|
242
|
+
# are required when given. `ca_certs` accepts one PEM string or an Array.
|
|
243
|
+
def apply_registry_opts(opts, auth, insecure, ca_certs)
|
|
244
|
+
if auth
|
|
245
|
+
username = auth[:username] || auth["username"]
|
|
246
|
+
password = auth[:password] || auth["password"]
|
|
247
|
+
unless username && password
|
|
248
|
+
# Report only the keys given, never the values — auth carries secrets.
|
|
249
|
+
raise ArgumentError,
|
|
250
|
+
"registry_auth needs :username and :password (got keys: #{auth.keys.inspect})"
|
|
251
|
+
end
|
|
252
|
+
opts["registry_username"] = username.to_s
|
|
253
|
+
opts["registry_password"] = password.to_s
|
|
254
|
+
end
|
|
255
|
+
opts["registry_insecure"] = true if insecure
|
|
256
|
+
opts["registry_ca_certs"] = Array(ca_certs).map(&:to_s) if ca_certs
|
|
257
|
+
end
|
|
258
|
+
|
|
228
259
|
# Normalize secrets into [env, value, host] triples for the native layer.
|
|
229
260
|
# Each entry is a Hash { env:, value:, host: } (string or symbol keys).
|
|
230
261
|
def normalize_secrets(secrets)
|
data/lib/microsandbox/version.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Microsandbox
|
|
4
|
-
# Gem version.
|
|
5
|
-
# the
|
|
6
|
-
|
|
4
|
+
# Gem version. Tracks the upstream microsandbox runtime (currently `v0.5.7`,
|
|
5
|
+
# the pinned core-crate tag); the patch segment advances for gem-only revisions
|
|
6
|
+
# that add bindings atop the same core. Must equal the native ext's Cargo crate
|
|
7
|
+
# version (`Native.version`), enforced by spec/unit/version_spec.rb.
|
|
8
|
+
VERSION = "0.5.8"
|
|
7
9
|
end
|
data/lib/microsandbox.rb
CHANGED
|
@@ -42,8 +42,14 @@ module Microsandbox
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
# Download and install the `msb` runtime + `libkrunfw` into
|
|
45
|
-
# `~/.microsandbox` (idempotent).
|
|
46
|
-
#
|
|
45
|
+
# `~/.microsandbox` (idempotent).
|
|
46
|
+
#
|
|
47
|
+
# When the gem is built from source, the native extension provisions the
|
|
48
|
+
# runtime at build time, so this is usually a no-op. Precompiled platform
|
|
49
|
+
# gems (which skip the local Rust build) do NOT provision it that way, so the
|
|
50
|
+
# runtime is fetched on first use — see {ensure_runtime!}. Call this
|
|
51
|
+
# explicitly to provision ahead of time (e.g. while baking a container
|
|
52
|
+
# image) so the first {Sandbox.create} doesn't pay the download.
|
|
47
53
|
# @return [nil]
|
|
48
54
|
def install
|
|
49
55
|
Native.install
|
|
@@ -55,6 +61,32 @@ module Microsandbox
|
|
|
55
61
|
Native.installed?
|
|
56
62
|
end
|
|
57
63
|
|
|
64
|
+
# Ensure the `msb` runtime + `libkrunfw` are present, provisioning them on
|
|
65
|
+
# first use if not. Called automatically by {Sandbox.create}/{Sandbox.start}
|
|
66
|
+
# so precompiled-gem users (who never ran the source build) get a working
|
|
67
|
+
# runtime without a manual {install} step.
|
|
68
|
+
#
|
|
69
|
+
# The download is attempted at most once per process. Opt out by setting
|
|
70
|
+
# `MICROSANDBOX_NO_AUTO_INSTALL` (e.g. air-gapped hosts that provision the
|
|
71
|
+
# runtime out of band); the subsequent operation then surfaces the missing
|
|
72
|
+
# runtime itself. Already-installed runtimes (e.g. source builds) skip
|
|
73
|
+
# straight through with only a cheap presence check.
|
|
74
|
+
# @return [nil]
|
|
75
|
+
def ensure_runtime!
|
|
76
|
+
return if @runtime_ready
|
|
77
|
+
if installed?
|
|
78
|
+
@runtime_ready = true
|
|
79
|
+
return
|
|
80
|
+
end
|
|
81
|
+
return if auto_install_disabled?
|
|
82
|
+
|
|
83
|
+
warn "[microsandbox] runtime (msb + libkrunfw) not found; " \
|
|
84
|
+
"downloading to ~/.microsandbox (set MICROSANDBOX_NO_AUTO_INSTALL to skip)..."
|
|
85
|
+
install
|
|
86
|
+
@runtime_ready = true
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
58
90
|
# @return [String] the resolved path to the `msb` runtime binary
|
|
59
91
|
def runtime_path
|
|
60
92
|
Native.resolved_msb_path
|
|
@@ -74,5 +106,14 @@ module Microsandbox
|
|
|
74
106
|
def all_sandbox_metrics
|
|
75
107
|
Native.all_sandbox_metrics.transform_values { |m| Metrics.new(m) }
|
|
76
108
|
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# Auto-provisioning is on by default; any non-empty, non-"0"/"false" value
|
|
113
|
+
# of MICROSANDBOX_NO_AUTO_INSTALL disables it.
|
|
114
|
+
def auto_install_disabled?
|
|
115
|
+
v = ENV["MICROSANDBOX_NO_AUTO_INSTALL"]
|
|
116
|
+
!v.nil? && !v.empty? && !%w[0 false no].include?(v.downcase)
|
|
117
|
+
end
|
|
77
118
|
end
|
|
78
119
|
end
|
data/sig/microsandbox.rbs
CHANGED
|
@@ -6,6 +6,7 @@ module Microsandbox
|
|
|
6
6
|
def self.version: () -> String
|
|
7
7
|
def self.install: () -> nil
|
|
8
8
|
def self.installed?: () -> bool
|
|
9
|
+
def self.ensure_runtime!: () -> nil
|
|
9
10
|
def self.runtime_path: () -> String
|
|
10
11
|
def self.runtime_path=: (String path) -> void
|
|
11
12
|
def self.all_sandbox_metrics: () -> Hash[String, Metrics]
|
|
@@ -143,6 +144,8 @@ module Microsandbox
|
|
|
143
144
|
?log_level: (String | Symbol)?, ?quiet_logs: bool, ?security: (String | Symbol)?,
|
|
144
145
|
?oci_upper_size: Integer?, ?max_duration: Integer?, ?idle_timeout: Integer?,
|
|
145
146
|
?rlimits: Hash[untyped, untyped]?, ?pull_policy: (String | Symbol)?,
|
|
147
|
+
?registry_auth: Hash[untyped, untyped]?, ?registry_insecure: bool,
|
|
148
|
+
?registry_ca_certs: (String | Array[String])?,
|
|
146
149
|
?secrets: Array[Hash[untyped, untyped]]?, ?detached: bool,
|
|
147
150
|
?replace: bool, ?replace_with_timeout: Numeric?)
|
|
148
151
|
?{ (Sandbox) -> untyped } -> untyped
|