use crate::rpc::{RpcIncoming, RpcMessage, RpcNotification, RpcRequest, RpcResponse};
use crate::{JSONRPC_VERSION, METHOD_ACTIONS_EXECUTE};
use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::sync::{Mutex, mpsc, oneshot};
#[async_trait]
pub trait Extension: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
async fn handle_rpc(&self, method: &str, params: Value) -> crate::Result<Value>;
async fn handle_notification(&self, _method: &str, _params: Value) {
}
async fn on_initialized(&self, _host: HostProxy) {
}
async fn on_config(&self, _config: std::collections::HashMap<String, serde_json::Value>) {
}
}
#[derive(Clone)]
pub struct HostProxy {
writer_tx: mpsc::Sender<String>,
pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>>,
next_id: Arc<AtomicU64>,
}
impl HostProxy {
pub(crate) fn new(
writer_tx: mpsc::Sender<String>,
pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>>,
) -> Self {
Self {
writer_tx,
pending,
next_id: Arc::new(AtomicU64::new(1)),
}
}
pub async fn notify(&self, method: &str, params: Value) -> crate::Result<()> {
let notif = RpcNotification::new(method, params);
let json = serde_json::to_string(¬if)?;
self.writer_tx
.send(json)
.await
.map_err(|_| crate::Error::internal_error("host connection closed"))?;
Ok(())
}
pub async fn request(&self, method: &str, params: Value) -> crate::Result<Value> {
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let id_str = format!("ext-{}", id);
let (tx, rx) = oneshot::channel();
self.pending.lock().await.insert(id_str.clone(), tx);
let request = serde_json::json!({
"jsonrpc": JSONRPC_VERSION,
"id": id_str,
"method": method,
"params": params,
});
let json = serde_json::to_string(&request)?;
self.writer_tx
.send(json)
.await
.map_err(|_| crate::Error::internal_error("host connection closed"))?;
match tokio::time::timeout(std::time::Duration::from_secs(30), rx).await {
Ok(Ok(response)) => response.into_result(),
Ok(Err(_)) => {
self.pending.lock().await.remove(&id_str);
Err(crate::Error::internal_error(format!(
"host dropped response channel for {method}"
)))
}
Err(_) => {
self.pending.lock().await.remove(&id_str);
Err(crate::Error::new(
crate::ErrorCode::Timeout,
format!("ext→host request '{method}' timed out after 30s"),
))
}
}
}
pub async fn execute_action(
&self,
action: crate::HostAction,
) -> crate::Result<crate::HostActionOutcome> {
let value = self
.request(
METHOD_ACTIONS_EXECUTE,
serde_json::json!({ "action": action }),
)
.await?;
serde_json::from_value(value).map_err(|err| crate::Error::invalid_params(err.to_string()))
}
}
pub(crate) struct MessageRouter<E: Extension> {
ext: Arc<E>,
}
impl<E: Extension + 'static> MessageRouter<E> {
pub(crate) fn new(ext: E) -> Self {
Self { ext: Arc::new(ext) }
}
pub(crate) async fn run(self) -> crate::Result<()> {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let reader = tokio::io::BufReader::new(stdin);
let mut writer = tokio::io::BufWriter::new(stdout);
let (writer_tx, mut writer_rx) = mpsc::channel::<String>(256);
let pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>> =
Arc::new(Mutex::new(HashMap::new()));
let host = HostProxy::new(writer_tx.clone(), pending.clone());
let ext_init = self.ext.clone();
let host_init = host.clone();
tokio::spawn(async move {
ext_init.on_initialized(host_init).await;
});
let writer_handle = tokio::spawn(async move {
while let Some(msg) = writer_rx.recv().await {
if writer.write_all(msg.as_bytes()).await.is_err() {
break;
}
if writer.write_all(b"\n").await.is_err() {
break;
}
if writer.flush().await.is_err() {
break;
}
}
});
self.read_loop(reader, writer_tx.clone(), pending).await?;
drop(writer_tx);
let _ = tokio::time::timeout(std::time::Duration::from_secs(2), writer_handle).await;
Ok(())
}
async fn read_loop(
&self,
mut reader: tokio::io::BufReader<tokio::io::Stdin>,
writer_tx: mpsc::Sender<String>,
pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>>,
) -> crate::Result<()> {
let mut buf = Vec::with_capacity(4096);
loop {
buf.clear();
let line = match read_bounded_line(&mut reader, &mut buf).await {
Ok(Some(line)) => line,
Ok(None) => return Ok(()), Err(msg) => {
let error_response =
RpcResponse::error(None, crate::ErrorCode::ParseError, msg);
let json = serde_json::to_string(&error_response)?;
let _ = writer_tx.send(json).await;
continue;
}
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
match RpcIncoming::parse(trimmed) {
Ok(RpcIncoming::Request(req)) => {
let ext = self.ext.clone();
let tx = writer_tx.clone();
tokio::spawn(async move {
let response = dispatch_request(&*ext, &req).await;
let json = serde_json::to_string(&response).unwrap_or_default();
let _ = tx.send(json).await;
});
}
Ok(RpcIncoming::Response(resp)) => {
if let Some(id) = resp.id.as_ref().and_then(|v| v.as_str()) {
if let Some(tx) = pending.lock().await.remove(id) {
let _ = tx.send(resp);
}
}
}
Ok(RpcIncoming::Notification(notif)) => {
let ext = self.ext.clone();
tokio::spawn(async move {
ext.handle_notification(¬if.method, notif.params).await;
});
}
Err(e) => {
let error_response =
RpcResponse::error(None, crate::ErrorCode::ParseError, e.to_string());
let json = serde_json::to_string(&error_response)?;
let _ = writer_tx.send(json).await;
}
}
}
}
}
const MAX_LINE_SIZE: usize = 16 * 1024 * 1024;
async fn read_bounded_line<'a, R: tokio::io::AsyncBufRead + Unpin>(
reader: &mut R,
buf: &'a mut Vec<u8>,
) -> Result<Option<&'a str>, String> {
buf.clear();
loop {
let available = reader.fill_buf().await.map_err(|e| e.to_string())?;
if available.is_empty() {
return if buf.is_empty() {
Ok(None)
} else {
let line = std::str::from_utf8(buf).map_err(|e| format!("invalid UTF-8: {e}"))?;
Ok(Some(line))
};
}
let (chunk, found_newline) = match available.iter().position(|&b| b == b'\n') {
Some(pos) => (&available[..=pos], true),
None => (available, false),
};
if buf.len() + chunk.len() > MAX_LINE_SIZE {
let chunk_len = chunk.len();
reader.consume(chunk_len);
if !found_newline {
let mut drain = Vec::new();
let _ = reader.read_until(b'\n', &mut drain).await;
}
return Err(format!(
"message too large (>{} bytes, limit {})",
MAX_LINE_SIZE, MAX_LINE_SIZE,
));
}
buf.extend_from_slice(chunk);
let chunk_len = chunk.len();
reader.consume(chunk_len);
if found_newline {
let line = std::str::from_utf8(buf).map_err(|e| format!("invalid UTF-8: {e}"))?;
return Ok(Some(line));
}
}
}
async fn dispatch_request<E: Extension>(ext: &E, req: &RpcRequest) -> RpcResponse {
let result = ext.handle_rpc(&req.method, req.params.clone()).await;
match result {
Ok(value) => RpcResponse::success(req.id.clone(), value),
Err(e) => RpcResponse::error(req.id.clone(), e.code(), e.message()),
}
}
pub(crate) struct ExtensionServe<E: Extension> {
ext: E,
}
impl<E: Extension> ExtensionServe<E> {
pub(crate) fn new(ext: E) -> Self {
Self { ext }
}
pub(crate) async fn run(self) -> crate::Result<()> {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
let mut reader = tokio::io::BufReader::new(stdin);
let mut writer = tokio::io::BufWriter::new(stdout);
let mut buf = Vec::with_capacity(4096);
loop {
buf.clear();
let line = match read_bounded_line(&mut reader, &mut buf).await {
Ok(Some(line)) => line,
Ok(None) => return Ok(()), Err(msg) => {
let error_response =
RpcResponse::error(None, crate::ErrorCode::ParseError, msg);
let response_json = serde_json::to_string(&error_response)?;
writer.write_all(response_json.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
continue;
}
};
let msg: RpcMessage = match serde_json::from_str(line.trim()) {
Ok(msg) => msg,
Err(e) => {
let error_response =
RpcResponse::error(None, crate::ErrorCode::ParseError, e.to_string());
let response_json = serde_json::to_string(&error_response)?;
writer.write_all(response_json.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
continue;
}
};
match msg {
RpcMessage::Request(req) => {
let response = self.handle_request(&req).await;
let response_json = serde_json::to_string(&response)?;
writer.write_all(response_json.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
}
RpcMessage::Notification(_notif) => {
}
}
}
}
async fn handle_request(&self, req: &RpcRequest) -> RpcResponse {
let result = self.ext.handle_rpc(&req.method, req.params.clone()).await;
match result {
Ok(value) => RpcResponse::success(req.id.clone(), value),
Err(e) => RpcResponse::error(req.id.clone(), e.code(), e.message()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct TestExtension;
#[async_trait]
impl Extension for TestExtension {
fn name(&self) -> &str {
"test-extension"
}
fn version(&self) -> &str {
"0.1.0"
}
async fn handle_rpc(&self, method: &str, _params: Value) -> crate::Result<Value> {
match method {
"echo" => Ok(serde_json::json!({"status": "ok"})),
"get_tools" => Ok(serde_json::json!([])),
_ => Err(crate::Error::method_not_found(method)),
}
}
}
#[tokio::test]
async fn test_extension_dispatch() {
let ext = TestExtension;
let req = RpcRequest {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::Value::String("1".to_string())),
method: "echo".to_string(),
params: serde_json::json!({}),
};
let response = dispatch_request(&ext, &req).await;
assert_eq!(
response.id,
Some(serde_json::Value::String("1".to_string()))
);
assert!(response.result.is_some());
}
#[tokio::test]
async fn test_unknown_method() {
let ext = TestExtension;
let req = RpcRequest {
jsonrpc: "2.0".to_string(),
id: Some(serde_json::Value::String("1".to_string())),
method: "unknown".to_string(),
params: serde_json::json!({}),
};
let response = dispatch_request(&ext, &req).await;
assert_eq!(
response.id,
Some(serde_json::Value::String("1".to_string()))
);
assert!(response.error.is_some());
let error = response.error.unwrap();
assert_eq!(error.code, -32601);
assert_eq!(error.label, "MethodNotFound");
}
#[tokio::test]
async fn test_host_proxy_notification() {
let (tx, mut rx) = mpsc::channel(16);
let pending = Arc::new(Mutex::new(HashMap::new()));
let proxy = HostProxy::new(tx, pending);
proxy
.notify("notifications/tools/list_changed", serde_json::json!({}))
.await
.unwrap();
let msg = rx.recv().await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
assert_eq!(parsed["method"], "notifications/tools/list_changed");
assert!(parsed.get("id").is_none());
}
#[tokio::test]
async fn test_host_proxy_request_response() {
let (tx, mut rx) = mpsc::channel(16);
let pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>> =
Arc::new(Mutex::new(HashMap::new()));
let proxy = HostProxy::new(tx, pending.clone());
let proxy_clone = proxy.clone();
let handle = tokio::spawn(async move {
proxy_clone
.request("sampling/create_message", serde_json::json!({"test": true}))
.await
});
let msg = rx.recv().await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
let id = parsed["id"].as_str().unwrap().to_string();
assert!(id.starts_with("ext-"));
assert_eq!(parsed["method"], "sampling/create_message");
let response = RpcResponse::success(
Some(Value::String(id.clone())),
serde_json::json!({"role": "assistant", "content": "hello"}),
);
if let Some(tx) = pending.lock().await.remove(&id) {
tx.send(response).unwrap();
}
let result = handle.await.unwrap().unwrap();
assert_eq!(result["role"], "assistant");
}
#[tokio::test]
async fn test_host_proxy_execute_action_request_response() {
let (tx, mut rx) = mpsc::channel(16);
let pending: Arc<Mutex<HashMap<String, oneshot::Sender<RpcResponse>>>> =
Arc::new(Mutex::new(HashMap::new()));
let proxy = HostProxy::new(tx, pending.clone());
let action = crate::HostAction::new(
"open-reader",
crate::actions::terminal::TERMINAL_CREATE_V1,
crate::actions::terminal::TerminalCreateParams::new("bookokrat"),
)
.unwrap();
let proxy_clone = proxy.clone();
let handle = tokio::spawn(async move { proxy_clone.execute_action(action).await });
let msg = rx.recv().await.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
let id = parsed["id"].as_str().unwrap().to_string();
assert_eq!(parsed["method"], "actions/execute");
assert_eq!(parsed["params"]["action"]["id"], "open-reader");
assert_eq!(parsed["params"]["action"]["type"], "terminal.create@1");
assert_eq!(parsed["params"]["action"]["params"]["command"], "bookokrat");
let response = RpcResponse::success(
Some(Value::String(id.clone())),
serde_json::json!({
"action_id": "open-reader",
"status": "completed",
"result": {
"terminal_id": "term_123",
"backend": "zellij",
"actual_placement": "background_session"
}
}),
);
if let Some(tx) = pending.lock().await.remove(&id) {
tx.send(response).unwrap();
}
let outcome = handle.await.unwrap().unwrap();
assert_eq!(outcome.action_id, "open-reader");
assert_eq!(outcome.status, crate::HostActionStatus::Completed);
assert_eq!(outcome.result.unwrap()["terminal_id"], "term_123");
}
#[tokio::test]
async fn test_bounded_read_normal_line() {
let data = b"hello world\n";
let mut reader = tokio::io::BufReader::new(&data[..]);
let mut buf = Vec::new();
let result = read_bounded_line(&mut reader, &mut buf).await;
let line = result.unwrap().unwrap();
assert_eq!(line.trim(), "hello world");
}
#[tokio::test]
async fn test_bounded_read_eof() {
let data = b"";
let mut reader = tokio::io::BufReader::new(&data[..]);
let mut buf = Vec::new();
let result = read_bounded_line(&mut reader, &mut buf).await;
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_bounded_read_partial_line_at_eof() {
let data = b"no newline";
let mut reader = tokio::io::BufReader::new(&data[..]);
let mut buf = Vec::new();
let result = read_bounded_line(&mut reader, &mut buf).await;
let line = result.unwrap().unwrap();
assert_eq!(line, "no newline");
}
#[tokio::test]
async fn test_bounded_read_multiple_lines() {
let data = b"line one\nline two\n";
let mut reader = tokio::io::BufReader::new(&data[..]);
let mut buf = Vec::new();
let line1 = read_bounded_line(&mut reader, &mut buf)
.await
.unwrap()
.unwrap();
assert_eq!(line1.trim(), "line one");
buf.clear();
let line2 = read_bounded_line(&mut reader, &mut buf)
.await
.unwrap()
.unwrap();
assert_eq!(line2.trim(), "line two");
}
#[tokio::test]
async fn test_bounded_read_rejects_oversized() {
let oversized = vec![b'x'; MAX_LINE_SIZE + 100];
let mut data = oversized;
data.push(b'\n');
let mut reader = tokio::io::BufReader::new(&data[..]);
let mut buf = Vec::new();
let result = read_bounded_line(&mut reader, &mut buf).await;
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(msg.contains("too large"));
}
#[tokio::test]
async fn test_bounded_read_exactly_at_limit() {
let mut data = vec![b'x'; MAX_LINE_SIZE - 1];
data.push(b'\n');
let mut reader = tokio::io::BufReader::new(&data[..]);
let mut buf = Vec::new();
let result = read_bounded_line(&mut reader, &mut buf).await;
assert!(result.is_ok());
let line = result.unwrap().unwrap();
assert_eq!(line.len(), MAX_LINE_SIZE);
}
#[tokio::test]
async fn test_bounded_read_empty_lines() {
let data = b"\n\nhello\n";
let mut reader = tokio::io::BufReader::new(&data[..]);
let mut buf = Vec::new();
let line1 = read_bounded_line(&mut reader, &mut buf)
.await
.unwrap()
.unwrap();
assert_eq!(line1.trim(), "");
buf.clear();
let line2 = read_bounded_line(&mut reader, &mut buf)
.await
.unwrap()
.unwrap();
assert_eq!(line2.trim(), "");
buf.clear();
let line3 = read_bounded_line(&mut reader, &mut buf)
.await
.unwrap()
.unwrap();
assert_eq!(line3.trim(), "hello");
}
}