microsandbox-rb 0.11.0 โ†’ 0.13.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.
data/README.md CHANGED
@@ -20,7 +20,13 @@ them. Our deepest thanks to the maintainers and community. ๐Ÿ™
20
20
  [Rust](https://github.com/superradcompany/microsandbox/tree/main/sdk) ยท
21
21
  [Python](https://github.com/superradcompany/microsandbox/tree/main/sdk/python) ยท
22
22
  [TypeScript / Node](https://github.com/superradcompany/microsandbox/tree/main/sdk/node-ts) ยท
23
- [Go](https://github.com/superradcompany/microsandbox/tree/main/sdk/go)
23
+ [Go](https://github.com/superradcompany/microsandbox/tree/main/sdk/go) ยท
24
+ [Ruby](https://github.com/superradcompany/microsandbox/tree/main/sdk/ruby)
25
+ (since upstream `v0.6.9` there is an **official** `microsandbox` gem โ€” a
26
+ compact veneer over the same Rust SDK. This gem predates it and covers a
27
+ larger surface (snapshots, SSH, streaming, volumes fs, network policy DSL,
28
+ RBS types); both define the `Microsandbox` module, so use one or the other,
29
+ not both, in a single process.)
24
30
  - **Agents** โ€” [Agent Skills](https://github.com/superradcompany/skills) ยท [MCP server](https://github.com/superradcompany/microsandbox-mcp)
25
31
  - **Community** โ€” [Discord](https://discord.gg/T95Y3XnEAK)
26
32
 
@@ -126,10 +132,13 @@ ensure
126
132
  # sb.kill # force (SIGKILL); sb.drain for a graceful drain
127
133
  end
128
134
 
129
- # Inspect / manage existing sandboxes. `get`/`list` return a controllable
135
+ # Inspect / manage existing sandboxes. `get` returns a controllable
130
136
  # 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, ...]
137
+ # `create`/`start`; fine-grained control lives on the handle). `list` returns a
138
+ # cursor-paginated, Enumerable SandboxPage of handles (runtime v0.6.8).
139
+ Microsandbox::Sandbox.list # => Microsandbox::SandboxPage (first page)
140
+ Microsandbox::Sandbox.list.map(&:name) # enumerate the page's handles
141
+ # next page: Sandbox.list_with(cursor: page.next_cursor, limit: 50)
133
142
  h = Microsandbox::Sandbox.get("box") # => Microsandbox::SandboxHandle
134
143
  h.status # :running, :stopped, :created, ...
135
144
  h.stop_with_timeout(5) # custom escalation timeout
@@ -190,6 +199,22 @@ end
190
199
  A non-zero exit is **not** an error โ€” inspect `exit_code`/`success?`. Spawn-time
191
200
  failures (e.g. command not found) and timeouts raise typed errors (see below).
192
201
 
202
+ **Default workload** (runtime `v0.6.9`): `create` is strictly boot-only โ€” it
203
+ never runs the image's `ENTRYPOINT`/`CMD`. Execute the image's own command
204
+ explicitly:
205
+
206
+ ```ruby
207
+ Microsandbox::Sandbox.create("worker", image: "example/worker:latest",
208
+ cmd: ["worker.py", "--once"]) do |sb| # cmd: overrides the durable image CMD
209
+ out = sb.exec_default(timeout: 300) # buffered; exec-style options
210
+ handle = sb.exec_default_stream # or streaming (returns an ExecHandle)
211
+ sb.attach_default # or interactive (host TTY)
212
+ end
213
+ ```
214
+
215
+ An image whose entrypoint and CMD resolve to no executable command raises
216
+ `Microsandbox::NoDefaultCommandError`.
217
+
193
218
  ### Guest filesystem
194
219
 
195
220
  ```ruby
@@ -253,6 +278,10 @@ Microsandbox::Sandbox.create("live", image: "public.ecr.aws/docker/library/alpin
253
278
  # Live resize โ€” applies to the running VM under the default :no_restart policy:
254
279
  sb.modify(cpus: 2, memory: 1024)
255
280
 
281
+ # Grow the root disk (managed upper or flat, MiB โ€” runtime v0.6.9). Applied
282
+ # while stopped; growth-only:
283
+ sb.modify(root_disk_size: 8192, policy: :next_start)
284
+
256
285
  # env/labels/workdir changes on a *running* sandbox require a restart, so the
257
286
  # default :no_restart policy rejects the whole apply (it raises rather than
258
287
  # partially applying). Persist them for the next start โ€” or restart now โ€”
@@ -427,9 +456,15 @@ Microsandbox.with_backend(:local) { Microsandbox::Sandbox.create("box", image: "
427
456
  ```
428
457
 
429
458
  Resolution order when no backend is set programmatically: `MSB_BACKEND`
430
- (`local`/`cloud`) โ†’ `MSB_API_URL` + `MSB_API_KEY` โ†’ `MSB_PROFILE` โ†’ the
431
- `active_profile` in `~/.microsandbox/config.json` (path overridable via
432
- `MSB_CONFIG_PATH`) โ†’ local. The cloud backend currently supports a subset of
459
+ (`local`/`cloud`) โ†’ `MSB_PROFILE` โ†’ the `active_profile` in
460
+ `~/.microsandbox/config.json` (path overridable via `MSB_CONFIG_PATH`) โ†’
461
+ local. **Cloud intent must be explicit** (since runtime `v0.6.9`): a bare
462
+ `MSB_API_KEY` is treated as credential material, not backend intent, and no
463
+ longer selects the cloud on its own โ€” pair it with `MSB_BACKEND=cloud` (which
464
+ reads `MSB_API_URL`/`MSB_API_KEY`), or select a cloud profile. Invalid cloud
465
+ configuration (e.g. `MSB_BACKEND=cloud` without a usable API key or cloud
466
+ profile) fails closed with `Microsandbox::InvalidConfigError` instead of
467
+ silently running locally. The cloud backend currently supports a subset of
433
468
  operations (create/start/stop/remove/get/list, one-shot exec, follow log
434
469
  streaming); unsupported operations raise `Microsandbox::UnsupportedError`.
435
470
 
@@ -442,8 +477,8 @@ change diverged the two numbers โ€” the gem version is **not** a reliable indica
442
477
  of the embedded runtime version. To learn which runtime a build wraps, ask it:
443
478
 
444
479
  ```ruby
445
- Microsandbox::VERSION # => "0.11.0" (the gem's own version)
446
- Microsandbox.runtime_version # => "v0.6.7" (the embedded upstream runtime tag)
480
+ Microsandbox::VERSION # => "0.13.0" (the gem's own version)
481
+ Microsandbox.runtime_version # => "v0.6.9" (the embedded upstream runtime tag)
447
482
  ```
448
483
 
449
484
  | Gem version | Upstream runtime | Notes |
@@ -465,6 +500,8 @@ Microsandbox.runtime_version # => "v0.6.7" (the embedded upstream runtime tag
465
500
  | `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
501
  | `0.10.0` | `v0.6.6` | `v0.6.6` API parity: live `modify`/resize, `ping`/`touch`, create `max_cpus`/`max_memory` |
467
502
  | `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 |
503
+ | `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 |
504
+ | `0.13.0` | `v0.6.9` | adopts upstream `v0.6.9` (**breaking**): a bare `MSB_API_KEY` no longer selects the cloud backend (explicit `MSB_BACKEND=cloud` or a cloud profile required; invalid cloud config fails closed with `InvalidConfigError`); snapshot payload integrity becomes opt-in (`record_integrity:`, `verify` can report `:not_recorded`). Parity: default-workload execution (`exec_default`/`exec_default_stream`/`attach_default`, `cmd:`), flat root disks (`RootDisk.flat`), `modify(root_disk_size:)`, `rate_limiter:`, `vsock:`, `default_backend_info`, `Volume.get_default` |
468
505
 
469
506
  **Going forward** โ€” the gem version moves on its own semver track and no longer
470
507
  mirrors the upstream tag:
@@ -544,7 +581,8 @@ lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
544
581
  `status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
545
582
  `stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
546
583
  `wait_until_stopped`/`config`/`config_json`/`snapshot` from
547
- `Sandbox.get`, and label-filtered `list_with`),
584
+ `Sandbox.get`, and the cursor-paginated `list`/`list_with` with label
585
+ filters),
548
586
  backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
549
587
  `exec`/`shell` (collected and streaming), interactive `attach`/
550
588
  `attach_shell`, the full guest filesystem (incl. streaming `read_stream`/
@@ -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.6.7).
10
- version = "0.11.0"
9
+ # The core-crate dependency below stays pinned at its own tag (v0.6.9).
10
+ version = "0.13.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.9", default-features = true, features = ["ssh"] }
39
+ microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.9" }
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");
@@ -161,10 +159,32 @@ fn default_backend_kind() -> String {
161
159
  .to_string()
162
160
  }
163
161
 
162
+ /// Secret-safe description of the active default backend (v0.6.9):
163
+ /// {kind, api_url, source, profile}. `source` names what selected the backend
164
+ /// (e.g. "MSB_BACKEND", "programmatic", "default"); the API key is never
165
+ /// included. Like `default_backend_kind`, the first call freezes ambient
166
+ /// env/profile resolution for the process.
167
+ fn default_backend_info() -> magnus::RHash {
168
+ let info = microsandbox::default_backend_info();
169
+ let hash = crate::runtime::ruby().hash_new();
170
+ let _ = hash.aset(
171
+ "kind",
172
+ match info.kind {
173
+ microsandbox::BackendKind::Local => "local",
174
+ microsandbox::BackendKind::Cloud => "cloud",
175
+ },
176
+ );
177
+ let _ = hash.aset("api_url", info.api_url);
178
+ let _ = hash.aset("source", info.source.as_str());
179
+ let _ = hash.aset("profile", info.profile);
180
+ hash
181
+ }
182
+
164
183
  pub fn define(_ruby: &Ruby, native: &RModule) -> Result<(), Error> {
165
184
  native.define_singleton_method("set_default_backend", function!(set_default_backend, 4))?;
166
185
  native.define_singleton_method("push_default_backend", function!(push_default_backend, 4))?;
167
186
  native.define_singleton_method("pop_default_backend", function!(pop_default_backend, 1))?;
168
187
  native.define_singleton_method("default_backend_kind", function!(default_backend_kind, 0))?;
188
+ native.define_singleton_method("default_backend_info", function!(default_backend_info, 0))?;
169
189
  Ok(())
170
190
  }
@@ -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.
@@ -60,6 +60,10 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
60
60
  // unconditionally enables the core's `net` feature (default-features),
61
61
  // so this variant is always present.
62
62
  NetworkBuilder(_) => "NetworkPolicyError",
63
+ // v0.6.9: `exec_default`/`attach_default` on an image whose resolved
64
+ // ENTRYPOINT+CMD provide no executable command. Mirrors the Python
65
+ // SDK's `NoDefaultCommandError`.
66
+ NoDefaultCommand => "NoDefaultCommandError",
63
67
  _ => "Error",
64
68
  }
65
69
  }
@@ -83,12 +87,96 @@ pub fn to_ruby(err: MicrosandboxError) -> Error {
83
87
  Err(_) => return Error::new(magnus::exception::runtime_error(), message),
84
88
  };
85
89
 
90
+ // `Unsupported` gets a Ruby-idiom message (`sandbox.kill` instead of
91
+ // `Sandbox::kill`) plus structured `operation` / `hint` attributes on the
92
+ // exception instance, mirroring the Python SDK's enrichment (v0.6.8).
93
+ if let MicrosandboxError::Unsupported { op, reason } = &err {
94
+ return unsupported_error(&ruby, &ruby_api_name(*op), &ruby_hint(reason));
95
+ }
96
+
86
97
  match exception_class(&ruby, class_name(&err)) {
87
98
  Some(class) => Error::new(class, message),
88
99
  None => Error::new(ruby.exception_runtime_error(), message),
89
100
  }
90
101
  }
91
102
 
103
+ /// `UnsupportedError` for shim-only entry points that require the local
104
+ /// backend but have no SDK [`Operation`] (Ruby-only diagnostic hooks such as
105
+ /// `Microsandbox.runtime_path`). `name` is the Ruby-facing API name. Mirrors
106
+ /// the Python SDK's name-based `local_only` helper.
107
+ #[allow(deprecated)]
108
+ pub fn local_only(name: &str) -> Error {
109
+ match Ruby::get() {
110
+ Ok(ruby) => unsupported_error(&ruby, name, "use a local backend"),
111
+ Err(_) => Error::new(
112
+ magnus::exception::runtime_error(),
113
+ format!("{name} is not supported by this backend: use a local backend"),
114
+ ),
115
+ }
116
+ }
117
+
118
+ /// Build a `Microsandbox::UnsupportedError` carrying the rendered message and
119
+ /// the structured `@operation` / `@hint` attributes.
120
+ fn unsupported_error(ruby: &Ruby, operation: &str, hint: &str) -> Error {
121
+ let message = format!("{operation} is not supported by this backend: {hint}");
122
+ let Some(class) = exception_class(ruby, "UnsupportedError") else {
123
+ return Error::new(ruby.exception_runtime_error(), message);
124
+ };
125
+ match class
126
+ .as_value()
127
+ .funcall::<_, _, magnus::Exception>("new", (message.as_str(),))
128
+ {
129
+ Ok(exc) => {
130
+ // Best-effort extras; the message already carries both.
131
+ let _ = exc
132
+ .funcall::<_, _, magnus::Value>("instance_variable_set", ("@operation", operation));
133
+ let _ = exc.funcall::<_, _, magnus::Value>("instance_variable_set", ("@hint", hint));
134
+ exc.into()
135
+ }
136
+ Err(_) => Error::new(class, message),
137
+ }
138
+ }
139
+
140
+ /// Render an [`Operation`] as the Ruby API it corresponds to: `Sandbox::kill`
141
+ /// becomes `sandbox.kill` and `SandboxFsOps::stat_handle` becomes
142
+ /// `sandbox_fs_ops.stat_handle`; a parenthetical keeps its Ruby keyword shape
143
+ /// (`log_stream(follow=false)` becomes `log_stream(follow: false)`). Plain
144
+ /// phrases without a `Type::method` shape (`config`, `snapshot operations`)
145
+ /// pass through as-is. Mirrors the Python SDK's `py_api_name`.
146
+ fn ruby_api_name(op: Operation) -> String {
147
+ let path = op.api_path();
148
+ let Some((ty, method)) = path.split_once("::") else {
149
+ return path.to_string();
150
+ };
151
+ format!("{}.{}", camel_to_snake(ty), method.replace('=', ": "))
152
+ }
153
+
154
+ /// Render an [`UnsupportedReason`] with `use instead` targets pointing at the
155
+ /// Ruby API name rather than the Rust path.
156
+ fn ruby_hint(reason: &UnsupportedReason) -> String {
157
+ match reason {
158
+ UnsupportedReason::UseInstead(op) => format!("use {}", ruby_api_name(*op)),
159
+ other => other.hint(),
160
+ }
161
+ }
162
+
163
+ /// Lower a `CamelCase` type name to `snake_case` (`SandboxFsOps` becomes
164
+ /// `sandbox_fs_ops`).
165
+ fn camel_to_snake(name: &str) -> String {
166
+ let mut out = String::with_capacity(name.len() + 4);
167
+ for (i, ch) in name.char_indices() {
168
+ if ch.is_ascii_uppercase() {
169
+ if i > 0 {
170
+ out.push('_');
171
+ }
172
+ out.push(ch.to_ascii_lowercase());
173
+ } else {
174
+ out.push(ch);
175
+ }
176
+ }
177
+ out
178
+ }
179
+
92
180
  /// A plain `Microsandbox::Error` (base) with a custom message โ€” used for
93
181
  /// binding-level validation errors that have no core counterpart.
94
182
  #[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
  }