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 +4 -4
- data/CHANGELOG.md +48 -0
- data/Cargo.lock +1 -1
- data/DESIGN.md +25 -16
- data/README.md +18 -11
- data/ext/microsandbox/Cargo.toml +1 -1
- data/ext/microsandbox/src/agent.rs +166 -0
- data/ext/microsandbox/src/conv.rs +19 -1
- data/ext/microsandbox/src/lib.rs +4 -0
- data/ext/microsandbox/src/sandbox.rs +494 -3
- data/ext/microsandbox/src/ssh.rs +317 -0
- data/lib/microsandbox/agent.rb +181 -0
- data/lib/microsandbox/network.rb +300 -0
- data/lib/microsandbox/patch.rb +98 -0
- data/lib/microsandbox/sandbox.rb +87 -3
- data/lib/microsandbox/ssh.rb +247 -0
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox.rb +4 -0
- data/sig/microsandbox.rbs +133 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 045a5bf021edaf5afcfcf487cf0d980c38018560cc842c44018863570236f4ed
|
|
4
|
+
data.tar.gz: 25791e8bab4051711794cb8c1e08bbabc2b4dbc79bf080b02908b620ee0bd06e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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), `
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
148
|
-
`security`, `max_duration`, `idle_timeout`, `rlimits`,
|
|
149
|
-
`registry_auth`/`registry_insecure`/`registry_ca_certs`,
|
|
150
|
-
`from_snapshot`, `detached`, and `replace`/`replace_with_timeout`.
|
|
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** (
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
the
|
|
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
|
|
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),
|
|
379
|
-
filesystem, metrics (per-sandbox,
|
|
380
|
-
streaming `metrics_stream`/`log_stream`),
|
|
381
|
-
named volumes,
|
|
382
|
-
boot-from-snapshot)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
`
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
data/ext/microsandbox/Cargo.toml
CHANGED
|
@@ -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.
|
|
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) {
|
data/ext/microsandbox/src/lib.rs
CHANGED
|
@@ -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
|
}
|