microsandbox-rb 0.15.0 → 0.17.0

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: 241dfe5d94b8c98d3c790994698b004a591d66229d9965799fc5886b01e5a19d
4
- data.tar.gz: a134b4cbb4b58a60d72d6a7ace30ab6333da4057d0868c1a6e351396b7ff4d00
3
+ metadata.gz: 37f44ca952f2885719237b06ea10b5316c383fee0a2fdfa5c51c76fd295c1591
4
+ data.tar.gz: 81e55203c1aea73d0368d93d7c36c73015b9a5a97634b69a3f5d805a48c7446c
5
5
  SHA512:
6
- metadata.gz: c326c0481781ae1f3fc19994dfd8e4c004c4bdc8aa81b33e67386538f4fa9ff58cb84514955079c39daa09817c8eba65ac7d4ebe91ab34698ac5fce8cfa9c574
7
- data.tar.gz: 1d93018f5982d2767cd66e29c50a195a3d138d7f60f93a2a3e6020384f41ce843e3a0b85d5152a2d17995a724ed9cc6169e72c0bcfc506017e7ae84b3acbe73f
6
+ metadata.gz: 49876e1d24e9e64ab486932f6180c511ea5142a93f8b1c0edb67f04847b861310a4972046b08babe11679f88835883fa80b739fd2f8d5f7ff5d49316f6c26c8d
7
+ data.tar.gz: 3bf053fb8c2e63099c128655fce91335c3ce35c7459af3616acad7d3b773f32184107740c124df2e7f122053d807f74c2d9f6337f66d0c6ed9121c4ddadd2eb3
data/CHANGELOG.md CHANGED
@@ -6,6 +6,203 @@ All notable changes to this gem are documented here. The format is based on
6
6
  microsandbox runtime it embeds; each release notes the upstream runtime tag it
7
7
  wraps, and the README's Versioning section keeps the full gem→runtime map.
8
8
 
