microsandbox-rb 0.11.0 → 0.12.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: 05d5b2e1200b6aff7dc7801259558a202064a01cac529ea55f2390790b7f84bb
4
- data.tar.gz: d4d357393a51e2b642dd9d6fc6e899a6a92a3895bf98495a01714fbdbb76c0ed
3
+ metadata.gz: 72468dc2fa865510c82ecc783988925460acbd7f7d92025f367964277b98e756
4
+ data.tar.gz: f4bca46d64a694a76a59b712b8d827e6ddff12ba4cb9708a33951f4969c8fce2
5
5
  SHA512:
6
- metadata.gz: 994d487f8a606de2664566519ba28964fcc1053e9277b768945472dfac105d34a48479b1677d8598fde596b4d5c9f37a31970d2cf3e3a2065f98a12fdfe85322
7
- data.tar.gz: c0d57191ef9fc97845c773216dd19194dbad4dd0c58f3525989d16df0c37088749fe880ccd89b53d71f9f9454c149429886de208b36cda2de4210f23b4bc05f8
6
+ metadata.gz: 31cace38acfb4b535aaeb5f9026acb4c63d5f1376637628c2947e1d5ff9c1492d388cde85f3ba38bd8d462bb0598d12f046467331a6507d6af2cd2720bb57137
7
+ data.tar.gz: 97e665bd96d8d7ae0719c34d5a7d7f7823966a70f793d251d034a519ad04da70a8925c0e17c01c854ab074de904aa9a1baa88b148f099afba33c751836f97a60
data/CHANGELOG.md CHANGED
@@ -6,6 +6,50 @@ All notable changes to this gem are documented here. The format is based on
6
6
  microsandbox runtime it embeds; each release notes the upstream runtime tag it
7
7
  wraps, and the README's Versioning section keeps the full gem→runtime map.
8
8
 
