microsandbox-rb 0.5.8 → 0.5.9

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: 56e1f366f8e18a95965bc917c65b739426499a147f1da4a2defb090dba4c375d
4
- data.tar.gz: 689f3c5518ddc52e6fd851d290e2629d3a3a9a043ba09653a6ce1041039a9a37
3
+ metadata.gz: 045a5bf021edaf5afcfcf487cf0d980c38018560cc842c44018863570236f4ed
4
+ data.tar.gz: 25791e8bab4051711794cb8c1e08bbabc2b4dbc79bf080b02908b620ee0bd06e
5
5
  SHA512:
6
- metadata.gz: 503d0a3460f47855742e5179ccafab4e7b65132e71af5d3c39ed77b14542ac231e6bdbb9ebe2021e06ff2da912f702c8ad44f5a0dd882343879954d529581729
7
- data.tar.gz: '08880c49fc0803d36ba7d1ee3b457c130528155a0ede5d3360c34fc805bc94feb4f6b88f7ae16287b97bdfefc32bbb8c1fdb7a7c89cfee3d46606c6a071efd6d'
6
+ metadata.gz: 1b89148498194edb82241979432ee2e47599f2b657805b05718bfd18a8e1104318833ef84eb5f5e47a66f3cec8dc369f742897bbfe60cdb07fb613b597ef6ac5
7
+ data.tar.gz: c25b033291b4fb5195349030dcf2296006fa8b3563c7e1f6804441ed5e554e496583b22ddbca32828dff1b36127082b4cb1e995ab654573afc46b09e012031fd
data/CHANGELOG.md CHANGED
@@ -6,6 +6,54 @@ upstream microsandbox runtime.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.9] - 2026-06-18
10
+
11
+ Closes the remaining roadmap items, bringing the binding surface to parity with
12
+ the official Python/Node/Go SDKs (still wrapping the same upstream core,
13
+ `v0.5.7`).
14
+
15
+ ### Added
16
+
17
+ - **Rootfs patches** — `Sandbox.create(patches: [...])` applies modifications to
18
+ the root filesystem before boot, built with the new `Microsandbox::Patch`
19
+ factory: `Patch.text`/`file`/`append`/`copy_file`/`copy_dir`/`symlink`/`mkdir`/
20
+ `remove`. Mirrors the `Patch` factory in the official SDKs. (OverlayFS/bind
21
+ roots only — not disk images.)
22
+ - **Custom per-rule network policies** — `Sandbox.create(network:)` now accepts,
23
+ besides the existing preset names, a `Microsandbox::NetworkPolicy` or a Hash
24
+ describing an ordered allow/deny rule list with per-direction defaults and bulk
25
+ domain denials. New `Microsandbox::NetworkPolicy` (`public_only`/`none`/
26
+ `allow_all`/`non_local`/`custom`), `Microsandbox::Rule` (`allow`/`deny`), and
27
+ `Microsandbox::Destination` (`any`/`ip`/`cidr`/`domain`/`domain_suffix`/
28
+ `group`, plus shorthand-string classification) factories. Destination
29
+ classification and rule composition mirror the official binding exactly.
30
+ - **Raw agent client** — `Microsandbox::AgentClient.connect_sandbox`/
31
+ `connect_path`/`socket_path` open the byte-level transport to a sandbox's
32
+ `agentd` relay socket: `request`, `stream` (→ `Microsandbox::AgentStream`,
33
+ `Enumerable` over `Microsandbox::AgentFrame`), `send_frame`, `ready_bytes`,
34
+ `close`, with the `FLAG_TERMINAL`/`FLAG_SESSION_START`/`FLAG_SHUTDOWN` frame
35
+ flags. Mirrors the official `AgentClient`.
36
+ - **SSH** — `Sandbox#ssh` returns a `Microsandbox::SshOps` to `open_client`
37
+ (→ `Microsandbox::SshClient`: `exec` → `Microsandbox::SshOutput`, `attach`,
38
+ `sftp` → `Microsandbox::SftpClient` with `read`/`write`/`mkdir`/`remove_file`/
39
+ `remove_dir`/`rename`/`symlink`/`real_path`/`read_link`, `close`) or
40
+ `prepare_server` (→ `Microsandbox::SshServer`: `serve_connection`, `close`).
41
+ - **Interactive attach** — `Sandbox#attach(command, args, …)` and
42
+ `Sandbox#attach_shell` couple the host terminal (raw mode, SIGWINCH) to a
43
+ command (or the default shell) in the sandbox and return its exit code. For
44
+ CLI use — requires a real TTY.
45
+ - RBS signatures for all of the above.
46
+
47
+ ### Notes
48
+
49
+ - Network policy: a `preset` and custom `rules:`/`default_egress:`/`default_ingress:`
50
+ are mutually exclusive (a preset already defines its rules and defaults); a
51
+ preset may still be layered with `deny_domains:`/`deny_domain_suffixes:`. A
52
+ hand-written rule Hash accepts the singular `protocol:`/`port:` keys (the
53
+ spelling the Go/Python `PolicyRule` use) as well as the plural forms. The
54
+ deny-list-only shorthand (`network: { deny_domains: [...] }`) keeps the rest of
55
+ the network reachable (permissive defaults), matching the official SDKs.
56
+
9
57
  ## [0.5.8] - 2026-06-17
