agentos_sidecar_browser/
lib.rs1#![forbid(unsafe_code)]
2
3#[cfg(target_arch = "wasm32")]
6mod wasm;
7#[cfg(target_arch = "wasm32")]
8pub use wasm::AgentOsBrowserSidecarWasm;
9
10mod acp_host;
11
12use std::collections::BTreeMap;
13use std::sync::Mutex;
14
15use agentos_native_sidecar_browser::{
16 BrowserExtension, BrowserExtensionContext, BrowserSidecar, BrowserSidecarBridge,
17 BrowserSidecarConfig, BrowserSidecarError,
18};
19use agentos_sidecar_core::{codec, error_response, AcpCore, AcpCoreError};
20
21use crate::acp_host::BrowserAcpHost;
22
23pub struct BrowserAcpExtension {
31 core: Mutex<AcpCore>,
32 executions: Mutex<BTreeMap<String, String>>,
34}
35
36impl BrowserAcpExtension {
37 pub fn new() -> Self {
38 Self {
39 core: Mutex::new(AcpCore::new()),
40 executions: Mutex::new(BTreeMap::new()),
41 }
42 }
43}
44
45impl Default for BrowserAcpExtension {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51fn to_browser_error(error: AcpCoreError) -> BrowserSidecarError {
52 BrowserSidecarError::InvalidState(error.to_string())
53}
54
55impl BrowserExtension for BrowserAcpExtension {
56 fn namespace(&self) -> &str {
57 agentos_protocol::ACP_EXTENSION_NAMESPACE
58 }
59
60 fn handle_request(
61 &self,
62 context: &mut BrowserExtensionContext<'_>,
63 payload: &[u8],
64 ) -> Result<Vec<u8>, BrowserSidecarError> {
65 let request = codec::decode_request(payload).map_err(to_browser_error)?;
66 let connection_id = context.connection_id().unwrap_or_default().to_string();
67 let vm_id = context
68 .vm_id()
69 .ok_or_else(|| {
70 BrowserSidecarError::InvalidState(String::from(
71 "ACP requests require VM ownership (no vm_id on the request)",
72 ))
73 })?
74 .to_string();
75
76 let mut executions = self.executions.lock().map_err(|_| {
77 BrowserSidecarError::InvalidState(String::from("ACP executions lock poisoned"))
78 })?;
79 let mut host = BrowserAcpHost::new(context, vm_id, &mut executions);
80 let mut core = self.core.lock().map_err(|_| {
81 BrowserSidecarError::InvalidState(String::from("ACP core lock poisoned"))
82 })?;
83 let response = match core.dispatch_resumable(&mut host, &connection_id, request) {
89 Ok(response) => response,
90 Err(error) => error_response(&error),
91 };
92 codec::encode_response(&response).map_err(to_browser_error)
93 }
94}
95
96pub fn extensions() -> Vec<Box<dyn BrowserExtension>> {
97 vec![Box::new(BrowserAcpExtension::new())]
98}
99
100pub fn browser_sidecar<B>(
101 bridge: B,
102 config: BrowserSidecarConfig,
103) -> Result<BrowserSidecar<B>, BrowserSidecarError>
104where
105 B: BrowserSidecarBridge,
106 <B as agentos_bridge::BridgeTypes>::Error: std::fmt::Debug,
107{
108 BrowserSidecar::with_extensions(bridge, config, extensions())
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use agentos_protocol::ACP_EXTENSION_NAMESPACE;
115
116 #[test]
117 fn browser_extensions_register_acp_namespace() {
118 let extensions = extensions();
119
120 assert_eq!(extensions.len(), 1);
121 assert_eq!(extensions[0].namespace(), ACP_EXTENSION_NAMESPACE);
122 }
123
124 #[test]
125 fn browser_acp_extension_rejects_invalid_payload() {
126 let extension = BrowserAcpExtension::new();
129 let mut host = NullBrowserExtensionHost;
130 let mut context = BrowserExtensionContext::new(&mut host);
131
132 let error = extension
133 .handle_request(&mut context, b"not-a-valid-acp-frame")
134 .expect_err("invalid ACP payload must be rejected");
135 assert!(error.to_string().contains("invalid ACP request"));
136 }
137
138 #[test]
139 fn browser_acp_extension_requires_vm_ownership() {
140 use agentos_protocol::generated::v1::{AcpGetSessionStateRequest, AcpRequest};
143 let extension = BrowserAcpExtension::new();
144 let mut host = NullBrowserExtensionHost;
145 let mut context = BrowserExtensionContext::new(&mut host);
146
147 let payload = serde_bare::to_vec(&AcpRequest::AcpGetSessionStateRequest(
148 AcpGetSessionStateRequest {
149 session_id: "s1".into(),
150 },
151 ))
152 .expect("encode");
153 let error = extension
154 .handle_request(&mut context, &payload)
155 .expect_err("missing vm ownership must fail");
156 assert!(error.to_string().contains("require VM ownership"));
157 }
158
159 struct NullBrowserExtensionHost;
160
161 impl agentos_native_sidecar_browser::BrowserExtensionHost for NullBrowserExtensionHost {
162 fn write_file(
163 &mut self,
164 _vm_id: &str,
165 _path: &str,
166 _contents: Vec<u8>,
167 ) -> Result<(), BrowserSidecarError> {
168 unreachable!("test ACP extension does not call browser context")
169 }
170
171 fn read_file(&mut self, _vm_id: &str, _path: &str) -> Result<Vec<u8>, BrowserSidecarError> {
172 unreachable!("test ACP extension does not call browser context")
173 }
174
175 fn mkdir(
176 &mut self,
177 _vm_id: &str,
178 _path: &str,
179 _recursive: bool,
180 ) -> Result<(), BrowserSidecarError> {
181 unreachable!("test ACP extension does not call browser context")
182 }
183
184 fn read_dir(
185 &mut self,
186 _vm_id: &str,
187 _path: &str,
188 ) -> Result<Vec<String>, BrowserSidecarError> {
189 unreachable!("test ACP extension does not call browser context")
190 }
191
192 fn create_javascript_context(
193 &mut self,
194 _request: agentos_bridge::CreateJavascriptContextRequest,
195 ) -> Result<agentos_bridge::GuestContextHandle, BrowserSidecarError> {
196 unreachable!("test ACP extension does not call browser context")
197 }
198
199 fn create_wasm_context(
200 &mut self,
201 _request: agentos_bridge::CreateWasmContextRequest,
202 ) -> Result<agentos_bridge::GuestContextHandle, BrowserSidecarError> {
203 unreachable!("test ACP extension does not call browser context")
204 }
205
206 fn start_execution(
207 &mut self,
208 _request: agentos_bridge::StartExecutionRequest,
209 ) -> Result<agentos_bridge::StartedExecution, BrowserSidecarError> {
210 unreachable!("test ACP extension does not call browser context")
211 }
212
213 fn write_stdin(
214 &mut self,
215 _request: agentos_bridge::WriteExecutionStdinRequest,
216 ) -> Result<(), BrowserSidecarError> {
217 unreachable!("test ACP extension does not call browser context")
218 }
219
220 fn close_stdin(
221 &mut self,
222 _request: agentos_bridge::ExecutionHandleRequest,
223 ) -> Result<(), BrowserSidecarError> {
224 unreachable!("test ACP extension does not call browser context")
225 }
226
227 fn kill_execution(
228 &mut self,
229 _request: agentos_bridge::KillExecutionRequest,
230 ) -> Result<(), BrowserSidecarError> {
231 unreachable!("test ACP extension does not call browser context")
232 }
233
234 fn poll_execution_event(
235 &mut self,
236 _request: agentos_bridge::PollExecutionEventRequest,
237 ) -> Result<Option<agentos_bridge::ExecutionEvent>, BrowserSidecarError> {
238 unreachable!("test ACP extension does not call browser context")
239 }
240 }
241}