microsandbox-rb 0.6.0 → 0.8.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: abfe8d0fedb192bc5efa95a741ae971475a2a5cc441879c092108bc8bdf9eed5
4
- data.tar.gz: ffeb9bda17817cc1c58bfe5cc9c7d58543cde155eead9d2e5eb3844c8a48b4dd
3
+ metadata.gz: 06dc745a315d5d79f21e702032d45a984dbf53110e3375bb560de9cb008866d6
4
+ data.tar.gz: 9168ece64600356ce88ed3c3ed42cc88f4354f3619abfdc32fabd578745cf6c4
5
5
  SHA512:
6
- metadata.gz: 73379d82644123c717cd7db9391cab9bf72c16b5cf398ea104fb51d2b608353ed9cd6783db55e1490d941ae67728f1c69fe0bd2dcec37c7ad541c56996afea9f
7
- data.tar.gz: b20914b13315697e89ec8a72e7620592a9c00d1e8fa908117ef24d64aea9755b431a2298b85d58ed00ca664065f0b626510c7f11fbb919860d3f839c93268a28
6
+ metadata.gz: d8fed4d0957be541dd5de600dd24386ff7b62d9933e45e244a49200185989cb8427279c80234038a4355aee296fdc96ec1cd5738e4bcf3fae48b27388f7dcad5
7
+ data.tar.gz: 9310a3b2d3a0322e701bc85cbfe6b820576b4254c3d49099bbfcab07690421c1e2d9705d0106e1d2931fb3516a0d462401758d5393552810427a6891ed6fc79e
data/CHANGELOG.md CHANGED
@@ -8,6 +8,138 @@ wraps, and the README's Versioning section keeps the full gem→runtime map.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.8.0] - 2026-06-25
12
+
13
+ Adopts upstream runtime **`v0.5.10`** (up from the `v0.5.8` that `0.7.0` shipped).
14
+ Runtime-only bump — no public Ruby API change.
15
+
16
+ ### Runtime
17
+
18
+ - **Adopted upstream `v0.5.10`** — the `microsandbox`/`microsandbox-network` git
19
+ deps and `Microsandbox::RUNTIME_VERSION` now pin `v0.5.10`. This is the runtime
20
+ bump originally attempted against `v0.5.9` during the `0.7.0` cycle and reverted:
21
+ upstream's `v0.5.9` git tag predated its own crate-version bump, so the prebuilt
22
+ runtime-provisioning path (`PREBUILT_VERSION = env!("CARGO_PKG_VERSION")`)
23
+ resolved to `0.5.8` and downloaded a `msb` that rejected the new `--config-fd`
24
+ flag the SDK unconditionally passes — every `Sandbox.create` died at boot.
25
+ Upstream chose not to re-tag (most package registries forbid republishing a tag)
26
+ and instead cut a clean **`v0.5.10`** whose tag carries the matching crate
27
+ version `0.5.10` (upstream
28
+ [#1029](https://github.com/superradcompany/microsandbox/issues/1029)). The bump
29
+ carries the following upstream changes:
30
+ - **Heartbeat no longer reclaims busy sandboxes** (upstream #1011). The host
31
+ watchdog is now idle-detection only — a healthy sandbox with an active (or
32
+ briefly starved) `exec` session is never killed for a stale heartbeat, the
33
+ way it could be before.
34
+ - **Launch config moved off the process argv** (upstream #1006). Bulky and
35
+ secret-bearing config (the network blob, env) is handed to the sandbox over
36
+ an inherited, unlinked-tempfile fd instead of `--`-flags, so it no longer
37
+ leaks into `ps` / `/proc/<pid>/cmdline`.
38
+
39
+ ### Changed
40
+
41
+ - **Directory bind mounts now carry a default 4 GiB guest-write quota**
42
+ (upstream #1020). Any `volumes:` entry that binds a host directory (e.g.
43
+ `volumes: { "/out" => "/host/out" }`) is given a `DEFAULT_BIND_QUOTA_MIB`
44
+ (4096 MiB) guest-write budget by the v0.5.10 runtime when no explicit quota is
45
+ set, so a sandbox can no longer fill the host disk through a bind mount. This
46
+ is a **behavior change**: a workload that wrote more than 4 GiB to a bind mount
47
+ under the `v0.5.8` runtime (`0.7.0`) will now fail with `ENOSPC`. The gem does
48
+ not yet expose a per-bind quota override (named-volume `Volume.create` accepts
49
+ `quota_mib:`, but the inline bind-mount path does not) — that escape hatch is a
50
+ tracked follow-up. Until then, route large-write mounts through a named volume
51
+ with an explicit `quota_mib:`.
52
+
53
+ ## [0.7.0] - 2026-06-23
54
+
55
+ A large parity release closing the binding gaps an audit against the upstream
56
+ Python/Node SDKs (at the wrapped `v0.5.8` runtime) surfaced. The runtime tag is
57
+ unchanged. Two genuine bug fixes; the rest is newly-exposed surface plus a few
58
+ behavior corrections (see **Changed**).
59
+
60
+ ### Fixed
61
+
62
+ - **Lossy UTF-8 decoding.** `LogEntry#text`, `ExecOutput#stdout`/`#stderr`,
63
+ `ExecEvent#text`, `SshOutput#stdout`/`#stderr`, and `SftpClient#read_text` now
64
+ scrub invalid byte sequences (replacing them with U+FFFD) so they always
65
+ return a *valid* UTF-8 String — matching the Python/Node SDKs. Previously they
66
+ re-tagged raw bytes as UTF-8 without transcoding, so captured output
67
+ containing invalid UTF-8 produced strings that raised downstream (regex,
68
+ concatenation, `JSON.generate`). Raw bytes remain available via `#data` /
69
+ `#stdout_bytes` / `#stderr_bytes`.
70
+ - **`runtime_path=` spec** no longer pollutes the process-wide set-once
71
+ `msb`-path `OnceLock` (it now stubs the native setter), removing an
72
+ order-dependent failure in combined unit+integration runs.
73
+
74
+ ### Added
75
+
76
+ - **Streaming image-pull progress** — `Sandbox.create_with_progress` returns a
77
+ `PullSession` (an `Enumerable` of progress-event Hashes) with `#sandbox` for
78
+ the booted sandbox.
79
+ - **Host-side volume filesystem** — `Volume.fs(name)` / `VolumeInfo#fs` return a
80
+ `VolumeFs` (read/read_text/write/list/mkdir/remove_file/remove_dir/exists?/
81
+ copy/rename/stat) that reads and writes a named volume without a running
82
+ sandbox.
83
+ - **Streaming guest filesystem** — `FS#read_stream` / `FS#write_stream`
84
+ (`FsReadStream`/`FsWriteSink`) for files too large to buffer in memory.
85
+ - **Full secrets surface** — `secrets:` entries accept `hosts:` / `host_patterns:`
86
+ (wildcards) allow-lists, `placeholder:`, `require_tls:`, injection toggles
87
+ (`inject_headers:`/`inject_basic_auth:`/`inject_query:`/`inject_body:`), and
88
+ per-secret `on_violation:`; plus a sandbox-level `on_secret_violation:`. The
89
+ block-variant actions accept both the underscore form (`block_and_log`) and the
90
+ upstream kebab-case wire spelling (`block-and-log`) used by the CLI / Go SDK /
91
+ config files; the bare `"passthrough"` string (passthrough-all-hosts, as in the
92
+ Python/Node SDKs) is also accepted, so a policy copied from another SDK ports
93
+ over unchanged.
94
+ - **Network configuration** — `Sandbox.create` now accepts `dns:` (nameservers/
95
+ rebind_protection/query_timeout_ms), `tls:` (interception tuning incl. bypass
96
+ patterns, intercepted ports, block_quic, and CA cert/key paths), `ipv4_pool:`/
97
+ `ipv6_pool:`, `max_connections:`, and `trust_host_cas:`.
98
+ - **Create options** — `init:`/`init_with` (hand guest PID 1 to an init system),
99
+ `ephemeral:` (auto-remove state on terminal), and disk-image `fstype:`.
100
+ `fstype:` is rejected up front unless `image:` is a disk-image path (a local
101
+ path ending in `.raw`/`.qcow2`/`.vmdk`); pairing it with an OCI reference no
102
+ longer routes the ref through the disk-image builder and fails at boot.
103
+ - **Full mount options** — `volumes:` now supports `{ tmpfs: }`, `{ disk:,
104
+ format:, fstype: }`, and per-mount `stat_virtualization:`/`host_permissions:`
105
+ alongside the existing bind/named + ro/noexec/nosuid/nodev flags. The pre-0.7.0
106
+ `options: %w[ro noexec]` array form is still honored (translated onto the
107
+ boolean flags); an unrecognized token now raises rather than being silently
108
+ dropped, so a requested read-only/noexec mount can't quietly become writable.
109
+ - **Snapshots** — `Snapshot.open`/`list_dir`/`reindex`, `SnapshotInfo#open`/
110
+ `#remove`, and `SandboxHandle#snapshot`/`#snapshot_to`. `SnapshotInfo` now
111
+ carries the full manifest (`image_manifest_digest`, `fstype`,
112
+ `source_sandbox`, `labels`) on the artifact-opening paths.
113
+ - **`SandboxHandle#config` / `#config_json`** — read the stored sandbox config.
114
+ - **Metrics** — `upper_used_bytes`, `upper_free_bytes`,
115
+ `upper_host_allocated_bytes` (OCI writable-upper-layer accounting).
116
+ - **`ImageDetail#config["labels"]`** — OCI config labels.
117
+ - **`Microsandbox.setup`** — customizable runtime install (`base_dir:`,
118
+ `version:`, `force:`, `skip_verify:`); `force:` repairs a corrupt install.
119
+
120
+ ### Changed
121
+
122
+ - **`exec`/`shell` stdin** is now a closed set: `nil`/`:null` = no stdin,
123
+ `:pipe` = streaming pipe (streaming variants only), a String = bytes. An
124
+ unrecognized Symbol now raises `ArgumentError` instead of being fed as its
125
+ characters (so a mistaken `stdin: :null` no longer sends the literal `"null"`).
126
+ - **Write methods reject non-Strings.** `FS#write`, `SftpClient#write`,
127
+ `ExecStdin#write`, `VolumeFs#write`, and `FsWriteSink#write` now raise
128
+ `TypeError` for non-String data instead of silently writing its `to_s` form.
129
+ - **Agent connect timeout.** `AgentClient.connect_sandbox`/`connect_path`
130
+ `timeout:` now treats `0` as an immediate deadline and raises on a
131
+ negative/non-finite value, instead of silently falling back to the default.
132
+ - **Secrets shorthand** still accepts `{ env:, value:, host: }`; the validation
133
+ message changed and a host allow-list is now required.
134
+
135
+ ### Docs
136
+
137
+ - README/DESIGN implemented-surface corrected to match the binding (and to list
138
+ the few secondary knobs still not exposed); assorted YARD fixes
139
+ (`runtime_path=` set-once note, `VolumeInfo#kind` `:dir`, `create`'s
140
+ `volumes:`/`from_snapshot:` params, `log_stream` `'all'` source); CHANGELOG
141
+ compare links added for 0.5.9–0.5.12.
142
+
11
143
  ## [0.6.0] - 2026-06-23
12
144
 
13
145
  This release puts the gem on its **own semantic version**, decoupled from the
@@ -291,7 +423,12 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
291
423
  core crate has Apple-native deps). Until precompiled gems are published,
292
424
  installing from source requires a Rust toolchain (stable >= 1.91).
293
425
 
294
- [Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.6.0...HEAD
426
+ [Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.7.0...HEAD
427
+ [0.7.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.6.0...v0.7.0
295
428
  [0.6.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.12...v0.6.0
429
+ [0.5.12]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.11...v0.5.12
430
+ [0.5.11]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.10...v0.5.11
431
+ [0.5.10]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.9...v0.5.10
432
+ [0.5.9]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.8...v0.5.9
296
433
  [0.5.8]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.7...v0.5.8
297
434
  [0.5.7]: https://github.com/superradcompany/microsandbox/releases/tag/v0.5.7
data/Cargo.lock CHANGED
@@ -3003,8 +3003,8 @@ dependencies = [
3003
3003
 
3004
3004
  [[package]]
3005
3005
  name = "microsandbox"
3006
- version = "0.5.8"
3007
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3006
+ version = "0.5.10"
3007
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3008
3008
  dependencies = [
3009
3009
  "anyhow",
3010
3010
  "astral-tokio-tar",
@@ -3056,8 +3056,8 @@ dependencies = [
3056
3056
 
3057
3057
  [[package]]
3058
3058
  name = "microsandbox-agent-client"
3059
- version = "0.5.8"
3060
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3059
+ version = "0.5.10"
3060
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3061
3061
  dependencies = [
3062
3062
  "ciborium",
3063
3063
  "microsandbox-protocol",
@@ -3069,8 +3069,8 @@ dependencies = [
3069
3069
 
3070
3070
  [[package]]
3071
3071
  name = "microsandbox-db"
3072
- version = "0.5.8"
3073
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3072
+ version = "0.5.10"
3073
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3074
3074
  dependencies = [
3075
3075
  "async-trait",
3076
3076
  "sea-orm",
@@ -3081,8 +3081,8 @@ dependencies = [
3081
3081
 
3082
3082
  [[package]]
3083
3083
  name = "microsandbox-filesystem"
3084
- version = "0.5.8"
3085
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3084
+ version = "0.5.10"
3085
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3086
3086
  dependencies = [
3087
3087
  "libc",
3088
3088
  "microsandbox-utils",
@@ -3094,8 +3094,8 @@ dependencies = [
3094
3094
 
3095
3095
  [[package]]
3096
3096
  name = "microsandbox-image"
3097
- version = "0.5.8"
3098
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3097
+ version = "0.5.10"
3098
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3099
3099
  dependencies = [
3100
3100
  "astral-tokio-tar",
3101
3101
  "async-compression",
@@ -3120,8 +3120,8 @@ dependencies = [
3120
3120
 
3121
3121
  [[package]]
3122
3122
  name = "microsandbox-metrics"
3123
- version = "0.5.8"
3124
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3123
+ version = "0.5.10"
3124
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3125
3125
  dependencies = [
3126
3126
  "chrono",
3127
3127
  "libc",
@@ -3131,8 +3131,8 @@ dependencies = [
3131
3131
 
3132
3132
  [[package]]
3133
3133
  name = "microsandbox-migration"
3134
- version = "0.5.8"
3135
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3134
+ version = "0.5.10"
3135
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3136
3136
  dependencies = [
3137
3137
  "sea-orm-migration",
3138
3138
  "serde_json",
@@ -3140,8 +3140,8 @@ dependencies = [
3140
3140
 
3141
3141
  [[package]]
3142
3142
  name = "microsandbox-network"
3143
- version = "0.5.8"
3144
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3143
+ version = "0.5.10"
3144
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3145
3145
  dependencies = [
3146
3146
  "base64",
3147
3147
  "bytes",
@@ -3180,8 +3180,8 @@ dependencies = [
3180
3180
 
3181
3181
  [[package]]
3182
3182
  name = "microsandbox-protocol"
3183
- version = "0.5.8"
3184
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3183
+ version = "0.5.10"
3184
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3185
3185
  dependencies = [
3186
3186
  "chrono",
3187
3187
  "ciborium",
@@ -3195,8 +3195,8 @@ dependencies = [
3195
3195
 
3196
3196
  [[package]]
3197
3197
  name = "microsandbox-runtime"
3198
- version = "0.5.8"
3199
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3198
+ version = "0.5.10"
3199
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3200
3200
  dependencies = [
3201
3201
  "bytes",
3202
3202
  "chrono",
@@ -3225,8 +3225,8 @@ dependencies = [
3225
3225
 
3226
3226
  [[package]]
3227
3227
  name = "microsandbox-types"
3228
- version = "0.5.8"
3229
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3228
+ version = "0.5.10"
3229
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3230
3230
  dependencies = [
3231
3231
  "chrono",
3232
3232
  "serde",
@@ -3237,8 +3237,8 @@ dependencies = [
3237
3237
 
3238
3238
  [[package]]
3239
3239
  name = "microsandbox-utils"
3240
- version = "0.5.8"
3241
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.8#47d0cdf1ef3b4b41885eb3efd67b759fd5540e9e"
3240
+ version = "0.5.10"
3241
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3242
3242
  dependencies = [
3243
3243
  "dirs",
3244
3244
  "libc",
@@ -3249,7 +3249,7 @@ dependencies = [
3249
3249
 
3250
3250
  [[package]]
3251
3251
  name = "microsandbox_rb"
3252
- version = "0.6.0"
3252
+ version = "0.8.0"
3253
3253
  dependencies = [
3254
3254
  "chrono",
3255
3255
  "futures",
data/DESIGN.md CHANGED
@@ -183,9 +183,19 @@ The binding is verified at four levels:
183
183
  microVM, confirming the gem manifest and source-install path are complete.
184
184
 
185
185
  **Roadmap:** the v1 roadmap (custom per-rule network policies, file patches,
186
- interactive `attach`/`attach_shell`, SSH, and the raw agent client) is now
187
- implemented see the list above. What remains is upstream-gated rather than a
188
- binding gap: surfacing newer core features as the pinned core-crate tag advances
189
- (e.g. additional network knobs like DNS/TLS-proxy tuning and per-mount stat
190
- virtualization), which slot in module-by-module exactly as the existing bindings
191
- do.
186
+ interactive `attach`/`attach_shell`, SSH, and the raw agent client) is
187
+ implemented, and so is the bulk of the v0.5.8 configuration surface that a
188
+ later parity pass added: streaming image-pull progress, host-side `VolumeFs`,
189
+ streaming guest fs (`read_stream`/`write_stream`), the full secrets surface,
190
+ network configuration (DNS, TLS-interception tuning, IPv4/IPv6 pools,
191
+ `max_connections`, `trust_host_cas`), `init`/`ephemeral`/disk-image `fstype`
192
+ create options, full mount options (tmpfs/disk + stat-virtualization/
193
+ host-permissions), and snapshot inspection (`open`/`list_dir`/`reindex`).
194
+
195
+ A few **secondary** upstream knobs remain unexposed (a genuine binding gap, not
196
+ upstream-gated — they exist at the pinned `v0.5.8`): per-published-port host
197
+ **bind address** (ports always bind loopback), network **interface overrides**,
198
+ and inline **named-volume create-mode** (pre-create with `Volume.create`, then
199
+ mount with `{ named: }`). These slot in module-by-module exactly as the existing
200
+ bindings do. Beyond those, surfacing genuinely newer core features is gated on
201
+ advancing the pinned core-crate tag.
data/README.md CHANGED
@@ -347,7 +347,7 @@ variable → an SDK-set override → the config file → `~/.microsandbox/bin/ms
347
347
  Microsandbox.installed? # => true/false
348
348
  Microsandbox.install # download + install the runtime (idempotent)
349
349
  Microsandbox.runtime_path # => "/Users/you/.microsandbox/bin/msb"
350
- Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override
350
+ Microsandbox.runtime_path = "/opt/microsandbox/bin/msb" # override (set-once)
351
351
  Microsandbox.libkrunfw_path = "/opt/microsandbox/lib/libkrunfw.dylib" # override (set-once)
352
352
  ```
353
353
 
@@ -383,8 +383,8 @@ change diverged the two numbers — the gem version is **not** a reliable indica
383
383
  of the embedded runtime version. To learn which runtime a build wraps, ask it:
384
384
 
385
385
  ```ruby
386
- Microsandbox::VERSION # => "0.6.0" (the gem's own version)
387
- Microsandbox.runtime_version # => "v0.5.8" (the embedded upstream runtime tag)
386
+ Microsandbox::VERSION # => "0.8.0" (the gem's own version)
387
+ Microsandbox.runtime_version # => "v0.5.10" (the embedded upstream runtime tag)
388
388
  ```
389
389
 
390
390
  | Gem version | Upstream runtime | Notes |
@@ -396,6 +396,8 @@ Microsandbox.runtime_version # => "v0.5.8" (the embedded upstream runtime tag
396
396
  | `0.5.11` | `v0.5.8` | gem-only revision |
397
397
  | `0.5.12` | `v0.5.8` | gem-only revision |
398
398
  | `0.6.0` | `v0.5.8` | gem version decoupled from the upstream tag; adds `runtime_version` |
399
+ | `0.7.0` | `v0.5.8` | SDK parity release (large binding-gap closure) |
400
+ | `0.8.0` | `v0.5.10` | adopts upstream `v0.5.10` (idle-only heartbeat, config-fd hardening, **4 GiB default bind-mount quota**); supersedes the reverted `v0.5.9` attempt |
399
401
 
400
402
  **Going forward** — the gem version moves on its own semver track and no longer
401
403
  mirrors the upstream tag:
@@ -470,25 +472,38 @@ or credential setup is needed.
470
472
  > Until promoted, users install the source gem (which compiles via `rb_sys`).
471
473
 
472
474
  See [DESIGN.md](DESIGN.md) for the architecture and the implemented-surface
473
- section. The binding now covers the full official-SDK surface: sandbox
475
+ section. The binding covers the official-SDK surface: sandbox
474
476
  lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
475
477
  `status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
476
478
  `stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
477
- `wait_until_stopped` from `Sandbox.get`, and label-filtered `list_with`),
479
+ `wait_until_stopped`/`config`/`config_json`/`snapshot`/`snapshot_to` from
480
+ `Sandbox.get`, and label-filtered `list_with`),
478
481
  backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
479
482
  `exec`/`shell` (collected and streaming), interactive `attach`/
480
- `attach_shell`, the full guest filesystem, metrics (per-sandbox,
481
- `Microsandbox.all_sandbox_metrics`, and streaming `metrics_stream`/`log_stream`),
482
- logs, OCI image-cache management, named volumes, snapshots (create/list/verify/
483
- export/import + boot-from-snapshot), **rootfs patches** (`Microsandbox::Patch`),
484
- **custom per-rule network policies** (`Microsandbox::NetworkPolicy`/`Rule`/
485
- `Destination`, alongside the presets), **SSH** (`Sandbox#ssh`
486
- `SshClient`/`SftpClient`/`SshServer`), and the **raw agent client**
487
- (`Microsandbox::AgentClient`). Create options span resources, network policy,
488
- `log_level`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches` and more;
489
- `exec`/`shell` take per-call `rlimits`, and `create` accepts
483
+ `attach_shell`, the full guest filesystem (incl. streaming `read_stream`/
484
+ `write_stream`), metrics (per-sandbox, `Microsandbox.all_sandbox_metrics`, and
485
+ streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
486
+ named volumes (incl. host-side `Volume.fs`/`VolumeInfo#fs` read/write),
487
+ snapshots (create/open/list/list_dir/reindex/verify/export/import +
488
+ boot-from-snapshot), streaming image-pull progress
489
+ (`Sandbox.create_with_progress` → `PullSession`), **rootfs patches**
490
+ (`Microsandbox::Patch`), **network configuration** (presets, custom per-rule
491
+ `Microsandbox::NetworkPolicy`/`Rule`/`Destination`, plus DNS, TLS interception,
492
+ IPv4/IPv6 pools, `max_connections`, `trust_host_cas`), **secrets** (multi-host /
493
+ wildcard allow-lists, injection toggles, per-secret + sandbox-level violation
494
+ policy), **SSH** (`Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`), and the
495
+ **raw agent client** (`Microsandbox::AgentClient`). Create options span
496
+ resources, `init`/`ephemeral`, disk-image `fstype`, network policy + config,
497
+ `log_level`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches`/`volumes`
498
+ (bind/named/tmpfs/disk with mount policies) and more; `exec`/`shell` take
499
+ per-call `rlimits`, and `create` accepts
490
500
  `registry_auth`/`registry_insecure`/`registry_ca_certs` for private and
491
- authenticated registries.
501
+ authenticated registries, plus customizable provisioning via `Microsandbox.setup`.
502
+
503
+ A few secondary upstream knobs are not yet exposed: per-published-port host bind
504
+ address (ports always bind loopback), network interface overrides, and inline
505
+ named-volume create-mode (pre-create the volume with `Volume.create`, then mount
506
+ it with `{ named: "…" }`).
492
507
 
493
508
  ## Contributing
494
509
 
@@ -6,8 +6,8 @@ name = "microsandbox_rb"
6
6
  description = "Ruby SDK native extension for microsandbox — secure, fast microVM-based sandboxing."
7
7
  # Must equal Microsandbox::VERSION (lib/microsandbox/version.rb) — Native.version
8
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.8).
10
- version = "0.6.0"
9
+ # The core-crate dependency below stays pinned at its own tag (v0.5.10).
10
+ version = "0.8.0"
11
11
  authors = ["Super Rad Company <development@superrad.company>"]
12
12
  repository = "https://github.com/superradcompany/microsandbox"
13
13
  license = "Apache-2.0"
@@ -35,8 +35,8 @@ rb-sys = "0.9"
35
35
  # `.cargo/config.toml.example`). "ssh" matches the feature set the Python/Node
36
36
  # SDKs ship with; default features add "prebuilt" (provisions msb + libkrunfw at
37
37
  # build time), "net", and "keyring".
38
- microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.8", default-features = true, features = ["ssh"] }
39
- microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.8" }
38
+ microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.10", default-features = true, features = ["ssh"] }
39
+ microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.10" }
40
40
 
41
41
  # Async core bridged to Ruby's synchronous API via a blocking tokio runtime.
42
42
  tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
@@ -40,7 +40,7 @@ impl AgentClient {
40
40
 
41
41
  /// Connect to a running sandbox by name. `timeout` is optional seconds.
42
42
  fn connect_sandbox(name: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
43
- let bridge = match dur(timeout) {
43
+ let bridge = match dur(timeout)? {
44
44
  Some(t) => block_on(AgentBridge::connect_sandbox_with_timeout(&name, t)),
45
45
  None => block_on(AgentBridge::connect_sandbox(&name)),
46
46
  }
@@ -50,7 +50,7 @@ impl AgentClient {
50
50
 
51
51
  /// Connect to an agentd relay socket by path. `timeout` is optional seconds.
52
52
  fn connect_path(path: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
53
- let bridge = match dur(timeout) {
53
+ let bridge = match dur(timeout)? {
54
54
  Some(t) => block_on(AgentBridge::connect_path_with_timeout(&path, t)),
55
55
  None => block_on(AgentBridge::connect_path(&path)),
56
56
  }
@@ -125,12 +125,23 @@ impl AgentClient {
125
125
  }
126
126
  }
127
127
 
128
- /// Convert seconds into a `Duration`, treating a non-positive/absent value as
129
- /// "use the default handshake timeout".
130
- fn dur(timeout: Option<f64>) -> Option<Duration> {
128
+ /// Convert an optional `timeout` (seconds) into an optional `Duration`,
129
+ /// mirroring the Python SDK's `timeout_duration`:
130
+ /// - absent (`nil`) `None` → the core's default handshake timeout
131
+ /// - `0` → an explicit zero deadline (fail fast), *not* "use the default"
132
+ /// - negative or non-finite (NaN/Inf) → a caller error (rather than being
133
+ /// silently swallowed into the default)
134
+ /// - finite but out of `Duration` range (e.g. `Float::MAX`) → a caller error
135
+ /// via `try_from_secs_f64`, rather than the panic `from_secs_f64` would raise
136
+ fn dur(timeout: Option<f64>) -> Result<Option<Duration>, Error> {
131
137
  match timeout {
132
- Some(t) if t.is_finite() && t > 0.0 => Some(Duration::from_secs_f64(t)),
133
- _ => None,
138
+ None => Ok(None),
139
+ Some(t) if t.is_finite() && t >= 0.0 => Duration::try_from_secs_f64(t)
140
+ .map(Some)
141
+ .map_err(|e| error::base_error(format!("timeout {t} seconds is out of range: {e}"))),
142
+ Some(t) => Err(error::base_error(format!(
143
+ "timeout must be a non-negative, finite number of seconds (got {t})"
144
+ ))),
134
145
  }
135
146
  }
136
147
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  use std::collections::HashMap;
7
7
 
8
- use magnus::{value::ReprValue, Error, RArray, RHash, TryConvert, Value};
8
+ use magnus::{value::ReprValue, Error, IntoValue, RArray, RHash, TryConvert, Value};
9
9
 
10
10
  /// Fetch a non-nil value for `key`, if present.
11
11
  fn get(hash: RHash, key: &str) -> Option<Value> {
@@ -90,3 +90,39 @@ pub fn opt_port_map(hash: RHash, key: &str) -> Result<Vec<(u16, u16)>, Error> {
90
90
  None => Ok(Vec::new()),
91
91
  }
92
92
  }
93
+
94
+ /// Recursively convert a `serde_json::Value` into a Ruby value. Used for
95
+ /// pass-through JSON whose shape is not fixed — e.g. an image config's OCI
96
+ /// `labels` object, which the Ruby layer hands back verbatim (mirroring the
97
+ /// Python SDK's `dict | None`).
98
+ pub fn json_to_ruby(value: &serde_json::Value) -> Value {
99
+ let ruby = crate::runtime::ruby();
100
+ match value {
101
+ serde_json::Value::Null => ruby.qnil().as_value(),
102
+ serde_json::Value::Bool(b) => b.into_value_with(&ruby),
103
+ serde_json::Value::Number(n) => {
104
+ if let Some(i) = n.as_i64() {
105
+ i.into_value_with(&ruby)
106
+ } else if let Some(u) = n.as_u64() {
107
+ u.into_value_with(&ruby)
108
+ } else {
109
+ n.as_f64().unwrap_or(0.0).into_value_with(&ruby)
110
+ }
111
+ }
112
+ serde_json::Value::String(s) => s.as_str().into_value_with(&ruby),
113
+ serde_json::Value::Array(items) => {
114
+ let arr = ruby.ary_new();
115
+ for item in items {
116
+ let _ = arr.push(json_to_ruby(item));
117
+ }
118
+ arr.into_value_with(&ruby)
119
+ }
120
+ serde_json::Value::Object(map) => {
121
+ let hash = ruby.hash_new();
122
+ for (k, v) in map {
123
+ let _ = hash.aset(k.as_str(), json_to_ruby(v));
124
+ }
125
+ hash.into_value_with(&ruby)
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,92 @@
1
+ //! Streaming guest-filesystem I/O: `Microsandbox::Native::FsReadStream` and
2
+ //! `Microsandbox::Native::FsWriteSink`.
3
+ //!
4
+ //! Wraps the core `SandboxFs::read_stream`/`write_stream` handles so large files
5
+ //! can be moved without buffering the whole thing in memory. The core
6
+ //! `FsReadStream::recv` needs `&mut self` and `FsWriteSink::close` consumes
7
+ //! `self`, so each is held behind a `tokio::Mutex` (the sink as an `Option`, so
8
+ //! `close` can take it and be idempotent). Each call drives the future to
9
+ //! completion with the GVL released; the Ruby layer wraps these as an
10
+ //! `Enumerable` reader and a writer with a block form.
11
+
12
+ use std::sync::Arc;
13
+
14
+ use magnus::{method, prelude::*, Error, RModule, RString, Ruby};
15
+ use microsandbox::sandbox::{FsReadStream, FsWriteSink};
16
+ use tokio::sync::Mutex;
17
+
18
+ use crate::error;
19
+ use crate::runtime::{block_on, ruby};
20
+
21
+ #[magnus::wrap(class = "Microsandbox::Native::FsReadStream", free_immediately, size)]
22
+ pub struct FsReadStreamHandle {
23
+ inner: Arc<Mutex<FsReadStream>>,
24
+ }
25
+
26
+ impl FsReadStreamHandle {
27
+ pub fn new(stream: FsReadStream) -> Self {
28
+ Self {
29
+ inner: Arc::new(Mutex::new(stream)),
30
+ }
31
+ }
32
+
33
+ /// Next chunk of bytes (ASCII-8BIT), or nil at end of stream.
34
+ fn recv(&self) -> Result<Option<RString>, Error> {
35
+ let inner = Arc::clone(&self.inner);
36
+ match block_on(async move { inner.lock().await.recv().await }).map_err(error::to_ruby)? {
37
+ Some(bytes) => Ok(Some(ruby().str_from_slice(bytes.as_ref()))),
38
+ None => Ok(None),
39
+ }
40
+ }
41
+ }
42
+
43
+ #[magnus::wrap(class = "Microsandbox::Native::FsWriteSink", free_immediately, size)]
44
+ pub struct FsWriteSinkHandle {
45
+ inner: Arc<Mutex<Option<FsWriteSink>>>,
46
+ }
47
+
48
+ impl FsWriteSinkHandle {
49
+ pub fn new(sink: FsWriteSink) -> Self {
50
+ Self {
51
+ inner: Arc::new(Mutex::new(Some(sink))),
52
+ }
53
+ }
54
+
55
+ /// Write a chunk of bytes. Errors if the sink is already closed.
56
+ fn write(&self, data: RString) -> Result<(), Error> {
57
+ // Copy out while the GVL is held (GC.compact could move the buffer).
58
+ let bytes = unsafe { data.as_slice() }.to_vec();
59
+ let inner = Arc::clone(&self.inner);
60
+ let result = block_on(async move {
61
+ let guard = inner.lock().await;
62
+ match guard.as_ref() {
63
+ Some(sink) => Some(sink.write(&bytes).await),
64
+ None => None,
65
+ }
66
+ });
67
+ match result {
68
+ Some(r) => r.map_err(error::to_ruby),
69
+ None => Err(error::base_error("write to a closed FsWriteSink")),
70
+ }
71
+ }
72
+
73
+ /// Flush and close the sink. Idempotent.
74
+ fn close(&self) -> Result<(), Error> {
75
+ let inner = Arc::clone(&self.inner);
76
+ let taken = block_on(async move { inner.lock().await.take() });
77
+ match taken {
78
+ Some(sink) => block_on(sink.close()).map_err(error::to_ruby),
79
+ None => Ok(()),
80
+ }
81
+ }
82
+ }
83
+
84
+ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
85
+ let rs = native.define_class("FsReadStream", ruby.class_object())?;
86
+ rs.define_method("recv", method!(FsReadStreamHandle::recv, 0))?;
87
+
88
+ let ws = native.define_class("FsWriteSink", ruby.class_object())?;
89
+ ws.define_method("write", method!(FsWriteSinkHandle::write, 1))?;
90
+ ws.define_method("close", method!(FsWriteSinkHandle::close, 0))?;
91
+ Ok(())
92
+ }
@@ -9,6 +9,7 @@ use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
9
9
  use microsandbox::image::{Image, ImageDetail, ImageHandle, ImagePruneReport};
10
10
 
11
11
  use crate::backend::with_local_backend;
12
+ use crate::conv;
12
13
  use crate::runtime::ruby;
13
14
 
14
15
  fn handle_to_hash(h: &ImageHandle) -> RHash {
@@ -43,6 +44,11 @@ fn detail_to_hash(detail: ImageDetail) -> RHash {
43
44
  let _ = c.aset("entrypoint", config.entrypoint);
44
45
  let _ = c.aset("working_dir", config.working_dir);
45
46
  let _ = c.aset("user", config.user);
47
+ // OCI config labels: a free-form JSON object (or nil). Converted to a
48
+ // Ruby Hash so `ImageDetail#config["labels"]` matches the Python/Node
49
+ // `config.labels` dict. (The pinned v0.5.8 runtime persists this as
50
+ // null today; the key exists for forward-compatibility/parity.)
51
+ let _ = c.aset("labels", config.labels.as_ref().map(conv::json_to_ruby));
46
52
  let _ = c.aset("stop_signal", config.stop_signal);
47
53
  let _ = hash.aset("config", c);
48
54
  } else {