10
58
 
11
59
  Closes the `Sandbox`-class lifecycle gap with the official Python/Node/Go SDKs
data/Cargo.lock CHANGED
@@ -3232,7 +3232,7 @@ dependencies = [
3232
3232
 
3233
3233
  [[package]]
3234
3234
  name = "microsandbox_rb"
3235
- version = "0.5.8"
3235
+ version = "0.5.9"
3236
3236
  dependencies = [
3237
3237
  "chrono",
3238
3238
  "futures",
data/DESIGN.md CHANGED
@@ -137,24 +137,32 @@ API (`fs.read`/`write`/`list`/`mkdir`/`remove`/`stat`/…), `metrics`,
137
137
  **OCI image-cache management** (`Image.get`/`list`/`inspect`/`remove`/`prune`),
138
138
  **named volumes** (`Volume.create`/`get`/`list`/`remove` + `volumes:` mounts),
139
139
  **snapshots** (`Snapshot.create`/`get`/`list`/`remove`/`verify`/`export`/`import`
140
- + `from_snapshot:` boot), `version`/`install`/`installed?`/`ensure_runtime!`,
141
- **registry auth** (`registry_auth`/`registry_insecure`/`registry_ca_certs` on
142
- `create`, for private/authenticated registries), and the typed error hierarchy.
140
+ + `from_snapshot:` boot), **rootfs patches** (`Patch.text`/`file`/`append`/
141
+ `copy_file`/`copy_dir`/`symlink`/`mkdir`/`remove` via `create(patches:)`),
142
+ **custom per-rule network policies** (`NetworkPolicy`/`Rule`/`Destination`
143
+ CIDR/IP/domain/suffix/group allow-deny rules with per-direction defaults and
144
+ bulk domain denials, alongside the presets), interactive **`attach`/
145
+ `attach_shell`** (host-TTY coupled — raw mode + SIGWINCH), **SSH**
146
+ (`Sandbox#ssh` → `SshClient`/`SftpClient`/`SshServer`), the **raw agent client**
147
+ (`AgentClient` → `AgentStream`/`AgentFrame`),
148
+ `version`/`install`/`installed?`/`ensure_runtime!`, **registry auth**
149
+ (`registry_auth`/`registry_insecure`/`registry_ca_certs` on `create`, for
150
+ private/authenticated registries), and the typed error hierarchy.
143
151
 
144
152
  Create options now cover `image`, `cpus`, `memory`, `oci_upper_size`, `env`,
145
153
  `workdir`, `shell`, `user`, `hostname`, `labels`, `scripts`, `entrypoint`,
146
- `ports`/`ports_udp`, `volumes`, `network` policy presets
147
- (`public_only`/`none`/`allow_all`/`non_local`), `log_level`, `quiet_logs`,
148
- `security`, `max_duration`, `idle_timeout`, `rlimits`, `pull_policy`,
149
- `registry_auth`/`registry_insecure`/`registry_ca_certs`, `secrets`,
150
- `from_snapshot`, `detached`, and `replace`/`replace_with_timeout`. `exec`/`shell`
151
- add per-call `rlimits`.
154
+ `ports`/`ports_udp`, `volumes`, `patches`, `network` (policy presets
155
+ `public_only`/`none`/`allow_all`/`non_local`, or a custom `NetworkPolicy`/Hash),
156
+ `log_level`, `quiet_logs`, `security`, `max_duration`, `idle_timeout`, `rlimits`,
157
+ `pull_policy`, `registry_auth`/`registry_insecure`/`registry_ca_certs`,
158
+ `secrets`, `from_snapshot`, `detached`, and `replace`/`replace_with_timeout`.
159
+ `exec`/`shell` add per-call `rlimits`.
152
160
 
153
161
  ## Verification
154
162
 
155
163
  The binding is verified at four levels:
156
164
 
157
- 1. **Unit** (140 examples) — the Ruby layer's option normalization and value
165
+ 1. **Unit** (192 examples) — the Ruby layer's option normalization and value
158
166
  objects, with the native layer stubbed.
159
167
  2. **Real-microVM integration** (`spec/integration`, opt-in via
160
168
  `MICROSANDBOX_INTEGRATION=1`) — boots actual sandboxes and round-trips
@@ -170,9 +178,10 @@ The binding is verified at four levels:
170
178
  shipped Rust source via `extconf.rb` and the installed gem boots a real
171
179
  microVM, confirming the gem manifest and source-install path are complete.
172
180
 
173
- **Roadmap:** custom per-rule network policies (CIDR/domain/group allow-deny
174
- rules the presets and secret-host allowances are covered), file patches,
175
- interactive `attach`/`attach_shell` (host-TTY coupled raw mode,
176
- SIGWINCH), SSH (`SshClient`/`SftpClient`/`SshServer`), and the raw agent client.
177
- The native layer is structured so these slot in module-by-module, exactly as in
178
- the Python binding.
181
+ **Roadmap:** the v1 roadmap (custom per-rule network policies, file patches,
182
+ interactive `attach`/`attach_shell`, SSH, and the raw agent client) is now
183
+ implemented see the list above. What remains is upstream-gated rather than a
184
+ binding gap: surfacing newer core features as the pinned core-crate tag advances
185
+ (e.g. additional network knobs like DNS/TLS-proxy tuning and per-mount stat
186
+ virtualization), which slot in module-by-module exactly as the existing bindings
187
+ do.
data/README.md CHANGED
@@ -14,6 +14,10 @@ This is an **unofficial, community-maintained** Ruby implementation — not part
14
14
  - **Command execution** — run commands or shell scripts and collect output
15
15
  - **Guest filesystem access** — read, write, list, copy, stat files inside a running sandbox
16
16
  - **Metrics & logs** — CPU, memory, disk and network I/O; captured stdout/stderr/system logs
17
+ - **Rootfs patches** — inject files, dirs, and symlinks into the image before boot (`Microsandbox::Patch`)
18
+ - **Fine-grained networking** — policy presets *and* custom CIDR/domain/group allow-deny rules (`Microsandbox::NetworkPolicy`)
19
+ - **SSH & SFTP** — native in-process SSH client/server and file transfer (`Sandbox#ssh`)
20
+ - **Raw agent client** — byte-level access to the guest `agentd` protocol (`Microsandbox::AgentClient`)
17
21
  - **Idiomatic Ruby** — keyword arguments, block-scoped lifecycle, a typed error hierarchy
18
22
  - **Thread-friendly** — the GVL is released during sandbox calls, so other Ruby threads keep running
19
23
 
@@ -372,19 +376,22 @@ one bound to the gem.
372
376
  > Until promoted, users install the source gem (which compiles via `rb_sys`).
373
377
 
374
378
  See [DESIGN.md](DESIGN.md) for the architecture and the implemented-surface
375
- section for what's covered today vs. on the roadmap. Covered: full sandbox
379
+ section. The binding now covers the full official-SDK surface: sandbox
376
380
  lifecycle (including the async `request_stop`/`request_kill`/`request_drain`/
377
381
  `wait_until_stopped`/`detach`/`owns_lifecycle?` controls and label-filtered
378
- `list_with`), `exec`/`shell` (collected and streaming), the full guest
379
- filesystem, metrics (per-sandbox, `Microsandbox.all_sandbox_metrics`, and
380
- streaming `metrics_stream`/`log_stream`), logs, OCI image-cache management,
381
- named volumes, and snapshots (create/list/verify/export/import +
382
- boot-from-snapshot). Create options span resources, network policy presets,
383
- `log_level`/`security`/`rlimits`/`pull_policy`/`secrets` and more; `exec`/`shell`
384
- take per-call `rlimits`, and `create` accepts `registry_auth`/`registry_insecure`/
385
- `registry_ca_certs` for private and authenticated registries. Still on the
386
- roadmap: custom per-rule network policies, file patches, interactive `attach`,
387
- SSH, and the raw agent client.
382
+ `list_with`), `exec`/`shell` (collected and streaming), interactive `attach`/
383
+ `attach_shell`, the full guest filesystem, metrics (per-sandbox,
384
+ `Microsandbox.all_sandbox_metrics`, and streaming `metrics_stream`/`log_stream`),
385
+ logs, OCI image-cache management, named volumes, snapshots (create/list/verify/
386
+ export/import + boot-from-snapshot), **rootfs patches** (`Microsandbox::Patch`),
387
+ **custom per-rule network policies** (`Microsandbox::NetworkPolicy`/`Rule`/
388
+ `Destination`, alongside the presets), **SSH** (`Sandbox#ssh`
389
+ `SshClient`/`SftpClient`/`SshServer`), and the **raw agent client**
390
+ (`Microsandbox::AgentClient`). Create options span resources, network policy,
391
+ `log_level`/`security`/`rlimits`/`pull_policy`/`secrets`/`patches` and more;
392
+ `exec`/`shell` take per-call `rlimits`, and `create` accepts
393
+ `registry_auth`/`registry_insecure`/`registry_ca_certs` for private and
394
+ authenticated registries.
388
395
 
