microsandbox-rb 0.5.7 → 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.
@@ -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
  }