9
+ ## [0.12.0] - 2026-07-30
10
+
11
+ Adopts upstream runtime **`v0.6.7` → `v0.6.8`** and mirrors its breaking SDK
12
+ surface (upstream #1232 "unify cloud backend and paginated listings"), keeping
13
+ parity with the Python/Node SDKs.
14
+
15
+ ### Breaking
16
+
17
+ - **Sandbox listing is cursor-paginated** (upstream #1232). `Sandbox.list` and
18
+ `Sandbox.list_with` now return a {Microsandbox::SandboxPage} — an Enumerable
19
+ page of `SandboxHandle`s carrying `#next_cursor` — instead of a plain Array.
20
+ `Sandbox.list` fetches the first page (upstream default size 20);
21
+ `Sandbox.list_with` gains `limit:` (1..100) and `cursor:` keywords alongside
22
+ the existing `labels:` filter. Code that only enumerates
23
+ (`list.each`/`map`/`to_a`) keeps working; code relying on the return value
24
+ *being* an Array (e.g. `list + other`, `Array ===`) must adapt, and listings
25
+ of more than one page must follow `page.next_cursor` via
26
+ `list_with(cursor:)`.
27
+ - **`UnsupportedError` messages are re-keyed by structured operations**
28
+ (upstream #1232): the core now reports the rejected API and a remedial hint
29
+ (e.g. `image.list is not supported by this backend: use a local backend`)
30
+ instead of the old free-text `feature`/`available_when` pair. The exception
31
+ additionally exposes the new structured attributes below.
32
+
33
+ ### Added
34
+
35
+ - {Microsandbox::SandboxPage} — Enumerable over its `#sandboxes`, plus
36
+ `#next_cursor`, `#size`/`#length`, `#empty?` and `#last_page?`.
37
+ - `Sandbox.list_with(labels:, limit:, cursor:)` pagination keywords.
38
+ - `UnsupportedError#operation` / `#hint` — the rejected API in Ruby rendering
39
+ (`"sandbox.kill"`) and the remedial hint (`"use a local backend"`),
40
+ mirroring the Python SDK's enriched `UnsupportedError`.
41
+
42
+ ### Changed
43
+
44
+ - Direct `VolumeFs` operations now route through the backend's volume trait
45
+ (upstream #1232) instead of a construction-time local-backend guard, so an
46
+ unsupported backend rejects each operation with a precise per-operation
47
+ error.
48
+ - The embedded runtime is `v0.6.8`; see the upstream release notes for
49
+ runtime-side changes (shared log registry for followed streams, cloud
50
+ sandbox reconnect fixes for `exec`/`ssh`, kernel sourced from
51
+ cdn.kernel.org).
52
+
9
53
  ## [0.11.0] - 2026-07-27
10
54
 
11
55
  Adopts upstream runtime **`v0.6.6` → `v0.6.7`** and mirrors its breaking SDK
data/Cargo.lock CHANGED
@@ -2954,8 +2954,8 @@ dependencies = [
2954
2954
 
2955
2955
  [[package]]
2956
2956
  name = "microsandbox"
2957
- version = "0.6.7"
2958
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
2957
+ version = "0.6.8"
2958
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
2959
2959
  dependencies = [
2960
2960
  "anyhow",
2961
2961
  "astral-tokio-tar",
@@ -2990,6 +2990,7 @@ dependencies = [
2990
2990
  "reqwest",
2991
2991
  "russh",
2992
2992
  "russh-sftp",
2993
+ "rustls",
2993
2994
  "scopeguard",
2994
2995
  "sea-orm",
2995
2996
  "serde",
@@ -3003,6 +3004,7 @@ dependencies = [
3003
3004
  "tracing",
3004
3005
  "typed-builder",
3005
3006
  "typed-path",
3007
+ "webpki-roots 1.0.7",
3006
3008
  "which",
3007
3009
  "windows-sys 0.61.2",
3008
3010
  "zeroize",
@@ -3010,8 +3012,8 @@ dependencies = [
3010
3012
 
3011
3013
  [[package]]
3012
3014
  name = "microsandbox-agent-client"
3013
- version = "0.6.7"
3014
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3015
+ version = "0.6.8"
3016
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3015
3017
  dependencies = [
3016
3018
  "ciborium",
3017
3019
  "microsandbox-protocol",
@@ -3023,8 +3025,8 @@ dependencies = [
3023
3025
 
3024
3026
  [[package]]
3025
3027
  name = "microsandbox-db"
3026
- version = "0.6.7"
3027
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3028
+ version = "0.6.8"
3029
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3028
3030
  dependencies = [
3029
3031
  "async-trait",
3030
3032
  "sea-orm",
@@ -3035,8 +3037,8 @@ dependencies = [
3035
3037
 
3036
3038
  [[package]]
3037
3039
  name = "microsandbox-filesystem"
3038
- version = "0.6.7"
3039
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3040
+ version = "0.6.8"
3041
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3040
3042
  dependencies = [
3041
3043
  "libc",
3042
3044
  "microsandbox-utils",
@@ -3048,8 +3050,8 @@ dependencies = [
3048
3050
 
3049
3051
  [[package]]
3050
3052
  name = "microsandbox-image"
3051
- version = "0.6.7"
3052
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3053
+ version = "0.6.8"
3054
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3053
3055
  dependencies = [
3054
3056
  "astral-tokio-tar",
3055
3057
  "async-compression",
@@ -3075,8 +3077,8 @@ dependencies = [
3075
3077
 
3076
3078
  [[package]]
3077
3079
  name = "microsandbox-metrics"
3078
- version = "0.6.7"
3079
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3080
+ version = "0.6.8"
3081
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3080
3082
  dependencies = [
3081
3083
  "chrono",
3082
3084
  "libc",
@@ -3087,8 +3089,8 @@ dependencies = [
3087
3089
 
3088
3090
  [[package]]
3089
3091
  name = "microsandbox-migration"
3090
- version = "0.6.7"
3091
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3092
+ version = "0.6.8"
3093
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3092
3094
  dependencies = [
3093
3095
  "sea-orm-migration",
3094
3096
  "serde_json",
@@ -3096,8 +3098,8 @@ dependencies = [
3096
3098
 
3097
3099
  [[package]]
3098
3100
  name = "microsandbox-network"
3099
- version = "0.6.7"
3100
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3101
+ version = "0.6.8"
3102
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3101
3103
  dependencies = [
3102
3104
  "base64",
3103
3105
  "bytes",
@@ -3140,8 +3142,8 @@ dependencies = [
3140
3142
 
3141
3143
  [[package]]
3142
3144
  name = "microsandbox-protocol"
3143
- version = "0.6.7"
3144
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3145
+ version = "0.6.8"
3146
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3145
3147
  dependencies = [
3146
3148
  "chrono",
3147
3149
  "ciborium",
@@ -3155,8 +3157,8 @@ dependencies = [
3155
3157
 
3156
3158
  [[package]]
3157
3159
  name = "microsandbox-runtime"
3158
- version = "0.6.7"
3159
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3160
+ version = "0.6.8"
3161
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3160
3162
  dependencies = [
3161
3163
  "bytes",
3162
3164
  "chrono",
@@ -3187,8 +3189,8 @@ dependencies = [
3187
3189
 
3188
3190
  [[package]]
3189
3191
  name = "microsandbox-types"
3190
- version = "0.6.7"
3191
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3192
+ version = "0.6.8"
3193
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3192
3194
  dependencies = [
3193
3195
  "chrono",
3194
3196
  "ipnetwork",
@@ -3201,8 +3203,8 @@ dependencies = [
3201
3203
 
3202
3204
  [[package]]
3203
3205
  name = "microsandbox-utils"
3204
- version = "0.6.7"
3205
- source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.7#0c54fd12acf7740f68d5bcd38e58a9fe82d51a45"
3206
+ version = "0.6.8"
3207
+ source = "git+https://github.com/superradcompany/microsandbox?tag=v0.6.8#7958cddeff2fce10dbcfd0c0d6a32dec93a9d5ea"
3206
3208
  dependencies = [
3207
3209
  "dirs",
3208
3210
  "libc",
@@ -3214,7 +3216,7 @@ dependencies = [
3214
3216
 
3215
3217
  [[package]]
3216
3218
  name = "microsandbox_rb"
3217
- version = "0.11.0"
3219
+ version = "0.12.0"
3218
3220
  dependencies = [
3219
3221
  "chrono",
3220
3222
  "futures",
@@ -6012,8 +6014,12 @@ checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
6012
6014
  dependencies = [
6013
6015
  "futures-util",
6014
6016
  "log",
6017
+ "rustls",
6018
+ "rustls-pki-types",
6015
6019
  "tokio",
6020
+ "tokio-rustls",
6016
6021
  "tungstenite",
6022
+ "webpki-roots 0.26.11",
6017
6023
  ]
6018
6024
 
6019
6025
  [[package]]
@@ -6170,6 +6176,8 @@ dependencies = [
6170
6176
  "httparse",
6171
6177
  "log",
6172
6178
  "rand 0.9.4",
6179
+ "rustls",
6180
+ "rustls-pki-types",
6173
6181
  "sha1 0.10.6",
6174
6182
  "thiserror 2.0.18",
6175
6183
  ]
data/README.md CHANGED
@@ -126,10 +126,13 @@ ensure
126
126
  # sb.kill # force (SIGKILL); sb.drain for a graceful drain
127
127
  end
128
128
 
129
- # Inspect / manage existing sandboxes. `get`/`list` return a controllable
129
+ # Inspect / manage existing sandboxes. `get` returns a controllable
130
130
  # SandboxHandle (the live `stop`/`kill`/`drain`/`wait` live on the object from
131
- # `create`/`start`; fine-grained control lives on the handle).
132
- Microsandbox::Sandbox.list # => [Microsandbox::SandboxHandle, ...]
131
+ # `create`/`start`; fine-grained control lives on the handle). `list` returns a
132
+ # cursor-paginated, Enumerable SandboxPage of handles (runtime v0.6.8).
133
+ Microsandbox::Sandbox.list # => Microsandbox::SandboxPage (first page)
134
+ Microsandbox::Sandbox.list.map(&:name) # enumerate the page's handles
135
+ # next page: Sandbox.list_with(cursor: page.next_cursor, limit: 50)
133
136
  h = Microsandbox::Sandbox.get("box") # => Microsandbox::SandboxHandle
134
137
  h.status # :running, :stopped, :created, ...
135
138
  h.stop_with_timeout(5) # custom escalation timeout
@@ -442,8 +445,8 @@ change diverged the two numbers — the gem version is **not** a reliable indica
442
445
  of the embedded runtime version. To learn which runtime a build wraps, ask it:
443
446
 
444
447
  ```ruby
445
- Microsandbox::VERSION # => "0.11.0" (the gem's own version)
446
- Microsandbox.runtime_version # => "v0.6.7" (the embedded upstream runtime tag)
448
+ Microsandbox::VERSION # => "0.12.0" (the gem's own version)
449
+ Microsandbox.runtime_version # => "v0.6.8" (the embedded upstream runtime tag)
447
450
  ```
448
451
 
449
452
  | Gem version | Upstream runtime | Notes |
@@ -465,6 +468,7 @@ Microsandbox.runtime_version # => "v0.6.7" (the embedded upstream runtime tag
465
468
  | `0.9.3` | `v0.6.6` | adopts upstream `v0.6.4`+`v0.6.6` (`v0.6.5` was yanked upstream): snapshot restore by pinned digest — fixes fatal restore-after-tag-republish bug, fragmented-UDP/PMTU relay fixes, exec kills the whole process group, ephemeral stop-wait tolerance, readdir RSS-leak fix; upstream API growth is additive-only — no Ruby surface change |
466
469
  | `0.10.0` | `v0.6.6` | `v0.6.6` API parity: live `modify`/resize, `ping`/`touch`, create `max_cpus`/`max_memory` |
467
470
  | `0.11.0` | `v0.6.7` | adopts upstream `v0.6.7` (**breaking**): network profiles replace `public_only`/`non_local`, structured `root_disk:` replaces `oci_upper_size:` (deprecated alias kept), snapshot descriptor contract (`create` re-keyed by name, `save`/`load` rename, `snapshot_to` removed, on-disk auto-migration), `Image.load`/`Image.save`, `follow_root_symlinks:`; runtime carries the GHSA-4vq3-cjpp-v7fg `msb copy` fix |
471
+ | `0.12.0` | `v0.6.8` | adopts upstream `v0.6.8` (**breaking**): `Sandbox.list`/`.list_with` return a cursor-paginated `SandboxPage` (`limit:`/`cursor:` keywords), `UnsupportedError` re-keyed by structured operations with `#operation`/`#hint`; runtime adds a shared log registry for followed streams and cloud exec/ssh reconnects |
468
472
 
469
473
  **Going forward** — the gem version moves on its own semver track and no longer
470
474
  mirrors the upstream tag:
@@ -544,7 +548,8 @@ lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
544
548
  `status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
545
549
  `stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
546
550
  `wait_until_stopped`/`config`/`config_json`/`snapshot` from
547
- `Sandbox.get`, and label-filtered `list_with`),
551
+ `Sandbox.get`, and the cursor-paginated `list`/`list_with` with label
552
+ filters),
548
553
  backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
549
554
  `exec`/`shell` (collected and streaming), interactive `attach`/
550
555
  `attach_shell`, the full guest filesystem (incl. streaming `read_stream`/
@@ -7,7 +7,7 @@ description = "Ruby SDK native extension for microsandbox — secure, fast micro
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
9
  # The core-crate dependency below stays pinned at its own tag (v0.6.7).
10
- version = "0.11.0"
10
+ version = "0.12.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.6.7", default-features = true, features = ["ssh"] }
39
- microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.7" }
38
+ microsandbox = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.8", default-features = true, features = ["ssh"] }
39
+ microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.8" }
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"] }
@@ -20,7 +20,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
20
20
  use std::sync::{Arc, Mutex, OnceLock};
21
21
 
22
22
  use magnus::{function, prelude::*, Error, RModule, Ruby};
23
- use microsandbox::{Backend, MicrosandboxError};
23
+ use microsandbox::{Backend, MicrosandboxError, Operation};
24
24
 
25
25
  use crate::error;
26
26
  use crate::runtime::block_on;
@@ -31,17 +31,14 @@ use crate::runtime::block_on;
31
31
  /// borrowing `&LocalBackend` from it (`as_local()` borrows the `Arc`). Pure
32
32
  /// Rust — safe to call inside `block_on` (no Ruby C API), and it returns a raw
33
33
  /// `MicrosandboxError` so the Ruby-exception mapping happens *after* `block_on`
34
- /// re-acquires the GVL. Cloud backends yield `Unsupported`, mirroring pyo3's
35
- /// `resolve_local`.
36
- pub fn local_backend() -> Result<Arc<dyn Backend>, MicrosandboxError> {
34
+ /// re-acquires the GVL. Cloud backends yield `Unsupported` for the given
35
+ /// operation (v0.6.8 `Operation`-keyed shape), mirroring pyo3's `resolve_local`.
36
+ pub fn local_backend(op: Operation) -> Result<Arc<dyn Backend>, MicrosandboxError> {
37
37
  let backend = microsandbox::default_backend();
38
38
  if backend.as_local().is_some() {
39
39
  Ok(backend)
40
40
  } else {
41
- Err(MicrosandboxError::Unsupported {
42
- feature: "this operation requires a local backend".into(),
43
- available_when: "with the local backend (the default)".into(),
44
- })
41
+ Err(MicrosandboxError::local_only(op))
45
42
  }
46
43
  }
47
44
 
@@ -53,10 +50,11 @@ pub fn local_backend() -> Result<Arc<dyn Backend>, MicrosandboxError> {
53
50
  /// it and returns the same `Arc` kept alive for the borrow — so it lives here
54
51
  /// once rather than at every call site.
55
52
  pub fn with_local_backend<T>(
53
+ operation: Operation,
56
54
  op: impl AsyncFnOnce(&microsandbox::LocalBackend) -> Result<T, MicrosandboxError>,
57
55
  ) -> Result<T, Error> {
58
56
  block_on(async move {
59
- let backend = local_backend()?;
57
+ let backend = local_backend(operation)?;
60
58
  let local = backend
61
59
  .as_local()
62
60
  .expect("local_backend() guarantees a local backend");
@@ -6,7 +6,7 @@
6
6
  //! is always the core error's `to_string()`.
7
7
 
8
8
  use magnus::{value::ReprValue, Error, ExceptionClass, Module, RClass, RModule, Ruby};
9
- use microsandbox::{AgentClientError, MicrosandboxError};
9
+ use microsandbox::{AgentClientError, MicrosandboxError, Operation, UnsupportedReason};
10
10
 
11
11
  /// The Ruby class (relative to the `Microsandbox` module) for a core error.
12
12
  /// `"Error"` is the base class; anything else is a named subclass.
@@ -83,12 +83,96 @@ pub fn to_ruby(err: MicrosandboxError) -> Error {
83
83
  Err(_) => return Error::new(magnus::exception::runtime_error(), message),
84
84
  };
85
85
 
86
+ // `Unsupported` gets a Ruby-idiom message (`sandbox.kill` instead of
87
+ // `Sandbox::kill`) plus structured `operation` / `hint` attributes on the
88
+ // exception instance, mirroring the Python SDK's enrichment (v0.6.8).
89
+ if let MicrosandboxError::Unsupported { op, reason } = &err {
90
+ return unsupported_error(&ruby, &ruby_api_name(*op), &ruby_hint(reason));
91
+ }
92
+
86
93
  match exception_class(&ruby, class_name(&err)) {
87
94
  Some(class) => Error::new(class, message),
88
95
  None => Error::new(ruby.exception_runtime_error(), message),
89
96
  }
90
97
  }
91
98
 
99
+ /// `UnsupportedError` for shim-only entry points that require the local
100
+ /// backend but have no SDK [`Operation`] (Ruby-only diagnostic hooks such as
101
+ /// `Microsandbox.runtime_path`). `name` is the Ruby-facing API name. Mirrors
102
+ /// the Python SDK's name-based `local_only` helper.
103
+ #[allow(deprecated)]
104
+ pub fn local_only(name: &str) -> Error {
105
+ match Ruby::get() {
106
+ Ok(ruby) => unsupported_error(&ruby, name, "use a local backend"),
107
+ Err(_) => Error::new(
108
+ magnus::exception::runtime_error(),
109
+ format!("{name} is not supported by this backend: use a local backend"),
110
+ ),
111
+ }
112
+ }
113
+
114
+ /// Build a `Microsandbox::UnsupportedError` carrying the rendered message and
115
+ /// the structured `@operation` / `@hint` attributes.
116
+ fn unsupported_error(ruby: &Ruby, operation: &str, hint: &str) -> Error {
117
+ let message = format!("{operation} is not supported by this backend: {hint}");
118
+ let Some(class) = exception_class(ruby, "UnsupportedError") else {
119
+ return Error::new(ruby.exception_runtime_error(), message);
120
+ };
121
+ match class
122
+ .as_value()
123
+ .funcall::<_, _, magnus::Exception>("new", (message.as_str(),))
124
+ {
125
+ Ok(exc) => {
126
+ // Best-effort extras; the message already carries both.
127
+ let _ = exc
128
+ .funcall::<_, _, magnus::Value>("instance_variable_set", ("@operation", operation));
129
+ let _ = exc.funcall::<_, _, magnus::Value>("instance_variable_set", ("@hint", hint));
130
+ exc.into()
131
+ }
132
+ Err(_) => Error::new(class, message),
133
+ }
134
+ }
135
+
136
+ /// Render an [`Operation`] as the Ruby API it corresponds to: `Sandbox::kill`
137
+ /// becomes `sandbox.kill` and `SandboxFsOps::stat_handle` becomes
138
+ /// `sandbox_fs_ops.stat_handle`; a parenthetical keeps its Ruby keyword shape
139
+ /// (`log_stream(follow=false)` becomes `log_stream(follow: false)`). Plain
140
+ /// phrases without a `Type::method` shape (`config`, `snapshot operations`)
141
+ /// pass through as-is. Mirrors the Python SDK's `py_api_name`.
142
+ fn ruby_api_name(op: Operation) -> String {
143
+ let path = op.api_path();
144
+ let Some((ty, method)) = path.split_once("::") else {
145
+ return path.to_string();
146
+ };
147
+ format!("{}.{}", camel_to_snake(ty), method.replace('=', ": "))
148
+ }
149
+
150
+ /// Render an [`UnsupportedReason`] with `use instead` targets pointing at the
151
+ /// Ruby API name rather than the Rust path.
152
+ fn ruby_hint(reason: &UnsupportedReason) -> String {
153
+ match reason {
154
+ UnsupportedReason::UseInstead(op) => format!("use {}", ruby_api_name(*op)),
155
+ other => other.hint(),
156
+ }
157
+ }
158
+
159
+ /// Lower a `CamelCase` type name to `snake_case` (`SandboxFsOps` becomes
160
+ /// `sandbox_fs_ops`).
161
+ fn camel_to_snake(name: &str) -> String {
162
+ let mut out = String::with_capacity(name.len() + 4);
163
+ for (i, ch) in name.char_indices() {
164
+ if ch.is_ascii_uppercase() {
165
+ if i > 0 {
166
+ out.push('_');
167
+ }
168
+ out.push(ch.to_ascii_lowercase());
169
+ } else {
170
+ out.push(ch);
171
+ }
172
+ }
173
+ out
174
+ }
175
+
92
176
  /// A plain `Microsandbox::Error` (base) with a custom message — used for
93
177
  /// binding-level validation errors that have no core counterpart.
94
178
  #[allow(deprecated)]
@@ -7,7 +7,7 @@
7
7
 
8
8
  use magnus::{function, prelude::*, Error, RArray, RHash, RModule, Ruby};
9
9
  use microsandbox::image::{Image, ImageDetail, ImageHandle, ImagePruneReport};
10
- use microsandbox::ImageArchiveFormat;
10
+ use microsandbox::{ImageArchiveFormat, Operation};
11
11
 
12
12
  use crate::backend::with_local_backend;
13
13
  use crate::conv;
@@ -84,12 +84,16 @@ fn report_to_hash(report: ImagePruneReport) -> RHash {
84
84
  }
85
85
 
86
86
  fn get(reference: String) -> Result<RHash, Error> {
87
- let handle = with_local_backend(async |local| Image::get_local(local, &reference).await)?;
87
+ let handle = with_local_backend(Operation::ImageGet, async |local| {
88
+ Image::get_local(local, &reference).await
89
+ })?;
88
90
  Ok(handle_to_hash(&handle))
89
91
  }
90
92
 
91
93
  fn list() -> Result<RArray, Error> {
92
- let handles = with_local_backend(async |local| Image::list_local(local).await)?;
94
+ let handles = with_local_backend(Operation::ImageList, async |local| {
95
+ Image::list_local(local).await
96
+ })?;
93
97
  let arr = ruby().ary_new();
94
98
  for h in handles.iter() {
95
99
  arr.push(handle_to_hash(h))?;
@@ -98,16 +102,22 @@ fn list() -> Result<RArray, Error> {
98
102
  }
99
103
 
100
104
  fn inspect(reference: String) -> Result<RHash, Error> {
101
- let detail = with_local_backend(async |local| Image::inspect_local(local, &reference).await)?;
105
+ let detail = with_local_backend(Operation::ImageInspect, async |local| {
106
+ Image::inspect_local(local, &reference).await
107
+ })?;
102
108
  Ok(detail_to_hash(detail))
103
109
  }
104
110
 
105
111
  fn remove(reference: String, force: bool) -> Result<(), Error> {
106
- with_local_backend(async |local| Image::remove_local(local, &reference, force).await)
112
+ with_local_backend(Operation::ImageRemove, async |local| {
113
+ Image::remove_local(local, &reference, force).await
114
+ })
107
115
  }
108
116
 
109
117
  fn prune() -> Result<RHash, Error> {
110
- let report = with_local_backend(async |local| Image::prune_local(local).await)?;
118
+ let report = with_local_backend(Operation::ImagePrune, async |local| {
119
+ Image::prune_local(local).await
120
+ })?;
111
121
  Ok(report_to_hash(report))
112
122
  }
113
123
 
@@ -117,7 +127,7 @@ fn prune() -> Result<RHash, Error> {
117
127
  /// `imageLoad` added in v0.6.7. (The `"-"` stdin form is spooled to a temp
118
128
  /// file by the Ruby layer — the core reads seekable files only.)
119
129
  fn load(input_path: String, tags: Vec<String>) -> Result<RArray, Error> {
120
- let handles = with_local_backend(async |local| {
130
+ let handles = with_local_backend(Operation::ImageLoad, async |local| {
121
131
  Image::load_local(local, std::path::Path::new(&input_path), tags).await
122
132
  })?;
123
133
  let arr = ruby().ary_new();
@@ -140,7 +150,7 @@ fn save(references: Vec<String>, output_path: String, format: String) -> Result<
140
150
  )))
141
151
  }
142
152
  };
143
- with_local_backend(async |local| {
153
+ with_local_backend(Operation::ImageSave, async |local| {
144
154
  Image::save_local(local, &references, std::path::Path::new(&output_path), fmt).await
145
155
  })
146
156
  }
@@ -33,9 +33,10 @@ fn version() -> String {
33
33
  /// Ruby Hash. Mirrors the official `all_sandbox_metrics` / `allSandboxMetrics`
34
34
  /// helpers (Python/Node/Go).
35
35
  fn all_sandbox_metrics() -> Result<RHash, Error> {
36
- let map = backend::with_local_backend(async |local| {
37
- microsandbox::sandbox::all_sandbox_metrics_local(local).await
38
- })?;
36
+ let map =
37
+ backend::with_local_backend(microsandbox::Operation::AllSandboxMetrics, async |local| {
38
+ microsandbox::sandbox::all_sandbox_metrics_local(local).await
39
+ })?;
39
40
  let hash = runtime::ruby().hash_new();
40
41
  for (name, metrics) in &map {
41
42
  hash.aset(name.as_str(), sandbox::metrics_to_hash(metrics))?;
@@ -107,12 +108,11 @@ fn set_runtime_libkrunfw_path(path: String) {
107
108
  /// backend's config.
108
109
  fn resolved_msb_path() -> Result<String, Error> {
109
110
  let backend = microsandbox::default_backend();
110
- let local = backend.as_local().ok_or_else(|| {
111
- error::to_ruby(microsandbox::MicrosandboxError::Unsupported {
112
- feature: "resolved_msb_path requires a local backend".into(),
113
- available_when: "with the local backend".into(),
114
- })
115
- })?;
111
+ // Shim-only entry point with no SDK `Operation` — report the Ruby-facing
112
+ // name (`Microsandbox.runtime_path`), like Python's name-based local_only.
113
+ let local = backend
114
+ .as_local()
115
+ .ok_or_else(|| error::local_only("runtime_path"))?;
116
116
  let path = local.config().resolve_msb_path().map_err(error::to_ruby)?;
117
117
  Ok(path.to_string_lossy().into_owned())
118
118
  }
@@ -20,9 +20,9 @@ use microsandbox::logs::{
20
20
  use microsandbox::sandbox::{
21
21
  AttachOptionsBuilder, DiskImageFormat, EnvVar, FsEntry, FsEntryKind, FsMetadata,
22
22
  HostPermissions, Patch, PullPolicy, PullProgress, PullProgressHandle, RlimitResource,
23
- RootDiskBuilder, SandboxBuilder, SandboxFilter, SandboxHandle, SandboxMetrics,
24
- SandboxModificationBuilder, SandboxModificationPatch, SandboxStatus, SandboxStopResult,
25
- SecretBuilder, SecretModificationPatch, SecretSource, SecurityProfile, StatVirtualization,
23
+ RootDiskBuilder, SandboxBuilder, SandboxHandle, SandboxMetrics, SandboxModificationBuilder,
24
+ SandboxModificationPatch, SandboxStatus, SandboxStopResult, SecretBuilder,
25
+ SecretModificationPatch, SecretSource, SecurityProfile, StatVirtualization,
26
26
  };
27
27
  use microsandbox::LogLevel;
28
28
  use microsandbox::MicrosandboxResult;
@@ -477,29 +477,44 @@ impl Sandbox {
477
477
  Ok(SbHandle::from_inner(handle))
478
478
  }
479
479
 
480
- /// All sandboxes as controllable handles.
481
- fn list() -> Result<RArray, Error> {
482
- let handles = block_on(microsandbox::Sandbox::list()).map_err(error::to_ruby)?;
480
+ /// Convert one `SandboxPage` into a `{ "sandboxes" => [SbHandle], "next_cursor" => String|nil }`
481
+ /// Ruby Hash (v0.6.8 paginated listing contract).
482
+ fn page_to_hash(page: microsandbox::sandbox::SandboxPage) -> Result<RHash, Error> {
483
483
  let arr = ruby().ary_new();
484
- for h in handles {
484
+ for h in page.sandboxes {
485
485
  arr.push(SbHandle::from_inner(h))?;
486
486
  }
487
- Ok(arr)
488
- }
489
-
490
- /// Sandboxes filtered by required `key=value` labels (AND-matched), as
491
- /// controllable handles. `opts` carries a string→string `labels` map.
492
- fn list_with(opts: RHash) -> Result<RArray, Error> {
493
- let mut filter = SandboxFilter::new();
494
- for (k, v) in conv::opt_string_map(opts, "labels")? {
495
- filter = filter.label(k, v);
496
- }
497
- let handles = block_on(microsandbox::Sandbox::list_with(filter)).map_err(error::to_ruby)?;
498
- let arr = ruby().ary_new();
499
- for h in handles {
500
- arr.push(SbHandle::from_inner(h))?;
501
- }
502
- Ok(arr)
487
+ let hash = ruby().hash_new();
488
+ hash.aset("sandboxes", arr)?;
489
+ hash.aset("next_cursor", page.next_cursor)?;
490
+ Ok(hash)
491
+ }
492
+
493
+ /// First page of sandboxes (default page size), as a page Hash of
494
+ /// controllable handles.
495
+ fn list() -> Result<RHash, Error> {
496
+ let page = block_on(microsandbox::Sandbox::list()).map_err(error::to_ruby)?;
497
+ Self::page_to_hash(page)
498
+ }
499
+
500
+ /// One configured page of sandboxes as a page Hash. `opts` carries an
501
+ /// optional string→string `labels` map (AND-matched), an optional `limit`
502
+ /// (1..=100) and an optional opaque `cursor` from a previous page.
503
+ fn list_with(opts: RHash) -> Result<RHash, Error> {
504
+ let labels = conv::opt_string_map(opts, "labels")?;
505
+ let limit = conv::opt_u32(opts, "limit")?;
506
+ let cursor = conv::opt_string(opts, "cursor")?;
507
+ let page = block_on(microsandbox::Sandbox::list_with(move |mut b| {
508
+ if let Some(limit) = limit {
509
+ b = b.limit(limit);
510
+ }
511
+ if let Some(cursor) = cursor {
512
+ b = b.cursor(cursor);
513
+ }
514
+ b.labels(labels)
515
+ }))
516
+ .map_err(error::to_ruby)?;
517
+ Self::page_to_hash(page)
503
518
  }
504
519
 
505
520
  /// Remove a (stopped) sandbox by name.
@@ -9,7 +9,6 @@ use magnus::{function, method, prelude::*, Error, RArray, RHash, RModule, RStrin
9
9
  use microsandbox::volume::VolumeHandle;
10
10
  use microsandbox::Backend;
11
11
 
12
- use crate::backend::local_backend;
13
12
  use crate::conv;
14
13
  use crate::error;
15
14
  use crate::runtime::{block_on, ruby};
@@ -108,10 +107,13 @@ pub struct VolumeFs {
108
107
  }
109
108
 
110
109
  impl VolumeFs {
111
- /// Resolve the (local) backend once and bind it to `name`.
110
+ /// Resolve the ambient backend once and bind it to `name`. As of v0.6.8
111
+ /// every `VolumeFs` op dispatches through the backend's `VolumeBackend`
112
+ /// trait, which yields precise per-operation `Unsupported` errors on
113
+ /// backends that can't serve it — no local-only downcast needed here.
112
114
  fn for_volume(name: String) -> Result<VolumeFs, Error> {
113
115
  Ok(VolumeFs {
114
- backend: local_backend().map_err(error::to_ruby)?,
116
+ backend: microsandbox::default_backend(),
115
117
  name,
116
118
  })
117
119
  }
@@ -90,4 +90,16 @@ module Microsandbox
90
90
  # cloud backend) is distinct from `UnsupportedOperationError` above.
91
91
  define_error(:CloudHttpError, "cloud-http")
92
92
  define_error(:UnsupportedError, "unsupported")
93
+
94
+ # As of runtime v0.6.8 the core keys Unsupported errors by a structured
95
+ # (operation, reason) pair. The native layer renders both into the message
96
+ # ("sandbox.kill is not supported by this backend: ...") and also attaches
97
+ # them here as structured attributes, mirroring the Python SDK's
98
+ # `UnsupportedError.operation` / `.hint`.
99
+ class UnsupportedError
100
+ # @return [String, nil] the rejected API in Ruby rendering, e.g. "sandbox.kill"
101
+ attr_reader :operation
102
+ # @return [String, nil] why it was rejected / what to use instead
103
+ attr_reader :hint
104
+ end
93
105
  end
@@ -202,6 +202,48 @@ module Microsandbox
202
202
  end
203
203
  end
204
204
 
205
+ # One page of sandbox handles, returned by {Sandbox.list} and
206
+ # {Sandbox.list_with} — sandbox listing is cursor-paginated as of runtime
207
+ # v0.6.8. Enumerable over its {SandboxHandle}s; fetch the following page by
208
+ # passing {#next_cursor} to {Sandbox.list_with}.
209
+ #
210
+ # @example Walk every page
211
+ # page = Microsandbox::Sandbox.list
212
+ # loop do
213
+ # page.each { |h| puts h.name }
214
+ # break if page.last_page?
215
+ # page = Microsandbox::Sandbox.list_with(cursor: page.next_cursor)
216
+ # end
217
+ class SandboxPage
218
+ include Enumerable
219
+
220
+ # @return [Array<SandboxHandle>] the sandboxes in this page
221
+ attr_reader :sandboxes
222
+ # @return [String, nil] opaque cursor for the next page; nil on the final page
223
+ attr_reader :next_cursor
224
+
225
+ # @api private — construct via {Sandbox.list}/{Sandbox.list_with}.
226
+ def initialize(sandboxes, next_cursor)
227
+ @sandboxes = sandboxes
228
+ @next_cursor = next_cursor
229
+ end
230
+
231
+ def each(&) = @sandboxes.each(&)
232
+
233
+ # @return [Integer]
234
+ def size = @sandboxes.size
235
+ alias_method :length, :size
236
+
237
+ def empty? = @sandboxes.empty?
238
+
239
+ # Whether this is the final page (no cursor to continue from).
240
+ def last_page? = @next_cursor.nil?
241
+
242
+ def inspect
243
+ "#<Microsandbox::SandboxPage size=#{size} next_cursor=#{@next_cursor.inspect}>"
244
+ end
245
+ end
246
+
205
247
  # A running sandbox (microVM) — the primary entry point of the SDK.
206
248
  #
207
249
  # @example Block form (auto-stops on exit)
@@ -478,18 +520,25 @@ module Microsandbox
478
520
  SandboxHandle.new(Native::Sandbox.get(name.to_s))
479
521
  end
480
522
 
481
- # List all sandboxes as controllable handles.
482
- # @return [Array<SandboxHandle>]
523
+ # List the first page of sandboxes (default page size 20) as controllable
524
+ # handles. Cursor-paginated as of runtime v0.6.8 — follow
525
+ # {SandboxPage#next_cursor} via {.list_with} for subsequent pages.
526
+ # @return [SandboxPage]
483
527
  def list
484
- Native::Sandbox.list.map { |h| SandboxHandle.new(h) }
528
+ build_page(Native::Sandbox.list)
485
529
  end
486
530
 
487
- # List sandboxes carrying all of the given labels (AND-matched).
488
- # @param labels [Hash] required key => value labels
489
- # @return [Array<SandboxHandle>]
490
- def list_with(labels: {})
531
+ # List one configured page of sandboxes.
532
+ # @param labels [Hash] required key => value labels (AND-matched)
533
+ # @param limit [Integer, nil] page size, 1..100 (upstream default 20)
534
+ # @param cursor [String, nil] opaque cursor from a previous page's
535
+ # {SandboxPage#next_cursor}
536
+ # @return [SandboxPage]
537
+ def list_with(labels: {}, limit: nil, cursor: nil)
491
538
  opts = {"labels" => stringify(labels)}
492
- Native::Sandbox.list_with(opts).map { |h| SandboxHandle.new(h) }
539
+ opts["limit"] = Integer(limit) if limit
540
+ opts["cursor"] = cursor.to_s if cursor
541
+ build_page(Native::Sandbox.list_with(opts))
493
542
  end
494
543
 
495
544
  # Remove a (stopped) sandbox by name.
@@ -518,6 +567,15 @@ module Microsandbox
518
567
  hash.each_with_object({}) { |(k, v), acc| acc[k.to_s] = v.to_s }
519
568
  end
520
569
 
570
+ # Wrap a native page Hash ({"sandboxes" => [...], "next_cursor" => ...})
571
+ # into a {SandboxPage} of {SandboxHandle}s.
572
+ def build_page(data)
573
+ SandboxPage.new(
574
+ data["sandboxes"].map { |h| SandboxHandle.new(h) },
575
+ data["next_cursor"]
576
+ )
577
+ end
578
+
521
579
  def intify_ports(ports)
522
580
  ports.each_with_object({}) { |(k, v), acc| acc[Integer(k)] = Integer(v) }
523
581
  end
@@ -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.11.0"
11
+ VERSION = "0.12.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.6.7"
18
+ RUNTIME_VERSION = "v0.6.8"
19
19
  end
data/sig/microsandbox.rbs CHANGED
@@ -61,7 +61,10 @@ module Microsandbox
61
61
  class MetricsUnavailableError < Error end
62
62
  class UnsupportedOperationError < Error end
63
63
  class CloudHttpError < Error end
64
- class UnsupportedError < Error end
64
+ class UnsupportedError < Error
65
+ def operation: () -> String?
66
+ def hint: () -> String?
67
+ end
65
68
 
66
69
  class ExecOutput
67
70
  def initialize: (Hash[String, untyped] data) -> void
@@ -198,6 +201,19 @@ module Microsandbox
198
201
  def observed_at: () -> Time
199
202
  end
200
203
 
204
+ class SandboxPage
205
+ include Enumerable[SandboxHandle]
206
+ def initialize: (Array[SandboxHandle] sandboxes, String? next_cursor) -> void
207
+ def sandboxes: () -> Array[SandboxHandle]
208
+ def next_cursor: () -> String?
209
+ def each: () { (SandboxHandle) -> void } -> Array[SandboxHandle]
210
+ | () -> Enumerator[SandboxHandle, Array[SandboxHandle]]
211
+ def size: () -> Integer
212
+ def length: () -> Integer
213
+ def empty?: () -> bool
214
+ def last_page?: () -> bool
215
+ end
216
+
201
217
  class PingResult
202
218
  def initialize: (Hash[String, untyped] data) -> void
203
219
  def name: () -> String
@@ -251,8 +267,8 @@ module Microsandbox
251
267
  def self.create_with_progress: (String name, **untyped) -> PullSession
252
268
  def self.start: (String name, ?detached: bool) -> Sandbox
253
269
  def self.get: (String name) -> SandboxHandle
254
- def self.list: () -> Array[SandboxHandle]
255
- def self.list_with: (?labels: Hash[untyped, untyped]) -> Array[SandboxHandle]
270
+ def self.list: () -> SandboxPage
271
+ def self.list_with: (?labels: Hash[untyped, untyped], ?limit: Integer?, ?cursor: String?) -> SandboxPage
256
272
  def self.remove: (String name) -> nil
257
273
 
258
274
  def name: () -> String
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.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao