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
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
//! SSH over a sandbox: `Microsandbox::Native::{SshClient, SftpClient, SshServer}`.
|
|
2
|
+
//!
|
|
3
|
+
//! Mirrors `sdk/python/src/ssh.rs`. The SSH ops are reached from the sandbox
|
|
4
|
+
//! (`Sandbox::ssh_open_client` / `ssh_prepare_server` in `sandbox.rs`); this
|
|
5
|
+
//! module holds the session wrappers. Each owns its core session behind an
|
|
6
|
+
//! `Arc<tokio::Mutex<Option<…>>>` so it can be consumed exactly once on close.
|
|
7
|
+
//!
|
|
8
|
+
//! Discipline: the async work runs inside `block_on` (GVL released), so it must
|
|
9
|
+
//! never touch the Ruby C API. Each method drives the future to a plain Rust
|
|
10
|
+
//! `Result`, then maps it to a Ruby exception *after* `block_on` returns.
|
|
11
|
+
|
|
12
|
+
use std::sync::Arc;
|
|
13
|
+
|
|
14
|
+
use magnus::{method, prelude::*, Error, RHash, RModule, RString, Ruby};
|
|
15
|
+
use microsandbox::sandbox::{
|
|
16
|
+
SftpClient as CoreSftp, SshClient as CoreSshClient, SshOutput, SshServer as CoreSshServer,
|
|
17
|
+
SshStdioStream,
|
|
18
|
+
};
|
|
19
|
+
use microsandbox::{MicrosandboxError, MicrosandboxResult};
|
|
20
|
+
use tokio::io::AsyncWriteExt;
|
|
21
|
+
use tokio::sync::Mutex;
|
|
22
|
+
|
|
23
|
+
use crate::error;
|
|
24
|
+
use crate::runtime::{block_on, ruby};
|
|
25
|
+
|
|
26
|
+
/// A core error for a session whose value was already taken (closed). Built
|
|
27
|
+
/// inside `block_on`, so it must be a plain Rust error, not a Ruby one.
|
|
28
|
+
fn consumed() -> MicrosandboxError {
|
|
29
|
+
MicrosandboxError::Custom("SSH session is already closed".into())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//--------------------------------------------------------------------------------------------------
|
|
33
|
+
// SshClient
|
|
34
|
+
//--------------------------------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
#[magnus::wrap(class = "Microsandbox::Native::SshClient", free_immediately, size)]
|
|
37
|
+
pub struct SshClient {
|
|
38
|
+
inner: Arc<Mutex<Option<CoreSshClient>>>,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl SshClient {
|
|
42
|
+
pub fn from_core(inner: CoreSshClient) -> Self {
|
|
43
|
+
Self {
|
|
44
|
+
inner: Arc::new(Mutex::new(Some(inner))),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Run a command over SSH and collect {status, stdout, stderr}.
|
|
49
|
+
fn exec(&self, command: String, tty: bool) -> Result<RHash, Error> {
|
|
50
|
+
let inner = Arc::clone(&self.inner);
|
|
51
|
+
let result: MicrosandboxResult<SshOutput> = block_on(async move {
|
|
52
|
+
let guard = inner.lock().await;
|
|
53
|
+
let client = guard.as_ref().ok_or_else(consumed)?;
|
|
54
|
+
client.exec_with(command, |b| b.tty(tty)).await
|
|
55
|
+
});
|
|
56
|
+
Ok(ssh_output_to_hash(result.map_err(error::to_ruby)?))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Attach the local terminal to an interactive SSH shell; returns the exit
|
|
60
|
+
/// status. Host-TTY coupled (raw mode); not meaningful without a real tty.
|
|
61
|
+
fn attach(&self, term: Option<String>, detach_keys: Option<String>) -> Result<i32, Error> {
|
|
62
|
+
let inner = Arc::clone(&self.inner);
|
|
63
|
+
let result: MicrosandboxResult<i32> = block_on(async move {
|
|
64
|
+
let guard = inner.lock().await;
|
|
65
|
+
let client = guard.as_ref().ok_or_else(consumed)?;
|
|
66
|
+
client
|
|
67
|
+
.attach_with(|mut b| {
|
|
68
|
+
if let Some(t) = term {
|
|
69
|
+
b = b.term(t);
|
|
70
|
+
}
|
|
71
|
+
if let Some(k) = detach_keys {
|
|
72
|
+
b = b.detach_keys(k);
|
|
73
|
+
}
|
|
74
|
+
b
|
|
75
|
+
})
|
|
76
|
+
.await
|
|
77
|
+
});
|
|
78
|
+
result.map_err(error::to_ruby)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Open an SFTP session over this SSH connection.
|
|
82
|
+
fn sftp(&self) -> Result<SftpClient, Error> {
|
|
83
|
+
let inner = Arc::clone(&self.inner);
|
|
84
|
+
let result: MicrosandboxResult<CoreSftp> = block_on(async move {
|
|
85
|
+
let guard = inner.lock().await;
|
|
86
|
+
let client = guard.as_ref().ok_or_else(consumed)?;
|
|
87
|
+
client.sftp().await
|
|
88
|
+
});
|
|
89
|
+
Ok(SftpClient::from_core(result.map_err(error::to_ruby)?))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Close the SSH client session. Idempotent.
|
|
93
|
+
fn close(&self) -> Result<(), Error> {
|
|
94
|
+
let inner = Arc::clone(&self.inner);
|
|
95
|
+
let result: MicrosandboxResult<()> = block_on(async move {
|
|
96
|
+
let client = {
|
|
97
|
+
let mut guard = inner.lock().await;
|
|
98
|
+
guard.take()
|
|
99
|
+
};
|
|
100
|
+
match client {
|
|
101
|
+
Some(c) => c.close().await,
|
|
102
|
+
None => Ok(()),
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
result.map_err(error::to_ruby)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn ssh_output_to_hash(output: SshOutput) -> RHash {
|
|
110
|
+
let r = ruby();
|
|
111
|
+
let hash = r.hash_new();
|
|
112
|
+
let _ = hash.aset("status", output.status);
|
|
113
|
+
let _ = hash.aset("success", output.status == 0);
|
|
114
|
+
let _ = hash.aset("stdout", r.str_from_slice(output.stdout.as_ref()));
|
|
115
|
+
let _ = hash.aset("stderr", r.str_from_slice(output.stderr.as_ref()));
|
|
116
|
+
hash
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
//--------------------------------------------------------------------------------------------------
|
|
120
|
+
// SftpClient
|
|
121
|
+
//--------------------------------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
#[magnus::wrap(class = "Microsandbox::Native::SftpClient", free_immediately, size)]
|
|
124
|
+
pub struct SftpClient {
|
|
125
|
+
inner: Arc<Mutex<Option<CoreSftp>>>,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Format an SFTP-layer error as a plain string inside `block_on`; the Ruby
|
|
129
|
+
/// exception is built from it afterward.
|
|
130
|
+
fn sftp_str(e: impl std::fmt::Display) -> String {
|
|
131
|
+
format!("SFTP error: {e}")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
impl SftpClient {
|
|
135
|
+
pub fn from_core(inner: CoreSftp) -> Self {
|
|
136
|
+
Self {
|
|
137
|
+
inner: Arc::new(Mutex::new(Some(inner))),
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn read(&self, path: String) -> Result<RString, Error> {
|
|
142
|
+
let inner = Arc::clone(&self.inner);
|
|
143
|
+
let result: Result<Vec<u8>, String> = block_on(async move {
|
|
144
|
+
let guard = inner.lock().await;
|
|
145
|
+
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
146
|
+
sftp.read(path).await.map_err(sftp_str)
|
|
147
|
+
});
|
|
148
|
+
Ok(ruby().str_from_slice(&result.map_err(error::base_error)?))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn write(&self, path: String, data: RString) -> Result<(), Error> {
|
|
152
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
153
|
+
let inner = Arc::clone(&self.inner);
|
|
154
|
+
let result: Result<(), String> = block_on(async move {
|
|
155
|
+
let guard = inner.lock().await;
|
|
156
|
+
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
157
|
+
let mut file = sftp.create(path).await.map_err(sftp_str)?;
|
|
158
|
+
file.write_all(&bytes).await.map_err(sftp_str)?;
|
|
159
|
+
file.shutdown().await.map_err(sftp_str)?;
|
|
160
|
+
Ok(())
|
|
161
|
+
});
|
|
162
|
+
result.map_err(error::base_error)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn mkdir(&self, path: String) -> Result<(), Error> {
|
|
166
|
+
let inner = Arc::clone(&self.inner);
|
|
167
|
+
let result: Result<(), String> = block_on(async move {
|
|
168
|
+
let guard = inner.lock().await;
|
|
169
|
+
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
170
|
+
sftp.create_dir(path).await.map_err(sftp_str)
|
|
171
|
+
});
|
|
172
|
+
result.map_err(error::base_error)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn remove_file(&self, path: String) -> Result<(), Error> {
|
|
176
|
+
let inner = Arc::clone(&self.inner);
|
|
177
|
+
let result: Result<(), String> = block_on(async move {
|
|
178
|
+
let guard = inner.lock().await;
|
|
179
|
+
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
180
|
+
sftp.remove_file(path).await.map_err(sftp_str)
|
|
181
|
+
});
|
|
182
|
+
result.map_err(error::base_error)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn remove_dir(&self, path: String) -> Result<(), Error> {
|
|
186
|
+
let inner = Arc::clone(&self.inner);
|
|
187
|
+
let result: Result<(), String> = block_on(async move {
|
|
188
|
+
let guard = inner.lock().await;
|
|
189
|
+
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
190
|
+
sftp.remove_dir(path).await.map_err(sftp_str)
|
|
191
|
+
});
|
|
192
|
+
result.map_err(error::base_error)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn rename(&self, old_path: String, new_path: String) -> Result<(), Error> {
|
|
196
|
+
let inner = Arc::clone(&self.inner);
|
|
197
|
+
let result: Result<(), String> = block_on(async move {
|
|
198
|
+
let guard = inner.lock().await;
|
|
199
|
+
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
200
|
+
sftp.rename(old_path, new_path).await.map_err(sftp_str)
|
|
201
|
+
});
|
|
202
|
+
result.map_err(error::base_error)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn symlink(&self, target: String, link_path: String) -> Result<(), Error> {
|
|
206
|
+
let inner = Arc::clone(&self.inner);
|
|
207
|
+
let result: Result<(), String> = block_on(async move {
|
|
208
|
+
let guard = inner.lock().await;
|
|
209
|
+
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
210
|
+
sftp.symlink(target, link_path).await.map_err(sftp_str)
|
|
211
|
+
});
|
|
212
|
+
result.map_err(error::base_error)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn real_path(&self, path: String) -> Result<String, Error> {
|
|
216
|
+
let inner = Arc::clone(&self.inner);
|
|
217
|
+
let result: Result<String, String> = block_on(async move {
|
|
218
|
+
let guard = inner.lock().await;
|
|
219
|
+
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
220
|
+
sftp.canonicalize(path).await.map_err(sftp_str)
|
|
221
|
+
});
|
|
222
|
+
result.map_err(error::base_error)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn read_link(&self, path: String) -> Result<String, Error> {
|
|
226
|
+
let inner = Arc::clone(&self.inner);
|
|
227
|
+
let result: Result<String, String> = block_on(async move {
|
|
228
|
+
let guard = inner.lock().await;
|
|
229
|
+
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
230
|
+
sftp.read_link(path).await.map_err(sftp_str)
|
|
231
|
+
});
|
|
232
|
+
result.map_err(error::base_error)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn close(&self) -> Result<(), Error> {
|
|
236
|
+
let inner = Arc::clone(&self.inner);
|
|
237
|
+
let result: Result<(), String> = block_on(async move {
|
|
238
|
+
let sftp = {
|
|
239
|
+
let mut guard = inner.lock().await;
|
|
240
|
+
guard.take()
|
|
241
|
+
};
|
|
242
|
+
match sftp {
|
|
243
|
+
Some(s) => s.close().await.map_err(sftp_str),
|
|
244
|
+
None => Ok(()),
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
result.map_err(error::base_error)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
//--------------------------------------------------------------------------------------------------
|
|
252
|
+
// SshServer
|
|
253
|
+
//--------------------------------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
#[magnus::wrap(class = "Microsandbox::Native::SshServer", free_immediately, size)]
|
|
256
|
+
pub struct SshServer {
|
|
257
|
+
inner: Arc<Mutex<Option<CoreSshServer>>>,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
impl SshServer {
|
|
261
|
+
pub fn from_core(inner: CoreSshServer) -> Self {
|
|
262
|
+
Self {
|
|
263
|
+
inner: Arc::new(Mutex::new(Some(inner))),
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// Serve one SSH connection over this process's stdin/stdout.
|
|
268
|
+
fn serve_connection(&self) -> Result<(), Error> {
|
|
269
|
+
let inner = Arc::clone(&self.inner);
|
|
270
|
+
let result: MicrosandboxResult<()> = block_on(async move {
|
|
271
|
+
let server = {
|
|
272
|
+
let guard = inner.lock().await;
|
|
273
|
+
guard.as_ref().cloned()
|
|
274
|
+
};
|
|
275
|
+
match server {
|
|
276
|
+
Some(s) => s.serve_connection(SshStdioStream::new()).await,
|
|
277
|
+
None => Err(consumed()),
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
result.map_err(error::to_ruby)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// Release the prepared server endpoint. Idempotent.
|
|
284
|
+
fn close(&self) -> Result<(), Error> {
|
|
285
|
+
let inner = Arc::clone(&self.inner);
|
|
286
|
+
block_on(async move {
|
|
287
|
+
inner.lock().await.take();
|
|
288
|
+
});
|
|
289
|
+
Ok(())
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
294
|
+
let client = native.define_class("SshClient", ruby.class_object())?;
|
|
295
|
+
client.define_method("exec", method!(SshClient::exec, 2))?;
|
|
296
|
+
client.define_method("attach", method!(SshClient::attach, 2))?;
|
|
297
|
+
client.define_method("sftp", method!(SshClient::sftp, 0))?;
|
|
298
|
+
client.define_method("close", method!(SshClient::close, 0))?;
|
|
299
|
+
|
|
300
|
+
let sftp = native.define_class("SftpClient", ruby.class_object())?;
|
|
301
|
+
sftp.define_method("read", method!(SftpClient::read, 1))?;
|
|
302
|
+
sftp.define_method("write", method!(SftpClient::write, 2))?;
|
|
303
|
+
sftp.define_method("mkdir", method!(SftpClient::mkdir, 1))?;
|
|
304
|
+
sftp.define_method("remove_file", method!(SftpClient::remove_file, 1))?;
|
|
305
|
+
sftp.define_method("remove_dir", method!(SftpClient::remove_dir, 1))?;
|
|
306
|
+
sftp.define_method("rename", method!(SftpClient::rename, 2))?;
|
|
307
|
+
sftp.define_method("symlink", method!(SftpClient::symlink, 2))?;
|
|
308
|
+
sftp.define_method("real_path", method!(SftpClient::real_path, 1))?;
|
|
309
|
+
sftp.define_method("read_link", method!(SftpClient::read_link, 1))?;
|
|
310
|
+
sftp.define_method("close", method!(SftpClient::close, 0))?;
|
|
311
|
+
|
|
312
|
+
let server = native.define_class("SshServer", ruby.class_object())?;
|
|
313
|
+
server.define_method("serve_connection", method!(SshServer::serve_connection, 0))?;
|
|
314
|
+
server.define_method("close", method!(SshServer::close, 0))?;
|
|
315
|
+
|
|
316
|
+
Ok(())
|
|
317
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Microsandbox
|
|
4
|
+
# A single raw agent-protocol frame: a correlation id, flag bits, and a
|
|
5
|
+
# CBOR-encoded body. Decode {#body} with a CBOR library of your choice.
|
|
6
|
+
class AgentFrame
|
|
7
|
+
# @return [Integer] correlation id from the frame header
|
|
8
|
+
attr_reader :id
|
|
9
|
+
# @return [Integer] flag bits (see {AgentClient::FLAG_TERMINAL} etc.)
|
|
10
|
+
attr_reader :flags
|
|
11
|
+
# @return [String] raw CBOR body bytes (ASCII-8BIT)
|
|
12
|
+
attr_reader :body
|
|
13
|
+
|
|
14
|
+
def initialize(data)
|
|
15
|
+
@id = data["id"]
|
|
16
|
+
@flags = data["flags"]
|
|
17
|
+
@body = data["body"]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Boolean] last frame of a stream
|
|
21
|
+
def terminal? = (@flags & AgentClient::FLAG_TERMINAL) != 0
|
|
22
|
+
|
|
23
|
+
# @return [Boolean] opens a new session
|
|
24
|
+
def session_start? = (@flags & AgentClient::FLAG_SESSION_START) != 0
|
|
25
|
+
|
|
26
|
+
# @return [Boolean] connection-shutdown signal
|
|
27
|
+
def shutdown? = (@flags & AgentClient::FLAG_SHUTDOWN) != 0
|
|
28
|
+
|
|
29
|
+
def inspect
|
|
30
|
+
"#<Microsandbox::AgentFrame id=#{@id} flags=0x#{@flags.to_s(16)} body=#{@body&.bytesize}B>"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# An open raw agent stream, from {AgentClient#stream}. {Enumerable}: iterate to
|
|
35
|
+
# consume {AgentFrame}s until the stream ends (the terminal frame is delivered,
|
|
36
|
+
# then iteration stops). Mirrors the official SDKs' `AgentStream`.
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# stream = client.stream(0, request_body)
|
|
40
|
+
# stream.each { |frame| handle(frame) }
|
|
41
|
+
class AgentStream
|
|
42
|
+
include Enumerable
|
|
43
|
+
|
|
44
|
+
# @return [Integer] the protocol correlation id (pass to {AgentClient#send_frame})
|
|
45
|
+
attr_reader :id
|
|
46
|
+
|
|
47
|
+
def initialize(native, id, handle)
|
|
48
|
+
@native = native
|
|
49
|
+
@id = id
|
|
50
|
+
@handle = handle
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Pull the next frame, or nil at end-of-stream.
|
|
54
|
+
# @return [AgentFrame, nil]
|
|
55
|
+
def recv
|
|
56
|
+
frame = @native.stream_next(@handle)
|
|
57
|
+
frame && AgentFrame.new(frame)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @yieldparam frame [AgentFrame]
|
|
61
|
+
# @return [self, Enumerator]
|
|
62
|
+
def each
|
|
63
|
+
return enum_for(:each) unless block_given?
|
|
64
|
+
|
|
65
|
+
while (frame = recv)
|
|
66
|
+
yield frame
|
|
67
|
+
end
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Close the stream and release its handle. Idempotent.
|
|
72
|
+
# @return [nil]
|
|
73
|
+
def close
|
|
74
|
+
@native.stream_close(@handle)
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A low-level **raw agent client** — the byte-level transport to a sandbox's
|
|
80
|
+
# `agentd` over its relay socket. This is the rawest tier of the SDK: it moves
|
|
81
|
+
# CBOR-encoded protocol frames in and out; encoding/decoding the bodies is up
|
|
82
|
+
# to you. Most users want {Sandbox#exec}/{Sandbox#fs} instead; reach for this
|
|
83
|
+
# to drive `agentd` protocol features the high-level API does not expose, or to
|
|
84
|
+
# bridge the relay socket to another transport (see {socket_path}).
|
|
85
|
+
#
|
|
86
|
+
# Mirrors the `AgentClient` in the official Python/Node/Go SDKs.
|
|
87
|
+
#
|
|
88
|
+
# @example one-shot request/response
|
|
89
|
+
# Microsandbox::AgentClient.connect_sandbox("my-box") do |client|
|
|
90
|
+
# frame = client.request(0, cbor_encoded_request)
|
|
91
|
+
# handle(frame.body)
|
|
92
|
+
# end
|
|
93
|
+
class AgentClient
|
|
94
|
+
# Frame flag bits (mirror the protocol constants in the other SDKs).
|
|
95
|
+
FLAG_TERMINAL = 0b0000_0001
|
|
96
|
+
FLAG_SESSION_START = 0b0000_0010
|
|
97
|
+
FLAG_SHUTDOWN = 0b0000_0100
|
|
98
|
+
|
|
99
|
+
class << self
|
|
100
|
+
# Connect to a running sandbox by name (max 128 UTF-8 bytes). With a block,
|
|
101
|
+
# the client is yielded and closed when the block returns.
|
|
102
|
+
# @param name [String]
|
|
103
|
+
# @param timeout [Numeric, nil] handshake timeout in seconds (default ~10s)
|
|
104
|
+
# @yieldparam client [AgentClient]
|
|
105
|
+
# @return [AgentClient, Object]
|
|
106
|
+
def connect_sandbox(name, timeout: nil, &block)
|
|
107
|
+
wrap(Native::AgentClient.connect_sandbox(name.to_s, timeout && Float(timeout)), &block)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Connect to an agentd relay socket by path. See {connect_sandbox}.
|
|
111
|
+
# @return [AgentClient, Object]
|
|
112
|
+
def connect_path(path, timeout: nil, &block)
|
|
113
|
+
wrap(Native::AgentClient.connect_path(path.to_s, timeout && Float(timeout)), &block)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Resolve a sandbox's agent relay socket path **without** connecting — the
|
|
117
|
+
# same path {connect_sandbox} would dial. Useful for bridging the socket to
|
|
118
|
+
# another byte transport. The sandbox need not be running.
|
|
119
|
+
# @return [String]
|
|
120
|
+
def socket_path(name)
|
|
121
|
+
Native::AgentClient.socket_path(name.to_s)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def wrap(native, &block)
|
|
127
|
+
client = new(native)
|
|
128
|
+
return client unless block
|
|
129
|
+
|
|
130
|
+
begin
|
|
131
|
+
block.call(client)
|
|
132
|
+
ensure
|
|
133
|
+
client.close
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def initialize(native)
|
|
139
|
+
@native = native
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Send one frame and await a single response frame.
|
|
143
|
+
# @param flags [Integer] frame flag bits
|
|
144
|
+
# @param body [String] CBOR-encoded body bytes
|
|
145
|
+
# @return [AgentFrame]
|
|
146
|
+
def request(flags, body)
|
|
147
|
+
AgentFrame.new(@native.request(Integer(flags), body.to_s))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Open a streaming session.
|
|
151
|
+
# @param flags [Integer]
|
|
152
|
+
# @param body [String]
|
|
153
|
+
# @return [AgentStream]
|
|
154
|
+
def stream(flags, body)
|
|
155
|
+
opened = @native.stream_open(Integer(flags), body.to_s)
|
|
156
|
+
AgentStream.new(@native, opened["id"], opened["handle"])
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Send a follow-up frame on an existing correlation id (e.g. a stream's
|
|
160
|
+
# {AgentStream#id}). Named +send_frame+ rather than +send+ so it does not
|
|
161
|
+
# shadow Ruby's `Object#send`. Maps to the protocol "send" in the other SDKs.
|
|
162
|
+
# @return [nil]
|
|
163
|
+
def send_frame(id, flags, body)
|
|
164
|
+
@native.send(Integer(id), Integer(flags), body.to_s)
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The cached handshake `core.ready` frame body (CBOR bytes).
|
|
169
|
+
# @return [String]
|
|
170
|
+
def ready_bytes
|
|
171
|
+
@native.ready_bytes
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Close the connection. Idempotent.
|
|
175
|
+
# @return [nil]
|
|
176
|
+
def close
|
|
177
|
+
@native.close
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|