oxios_kernel/kernel_handle/browser_api.rs
1//! Browser API — browser backend facade.
2//!
3//! When the `browser` feature is enabled, lazily initializes an
4//! `Arc<oxibrowser_core::Browser>` on first use from an async context.
5//! This avoids the `block_on` panic that occurs when constructing
6//! inside an existing tokio runtime.
7
8use std::sync::Arc;
9
10/// Browser management system calls.
11///
12/// Wraps the embedded OxiBrowser engine. `BrowserTool` borrows the `Browser`
13/// via `browser()` for agent tool calls.
14///
15/// Initialization is **lazy**: the browser engine is only created on the
16/// first call to [`browser()`](Self::browser) from an async context.
17/// [`from_config()`](Self::from_config) and [`Default::default()`] are
18/// cheap and never panic.
19#[cfg(feature = "browser")]
20pub struct BrowserApi {
21 inner: tokio::sync::OnceCell<Arc<oxibrowser_core::Browser>>,
22 config: Option<oxibrowser_core::BrowserConfig>,
23}
24
25#[cfg(feature = "browser")]
26impl Clone for BrowserApi {
27 fn clone(&self) -> Self {
28 Self {
29 inner: self.inner.clone(),
30 config: self.config.clone(),
31 }
32 }
33}
34
35#[cfg(feature = "browser")]
36impl BrowserApi {
37 /// Create a new BrowserApi that will lazily initialize a Browser from config.
38 ///
39 /// This is cheap — no I/O or runtime calls happen here.
40 pub fn from_config(config: &oxibrowser_core::BrowserConfig) -> Self {
41 Self {
42 inner: tokio::sync::OnceCell::new(),
43 config: Some(config.clone()),
44 }
45 }
46
47 /// Create a new BrowserApi from an already-initialized Browser.
48 pub fn new(browser: Arc<oxibrowser_core::Browser>) -> Self {
49 let cell = tokio::sync::OnceCell::new();
50 // SAFETY: cell was just created, it's empty.
51 match cell.set(browser) {
52 Ok(()) => {}
53 Err(_) => unreachable!("OnceCell was just created"),
54 }
55 Self {
56 inner: cell,
57 config: None,
58 }
59 }
60
61 /// Get the browser engine, initializing it lazily if needed.
62 ///
63 /// Must be called from an async context (tokio runtime active).
64 /// Returns `Err` if no config was provided and the browser was not
65 /// pre-initialized.
66 pub async fn browser(&self) -> anyhow::Result<&Arc<oxibrowser_core::Browser>> {
67 self.inner
68 .get_or_try_init(|| async {
69 let config = self
70 .config
71 .as_ref()
72 .ok_or_else(|| anyhow::anyhow!("BrowserApi has no config and was not pre-initialized"))?;
73 let browser = oxibrowser_core::Browser::new(config.clone())
74 .await
75 .map_err(|e| anyhow::anyhow!("Failed to initialize browser engine: {}", e))?;
76 Ok(Arc::new(browser))
77 })
78 .await
79 }
80
81 /// Shut down the browser engine (if it was initialized).
82 pub async fn shutdown(&self) -> anyhow::Result<()> {
83 if let Some(browser) = self.inner.get() {
84 browser.close().await?;
85 }
86 Ok(())
87 }
88}
89
90/// Default — creates an uninitialized BrowserApi.
91///
92/// This is used by `from_subsystems` and when `browser.enabled = false`.
93/// It will return an error on [`browser()`](Self::browser) if no config
94/// is ever set, but will **not panic**.
95#[cfg(feature = "browser")]
96impl Default for BrowserApi {
97 fn default() -> Self {
98 Self {
99 inner: tokio::sync::OnceCell::new(),
100 config: None,
101 }
102 }
103}
104
105/// Zero-sized browser placeholder when the `browser` feature is disabled.
106#[cfg(not(feature = "browser"))]
107pub struct BrowserApi;
108
109#[cfg(not(feature = "browser"))]
110impl Default for BrowserApi {
111 fn default() -> Self {
112 BrowserApi
113 }
114}