389
396
  ## License
390
397
 
@@ -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.5.7).
10
- version = "0.5.8"
10
+ version = "0.5.9"
11
11
  authors = ["Super Rad Company <development@superrad.company>"]
12
12
  repository = "https://github.com/superradcompany/microsandbox"
13
13
  license = "Apache-2.0"
@@ -0,0 +1,166 @@
1
+ //! Raw agent client: `Microsandbox::Native::AgentClient`.
2
+ //!
3
+ //! Mirrors `sdk/python/src/agent.rs`. Wraps the core `AgentBridge` — the
4
+ //! FFI-shaped, bytes-in/bytes-out façade over a sandbox's agentd relay socket.
5
+ //! Frames are moved as raw CBOR bodies; (de)serialization stays in Ruby. Streams
6
+ //! are referenced by opaque `u64` handles so the Ruby layer never owns a tokio
7
+ //! receiver. Every call runs on the shared tokio runtime with the GVL released.
8
+
9
+ use std::sync::Arc;
10
+ use std::time::Duration;
11
+
12
+ use magnus::{function, method, prelude::*, Error, RHash, RModule, RString, Ruby};
13
+ use microsandbox::agent::AgentClient as CoreAgentClient;
14
+ use microsandbox::{AgentBridge, BridgeFrame, MicrosandboxError};
15
+
16
+ use crate::error;
17
+ use crate::runtime::{block_on, ruby};
18
+
19
+ /// Map an agent-client error onto the Ruby exception hierarchy (via the core
20
+ /// `MicrosandboxError::AgentClient` wrapper, exactly like the Python binding).
21
+ fn to_ruby_agent(err: microsandbox::AgentClientError) -> Error {
22
+ error::to_ruby(MicrosandboxError::AgentClient(err))
23
+ }
24
+
25
+ #[magnus::wrap(class = "Microsandbox::Native::AgentClient", free_immediately, size)]
26
+ pub struct AgentClient {
27
+ inner: Arc<AgentBridge>,
28
+ }
29
+
30
+ impl AgentClient {
31
+ fn from_bridge(bridge: AgentBridge) -> Self {
32
+ Self {
33
+ inner: Arc::new(bridge),
34
+ }
35
+ }
36
+
37
+ //----------------------------------------------------------------------
38
+ // Connection (singleton methods)
39
+ //----------------------------------------------------------------------
40
+
41
+ /// Connect to a running sandbox by name. `timeout` is optional seconds.
42
+ fn connect_sandbox(name: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
43
+ let bridge = match dur(timeout) {
44
+ Some(t) => block_on(AgentBridge::connect_sandbox_with_timeout(&name, t)),
45
+ None => block_on(AgentBridge::connect_sandbox(&name)),
46
+ }
47
+ .map_err(to_ruby_agent)?;
48
+ Ok(AgentClient::from_bridge(bridge))
49
+ }
50
+
51
+ /// Connect to an agentd relay socket by path. `timeout` is optional seconds.
52
+ fn connect_path(path: String, timeout: Option<f64>) -> Result<AgentClient, Error> {
53
+ let bridge = match dur(timeout) {
54
+ Some(t) => block_on(AgentBridge::connect_path_with_timeout(&path, t)),
55
+ None => block_on(AgentBridge::connect_path(&path)),
56
+ }
57
+ .map_err(to_ruby_agent)?;
58
+ Ok(AgentClient::from_bridge(bridge))
59
+ }
60
+
61
+ /// Resolve a sandbox's agent relay socket path without connecting.
62
+ fn socket_path(name: String) -> Result<String, Error> {
63
+ let path = CoreAgentClient::socket_path(&name).map_err(error::to_ruby)?;
64
+ Ok(path.to_string_lossy().into_owned())
65
+ }
66
+
67
+ //----------------------------------------------------------------------
68
+ // Instance methods
69
+ //----------------------------------------------------------------------
70
+
71
+ /// Send one frame and await a single response frame ({id, flags, body}).
72
+ fn request(&self, flags: u8, body: RString) -> Result<RHash, Error> {
73
+ let body = unsafe { body.as_slice() }.to_vec();
74
+ let inner = Arc::clone(&self.inner);
75
+ let frame =
76
+ block_on(async move { inner.request(flags, body).await }).map_err(to_ruby_agent)?;
77
+ Ok(frame_to_hash(frame))
78
+ }
79
+
80
+ /// Open a streaming session; returns {id, handle}.
81
+ fn stream_open(&self, flags: u8, body: RString) -> Result<RHash, Error> {
82
+ let body = unsafe { body.as_slice() }.to_vec();
83
+ let inner = Arc::clone(&self.inner);
84
+ let (id, handle) =
85
+ block_on(async move { inner.stream_open(flags, body).await }).map_err(to_ruby_agent)?;
86
+ let hash = ruby().hash_new();
87
+ hash.aset("id", id)?;
88
+ hash.aset("handle", handle)?;
89
+ Ok(hash)
90
+ }
91
+
92
+ /// Pull the next frame from a stream; nil at end-of-stream.
93
+ fn stream_next(&self, handle: u64) -> Result<Option<RHash>, Error> {
94
+ let inner = Arc::clone(&self.inner);
95
+ let frame =
96
+ block_on(async move { inner.stream_next(handle).await }).map_err(to_ruby_agent)?;
97
+ Ok(frame.map(frame_to_hash))
98
+ }
99
+
100
+ /// Close a stream handle. Idempotent.
101
+ fn stream_close(&self, handle: u64) -> Result<(), Error> {
102
+ let inner = Arc::clone(&self.inner);
103
+ block_on(async move { inner.stream_close(handle).await });
104
+ Ok(())
105
+ }
106
+
107
+ /// Send a follow-up frame on an existing correlation id.
108
+ fn send(&self, id: u32, flags: u8, body: RString) -> Result<(), Error> {
109
+ let body = unsafe { body.as_slice() }.to_vec();
110
+ let inner = Arc::clone(&self.inner);
111
+ block_on(async move { inner.send(id, flags, body).await }).map_err(to_ruby_agent)
112
+ }
113
+
114
+ /// Cached handshake `core.ready` frame body bytes (CBOR).
115
+ fn ready_bytes(&self) -> Result<RString, Error> {
116
+ let bytes = self.inner.ready_bytes().map_err(to_ruby_agent)?;
117
+ Ok(ruby().str_from_slice(&bytes))
118
+ }
119
+
120
+ /// Close the connection. Idempotent.
121
+ fn close(&self) -> Result<(), Error> {
122
+ let inner = Arc::clone(&self.inner);
123
+ block_on(async move { inner.close().await });
124
+ Ok(())
125
+ }
126
+ }
127
+
128
+ /// Convert seconds into a `Duration`, treating a non-positive/absent value as
129
+ /// "use the default handshake timeout".
130
+ fn dur(timeout: Option<f64>) -> Option<Duration> {
131
+ match timeout {
132
+ Some(t) if t.is_finite() && t > 0.0 => Some(Duration::from_secs_f64(t)),
133
+ _ => None,
134
+ }
135
+ }
136
+
137
+ /// Shape a `BridgeFrame` into a Ruby Hash. `body` is binary (ASCII-8BIT).
138
+ fn frame_to_hash(frame: BridgeFrame) -> RHash {
139
+ let r = ruby();
140
+ let hash = r.hash_new();
141
+ let _ = hash.aset("id", frame.id);
142
+ let _ = hash.aset("flags", frame.flags);
143
+ let _ = hash.aset("body", r.str_from_slice(&frame.body));
144
+ hash
145
+ }
146
+
147
+ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
148
+ let class = native.define_class("AgentClient", ruby.class_object())?;
149
+
150
+ class.define_singleton_method(
151
+ "connect_sandbox",
152
+ function!(AgentClient::connect_sandbox, 2),
153
+ )?;
154
+ class.define_singleton_method("connect_path", function!(AgentClient::connect_path, 2))?;
155
+ class.define_singleton_method("socket_path", function!(AgentClient::socket_path, 1))?;
156
+
157
+ class.define_method("request", method!(AgentClient::request, 2))?;
158
+ class.define_method("stream_open", method!(AgentClient::stream_open, 2))?;
159
+ class.define_method("stream_next", method!(AgentClient::stream_next, 1))?;
160
+ class.define_method("stream_close", method!(AgentClient::stream_close, 1))?;
161
+ class.define_method("send", method!(AgentClient::send, 3))?;
162
+ class.define_method("ready_bytes", method!(AgentClient::ready_bytes, 0))?;
163
+ class.define_method("close", method!(AgentClient::close, 0))?;
164
+
165
+ Ok(())
166
+ }
@@ -5,7 +5,7 @@
5
5
 
