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