Skip to main content

agentos_execution/
v8_runtime.rs

1//! V8 isolate runtime manager backed by the embedded V8 runtime.
2
3use 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
10/// Manages an embedded V8 runtime and its IPC connection.
11pub struct V8Runtime {
12    runtime: EmbeddedRuntimeHandle,
13    reader: BufReader<UnixStream>,
14    writer: UnixStream,
15}
16
17impl V8Runtime {
18    /// Spawn the embedded V8 runtime and connect over IPC.
19    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    /// Create a new V8 isolate session.
32    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    /// Inject per-session globals (processConfig, osConfig) as CBOR payload.
48    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    /// Execute bridge code + user code in a session.
56    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    /// Send a bridge response back to the V8 isolate.
77    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    /// Send a stream event to the V8 isolate (stdin data, timer, child process events).
93    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    /// Terminate execution in a session.
107    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    /// Destroy a session.
114    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    /// Read the next frame from the V8 runtime.
121    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
151/// Thread-safe wrapper for V8Runtime that allows sending from multiple threads.
152pub 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
176/// Bridge call method name mapping from V8 polyfill names to sidecar sync RPC names.
177/// The V8 polyfills use underscore-prefixed camelCase names while the sidecar
178/// uses dot-separated category.method names. The mapping lives in
179/// `bridge-contract.json` so bridge installation and dispatch drift together.
180pub 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
188/// Deserialize a CBOR payload into a JSON array of arguments.
189/// The V8 bridge serializes bridge call args as a CBOR array.
190pub 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
222/// Serialize a JSON value to CBOR bytes for bridge responses.
223pub 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            // Encode binary data as base64 with a type marker
273            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            // Check for Buffer type marker
316            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
332/// Public base64 encode for use in bridge call handlers.
333pub 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}