6
6
  use std::collections::HashMap;
7
7
 
8
- use magnus::{value::ReprValue, Error, RHash, TryConvert, Value};
8
+ use magnus::{value::ReprValue, Error, RArray, RHash, TryConvert, Value};
9
9
 
10
10
  /// Fetch a non-nil value for `key`, if present.
11
11
  fn get(hash: RHash, key: &str) -> Option<Value> {
@@ -62,6 +62,24 @@ pub fn opt_string_vec(hash: RHash, key: &str) -> Result<Vec<String>, Error> {
62
62
  }
63
63
  }
64
64
 
65
+ /// Array of `Hash`es (e.g. `patches`, custom-policy `rules`). Empty if absent.
66
+ ///
67
+ /// `RHash` is a GC-managed handle and so cannot be collected via the blanket
68
+ /// `Vec<T: TryConvert>` path; we walk the `Array` and convert each element.
69
+ pub fn opt_hash_vec(hash: RHash, key: &str) -> Result<Vec<RHash>, Error> {
70
+ match get(hash, key) {
71
+ Some(v) => {
72
+ let arr = RArray::try_convert(v)?;
73
+ let mut out = Vec::with_capacity(arr.len());
74
+ for i in 0..arr.len() {
75
+ out.push(arr.entry::<RHash>(i as isize)?);
76
+ }
77
+ Ok(out)
78
+ }
79
+ None => Ok(Vec::new()),
80
+ }
81
+ }
82
+
65
83
  /// `u16`→`u16` port map (host→guest TCP). Empty if absent.
66
84
  pub fn opt_port_map(hash: RHash, key: &str) -> Result<Vec<(u16, u16)>, Error> {
67
85
  match get(hash, key) {
@@ -8,6 +8,7 @@
8
8
  //! Everything here lives under `Microsandbox::Native`; the ergonomic, idiomatic
9
9
  //! surface is the pure-Ruby layer in `lib/microsandbox/`.
10
10
 
11
+ mod agent;
11
12
  mod conv;
12
13
  mod error;
13
14
  mod exec;
@@ -15,6 +16,7 @@ mod image;
15
16
  mod runtime;
16
17
  mod sandbox;
17
18
  mod snapshot;
19
+ mod ssh;
18
20
  mod stream;
19
21
  mod volume;
20
22
 
@@ -79,6 +81,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
79
81
  snapshot::define(ruby, &native)?;
80
82
  image::define(ruby, &native)?;
81
83
  volume::define(ruby, &native)?;
84
+ agent::define(ruby, &native)?;
85
+ ssh::define(ruby, &native)?;
82
86
 
83
87
  Ok(())
84
88
  }