1use crate::v8_ipc::{self, BinaryFrame};
4use agentos_bridge::queue_tracker::{tracked_sync_channel, TrackedLimit, TrackedReceiver};
5use agentos_v8_runtime::embedded_runtime::{
6 shared_embedded_runtime, EmbeddedV8Runtime, EmbeddedV8SessionHandle,
7};
8use agentos_v8_runtime::runtime_protocol::{RuntimeCommand, RuntimeEvent, WarmSessionHint};
9use std::io::{self, Cursor};
10use std::sync::{Arc, OnceLock};
11use std::thread;
12
13const V8_SESSION_FRAME_CHANNEL_CAPACITY: usize = 1024;
14
15const V8_BRIDGE_CODE: &str = concat!(
17 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge.js")),
18 "\n",
19 include_str!(concat!(env!("OUT_DIR"), "/v8-bridge-zlib.js"))
20);
21
22pub struct V8RuntimeHost {
24 shared: Arc<SharedEmbeddedRuntimeClient>,
25}
26
27struct SharedEmbeddedRuntimeClient {
28 runtime: Arc<EmbeddedV8Runtime>,
29}
30
31impl V8RuntimeHost {
32 pub fn spawn() -> io::Result<Self> {
34 Ok(V8RuntimeHost {
35 shared: shared_embedded_runtime_client()?,
36 })
37 }
38
39 pub fn register_session(&self, session_id: &str) -> io::Result<TrackedReceiver<BinaryFrame>> {
41 let (runtime_receiver, registration) = self
42 .shared
43 .runtime
44 .register_session_with_output_registration(session_id)?;
45 let (sender, receiver) = tracked_sync_channel(
46 TrackedLimit::V8SessionFrames,
47 V8_SESSION_FRAME_CHANNEL_CAPACITY,
48 );
49 let thread_name = format!("secure-exec-v8-session-{session_id}");
50 let runtime = Arc::clone(&self.shared.runtime);
51 let runtime_for_thread = Arc::clone(&runtime);
52
53 let spawn_result = thread::Builder::new().name(thread_name).spawn(move || {
54 while let Ok(frame) = runtime_receiver.recv() {
55 if sender.send(from_runtime_event(frame)).is_err() {
65 let _ = runtime_for_thread.destroy_session_if_output_current(®istration);
66 break;
67 }
68 }
69 });
70 if let Err(error) = spawn_result {
71 runtime.unregister_session(session_id);
72 return Err(error);
73 }
74
75 Ok(receiver)
76 }
77
78 pub fn unregister_session(&self, session_id: &str) {
80 self.shared.runtime.unregister_session(session_id);
81 }
82
83 pub fn create_session(
84 &self,
85 session_id: String,
86 heap_limit_mb: u32,
87 cpu_time_limit_ms: u32,
88 wall_clock_limit_ms: u32,
89 warm_hint: Option<WarmSessionHint>,
90 ) -> io::Result<()> {
91 self.shared.runtime.dispatch(RuntimeCommand::CreateSession {
92 session_id,
93 heap_limit_mb: non_zero_option(heap_limit_mb),
94 cpu_time_limit_ms: non_zero_option(cpu_time_limit_ms),
95 wall_clock_limit_ms: non_zero_option(wall_clock_limit_ms),
96 warm_hint,
97 })
98 }
99
100 pub fn create_session_from_command(&self, command: RuntimeCommand) -> io::Result<()> {
101 self.shared.runtime.dispatch(command)
102 }
103
104 pub fn send_frame(&self, frame: &BinaryFrame) -> io::Result<()> {
106 self.shared.runtime.dispatch(to_runtime_command(frame)?)
107 }
108
109 pub fn bridge_code() -> &'static str {
111 V8_BRIDGE_CODE
112 }
113
114 pub fn pre_warm_snapshot(&self, userland_code: &str) -> io::Result<()> {
120 if userland_code.is_empty() {
121 return Ok(());
122 }
123 self.shared.runtime.dispatch(RuntimeCommand::WarmSnapshot {
124 bridge_code: Self::bridge_code().to_owned(),
125 userland_code: userland_code.to_owned(),
126 })
127 }
128
129 pub fn pre_warm_workers(&self, userland_code: &str, heap_limit_mb: u32, count: usize) {
130 self.shared.runtime.pre_warm_workers(
131 Self::bridge_code().to_owned(),
132 userland_code.to_owned(),
133 non_zero_option(heap_limit_mb),
134 count,
135 );
136 }
137
138 pub fn seed_default_warm_workers_async(&self) {
139 static DEFAULT_WARM_STARTED: OnceLock<()> = OnceLock::new();
140 let runtime = Arc::clone(&self.shared.runtime);
141 let _ = DEFAULT_WARM_STARTED.get_or_init(|| {
142 let _ = thread::Builder::new()
143 .name(String::from("secure-exec-v8-default-warm"))
144 .spawn(move || {
145 runtime.pre_warm_workers(
146 V8_BRIDGE_CODE.to_owned(),
147 String::new(),
148 None,
149 warm_worker_count(),
150 );
151 });
152 });
153 }
154
155 pub fn snapshot_ready(&self, userland_code: &str) -> bool {
158 self.shared
159 .runtime
160 .snapshot_ready(Self::bridge_code(), userland_code)
161 }
162
163 pub fn warm_snapshot_async(userland_code: String) {
167 if userland_code.is_empty() {
168 return;
169 }
170 static WASM_RUNNER_WARM_STARTED: OnceLock<()> = OnceLock::new();
171 let _ = WASM_RUNNER_WARM_STARTED.get_or_init(|| {
172 let _ = thread::Builder::new()
173 .name(String::from("secure-exec-wasm-snapshot-warm"))
174 .spawn(move || {
175 let Ok(host) = V8RuntimeHost::spawn() else {
176 return;
177 };
178 if let Err(error) = host.pre_warm_snapshot(&userland_code) {
179 eprintln!("agentos-v8-runtime: wasm runner snapshot warm failed: {error}");
180 }
181 });
182 });
183 }
184
185 pub fn session_handle(&self, session_id: String) -> V8SessionHandle {
187 V8SessionHandle::new(session_id, Arc::clone(&self.shared.runtime))
188 }
189
190 pub fn child_pid(&self) -> u32 {
191 0
192 }
193
194 pub fn is_alive(&mut self) -> io::Result<bool> {
195 Ok(self.shared.runtime.is_alive())
196 }
197
198 #[cfg(test)]
199 fn runtime_ptr(&self) -> usize {
200 Arc::as_ptr(&self.shared.runtime) as usize
201 }
202}
203
204fn non_zero_option(value: u32) -> Option<u32> {
205 (value > 0).then_some(value)
206}
207
208fn warm_worker_count() -> usize {
209 std::env::var("AGENTOS_V8_WARM_ISOLATES")
210 .ok()
211 .and_then(|value| value.parse::<usize>().ok())
212 .unwrap_or(2)
213}
214
215pub struct V8SessionHandle {
218 inner: EmbeddedV8SessionHandle,
219}
220
221impl std::fmt::Debug for V8SessionHandle {
222 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223 f.debug_struct("V8SessionHandle")
224 .field("session_id", &self.inner.session_id())
225 .finish()
226 }
227}
228
229impl V8SessionHandle {
230 pub fn new(session_id: String, runtime: Arc<EmbeddedV8Runtime>) -> Self {
231 Self {
232 inner: runtime.session_handle(session_id),
233 }
234 }
235
236 pub fn send_bridge_response(
238 &self,
239 call_id: u64,
240 status: u8,
241 payload: Vec<u8>,
242 ) -> io::Result<()> {
243 self.inner.send_bridge_response(call_id, status, payload)
244 }
245
246 pub fn send_stream_event(&self, event_type: &str, payload: Vec<u8>) -> io::Result<()> {
248 self.inner.send_stream_event(event_type, payload)
249 }
250
251 pub fn set_module_reader(
254 &self,
255 reader: Box<dyn agentos_v8_runtime::execution::GuestModuleReader>,
256 ) -> io::Result<()> {
257 self.inner.set_module_reader(reader)
258 }
259
260 #[allow(clippy::too_many_arguments)] pub fn execute(
264 &self,
265 mode: u8,
266 file_path: String,
267 bridge_code: String,
268 post_restore_script: String,
269 userland_code: String,
270 high_resolution_time: bool,
271 user_code: String,
272 wasm_module_bytes: Option<Arc<Vec<u8>>>,
273 ) -> io::Result<()> {
274 self.inner.execute(
275 mode,
276 file_path,
277 bridge_code,
278 post_restore_script,
279 userland_code,
280 high_resolution_time,
281 user_code,
282 wasm_module_bytes,
283 )
284 }
285
286 pub fn terminate(&self) -> io::Result<()> {
288 self.inner.terminate()
289 }
290
291 pub fn destroy(&self) -> io::Result<()> {
293 let _ = self.inner.terminate();
294 self.inner.destroy()
295 }
296
297 pub fn session_id(&self) -> &str {
298 self.inner.session_id()
299 }
300}
301
302impl Clone for V8SessionHandle {
303 fn clone(&self) -> Self {
304 Self {
305 inner: self.inner.clone(),
306 }
307 }
308}
309
310pub fn ensure_runtime_initialized() -> io::Result<()> {
319 shared_embedded_runtime_client().map(|_| ())
320}
321
322pub fn pre_warm_agent_snapshot(userland_code: &str) -> io::Result<()> {
323 if userland_code.is_empty() {
324 return Ok(());
325 }
326 let userland = userland_code.to_owned();
327 let handle = std::thread::Builder::new()
333 .name("agentos-snapshot-prewarm".to_owned())
334 .spawn(move || -> io::Result<()> {
335 let client = shared_embedded_runtime_client()?;
336 client.runtime.dispatch(RuntimeCommand::WarmSnapshot {
337 bridge_code: V8_BRIDGE_CODE.to_owned(),
338 userland_code: userland,
339 })
340 })?;
341 handle
342 .join()
343 .map_err(|_| io::Error::other("snapshot pre-warm thread panicked"))?
344}
345
346fn shared_embedded_runtime_client() -> io::Result<Arc<SharedEmbeddedRuntimeClient>> {
347 static SHARED_RUNTIME: OnceLock<Arc<SharedEmbeddedRuntimeClient>> = OnceLock::new();
348 static SHARED_RUNTIME_INIT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
349
350 if let Some(shared) = SHARED_RUNTIME.get() {
351 return Ok(Arc::clone(shared));
352 }
353
354 let _guard = SHARED_RUNTIME_INIT_LOCK
355 .lock()
356 .expect("shared embedded runtime init lock poisoned");
357 if let Some(shared) = SHARED_RUNTIME.get() {
358 return Ok(Arc::clone(shared));
359 }
360
361 let shared = Arc::new(SharedEmbeddedRuntimeClient {
362 runtime: shared_embedded_runtime()?,
363 });
364 let _ = SHARED_RUNTIME.set(Arc::clone(&shared));
365 Ok(shared)
366}
367
368fn to_runtime_command(frame: &BinaryFrame) -> io::Result<RuntimeCommand> {
369 let bytes = v8_ipc::encode_frame(frame)?;
370 let runtime_frame = agentos_v8_runtime::ipc_binary::read_frame(&mut Cursor::new(bytes))?;
371 RuntimeCommand::try_from(runtime_frame)
372}
373
374fn from_runtime_event(event: RuntimeEvent) -> BinaryFrame {
375 match event {
376 RuntimeEvent::BridgeCall {
377 session_id,
378 call_id,
379 method,
380 payload,
381 } => BinaryFrame::BridgeCall {
382 session_id,
383 call_id,
384 method,
385 payload,
386 },
387 RuntimeEvent::ExecutionResult {
388 session_id,
389 exit_code,
390 exports,
391 error,
392 } => BinaryFrame::ExecutionResult {
393 session_id,
394 exit_code,
395 exports,
396 error: error.map(from_runtime_execution_error),
397 },
398 RuntimeEvent::Log {
399 session_id,
400 channel,
401 message,
402 } => BinaryFrame::Log {
403 session_id,
404 channel,
405 message,
406 },
407 RuntimeEvent::StreamCallback {
408 session_id,
409 callback_type,
410 payload,
411 } => BinaryFrame::StreamCallback {
412 session_id,
413 callback_type,
414 payload,
415 },
416 }
417}
418
419fn from_runtime_execution_error(
420 error: agentos_v8_runtime::ipc_binary::ExecutionErrorBin,
421) -> v8_ipc::ExecutionErrorBin {
422 v8_ipc::ExecutionErrorBin {
423 error_type: error.error_type,
424 message: error.message,
425 stack: error.stack,
426 code: error.code,
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use std::sync::atomic::{AtomicU64, Ordering};
434
435 static NEXT_TEST_SESSION_ID: AtomicU64 = AtomicU64::new(1);
436
437 fn next_session_id() -> String {
438 format!(
439 "embedded-runtime-host-{}",
440 NEXT_TEST_SESSION_ID.fetch_add(1, Ordering::Relaxed)
441 )
442 }
443
444 #[test]
445 fn embedded_runtime_host_reuses_shared_runtime_service() {
446 let first = V8RuntimeHost::spawn().expect("spawn V8 runtime host");
447 let second = V8RuntimeHost::spawn().expect("spawn V8 runtime host");
448 assert_eq!(
449 first.runtime_ptr(),
450 second.runtime_ptr(),
451 "V8 runtime hosts should reuse the same embedded runtime service"
452 );
453 }
454
455 #[test]
456 fn embedded_runtime_host_create_destroy_recycles_session_ids() {
457 let host = V8RuntimeHost::spawn().expect("spawn V8 runtime host");
458 let session_id = next_session_id();
459
460 let _first_receiver = host
461 .register_session(&session_id)
462 .expect("register session output");
463 host.send_frame(&BinaryFrame::CreateSession {
464 session_id: session_id.clone(),
465 heap_limit_mb: 0,
466 cpu_time_limit_ms: 0,
467 wall_clock_limit_ms: 0,
468 })
469 .expect("create embedded runtime session");
470
471 let duplicate_error = host
472 .send_frame(&BinaryFrame::CreateSession {
473 session_id: session_id.clone(),
474 heap_limit_mb: 0,
475 cpu_time_limit_ms: 0,
476 wall_clock_limit_ms: 0,
477 })
478 .expect_err("duplicate session ids should be rejected");
479 assert_eq!(duplicate_error.kind(), io::ErrorKind::Other);
480
481 host.session_handle(session_id.clone())
482 .destroy()
483 .expect("destroy embedded runtime session");
484
485 let _second_receiver = host
486 .register_session(&session_id)
487 .expect("re-register session output");
488 host.send_frame(&BinaryFrame::CreateSession {
489 session_id: session_id.clone(),
490 heap_limit_mb: 0,
491 cpu_time_limit_ms: 0,
492 wall_clock_limit_ms: 0,
493 })
494 .expect("recreate embedded runtime session");
495
496 host.session_handle(session_id)
497 .destroy()
498 .expect("destroy recreated session");
499 }
500}