Skip to main content

omegon_extension/
extension.rs

1//! Extension trait and serving infrastructure.
2//!
3//! v1: `ExtensionServe` — simple request/response loop (backward compat).
4//! v2: `MessageRouter` — bidirectional communication with `HostProxy`.
5
6use crate::rpc::{RpcIncoming, RpcMessage, RpcNotification, RpcRequest, RpcResponse};
7use async_trait::async_trait;
8use serde_json::Value;
9use std::collections::HashMap;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicU64, Ordering};
12use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
13use tokio::sync::{Mutex, mpsc, oneshot};
14
15// ─── Extension Trait ──────────────────────────────────────────────────────
16
17/// Extension trait — implement this to create an extension.
18///
19/// The extension SDK will handle all RPC protocol details. You only need
20/// to implement method dispatch and return JSON results.
21#[async_trait]
22pub trait Extension: Send + Sync {
23    /// Extension name (must match manifest).
24    fn name(&self) -> &str;
25
26    /// Extension version (should match manifest).
27    fn version(&self) -> &str;
28
29    /// Handle an RPC method call.
30    ///
31    /// Return `Ok(Value)` on success, or an `Err(Error)` with a typed error code.
32    /// Unknown methods should return `Error::method_not_found(method)`.
33    ///
34    /// # Safety
35    ///
36    /// - Parameter validation happens here. Return `Error::invalid_params()` if args don't make sense.
37    /// - Panics in this method will crash the extension (not omegon).
38    /// - Timeouts are enforced by the parent process — don't block indefinitely.
39    async fn handle_rpc(&self, method: &str, params: Value) -> crate::Result<Value>;
40
41    /// Handle a notification from the host. Default is no-op.
42    ///
43    /// Called for messages with no `id` (fire-and-forget). The return value
44    /// is ignored — notifications never produce a response.
45    async fn handle_notification(&self, _method: &str, _params: Value) {
46        // Default: ignore notifications
47    }
48
49    /// Called after the initialize handshake completes. Default is no-op.
50    ///
51    /// The `host` proxy is available for sending notifications or requests
52    /// back to the host. Store it if you need it during tool execution.
53    async fn on_initialized(&self, _host: HostProxy) {
54        // Default: no-op
55    }
56
57    /// Called when the host delivers configuration values declared in the
58    /// manifest's `[config]` section. Extensions should store these for use
59    /// during tool execution.
60    ///
61    /// Called after `on_initialized`, before any tool invocations. May be
62    /// called again if the user updates config at runtime (hot-reload).
63    async fn on_config(&self, _config: std::collections::HashMap<String, serde_json::Value>) {
64        // Default: ignore config
65    }
66}
67
68// ─── HostProxy ────────────────────────────────────────────────────────────
69
70/// Proxy for sending messages from an extension back to the host.
71///
72/// Extensions receive this via `on_initialized()`. It can be cloned and
73/// stored for later use during tool execution.
74#[derive(Clone)]
75pub struct HostProxy {
76    writer_tx: mpsc::Sender<String>,
77    pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>>,
78    next_id: Arc<AtomicU64>,
79}
80
81impl HostProxy {
82    pub(crate) fn new(
83        writer_tx: mpsc::Sender<String>,
84        pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>>,
85    ) -> Self {
86        Self {
87            writer_tx,
88            pending,
89            next_id: Arc::new(AtomicU64::new(1)),
90        }
91    }
92
93    /// Send a notification to the host (fire-and-forget, no response).
94    pub async fn notify(&self, method: &str, params: Value) -> crate::Result<()> {
95        let notif = RpcNotification::new(method, params);
96        let json = serde_json::to_string(&notif)?;
97        self.writer_tx
98            .send(json)
99            .await
100            .map_err(|_| crate::Error::internal_error("host connection closed"))?;
101        Ok(())
102    }
103
104    /// Send a request to the host and await the response.
105    ///
106    /// Used for sampling, elicitation, and other ext→host calls.
107    ///
108    /// Includes a 30-second timeout to prevent indefinite hangs.
109    pub async fn request(&self, method: &str, params: Value) -> crate::Result<Value> {
110        let id = self.next_id.fetch_add(1, Ordering::SeqCst);
111        let id_str = format!("ext-{}", id);
112
113        let (tx, rx) = oneshot::channel();
114        self.pending.lock().await.insert(id_str.clone(), tx);
115
116        let request = serde_json::json!({
117            "jsonrpc": "2.0",
118            "id": id_str,
119            "method": method,
120            "params": params,
121        });
122        let json = serde_json::to_string(&request)?;
123        self.writer_tx
124            .send(json)
125            .await
126            .map_err(|_| crate::Error::internal_error("host connection closed"))?;
127
128        match tokio::time::timeout(std::time::Duration::from_secs(30), rx).await {
129            Ok(Ok(response)) => response.into_result(),
130            Ok(Err(_)) => {
131                self.pending.lock().await.remove(&id_str);
132                Err(crate::Error::internal_error(format!(
133                    "host dropped response channel for {method}"
134                )))
135            }
136            Err(_) => {
137                self.pending.lock().await.remove(&id_str);
138                Err(crate::Error::new(
139                    crate::ErrorCode::Timeout,
140                    format!("ext→host request '{method}' timed out after 30s"),
141                ))
142            }
143        }
144    }
145}
146
147// ─── v2 Message Router ───────────────────────────────────────────────────
148
149/// Bidirectional message router for v2 extensions.
150///
151/// Spawns two tasks:
152/// - Reader: parses incoming messages, routes responses to pending table,
153///   dispatches requests/notifications to the Extension trait
154/// - Writer: serializes outgoing messages from the mpsc channel to stdout
155pub(crate) struct MessageRouter<E: Extension> {
156    ext: Arc<E>,
157}
158
159impl<E: Extension + 'static> MessageRouter<E> {
160    pub(crate) fn new(ext: E) -> Self {
161        Self { ext: Arc::new(ext) }
162    }
163
164    /// Run the bidirectional message router until EOF.
165    pub(crate) async fn run(self) -> crate::Result<()> {
166        let stdin = tokio::io::stdin();
167        let stdout = tokio::io::stdout();
168
169        let reader = tokio::io::BufReader::new(stdin);
170        let mut writer = tokio::io::BufWriter::new(stdout);
171
172        // Channel for outgoing messages (responses + ext-initiated notifications/requests).
173        let (writer_tx, mut writer_rx) = mpsc::channel::<String>(256);
174
175        // Pending response table for ext→host requests.
176        let pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>> =
177            Arc::new(Mutex::new(HashMap::new()));
178
179        // Host proxy for the extension to use.
180        let host = HostProxy::new(writer_tx.clone(), pending.clone());
181
182        // Notify the extension that it can now send messages.
183        // This happens asynchronously — don't block the router startup.
184        let ext_init = self.ext.clone();
185        let host_init = host.clone();
186        tokio::spawn(async move {
187            ext_init.on_initialized(host_init).await;
188        });
189
190        // Writer task: drain outgoing messages to stdout.
191        let writer_handle = tokio::spawn(async move {
192            while let Some(msg) = writer_rx.recv().await {
193                if writer.write_all(msg.as_bytes()).await.is_err() {
194                    break;
195                }
196                if writer.write_all(b"\n").await.is_err() {
197                    break;
198                }
199                if writer.flush().await.is_err() {
200                    break;
201                }
202            }
203        });
204
205        // Reader loop: process incoming messages on the main task.
206        self.read_loop(reader, writer_tx.clone(), pending).await?;
207
208        // Shut down writer gracefully — drop the sender so the writer task
209        // drains remaining messages and exits naturally.
210        drop(writer_tx);
211        // Give the writer a moment to flush, then abort if it's stuck.
212        let _ = tokio::time::timeout(std::time::Duration::from_secs(2), writer_handle).await;
213        Ok(())
214    }
215
216    async fn read_loop(
217        &self,
218        mut reader: tokio::io::BufReader<tokio::io::Stdin>,
219        writer_tx: mpsc::Sender<String>,
220        pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>>,
221    ) -> crate::Result<()> {
222        let mut buf = Vec::with_capacity(4096);
223
224        loop {
225            buf.clear();
226            let line = match read_bounded_line(&mut reader, &mut buf).await {
227                Ok(Some(line)) => line,
228                Ok(None) => return Ok(()), // EOF
229                Err(msg) => {
230                    let error_response =
231                        RpcResponse::error(None, crate::ErrorCode::ParseError, msg);
232                    let json = serde_json::to_string(&error_response)?;
233                    let _ = writer_tx.send(json).await;
234                    continue;
235                }
236            };
237
238            let trimmed = line.trim();
239            if trimmed.is_empty() {
240                continue;
241            }
242
243            // Parse and route.
244            match RpcIncoming::parse(trimmed) {
245                Ok(RpcIncoming::Request(req)) => {
246                    // Incoming request from host — dispatch and respond.
247                    let ext = self.ext.clone();
248                    let tx = writer_tx.clone();
249                    tokio::spawn(async move {
250                        let response = dispatch_request(&*ext, &req).await;
251                        let json = serde_json::to_string(&response).unwrap_or_default();
252                        let _ = tx.send(json).await;
253                    });
254                }
255                Ok(RpcIncoming::Response(resp)) => {
256                    // Response to one of our pending ext→host requests.
257                    if let Some(id) = resp.id.as_ref().and_then(|v| v.as_str()) {
258                        if let Some(tx) = pending.lock().await.remove(id) {
259                            let _ = tx.send(resp);
260                        }
261                    }
262                }
263                Ok(RpcIncoming::Notification(notif)) => {
264                    // Notification from host — dispatch, no response.
265                    let ext = self.ext.clone();
266                    tokio::spawn(async move {
267                        ext.handle_notification(&notif.method, notif.params).await;
268                    });
269                }
270                Err(e) => {
271                    // Parse error — send error response.
272                    let error_response =
273                        RpcResponse::error(None, crate::ErrorCode::ParseError, e.to_string());
274                    let json = serde_json::to_string(&error_response)?;
275                    let _ = writer_tx.send(json).await;
276                }
277            }
278        }
279    }
280}
281
282// ─── Bounded line reader ──────────────────────────────────────────────
283
284/// 16 MiB cap to prevent OOM from unbounded reads.
285const MAX_LINE_SIZE: usize = 16 * 1024 * 1024;
286
287/// Read a single newline-terminated line with a bounded size limit.
288///
289/// Returns:
290/// - `Ok(Some(line))` — a valid line within the size limit
291/// - `Ok(None)` — EOF (pipe closed)
292/// - `Err(message)` — line exceeded MAX_LINE_SIZE (discarded from pipe)
293///
294/// Uses `fill_buf()` + `consume()` to check size *during* the read,
295/// preventing OOM from malicious oversized messages. If the limit is
296/// exceeded, the remainder of the line is consumed and discarded.
297async fn read_bounded_line<'a, R: tokio::io::AsyncBufRead + Unpin>(
298    reader: &mut R,
299    buf: &'a mut Vec<u8>,
300) -> Result<Option<&'a str>, String> {
301    buf.clear();
302
303    loop {
304        let available = reader.fill_buf().await.map_err(|e| e.to_string())?;
305
306        if available.is_empty() {
307            // EOF
308            return if buf.is_empty() {
309                Ok(None)
310            } else {
311                // Partial line at EOF — process it.
312                let line = std::str::from_utf8(buf).map_err(|e| format!("invalid UTF-8: {e}"))?;
313                Ok(Some(line))
314            };
315        }
316
317        // Find newline in available data.
318        let (chunk, found_newline) = match available.iter().position(|&b| b == b'\n') {
319            Some(pos) => (&available[..=pos], true),
320            None => (available, false),
321        };
322
323        // Check size limit *before* copying.
324        if buf.len() + chunk.len() > MAX_LINE_SIZE {
325            // Oversized. Consume this chunk and drain the rest of the line.
326            let chunk_len = chunk.len();
327            reader.consume(chunk_len);
328            if !found_newline {
329                // Drain until newline or EOF.
330                let mut drain = Vec::new();
331                let _ = reader.read_until(b'\n', &mut drain).await;
332            }
333            return Err(format!(
334                "message too large (>{} bytes, limit {})",
335                MAX_LINE_SIZE, MAX_LINE_SIZE,
336            ));
337        }
338
339        buf.extend_from_slice(chunk);
340        let chunk_len = chunk.len();
341        reader.consume(chunk_len);
342
343        if found_newline {
344            let line = std::str::from_utf8(buf).map_err(|e| format!("invalid UTF-8: {e}"))?;
345            return Ok(Some(line));
346        }
347    }
348}
349
350/// Dispatch an incoming request to the extension's handle_rpc method.
351async fn dispatch_request<E: Extension>(ext: &E, req: &RpcRequest) -> RpcResponse {
352    let result = ext.handle_rpc(&req.method, req.params.clone()).await;
353
354    match result {
355        Ok(value) => RpcResponse::success(req.id.clone(), value),
356        Err(e) => RpcResponse::error(req.id.clone(), e.code(), e.message()),
357    }
358}
359
360// ─── v1 Extension Serve (backward compat) ────────────────────────────────
361
362/// Extension serving loop (v1). Created by [`crate::serve()`], runs until shutdown.
363///
364/// This is the simple request/response loop without bidirectional support.
365/// Use `serve_v2()` for new extensions that need to send notifications or
366/// requests back to the host.
367pub(crate) struct ExtensionServe<E: Extension> {
368    ext: E,
369}
370
371impl<E: Extension> ExtensionServe<E> {
372    pub(crate) fn new(ext: E) -> Self {
373        Self { ext }
374    }
375
376    /// Run the extension serving loop.
377    pub(crate) async fn run(self) -> crate::Result<()> {
378        let stdin = tokio::io::stdin();
379        let stdout = tokio::io::stdout();
380
381        let mut reader = tokio::io::BufReader::new(stdin);
382        let mut writer = tokio::io::BufWriter::new(stdout);
383
384        let mut buf = Vec::with_capacity(4096);
385
386        loop {
387            buf.clear();
388            let line = match read_bounded_line(&mut reader, &mut buf).await {
389                Ok(Some(line)) => line,
390                Ok(None) => return Ok(()), // EOF
391                Err(msg) => {
392                    let error_response =
393                        RpcResponse::error(None, crate::ErrorCode::ParseError, msg);
394                    let response_json = serde_json::to_string(&error_response)?;
395                    writer.write_all(response_json.as_bytes()).await?;
396                    writer.write_all(b"\n").await?;
397                    writer.flush().await?;
398                    continue;
399                }
400            };
401
402            // Parse incoming RPC message
403            let msg: RpcMessage = match serde_json::from_str(line.trim()) {
404                Ok(msg) => msg,
405                Err(e) => {
406                    let error_response =
407                        RpcResponse::error(None, crate::ErrorCode::ParseError, e.to_string());
408                    let response_json = serde_json::to_string(&error_response)?;
409                    writer.write_all(response_json.as_bytes()).await?;
410                    writer.write_all(b"\n").await?;
411                    writer.flush().await?;
412                    continue;
413                }
414            };
415
416            match msg {
417                RpcMessage::Request(req) => {
418                    let response = self.handle_request(&req).await;
419                    let response_json = serde_json::to_string(&response)?;
420                    writer.write_all(response_json.as_bytes()).await?;
421                    writer.write_all(b"\n").await?;
422                    writer.flush().await?;
423                }
424                RpcMessage::Notification(_notif) => {
425                    // Ignore notifications — v1 extension doesn't process them
426                }
427            }
428        }
429    }
430
431    async fn handle_request(&self, req: &RpcRequest) -> RpcResponse {
432        let result = self.ext.handle_rpc(&req.method, req.params.clone()).await;
433
434        match result {
435            Ok(value) => RpcResponse::success(req.id.clone(), value),
436            Err(e) => RpcResponse::error(req.id.clone(), e.code(), e.message()),
437        }
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    #[derive(Default)]
446    struct TestExtension;
447
448    #[async_trait]
449    impl Extension for TestExtension {
450        fn name(&self) -> &str {
451            "test-extension"
452        }
453
454        fn version(&self) -> &str {
455            "0.1.0"
456        }
457
458        async fn handle_rpc(&self, method: &str, _params: Value) -> crate::Result<Value> {
459            match method {
460                "echo" => Ok(serde_json::json!({"status": "ok"})),
461                "get_tools" => Ok(serde_json::json!([])),
462                _ => Err(crate::Error::method_not_found(method)),
463            }
464        }
465    }
466
467    #[tokio::test]
468    async fn test_extension_dispatch() {
469        let ext = TestExtension;
470
471        let req = RpcRequest {
472            jsonrpc: "2.0".to_string(),
473            id: Some(serde_json::Value::String("1".to_string())),
474            method: "echo".to_string(),
475            params: serde_json::json!({}),
476        };
477
478        let response = dispatch_request(&ext, &req).await;
479
480        assert_eq!(
481            response.id,
482            Some(serde_json::Value::String("1".to_string()))
483        );
484        assert!(response.result.is_some());
485    }
486
487    #[tokio::test]
488    async fn test_unknown_method() {
489        let ext = TestExtension;
490
491        let req = RpcRequest {
492            jsonrpc: "2.0".to_string(),
493            id: Some(serde_json::Value::String("1".to_string())),
494            method: "unknown".to_string(),
495            params: serde_json::json!({}),
496        };
497
498        let response = dispatch_request(&ext, &req).await;
499
500        assert_eq!(
501            response.id,
502            Some(serde_json::Value::String("1".to_string()))
503        );
504        assert!(response.error.is_some());
505        let error = response.error.unwrap();
506        assert_eq!(error.code, -32601);
507        assert_eq!(error.label, "MethodNotFound");
508    }
509
510    #[tokio::test]
511    async fn test_host_proxy_notification() {
512        let (tx, mut rx) = mpsc::channel(16);
513        let pending = Arc::new(Mutex::new(HashMap::new()));
514        let proxy = HostProxy::new(tx, pending);
515
516        proxy
517            .notify("notifications/tools/list_changed", serde_json::json!({}))
518            .await
519            .unwrap();
520
521        let msg = rx.recv().await.unwrap();
522        let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
523        assert_eq!(parsed["method"], "notifications/tools/list_changed");
524        assert!(parsed.get("id").is_none());
525    }
526
527    #[tokio::test]
528    async fn test_host_proxy_request_response() {
529        let (tx, mut rx) = mpsc::channel(16);
530        let pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>> =
531            Arc::new(Mutex::new(HashMap::new()));
532        let proxy = HostProxy::new(tx, pending.clone());
533
534        // Spawn request in background.
535        let proxy_clone = proxy.clone();
536        let handle = tokio::spawn(async move {
537            proxy_clone
538                .request("sampling/create_message", serde_json::json!({"test": true}))
539                .await
540        });
541
542        // Read the outgoing request.
543        let msg = rx.recv().await.unwrap();
544        let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
545        let id = parsed["id"].as_str().unwrap().to_string();
546        assert!(id.starts_with("ext-"));
547        assert_eq!(parsed["method"], "sampling/create_message");
548
549        // Simulate host response.
550        let response = RpcResponse::success(
551            Some(Value::String(id.clone())),
552            serde_json::json!({"role": "assistant", "content": "hello"}),
553        );
554        if let Some(tx) = pending.lock().await.remove(&id) {
555            tx.send(response).unwrap();
556        }
557
558        // Check the extension got the result.
559        let result = handle.await.unwrap().unwrap();
560        assert_eq!(result["role"], "assistant");
561    }
562
563    // ─── Bounded read tests ──────────────────────────────────────────
564
565    #[tokio::test]
566    async fn test_bounded_read_normal_line() {
567        let data = b"hello world\n";
568        let mut reader = tokio::io::BufReader::new(&data[..]);
569        let mut buf = Vec::new();
570
571        let result = read_bounded_line(&mut reader, &mut buf).await;
572        let line = result.unwrap().unwrap();
573        assert_eq!(line.trim(), "hello world");
574    }
575
576    #[tokio::test]
577    async fn test_bounded_read_eof() {
578        let data = b"";
579        let mut reader = tokio::io::BufReader::new(&data[..]);
580        let mut buf = Vec::new();
581
582        let result = read_bounded_line(&mut reader, &mut buf).await;
583        assert!(result.unwrap().is_none());
584    }
585
586    #[tokio::test]
587    async fn test_bounded_read_partial_line_at_eof() {
588        let data = b"no newline";
589        let mut reader = tokio::io::BufReader::new(&data[..]);
590        let mut buf = Vec::new();
591
592        let result = read_bounded_line(&mut reader, &mut buf).await;
593        let line = result.unwrap().unwrap();
594        assert_eq!(line, "no newline");
595    }
596
597    #[tokio::test]
598    async fn test_bounded_read_multiple_lines() {
599        let data = b"line one\nline two\n";
600        let mut reader = tokio::io::BufReader::new(&data[..]);
601        let mut buf = Vec::new();
602
603        let line1 = read_bounded_line(&mut reader, &mut buf)
604            .await
605            .unwrap()
606            .unwrap();
607        assert_eq!(line1.trim(), "line one");
608
609        buf.clear();
610        let line2 = read_bounded_line(&mut reader, &mut buf)
611            .await
612            .unwrap()
613            .unwrap();
614        assert_eq!(line2.trim(), "line two");
615    }
616
617    #[tokio::test]
618    async fn test_bounded_read_rejects_oversized() {
619        // Create a line larger than MAX_LINE_SIZE
620        let oversized = vec![b'x'; MAX_LINE_SIZE + 100];
621        let mut data = oversized;
622        data.push(b'\n');
623        let mut reader = tokio::io::BufReader::new(&data[..]);
624        let mut buf = Vec::new();
625
626        let result = read_bounded_line(&mut reader, &mut buf).await;
627        assert!(result.is_err());
628        let msg = result.unwrap_err();
629        assert!(msg.contains("too large"));
630    }
631
632    #[tokio::test]
633    async fn test_bounded_read_exactly_at_limit() {
634        // Line exactly at MAX_LINE_SIZE (including newline)
635        let mut data = vec![b'x'; MAX_LINE_SIZE - 1];
636        data.push(b'\n');
637        let mut reader = tokio::io::BufReader::new(&data[..]);
638        let mut buf = Vec::new();
639
640        let result = read_bounded_line(&mut reader, &mut buf).await;
641        assert!(result.is_ok());
642        let line = result.unwrap().unwrap();
643        assert_eq!(line.len(), MAX_LINE_SIZE);
644    }
645
646    #[tokio::test]
647    async fn test_bounded_read_empty_lines() {
648        let data = b"\n\nhello\n";
649        let mut reader = tokio::io::BufReader::new(&data[..]);
650        let mut buf = Vec::new();
651
652        // First line is just "\n"
653        let line1 = read_bounded_line(&mut reader, &mut buf)
654            .await
655            .unwrap()
656            .unwrap();
657        assert_eq!(line1.trim(), "");
658
659        buf.clear();
660        let line2 = read_bounded_line(&mut reader, &mut buf)
661            .await
662            .unwrap()
663            .unwrap();
664        assert_eq!(line2.trim(), "");
665
666        buf.clear();
667        let line3 = read_bounded_line(&mut reader, &mut buf)
668            .await
669            .unwrap()
670            .unwrap();
671        assert_eq!(line3.trim(), "hello");
672    }
673}