microsandbox-rb 0.8.1 → 0.9.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: f8cc1edaf4e221fe6e4addf4eea0d06e63370dd73bbcb1a7eb4d13e91bac7b40
4
- data.tar.gz: 17f18d5aefead5fbb584a7b06ecd82d0aade2394c3e9c48aafd0f5d98673abfd
3
+ metadata.gz: 3395a95418c2c6e159365140455797f0b40318aa585aea8b0b4ac0b93f521957
4
+ data.tar.gz: e39164f8143656a0f069720f3359e133bffe2cb5438146dab3b3e629abbe9647
5
5
  SHA512:
6
- metadata.gz: e7c98769be2508875e54b1449d290c18b63af3189af497d311c6222073b6018290950faa4438c165f656928d175e6f6a50ec0f7af6323a8174dbf0e460b99042
7
- data.tar.gz: 7a8edef36f6f7e3c12a5075e684d64ca71570350dd678ebcd3b819a9ebfab309007e2b71e348f1c0925ab8e3beb474787aeec741935f5e803676f5ce00daf513
6
+ metadata.gz: d07ddd920c69d73c86372176745047ed27c515a5ba3dd30bb1c9fe5ddcf2de39009dc670d1f44965d38ea6c8399e84224640e51fe4caf792f40291ab49befbe1
7
+ data.tar.gz: 2eb6879b20289edbcebf80eee934bfd81d176bf6f8e3c577f3da21d8e987e15315db486c612f5f1bd8fa296a35ba4f3244e293496116bce321ae7bd9a9201530
data/CHANGELOG.md CHANGED
@@ -6,7 +6,164 @@ 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]
9
+ ## [0.9.0] - 2026-06-29
10
+
11
+ Adopts upstream runtime **`v0.5.10` → `v0.6.1`** (spanning the upstream `v0.6.0`
12
+ and `v0.6.1` releases). The upstream public SDK surface is purely additive — no
13
+ items were removed or re-signatured — so the gem's Ruby API is unchanged and the
14
+ existing bindings compile against `v0.6.1` untouched. Per the README's versioning
15
+ policy, adopting a new upstream runtime moves the gem onto its own `0.9` line.
16
+
17
+ ### Changed
18
+
19
+ - **Embedded runtime is now `v0.6.1`** (`Microsandbox::RUNTIME_VERSION` /
20
+ `Microsandbox.runtime_version`). Upstream fixes inherited by the synchronous
21
+ Ruby API:
22
+ - **Zombie sandbox runtimes no longer block** (upstream #1036): the SDK stops
23
+ waiting on a sandbox runtime that has already exited, so lifecycle calls
24
+ return promptly instead of hanging on a dead child.
25
+ - **Secrets are substituted through `CONNECT` proxies** (upstream #1022):
26
+ `microsandbox-network` now applies secret injection on tunnelled (HTTPS
27
+ `CONNECT`) requests, not only on plain-HTTP ones.
28
+ - **Stale sandboxes are stopped and cleaned up** (upstream #1050).
29
+ - Windows host support, `msb` CLI additions (`--no-tty`, self-downgrade,
30
+ cross-platform `doctor`), and the `msb_krun` `0.1.17 → 0.1.19` bump are
31
+ inherited but do not affect the macOS/Linux Ruby build or API surface.
32
+
33
+ ### Notes
34
+
35
+ - The new upstream **host-directory bind rootfs** (`ImageBuilder::bind`, upstream
36
+ #1021) is intentionally **not** exposed in this release: it requires a real
37
+ microVM boot to exercise, and the Python SDK parity reference only stubs the
38
+ type without wiring it into the sandbox builder. Tracked as a possible
39
+ follow-up.
40
+
41
+ ## [0.8.2] - 2026-06-29
42
+
43
+ Gem-only release on the `v0.5.10` runtime (unchanged). Bundles the post-`0.8.1`
44
+ audit follow-ups: a secret-leak fix, typed snapshot errors, panic-free duration
45
+ parsing, the precompiled fat-gem loader + `extconf` preflight corrections, and a
46
+ sweep of threading/streaming/SSH documentation.
47
+
48
+ ### Documentation
49
+
50
+ - **Calling-thread non-preemption is now documented** (issue #24). The GVL is
51
+ released during native calls so *other* threads keep running, but the
52
+ *calling* thread blocks uninterruptibly until the call returns —
53
+ `Timeout::timeout`/`Thread#kill`/Ctrl-C can't interrupt it. README and
54
+ DESIGN.md now state this and steer callers to the genuinely-bounding
55
+ `exec(timeout:)` / `shell(timeout:)` knobs, and clarify that
56
+ `AgentClient.connect_sandbox`/`connect_path`'s `timeout:` bounds only the
57
+ connect handshake while `AgentClient#request`/`#stream` and the streaming
58
+ paths have no timeout knob and can block indefinitely — rather than reaching
59
+ for `Timeout::timeout`.
60
+ - **`exec` `timeout: 0` semantics clarified** (issue #29). The `@param timeout`
61
+ doc now notes the asymmetry: omit or `nil` means *no* timeout, while `0` is an
62
+ immediate (zero) deadline that kills the command before any output and raises
63
+ `ExecTimeoutError` — so use `nil`/omit, never `0`, for "no limit". Also noted
64
+ that `exec_stream`/`shell_stream` accept `timeout:` but do **not** apply it
65
+ (the streaming path discards it).
66
+ - **Streaming classes documented as single-pass / single-consumer** (issues #34,
67
+ #31). `ExecHandle`, `LogStream`, `MetricsStream`, `FsReadStream`,
68
+ `PullSession`, and `AgentStream` are `Enumerable` but drain a one-shot native
69
+ channel: forward-only, not rewindable, and meant for one consumer on one
70
+ thread. A second `each` (or a combinator after a partial drain) silently
71
+ yields nothing. Noted on each class and in a README streaming caveat.
72
+ - **SSH `close` disconnect behavior documented** (issue #33). `SshClient#close` /
73
+ `SftpClient#close` send the graceful protocol disconnect; relying on GC skips
74
+ it (only the in-process server task is aborted). The block-less
75
+ `open_client`/`sftp` docs now tell callers to `close` (or use the block form)
76
+ for a clean disconnect.
77
+
78
+ ### Internal
79
+
80
+ - **`DESIGN.md` refreshed** (issue #35). The stale runtime-pin references
81
+ (`v0.5.7`/`v0.5.8`) now point at `v0.5.10` via `RUNTIME_VERSION`, and the
82
+ hard-coded unit-example count is replaced with rot-proof phrasing.
83
+ - **RBS gains a note on SDK-constructed types** (issue #36). `sig/microsandbox.rbs`
84
+ now carries a top-of-file note explaining that native-backed value/handle/stream
85
+ types are constructed by the SDK from an internal native handle or data hash, not
86
+ by user code. Their `initialize` signatures are kept: RBS derives `new` from
87
+ `initialize`, so omitting them would not hide the constructor — it would
88
+ synthesize a misleading zero-arg `() -> instance` that Ruby actually rejects.
89
+
90
+ ### Fixed
91
+
92
+ - **Precompiled fat-gem loader now finds the staged binary** (issue #25). The
93
+ native-extension require used `RbConfig::CONFIG["ruby_version"]` — the API
94
+ string `"3.4.0"` — but rake-compiler stages a multi-version fat gem's binaries
95
+ under the **major.minor** subdir (`3.4`), so the versioned require always
96
+ missed and fell to the flat-path rescue, which is **absent** in a precompiled
97
+ gem (only the versioned binary is packed). Every fat-gem install would have
98
+ failed at `require "microsandbox"` the moment precompiled gems are promoted.
99
+ The loader now derives the subdir from `RUBY_VERSION[/\d+\.\d+/]` (`"3.4"`),
100
+ matching the staged path; the flat-path rescue still covers source builds.
101
+ - **`extconf` MSRV preflight probes the compiler the build actually runs**
102
+ (issue #39). The preflight ran a bare `rustc --version` and hard-aborted when
103
+ `< 1.91`, but the build is driven by `cargo`, which resolves its compiler from
104
+ `$RUSTC` if set, otherwise the bare `rustc` on PATH — it never uses the `rustc`
105
+ beside the `cargo` binary, and the rustup `cargo` shim neither sets `$RUSTC` nor
106
+ reorders PATH. The preflight now mirrors that exact resolution (`$RUSTC`, else
107
+ PATH `rustc`), so it neither false-passes when a stale non-rustup `rustc`
108
+ shadows a rustup `cargo` (the build would compile with that stale `rustc` and
109
+ fail deep in smoltcp) nor false-aborts when `$RUSTC` points at a newer compiler.
110
+
111
+ ### Internal
112
+
113
+ - **CI now installs the packed gem from source** (issue #37). A new `package`
114
+ job runs `rake build`, `gem install`s the packed gem (exercising the gemspec
115
+ `spec.files` glob and the full from-gem `extconf` + `cargo` compile against the
116
+ packed `Cargo.toml`/`Cargo.lock`/`rust-toolchain.toml`), and requires it from
117
+ outside the repo. Previously every job compiled the working tree in place, so
118
+ a packaging regression could reach RubyGems undetected.
119
+ - **`version_spec` now guards the `Cargo.lock` version** (issue #38). The spec
120
+ already asserted `Native.version == VERSION` and the runtime-tag pin, but
121
+ nothing checked the `microsandbox_rb` version in the committed `Cargo.lock`,
122
+ which the gemspec packs. A release that bumped `version.rb` + `Cargo.toml` but
123
+ forgot to refresh the lock would ship a stale lock (and a `--locked` build
124
+ would reject it) — a recurring release mistake this now catches.
125
+ ### Security
126
+
127
+ - **Secret values no longer leak into `ArgumentError` messages** (issue #23).
128
+ `Sandbox.create(secrets:)` validation interpolated the whole secret spec via
129
+ `spec.inspect` into two error messages — and because the `:value`-present
130
+ guard runs first, the "needs `:host`/`:hosts`/`:host_patterns`" error *always*
131
+ embedded the cleartext secret value (and the env/value error did whenever a
132
+ value was supplied). Such messages routinely reach logs and error trackers.
133
+ Both messages now report the spec's keys only (`spec.keys.inspect`), mirroring
134
+ the existing `registry_auth` handling, with a unit spec asserting the value
135
+ is never present in the raised message.
136
+
137
+ ### Fixed
138
+
139
+ - **Native duration parsing is panic-free regardless of the Ruby layer**
140
+ (issue #30). The native binding called `Duration::from_secs_f64` directly at
141
+ five sites (`exec`/`shell` timeout, `stop_with_timeout`, `kill_with_timeout`,
142
+ `metrics_stream` interval, `replace_with_timeout`); that panics on NaN/Inf/
143
+ negative *and on finite-but-out-of-range* values (e.g. `Float::MAX`), which
144
+ surfaced as an ugly panic-turned-exception. The Ruby `coerce_duration` guard
145
+ set no upper bound, so a large finite value still reached and panicked the
146
+ native layer. All five sites now route through a `secs_to_duration` helper
147
+ (`try_from_secs_f64` + a clean `Microsandbox::Error`), matching the existing
148
+ agent-client pattern — defense in depth so the native layer is panic-free on
149
+ its own.
150
+ ### Added
151
+
152
+ - **Typed snapshot error classes** (issue #28). The five core snapshot error
153
+ variants — reachable through the gem's fully-wired `Snapshot` API — previously
154
+ collapsed to the base `Microsandbox::Error`, forcing callers to string-match
155
+ the message. They now raise typed subclasses:
156
+ `SnapshotNotFoundError` (`snapshot-not-found`),
157
+ `SnapshotAlreadyExistsError` (`snapshot-already-exists`),
158
+ `SnapshotSandboxRunningError` (`snapshot-sandbox-running`),
159
+ `SnapshotImageMissingError` (`snapshot-image-missing`), and
160
+ `SnapshotIntegrityError` (`snapshot-integrity`). This goes **beyond** the
161
+ Python SDK mirror (which defines no snapshot classes), matching the Go SDK's
162
+ per-variant coverage — a deliberate divergence. Additionally, the previously
163
+ orphaned `NetworkPolicyError` now also carries the core's `NetworkBuilder`
164
+ build/validation error (a `network(|n| ...)` failure), which previously fell
165
+ through to the base `Error`. All additive — existing `rescue Microsandbox::Error`
166
+ handlers still catch them.
10
167
 
11
168
  ## [0.8.1] - 2026-06-25
12
169
 
@@ -455,7 +612,11 @@ microsandbox runtime, aligned with the official Python/Node/Go SDKs.
455
612
  core crate has Apple-native deps). Until precompiled gems are published,
456
613
  installing from source requires a Rust toolchain (stable >= 1.91).
457
614
 
458
- [Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.7.0...HEAD
615
+ [Unreleased]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.9.0...HEAD
616
+ [0.9.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.8.2...v0.9.0
617
+ [0.8.2]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.8.1...v0.8.2
618
+ [0.8.1]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.8.0...v0.8.1
619
+ [0.8.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.7.0...v0.8.0
459
620
  [0.7.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.6.0...v0.7.0
460
621
  [0.6.0]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.12...v0.6.0
461
622
  [0.5.12]: https://github.com/ya-luotao/microsandbox-rb/compare/v0.5.11...v0.5.12
data/Cargo.lock CHANGED
@@ -2460,26 +2460,6 @@ dependencies = [
2460
2460
  "icu_properties",
2461
2461
  ]
2462
2462
 
2463
- [[package]]
2464
- name = "imago"
2465
- version = "0.2.3"
2466
- source = "registry+https://github.com/rust-lang/crates.io-index"
2467
- checksum = "ae7cfee876c698a1a2ed9c705ab18f21acbed82110f19b51cc458de73426fe2c"
2468
- dependencies = [
2469
- "async-trait",
2470
- "bincode",
2471
- "cfg-if",
2472
- "libc",
2473
- "miniz_oxide",
2474
- "nix 0.30.1",
2475
- "page_size",
2476
- "rustc_version",
2477
- "tokio",
2478
- "tracing",
2479
- "vm-memory 0.18.0",
2480
- "windows-sys 0.61.2",
2481
- ]
2482
-
2483
2463
  [[package]]
2484
2464
  name = "indexmap"
2485
2465
  version = "2.14.0"
@@ -2875,15 +2855,6 @@ dependencies = [
2875
2855
  "libc",
2876
2856
  ]
2877
2857
 
2878
- [[package]]
2879
- name = "linux-loader"
2880
- version = "0.13.0"
2881
- source = "registry+https://github.com/rust-lang/crates.io-index"
2882
- checksum = "870c3814345f050991f99869417779f6062542bcf4ed81db7a1b926ad1306638"
2883
- dependencies = [
2884
- "vm-memory 0.16.2",
2885
- ]
2886
-
2887
2858
  [[package]]
2888
2859
  name = "linux-raw-sys"
2889
2860
  version = "0.12.1"
@@ -3003,8 +2974,8 @@ dependencies = [
3003
2974
 
3004
2975
  [[package]]
3005
2976
  name = "microsandbox"
3006
- version = "0.5.10"
3007
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
2977
+ version = "0.6.1"
2978
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3008
2979
  dependencies = [
3009
2980
  "anyhow",
3010
2981
  "astral-tokio-tar",
@@ -3052,12 +3023,13 @@ dependencies = [
3052
3023
  "tracing",
3053
3024
  "typed-builder",
3054
3025
  "which",
3026
+ "windows-sys 0.61.2",
3055
3027
  ]
3056
3028
 
3057
3029
  [[package]]
3058
3030
  name = "microsandbox-agent-client"
3059
- version = "0.5.10"
3060
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3031
+ version = "0.6.1"
3032
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3061
3033
  dependencies = [
3062
3034
  "ciborium",
3063
3035
  "microsandbox-protocol",
@@ -3069,8 +3041,8 @@ dependencies = [
3069
3041
 
3070
3042
  [[package]]
3071
3043
  name = "microsandbox-db"
3072
- version = "0.5.10"
3073
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3044
+ version = "0.6.1"
3045
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3074
3046
  dependencies = [
3075
3047
  "async-trait",
3076
3048
  "sea-orm",
@@ -3081,8 +3053,8 @@ dependencies = [
3081
3053
 
3082
3054
  [[package]]
3083
3055
  name = "microsandbox-filesystem"
3084
- version = "0.5.10"
3085
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3056
+ version = "0.6.1"
3057
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3086
3058
  dependencies = [
3087
3059
  "libc",
3088
3060
  "microsandbox-utils",
@@ -3094,8 +3066,8 @@ dependencies = [
3094
3066
 
3095
3067
  [[package]]
3096
3068
  name = "microsandbox-image"
3097
- version = "0.5.10"
3098
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3069
+ version = "0.6.1"
3070
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3099
3071
  dependencies = [
3100
3072
  "astral-tokio-tar",
3101
3073
  "async-compression",
@@ -3115,24 +3087,25 @@ dependencies = [
3115
3087
  "tokio",
3116
3088
  "tokio-util",
3117
3089
  "tracing",
3118
- "xattr",
3090
+ "windows-sys 0.61.2",
3119
3091
  ]
3120
3092
 
3121
3093
  [[package]]
3122
3094
  name = "microsandbox-metrics"
3123
- version = "0.5.10"
3124
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3095
+ version = "0.6.1"
3096
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3125
3097
  dependencies = [
3126
3098
  "chrono",
3127
3099
  "libc",
3128
3100
  "thiserror 2.0.18",
3129
3101
  "tracing",
3102
+ "windows-sys 0.61.2",
3130
3103
  ]
3131
3104
 
3132
3105
  [[package]]
3133
3106
  name = "microsandbox-migration"
3134
- version = "0.5.10"
3135
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3107
+ version = "0.6.1"
3108
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3136
3109
  dependencies = [
3137
3110
  "sea-orm-migration",
3138
3111
  "serde_json",
@@ -3140,8 +3113,8 @@ dependencies = [
3140
3113
 
3141
3114
  [[package]]
3142
3115
  name = "microsandbox-network"
3143
- version = "0.5.10"
3144
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3116
+ version = "0.6.1"
3117
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3145
3118
  dependencies = [
3146
3119
  "base64",
3147
3120
  "bytes",
@@ -3159,6 +3132,7 @@ dependencies = [
3159
3132
  "microsandbox-protocol",
3160
3133
  "microsandbox-utils",
3161
3134
  "msb_krun",
3135
+ "msb_krun_utils",
3162
3136
  "parking_lot",
3163
3137
  "pem",
3164
3138
  "percent-encoding",
@@ -3176,12 +3150,13 @@ dependencies = [
3176
3150
  "tokio",
3177
3151
  "tokio-rustls",
3178
3152
  "tracing",
3153
+ "windows-sys 0.61.2",
3179
3154
  ]
3180
3155
 
3181
3156
  [[package]]
3182
3157
  name = "microsandbox-protocol"
3183
- version = "0.5.10"
3184
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3158
+ version = "0.6.1"
3159
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3185
3160
  dependencies = [
3186
3161
  "chrono",
3187
3162
  "ciborium",
@@ -3195,8 +3170,8 @@ dependencies = [
3195
3170
 
3196
3171
  [[package]]
3197
3172
  name = "microsandbox-runtime"
3198
- version = "0.5.10"
3199
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3173
+ version = "0.6.1"
3174
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3200
3175
  dependencies = [
3201
3176
  "bytes",
3202
3177
  "chrono",
@@ -3221,12 +3196,13 @@ dependencies = [
3221
3196
  "thiserror 2.0.18",
3222
3197
  "tokio",
3223
3198
  "tracing",
3199
+ "windows-sys 0.61.2",
3224
3200
  ]
3225
3201
 
3226
3202
  [[package]]
3227
3203
  name = "microsandbox-types"
3228
- version = "0.5.10"
3229
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3204
+ version = "0.6.1"
3205
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3230
3206
  dependencies = [
3231
3207
  "chrono",
3232
3208
  "serde",
@@ -3237,19 +3213,20 @@ dependencies = [
3237
3213
 
3238
3214
  [[package]]
3239
3215
  name = "microsandbox-utils"
3240
- version = "0.5.10"
3241
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.5.10#a62c4e45da7320e89aafc44b8a44fc74ec49fb53"
3216
+ version = "0.6.1"
3217
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.1#868ac5a8f04fadca357b377537e7a5740a856509"
3242
3218
  dependencies = [
3243
3219
  "dirs",
3244
3220
  "libc",
3245
3221
  "reflink-copy",
3246
3222
  "scopeguard",
3247
3223
  "ureq",
3224
+ "windows-sys 0.61.2",
3248
3225
  ]
3249
3226
 
3250
3227
  [[package]]
3251
3228
  name = "microsandbox_rb"
3252
- version = "0.8.1"
3229
+ version = "0.9.0"
3253
3230
  dependencies = [
3254
3231
  "chrono",
3255
3232
  "futures",
@@ -3321,11 +3298,31 @@ dependencies = [
3321
3298
  "num-traits",
3322
3299
  ]
3323
3300
 
3301
+ [[package]]
3302
+ name = "msb-imago"
3303
+ version = "0.1.0"
3304
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3305
+ checksum = "16db000f7280d4c8a78b8b95f64a41006d447fd116ecd69d984d93774cb85d09"
3306
+ dependencies = [
3307
+ "async-trait",
3308
+ "bincode",
3309
+ "cfg-if",
3310
+ "libc",
3311
+ "miniz_oxide",
3312
+ "nix 0.30.1",
3313
+ "page_size",
3314
+ "rustc_version",
3315
+ "tokio",
3316
+ "tracing",
3317
+ "vm-memory",
3318
+ "windows-sys 0.61.2",
3319
+ ]
3320
+
3324
3321
  [[package]]
3325
3322
  name = "msb_krun"
3326
- version = "0.1.17"
3323
+ version = "0.1.19"
3327
3324
  source = "registry+https://github.com/rust-lang/crates.io-index"
3328
- checksum = "72a554acf513be16b336fbcd4a9bab29a78b06a5ddf94e917481cfeafc1078f5"
3325
+ checksum = "c0fb3437734ef4103d3891d3cbd51a8726f196af350c68cb044a1860207c64d3"
3329
3326
  dependencies = [
3330
3327
  "crossbeam-channel",
3331
3328
  "kvm-bindings",
@@ -3338,14 +3335,14 @@ dependencies = [
3338
3335
  "msb_krun_polly",
3339
3336
  "msb_krun_utils",
3340
3337
  "msb_krun_vmm",
3341
- "vm-memory 0.16.2",
3338
+ "vm-memory",
3342
3339
  ]
3343
3340
 
3344
3341
  [[package]]
3345
3342
  name = "msb_krun_arch"
3346
- version = "0.1.17"
3343
+ version = "0.1.19"
3347
3344
  source = "registry+https://github.com/rust-lang/crates.io-index"
3348
- checksum = "9d270d914cee3125cf71734b9a5aa60fe7d9648f09511999199424e0fe1a88c6"
3345
+ checksum = "e25fe665643d66900ec9407ef0d3afdb5cd4ca97ad394c46b8243bfa6344bfc0"
3349
3346
  dependencies = [
3350
3347
  "kvm-bindings",
3351
3348
  "kvm-ioctls",
@@ -3353,21 +3350,20 @@ dependencies = [
3353
3350
  "msb_krun_arch_gen",
3354
3351
  "msb_krun_smbios",
3355
3352
  "msb_krun_utils",
3356
- "vm-memory 0.16.2",
3357
- "vmm-sys-util",
3353
+ "vm-memory",
3358
3354
  ]
3359
3355
 
3360
3356
  [[package]]
3361
3357
  name = "msb_krun_arch_gen"
3362
- version = "0.1.17"
3358
+ version = "0.1.19"
3363
3359
  source = "registry+https://github.com/rust-lang/crates.io-index"
3364
- checksum = "8372d1e4e8c853e20ffb5568a4cc3f930a2bf1ce25bc346812ac7b84e84639d4"
3360
+ checksum = "7e50dfac228ad088917f8ffb2920c4a56d762ebc5a90950ee7bf5414253b02aa"
3365
3361
 
3366
3362
  [[package]]
3367
3363
  name = "msb_krun_cpuid"
3368
- version = "0.1.17"
3364
+ version = "0.1.19"
3369
3365
  source = "registry+https://github.com/rust-lang/crates.io-index"
3370
- checksum = "179542321455c65a8e4b14c09bc8443227a89cd46ef2c9c06f0ee1ab11f510ea"
3366
+ checksum = "fd5221749c0224858b075b61f3f2a0964dbfe367093e9ee650b6b0e3d690c3b2"
3371
3367
  dependencies = [
3372
3368
  "kvm-bindings",
3373
3369
  "kvm-ioctls",
@@ -3376,37 +3372,39 @@ dependencies = [
3376
3372
 
3377
3373
  [[package]]
3378
3374
  name = "msb_krun_devices"
3379
- version = "0.1.17"
3375
+ version = "0.1.19"
3380
3376
  source = "registry+https://github.com/rust-lang/crates.io-index"
3381
- checksum = "67350e76fd466e1a089b89fa548ba85a92f237c2d3dab179ba675cfd18eb9a39"
3377
+ checksum = "4fee50bfdf1a1258e33bcf0ee8631bb805db79b8d442161ab63d700689a992a8"
3382
3378
  dependencies = [
3383
3379
  "bitflags 1.3.2",
3384
3380
  "capng",
3385
3381
  "caps",
3386
3382
  "crossbeam-channel",
3387
- "imago",
3388
3383
  "kvm-bindings",
3389
3384
  "kvm-ioctls",
3390
3385
  "libc",
3391
3386
  "libloading",
3392
3387
  "log",
3393
3388
  "lru",
3389
+ "msb-imago",
3394
3390
  "msb_krun_arch",
3395
3391
  "msb_krun_hvf",
3396
3392
  "msb_krun_polly",
3397
3393
  "msb_krun_utils",
3398
3394
  "nix 0.30.1",
3399
3395
  "rand 0.9.4",
3396
+ "tokio",
3400
3397
  "virtio-bindings",
3401
3398
  "vm-fdt",
3402
- "vm-memory 0.16.2",
3399
+ "vm-memory",
3400
+ "windows-sys 0.61.2",
3403
3401
  ]
3404
3402
 
3405
3403
  [[package]]
3406
3404
  name = "msb_krun_hvf"
3407
- version = "0.1.17"
3405
+ version = "0.1.19"
3408
3406
  source = "registry+https://github.com/rust-lang/crates.io-index"
3409
- checksum = "13eaca12857149c3552007a18c7f863ccc6b4dddcf2863c0bcc8fbc2aa2d4c6e"
3407
+ checksum = "3f2300bbe4dbd56bdc3ffc2384baf82e299fff8a45e844f41c4a0be64210ed47"
3410
3408
  dependencies = [
3411
3409
  "crossbeam-channel",
3412
3410
  "libloading",
@@ -3416,19 +3414,19 @@ dependencies = [
3416
3414
 
3417
3415
  [[package]]
3418
3416
  name = "msb_krun_kernel"
3419
- version = "0.1.17"
3417
+ version = "0.1.19"
3420
3418
  source = "registry+https://github.com/rust-lang/crates.io-index"
3421
- checksum = "514f7493860978b08819fdcc4ef3d1887fd4d2b5e26ad0a0eabcec005410df94"
3419
+ checksum = "3402b8879bd5b91f33d069fefa00fd0416f12cd660cfd7538da8e77201f2dda5"
3422
3420
  dependencies = [
3423
3421
  "msb_krun_utils",
3424
- "vm-memory 0.16.2",
3422
+ "vm-memory",
3425
3423
  ]
3426
3424
 
3427
3425
  [[package]]
3428
3426
  name = "msb_krun_polly"
3429
- version = "0.1.17"
3427
+ version = "0.1.19"
3430
3428
  source = "registry+https://github.com/rust-lang/crates.io-index"
3431
- checksum = "08ebbb0336b9cd8dcffa9c47d01a729de4f83c39fc099620f4f9c4d8ef69f460"
3429
+ checksum = "da550bf268cb9217255ed152bfca9f61275a2796237fd180989958c7f54dc137"
3432
3430
  dependencies = [
3433
3431
  "libc",
3434
3432
  "msb_krun_utils",
@@ -3436,18 +3434,18 @@ dependencies = [
3436
3434
 
3437
3435
  [[package]]
3438
3436
  name = "msb_krun_smbios"
3439
- version = "0.1.17"
3437
+ version = "0.1.19"
3440
3438
  source = "registry+https://github.com/rust-lang/crates.io-index"
3441
- checksum = "f25cb991386c6aca7c74d0bd8b327a807290830e439fbb473c5e1b62a7c66d32"
3439
+ checksum = "2c0737818e69346348f8ba2a7197cbec2336e807e5f7f094c80e0ca1635ca054"
3442
3440
  dependencies = [
3443
- "vm-memory 0.16.2",
3441
+ "vm-memory",
3444
3442
  ]
3445
3443
 
3446
3444
  [[package]]
3447
3445
  name = "msb_krun_utils"
3448
- version = "0.1.17"
3446
+ version = "0.1.19"
3449
3447
  source = "registry+https://github.com/rust-lang/crates.io-index"
3450
- checksum = "b7104f87f235ef5cc320ffa760b7cd3635960b58cdc5e1ec89df852fc538f042"
3448
+ checksum = "fb370035195bfd6d60e0b4ad8b0d68dcf9d138f144d033087a960117920a126e"
3451
3449
  dependencies = [
3452
3450
  "bitflags 1.3.2",
3453
3451
  "crossbeam-channel",
@@ -3456,13 +3454,14 @@ dependencies = [
3456
3454
  "log",
3457
3455
  "nix 0.30.1",
3458
3456
  "vmm-sys-util",
3457
+ "windows-sys 0.61.2",
3459
3458
  ]
3460
3459
 
3461
3460
  [[package]]
3462
3461
  name = "msb_krun_vmm"
3463
- version = "0.1.17"
3462
+ version = "0.1.19"
3464
3463
  source = "registry+https://github.com/rust-lang/crates.io-index"
3465
- checksum = "6742a7fba174849b924a370bfba0fe7be8435253bff26303aec34ac89641ae63"
3464
+ checksum = "68e9f485f720981fda9071b2711d4f8406b6c67ebf6bfa2bb8e83f3cc6c2aaea"
3466
3465
  dependencies = [
3467
3466
  "bzip2",
3468
3467
  "crossbeam-channel",
@@ -3470,7 +3469,7 @@ dependencies = [
3470
3469
  "kvm-bindings",
3471
3470
  "kvm-ioctls",
3472
3471
  "libc",
3473
- "linux-loader",
3472
+ "libloading",
3474
3473
  "log",
3475
3474
  "msb_krun_arch",
3476
3475
  "msb_krun_arch_gen",
@@ -3481,8 +3480,8 @@ dependencies = [
3481
3480
  "msb_krun_polly",
3482
3481
  "msb_krun_utils",
3483
3482
  "nix 0.30.1",
3484
- "vm-memory 0.16.2",
3485
- "vmm-sys-util",
3483
+ "vm-memory",
3484
+ "windows-sys 0.61.2",
3486
3485
  "zstd",
3487
3486
  ]
3488
3487
 
@@ -6396,24 +6395,15 @@ version = "0.3.0"
6396
6395
  source = "registry+https://github.com/rust-lang/crates.io-index"
6397
6396
  checksum = "7e21282841a059bb62627ce8441c491f09603622cd5a21c43bfedc85a2952f23"
6398
6397
 
6399
- [[package]]
6400
- name = "vm-memory"
6401
- version = "0.16.2"
6402
- source = "registry+https://github.com/rust-lang/crates.io-index"
6403
- checksum = "1fd5e56d48353c5f54ef50bd158a0452fc82f5383da840f7b8efc31695dd3b9d"
6404
- dependencies = [
6405
- "libc",
6406
- "thiserror 1.0.69",
6407
- "winapi",
6408
- ]
6409
-
6410
6398
  [[package]]
6411
6399
  name = "vm-memory"
6412
6400
  version = "0.18.0"
6413
6401
  source = "registry+https://github.com/rust-lang/crates.io-index"
6414
6402
  checksum = "9b55e753c7725603745cb32b2287ef7ef3da05c03c7702cda3fa8abe25ae0465"
6415
6403
  dependencies = [
6404
+ "libc",
6416
6405
  "thiserror 2.0.18",
6406
+ "winapi",
6417
6407
  ]
6418
6408
 
6419
6409
  [[package]]
data/DESIGN.md CHANGED
@@ -58,6 +58,23 @@ only (never touches the Ruby C API) and uses `catch_unwind` so a Rust panic is
58
58
  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
+ **The *calling* thread is not preemptible during the call.** `nogvl` passes a
62
+ null unblock-function to `rb_thread_call_without_gvl`, and Ruby only checks
63
+ pending interrupts *before* and *after* the GVL-released region — so while a
64
+ native call blocks, the thread that issued it cannot be interrupted:
65
+ `Timeout::timeout` (which relies on `Thread#raise`), `Thread#kill`, and `SIGINT`
66
+ (Ctrl-C) are all deferred until the call returns on its own. The GVL release
67
+ keeps *other* threads live; it does **not** make the calling thread
68
+ cancelable. This is harmless for the bounded calls (`exec`/`shell` with a
69
+ `timeout:`, `AgentClient` with a `timeout:`) because the deadline fires inside
70
+ the future, but the unbounded/streaming paths — `exec`/`shell` with
71
+ `timeout: nil`, `ExecHandle#recv`/`#wait`, `log_stream`/`metrics_stream` `recv`
72
+ (especially `follow: true`), `Sandbox#wait`/`SandboxHandle#wait_until_stopped`,
73
+ and `AgentClient#request` with no timeout — can block their caller indefinitely
74
+ if the guest wedges or a relay drops. **Bound such calls with the explicit
75
+ `timeout:` knobs rather than wrapping them in `Timeout::timeout`**, which will
76
+ not fire while the native call is in flight.
77
+
61
78
  ## Error mapping
62
79
 
63
80
  The core returns one big `MicrosandboxError` enum. `error.rs` maps each variant
@@ -88,7 +105,8 @@ out (air-gapped hosts that provision out of band). libkrunfw is `dlopen`'d by
88
105
  ## Core-crate dependency (self-contained)
89
106
 
90
107
  `ext/microsandbox/Cargo.toml` depends on the core crate via a **pinned git tag**
91
- (`microsandbox` / `microsandbox-network` at `v0.5.7`), so the gem builds anywhere
108
+ (`microsandbox` / `microsandbox-network`, pinned to the same tag as
109
+ `Microsandbox::RUNTIME_VERSION` — currently `v0.6.1`), so the gem builds anywhere
92
110
  — CI, `rake-compiler-dock` release containers, and end-user source installs —
93
111
  without an adjacent checkout. For fast local development against a sibling
94
112
  microsandbox checkout, copy `.cargo/config.toml.example` to `.cargo/config.toml`
@@ -166,8 +184,8 @@ Create options now cover `image`, `cpus`, `memory`, `oci_upper_size`, `env`,
166
184
 
167
185
  The binding is verified at four levels:
168
186
 
169
- 1. **Unit** (192 examples) — the Ruby layer's option normalization and value
170
- objects, with the native layer stubbed.
187
+ 1. **Unit** (several hundred examples) — the Ruby layer's option normalization
188
+ and value objects, with the native layer stubbed.
171
189
  2. **Real-microVM integration** (`spec/integration`, opt-in via
172
190
  `MICROSANDBOX_INTEGRATION=1`) — boots actual sandboxes and round-trips
173
191
  `exec`/`shell`/`fs`/`metrics`/`logs`/streaming/snapshots. Run locally on
@@ -193,7 +211,7 @@ create options, full mount options (tmpfs/disk + stat-virtualization/
193
211
  host-permissions), and snapshot inspection (`open`/`list_dir`/`reindex`).
194
212
 
195
213
  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
214
+ upstream-gated — they exist at the pinned `v0.6.1` runtime): per-published-port host
197
215
  **bind address** (ports always bind loopback), network **interface overrides**,
198
216
  and inline **named-volume create-mode** (pre-create with `Volume.create`, then
199
217
  mount with `{ named: }`). These slot in module-by-module exactly as the existing
data/README.md CHANGED
@@ -43,7 +43,7 @@ them. Our deepest thanks to the maintainers and community. 🙏
43
43
  - **SSH & SFTP** — native in-process SSH client/server and file transfer (`Sandbox#ssh`)
44
44
  - **Raw agent client** — byte-level access to the guest `agentd` protocol (`Microsandbox::AgentClient`)
45
45
  - **Idiomatic Ruby** — keyword arguments, block-scoped lifecycle, a typed error hierarchy
46
- - **Thread-friendly** — the GVL is released during sandbox calls, so other Ruby threads keep running
46
+ - **Thread-friendly** — the GVL is released during sandbox calls, so _other_ Ruby threads keep running. The _calling_ thread blocks uninterruptibly until the call returns (`Timeout::timeout`/`Thread#kill`/Ctrl-C can't interrupt a blocked native call), so bound long-running work with a real deadline where one exists: `exec(timeout:)` / `shell(timeout:)` kill the guest command after N seconds, and `AgentClient.connect_sandbox`/`connect_path` take a `timeout:` that bounds only the connect handshake. The streaming paths and `AgentClient#request`/`#stream` have no timeout knob and can block indefinitely if the guest wedges — see DESIGN.md
47
47
 
48
48
  ## Requirements
49
49
 
@@ -215,6 +215,9 @@ end
215
215
 
216
216
  ```ruby
217
217
  Microsandbox::Sandbox.create("obs", image: "public.ecr.aws/docker/library/alpine:latest") do |sb|
218
+ # On the v0.6.1 runtime the metrics slot goes live a beat after create returns,
219
+ # so `metrics` can briefly raise "no live metrics slot" right after boot —
220
+ # retry for a few hundred ms rather than treating the first failure as fatal.
218
221
  m = sb.metrics # => Microsandbox::Metrics
219
222
  m.cpu_percent
220
223
  m.memory_bytes
@@ -244,6 +247,13 @@ Microsandbox::Sandbox.create("stream", image: "public.ecr.aws/docker/library/pyt
244
247
  end
245
248
  ```
246
249
 
250
+ > **Streams are single-pass.** `ExecHandle`, `LogStream`, `MetricsStream`,
251
+ > `FsReadStream`, `PullSession`, and `AgentStream` are `Enumerable`, but `each`
252
+ > drains a one-shot native channel — they are forward-only, not rewindable, and
253
+ > meant for a single consumer. Iterate (or `collect`/`read`) exactly once: a
254
+ > second `each`, or a combinator after a partial drain (`count` then `each`,
255
+ > `to_a` twice), silently yields nothing. Don't share one handle across threads.
256
+
247
257
  ### Images
248
258
 
249
259
  Manage the local OCI image cache (images are pulled automatically on `create`):
@@ -383,8 +393,8 @@ change diverged the two numbers — the gem version is **not** a reliable indica
383
393
  of the embedded runtime version. To learn which runtime a build wraps, ask it:
384
394
 
385
395
  ```ruby
386
- Microsandbox::VERSION # => "0.8.0" (the gem's own version)
387
- Microsandbox.runtime_version # => "v0.5.10" (the embedded upstream runtime tag)
396
+ Microsandbox::VERSION # => "0.9.0" (the gem's own version)
397
+ Microsandbox.runtime_version # => "v0.6.1" (the embedded upstream runtime tag)
388
398
  ```
389
399
 
390
400
  | Gem version | Upstream runtime | Notes |
@@ -399,6 +409,8 @@ Microsandbox.runtime_version # => "v0.5.10" (the embedded upstream runtime ta
399
409
  | `0.7.0` | `v0.5.8` | SDK parity release (large binding-gap closure) |
400
410
  | `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 |
401
411
  | `0.8.1` | `v0.5.10` | gem-only: re-provision a stale local runtime; per-bind-mount `quota_mib:` override |
412
+ | `0.8.2` | `v0.5.10` | gem-only: redact secrets from errors, typed snapshot errors, panic-free durations, fat-gem loader + `extconf` preflight fixes, threading/streaming docs |
413
+ | `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 |
402
414
 
403
415
  **Going forward** — the gem version moves on its own semver track and no longer
404
416
  mirrors the upstream tag:
@@ -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.10).
10
- version = "0.8.1"
9
+ # The core-crate dependency below stays pinned at its own tag (v0.6.1).
10
+ version = "0.9.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.10", default-features = true, features = ["ssh"] }
39
- microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.5.10" }
38
+ microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.1", default-features = true, features = ["ssh"] }
39
+ microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.1" }
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"] }
@@ -2,31 +2,46 @@
2
2
 
3
3
  require "mkmf"
4
4
  require "rb_sys/mkmf"
5
+ require "shellwords"
5
6
 
6
7
  # Preflight: the embedded microsandbox core is edition 2024 and pulls smoltcp,
7
- # which sets a Minimum Supported Rust Version of 1.91. A `cargo` from an older
8
- # toolchain (commonly Homebrew's rustc, which shadows a newer rustup on PATH and
9
- # ignores this gem's rust-toolchain.toml) fails deep in the build with a cryptic
10
- # "rustc X is not supported by smoltcp" error. Detect it up front and explain
11
- # the fix instead.
8
+ # which sets a Minimum Supported Rust Version of 1.91. An older rustc (commonly
9
+ # Homebrew's, which shadows a newer rustup on PATH and ignores this gem's
10
+ # rust-toolchain.toml) fails deep in the build with a cryptic "rustc X is not
11
+ # supported by smoltcp" error. Detect it up front and explain the fix instead.
12
+ #
13
+ # Probe the *same* rustc the build will invoke. create_rust_makefile drives the
14
+ # build through `cargo`, and cargo resolves its compiler exactly as: the `RUSTC`
15
+ # env var if set, otherwise the bare `rustc` found on PATH. It does NOT use the
16
+ # `rustc` sitting beside the `cargo` binary, and the rustup `cargo` shim neither
17
+ # sets `RUSTC` nor prepends its toolchain bin to PATH — toolchain selection
18
+ # survives only because the PATH `rustc` is normally itself a rustup shim that
19
+ # honors rust-toolchain.toml. So a non-rustup rustc earlier on PATH (which reads
20
+ # neither RUSTUP_TOOLCHAIN nor the toolchain file) is what the build actually
21
+ # runs. Mirroring cargo's RUSTC-then-PATH resolution is the only probe that
22
+ # neither false-passes (the trap of checking the cargo sibling, which stays a
23
+ # valid rustup shim while the build silently uses the stale PATH rustc) nor
24
+ # false-aborts (when `RUSTC` points at a newer compiler than the PATH `rustc`).
12
25
  MSRV = Gem::Version.new("1.91")
26
+
27
+ def build_rustc
28
+ rustc = ENV["RUSTC"].to_s.strip
29
+ rustc.empty? ? "rustc" : rustc
30
+ end
31
+
32
+ rustc = build_rustc
13
33
  rustc_version = begin
14
- out = `rustc --version 2>/dev/null`
34
+ out = `#{rustc.shellescape} --version 2>/dev/null`
15
35
  out[/\d+\.\d+(\.\d+)?/] && Gem::Version.new(out[/\d+\.\d+(\.\d+)?/])
16
36
  rescue
17
37
  nil
18
38
  end
19
39
 
20
40
  if rustc_version && rustc_version < MSRV
21
- which_rustc = begin
22
- `which rustc 2>/dev/null`.strip
23
- rescue
24
- ""
25
- end
26
41
  abort(<<~MSG)
27
42
 
28
43
  [microsandbox-rb] Rust #{rustc_version} is too old — the embedded core requires rustc >= #{MSRV}.
29
- Found: #{which_rustc.empty? ? "rustc" : which_rustc} (#{rustc_version})
44
+ Found: #{rustc} (#{rustc_version})
30
45
 
31
46
  This usually means an older rustc (e.g. Homebrew's) is ahead of a newer
32
47
  rustup toolchain on your PATH. Fixes:
@@ -37,8 +52,12 @@ if rustc_version && rustc_version < MSRV
37
52
  rustup install stable && rustup default stable
38
53
  • Or upgrade your system Rust to >= #{MSRV}.
39
54
 
40
- (This gem ships a rust-toolchain.toml pinning `stable`; the rustup `cargo`
41
- shim honors it, but a non-rustup `cargo` does not.)
55
+ Or point `RUSTC` at a recent compiler (cargo honors it over PATH):
56
+ RUSTC="$HOME/.cargo/bin/rustc" gem install microsandbox-rb
57
+
58
+ (This gem ships a rust-toolchain.toml pinning `stable`, but only a rustup
59
+ `rustc` shim reads it — a non-rustup `rustc` ahead on PATH ignores it, and
60
+ cargo invokes that PATH `rustc` unless `RUSTC` says otherwise.)
42
61
  MSG
43
62
  end
44
63
 
@@ -34,6 +34,22 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
34
34
  // client's `UnsupportedOperation` above.
35
35
  CloudHttp { .. } => "CloudHttpError",
36
36
  Unsupported { .. } => "UnsupportedError",
37
+ // Snapshot operations, all reachable through the gem's fully-wired
38
+ // `Snapshot` API. Upstream raises these un-wrapped, so without a mapping
39
+ // they collapse to the base `Error` and callers must string-match the
40
+ // message. This goes BEYOND the Python mirror (which has no Snapshot
41
+ // classes and matches the Go SDK's per-variant coverage instead) — a
42
+ // deliberate divergence noted in `lib/microsandbox/errors.rb`.
43
+ SnapshotNotFound(_) => "SnapshotNotFoundError",
44
+ SnapshotAlreadyExists(_) => "SnapshotAlreadyExistsError",
45
+ SnapshotSandboxRunning(_) => "SnapshotSandboxRunningError",
46
+ SnapshotImageMissing(_) => "SnapshotImageMissingError",
47
+ SnapshotIntegrity(_) => "SnapshotIntegrityError",
48
+ // Give the already-defined-but-orphaned `NetworkPolicyError` a mapping:
49
+ // a builder parse/validation error from `network(|n| ...)`. The gem
50
+ // unconditionally enables the core's `net` feature (default-features),
51
+ // so this variant is always present.
52
+ NetworkBuilder(_) => "NetworkPolicyError",
37
53
  _ => "Error",
38
54
  }
39
55
  }
@@ -40,6 +40,21 @@ use crate::exec::ExecHandle;
40
40
  use crate::runtime::{block_on, ruby};
41
41
  use crate::stream::{LogStream, MetricsStream};
42
42
 
43
+ /// Convert a seconds `f64` into a `Duration`, surfacing a clean Ruby error
44
+ /// instead of the panic `Duration::from_secs_f64` raises on NaN/Inf/negative —
45
+ /// *and on finite-but-out-of-range* values (e.g. `Float::MAX`). The Ruby
46
+ /// `coerce_duration` already guards the public paths, but it sets no upper
47
+ /// bound, so a large finite value would still reach (and panic) the native
48
+ /// layer; this keeps the native layer panic-free regardless of the Ruby layer,
49
+ /// mirroring `agent::dur`.
50
+ fn secs_to_duration(secs: f64) -> Result<Duration, Error> {
51
+ Duration::try_from_secs_f64(secs).map_err(|e| {
52
+ error::base_error(format!(
53
+ "duration must be a non-negative, finite number of seconds in range (got {secs}: {e})"
54
+ ))
55
+ })
56
+ }
57
+
43
58
  #[magnus::wrap(class = "Microsandbox::Native::Sandbox", free_immediately, size)]
44
59
  pub struct Sandbox {
45
60
  inner: microsandbox::Sandbox,
@@ -380,7 +395,7 @@ impl Sandbox {
380
395
  b = b.detached(true);
381
396
  }
382
397
  if let Some(secs) = conv::opt_f64(opts, "replace_with_timeout")? {
383
- b = b.replace_with_timeout(Duration::from_secs_f64(secs));
398
+ b = b.replace_with_timeout(secs_to_duration(secs)?);
384
399
  } else if conv::opt_bool(opts, "replace")? {
385
400
  b = b.replace();
386
401
  }
@@ -579,14 +594,22 @@ impl Sandbox {
579
594
 
580
595
  /// Stream metrics snapshots at `interval` seconds. Returns a MetricsStream
581
596
  /// to pull snapshots from.
582
- fn metrics_stream(&self, interval: f64) -> MetricsStream {
583
- let dur = Duration::from_secs_f64(if interval <= 0.0 { 1.0 } else { interval });
597
+ fn metrics_stream(&self, interval: f64) -> Result<MetricsStream, Error> {
598
+ // `interval <= 0.0` (0 or negative) keeps the prior 1s-default behavior;
599
+ // NaN (for which `<= 0.0` is false) and finite-but-out-of-range values
600
+ // fall through to `secs_to_duration`, which errors cleanly rather than
601
+ // letting `from_secs_f64` panic across the FFI boundary.
602
+ let dur = if interval <= 0.0 {
603
+ Duration::from_secs(1)
604
+ } else {
605
+ secs_to_duration(interval)?
606
+ };
584
607
  // `metrics_stream` is synchronous but builds a `tokio::time::interval`,
585
608
  // which panics ("no reactor running") unless constructed inside the
586
609
  // runtime context — so build it under `block_on`. (`log_stream` is async
587
610
  // and already runs inside `block_on`, so it needs no such wrapper.)
588
611
  let stream = block_on(async { self.inner.metrics_stream(dur) });
589
- MetricsStream::from_stream(stream)
612
+ Ok(MetricsStream::from_stream(stream))
590
613
  }
591
614
 
592
615
  /// Stream captured logs as they appear. `opts`: sources, since_ms,
@@ -1544,7 +1567,9 @@ impl ExecOpts {
1544
1567
  cwd: conv::opt_string(opts, "cwd")?,
1545
1568
  user: conv::opt_string(opts, "user")?,
1546
1569
  env: conv::opt_string_map(opts, "env")?,
1547
- timeout: conv::opt_f64(opts, "timeout")?.map(Duration::from_secs_f64),
1570
+ timeout: conv::opt_f64(opts, "timeout")?
1571
+ .map(secs_to_duration)
1572
+ .transpose()?,
1548
1573
  tty: conv::opt_bool(opts, "tty")?,
1549
1574
  stdin,
1550
1575
  stdin_pipe: conv::opt_bool(opts, "stdin_pipe")?,
@@ -1907,8 +1932,7 @@ impl SbHandle {
1907
1932
 
1908
1933
  /// Graceful stop with a custom escalation timeout (seconds).
1909
1934
  fn stop_with_timeout(&self, secs: f64) -> Result<(), Error> {
1910
- block_on(self.inner.stop_with_timeout(Duration::from_secs_f64(secs)))
1911
- .map_err(error::to_ruby)
1935
+ block_on(self.inner.stop_with_timeout(secs_to_duration(secs)?)).map_err(error::to_ruby)
1912
1936
  }
1913
1937
 
1914
1938
  /// Force kill (SIGKILL) and wait.
@@ -1918,8 +1942,7 @@ impl SbHandle {
1918
1942
 
1919
1943
  /// Force kill, waiting up to `secs` for the process to disappear.
1920
1944
  fn kill_with_timeout(&self, secs: f64) -> Result<(), Error> {
1921
- block_on(self.inner.kill_with_timeout(Duration::from_secs_f64(secs)))
1922
- .map_err(error::to_ruby)
1945
+ block_on(self.inner.kill_with_timeout(secs_to_duration(secs)?)).map_err(error::to_ruby)
1923
1946
  }
1924
1947
 
1925
1948
  /// Send the graceful-shutdown request and return without waiting.
@@ -35,6 +35,10 @@ module Microsandbox
35
35
  # consume {AgentFrame}s until the stream ends (the terminal frame is delivered,
36
36
  # then iteration stops). Mirrors the official SDKs' `AgentStream`.
37
37
  #
38
+ # @note **Single-pass, forward-only, single-consumer.** `each` drains a
39
+ # one-shot native channel — not rewindable, iterate once from a single
40
+ # thread; a second pass or a post-drain combinator yields nothing.
41
+ #
38
42
  # @example
39
43
  # stream = client.stream(0, request_body)
40
44
  # stream.each { |frame| handle(frame) }
@@ -51,7 +51,22 @@ module Microsandbox
51
51
  define_error(:ImageInUseError, "image-in-use")
52
52
  define_error(:ImagePullFailedError, "image-pull-failed")
53
53
 
54
+ # Snapshot errors ---------------------------------------------------------
55
+ # These go BEYOND the Python SDK mirror (which has no Snapshot classes and
56
+ # collapses all five to its base error) — a deliberate divergence matching the
57
+ # Go SDK's per-variant coverage, so callers can rescue a missing/duplicate/
58
+ # running-source/missing-image/corrupt snapshot specifically. The native layer
59
+ # (ext/microsandbox/src/error.rs) maps the five core Snapshot* variants here.
60
+ define_error(:SnapshotNotFoundError, "snapshot-not-found")
61
+ define_error(:SnapshotAlreadyExistsError, "snapshot-already-exists")
62
+ define_error(:SnapshotSandboxRunningError, "snapshot-sandbox-running")
63
+ define_error(:SnapshotImageMissingError, "snapshot-image-missing")
64
+ define_error(:SnapshotIntegrityError, "snapshot-integrity")
65
+
54
66
  # Networking / secrets errors ---------------------------------------------
67
+ # NetworkPolicyError now also carries the core's `NetworkBuilder` build/parse
68
+ # error (a `network(|n| ...)` validation failure), which was previously
69
+ # unmapped and fell through to the base Error.
55
70
  define_error(:NetworkPolicyError, "network-policy-error")
56
71
  define_error(:SecretViolationError, "secret-violation")
57
72
  define_error(:TlsError, "tls-error")
@@ -86,6 +86,13 @@ module Microsandbox
86
86
  # Iterate it (it is {Enumerable}) to consume {ExecEvent}s as they arrive, or
87
87
  # call {#collect} to drain it into an {ExecOutput}.
88
88
  #
89
+ # @note **Single-pass, forward-only, single-consumer.** `each`/`collect` drain
90
+ # a one-shot native event channel, so the handle is *not* rewindable: a
91
+ # second `each`, or `collect` after a partial `each` (and vice versa),
92
+ # yields only what is left. Consume it once, from one thread. (An
93
+ # out-of-band {#kill}/{#signal} via the control channel is the exception —
94
+ # it can unblock a parked `each` from another thread.)
95
+ #
89
96
  # @example
90
97
  # handle = sb.exec_stream("python", ["-u", "script.py"])
91
98
  # handle.each do |event|
@@ -199,6 +199,11 @@ module Microsandbox
199
199
  # A streaming reader over a guest file, from {FS#read_stream}. Iterate it (it
200
200
  # is {Enumerable}) to consume byte chunks (ASCII-8BIT) as they arrive, or call
201
201
  # {#read} to drain it into one String.
202
+ #
203
+ # @note **Single-pass, forward-only, single-consumer.** `each`/`read` drain a
204
+ # one-shot native channel — not rewindable, and not safe to share across
205
+ # threads. Consume it once: a second `each`, or `read` after a partial
206
+ # `each`, yields only the remaining bytes.
202
207
  class FsReadStream
203
208
  include Enumerable
204
209
 
@@ -526,7 +526,10 @@ module Microsandbox
526
526
  env = spec[:env] || spec["env"]
527
527
  value = spec[:value] || spec["value"]
528
528
  unless env && value
529
- raise ArgumentError, "secret spec needs :env and :value (got #{spec.inspect})"
529
+ # Report only the keys given, never the values a secret spec carries
530
+ # the cleartext :value, and exception messages routinely land in logs
531
+ # and error trackers. Mirrors apply_registry_opts above.
532
+ raise ArgumentError, "secret spec needs :env and :value (got keys: #{spec.keys.inspect})"
530
533
  end
531
534
  out = {"env" => env.to_s, "value" => value.to_s}
532
535
  hosts = Array(spec[:hosts] || spec["hosts"]).map(&:to_s)
@@ -534,8 +537,10 @@ module Microsandbox
534
537
  hosts << single.to_s if single
535
538
  patterns = Array(spec[:host_patterns] || spec["host_patterns"]).map(&:to_s)
536
539
  if hosts.empty? && patterns.empty?
540
+ # Keys only — this branch runs after :value is confirmed present, so
541
+ # spec.inspect would always embed the cleartext secret value.
537
542
  raise ArgumentError,
538
- "secret spec needs :host, :hosts, or :host_patterns (got #{spec.inspect})"
543
+ "secret spec needs :host, :hosts, or :host_patterns (got keys: #{spec.keys.inspect})"
539
544
  end
540
545
  out["hosts"] = hosts unless hosts.empty?
541
546
  out["host_patterns"] = patterns unless patterns.empty?
@@ -820,7 +825,11 @@ module Microsandbox
820
825
  # @param cwd [String, nil] working directory
821
826
  # @param user [String, nil] user to run as
822
827
  # @param env [Hash, nil] extra environment variables
823
- # @param timeout [Numeric, nil] kill after N seconds
828
+ # @param timeout [Numeric, nil] kill the command after N seconds and raise
829
+ # {ExecTimeoutError}. Omit or pass +nil+ for *no* timeout. Note the
830
+ # asymmetry: +0+ is **not** "disable" — it is an immediate (zero) deadline,
831
+ # so the command is killed before producing any output and {ExecTimeoutError}
832
+ # is raised. Use +nil+/omit, never +0+, to mean "no limit".
824
833
  # @param tty [Boolean] allocate a pseudo-terminal
825
834
  # @param stdin [String, Symbol, nil] bytes to feed to stdin, or +:pipe+ to
826
835
  # open a streaming stdin pipe (write/close it via {ExecHandle#stdin}; only
@@ -843,6 +852,11 @@ module Microsandbox
843
852
  # Pass +stdin: :pipe+ to feed the process interactively: {ExecHandle#stdin}
844
853
  # then returns a writable sink; close it to send EOF (a process like +cat+
845
854
  # that reads until EOF will otherwise block forever).
855
+ #
856
+ # @note +timeout:+ is accepted for signature symmetry with {#exec} but is
857
+ # **not applied** on the streaming path (the runtime discards it). Enforce a
858
+ # deadline yourself around the iteration, or use blocking {#exec} with
859
+ # +timeout:+ for the kill-after-N-seconds behavior.
846
860
  # @return [ExecHandle]
847
861
  # @see ExecHandle
848
862
  def exec_stream(command, args = [], cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
@@ -851,6 +865,8 @@ module Microsandbox
851
865
  end
852
866
 
853
867
  # Run a shell script and stream its output as it arrives.
868
+ # @note Like {#exec_stream}, +timeout:+ is accepted but **not applied** on
869
+ # the streaming path.
854
870
  # @return [ExecHandle]
855
871
  def shell_stream(script, cwd: nil, user: nil, env: nil, timeout: nil, tty: false, stdin: nil, rlimits: nil)
856
872
  ExecHandle.new(@native.shell_stream(script.to_s,
@@ -912,6 +928,14 @@ module Microsandbox
912
928
  end
913
929
 
914
930
  # Latest resource-usage snapshot.
931
+ #
932
+ # Raises a {Microsandbox::Error} ("sandbox N has no live metrics slot") when
933
+ # called in the brief window right after {Sandbox.create} returns, before the
934
+ # runtime has registered the sandbox's metrics slot. On the `v0.6.1` runtime
935
+ # the spawn handshake no longer blocks create until the first sample is
936
+ # written, so the slot goes live a beat *after* boot (within a few hundred
937
+ # milliseconds); retry for that window rather than treating the first failure
938
+ # as fatal.
915
939
  # @return [Metrics]
916
940
  def metrics
917
941
  Metrics.new(@native.metrics)
@@ -936,6 +960,14 @@ module Microsandbox
936
960
 
937
961
  # Stream resource-usage snapshots, one per interval tick, until the sandbox
938
962
  # stops. Requires metrics to be enabled for the sandbox.
963
+ #
964
+ # The first tick fires immediately, so opening the stream right after
965
+ # {Sandbox.create} can hit the same metrics-slot startup window as {#metrics}
966
+ # and yield a transient "no live metrics slot" error on that first tick.
967
+ # Because the stream is single-pass (a drained or errored stream is spent),
968
+ # make sure the slot is live *before* opening it — e.g. retry {#metrics}
969
+ # until it succeeds (the slot goes live within a few hundred milliseconds of
970
+ # boot), then call {#metrics_stream}.
939
971
  # @param interval [Numeric] seconds between snapshots
940
972
  # @return [MetricsStream] an {Enumerable} of {Metrics}
941
973
  def metrics_stream(interval: 1.0)
@@ -1083,6 +1115,12 @@ module Microsandbox
1083
1115
  # "kind" key (e.g. "resolving", "resolved", "layer_download_progress",
1084
1116
  # "layer_materialize_progress", "complete") plus kind-specific fields.
1085
1117
  #
1118
+ # @note **Single-pass, forward-only, single-consumer.** `each` drains a
1119
+ # one-shot native progress channel — not rewindable, iterate once from a
1120
+ # single thread. {#sandbox} works whether or not you iterated (it awaits the
1121
+ # create either way), so you can skip `each` entirely; but don't expect a
1122
+ # second `each` to replay the events.
1123
+ #
1086
1124
  # @example
1087
1125
  # session = Microsandbox::Sandbox.create_with_progress("box", image: "python")
1088
1126
  # session.each { |ev| puts "#{ev["kind"]} #{ev["downloaded_bytes"]}" }
@@ -121,7 +121,10 @@ module Microsandbox
121
121
  @native.read_link(path.to_s)
122
122
  end
123
123
 
124
- # Close the SFTP session. Idempotent.
124
+ # Close the SFTP session. Idempotent. Sends the graceful SFTP close;
125
+ # relying on GC to reclaim an unclosed session drops it abruptly (no
126
+ # graceful close), so call this — or use the block form of
127
+ # {SshClient#sftp} — for a clean shutdown.
125
128
  # @return [nil]
126
129
  def close
127
130
  @native.close
@@ -161,7 +164,9 @@ module Microsandbox
161
164
  end
162
165
 
163
166
  # Open an SFTP session over this connection. With a block, the session is
164
- # yielded and closed when the block returns.
167
+ # yielded and closed when the block returns. Without a block you own the
168
+ # returned {SftpClient} and must call {SftpClient#close} (letting it GC skips
169
+ # the graceful disconnect).
165
170
  # @yieldparam sftp [SftpClient]
166
171
  # @return [SftpClient, Object]
167
172
  def sftp
@@ -175,7 +180,11 @@ module Microsandbox
175
180
  end
176
181
  end
177
182
 
178
- # Close the SSH client session. Idempotent.
183
+ # Close the SSH client session. Idempotent. Sends the graceful protocol
184
+ # disconnect (russh `Disconnect::ByApplication`); if the client is instead
185
+ # left to GC, only the in-process server task is aborted and that disconnect
186
+ # is skipped. Prefer the block form of {SshOps#open_client}, or call this in
187
+ # an `ensure`, so the session closes cleanly.
179
188
  # @return [nil]
180
189
  def close
181
190
  @native.close
@@ -216,7 +225,9 @@ module Microsandbox
216
225
  end
217
226
 
218
227
  # Open a native in-process SSH client to the sandbox. With a block, the
219
- # client is yielded and closed when the block returns.
228
+ # client is yielded and closed when the block returns. Without a block you
229
+ # own the returned {SshClient} and must call {SshClient#close} for a clean
230
+ # protocol disconnect (letting it GC only aborts the in-process server task).
220
231
  # @param user [String] guest user to authenticate as (default "root")
221
232
  # @param term [String, nil] TERM value for the session
222
233
  # @param sftp [Boolean] enable the SFTP subsystem (default true)
@@ -6,6 +6,14 @@ module Microsandbox
6
6
  # iteration blocks for new entries until the sandbox stops; otherwise it ends
7
7
  # once the historical log is drained.
8
8
  #
9
+ # @note **Single-pass, forward-only, single-consumer.** `each` drains a
10
+ # one-shot native channel, so it is *not* rewindable: a second `each` — or
11
+ # any `Enumerable` combinator after a partial drain (`to_a` twice, `count`
12
+ # then `each`, `first(n)` then `each`) — silently yields nothing. Iterate
13
+ # exactly once, from one thread. With `follow: true`, `each` blocks the
14
+ # calling thread uninterruptibly until the sandbox stops (see DESIGN.md on
15
+ # GVL release).
16
+ #
9
17
  # @example
10
18
  # sb.log_stream(follow: true).each { |entry| print entry.text }
11
19
  class LogStream
@@ -31,6 +39,11 @@ module Microsandbox
31
39
  # Enumerable: iteration yields one snapshot per interval tick until the
32
40
  # sandbox stops.
33
41
  #
42
+ # @note **Single-pass, forward-only, single-consumer.** Like {LogStream},
43
+ # `each` drains a one-shot native channel — not rewindable, iterate once
44
+ # from a single thread; a second pass or a post-drain combinator yields
45
+ # nothing.
46
+ #
34
47
  # @example
35
48
  # sb.metrics_stream(interval: 0.5).each { |m| puts m.cpu_percent }
36
49
  class MetricsStream
@@ -8,12 +8,12 @@ module Microsandbox
8
8
  # Versioning section of the README for the full gem-to-runtime map. Must equal
9
9
  # the native ext's Cargo crate version (`Native.version`), enforced by
10
10
  # spec/unit/version_spec.rb.
11
- VERSION = "0.8.1"
11
+ VERSION = "0.9.0"
12
12
 
13
13
  # The upstream microsandbox runtime release this gem build embeds — the `tag`
14
14
  # pinned on the `microsandbox`/`microsandbox-network` git deps in
15
15
  # ext/microsandbox/Cargo.toml. Exposed at runtime as
16
16
  # {Microsandbox.runtime_version}. spec/unit/version_spec.rb asserts it stays in
17
17
  # sync with the Cargo tag so it can't silently drift out of date.
18
- RUNTIME_VERSION = "v0.5.10"
18
+ RUNTIME_VERSION = "v0.6.1"
19
19
  end
data/lib/microsandbox.rb CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
3
  require_relative "microsandbox/version"
4
4
 
5
- # Load the compiled native extension. Precompiled platform gems ship a
6
- # version-specific subdirectory (e.g. lib/microsandbox/3.4/microsandbox_rb.bundle);
7
- # fall back to the flat path used by source builds.
5
+ # Load the compiled native extension. Precompiled platform gems stage the
6
+ # binary under a major.minor subdirectory (e.g.
7
+ # lib/microsandbox/3.4/microsandbox_rb.bundle) that is the directory
8
+ # rake-compiler builds (it matches the `ruby_version` against /(\d+\.\d+)/), NOT
9
+ # the API string "3.4.0" that RbConfig::CONFIG["ruby_version"] returns. Use
10
+ # RUBY_VERSION's major.minor so the require hits the staged path; fall back to
11
+ # the flat path source builds produce.
8
12
  begin
9
- ruby_version = RbConfig::CONFIG["ruby_version"].to_s
10
- require_relative "microsandbox/#{ruby_version}/microsandbox_rb"
13
+ abi_version = RUBY_VERSION[/\d+\.\d+/]
14
+ require_relative "microsandbox/#{abi_version}/microsandbox_rb"
11
15
  rescue LoadError
12
16
  require_relative "microsandbox/microsandbox_rb"
13
17
  end
data/sig/microsandbox.rbs CHANGED
@@ -1,4 +1,13 @@
1
1
  # Type signatures for the public microsandbox Ruby API.
2
+ #
3
+ # Note: value, stream, and handle types (e.g. ExecOutput, Metrics, SandboxHandle,
4
+ # the Ssh* clients, AgentClient/AgentStream, PullSession, Snapshot*/Image*/Volume*)
5
+ # are constructed by the SDK from an internal native handle or data hash, not by
6
+ # user code — you receive them from SDK calls. Their `initialize` is documented
7
+ # below only to keep `new`'s synthesized signature accurate (RBS derives `new`
8
+ # from `initialize`; omitting it does NOT hide the constructor — it falls back to
9
+ # a misleading zero-arg `() -> instance` that Ruby actually rejects). Treat these
10
+ # constructors as internal.
2
11
 
3
12
  module Microsandbox
4
13
  VERSION: String
@@ -38,6 +47,11 @@ module Microsandbox
38
47
  class ImageNotFoundError < Error end
39
48
  class ImageInUseError < Error end
40
49
  class ImagePullFailedError < Error end
50
+ class SnapshotNotFoundError < Error end
51
+ class SnapshotAlreadyExistsError < Error end
52
+ class SnapshotSandboxRunningError < Error end
53
+ class SnapshotImageMissingError < Error end
54
+ class SnapshotIntegrityError < Error end
41
55
  class NetworkPolicyError < Error end
42
56
  class SecretViolationError < Error end
43
57
  class TlsError < Error end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: microsandbox-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao