microsandbox-rb 0.9.2 → 0.11.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
@@ -39,7 +39,7 @@ them. Our deepest thanks to the maintainers and community. 🙏
39
39
  - **Guest filesystem access** — read, write, list, copy, stat files inside a running sandbox
40
40
  - **Metrics & logs** — CPU, memory, disk and network I/O; captured stdout/stderr/system logs
41
41
  - **Rootfs patches** — inject files, dirs, and symlinks into the image before boot (`Microsandbox::Patch`)
42
- - **Fine-grained networking** — policy presets *and* custom CIDR/domain/group allow-deny rules (`Microsandbox::NetworkPolicy`)
42
+ - **Fine-grained networking** — composable profiles (`:public`/`:private`/`:host`) *and* custom CIDR/domain/group allow-deny rules (`Microsandbox::NetworkPolicy`)
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
@@ -160,7 +160,7 @@ Microsandbox::Sandbox.create(
160
160
  workdir: "/app",
161
161
  labels: { "team" => "research" },
162
162
  ports: { 8080 => 80 }, # host => guest (TCP)
163
- network: "public_only", # or "none" for airgapped
163
+ network: [:public], # composable profiles; :none for airgapped
164
164
  replace: true # replace an existing sandbox of the same name
165
165
  ) do |sb|
166
166
  # ...
@@ -229,6 +229,55 @@ Microsandbox::Sandbox.create("obs", image: "public.ecr.aws/docker/library/alpine
229
229
  end
230
230
  ```
231
231
 
232
+ ### Live modification & health
233
+
234
+ Resize, reconfigure, or rotate secrets on a sandbox **without recreating it**,
235
+ and probe the guest agent's liveness. To leave headroom for a live CPU/memory
236
+ resize, reserve a ceiling at create time with `max_cpus:`/`max_memory:`:
237
+
238
+ ```ruby
239
+ Microsandbox::Sandbox.create("live", image: "public.ecr.aws/docker/library/alpine:latest",
240
+ cpus: 1, max_cpus: 4, memory: 512, max_memory: 2048) do |sb|
241
+ # Health check — does NOT refresh the idle timer:
242
+ ping = sb.ping # => Microsandbox::PingResult
243
+ ping.latency_ms # round-trip latency
244
+
245
+ # Explicitly refresh the idle-activity timer (resets any idle_timeout:):
246
+ sb.touch.activity_seq # => Microsandbox::TouchResult
247
+
248
+ # Preview a change without applying it:
249
+ plan = sb.modify(cpus: 2, memory: 1024, dry_run: true)
250
+ plan.applied? # => false
251
+ plan.changes # => [{ kind: "config", field: "cpus", ... }, ...]
252
+
253
+ # Live resize — applies to the running VM under the default :no_restart policy:
254
+ sb.modify(cpus: 2, memory: 1024)
255
+
256
+ # env/labels/workdir changes on a *running* sandbox require a restart, so the
257
+ # default :no_restart policy rejects the whole apply (it raises rather than
258
+ # partially applying). Persist them for the next start — or restart now —
259
+ # by saying so explicitly:
260
+ sb.modify(env: { "TIER" => "prod" }, remove_env: ["DEBUG"],
261
+ labels: { "role" => "worker" }, policy: :next_start) # or policy: :restart
262
+
263
+ # Rotating/removing an *existing* secret (or updating its allowed hosts) is
264
+ # live; *adding* a new secret is restart-required, like env. Specs are keyed
265
+ # by name; env:/store:/value: are mutually exclusive:
266
+ sb.modify(
267
+ secrets: { "API_KEY" => { env: "HOST_API_KEY", allowed_hosts: ["api.example.com"] } },
268
+ remove_secrets: ["OLD_TOKEN"],
269
+ )
270
+ end
271
+ ```
272
+
273
+ The apply is **all-or-nothing**: under a given `policy:` every planned change
274
+ must be applicable, or the whole `modify` raises — nothing is partially
275
+ applied. Use `dry_run: true` to inspect each change's `disposition` (`"live"`,
276
+ `"next start"`, `"requires restart"`) before committing.
277
+
278
+ `SandboxHandle` (from `Sandbox.get`/`list`) carries the same `#ping`/`#touch`/
279
+ `#modify`; on a stopped sandbox `#ping`/`#touch` raise `SandboxNotRunningError`.
280
+
232
281
  ### Streaming output
233
282
 
234
283
  For long-running commands, stream events as they arrive instead of waiting:
@@ -393,8 +442,8 @@ change diverged the two numbers — the gem version is **not** a reliable indica
393
442
  of the embedded runtime version. To learn which runtime a build wraps, ask it:
394
443
 
395
444
  ```ruby
396
- Microsandbox::VERSION # => "0.9.2" (the gem's own version)
397
- Microsandbox.runtime_version # => "v0.6.3" (the embedded upstream runtime tag)
445
+ Microsandbox::VERSION # => "0.11.0" (the gem's own version)
446
+ Microsandbox.runtime_version # => "v0.6.7" (the embedded upstream runtime tag)
398
447
  ```
399
448
 
400
449
  | Gem version | Upstream runtime | Notes |
@@ -413,6 +462,9 @@ Microsandbox.runtime_version # => "v0.6.3" (the embedded upstream runtime tag
413
462
  | `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 |
414
463
  | `0.9.1` | `v0.6.2` | adopts upstream `v0.6.2` (faster image loads/pulls via early cache gate + zlib-rs); upstream API unchanged — no Ruby surface change |
415
464
  | `0.9.2` | `v0.6.3` | adopts upstream `v0.6.3` (v4-only sandboxes stop advertising AAAA DNS answers — fixes guest gRPC/c-ares preferring unreachable IPv6; scoped upstream TLS verification); glue moves to `*_local` SDK variants — no Ruby surface change |
465
+ | `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
+ | `0.10.0` | `v0.6.6` | `v0.6.6` API parity: live `modify`/resize, `ping`/`touch`, create `max_cpus`/`max_memory` |
467
+ | `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 |
416
468
 
417
469
  **Going forward** — the gem version moves on its own semver track and no longer
418
470
  mirrors the upstream tag:
@@ -491,7 +543,7 @@ section. The binding covers the official-SDK surface: sandbox
491
543
  lifecycle (the live `Sandbox` `stop`/`stop_and_wait`/`kill`/`drain`/`wait`/
492
544
  `status`/`detach`/`owns_lifecycle?`, plus the `SandboxHandle` controls
493
545
  `stop_with_timeout`/`request_stop`/`request_kill`/`request_drain`/
494
- `wait_until_stopped`/`config`/`config_json`/`snapshot`/`snapshot_to` from
546
+ `wait_until_stopped`/`config`/`config_json`/`snapshot` from
495
547
  `Sandbox.get`, and label-filtered `list_with`),
496
548
  backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
497
549
  `exec`/`shell` (collected and streaming), interactive `attach`/
@@ -499,10 +551,12 @@ backend routing (`set_default_backend`/`with_backend`/`default_backend_kind`),
499
551
  `write_stream`), metrics (per-sandbox, `Microsandbox.all_sandbox_metrics`, and
500
552
  streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
501
553
  named volumes (incl. host-side `Volume.fs`/`VolumeInfo#fs` read/write),
502
- snapshots (create/open/list/list_dir/reindex/verify/export/import +
503
- boot-from-snapshot), streaming image-pull progress
504
- (`Sandbox.create_with_progress` → `PullSession`), **rootfs patches**
505
- (`Microsandbox::Patch`), **network configuration** (presets, custom per-rule
554
+ snapshots (create/open/list/list_dir/reindex/verify/save/load +
555
+ boot-from-snapshot), image archives (`Image.load`/`Image.save`), streaming
556
+ image-pull progress (`Sandbox.create_with_progress` → `PullSession`),
557
+ **rootfs patches** (`Microsandbox::Patch`), the structured **root disk**
558
+ (`root_disk:` managed/tmpfs/disk via `Microsandbox::RootDisk`), **network
559
+ configuration** (composable profiles, custom per-rule
506
560
  `Microsandbox::NetworkPolicy`/`Rule`/`Destination`, plus DNS, TLS interception,
507
561
  IPv4/IPv6 pools, `max_connections`, `trust_host_cas`), **secrets** (multi-host /
508
562
  wildcard allow-lists, injection toggles, per-secret + sandbox-level violation
@@ -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.3).
10
- version = "0.9.2"
9
+ # The core-crate dependency below stays pinned at its own tag (v0.6.7).
10
+ version = "0.11.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.3", default-features = true, features = ["ssh"] }
39
- microsandbox-network = { git = "https://github.com/superradcompany/microsandbox", tag = "v0.6.3" }
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" }
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"] }
@@ -17,6 +17,12 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
17
17
  SandboxNotFound(_) => "SandboxNotFoundError",
18
18
  SandboxAlreadyExists(_) => "SandboxAlreadyExistsError",
19
19
  SandboxStillRunning(_) => "SandboxStillRunningError",
20
+ // v0.6.6 (#1099): the sandbox exists but isn't running. Raised by the
21
+ // handle's exec/attach/ping/touch not-running guards and the fs
22
+ // agent-endpoint lookup. The `SandboxNotRunningError` class already
23
+ // existed (mirroring the Python SDK); this wires the new core variant to
24
+ // it instead of letting it collapse to the base `Error`.
25
+ SandboxNotRunning(_) => "SandboxNotRunningError",
20
26
  ExecTimeout(_) => "ExecTimeoutError",
21
27
  ExecFailed(_) => "ExecFailedError",
22
28
  SandboxFsOps(_) => "FilesystemError",
@@ -45,6 +51,10 @@ fn class_name(err: &MicrosandboxError) -> &'static str {
45
51
  SnapshotSandboxRunning(_) => "SnapshotSandboxRunningError",
46
52
  SnapshotImageMissing(_) => "SnapshotImageMissingError",
47
53
  SnapshotIntegrity(_) => "SnapshotIntegrityError",
54
+ // v0.6.7 (#1200): the automatic v0.6.6→v0.6.7 snapshot-descriptor
55
+ // migration (run at backend connect / artifact open) failed and needs
56
+ // repair. Mirrors the Python `SnapshotMigrationError`.
57
+ SnapshotMigration { .. } => "SnapshotMigrationError",
48
58
  // Give the already-defined-but-orphaned `NetworkPolicyError` a mapping:
49
59
  // a builder parse/validation error from `network(|n| ...)`. The gem
50
60
  // unconditionally enables the core's `net` feature (default-features),
@@ -7,9 +7,11 @@
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
11
 
11
12
  use crate::backend::with_local_backend;
12
13
  use crate::conv;
14
+ use crate::error;
13
15
  use crate::runtime::ruby;
14
16
 
15
17
  fn handle_to_hash(h: &ImageHandle) -> RHash {
@@ -109,6 +111,40 @@ fn prune() -> Result<RHash, Error> {
109
111
  Ok(report_to_hash(report))
110
112
  }
111
113
 
114
+ /// Load images into the cache from a local `docker save` tarball or an OCI
115
+ /// Image Layout archive. `tags` optionally retags what was loaded. Returns an
116
+ /// Array of image-handle Hashes. Mirrors the Python `Image.load` / Node
117
+ /// `imageLoad` added in v0.6.7. (The `"-"` stdin form is spooled to a temp
118
+ /// file by the Ruby layer — the core reads seekable files only.)
119
+ fn load(input_path: String, tags: Vec<String>) -> Result<RArray, Error> {
120
+ let handles = with_local_backend(async |local| {
121
+ Image::load_local(local, std::path::Path::new(&input_path), tags).await
122
+ })?;
123
+ let arr = ruby().ary_new();
124
+ for h in handles.iter() {
125
+ arr.push(handle_to_hash(h))?;
126
+ }
127
+ Ok(arr)
128
+ }
129
+
130
+ /// Save cached image(s) into a local archive. `format` is "docker" (default
131
+ /// shape callers expect from `docker load`) or "oci" (OCI Image Layout).
132
+ /// Mirrors the Python `Image.save` / Node `imageSave` added in v0.6.7.
133
+ fn save(references: Vec<String>, output_path: String, format: String) -> Result<(), Error> {
134
+ let fmt = match format.as_str() {
135
+ "docker" => ImageArchiveFormat::Docker,
136
+ "oci" => ImageArchiveFormat::Oci,
137
+ other => {
138
+ return Err(error::base_error(format!(
139
+ "invalid archive format {other:?}: expected \"docker\" or \"oci\""
140
+ )))
141
+ }
142
+ };
143
+ with_local_backend(async |local| {
144
+ Image::save_local(local, &references, std::path::Path::new(&output_path), fmt).await
145
+ })
146
+ }
147
+
112
148
  pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
113
149
  let class = native.define_class("Image", ruby.class_object())?;
114
150
  class.define_singleton_method("get", function!(get, 1))?;
@@ -116,5 +152,7 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
116
152
  class.define_singleton_method("inspect", function!(inspect, 1))?;
117
153
  class.define_singleton_method("remove", function!(remove, 2))?;
118
154
  class.define_singleton_method("prune", function!(prune, 0))?;
155
+ class.define_singleton_method("load", function!(load, 2))?;
156
+ class.define_singleton_method("save", function!(save, 3))?;
119
157
  Ok(())
120
158
  }