1use crate::v8_ipc::{self, BinaryFrame};
4use agentos_runtime::RuntimeContext;
5use agentos_v8_runtime::embedded_runtime::{spawn_embedded_runtime_ipc, EmbeddedRuntimeHandle};
6use serde_json::Value;
7use std::io::{self, BufReader, Read, Write};
8use std::os::unix::net::UnixStream;
9use std::sync::{Arc, Mutex};
10
11pub struct V8Runtime {
13 runtime: EmbeddedRuntimeHandle,
14 reader: BufReader<UnixStream>,
15 writer: UnixStream,
16}
17
18impl V8Runtime {
19 pub fn spawn(runtime_context: &RuntimeContext) -> io::Result<Self> {
21 let (stream, runtime) = spawn_embedded_runtime_ipc(None, runtime_context.clone())?;
22 let writer = stream.try_clone()?;
23 let reader = BufReader::new(stream);
24
25 Ok(V8Runtime {
26 runtime,
27 reader,
28 writer,
29 })
30 }
31
32 pub fn create_session(
34 &mut self,
35 session_id: &str,
36 heap_limit_mb: u32,
37 cpu_time_limit_ms: u32,
38 wall_clock_limit_ms: u32,
39 ) -> io::Result<()> {
40 self.send_frame(&BinaryFrame::CreateSession {
41 session_id: session_id.to_owned(),
42 heap_limit_mb,
43 cpu_time_limit_ms,
44 wall_clock_limit_ms,
45 })
46 }
47
48 pub fn inject_globals(&mut self, session_id: &str, payload: Vec<u8>) -> io::Result<()> {
50 self.send_frame(&BinaryFrame::InjectGlobals {
51 session_id: session_id.to_owned(),
52 payload,
53 })
54 }
55
56 pub fn execute(
58 &mut self,
59 session_id: &str,
60 mode: u8,
61 file_path: &str,
62 bridge_code: &str,
63 user_code: &str,
64 ) -> io::Result<()> {
65 self.send_frame(&BinaryFrame::Execute {
66 session_id: session_id.to_owned(),
67 mode,
68 file_path: file_path.to_owned(),
69 bridge_code: bridge_code.to_owned(),
70 post_restore_script: String::new(),
71 userland_code: String::new(),
72 high_resolution_time: false,
73 user_code: user_code.to_owned(),
74 })
75 }
76
77 pub fn send_bridge_response(
79 &mut self,
80 session_id: &str,
81 call_id: u64,
82 status: u8,
83 payload: Vec<u8>,
84 ) -> io::Result<()> {
85 self.send_frame(&BinaryFrame::BridgeResponse {
86 session_id: session_id.to_owned(),
87 call_id,
88 status,
89 payload,
90 })
91 }
92
93 pub fn send_stream_event(
95 &mut self,
96 session_id: &str,
97 event_type: &str,
98 payload: Vec<u8>,
99 ) -> io::Result<()> {
100 self.send_frame(&BinaryFrame::StreamEvent {
101 session_id: session_id.to_owned(),
102 event_type: event_type.to_owned(),
103 payload,
104 })
105 }
106
107 pub fn terminate_execution(&mut self, session_id: &str) -> io::Result<()> {
109 self.send_frame(&BinaryFrame::TerminateExecution {
110 session_id: session_id.to_owned(),
111 })
112 }
113
114 pub fn destroy_session(&mut self, session_id: &str) -> io::Result<()> {
116 self.send_frame(&BinaryFrame::DestroySession {
117 session_id: session_id.to_owned(),
118 })
119 }
120
121 pub fn read_frame(&mut self) -> io::Result<BinaryFrame> {
123 let mut len_buf = [0u8; 4];
124 self.reader.read_exact(&mut len_buf)?;
125 let total_len = u32::from_be_bytes(len_buf);
126
127 if total_len > 64 * 1024 * 1024 {
128 return Err(io::Error::new(
129 io::ErrorKind::InvalidData,
130 format!("frame size {total_len} exceeds maximum"),
131 ));
132 }
133
134 let mut buf = vec![0u8; total_len as usize];
135 self.reader.read_exact(&mut buf)?;
136 v8_ipc::decode_frame(&buf)
137 }
138
139 fn send_frame(&mut self, frame: &BinaryFrame) -> io::Result<()> {
140 let bytes = v8_ipc::encode_frame(frame)?;
141 self.writer.write_all(&bytes)?;
142 self.writer.flush()
143 }
144}
145
146impl Drop for V8Runtime {
147 fn drop(&mut self) {
148 self.runtime.shutdown();
149 }
150}
151
152pub struct SharedV8Runtime {
154 inner: Arc<Mutex<V8Runtime>>,
155}
156
157impl SharedV8Runtime {
158 pub fn new(runtime: V8Runtime) -> Self {
159 Self {
160 inner: Arc::new(Mutex::new(runtime)),
161 }
162 }
163
164 pub fn lock(&self) -> std::sync::MutexGuard<'_, V8Runtime> {
165 self.inner.lock().expect("V8 runtime lock poisoned")
166 }
167}
168
169impl Clone for SharedV8Runtime {
170 fn clone(&self) -> Self {
171 Self {
172 inner: self.inner.clone(),
173 }
174 }
175}
176
177pub fn map_bridge_method(method: &str) -> (&str, bool) {
182 if let Some(target) = agentos_bridge::bridge_contract().dispatch.get(method) {
183 (target.method.as_str(), target.translate_args)
184 } else {
185 (method, false)
186 }
187}
188
189pub fn cbor_payload_to_json_args(payload: &[u8]) -> io::Result<Vec<Value>> {
192 if payload.is_empty() {
193 return Ok(vec![]);
194 }
195 let cbor_value: ciborium::value::Value = ciborium::de::from_reader(payload).map_err(|e| {
196 io::Error::new(
197 io::ErrorKind::InvalidData,
198 format!("failed to deserialize CBOR bridge call payload: {e}"),
199 )
200 })?;
201 match cbor_to_json(cbor_value) {
202 Value::Array(arr) => Ok(arr),
203 single => Ok(vec![single]),
204 }
205}
206
207pub fn cbor_payload_raw_byte_arg(payload: &[u8], index: usize) -> io::Result<Option<Vec<u8>>> {
208 if payload.is_empty() {
209 return Ok(None);
210 }
211 let cbor_value: ciborium::value::Value = ciborium::de::from_reader(payload).map_err(|e| {
212 io::Error::new(
213 io::ErrorKind::InvalidData,
214 format!("failed to deserialize CBOR bridge call payload: {e}"),
215 )
216 })?;
217 let Some(value) = cbor_array_arg(&cbor_value, index) else {
218 return Ok(None);
219 };
220 Ok(cbor_raw_bytes(value).map(ToOwned::to_owned))
221}
222
223pub fn json_to_cbor_payload(value: &Value) -> io::Result<Vec<u8>> {
225 let cbor_value = json_to_cbor(value);
226 let mut buf = Vec::new();
227 ciborium::ser::into_writer(&cbor_value, &mut buf).map_err(|e| {
228 io::Error::new(
229 io::ErrorKind::InvalidData,
230 format!("failed to serialize CBOR bridge response: {e}"),
231 )
232 })?;
233 Ok(buf)
234}
235
236fn cbor_array_arg(value: &ciborium::value::Value, index: usize) -> Option<&ciborium::value::Value> {
237 match value {
238 ciborium::value::Value::Array(values) => values.get(index),
239 value if index == 0 => Some(value),
240 _ => None,
241 }
242}
243
244fn cbor_raw_bytes(value: &ciborium::value::Value) -> Option<&[u8]> {
245 match value {
246 ciborium::value::Value::Bytes(bytes) => Some(bytes),
247 ciborium::value::Value::Tag(_, inner) => cbor_raw_bytes(inner),
248 _ => None,
249 }
250}
251
252fn cbor_to_json(value: ciborium::value::Value) -> Value {
253 use ciborium::value::Value as Cbor;
254 match value {
255 Cbor::Null => Value::Null,
256 Cbor::Bool(b) => Value::Bool(b),
257 Cbor::Integer(i) => {
258 let n: i128 = i.into();
259 if let Ok(n) = i64::try_from(n) {
260 Value::Number(n.into())
261 } else if let Ok(n) = u64::try_from(n) {
262 Value::Number(n.into())
263 } else {
264 Value::Number(serde_json::Number::from_f64(n as f64).unwrap_or(0.into()))
265 }
266 }
267 Cbor::Float(f) => serde_json::Number::from_f64(f)
268 .map(Value::Number)
269 .unwrap_or(Value::Null),
270 Cbor::Text(s) => Value::String(s),
271 Cbor::Bytes(b) => {
272 use serde_json::json;
273 json!({ "__type": "Buffer", "data": base64_encode(&b) })
275 }
276 Cbor::Array(arr) => Value::Array(arr.into_iter().map(cbor_to_json).collect()),
277 Cbor::Map(map) => {
278 let mut obj = serde_json::Map::new();
279 for (k, v) in map {
280 let key = match k {
281 Cbor::Text(s) => s,
282 Cbor::Integer(i) => {
283 let n: i128 = i.into();
284 n.to_string()
285 }
286 other => format!("{other:?}"),
287 };
288 obj.insert(key, cbor_to_json(v));
289 }
290 Value::Object(obj)
291 }
292 Cbor::Tag(_, inner) => cbor_to_json(*inner),
293 _ => Value::Null,
294 }
295}
296
297fn json_to_cbor(value: &Value) -> ciborium::value::Value {
298 use ciborium::value::Value as Cbor;
299 match value {
300 Value::Null => Cbor::Null,
301 Value::Bool(b) => Cbor::Bool(*b),
302 Value::Number(n) => {
303 if let Some(i) = n.as_i64() {
304 Cbor::Integer(i.into())
305 } else if let Some(u) = n.as_u64() {
306 Cbor::Integer(u.into())
307 } else if let Some(f) = n.as_f64() {
308 Cbor::Float(f)
309 } else {
310 Cbor::Null
311 }
312 }
313 Value::String(s) => Cbor::Text(s.clone()),
314 Value::Array(arr) => Cbor::Array(arr.iter().map(json_to_cbor).collect()),
315 Value::Object(map) => {
316 if map.get("__type").and_then(Value::as_str) == Some("Buffer") {
318 if let Some(data) = map.get("data").and_then(Value::as_str) {
319 if let Ok(bytes) = base64_decode(data) {
320 return Cbor::Bytes(bytes);
321 }
322 }
323 }
324 Cbor::Map(
325 map.iter()
326 .map(|(k, v)| (Cbor::Text(k.clone()), json_to_cbor(v)))
327 .collect(),
328 )
329 }
330 }
331}
332
333pub fn base64_encode_pub(data: &[u8]) -> String {
335 base64_encode(data)
336}
337
338pub fn base64_decode_pub(input: &str) -> Option<Vec<u8>> {
339 base64_decode(input).ok()
340}
341
342fn base64_encode(data: &[u8]) -> String {
343 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
344 let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
345 for chunk in data.chunks(3) {
346 let b0 = chunk[0] as u32;
347 let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
348 let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
349 let triple = (b0 << 16) | (b1 << 8) | b2;
350 result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
351 result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
352 if chunk.len() > 1 {
353 result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
354 } else {
355 result.push('=');
356 }
357 if chunk.len() > 2 {
358 result.push(CHARS[(triple & 0x3F) as usize] as char);
359 } else {
360 result.push('=');
361 }
362 }
363 result
364}
365
366fn base64_decode(input: &str) -> Result<Vec<u8>, ()> {
367 fn decode_char(c: u8) -> Result<u8, ()> {
368 match c {
369 b'A'..=b'Z' => Ok(c - b'A'),
370 b'a'..=b'z' => Ok(c - b'a' + 26),
371 b'0'..=b'9' => Ok(c - b'0' + 52),
372 b'+' => Ok(62),
373 b'/' => Ok(63),
374 b'=' => Ok(0),
375 _ => Err(()),
376 }
377 }
378 let bytes = input.as_bytes();
379 let mut result = Vec::with_capacity(bytes.len() * 3 / 4);
380 for chunk in bytes.chunks(4) {
381 if chunk.len() < 4 {
382 return Err(());
383 }
384 let a = decode_char(chunk[0])?;
385 let b = decode_char(chunk[1])?;
386 let c = decode_char(chunk[2])?;
387 let d = decode_char(chunk[3])?;
388 let triple = ((a as u32) << 18) | ((b as u32) << 12) | ((c as u32) << 6) | (d as u32);
389 result.push((triple >> 16) as u8);
390 if chunk[2] != b'=' {
391 result.push((triple >> 8) as u8);
392 }
393 if chunk[3] != b'=' {
394 result.push(triple as u8);
395 }
396 }
397 Ok(result)
398}
399
400#[cfg(test)]
401mod tests {
402 use super::{json_to_cbor_payload, map_bridge_method};
403 use serde_json::json;
404
405 #[test]
406 fn cbor_byte_string_boundary_includes_five_byte_header() {
407 let payload_limit = 256 * 1024;
408 let raw_bytes = payload_limit - 5;
409 let encoded = json_to_cbor_payload(&json!({
410 "__type": "Buffer",
411 "data": super::base64_encode_pub(&vec![0xA5; raw_bytes]),
412 }))
413 .expect("encode boundary byte string");
414 assert_eq!(encoded.len(), payload_limit);
415
416 let oversized = json_to_cbor_payload(&json!({
417 "__type": "Buffer",
418 "data": super::base64_encode_pub(&vec![0xA5; raw_bytes + 1]),
419 }))
420 .expect("encode oversized byte string");
421 assert_eq!(oversized.len(), payload_limit + 1);
422 }
423
424 #[test]
425 fn audited_bridge_methods_map_to_named_handlers() {
426 for method in [
427 "_cryptoHashDigest",
428 "_cryptoSubtle",
429 "_networkHttp2ServerListenRaw",
430 "_networkHttpServerRequestRaw",
431 "_networkHttp2SessionConnectRaw",
432 "_networkHttp2StreamRespondRaw",
433 "_upgradeSocketWriteRaw",
434 "_netSocketSetNoDelayRaw",
435 "_kernelStdioWriteRaw",
436 "_kernelPollRaw",
437 "_kernelFlockRaw",
438 "_kernelTtySizeRaw",
439 "_netSocketUpgradeTlsRaw",
440 "_tlsGetCiphersRaw",
441 "_dgramSocketAddressRaw",
442 "_dgramSocketSetBufferSizeRaw",
443 ] {
444 let (mapped, _) = map_bridge_method(method);
445 assert_ne!(mapped, method, "missing bridge-method mapping for {method}");
446 }
447 }
448
449 #[test]
450 fn http_request_bridge_shortcut_is_not_mapped() {
451 assert_eq!(
452 map_bridge_method("_networkHttpRequestRaw"),
453 ("_networkHttpRequestRaw", false)
454 );
455 }
456}