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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d79b0ba5b32f44bda4e984d0a231c842a0ce938ffe6859776ead5270c1043561
4
- data.tar.gz: 96a97a6c3008070fdb3c32189973736f72d522942d94761d2656e0ffa0dfc030
3
+ metadata.gz: 56e1f366f8e18a95965bc917c65b739426499a147f1da4a2defb090dba4c375d
4
+ data.tar.gz: 689f3c5518ddc52e6fd851d290e2629d3a3a9a043ba09653a6ce1041039a9a37
5
5
  SHA512:
6
- metadata.gz: 75291b5c9e1de3fe6b8b444d327d1716be099846fb87121ed9d5d00bf514bae39168cb4f7d246dabe6e41e8383b975f10e2bb7d68e0080ee9fb4ff59d546fad5
7
- data.tar.gz: eaeb263b8b666f55f6614d88f9a2643379820d7f3be293c2b87ab1810bf164be3a7c6e5f5cb36860abcbd6ab03120cd7f7a91bdaeb1652e5048f369b396758ef
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
- Closes the `Sandbox`-class lifecycle gap with the official Python/Node/Go SDKs.
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
@@ -3232,7 +3232,7 @@ dependencies = [
3232
3232
 
3233
3233
  [[package]]
3234
3234
  name = "microsandbox_rb"
3235
- version = "0.5.7"
3235
+ version = "0.5.8"
3236
3236
  dependencies = [
3237
3237
  "chrono",
3238
3238
  "futures",
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 in CI on a `vX.Y.Z` tag
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. Published
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?`, and
124
- the typed error hierarchy.
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`, `secrets`,
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** (130 examples) — the Ruby layer's option normalization and value
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
- registry auth, interactive `attach`/`attach_shell` (host-TTY coupled — raw mode,
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
- - Building from source additionally needs a **Rust** toolchain (stable >= 1.91)
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
- The first build downloads the `msb` runtime and `libkrunfw` firmware into
43
- `~/.microsandbox`. You can (re)provision them explicitly at any time:
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. *(Recommended first time)* run the workflow's manual `dry_run` dispatch to
305
- build all platform gems without publishing confirms the `arm64-darwin`
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`. Still on the roadmap: custom per-rule network policies,
324
- file patches, registry auth, interactive `attach`, SSH, and the raw agent client.
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
 
@@ -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 = "0.5.7"
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"
@@ -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
  //--------------------------------------------------------------------------------------------------
@@ -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, secrets: 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)
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Microsandbox
4
- # Gem version. Kept in lock-step with the upstream microsandbox runtime and
5
- # the official Python/Node/Go SDKs (workspace version in the microsandbox repo).
6
- VERSION = "0.5.7"
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). Usually unnecessary: the native
46
- # extension provisions the runtime at build time.
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: microsandbox-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.7
4
+ version: 0.5.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao