microsandbox-rb 0.17.1 → 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 +94 -8
- data/Cargo.lock +1 -1
- data/DESIGN.md +85 -36
- data/README.md +6 -6
- data/SECURITY.md +1 -1
- data/docs/execution.md +8 -1
- data/docs/lifecycle.md +33 -0
- data/docs/releasing.md +85 -34
- data/docs/runtime.md +3 -1
- data/docs/surface.md +13 -8
- 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/image.rs +2 -2
- data/ext/microsandbox/src/lib.rs +2 -2
- data/ext/microsandbox/src/runtime.rs +189 -13
- data/ext/microsandbox/src/sandbox.rs +88 -71
- 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 +18 -16
- data/lib/microsandbox/sandbox.rb +75 -4
- data/lib/microsandbox/version.rb +1 -1
- data/sig/microsandbox.rbs +24 -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: f76670a6753a6503a0764ed173db3c33a1eff9a62de7a7d9343bc76f8143c1ba
|
|
4
|
+
data.tar.gz: 80d4d4f1c2bbb3f47dd8f714602e6b01a841d62971b70b5682d3ad0c7dcff904
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 74429f29417c1b2c2e6aeee35948d3c5201d5ac9d001beba4efceca484dbea53f0041e84f0f7fa0fef5933a89f3e317737fb134d6329267eb5e239fe56d7e297
|
|
7
|
+
data.tar.gz: d4d5072d1a68079a8bfe199e8c53e0d92e7614515e1326e81333f857c1b53b681575f8df2162abb167ea6228841ffba2631f4d2b8df38552040c946966223c2c
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,81 @@ wraps, and the README's Versioning section keeps the full gem→runtime map.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.17.2] - 2026-10-12
|
|
12
|
+
|
|
13
|
+
Gem-only fix and release-pipeline hardening release; the embedded runtime
|
|
14
|
+
stays at **`v0.6.18`**. One behaviour change is visible: under
|
|
15
|
+
`Thread.handle_interrupt(X => :on_blocking)` a native call now raises an
|
|
16
|
+
already-deferred exception before it starts instead of after it finishes,
|
|
17
|
+
except in the four sandbox-acquisition methods, which deliver it after the
|
|
18
|
+
acquisition (see the first Fixed entry).
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Two contract specs keep the hand-maintained parts honest:
|
|
23
|
+
`spec/unit/rbs_contract_spec.rb` compares `sig/microsandbox.rbs` with the
|
|
24
|
+
loaded gem, and `spec/unit/docs_contract_spec.rb` checks the docs against
|
|
25
|
+
the code (version table, CHANGELOG links, error table, option list,
|
|
26
|
+
examples, release checklist). CI also runs `rbs -I sig validate`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- **Release pipeline hardened** (no change to the gem's code or API). A tag
|
|
31
|
+
now publishes only after a preflight passes — the tag equals the three
|
|
32
|
+
version constants, its commit is on `main`, and CI is green for that exact
|
|
33
|
+
commit — and after the companion `microsandbox-rb-binaries` gems have built
|
|
34
|
+
(a companion build failure now blocks the SDK publish; a companion push
|
|
35
|
+
failure still needs a re-run of `publish-binaries`). The published gem is built
|
|
36
|
+
with plain `gem build`, every workflow action is pinned to a commit SHA
|
|
37
|
+
(kept current by Dependabot), and CI checks `Cargo.lock` against the RustSec
|
|
38
|
+
advisory database with `cargo deny`. See [docs/releasing.md](docs/releasing.md).
|
|
39
|
+
- **The precompiled-extension release job is removed.** The manual-dispatch
|
|
40
|
+
`cross-gems` job never produced a gem. Precompiled extension gems are not
|
|
41
|
+
built or published; every platform installs the source gem (compiled via
|
|
42
|
+
`rb_sys`), exactly as before. The `microsandbox-rb-binaries` gems are
|
|
43
|
+
unaffected.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **An interrupted call no longer leaks what it produced.** A `Timeout.timeout`
|
|
48
|
+
(or any `Thread#raise`), a `Thread#kill` or a Ctrl-C that arrived while a
|
|
49
|
+
native call was in flight used to discard the call's result without
|
|
50
|
+
releasing it: `Timeout.timeout { Sandbox.create(...) }` raised
|
|
51
|
+
`Timeout::Error` and left the sandbox running with no Ruby object owning it
|
|
52
|
+
until the process exited, an interrupted `exec_stream` left its guest
|
|
53
|
+
session open with nothing able to kill it, and an interrupted `exec` kept
|
|
54
|
+
its collected output in memory. The result now always reaches Ruby, and
|
|
55
|
+
what happens to it depends on the interrupt:
|
|
56
|
+
- **`Timeout`/`Thread#raise` and `Thread#kill`** during `Sandbox.create`,
|
|
57
|
+
`Sandbox.connect_or_create`, `Sandbox.start` or `PullSession#sandbox`: the
|
|
58
|
+
sandbox is stopped before the exception propagates — only one the call
|
|
59
|
+
owns; a sandbox `connect_or_create` merely connected to is left running.
|
|
60
|
+
The stop is best-effort: if it fails, the interrupt is still what you
|
|
61
|
+
see, and the sandbox is torn down when its object is garbage-collected.
|
|
62
|
+
- **Ctrl-C (`SIGINT`)** during those four, and **any interrupt** during
|
|
63
|
+
every other call (`exec_stream`, streams, SSH clients, …): nothing is
|
|
64
|
+
stopped at once; the result is released — and a sandbox torn down — when
|
|
65
|
+
its unreferenced object is garbage-collected.
|
|
66
|
+
|
|
67
|
+
Native calls are still uninterruptible, so the exception is raised once the
|
|
68
|
+
call has finished, and in the first case only after the cleanup stop as well
|
|
69
|
+
(a timed-out create therefore returns later than before). One visible
|
|
70
|
+
difference for `Thread.handle_interrupt(X => :on_blocking)`: a native call
|
|
71
|
+
now raises an exception that is already deferred *before* it starts, where
|
|
72
|
+
it used to raise it after finishing. The four acquisition methods above are
|
|
73
|
+
the exception — they hold every queued interrupt back until the sandbox is
|
|
74
|
+
in the caller's hands, so there a deferred exception is delivered after the
|
|
75
|
+
acquisition (at the thread's next blocking operation), not before it.
|
|
76
|
+
- **RBS: `Klass.new` now type-checks with its real arguments.** 24 classes
|
|
77
|
+
whose constructor takes arguments had no `initialize` declaration, so RBS
|
|
78
|
+
synthesized a zero-argument `new` for them.
|
|
79
|
+
- **Docs:** `docs/surface.md` now lists every `create`/`modify` keyword;
|
|
80
|
+
`DESIGN.md` no longer names a runtime tag the gem stopped pinning long ago;
|
|
81
|
+
the `with_backend` example in `docs/runtime.md` is valid Ruby. `0.9.3`
|
|
82
|
+
(never tagged or published) is folded into `0.10.0`, both in this file and
|
|
83
|
+
in the README's Versioning table, and this file now defines the compare
|
|
84
|
+
links for `0.9.1`–`0.13.0`.
|
|
85
|
+
|
|
11
86
|
## [0.17.1] - 2026-10-12
|
|
12
87
|
|
|
13
88
|
Gem-only security and hardening release; the embedded runtime stays at
|
|
@@ -659,8 +734,10 @@ could make a later CLI copy-out overwrite an arbitrary host file — the SDK
|
|
|
659
734
|
|
|
660
735
|
Ruby bindings for the additive `v0.6.6` SDK surface (upstream #1099 / #1128),
|
|
661
736
|
bringing the gem to parity with the Python and Node SDKs, which already ship
|
|
662
|
-
this API.
|
|
663
|
-
|
|
737
|
+
this API. This release also adopts the upstream runtime **`v0.6.3` →
|
|
738
|
+
`v0.6.6`**: that work was prepared as `0.9.3`, which was never tagged or
|
|
739
|
+
published, so the runtime bump and the bindings for its new surface first
|
|
740
|
+
shipped together here. Neither part is a breaking change.
|
|
664
741
|
|
|
665
742
|
### Added
|
|
666
743
|
|
|
@@ -693,16 +770,18 @@ Deliberately **not** exposed, matching the Python and Node SDKs (both surface
|
|
|
693
770
|
these only through the CLI): `modify`'s `oci_upper_size` overlay-grow field, and
|
|
694
771
|
the `SandboxMetricsReport` / per-sandbox metrics-report helpers.
|
|
695
772
|
|
|
696
|
-
|
|
773
|
+
### Runtime adoption v0.6.3 → v0.6.6 (prepared as 0.9.3, first released in 0.10.0)
|
|
774
|
+
|
|
775
|
+
`0.9.3` was never tagged or published; everything below shipped in `0.10.0`.
|
|
697
776
|
|
|
698
777
|
Adopts upstream runtime **`v0.6.3` → `v0.6.6`** (spanning upstream `v0.6.4` and
|
|
699
778
|
`v0.6.6`; `v0.6.5` was yanked upstream — its GitHub release was pulled and the
|
|
700
779
|
type-model refactor it shipped, #1014, was reverted in `v0.6.6`, #1143). The
|
|
701
780
|
upstream SDK crate's public API grew only *additive* surface (live
|
|
702
|
-
modify/resize, ping/touch
|
|
703
|
-
|
|
781
|
+
modify/resize, ping/touch). The Ruby bindings for it are the `### Added`
|
|
782
|
+
entries above.
|
|
704
783
|
|
|
705
|
-
|
|
784
|
+
#### Fixed
|
|
706
785
|
|
|
707
786
|
- **Snapshot restore survives upstream tag republishes** (upstream #1130):
|
|
708
787
|
restore now pulls the OCI image by its pinned manifest digest
|
|
@@ -722,7 +801,7 @@ so this is a pure runtime bump with no Ruby API change.
|
|
|
722
801
|
- **Filesystem snapshot `readdir` streams entries** (upstream #1133), fixing
|
|
723
802
|
unbounded memory growth (RSS leak) when listing very large directories.
|
|
724
803
|
|
|
725
|
-
|
|
804
|
+
#### Changed
|
|
726
805
|
|
|
727
806
|
- **Embedded runtime is now `v0.6.6`** (`Microsandbox::RUNTIME_VERSION` /
|
|
728
807
|
`Microsandbox.runtime_version`). Also inherited:
|
|
@@ -1391,12 +1470,19 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
|
|
|
1391
1470
|
core crate has Apple-native deps). Until precompiled gems are published,
|
|
1392
1471
|
installing from source requires a Rust toolchain (stable >= 1.91).
|
|
1393
1472
|
|
|
1394
|
-
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.
|
|
1473
|
+
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.2...HEAD
|
|
1474
|
+
[0.17.2]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.1...v0.17.2
|
|
1395
1475
|
[0.17.1]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.0...v0.17.1
|
|
1396
1476
|
[0.17.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.16.0...v0.17.0
|
|
1397
1477
|
[0.16.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.15.0...v0.16.0
|
|
1398
1478
|
[0.15.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.14.0...v0.15.0
|
|
1399
1479
|
[0.14.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.13.0...v0.14.0
|
|
1480
|
+
[0.13.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.12.0...v0.13.0
|
|
1481
|
+
[0.12.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.11.0...v0.12.0
|
|
1482
|
+
[0.11.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.10.0...v0.11.0
|
|
1483
|
+
[0.10.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.9.2...v0.10.0
|
|
1484
|
+
[0.9.2]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.9.1...v0.9.2
|
|
1485
|
+
[0.9.1]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.9.0...v0.9.1
|
|
1400
1486
|
[0.9.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.8.2...v0.9.0
|
|
1401
1487
|
[0.8.2]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.8.1...v0.8.2
|
|
1402
1488
|
[0.8.1]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.8.0...v0.8.1
|
data/Cargo.lock
CHANGED
data/DESIGN.md
CHANGED
|
@@ -50,7 +50,7 @@ multi-threaded tokio runtime via `runtime.block_on(fut)`.
|
|
|
50
50
|
|
|
51
51
|
### GVL release
|
|
52
52
|
|
|
53
|
-
`block_on` is wrapped in `nogvl` (`
|
|
53
|
+
`block_on` is wrapped in `nogvl` (`rb_thread_call_without_gvl2`) so the Ruby
|
|
54
54
|
Global VM Lock is **released** while a sandbox operation is in flight. Without
|
|
55
55
|
this, a long-running `exec` would freeze every other Ruby thread/fiber in the
|
|
56
56
|
process (a real problem under Rails/Puma). The `nogvl` closure runs pure Rust
|
|
@@ -59,21 +59,81 @@ captured and re-raised *after* the GVL is re-acquired rather than unwinding
|
|
|
59
59
|
across the C frame (which would be UB).
|
|
60
60
|
|
|
61
61
|
**The *calling* thread is not preemptible during the call.** `nogvl` passes a
|
|
62
|
-
null unblock-function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
null unblock-function, so while a native call blocks, the thread that issued it
|
|
63
|
+
cannot be interrupted: `Timeout::timeout` (which relies on `Thread#raise`),
|
|
64
|
+
`Thread#kill`, and `SIGINT` (Ctrl-C) are all deferred until the call returns on
|
|
65
|
+
its own. The GVL release keeps *other* threads live; it does **not** make the
|
|
66
|
+
calling thread cancelable. This is harmless for the bounded calls
|
|
67
|
+
(`exec`/`shell` with a `timeout:`, `AgentClient` with a `timeout:`) because the
|
|
68
|
+
deadline fires inside the future, but the unbounded/streaming paths —
|
|
69
|
+
`exec`/`shell` with `timeout: nil`, `ExecHandle#recv`/`#wait`,
|
|
70
|
+
`log_stream`/`metrics_stream` `recv` (especially `follow: true`),
|
|
71
|
+
`Sandbox#wait`/`SandboxHandle#wait_until_stopped`, and `AgentClient#request`
|
|
72
|
+
with no timeout — can block their caller indefinitely if the guest wedges or a
|
|
73
|
+
relay drops. **Bound such calls with the explicit `timeout:` knobs rather than
|
|
74
|
+
wrapping them in `Timeout::timeout`**, which will not fire while the native
|
|
75
|
+
call is in flight.
|
|
76
|
+
|
|
77
|
+
### Interrupts never lose a result
|
|
78
|
+
|
|
79
|
+
An interrupt that arrives during a native call is delivered **after the call
|
|
80
|
+
returns**, and the call's result is not lost. Ruby delivers `Thread#raise`,
|
|
81
|
+
`Thread#kill` and `SIGINT` with a `longjmp`, which skips Rust frames without
|
|
82
|
+
running destructors; if that happened while the value a call had just produced
|
|
83
|
+
(a booted sandbox, a guest exec session, megabytes of output) lived only in a
|
|
84
|
+
Rust frame, it would be neither returned to Ruby nor dropped. The plain
|
|
85
|
+
`rb_thread_call_without_gvl` does exactly that — it checks interrupts once more
|
|
86
|
+
after the callback, before returning — which is why it is not used. Two rules
|
|
87
|
+
in `runtime.rs` replace it:
|
|
88
|
+
|
|
89
|
+
1. `nogvl` checks interrupts itself, under `rb_protect`, **only before the work
|
|
90
|
+
starts**: once up front, and again whenever `rb_thread_call_without_gvl2`
|
|
91
|
+
declined to run the callback because an interrupt was pending at entry. A
|
|
92
|
+
raise or kill there is an ordinary `Err` that unwinds the Rust frames
|
|
93
|
+
normally — nothing has happened yet. After the callback has run, nothing is
|
|
94
|
+
checked. (`nogvl` and `block_on` therefore return `Result`, and every call
|
|
95
|
+
site propagates it with `?`. A bound method that needs two awaits does both
|
|
96
|
+
in one `block_on`, so no value sits between two entry checks.)
|
|
97
|
+
2. Between `block_on` returning and the bound method returning its value, the
|
|
98
|
+
binding does not call into Ruby: no `funcall` (the VM checks interrupts
|
|
99
|
+
after every one), only C-level Hash/Array/String construction and
|
|
100
|
+
`TypedData` wrapping. The class lookup magnus would otherwise do with
|
|
101
|
+
`const_get` on a type's first wrap is resolved at extension load instead.
|
|
102
|
+
|
|
103
|
+
So the next interrupt checkpoint after a native call is the Ruby VM's own,
|
|
104
|
+
reached once the result is a Ruby object. What happens to it then:
|
|
105
|
+
|
|
106
|
+
- **`Sandbox.create`, `Sandbox.connect_or_create`, `Sandbox.start`, and
|
|
107
|
+
`PullSession#sandbox`** mask interrupts (`Thread.handle_interrupt(Object =>
|
|
108
|
+
:never)`) around the native call plus the wrap, and an `ensure` **stops the
|
|
109
|
+
sandbox** when an exception or a kill cuts in before the caller has the
|
|
110
|
+
object — but only a sandbox the call owns (`Sandbox#owns_lifecycle?`, the
|
|
111
|
+
same predicate as `connect_or_create`'s block form): one that was already
|
|
112
|
+
running for someone else is left running. A `SIGINT` cannot be masked this
|
|
113
|
+
way; it can arrive before the wrapper is assigned, and then the next case
|
|
114
|
+
applies.
|
|
115
|
+
- **Every other wrapper** (`ExecHandle`, streams, SSH clients, write sinks, …)
|
|
116
|
+
and every plain result simply becomes garbage; GC frees the wrapper and the
|
|
117
|
+
Rust value's `Drop` closes the session or connection.
|
|
118
|
+
|
|
119
|
+
The calls themselves remain uninterruptible, so `Timeout.timeout { Sandbox.create(...) }`
|
|
120
|
+
still waits for the create to finish — and now also for the stop that cleans
|
|
121
|
+
it up — before `Timeout::Error` surfaces. That stop is best-effort (a failure
|
|
122
|
+
is swallowed so the interrupt is what the caller sees; GC then tears the
|
|
123
|
+
sandbox down).
|
|
124
|
+
|
|
125
|
+
`Thread.handle_interrupt(X => :on_blocking)` treats a native call as the
|
|
126
|
+
blocking point at its *start*: the bridge raises an exception that is already
|
|
127
|
+
deferred before the call begins, and one that arrives during the call waits
|
|
128
|
+
for the thread's next blocking operation. The four acquisition methods are
|
|
129
|
+
the exception: their own `Object => :never` mask sits on top of the caller's,
|
|
130
|
+
so a deferred exception is not raised at the native call's entry there — the
|
|
131
|
+
sandbox is returned and the exception is delivered at the next blocking
|
|
132
|
+
operation after the acquisition.
|
|
133
|
+
|
|
134
|
+
`spec/fake_runtime/interrupt_safety_spec.rb` pins all of this against the real
|
|
135
|
+
extension, driving it with a fake `msb` (`spec/support/fake_runtime/`) whose
|
|
136
|
+
log shows whether a sandbox was stopped, dropped, or left attached.
|
|
77
137
|
|
|
78
138
|
## Error mapping
|
|
79
139
|
|
|
@@ -185,7 +245,7 @@ extension.
|
|
|
185
245
|
`ext/microsandbox/Cargo.toml` depends on the core crate via a **pinned git tag**
|
|
186
246
|
(`microsandbox` / `microsandbox-network`, pinned to the same tag as
|
|
187
247
|
`Microsandbox::RUNTIME_VERSION` — currently `v0.6.18`), so the gem builds anywhere
|
|
188
|
-
— CI
|
|
248
|
+
— CI and end-user source installs —
|
|
189
249
|
without an adjacent checkout. For fast local development against a sibling
|
|
190
250
|
microsandbox checkout, copy `.cargo/config.toml.example` to `.cargo/config.toml`
|
|
191
251
|
(gitignored); its `paths` override builds against the local crates instead of
|
|
@@ -195,25 +255,13 @@ git. The override must never be committed — it would break container builds.
|
|
|
195
255
|
|
|
196
256
|
* **Source gem**: compiles the extension via `extconf.rb` (rb-sys
|
|
197
257
|
`create_rust_makefile`); requires a Rust toolchain (MSRV below).
|
|
198
|
-
* **Precompiled platform gems**: built
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
host runtime comes from the `microsandbox-rb-binaries` gem — or, without it, is
|
|
204
|
-
fetched on first use (see above). The guest `agentd` is baked into
|
|
205
|
-
the extension by *target* arch (`filesystem/build.rs` uses
|
|
206
|
-
`CARGO_CFG_TARGET_ARCH` + `include_bytes!`), so it cross-compiles correctly;
|
|
207
|
-
the real cross work is linking the *target* native libs — `libcap-ng` on Linux
|
|
208
|
-
(via Debian multiarch for `aarch64-linux`) and the Hypervisor + Security
|
|
209
|
-
frameworks on macOS (via osxcross; `arm64-darwin` is the platform still to
|
|
210
|
-
confirm — if osxcross can't link them, move it to a native `macos-14` runner).
|
|
211
|
-
The job is gated to `workflow_dispatch` and **not** auto-published on tags:
|
|
212
|
-
since CI can't boot a microVM to prove a built gem actually works, gems are
|
|
213
|
-
promoted to the publish path manually after per-platform validation. Published
|
|
214
|
-
to RubyGems via Trusted Publishing (OIDC). See [docs/releasing.md](docs/releasing.md).
|
|
258
|
+
* **Precompiled platform gems**: not built or published. Every platform
|
|
259
|
+
installs the source gem, which compiles the extension via `rb_sys` at install
|
|
260
|
+
time; the host runtime comes from the `microsandbox-rb-binaries` gem — or,
|
|
261
|
+
without it, is fetched on first use (see above). The companion binaries gems
|
|
262
|
+
below are unaffected. See [docs/releasing.md](docs/releasing.md).
|
|
215
263
|
* **Runtime binaries gems** (`microsandbox-rb-binaries`): a distinct artifact
|
|
216
|
-
from the
|
|
264
|
+
from the SDK gem — no Ruby code beyond a small
|
|
217
265
|
locator module, just the verified upstream `msb` + `libkrunfw` for one
|
|
218
266
|
platform. CI's `binaries` job vendors and builds all three platforms on every
|
|
219
267
|
run and smoke-tests the host one; the `package` job installs the source gem
|
|
@@ -269,7 +317,8 @@ create options, full mount options (tmpfs/disk + stat-virtualization/
|
|
|
269
317
|
host-permissions), and snapshot inspection (`open`/`list_dir`/`reindex`).
|
|
270
318
|
|
|
271
319
|
A few **secondary** upstream knobs remain unexposed (a genuine binding gap, not
|
|
272
|
-
upstream-gated — they exist at the pinned
|
|
320
|
+
upstream-gated — they exist at the pinned runtime,
|
|
321
|
+
`Microsandbox::RUNTIME_VERSION`): per-published-port host
|
|
273
322
|
**bind address** (ports always bind loopback), network **interface overrides**,
|
|
274
323
|
and inline **named-volume create-mode** (pre-create with `Volume.create`, then
|
|
275
324
|
mount with `{ named: }`). These slot in module-by-module exactly as the existing
|
data/README.md
CHANGED
|
@@ -40,7 +40,7 @@ gem compiles the Rust extension, so the first install takes a few minutes.
|
|
|
40
40
|
- Linux with KVM enabled, or macOS on Apple Silicon. Cloud-only users
|
|
41
41
|
(`MSB_BACKEND=cloud`) need neither.
|
|
42
42
|
- A stable Rust toolchain (1.91 or newer) on `PATH` to compile the source gem.
|
|
43
|
-
Precompiled extension gems
|
|
43
|
+
Precompiled extension gems are not published; see
|
|
44
44
|
[docs/releasing.md](docs/releasing.md).
|
|
45
45
|
- The `msb` runtime and `libkrunfw` firmware, from the companion gem above or
|
|
46
46
|
downloaded on first use. Provision ahead of time (a Docker layer, an
|
|
@@ -170,7 +170,7 @@ See [docs/observability.md](docs/observability.md).
|
|
|
170
170
|
| Runtime binaries gem, `msb` resolution order, backends, environment variables | [docs/runtime.md](docs/runtime.md) |
|
|
171
171
|
| Error classes and stable codes | [docs/errors.md](docs/errors.md) |
|
|
172
172
|
| Complete supported surface and what is not yet exposed | [docs/surface.md](docs/surface.md) |
|
|
173
|
-
| Release process,
|
|
173
|
+
| Release process, release preflight, binaries gems | [docs/releasing.md](docs/releasing.md) |
|
|
174
174
|
| Architecture: native extension, sync bridge, GVL, error mapping | [DESIGN.md](DESIGN.md) |
|
|
175
175
|
| Companion runtime gem internals | [binaries/README.md](https://github.com/ya-luotao/microsandbox-rb/blob/main/binaries/README.md) |
|
|
176
176
|
|
|
@@ -209,7 +209,7 @@ share the upstream tag, but the numbers have diverged since and the gem version
|
|
|
209
209
|
is **not** an indicator of the embedded runtime. Ask the build instead:
|
|
210
210
|
|
|
211
211
|
```ruby
|
|
212
|
-
Microsandbox::VERSION # => "0.17.
|
|
212
|
+
Microsandbox::VERSION # => "0.17.2" (the gem's own version)
|
|
213
213
|
Microsandbox.runtime_version # => "v0.6.18" (the embedded upstream runtime tag)
|
|
214
214
|
```
|
|
215
215
|
|
|
@@ -237,8 +237,7 @@ constants against this gem's. Every release records its runtime in
|
|
|
237
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 |
|
|
238
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 |
|
|
239
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 |
|
|
240
|
-
| `0.
|
|
241
|
-
| `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` |
|
|
242
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 |
|
|
243
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 |
|
|
244
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` |
|
|
@@ -247,13 +246,14 @@ constants against this gem's. Every release records its runtime in
|
|
|
247
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` |
|
|
248
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:` |
|
|
249
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 |
|
|
250
250
|
|
|
251
251
|
## Development
|
|
252
252
|
|
|
253
253
|
```bash
|
|
254
254
|
bin/setup # bundle install + compile the native extension
|
|
255
255
|
bundle exec rake compile # rebuild the extension (debug); compile:release for optimized
|
|
256
|
-
bundle exec rake spec # unit specs (no runtime needed)
|
|
256
|
+
bundle exec rake spec # unit + fake-runtime specs (no runtime needed)
|
|
257
257
|
MICROSANDBOX_INTEGRATION=1 bundle exec rspec spec/integration # boot real microVMs (KVM / Apple Silicon)
|
|
258
258
|
bundle exec standardrb # Ruby lint
|
|
259
259
|
cargo fmt --check --manifest-path ext/microsandbox/Cargo.toml
|
data/SECURITY.md
CHANGED
|
@@ -32,7 +32,7 @@ entry in some of them refers to the upstream project's own Ruby gem, not to
|
|
|
32
32
|
|
|
33
33
|
| Advisory | Severity | Affected `microsandbox-rb` | Status |
|
|
34
34
|
|---|---|---|---|
|
|
35
|
-
| [GHSA-mcw9-9rf6-p4rm](https://github.com/superradcompany/microsandbox/security/advisories/GHSA-mcw9-9rf6-p4rm) — relative bind-mount paths re-resolved against the working directory on restart | High | `<= 0.17.0` | Fixed in `0.17.1` for sandboxes created through this gem (paths are pinned at create time in the binding). Sandboxes created earlier with a relative path keep it: recreate them. The `msb` CLI this gem installs is still runtime `v0.6.18`, so a sandbox created with `msb` directly is not covered: use absolute paths there. |
|
|
35
|
+
| [GHSA-mcw9-9rf6-p4rm](https://github.com/superradcompany/microsandbox/security/advisories/GHSA-mcw9-9rf6-p4rm) — relative bind-mount paths re-resolved against the working directory on restart | High | `<= 0.17.0` | Fixed in `0.17.1` for sandboxes created through this gem (paths are pinned at create time in the binding). Sandboxes created earlier with a relative path keep it: recreate them. The `msb` CLI this gem installs is still runtime `v0.6.18`, so a sandbox created with `msb` directly is not covered: use absolute paths there. Tracked for this gem as [GHSA-qrhv-m6hf-9cmg](https://github.com/ya-luotao/microsandbox-rb/security/advisories/GHSA-qrhv-m6hf-9cmg). |
|
|
36
36
|
| [GHSA-cg9x-2vfm-3j58](https://github.com/superradcompany/microsandbox/security/advisories/GHSA-cg9x-2vfm-3j58) — NAT64 addresses can bypass destination-group network policy | High | all versions (runtime `< v0.7.5`) | **Not fixed in this gem yet**; needs the runtime fix. Only exploitable where the host network routes a NAT64 prefix. Mitigate by denying the prefix explicitly (see below) or blocking it at the host. |
|
|
37
37
|
| [GHSA-3mpp-52gc-935r](https://github.com/superradcompany/microsandbox/security/advisories/GHSA-3mpp-52gc-935r) — hostname allowlist bypass via mismatched HTTP `Host` / `:authority` | High | `<= 0.16.0` | Runtime fix included from `0.17.0`. If you rely on a hostname allowlist for HTTPS without TLS interception, also pass `strict: true` (off by default in this gem). |
|
|
38
38
|
| [GHSA-gj24-29j2-3x78](https://github.com/superradcompany/microsandbox/security/advisories/GHSA-gj24-29j2-3x78) — egress allowlist bypass on malformed HTTP/1 request lines | Medium | `<= 0.12.0` | Runtime fix included from `0.13.0`. |
|
data/docs/execution.md
CHANGED
|
@@ -76,7 +76,14 @@ Don't share one handle across threads.
|
|
|
76
76
|
The GVL is released during sandbox calls, so *other* Ruby threads keep
|
|
77
77
|
running. The *calling* thread blocks uninterruptibly until the call returns:
|
|
78
78
|
`Timeout::timeout`, `Thread#kill`, and Ctrl-C cannot interrupt a blocked native
|
|
79
|
-
call
|
|
79
|
+
call — they are delivered once it has returned. Its result is not lost when
|
|
80
|
+
that happens: an interrupted `exec_stream` handle, stream or SSH client is
|
|
81
|
+
closed when the unreferenced object is garbage-collected, and a
|
|
82
|
+
`Sandbox.create` interrupted by a `Timeout`/`Thread#raise` or a `Thread#kill`
|
|
83
|
+
stops the sandbox it booted (after a Ctrl-C it is torn down by garbage
|
|
84
|
+
collection instead; see
|
|
85
|
+
[lifecycle.md](lifecycle.md#timeouts-and-interrupts-around-a-create)). Bound
|
|
86
|
+
long-running work with a real deadline where one exists:
|
|
80
87
|
|
|
81
88
|
- `exec(timeout:)` / `shell(timeout:)` kill the guest command after N seconds.
|
|
82
89
|
- `AgentClient.connect_sandbox` / `connect_path` take a `timeout:` that bounds
|
data/docs/lifecycle.md
CHANGED
|
@@ -57,6 +57,39 @@ Microsandbox::Sandbox.remove("box") # remove a stopped sandbox
|
|
|
57
57
|
`ENTRYPOINT`/`CMD`. See [execution.md](execution.md#default-workload) for
|
|
58
58
|
`exec_default`.
|
|
59
59
|
|
|
60
|
+
### Timeouts and interrupts around a create
|
|
61
|
+
|
|
62
|
+
A create cannot be interrupted while it runs, so `Timeout.timeout(5) {
|
|
63
|
+
Sandbox.create(...) }` does not return after 5 seconds: `Timeout::Error` is
|
|
64
|
+
raised only once the create has finished. When that happens the sandbox is
|
|
65
|
+
**stopped before the exception reaches you** — the call returns only after the
|
|
66
|
+
create *and* that cleanup stop have completed, so it takes longer than the
|
|
67
|
+
create alone — and nothing is left running without an owner. The same holds
|
|
68
|
+
for `Thread#raise` and `Thread#kill`, and for `Sandbox.connect_or_create`,
|
|
69
|
+
`Sandbox.start` and `PullSession#sandbox`. Two details:
|
|
70
|
+
|
|
71
|
+
- The sandbox is stopped, not removed. Like any stopped sandbox it keeps its
|
|
72
|
+
name until you `Sandbox.remove` it (or pass `ephemeral: true`), so retrying
|
|
73
|
+
the create under the same name raises `SandboxAlreadyExistsError`.
|
|
74
|
+
- Only a sandbox the interrupted call owns is stopped. `connect_or_create`
|
|
75
|
+
that merely connected to a sandbox already running for someone else leaves
|
|
76
|
+
it running, exactly as its block form does.
|
|
77
|
+
|
|
78
|
+
The cleanup stop is best-effort: if it fails, the interrupt is still the
|
|
79
|
+
exception you get, and the sandbox is torn down when its now unreferenced
|
|
80
|
+
object is garbage-collected.
|
|
81
|
+
|
|
82
|
+
A Ctrl-C (`SIGINT`) is the one interrupt that cannot be held back until the
|
|
83
|
+
sandbox is in your hands: one that lands during the create is raised the
|
|
84
|
+
moment the create returns, before anything can stop the sandbox, which is then
|
|
85
|
+
torn down when the unreferenced object is garbage-collected instead of
|
|
86
|
+
immediately.
|
|
87
|
+
|
|
88
|
+
These methods hold queued interrupts back while they acquire the sandbox, so
|
|
89
|
+
an exception you deferred with `Thread.handle_interrupt(X => :on_blocking)` is
|
|
90
|
+
not raised by them: you get the sandbox, and the exception arrives at the
|
|
91
|
+
thread's next blocking operation.
|
|
92
|
+
|
|
60
93
|
## Convergent lifecycle
|
|
61
94
|
|
|
62
95
|
Idempotent operations that take a name to the state you want, whatever state
|
data/docs/releasing.md
CHANGED
|
@@ -7,54 +7,105 @@ or credential setup is needed.
|
|
|
7
7
|
|
|
8
8
|
## Each release
|
|
9
9
|
|
|
10
|
-
1. Bump the gem's **own** version
|
|
11
|
-
`[package] version` in `ext/microsandbox/Cargo.toml` (they must stay equal) —
|
|
12
|
-
on its independent semver track (see the
|
|
10
|
+
1. Bump the gem's **own** version on its independent semver track (see the
|
|
13
11
|
[Versioning](../README.md#versioning) section of the README); **don't** pick
|
|
14
|
-
the number to mirror the upstream tag.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`
|
|
18
|
-
`
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
the number to mirror the upstream tag. The version is written in three
|
|
13
|
+
files that must stay equal (`spec/unit/version_spec.rb` asserts it), and
|
|
14
|
+
recorded in the lockfile:
|
|
15
|
+
- `lib/microsandbox/version.rb` — `Microsandbox::VERSION`;
|
|
16
|
+
- `ext/microsandbox/Cargo.toml` — the `[package] version`;
|
|
17
|
+
- `binaries/lib/microsandbox/binaries.rb` — `Microsandbox::Binaries::VERSION`
|
|
18
|
+
(the companion gem ships in lockstep);
|
|
19
|
+
- `Cargo.lock` — committed and packed into the gem, and it records the
|
|
20
|
+
extension crate's version, so a version bump must commit the regenerated
|
|
21
|
+
lock (`bundle exec rake compile` rewrites it).
|
|
22
|
+
|
|
23
|
+
If the release also adopts a new upstream runtime, move the runtime tag
|
|
24
|
+
everywhere it is named:
|
|
25
|
+
- `ext/microsandbox/Cargo.toml` — the `tag = "vX.Y.Z"` on **all three**
|
|
26
|
+
upstream git deps (`microsandbox`, `microsandbox-network`,
|
|
27
|
+
`microsandbox-runtime`);
|
|
28
|
+
- `lib/microsandbox/version.rb` — `Microsandbox::RUNTIME_VERSION`;
|
|
29
|
+
- `binaries/lib/microsandbox/binaries.rb` —
|
|
30
|
+
`Microsandbox::Binaries::RUNTIME_VERSION`;
|
|
31
|
+
- `binaries/checksums/<tag>.sha256` — the release's `checksums.sha256`,
|
|
32
|
+
committed (vendoring fails closed without it);
|
|
33
|
+
- `Cargo.lock` — regenerated again, since it pins the git deps to the new
|
|
34
|
+
tag's commit.
|
|
35
|
+
|
|
36
|
+
Then add a row to the Versioning table in the README and update
|
|
37
|
+
`CHANGELOG.md`.
|
|
38
|
+
2. Push a `vX.Y.Z` tag. Once the [release preflight](#release-preflight)
|
|
39
|
+
passes, CI pushes the **source gem** to RubyGems via
|
|
40
|
+
`rubygems/configure-rubygems-credentials` (OIDC, `id-token: write`) — no
|
|
25
41
|
`RUBYGEMS_API_KEY` secret required — then creates the GitHub Release.
|
|
26
42
|
|
|
43
|
+
## Release preflight
|
|
44
|
+
|
|
45
|
+
`release.yml`'s `preflight` job runs `.github/scripts/release_preflight.sh
|
|
46
|
+
<tag>` and the `publish` job needs it, so nothing reaches RubyGems unless:
|
|
47
|
+
|
|
48
|
+
- the tag is `vX.Y.Z` and the three version constants **in the tagged commit**
|
|
49
|
+
— `Microsandbox::VERSION`, `[package] version` in
|
|
50
|
+
`ext/microsandbox/Cargo.toml`, `Microsandbox::Binaries::VERSION` — all equal
|
|
51
|
+
`X.Y.Z`;
|
|
52
|
+
- the tagged commit is on `main`;
|
|
53
|
+
- the `CI` workflow's push run for that exact commit concluded `success`.
|
|
54
|
+
|
|
55
|
+
Pushing `main` and the tag together is fine: the preflight **waits** for that
|
|
56
|
+
CI run to appear and finish (polling every 30 s, up to 45 min —
|
|
57
|
+
`PREFLIGHT_CI_WAIT_SECONDS` / `PREFLIGHT_CI_POLL_SECONDS`), so a release
|
|
58
|
+
publishes only after CI, including the real-microVM integration lanes, is
|
|
59
|
+
green. A failed or timed-out preflight leaves the tag unpublished. How to
|
|
60
|
+
recover depends on whether the tag has to move:
|
|
61
|
+
|
|
62
|
+
- **The tagged commit is right** (CI flaked, or was still red): re-run the
|
|
63
|
+
`CI` run for that commit until it is green, then re-run the failed `Release`
|
|
64
|
+
run.
|
|
65
|
+
- **The tag must point at a different commit** (wrong version constants, not
|
|
66
|
+
on `main`): delete the tag, fix the commit, and push the tag again — that
|
|
67
|
+
starts a new `Release` run. Never re-run the old run after retagging: a
|
|
68
|
+
re-run keeps the commit it was started for, so it checks the old commit
|
|
69
|
+
again and fails the same way.
|
|
70
|
+
|
|
71
|
+
Dry-run it against an existing tag — nothing is published on a manual
|
|
72
|
+
dispatch — or run the script locally from a full clone:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
gh workflow run release.yml --ref main -f preflight_ref=vX.Y.Z
|
|
76
|
+
GH_TOKEN=$(gh auth token) .github/scripts/release_preflight.sh vX.Y.Z
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
It is a workflow-level gate; branch protection and a protected environment for
|
|
80
|
+
the trusted publisher are separate repository settings.
|
|
81
|
+
|
|
82
|
+
CI's `lint` job also runs `cargo deny check advisories` against `Cargo.lock`;
|
|
83
|
+
the exceptions (advisories with no fixed release at the pinned runtime) are in
|
|
84
|
+
`deny.toml` at the repository root — re-check them on every runtime adoption.
|
|
85
|
+
|
|
27
86
|
## Precompiled per-platform gems
|
|
28
87
|
|
|
29
|
-
Precompiled *extension* gems are built
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
prove otherwise — so promotion is manual after validating the artifact on each
|
|
35
|
-
platform. A precompiled gem ships the compiled extension (with the guest
|
|
36
|
-
`agentd` baked in by *target* arch); the host-side `msb` + `libkrunfw` runtime
|
|
37
|
-
is **not** in it — that comes from the companion `microsandbox-rb-binaries`
|
|
38
|
-
gem, or, when that isn't installed, is fetched into `~/.microsandbox` on first
|
|
39
|
-
use by `Microsandbox.ensure_runtime!` (libkrunfw is `dlopen`'d by `msb` at
|
|
40
|
-
runtime, never linked into the gem). The real cross-compile work is linking the
|
|
41
|
-
*target* native libraries — `libcap-ng` on Linux (handled via Debian multiarch
|
|
42
|
-
in the workflow) and the Hypervisor + Security frameworks on macOS (via
|
|
43
|
-
osxcross; the one platform left to confirm). Until promoted, users install the
|
|
44
|
-
source gem (which compiles via `rb_sys`).
|
|
88
|
+
Precompiled *extension* gems are not built or published. Users install the
|
|
89
|
+
source gem, which compiles the extension via `rb_sys` at install time (so a
|
|
90
|
+
stable Rust toolchain is required on the installing host). The companion
|
|
91
|
+
`microsandbox-rb-binaries` gems below are unaffected: they carry the host
|
|
92
|
+
runtime, not the extension.
|
|
45
93
|
|
|
46
94
|
## Runtime binaries gems
|
|
47
95
|
|
|
48
96
|
`microsandbox-rb-binaries` (source in `binaries/`) is a *separate* artifact from
|
|
49
|
-
the
|
|
97
|
+
the SDK gem: it carries no Ruby extension, only the
|
|
50
98
|
upstream `msb` + `libkrunfw` for one platform, verified against the release's
|
|
51
99
|
published `checksums.sha256` when vendored. CI's `binaries` job vendors and
|
|
52
100
|
builds all three (`arm64-darwin`, `x86_64-linux-gnu`, `aarch64-linux-gnu`) on
|
|
53
101
|
every run and smoke-tests the host one; `release.yml`'s `binaries-gems` job
|
|
54
102
|
does the same on a tag and a separate `publish-binaries` job pushes them after
|
|
55
|
-
the SDK gem is live
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
the SDK gem is live. The SDK `publish` job needs `binaries-gems`, so a
|
|
104
|
+
companion **build** failure blocks the SDK publish as well. A companion
|
|
105
|
+
**push** failure is its own red job once the SDK gem is live: the release is
|
|
106
|
+
then incomplete until `publish-binaries` is re-run. They use their own RubyGems
|
|
107
|
+
trusted-publisher entry (same repo + workflow, gem name
|
|
108
|
+
`microsandbox-rb-binaries`). To build them by hand:
|
|
58
109
|
|
|
59
110
|
```bash
|
|
60
111
|
rake -C binaries vendor[arm64-darwin] # download + sha256-verify into binaries/vendor
|
data/docs/runtime.md
CHANGED
|
@@ -85,7 +85,9 @@ Microsandbox.set_default_backend(:cloud, url: "https://api.example.com", api_key
|
|
|
85
85
|
Microsandbox.set_default_backend(:cloud, profile: "prod")
|
|
86
86
|
|
|
87
87
|
# Scoped override (restored afterward, even on error):
|
|
88
|
-
Microsandbox.with_backend(:local)
|
|
88
|
+
Microsandbox.with_backend(:local) do
|
|
89
|
+
Microsandbox::Sandbox.create("box", image: "alpine") { |sb| puts sb.shell("uname -a").stdout }
|
|
90
|
+
end
|
|
89
91
|
```
|
|
90
92
|
|
|
91
93
|
Resolution order when no backend is set programmatically: `MSB_BACKEND`
|