microsandbox-rb 0.16.0 → 0.17.1
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 +234 -1
- data/Cargo.lock +236 -201
- data/DESIGN.md +7 -38
- data/README.md +178 -626
- data/SECURITY.md +65 -0
- data/docs/configuration.md +154 -0
- data/docs/errors.md +74 -0
- data/docs/execution.md +101 -0
- data/docs/filesystem.md +45 -0
- data/docs/images.md +65 -0
- data/docs/lifecycle.md +108 -0
- data/docs/observability.md +99 -0
- data/docs/releasing.md +66 -0
- data/docs/runtime.md +152 -0
- data/docs/surface.md +70 -0
- data/ext/microsandbox/Cargo.toml +4 -4
- data/ext/microsandbox/src/error.rs +7 -0
- data/ext/microsandbox/src/host_path.rs +108 -0
- data/ext/microsandbox/src/lib.rs +1 -0
- data/ext/microsandbox/src/sandbox.rs +143 -31
- data/ext/microsandbox/src/volume.rs +43 -12
- data/lib/microsandbox/fs.rb +4 -8
- data/lib/microsandbox/image.rb +2 -2
- data/lib/microsandbox/log_entry.rb +1 -1
- data/lib/microsandbox/metrics.rb +1 -1
- data/lib/microsandbox/outbound_proxy.rb +340 -0
- data/lib/microsandbox/sandbox.rb +73 -10
- data/lib/microsandbox/snapshot.rb +1 -1
- data/lib/microsandbox/ssh.rb +2 -10
- data/lib/microsandbox/streams.rb +8 -5
- data/lib/microsandbox/version.rb +2 -2
- data/lib/microsandbox/volume.rb +31 -14
- data/lib/microsandbox-rb.rb +7 -0
- data/lib/microsandbox.rb +89 -31
- data/sig/microsandbox.rbs +40 -1
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fbc8d09d8029bef1ec00d128bf554b037c937e51ebe8b9b8a6014043fce178e
|
|
4
|
+
data.tar.gz: 626313c0471102e108793f3880bcd1f1e30ba9f48470274bf53495a86cfadff7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49461a4a9ca44f5af82d62fe30086bd4b153d3885267274fce7de8328b071cb418055f202b3b2feea3f149d76572a2a78874596c707ac094d62f7cc5171269aa
|
|
7
|
+
data.tar.gz: 929b0a19b88eb839d371899191e6a0229eded4eb616100dbd8d316352eac3bf68dcfefa91fb04c4ce41ad540deaa41233c1533d2471222bbf4772d8708451be6
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,235 @@ 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
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
## [0.17.1] - 2026-10-12
|
|
12
|
+
|
|
13
|
+
Gem-only security and hardening release; the embedded runtime stays at
|
|
14
|
+
**`v0.6.18`**. Upgrading is recommended for everyone on `0.17.0` or earlier.
|
|
15
|
+
Two fixes tighten input handling and can reject input that used to be
|
|
16
|
+
accepted: `Volume.fs` paths containing a `..` segment, and
|
|
17
|
+
`Microsandbox.runtime_path = nil`.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- **Relative host paths are pinned when a sandbox is created**
|
|
22
|
+
([GHSA-mcw9-9rf6-p4rm](https://github.com/superradcompany/microsandbox/security/advisories/GHSA-mcw9-9rf6-p4rm),
|
|
23
|
+
high; fixed upstream in runtime `v0.7.6`, which this gem does not embed yet).
|
|
24
|
+
The embedded runtime stored a relative host path exactly as written and
|
|
25
|
+
resolved it against the working directory of whichever process later
|
|
26
|
+
started the sandbox, so restarting a sandbox from another directory mounted
|
|
27
|
+
a different host directory than the one originally intended. The native
|
|
28
|
+
layer now anchors every relative host path to the creating process's
|
|
29
|
+
directory before the configuration is persisted: bind and disk mount
|
|
30
|
+
sources in `volumes:`, a directory or disk-image `image:`, a `root_disk:`
|
|
31
|
+
image path, `Patch.copy_file` / `Patch.copy_dir` sources, the `tls:`
|
|
32
|
+
certificate paths, and a path-form `from_snapshot:`. This covers sandboxes
|
|
33
|
+
created through the gem; the `msb` CLI the gem installs is unchanged, so use
|
|
34
|
+
absolute paths with it. Paths are anchored
|
|
35
|
+
without following symlinks and without requiring the target to exist, so a
|
|
36
|
+
symlinked bind-mount root is still refused unless
|
|
37
|
+
`follow_root_symlinks: true`. Absolute paths, named volumes, and OCI
|
|
38
|
+
references are unchanged, and on a cloud backend paths are forwarded as
|
|
39
|
+
written (they belong to the remote host).
|
|
40
|
+
|
|
41
|
+
**Existing sandboxes are not rewritten.** A sandbox created by an earlier
|
|
42
|
+
gem version with a relative path keeps that saved path and its old
|
|
43
|
+
behaviour: recreate it, or only ever start it from its original directory.
|
|
44
|
+
Affects every earlier `microsandbox-rb` release; the upstream advisory does
|
|
45
|
+
not list this gem.
|
|
46
|
+
- **Errors no longer repeat values that can be credentials.** A URL-style
|
|
47
|
+
proxy address (`socks5://user:pass@host:port`) is refused by shape without
|
|
48
|
+
being echoed; an `env:` / `labels:` / `scripts:` value that is not a Hash
|
|
49
|
+
(or convertible to one, like `ENV`), a malformed patch, and a non-Hash
|
|
50
|
+
secret spec are reported by class name only.
|
|
51
|
+
- **Dependencies with published advisories updated in `Cargo.lock`**:
|
|
52
|
+
`russh` 0.63.1 → 0.63.3 (GHSA-35g8-35p8-c8fw, SSH server side),
|
|
53
|
+
`rustls` 0.23.41 → 0.23.45 (RUSTSEC-2026-0285), `h2` 0.4.15 → 0.4.16
|
|
54
|
+
(RUSTSEC-2026-0258), plus `anyhow` 1.0.104 and `event-listener` 5.4.2
|
|
55
|
+
(unsoundness fixes). Those pulled in `aws-lc-rs` 1.18.1, `aws-lc-sys`
|
|
56
|
+
0.45.0, `rustls-webpki` 0.103.15, and `num-bigint` 0.5.1 in place of
|
|
57
|
+
`internal-russh-num-bigint`. `rkyv` and `rsa` have no compatible fixed
|
|
58
|
+
release at the pinned runtime and are unchanged.
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- `SECURITY.md`: how to report a vulnerability privately, and a table mapping
|
|
63
|
+
upstream microsandbox advisories to the `microsandbox-rb` versions they
|
|
64
|
+
affect (upstream advisories do not list this gem). Shipped in the gem.
|
|
65
|
+
- `require "microsandbox-rb"` as an entry point named after the gem. Outside
|
|
66
|
+
Bundler, `require "microsandbox"` can resolve to the unrelated `microsandbox`
|
|
67
|
+
gem on RubyGems.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **Documentation examples that did not run.** The README quick start failed
|
|
72
|
+
on its second run (`SandboxAlreadyExistsError`; it now uses
|
|
73
|
+
`ephemeral: true`), and the README / configuration-guide `secrets:` example
|
|
74
|
+
used the `#modify` shape and raised `TypeError`. The observability and
|
|
75
|
+
errors guides named the wrong error class for the metrics startup window
|
|
76
|
+
(it is the base `Microsandbox::Error`, matched by message), and
|
|
77
|
+
`log_stream(follow: true)` was documented as ending when the sandbox stops
|
|
78
|
+
(it does not). The guides now also state that an inline secret `value:` is
|
|
79
|
+
stored in cleartext in the local catalog until the sandbox is removed, that
|
|
80
|
+
the block form stops but does not remove, that a runtime home belongs to one
|
|
81
|
+
runtime line (`MSB_HOME`), and list known issues at the pinned runtime
|
|
82
|
+
(including guest `fallocate` truncating bind-mounted files on macOS hosts).
|
|
83
|
+
- **A rejected option no longer triggers a runtime download.** The first-use
|
|
84
|
+
runtime check ran before most argument validation, so `Sandbox.create` with
|
|
85
|
+
a bad option downloaded and installed `msb` before raising. It now runs
|
|
86
|
+
after the options are validated, in `create`, `connect_or_create` and
|
|
87
|
+
`create_with_progress`.
|
|
88
|
+
- **First-use provisioning is serialized within a process.** Several threads
|
|
89
|
+
making their first `Sandbox.create` at once each ran the installer, which
|
|
90
|
+
replaces files in place. `Microsandbox.install`, `Microsandbox.setup` and
|
|
91
|
+
the first-use check now share one lock, and the check runs once.
|
|
92
|
+
- **`Microsandbox.runtime_path = nil` / `libkrunfw_path = nil`** (or `""`)
|
|
93
|
+
raise `ArgumentError` instead of writing an empty path into the set-once
|
|
94
|
+
slot for the rest of the process.
|
|
95
|
+
- **Companion gem detection no longer depends on the host project's name.**
|
|
96
|
+
When the host project is itself a gem with its bundle installed inside the
|
|
97
|
+
project directory, `microsandbox-rb-binaries` could be attributed to the
|
|
98
|
+
host project and ignored, depending on gem load order. The owner is now
|
|
99
|
+
the gem with the innermost matching root.
|
|
100
|
+
- **`patches:` with `from_snapshot:`** raises `ArgumentError` up front. The
|
|
101
|
+
runtime rejects the pair only after claiming the sandbox name on disk,
|
|
102
|
+
which left an empty directory holding the name.
|
|
103
|
+
- **Block forms keep the caller's exception.** `fs.write_stream { }`,
|
|
104
|
+
`ssh.open_client { }` and `client.sftp { }` closed in a bare `ensure`, so a
|
|
105
|
+
failing close replaced whatever the block had raised. The block's exception
|
|
106
|
+
now wins; a close failure is still raised when the block itself succeeded.
|
|
107
|
+
- **`PullSession#sandbox`** re-raises the original create failure when called
|
|
108
|
+
again, instead of an unrelated "already taken" error.
|
|
109
|
+
- **Timestamps are exact to the millisecond.** `Time.at(ms / 1000.0)` went
|
|
110
|
+
through a Float and reported one millisecond low for about half of all
|
|
111
|
+
values (`FsEntry#modified`, `FsMetadata#modified` / `#created`,
|
|
112
|
+
`LogEntry#timestamp`, `Metrics#timestamp`, and every `created_at` /
|
|
113
|
+
`updated_at` / `last_used_at` / `observed_at` reader).
|
|
114
|
+
- **`secrets:` given as a `{ "NAME" => { ... } }` Hash** (the `#modify`
|
|
115
|
+
shape) raises a descriptive `ArgumentError` instead of `TypeError`; a single
|
|
116
|
+
bare spec Hash is accepted as a list of one, as is a single patch Hash for
|
|
117
|
+
`patches:`.
|
|
118
|
+
|
|
119
|
+
### Changed
|
|
120
|
+
|
|
121
|
+
- **`Volume.fs` paths may not contain a `..` segment.** Every `VolumeFs` operation
|
|
122
|
+
(`read`, `read_text`, `write`, `list`, `mkdir`, `remove_file`, `remove_dir`,
|
|
123
|
+
`exists?`, `copy`, `rename`, `stat`) now refuses a path that contains a `..`
|
|
124
|
+
segment with `FilesystemError`, in the native layer as well as the Ruby
|
|
125
|
+
wrapper. Volume paths are relative to the volume root and never need one.
|
|
126
|
+
A path such as `"a/../b"` that used to be accepted must now be written as
|
|
127
|
+
`"b"`. Names that merely contain dots (`"..hidden"`, `"a..b"`) are fine.
|
|
128
|
+
- **README restructured** along the lines of a conventional Ruby SDK README:
|
|
129
|
+
badges, a Highlights list, a short install + quick start, a Documentation
|
|
130
|
+
table, and a sourced comparison with the official `microsandbox` gem. The
|
|
131
|
+
detailed guides moved into `docs/` (`lifecycle`, `configuration`,
|
|
132
|
+
`execution`, `filesystem`, `observability`, `images`, `runtime`, `errors`,
|
|
133
|
+
`surface`, `releasing`), which the gem now ships (`spec.files`), so the
|
|
134
|
+
links resolve on rubygems.org and rubydoc.info. `docs/surface.md` replaces
|
|
135
|
+
the duplicated implemented-surface prose in README and `DESIGN.md` as the
|
|
136
|
+
single scope statement. `docs/errors.md` lists every error class and marks
|
|
137
|
+
the four the native layer does not currently raise (`PathNotFoundError`,
|
|
138
|
+
`ImagePullFailedError`, `SecretViolationError`, `TlsError`) as reserved.
|
|
139
|
+
|
|
140
|
+
## [0.17.0] - 2026-09-13
|
|
141
|
+
|
|
142
|
+
Adopts upstream runtime **`v0.6.16` → `v0.6.18`**, stepping through the
|
|
143
|
+
intermediate tag (`v0.6.17` verified and committed on its own).
|
|
144
|
+
|
|
145
|
+
### Added
|
|
146
|
+
|
|
147
|
+
- **Strict hostname policy** — `strict:` (Boolean, default `false`) on
|
|
148
|
+
`Sandbox.create` / `connect_or_create` / `create_with_progress` (upstream
|
|
149
|
+
`v0.6.18`, from the security fork-merge below). In strict mode a flow that the
|
|
150
|
+
egress policy allows *only* through a hostname rule must be backed by an
|
|
151
|
+
inspectable request authority — the plain-HTTP `Host` header, or SNI /
|
|
152
|
+
`:authority` under TLS interception. Without that visibility (bypassed or
|
|
153
|
+
non-intercepted HTTPS) the runtime fails closed *before* dialing upstream
|
|
154
|
+
instead of trusting opaque hostname evidence. Mirrors the Python SDK's
|
|
155
|
+
`Network(strict=...)`. An explicit `false` is sent as-is (not dropped), like
|
|
156
|
+
`trust_host_cas:`. Create-only in Ruby, matching the Python SDK's `modify`
|
|
157
|
+
surface (which does not expose `strict`); the core itself does carry a
|
|
158
|
+
generated `strict` field on its network config patch (`ConfigPatch` derive),
|
|
159
|
+
so this is an SDK-parity choice, not a core limitation. Accepted by the cloud
|
|
160
|
+
create contract (`CloudNetworkSpec.strict`), so there is no `UnsupportedError`
|
|
161
|
+
path.
|
|
162
|
+
|
|
163
|
+
- **Outbound SOCKS proxies** — `proxy:` on `Sandbox.create` /
|
|
164
|
+
`connect_or_create` / `create_with_progress` (upstream #1234 SOCKS4/SOCKS5
|
|
165
|
+
and #1507 SOCKS5 UDP + credentials, `v0.6.17`). The runtime's host-side
|
|
166
|
+
network stack dials the proxy for the sandbox's egress — SOCKS4 for TCP,
|
|
167
|
+
SOCKS5 for TCP and non-DNS UDP — uniformly for TLS-intercepted and
|
|
168
|
+
bypassed/plain TCP, while the egress policy (`network:`) still decides which
|
|
169
|
+
destinations may be reached. The address is resolved from the **host**
|
|
170
|
+
(`127.0.0.1` is the host's loopback). Spelled like the Python SDK's
|
|
171
|
+
`proxy=`, as a top-level create option rather than inside `network:`:
|
|
172
|
+
- `Microsandbox::OutboundProxy.socks4(address, user_id: nil)` and
|
|
173
|
+
`Microsandbox::OutboundProxy.socks5(address)`, the latter chaining
|
|
174
|
+
`#credentials(username, password)` which returns a **new** proxy (the
|
|
175
|
+
objects are immutable, like Python's frozen dataclass).
|
|
176
|
+
- `Microsandbox::SecretSource.env("VAR")` — the only password source: the
|
|
177
|
+
host environment variable's *name* is what reaches the runtime, which reads
|
|
178
|
+
the value host-side. No password value ever travels through the binding or
|
|
179
|
+
appears in `inspect`/error output.
|
|
180
|
+
- The equivalent plain Hash is accepted too: `{ protocol: :socks5,
|
|
181
|
+
address: "IP:port", credentials: { username:, password: { env: "VAR" } } }`
|
|
182
|
+
or `{ protocol: :socks4, address:, user_id: }`.
|
|
183
|
+
- Validation mirrors the Python SDK's `OutboundProxy.__post_init__`
|
|
184
|
+
(`ArgumentError`): `user_id` only for SOCKS4, credentials only for SOCKS5,
|
|
185
|
+
username and password together or neither, a non-empty String address.
|
|
186
|
+
`IP:port` parsing is left to the core, which reports an unparseable address
|
|
187
|
+
(or an invalid SOCKS4 user ID) at create time, before any boot; the binding
|
|
188
|
+
surfaces that as `InvalidConfigError` — a malformed proxy is a configuration
|
|
189
|
+
mistake, not a policy one — while every other network-builder error keeps
|
|
190
|
+
its `NetworkPolicyError` class. The cloud backend rejects the option with
|
|
191
|
+
`UnsupportedError` (`sandbox.create`, hint naming `network.outbound_proxy`).
|
|
192
|
+
|
|
193
|
+
### Changed
|
|
194
|
+
|
|
195
|
+
- **Plain-HTTP requests are now checked against the egress policy by their
|
|
196
|
+
`Host` / `:authority`, whenever the policy has any domain rule — in
|
|
197
|
+
non-strict mode too** (upstream `v0.6.18` hardening, `crates/network/lib/
|
|
198
|
+
tcp/proxy.rs`: `enforce_http_authority = network_policy.has_domain_rules()`).
|
|
199
|
+
Previously a plain-HTTP connection admitted by an IP/CIDR/group rule could
|
|
200
|
+
name any host in its `Host` header; now, if the policy carries `Domain` /
|
|
201
|
+
`DomainSuffix` rules (including `deny_domains:` / `deny_domain_suffixes:`,
|
|
202
|
+
which are domain rules), the request's authority is evaluated against the
|
|
203
|
+
**ordered** egress policy (rules first, then `default_egress`) together with
|
|
204
|
+
the real destination IP/port, and — for a request carrying valid authority
|
|
205
|
+
metadata — the request is denied only when that evaluation denies it.
|
|
206
|
+
Separately, a plain-HTTP request with a *missing*, *duplicate* or
|
|
207
|
+
*unparsable* `Host` / `:authority` is now rejected outright whenever domain
|
|
208
|
+
rules are present (the upstream validator requires exactly one well-formed
|
|
209
|
+
authority before any policy evaluation), so an HTTP/1.0 request without a
|
|
210
|
+
`Host` header no longer passes on such a policy. Existing IP/CIDR/group allows and a permissive
|
|
211
|
+
`default_egress` still count: a plain-HTTP request to an allowed IP that
|
|
212
|
+
sends an unrelated `Host` is still allowed unless a rule (or the default)
|
|
213
|
+
denies that hostname. Policies with no domain rules are unaffected, and so is
|
|
214
|
+
HTTPS without interception. What changes in practice: an IP-based request
|
|
215
|
+
whose `Host` names a host the ordered policy *denies* (a `deny_domains:`
|
|
216
|
+
entry, or a domain not matched by any allow under `default_egress: :deny`)
|
|
217
|
+
now fails even in non-strict mode — allow the hostname it actually sends.
|
|
218
|
+
`strict:` is not a substitute for a restrictive policy; it only tightens
|
|
219
|
+
hostname-rule allows that lack an inspectable authority.
|
|
220
|
+
|
|
221
|
+
### Security
|
|
222
|
+
|
|
223
|
+
- Runtime `v0.6.18` carries the upstream security fork-merge (`439a88a5`, no
|
|
224
|
+
public advisory identifier at the time of writing): aligns HTTP `Host` policy
|
|
225
|
+
checks with the rest of the egress enforcement (the Changed note above) and
|
|
226
|
+
adds the fail-closed pre-connect strict hostname mode (`strict:` above). No
|
|
227
|
+
gem-side change was needed beyond exposing `strict:`.
|
|
228
|
+
|
|
229
|
+
### Fixed
|
|
230
|
+
|
|
231
|
+
- Runtime `v0.6.17` restores the released database-migration order (upstream
|
|
232
|
+
#1517, fixing #1502): a local `~/.microsandbox` database last opened by an
|
|
233
|
+
upstream **`v0.6.15`** binary — typically the `msb` CLI, since this gem never
|
|
234
|
+
shipped `v0.6.15` on its own — failed to open under `v0.6.16` with "database
|
|
235
|
+
schema is newer than this msb binary". Any such database now migrates
|
|
236
|
+
forward cleanly on the next `require "microsandbox"` + first operation.
|
|
237
|
+
|
|
9
238
|
## [0.16.0] - 2026-09-01
|
|
10
239
|
|
|
11
240
|
Adopts upstream runtime **`v0.6.14` → `v0.6.16`**, stepping through the
|
|
@@ -1162,7 +1391,11 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
|
|
|
1162
1391
|
core crate has Apple-native deps). Until precompiled gems are published,
|
|
1163
1392
|
installing from source requires a Rust toolchain (stable >= 1.91).
|
|
1164
1393
|
|
|
1165
|
-
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.
|
|
1394
|
+
[Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.1...HEAD
|
|
1395
|
+
[0.17.1]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.17.0...v0.17.1
|
|
1396
|
+
[0.17.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.16.0...v0.17.0
|
|
1397
|
+
[0.16.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.15.0...v0.16.0
|
|
1398
|
+
[0.15.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.14.0...v0.15.0
|
|
1166
1399
|
[0.14.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.13.0...v0.14.0
|
|
1167
1400
|
[0.9.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.8.2...v0.9.0
|
|
1168
1401
|
[0.8.2]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.8.1...v0.8.2
|