9
+ ## [0.17.0] - 2026-09-13
10
+
11
+ Adopts upstream runtime **`v0.6.16` → `v0.6.18`**, stepping through the
12
+ intermediate tag (`v0.6.17` verified and committed on its own).
13
+
14
+ ### Added
15
+
16
+ - **Strict hostname policy** — `strict:` (Boolean, default `false`) on
17
+ `Sandbox.create` / `connect_or_create` / `create_with_progress` (upstream
18
+ `v0.6.18`, from the security fork-merge below). In strict mode a flow that the
19
+ egress policy allows *only* through a hostname rule must be backed by an
20
+ inspectable request authority — the plain-HTTP `Host` header, or SNI /
21
+ `:authority` under TLS interception. Without that visibility (bypassed or
22
+ non-intercepted HTTPS) the runtime fails closed *before* dialing upstream
23
+ instead of trusting opaque hostname evidence. Mirrors the Python SDK's
24
+ `Network(strict=...)`. An explicit `false` is sent as-is (not dropped), like
25
+ `trust_host_cas:`. Create-only in Ruby, matching the Python SDK's `modify`
26
+ surface (which does not expose `strict`); the core itself does carry a
27
+ generated `strict` field on its network config patch (`ConfigPatch` derive),
28
+ so this is an SDK-parity choice, not a core limitation. Accepted by the cloud
29
+ create contract (`CloudNetworkSpec.strict`), so there is no `UnsupportedError`
30
+ path.
31
+
32
+ - **Outbound SOCKS proxies** — `proxy:` on `Sandbox.create` /
33
+ `connect_or_create` / `create_with_progress` (upstream #1234 SOCKS4/SOCKS5
34
+ and #1507 SOCKS5 UDP + credentials, `v0.6.17`). The runtime's host-side
35
+ network stack dials the proxy for the sandbox's egress — SOCKS4 for TCP,
36
+ SOCKS5 for TCP and non-DNS UDP — uniformly for TLS-intercepted and
37
+ bypassed/plain TCP, while the egress policy (`network:`) still decides which
38
+ destinations may be reached. The address is resolved from the **host**
39
+ (`127.0.0.1` is the host's loopback). Spelled like the Python SDK's
40
+ `proxy=`, as a top-level create option rather than inside `network:`:
41
+ - `Microsandbox::OutboundProxy.socks4(address, user_id: nil)` and
42
+ `Microsandbox::OutboundProxy.socks5(address)`, the latter chaining
43
+ `#credentials(username, password)` which returns a **new** proxy (the
44
+ objects are immutable, like Python's frozen dataclass).
45
+ - `Microsandbox::SecretSource.env("VAR")` — the only password source: the
46
+ host environment variable's *name* is what reaches the runtime, which reads
47
+ the value host-side. No password value ever travels through the binding or
48
+ appears in `inspect`/error output.
49
+ - The equivalent plain Hash is accepted too: `{ protocol: :socks5,
50
+ address: "IP:port", credentials: { username:, password: { env: "VAR" } } }`
51
+ or `{ protocol: :socks4, address:, user_id: }`.
52
+ - Validation mirrors the Python SDK's `OutboundProxy.__post_init__`
53
+ (`ArgumentError`): `user_id` only for SOCKS4, credentials only for SOCKS5,
54
+ username and password together or neither, a non-empty String address.
55
+ `IP:port` parsing is left to the core, which reports an unparseable address
56
+ (or an invalid SOCKS4 user ID) at create time, before any boot; the binding
57
+ surfaces that as `InvalidConfigError` — a malformed proxy is a configuration
58
+ mistake, not a policy one — while every other network-builder error keeps
59
+ its `NetworkPolicyError` class. The cloud backend rejects the option with
60
+ `UnsupportedError` (`sandbox.create`, hint naming `network.outbound_proxy`).
61
+
62
+ ### Changed
63
+
64
+ - **Plain-HTTP requests are now checked against the egress policy by their
65
+ `Host` / `:authority`, whenever the policy has any domain rule — in
66
+ non-strict mode too** (upstream `v0.6.18` hardening, `crates/network/lib/
67
+ tcp/proxy.rs`: `enforce_http_authority = network_policy.has_domain_rules()`).
68
+ Previously a plain-HTTP connection admitted by an IP/CIDR/group rule could
69
+ name any host in its `Host` header; now, if the policy carries `Domain` /
70
+ `DomainSuffix` rules (including `deny_domains:` / `deny_domain_suffixes:`,
71
+ which are domain rules), the request's authority is evaluated against the
72
+ **ordered** egress policy (rules first, then `default_egress`) together with
73
+ the real destination IP/port, and — for a request carrying valid authority
74
+ metadata — the request is denied only when that evaluation denies it.
75
+ Separately, a plain-HTTP request with a *missing*, *duplicate* or
76
+ *unparsable* `Host` / `:authority` is now rejected outright whenever domain
77
+ rules are present (the upstream validator requires exactly one well-formed
78
+ authority before any policy evaluation), so an HTTP/1.0 request without a
79
+ `Host` header no longer passes on such a policy. Existing IP/CIDR/group allows and a permissive
80
+ `default_egress` still count: a plain-HTTP request to an allowed IP that
81
+ sends an unrelated `Host` is still allowed unless a rule (or the default)
82
+ denies that hostname. Policies with no domain rules are unaffected, and so is
83
+ HTTPS without interception. What changes in practice: an IP-based request
84
+ whose `Host` names a host the ordered policy *denies* (a `deny_domains:`
85
+ entry, or a domain not matched by any allow under `default_egress: :deny`)
86
+ now fails even in non-strict mode — allow the hostname it actually sends.
87
+ `strict:` is not a substitute for a restrictive policy; it only tightens
88
+ hostname-rule allows that lack an inspectable authority.
89
+
90
+ ### Security
91
+
92
+ - Runtime `v0.6.18` carries the upstream security fork-merge (`439a88a5`, no
93
+ public advisory identifier at the time of writing): aligns HTTP `Host` policy
94
+ checks with the rest of the egress enforcement (the Changed note above) and
95
+ adds the fail-closed pre-connect strict hostname mode (`strict:` above). No
96
+ gem-side change was needed beyond exposing `strict:`.
97
+
98
+ ### Fixed
99
+
100
+ - Runtime `v0.6.17` restores the released database-migration order (upstream
101
+ #1517, fixing #1502): a local `~/.microsandbox` database last opened by an
102
+ upstream **`v0.6.15`** binary — typically the `msb` CLI, since this gem never
103
+ shipped `v0.6.15` on its own — failed to open under `v0.6.16` with "database
104
+ schema is newer than this msb binary". Any such database now migrates
105
+ forward cleanly on the next `require "microsandbox"` + first operation.
106
+
107
+ ## [0.16.0] - 2026-09-01
108
+
109
+ Adopts upstream runtime **`v0.6.14` → `v0.6.16`**, stepping through the
110
+ intermediate tag (`v0.6.15` verified and committed on its own).
111
+
112
+ ### Added
113
+
114
+ - Per-mount fallback ownership: a bind or named mount in `volumes:` accepts
115
+ `uid:`/`gid:`, pinning the guest owner presented for host files that carry no
116
+ per-file stat override (upstream #1451, `v0.6.15`). They travel on the wire as
117
+ the core's `override_uid`/`override_gid` and mirror the Python SDK's public
118
+ `uid:`/`gid:` spelling. Validation matches the Python SDK: the pair must be
119
+ given together, each must be a plain `Integer` in `0..4294967295` (strings,
120
+ Floats and Booleans are rejected rather than coerced — a truncated or parsed
121
+ owner ID is never what the caller meant), and they conflict with both
122
+ `stat_virtualization: :off` (no overlay to rewrite the owner in) and
123
+ tmpfs/disk mounts. The one Python rule with no Ruby counterpart is
124
+ "unsupported for disk-backed named volumes": a Ruby `{ named: "vol" }` spec
125
+ only references an existing volume by name, so the volume's kind is known
126
+ only to the core, which rejects that combination at create time.
127
+ - **Convergent lifecycle APIs** (upstream #1462, `v0.6.16`) — idempotent
128
+ operations that take a name to the state you want, whatever state it is in
129
+ now:
130
+ - `Sandbox.connect_or_create(name, **kwargs)` — connect to (and start) the
131
+ persisted sandbox with this name, or create it when none exists; concurrent
132
+ callers converge on the winning identity instead of one losing to a name
133
+ clash. Takes exactly `create`'s keyword options — the same normalization
134
+ path, not a second copy of it — and they apply only when a create actually
135
+ happens. The block form yields the sandbox and then stops it **only when the
136
+ call owns its lifecycle** — a deliberate divergence from `create`, whose
137
+ block form always stops: `connect_or_create` can hand back a sandbox this
138
+ process did not start, and tearing down someone else's long-lived service on
139
+ the way out of a block is never what the caller meant. So a sandbox created
140
+ here is stopped as usual; one merely *connected* to, or started with
141
+ `detached: true`, is left running. `replace:` / `replace_with_timeout:` are
142
+ accepted for kwargs parity but rejected by the core with
143
+ `InvalidConfigError`: replacing a sandbox is the opposite of converging
144
+ on it.
145
+ - `Sandbox#id` and `SandboxHandle#id` — the opaque, backend-assigned identity
146
+ of the *persisted* sandbox. Names are reusable labels; this changes when a
147
+ name is removed and recreated.
148
+ - `Sandbox#wait_for_status(status)` and `SandboxHandle#wait_for_status(status)`
149
+ — block until this exact sandbox reaches one of `:created`, `:starting`,
150
+ `:running`, `:draining`, `:paused`, `:stopped`, `:crashed`, returning a fresh
151
+ `SandboxHandle` (from both receivers, mirroring the official SDKs). There is
152
+ no built-in timeout and the wait is uninterruptible from Ruby (the native
153
+ call releases the GVL with no unblock function), so an unknown status name
154
+ raises `ArgumentError` up front rather than blocking forever — and only
155
+ states the active backend can reach are worth waiting for (`:created` and
156
+ `:paused` are cloud-side). Impose a deadline by running the call on its own
157
+ Thread.
158
+ - `Sandbox#restart(force:, timeout:, detached:)` and
159
+ `SandboxHandle#restart(...)` — stop and start this exact sandbox, returning a
160
+ new live `Sandbox`.
161
+ - `Sandbox#destroy(force:, timeout:)` and `SandboxHandle#destroy(...)` — stop
162
+ and remove this exact sandbox in one step.
163
+ - `SandboxHandle#connect_or_start(detached:)` — connect when the sandbox is
164
+ running, wait while it is starting, start it when it is created/stopped/
165
+ crashed.
166
+ - `Microsandbox::SandboxReplacedError` (code `"sandbox-replaced"`) — raised by
167
+ every one of the identity-checked operations above when the name now refers to
168
+ a different persisted sandbox, instead of quietly acting on the replacement.
169
+ Mirrors the Python SDK's `SandboxReplacedError`; wired into the native
170
+ error-code mapping like the other `sandbox-*` codes.
171
+
172
+ ### Fixed
173
+
174
+ - **The live `Sandbox#stop` no longer stops a same-name replacement.** The
175
+ binding re-fetched a `SandboxHandle` *by name* and stopped that — a pattern
176
+ left over from before the core's live `stop` was identity-scoped. As of
177
+ runtime v0.6.16 the core routes `stop` → `request_stop` →
178
+ `stop_identified(name, self.identity())`, so the fix is to route through the
179
+ live sandbox (`self.inner.stop()`) the way `kill`/`drain`/`wait`/
180
+ `stop_and_wait` already did, and the way both official bindings do. A sandbox
181
+ removed and recreated under the same name now raises `SandboxReplacedError`
182
+ (or `SandboxNotFoundError` when nothing holds the name) instead of terminating
183
+ whoever answers to it. This also hardens `Sandbox.connect_or_create`'s block
184
+ form, whose teardown calls `#stop`.
185
+ - **Operations that can boot a microVM now provision the runtime first.**
186
+ `SandboxHandle#connect_or_start`, `SandboxHandle#restart` and
187
+ `Sandbox#restart` were missing the `ensure_runtime!` call `Sandbox.create` and
188
+ `Sandbox.start` make, so on a machine with no runtime installed they failed in
189
+ the native layer instead of fetching it. (`Sandbox.connect_or_create` was
190
+ already covered — it shares `create`'s option builder.)
191
+
192
+ ### Changed
193
+
194
+ - Upstream runtime highlights carried without further Ruby surface:
195
+ - `v0.6.15` — read-only mounts no longer fail a write probe at mount time,
196
+ Windows DNS/NTFS handling, and the mount-ownership core work above.
197
+ - `v0.6.16` — log retrieval rerouted through the SDK backends (#1459; the
198
+ `logs`/`log_stream` bindings are unchanged and keep their behaviour — the
199
+ new `follow_logs`/`boot_error` core entry points are not bound, matching the
200
+ Python binding), sandbox config overlaid by field presence with
201
+ `LocalConfig` renamed to `GlobalConfig` (#1460; the gem touches neither —
202
+ the ext only calls `config::set_sdk_*_path` — and no Ruby config
203
+ round-trip or `modify` semantics changed), network-slot recycling and
204
+ migration, single-file mount isolation, empty `MSB_HOME` treated as unset.
205
+
9
206
  ## [0.15.0] - 2026-08-24
10
207
 
11
208
  Adopts upstream runtime **`v0.6.9` → `v0.6.14`**, stepping through every