1use std::collections::{HashMap, HashSet};
7use std::path::PathBuf;
8use std::process::Stdio;
9use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
10use std::sync::Arc;
11
12use async_trait::async_trait;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
16use tokio::process::{Child, ChildStdin, ChildStdout, Command};
17use tokio::sync::{mpsc, oneshot, Mutex, RwLock};
18use tokio::task::JoinHandle;
19
20use super::{ExtensionHandler, ExtensionHealth, RestartPolicy};
21use crate::extensions::hooks::events::{HookEvent, HookResult};
22use crate::extensions::manifest::CURRENT_EXTENSION_PROTOCOL_VERSION;
23
24#[derive(Serialize)]
25struct JsonRpcRequest {
26 jsonrpc: &'static str,
27 method: String,
28 params: Value,
29 id: u64,
30}
31
32#[derive(Serialize)]
33struct InitializeParams {
34 synaps_version: &'static str,
35 extension_protocol_version: u32,
36 plugin_id: String,
37 plugin_root: Option<String>,
38 config: Value,
39}
40
41#[derive(Debug, Clone, PartialEq, Deserialize)]
42pub struct RegisteredExtensionToolSpec {
43 pub name: String,
44 pub description: String,
45 pub input_schema: Value,
46}
47
48#[derive(Debug, Clone, PartialEq, Deserialize)]
49pub struct RegisteredProviderSpec {
50 pub id: String,
51 pub display_name: String,
52 pub description: String,
53 #[serde(default)]
54 pub models: Vec<RegisteredProviderModelSpec>,
55 #[serde(default)]
56 pub config_schema: Option<Value>,
57}
58
59#[derive(Debug, Clone, PartialEq, Deserialize)]
60pub struct RegisteredProviderModelSpec {
61 pub id: String,
62 #[serde(default)]
63 pub display_name: Option<String>,
64 #[serde(default)]
65 pub capabilities: Value,
66 #[serde(default)]
67 pub context_window: Option<u64>,
68}
69
70#[derive(Debug, Clone, Serialize)]
71pub struct ProviderCompleteParams {
72 pub provider_id: String,
73 pub model_id: String,
74 pub model: String,
75 pub messages: Vec<Value>,
76 pub system_prompt: Option<String>,
77 pub tools: Vec<Value>,
78 pub temperature: Option<f32>,
79 pub max_tokens: Option<u32>,
80 pub thinking_budget: u32,
81}
82
83#[derive(Debug, Clone, PartialEq, Deserialize)]
84pub struct ProviderCompleteResult {
85 pub content: Vec<Value>,
86 #[serde(default)]
87 pub stop_reason: Option<String>,
88 #[serde(default)]
89 pub usage: Option<Value>,
90}
91
92#[derive(Debug, Clone, PartialEq)]
93pub struct ProviderToolUse {
94 pub id: String,
95 pub name: String,
96 pub input: Value,
97}
98
99#[derive(Debug, Clone, PartialEq)]
101pub enum ProviderStreamEvent {
102 TextDelta { text: String },
104 ThinkingDelta { text: String },
106 ToolUse {
108 id: String,
109 name: String,
110 input: Value,
111 },
112 Usage { usage: Value },
114 Error { message: String },
116 Done,
118}
119
120pub fn parse_provider_stream_event(params: &Value) -> Result<ProviderStreamEvent, String> {
126 let inner = match params.get("event") {
127 Some(ev) => ev,
128 None => params,
129 };
130 let obj = inner
131 .as_object()
132 .ok_or_else(|| "provider stream event must be a JSON object".to_string())?;
133
134 let ty = obj
135 .get("type")
136 .and_then(Value::as_str)
137 .ok_or_else(|| "provider stream event missing type".to_string())?;
138
139 match ty {
140 "text" => {
141 let text = obj
142 .get("delta")
143 .or_else(|| obj.get("text"))
144 .and_then(Value::as_str)
145 .ok_or_else(|| {
146 "provider stream text event missing 'delta' or 'text'".to_string()
147 })?;
148 Ok(ProviderStreamEvent::TextDelta {
149 text: text.to_string(),
150 })
151 }
152 "thinking" => {
153 let text = obj
154 .get("delta")
155 .or_else(|| obj.get("text"))
156 .and_then(Value::as_str)
157 .ok_or_else(|| {
158 "provider stream thinking event missing 'delta' or 'text'".to_string()
159 })?;
160 Ok(ProviderStreamEvent::ThinkingDelta {
161 text: text.to_string(),
162 })
163 }
164 "tool_use" => {
165 let id = obj
166 .get("id")
167 .and_then(Value::as_str)
168 .ok_or_else(|| "provider stream tool_use missing id".to_string())?;
169 if id.is_empty() {
170 return Err("provider stream tool_use id must be non-empty".to_string());
171 }
172 let name = obj
173 .get("name")
174 .and_then(Value::as_str)
175 .ok_or_else(|| "provider stream tool_use missing name".to_string())?;
176 if name.is_empty() {
177 return Err("provider stream tool_use name must be non-empty".to_string());
178 }
179 let input = match obj.get("input") {
180 None => Value::Object(Default::default()),
181 Some(v) if v.is_object() => v.clone(),
182 Some(_) => {
183 return Err("provider stream tool_use input must be a JSON object".to_string());
184 }
185 };
186 Ok(ProviderStreamEvent::ToolUse {
187 id: id.to_string(),
188 name: name.to_string(),
189 input,
190 })
191 }
192 "usage" => {
193 let mut clone = obj.clone();
194 clone.remove("type");
195 Ok(ProviderStreamEvent::Usage {
196 usage: Value::Object(clone),
197 })
198 }
199 "error" => {
200 let message = obj
201 .get("message")
202 .and_then(Value::as_str)
203 .ok_or_else(|| "provider stream error missing message".to_string())?;
204 if message.is_empty() {
205 return Err("provider stream error message must be non-empty".to_string());
206 }
207 Ok(ProviderStreamEvent::Error {
208 message: message.to_string(),
209 })
210 }
211 "done" => Ok(ProviderStreamEvent::Done),
212 other => Err(format!("unknown provider stream event type: {other}")),
213 }
214}
215
216pub async fn execute_provider_tool_use(
217 registry: &crate::ToolRegistry,
218 hook_bus: &Arc<crate::extensions::hooks::HookBus>,
219 tool_use: ProviderToolUse,
220 ctx: crate::ToolContext,
221 max_tool_output: usize,
222) -> Value {
223 let tool_id = tool_use.id;
224 let tool_name = tool_use.name;
225 let input = tool_use.input;
226
227 let Some(tool) = registry.get(&tool_name).cloned() else {
228 return serde_json::json!({
229 "type": "tool_result",
230 "tool_use_id": tool_id,
231 "content": format!("Unknown tool: {}", tool_name),
232 "is_error": true,
233 });
234 };
235
236 let runtime_name = registry.runtime_name_for_api(&tool_name).to_string();
237 let input = registry.translate_input_for_api_tool(&tool_name, input);
238 let decision = crate::runtime::resolve_before_tool_call_decision(
239 input.clone(),
240 crate::runtime::emit_before_tool_call(
241 hook_bus,
242 &tool_name,
243 Some(&runtime_name),
244 input.clone(),
245 )
246 .await,
247 ctx.capabilities.secret_prompt.as_ref(),
248 false,
249 )
250 .await;
251
252 let crate::runtime::BeforeToolCallDecision::Continue { input } = decision else {
253 let crate::runtime::BeforeToolCallDecision::Block { reason } = decision else {
254 unreachable!()
255 };
256 return serde_json::json!({
257 "type": "tool_result",
258 "tool_use_id": tool_id,
259 "content": format!("Tool call blocked by extension: {}", reason),
260 "is_error": true,
261 });
262 };
263
264 let input_for_hook = input.clone();
265 let (result, is_error) = match tool.execute(input, ctx).await {
266 Ok(output) => (output, false),
267 Err(error) => (format!("Tool execution failed: {}", error), true),
268 };
269 let result = crate::runtime::emit_after_tool_call(
270 hook_bus,
271 &tool_name,
272 Some(&runtime_name),
273 input_for_hook,
274 result,
275 max_tool_output,
276 )
277 .await;
278
279 let mut response = serde_json::json!({
280 "type": "tool_result",
281 "tool_use_id": tool_id,
282 "content": crate::truncate_str(&result, max_tool_output).to_string(),
283 });
284 if is_error {
285 response["is_error"] = serde_json::json!(true);
286 }
287 response
288}
289
290pub async fn complete_provider_with_tools<F>(
291 handler: Arc<dyn ExtensionHandler>,
292 mut params: ProviderCompleteParams,
293 registry: &crate::ToolRegistry,
294 hook_bus: &Arc<crate::extensions::hooks::HookBus>,
295 mut context_factory: F,
296 max_tool_output: usize,
297 max_iterations: usize,
298) -> Result<ProviderCompleteResult, String>
299where
300 F: FnMut() -> crate::ToolContext,
301{
302 let max_iterations = max_iterations.max(1);
303 for iteration in 0..max_iterations {
304 let result = handler.provider_complete(params.clone()).await?;
305 let tool_uses = extract_provider_tool_uses(&result.content)?;
306 if tool_uses.is_empty() {
307 return Ok(result);
308 }
309 if iteration + 1 == max_iterations {
310 return Err(format!(
311 "extension provider '{}' exceeded provider tool-use iteration limit ({})",
312 handler.id(),
313 max_iterations,
314 ));
315 }
316
317 let assistant_content = result.content.clone();
318 params.messages.push(serde_json::json!({
319 "role": "assistant",
320 "content": assistant_content,
321 }));
322
323 let mut tool_results = Vec::with_capacity(tool_uses.len());
324 for tool_use in tool_uses {
325 tool_results.push(
326 execute_provider_tool_use(
327 registry,
328 hook_bus,
329 tool_use,
330 context_factory(),
331 max_tool_output,
332 )
333 .await,
334 );
335 }
336 params.messages.push(serde_json::json!({
337 "role": "user",
338 "content": tool_results,
339 }));
340 }
341 Err(format!(
342 "extension provider '{}' exceeded provider tool-use iteration limit ({})",
343 handler.id(),
344 max_iterations,
345 ))
346}
347
348pub fn extract_provider_tool_uses(content: &[Value]) -> Result<Vec<ProviderToolUse>, String> {
349 let mut tool_uses = Vec::new();
350 for block in content {
351 if block.get("type").and_then(Value::as_str) != Some("tool_use") {
352 continue;
353 }
354 let id = block
355 .get("id")
356 .and_then(Value::as_str)
357 .ok_or_else(|| "provider tool_use missing id".to_string())?;
358 let name = block
359 .get("name")
360 .and_then(Value::as_str)
361 .ok_or_else(|| "provider tool_use missing name".to_string())?;
362 if id.trim().is_empty() {
363 return Err("provider tool_use id is empty".to_string());
364 }
365 if name.trim().is_empty() {
366 return Err("provider tool_use name is empty".to_string());
367 }
368 let input = block
369 .get("input")
370 .cloned()
371 .unwrap_or_else(|| serde_json::json!({}));
372 if !input.is_object() {
373 return Err(format!(
374 "provider tool_use '{}' input must be a JSON object",
375 id
376 ));
377 }
378 tool_uses.push(ProviderToolUse {
379 id: id.to_string(),
380 name: name.to_string(),
381 input,
382 });
383 }
384 Ok(tool_uses)
385}
386
387#[derive(Debug, Clone, Default, PartialEq)]
388pub struct InitializeCapabilitiesResult {
389 pub tools: Vec<RegisteredExtensionToolSpec>,
390 pub providers: Vec<RegisteredProviderSpec>,
391 pub capabilities: Vec<CapabilityDeclaration>,
397}
398
399#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
408pub struct CapabilityDeclaration {
409 pub kind: String,
413 pub name: String,
416 #[serde(default)]
421 pub permissions: Vec<String>,
422 #[serde(default, skip_serializing_if = "is_null_value")]
426 pub params: serde_json::Value,
427}
428
429fn is_null_value(v: &serde_json::Value) -> bool {
430 v.is_null()
431}
432
433pub fn validate_capability(
443 decl: &CapabilityDeclaration,
444 granted: &crate::extensions::permissions::PermissionSet,
445) -> Result<(), String> {
446 use crate::extensions::permissions::Permission;
447 if decl.kind.trim().is_empty() {
448 return Err("capability 'kind' must be non-empty".to_string());
449 }
450 if decl.name.trim().is_empty() {
451 return Err("capability 'name' must be non-empty".to_string());
452 }
453 for perm_name in &decl.permissions {
454 let parsed = Permission::parse(perm_name).ok_or_else(|| {
455 format!(
456 "capability '{}' declares unknown permission '{}'",
457 decl.kind, perm_name
458 )
459 })?;
460 if !granted.has(parsed) {
461 return Err(format!(
462 "capability '{}' requires permission '{}' but it is not granted",
463 decl.kind, perm_name
464 ));
465 }
466 }
467 Ok(())
468}
469
470#[derive(Deserialize)]
471struct InitializeResult {
472 protocol_version: u32,
473 #[serde(default)]
474 capabilities: InitializeCapabilities,
475}
476
477#[derive(Default, Deserialize)]
478struct InitializeCapabilities {
479 #[serde(default)]
480 tools: Vec<RegisteredExtensionToolSpec>,
481 #[serde(default)]
482 providers: Vec<RegisteredProviderSpec>,
483 #[serde(default)]
485 capabilities: Vec<CapabilityDeclaration>,
486}
487
488#[doc(hidden)]
494#[derive(Debug, Clone)]
495pub struct NotificationFrame {
496 pub method: String,
497 pub params: Value,
498}
499
500struct Inbox {
507 pending: Mutex<HashMap<u64, oneshot::Sender<Result<Value, String>>>>,
508 notification_sinks: Mutex<Vec<(usize, mpsc::UnboundedSender<NotificationFrame>)>>,
514 next_sink_id: std::sync::atomic::AtomicUsize,
518 closed: std::sync::atomic::AtomicBool,
521 permissions: RwLock<Option<crate::extensions::permissions::PermissionSet>>,
524 inbound_stdin: Mutex<Option<Arc<Mutex<ChildStdin>>>>,
528 extension_id: String,
530}
531
532impl Inbox {
533 fn new(extension_id: String) -> Self {
534 Self {
535 pending: Mutex::new(HashMap::new()),
536 notification_sinks: Mutex::new(Vec::new()),
537 next_sink_id: std::sync::atomic::AtomicUsize::new(0),
538 closed: std::sync::atomic::AtomicBool::new(false),
539 permissions: RwLock::new(None),
540 inbound_stdin: Mutex::new(None),
541 extension_id,
542 }
543 }
544
545 async fn fail_all_pending(&self, reason: &str) {
548 self.closed
549 .store(true, std::sync::atomic::Ordering::Release);
550 let drained: Vec<_> = {
551 let mut pending = self.pending.lock().await;
552 pending.drain().collect()
553 };
554 for (_, tx) in drained {
555 let _ = tx.send(Err(reason.to_string()));
556 }
557 }
558}
559
560struct ProcessState {
561 child: Child,
562 stdin: Arc<Mutex<ChildStdin>>,
563 reader_handle: JoinHandle<()>,
564}
565
566pub struct ProcessExtension {
568 id: String,
569 command: String,
570 args: Vec<String>,
571 cwd: Option<PathBuf>,
572 state: Arc<Mutex<Option<ProcessState>>>,
573 call_lock: Arc<Mutex<()>>,
575 next_id: AtomicU64,
576 restart_count: AtomicUsize,
577 total_restarts: AtomicUsize,
583 pub(crate) restart_policy: RestartPolicy,
585 inbox: Arc<Inbox>,
589}
590
591impl ProcessExtension {
592 pub async fn spawn(id: &str, command: &str, args: &[String]) -> Result<Self, String> {
593 Self::spawn_with_cwd(id, command, args, None).await
594 }
595
596 pub async fn spawn_with_cwd(
601 id: &str,
602 command: &str,
603 args: &[String],
604 cwd: Option<PathBuf>,
605 ) -> Result<Self, String> {
606 let inbox = Arc::new(Inbox::new(id.to_string()));
607 let state = Self::spawn_state(id, command, args, cwd.as_ref(), inbox.clone()).await?;
608 Ok(Self {
609 id: id.to_string(),
610 command: command.to_string(),
611 args: args.to_vec(),
612 cwd,
613 state: Arc::new(Mutex::new(Some(state))),
614 call_lock: Arc::new(Mutex::new(())),
615 next_id: AtomicU64::new(1),
616 restart_count: AtomicUsize::new(0),
617 total_restarts: AtomicUsize::new(0),
618 restart_policy: RestartPolicy::default(),
619 inbox,
620 })
621 }
622
623 pub fn with_restart_policy(mut self, policy: RestartPolicy) -> Self {
625 self.restart_policy = policy;
626 self
627 }
628
629 async fn spawn_state(
630 id: &str,
631 command: &str,
632 args: &[String],
633 cwd: Option<&PathBuf>,
634 inbox: Arc<Inbox>,
635 ) -> Result<ProcessState, String> {
636 let mut cmd = Command::new(command);
637 cmd.args(args)
638 .stdin(Stdio::piped())
639 .stdout(Stdio::piped())
640 .stderr(Stdio::piped());
641 if let Some(cwd) = cwd {
642 cmd.current_dir(cwd);
643 }
644
645 cmd.env_clear();
651 for var in &["PATH", "HOME", "LANG", "TERM", "XDG_RUNTIME_DIR"] {
652 if let Ok(val) = std::env::var(var) {
653 cmd.env(var, val);
654 }
655 }
656
657 cmd.kill_on_drop(true);
658
659 let mut child = cmd
660 .spawn()
661 .map_err(|e| format!("Failed to spawn extension '{}': {}", id, e))?;
662
663 let stdin = child
664 .stdin
665 .take()
666 .ok_or_else(|| format!("No stdin for extension '{}'", id))?;
667 let stdout = child
668 .stdout
669 .take()
670 .ok_or_else(|| format!("No stdout for extension '{}'", id))?;
671 if let Some(stderr) = child.stderr.take() {
672 let extension_id = id.to_string();
673 tokio::spawn(async move {
674 let mut lines = BufReader::new(stderr).lines();
675 loop {
676 match lines.next_line().await {
677 Ok(Some(line)) => {
678 tracing::debug!(extension = %extension_id, stderr = %line);
679 }
680 Ok(None) => break,
681 Err(error) => {
682 tracing::debug!(
683 extension = %extension_id,
684 error = %error,
685 "Failed to read extension stderr",
686 );
687 break;
688 }
689 }
690 }
691 });
692 }
693
694 let reader_handle = Self::spawn_reader(stdout, inbox.clone(), id.to_string());
695
696 let stdin_arc = Arc::new(Mutex::new(stdin));
697 *inbox.inbound_stdin.lock().await = Some(stdin_arc.clone());
700
701 Ok(ProcessState {
702 child,
703 stdin: stdin_arc,
704 reader_handle,
705 })
706 }
707
708 fn spawn_reader(
713 stdout: ChildStdout,
714 inbox: Arc<Inbox>,
715 extension_id: String,
716 ) -> JoinHandle<()> {
717 tokio::spawn(async move {
718 let mut reader = BufReader::new(stdout);
719 loop {
720 match Self::read_one_frame(&mut reader, &extension_id).await {
721 Ok(Some(value)) => {
722 Self::dispatch_frame(value, &inbox, &extension_id).await;
723 }
724 Ok(None) => {
725 tracing::debug!(
726 extension = %extension_id,
727 "Extension stdout closed (EOF); failing pending requests",
728 );
729 inbox.fail_all_pending("transport closed: EOF").await;
730 inbox.notification_sinks.lock().await.clear();
732 return;
733 }
734 Err(error) => {
735 tracing::debug!(
736 extension = %extension_id,
737 error = %error,
738 "Extension transport read error",
739 );
740 inbox
741 .fail_all_pending(&format!("transport error: {}", error))
742 .await;
743 inbox.notification_sinks.lock().await.clear();
744 return;
745 }
746 }
747 }
748 })
749 }
750
751 async fn read_one_frame(
755 reader: &mut BufReader<ChildStdout>,
756 extension_id: &str,
757 ) -> Result<Option<Value>, String> {
758 let mut content_length: Option<usize> = None;
759 let mut saw_any_header = false;
760 loop {
761 let mut header_line = String::new();
762 let n = reader
763 .read_line(&mut header_line)
764 .await
765 .map_err(|e| format!("Read header error: {}", e))?;
766 if n == 0 {
767 if saw_any_header {
768 return Err("Unexpected EOF while reading response headers".into());
769 }
770 return Ok(None);
771 }
772 saw_any_header = true;
773 if header_line.len() > 1024 {
774 return Err(format!(
775 "Extension '{}' header line too long ({} bytes)",
776 extension_id,
777 header_line.len()
778 ));
779 }
780 let trimmed = header_line.trim();
781 if trimmed.is_empty() {
782 break;
783 }
784 if let Some((name, value)) = trimmed.split_once(':') {
785 if name.trim().eq_ignore_ascii_case("Content-Length") {
786 content_length = Some(value.trim().parse().map_err(|_| {
787 format!("Invalid Content-Length value: {:?}", value.trim())
788 })?);
789 }
790 }
791 }
792 let content_length = content_length.ok_or_else(|| {
793 format!(
794 "Extension '{}' frame missing Content-Length header",
795 extension_id
796 )
797 })?;
798 const MAX_RESPONSE_SIZE: usize = 4 * 1024 * 1024;
799 if content_length > MAX_RESPONSE_SIZE {
800 return Err(format!(
801 "Extension '{}' frame too large: {} bytes (max {})",
802 extension_id, content_length, MAX_RESPONSE_SIZE
803 ));
804 }
805 let mut buf = vec![0u8; content_length];
806 tokio::io::AsyncReadExt::read_exact(reader, &mut buf)
807 .await
808 .map_err(|e| format!("Read body error: {}", e))?;
809 let value: Value =
810 serde_json::from_slice(&buf).map_err(|e| format!("Parse frame error: {}", e))?;
811 Ok(Some(value))
812 }
813
814 async fn dispatch_frame(value: Value, inbox: &Arc<Inbox>, extension_id: &str) {
820 let id_field = value.get("id");
821 let id_is_present = !matches!(id_field, None | Some(Value::Null));
822 let method_field = value
823 .get("method")
824 .and_then(Value::as_str)
825 .map(str::to_string);
826
827 if id_is_present && method_field.is_some() {
828 let id = match id_field.and_then(Value::as_u64) {
831 Some(id) => id,
832 None => {
833 tracing::trace!(
834 extension = %extension_id,
835 frame = %value,
836 "Discarding inbound request with non-numeric id",
837 );
838 return;
839 }
840 };
841 let Some(method) = method_field else { return };
842 let params = value.get("params").cloned().unwrap_or(Value::Null);
843 let inbox = inbox.clone();
844 let extension_id = extension_id.to_string();
845 tokio::spawn(async move {
846 let outcome = Self::handle_inbound_request(&inbox, &method, params).await;
847 let payload = match outcome {
848 Ok(result) => serde_json::json!({
849 "jsonrpc": "2.0",
850 "id": id,
851 "result": result,
852 }),
853 Err((code, message)) => serde_json::json!({
854 "jsonrpc": "2.0",
855 "id": id,
856 "error": {"code": code, "message": message},
857 }),
858 };
859 let stdin_handle = inbox.inbound_stdin.lock().await.clone();
860 if let Some(stdin) = stdin_handle {
861 let body = match serde_json::to_string(&payload) {
862 Ok(s) => s,
863 Err(error) => {
864 tracing::warn!(
865 extension = %extension_id,
866 error = %error,
867 "Failed to serialize inbound response",
868 );
869 return;
870 }
871 };
872 let frame = format!("Content-Length: {}\r\n\r\n{}", body.len(), body);
873 let mut stdin = stdin.lock().await;
874 if let Err(error) = stdin.write_all(frame.as_bytes()).await {
875 tracing::warn!(
876 extension = %extension_id,
877 error = %error,
878 "Failed to write inbound response",
879 );
880 return;
881 }
882 if let Err(error) = stdin.flush().await {
883 tracing::warn!(
884 extension = %extension_id,
885 error = %error,
886 "Failed to flush inbound response",
887 );
888 }
889 } else {
890 tracing::warn!(
891 extension = %extension_id,
892 "No stdin available to reply to inbound request",
893 );
894 }
895 });
896 return;
897 }
898
899 if id_is_present {
900 let id = match id_field.and_then(Value::as_u64) {
901 Some(id) => id,
902 None => {
903 tracing::trace!(
904 extension = %extension_id,
905 frame = %value,
906 "Discarding frame with non-numeric id",
907 );
908 return;
909 }
910 };
911 let sender = inbox.pending.lock().await.remove(&id);
912 match sender {
913 Some(tx) => {
914 let payload = if let Some(err) = value.get("error") {
915 let message = err
916 .get("message")
917 .and_then(Value::as_str)
918 .unwrap_or("unknown extension error")
919 .to_string();
920 Err(format!("Extension error: {}", message))
921 } else {
922 Ok(value.get("result").cloned().unwrap_or(Value::Null))
923 };
924 let _ = tx.send(payload);
925 }
926 None => {
927 tracing::trace!(
928 extension = %extension_id,
929 id = id,
930 "Response with unknown id (no pending request); dropping",
931 );
932 }
933 }
934 } else if let Some(method) = value.get("method").and_then(Value::as_str) {
935 let params = value.get("params").cloned().unwrap_or(Value::Null);
936 let frame = NotificationFrame {
937 method: method.to_string(),
938 params,
939 };
940 let mut sinks = inbox.notification_sinks.lock().await;
941 if sinks.is_empty() {
942 tracing::trace!(
943 extension = %extension_id,
944 method = %method,
945 "Notification with no active subscribers; dropping",
946 );
947 } else {
948 sinks.retain(|(_, tx)| tx.send(frame.clone()).is_ok());
952 }
953 } else {
954 tracing::trace!(
955 extension = %extension_id,
956 frame = %value,
957 "Unrecognized frame; dropping",
958 );
959 }
960 }
961
962 pub fn restart_count(&self) -> usize {
963 self.total_restarts.load(Ordering::Relaxed)
966 }
967
968 pub async fn set_permissions(&self, perms: crate::extensions::permissions::PermissionSet) {
971 *self.inbox.permissions.write().await = Some(perms);
972 }
973
974 #[allow(clippy::doc_lazy_continuation)]
982 async fn handle_inbound_request(
983 inbox: &Arc<Inbox>,
984 method: &str,
985 params: Value,
986 ) -> Result<Value, (i32, String)> {
987 use crate::extensions::permissions::Permission;
988 use crate::memory::store::{self, MemoryQuery};
989
990 match method {
991 "memory.append" => {
992 Self::require_permission(inbox, Permission::MemoryWrite, "memory.write").await?;
993 let namespace = Self::param_str(¶ms, "namespace")?;
994 Self::require_namespace_matches(inbox, &namespace).await?;
995 let content = Self::param_str(¶ms, "content")?;
996 let tags = match params.get("tags") {
997 None | Some(Value::Null) => Vec::new(),
998 Some(Value::Array(arr)) => {
999 let mut out = Vec::with_capacity(arr.len());
1000 for v in arr {
1001 match v.as_str() {
1002 Some(s) => out.push(s.to_string()),
1003 None => {
1004 return Err((
1005 -32602,
1006 "tags must be an array of strings".to_string(),
1007 ))
1008 }
1009 }
1010 }
1011 out
1012 }
1013 _ => return Err((-32602, "tags must be an array of strings".to_string())),
1014 };
1015 let meta = match params.get("meta") {
1016 None | Some(Value::Null) => None,
1017 Some(v) => Some(v.clone()),
1018 };
1019 let record = store::new_record(namespace, content, tags, meta);
1020 let timestamp_ms = record.timestamp_ms;
1021 store::append(&record).map_err(|e| (-32000, e.to_string()))?;
1022 Ok(serde_json::json!({"ok": true, "timestamp_ms": timestamp_ms}))
1023 }
1024 "memory.query" => {
1025 Self::require_permission(inbox, Permission::MemoryRead, "memory.read").await?;
1026 let namespace = Self::param_str(¶ms, "namespace")?;
1027 Self::require_namespace_matches(inbox, &namespace).await?;
1028 let q = MemoryQuery {
1029 content_contains: params
1030 .get("content_contains")
1031 .and_then(Value::as_str)
1032 .map(str::to_string),
1033 tag_prefix: params
1034 .get("tag_prefix")
1035 .and_then(Value::as_str)
1036 .map(str::to_string),
1037 since_ms: params.get("since_ms").and_then(Value::as_u64),
1038 until_ms: params.get("until_ms").and_then(Value::as_u64),
1039 limit: params
1040 .get("limit")
1041 .and_then(Value::as_u64)
1042 .map(|n| n as usize),
1043 };
1044 let records = store::query(&namespace, &q).map_err(|e| (-32000, e.to_string()))?;
1045 Ok(serde_json::json!({"records": records}))
1046 }
1047 "config.get" => {
1048 let key = Self::param_str(¶ms, "key")?;
1049 Self::validate_config_key(&key)?;
1050 let value =
1051 crate::extensions::config_store::read_plugin_config(&inbox.extension_id, &key);
1052 Ok(serde_json::json!({"value": value}))
1053 }
1054 "config.set" => {
1055 Self::require_permission(inbox, Permission::ConfigWrite, "config.write").await?;
1056 let key = Self::param_str(¶ms, "key")?;
1057 Self::validate_config_key(&key)?;
1058 let value = Self::param_str(¶ms, "value")?;
1059 crate::extensions::config_store::write_plugin_config(
1060 &inbox.extension_id,
1061 &key,
1062 &value,
1063 )
1064 .map_err(|e| (-32000, e.to_string()))?;
1065 Ok(serde_json::json!({"ok": true}))
1066 }
1067 "config.subscribe" => {
1068 Self::require_permission(inbox, Permission::ConfigSubscribe, "config.subscribe")
1069 .await?;
1070 Ok(serde_json::json!({"ok": true}))
1074 }
1075 other => Err((-32601, format!("method not found: {other}"))),
1076 }
1077 }
1078
1079 async fn require_permission(
1080 inbox: &Arc<Inbox>,
1081 perm: crate::extensions::permissions::Permission,
1082 wire: &str,
1083 ) -> Result<(), (i32, String)> {
1084 let guard = inbox.permissions.read().await;
1085 match guard.as_ref() {
1086 Some(set) if set.has(perm) => Ok(()),
1087 _ => Err((-32602, format!("permission denied: {wire} required"))),
1088 }
1089 }
1090
1091 async fn require_namespace_matches(
1092 inbox: &Arc<Inbox>,
1093 namespace: &str,
1094 ) -> Result<(), (i32, String)> {
1095 if namespace == inbox.extension_id {
1096 Ok(())
1097 } else {
1098 Err((
1099 -32602,
1100 format!(
1101 "namespace must equal extension id '{}' (got '{}')",
1102 inbox.extension_id, namespace
1103 ),
1104 ))
1105 }
1106 }
1107
1108 fn param_str(params: &Value, name: &str) -> Result<String, (i32, String)> {
1109 params
1110 .get(name)
1111 .and_then(Value::as_str)
1112 .map(str::to_string)
1113 .ok_or_else(|| (-32602, format!("missing or invalid '{name}' parameter")))
1114 }
1115
1116 fn validate_config_key(key: &str) -> Result<(), (i32, String)> {
1117 let trimmed = key.trim();
1118 if trimmed.is_empty() {
1119 return Err((-32602, "config key must be non-empty".to_string()));
1120 }
1121 if trimmed.contains('.') || trimmed.contains('/') || trimmed.contains(' ') {
1122 return Err((
1123 -32602,
1124 "config key must not contain dots, slashes, or spaces".to_string(),
1125 ));
1126 }
1127 Ok(())
1128 }
1129
1130 pub async fn initialize(
1131 &self,
1132 plugin_root: Option<PathBuf>,
1133 config: Value,
1134 ) -> Result<InitializeCapabilitiesResult, String> {
1135 let params = InitializeParams {
1136 synaps_version: env!("CARGO_PKG_VERSION"),
1137 extension_protocol_version: CURRENT_EXTENSION_PROTOCOL_VERSION,
1138 plugin_id: self.id.clone(),
1139 plugin_root: plugin_root
1140 .or_else(|| self.cwd.clone())
1141 .map(|path| path.to_string_lossy().to_string()),
1142 config,
1143 };
1144 let value = self
1145 .call_no_restart(
1146 "initialize",
1147 serde_json::to_value(params).map_err(|e| e.to_string())?,
1148 )
1149 .await?;
1150 Self::parse_initialize_result(&self.id, value)
1151 }
1152
1153 fn parse_initialize_result(
1154 id: &str,
1155 value: Value,
1156 ) -> Result<InitializeCapabilitiesResult, String> {
1157 let result: InitializeResult = serde_json::from_value(value)
1158 .map_err(|e| format!("Invalid initialize response from extension '{}': {}", id, e))?;
1159 if result.protocol_version != CURRENT_EXTENSION_PROTOCOL_VERSION {
1160 return Err(format!(
1161 "Extension '{}' initialize returned unsupported protocol_version {} (supported: {})",
1162 id, result.protocol_version, CURRENT_EXTENSION_PROTOCOL_VERSION,
1163 ));
1164 }
1165 Self::validate_registered_tool_specs(id, &result.capabilities.tools)?;
1166 Self::validate_registered_provider_specs(id, &result.capabilities.providers)?;
1167 Ok(InitializeCapabilitiesResult {
1168 tools: result.capabilities.tools,
1169 providers: result.capabilities.providers,
1170 capabilities: result.capabilities.capabilities,
1171 })
1172 }
1173
1174 fn validate_registered_tool_specs(
1175 id: &str,
1176 tools: &[RegisteredExtensionToolSpec],
1177 ) -> Result<(), String> {
1178 use crate::extensions::validation::{validate_id_segment, IdValidationError};
1179 let mut names = HashSet::new();
1180 for tool in tools {
1181 let name = tool.name.trim();
1182 if let Err(err) = validate_id_segment(name) {
1183 return Err(match err {
1184 IdValidationError::Empty => format!(
1185 "Extension '{}' registered a tool with an empty tool name",
1186 id
1187 ),
1188 IdValidationError::ContainsReserved { ch } => format!(
1189 "Extension '{}' registered tool '{}' with invalid tool name: '{}' is reserved",
1190 id, name, ch
1191 ),
1192 IdValidationError::TooLong { len, max } => format!(
1193 "Extension '{}' registered tool '{}' with invalid tool name: must be at most {} chars (got {})",
1194 id, name, max, len
1195 ),
1196 IdValidationError::ContainsWhitespace => format!(
1197 "Extension '{}' registered tool '{}' with invalid tool name: must not contain whitespace",
1198 id, name
1199 ),
1200 IdValidationError::ContainsControl { ch } => format!(
1201 "Extension '{}' registered tool '{}' with invalid tool name: contains control character U+{:04X}",
1202 id, name, ch as u32
1203 ),
1204 });
1205 }
1206 if !names.insert(name.to_string()) {
1207 return Err(format!(
1208 "Extension '{}' registered duplicate tool name '{}'",
1209 id, name
1210 ));
1211 }
1212 if tool.description.trim().is_empty() {
1213 return Err(format!(
1214 "Extension '{}' registered tool '{}' with an empty description",
1215 id, name,
1216 ));
1217 }
1218 if !tool.input_schema.is_object() {
1219 return Err(format!(
1220 "Extension '{}' registered tool '{}' with invalid input_schema: input_schema must be a JSON object",
1221 id, name,
1222 ));
1223 }
1224 }
1225 Ok(())
1226 }
1227
1228 fn validate_registered_provider_specs(
1229 id: &str,
1230 providers: &[RegisteredProviderSpec],
1231 ) -> Result<(), String> {
1232 use crate::extensions::validation::{validate_id_segment, IdValidationError};
1233 for provider in providers {
1234 let provider_id = provider.id.trim();
1235 match validate_id_segment(provider_id) {
1236 Ok(()) => {
1237 if !Self::is_safe_provider_id(provider_id) {
1238 return Err(format!(
1239 "Extension '{}' registered provider '{}' with invalid provider id",
1240 id, provider_id
1241 ));
1242 }
1243 }
1244 Err(IdValidationError::Empty) => {
1245 return Err(format!(
1246 "Extension '{}' registered provider with empty provider id",
1247 id
1248 ));
1249 }
1250 Err(err) => {
1251 return Err(format!(
1252 "Extension '{}' registered provider '{}' with invalid provider id: {}",
1253 id, provider_id, err
1254 ));
1255 }
1256 }
1257 if provider.display_name.trim().is_empty() {
1258 return Err(format!(
1259 "Extension '{}' registered provider '{}' with empty display_name",
1260 id, provider_id,
1261 ));
1262 }
1263 if provider.description.trim().is_empty() {
1264 return Err(format!(
1265 "Extension '{}' registered provider '{}' with empty description",
1266 id, provider_id,
1267 ));
1268 }
1269 if provider.models.is_empty() {
1270 return Err(format!(
1271 "Extension '{}' registered provider '{}' must declare at least one model",
1272 id, provider_id,
1273 ));
1274 }
1275 let mut model_ids = HashSet::new();
1276 for model in &provider.models {
1277 let model_id = model.id.trim();
1278 if let Err(err) = validate_id_segment(model_id) {
1279 return Err(match err {
1280 IdValidationError::Empty => format!(
1281 "Extension '{}' registered provider '{}' with empty model id",
1282 id, provider_id
1283 ),
1284 IdValidationError::ContainsReserved { ch } => format!(
1285 "Extension '{}' registered provider '{}' with invalid model id '{}': '{}' is reserved",
1286 id, provider_id, model_id, ch
1287 ),
1288 IdValidationError::TooLong { len, max } => format!(
1289 "Extension '{}' registered provider '{}' with invalid model id '{}': must be at most {} chars (got {})",
1290 id, provider_id, model_id, max, len
1291 ),
1292 IdValidationError::ContainsWhitespace => format!(
1293 "Extension '{}' registered provider '{}' with invalid model id '{}': must not contain whitespace",
1294 id, provider_id, model_id
1295 ),
1296 IdValidationError::ContainsControl { ch } => format!(
1297 "Extension '{}' registered provider '{}' with invalid model id '{}': contains control character U+{:04X}",
1298 id, provider_id, model_id, ch as u32
1299 ),
1300 });
1301 }
1302 if !model_ids.insert(model_id.to_string()) {
1303 return Err(format!(
1304 "Extension '{}' registered provider '{}' with duplicate model id '{}'",
1305 id, provider_id, model_id,
1306 ));
1307 }
1308 }
1309 if let Some(config_schema) = &provider.config_schema {
1310 if !config_schema.is_object() {
1311 return Err(format!(
1312 "Extension '{}' registered provider '{}' with invalid config_schema: config_schema must be a JSON object",
1313 id, provider_id,
1314 ));
1315 }
1316 }
1317 }
1318 Ok(())
1319 }
1320
1321 fn is_safe_provider_id(id: &str) -> bool {
1322 !id.is_empty()
1323 && !id.contains(':')
1324 && id
1325 .chars()
1326 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
1327 }
1328
1329 #[doc(hidden)]
1330 pub async fn initialize_for_test(&self, plugin_root: Option<PathBuf>) -> Result<(), String> {
1331 self.initialize(plugin_root, Value::Object(Default::default()))
1332 .await
1333 .map(|_| ())
1334 }
1335
1336 async fn restart_locked(&self, state: &mut Option<ProcessState>) -> Result<(), String> {
1337 let attempted = self.restart_count.fetch_add(1, Ordering::Relaxed) + 1;
1338 self.total_restarts.fetch_add(1, Ordering::Relaxed);
1339 let max_attempts = self.restart_policy.max_attempts;
1340 if attempted > max_attempts as usize {
1341 *state = None;
1342 return Err(format!(
1343 "Extension '{}' exceeded restart limit ({})",
1344 self.id, max_attempts,
1345 ));
1346 }
1347
1348 if let Some(old) = state.take() {
1349 old.reader_handle.abort();
1350 let mut child = old.child;
1351 let _ = child.kill().await;
1352 }
1353 self.inbox
1355 .fail_all_pending("transport closed: process restarting")
1356 .await;
1357
1358 let delay = self
1359 .restart_policy
1360 .delay_for_attempt(attempted as u32)
1361 .unwrap_or_default();
1362
1363 tracing::warn!(
1364 extension = %self.id,
1365 attempt = attempted,
1366 max_attempts = max_attempts,
1367 delay_ms = delay.as_millis() as u64,
1368 "Restarting extension process after transport failure",
1369 );
1370
1371 if !delay.is_zero() {
1372 tokio::time::sleep(delay).await;
1373 }
1374
1375 *state = Some(
1376 Self::spawn_state(
1377 &self.id,
1378 &self.command,
1379 &self.args,
1380 self.cwd.as_ref(),
1381 self.inbox.clone(),
1382 )
1383 .await?,
1384 );
1385 self.inbox
1387 .closed
1388 .store(false, std::sync::atomic::Ordering::Release);
1389 self.initialize_locked(state).await?;
1390 Ok(())
1397 }
1398
1399 async fn initialize_locked(&self, state: &mut Option<ProcessState>) -> Result<(), String> {
1400 let params = InitializeParams {
1401 synaps_version: env!("CARGO_PKG_VERSION"),
1402 extension_protocol_version: CURRENT_EXTENSION_PROTOCOL_VERSION,
1403 plugin_id: self.id.clone(),
1404 plugin_root: self
1405 .cwd
1406 .clone()
1407 .map(|path| path.to_string_lossy().to_string()),
1408 config: Value::Object(Default::default()),
1409 };
1410 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1411 let value = tokio::time::timeout(
1412 std::time::Duration::from_secs(10),
1413 self.call_once_locked(
1414 state.as_mut().expect("state should exist for initialize"),
1415 "initialize",
1416 serde_json::to_value(params).map_err(|e| e.to_string())?,
1417 id,
1418 ),
1419 )
1420 .await
1421 .map_err(|_| format!("Extension '{}' initialize timed out after 10s", self.id))??;
1422 Self::parse_initialize_result(&self.id, value).map(|_| ())
1423 }
1424
1425 async fn call_once_locked(
1429 &self,
1430 state: &mut ProcessState,
1431 method: &str,
1432 params: Value,
1433 id: u64,
1434 ) -> Result<Value, String> {
1435 let body = serde_json::to_string(&JsonRpcRequest {
1436 jsonrpc: "2.0",
1437 method: method.to_string(),
1438 params,
1439 id,
1440 })
1441 .map_err(|e| format!("Serialize error: {}", e))?;
1442
1443 let (tx, rx) = oneshot::channel::<Result<Value, String>>();
1444 if self.inbox.closed.load(std::sync::atomic::Ordering::Acquire) {
1447 return Err("transport closed: inbox is shut down".to_string());
1448 }
1449
1450 self.inbox.pending.lock().await.insert(id, tx);
1453
1454 if self.inbox.closed.load(std::sync::atomic::Ordering::Acquire) {
1457 self.inbox.pending.lock().await.remove(&id);
1458 return Err("transport closed: inbox shut down during registration".to_string());
1459 }
1460
1461 let frame = format!("Content-Length: {}\r\n\r\n{}", body.len(), body);
1462 let write_result = {
1463 let mut stdin = state.stdin.lock().await;
1464 match stdin.write_all(frame.as_bytes()).await {
1465 Ok(()) => stdin.flush().await,
1466 Err(e) => Err(e),
1467 }
1468 };
1469 if let Err(e) = write_result {
1470 self.inbox.pending.lock().await.remove(&id);
1472 return Err(format!("Write error: {}", e));
1473 }
1474
1475 match rx.await {
1476 Ok(payload) => payload,
1477 Err(_) => {
1478 self.inbox.pending.lock().await.remove(&id);
1483 Err("transport closed: response channel dropped".to_string())
1484 }
1485 }
1486 }
1487
1488 async fn call_no_restart(&self, method: &str, params: Value) -> Result<Value, String> {
1489 let _call_guard = self.call_lock.lock().await;
1490 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1491 let mut state_guard = self.state.lock().await;
1492 if state_guard.is_none() {
1493 *state_guard = Some(
1494 Self::spawn_state(
1495 &self.id,
1496 &self.command,
1497 &self.args,
1498 self.cwd.as_ref(),
1499 self.inbox.clone(),
1500 )
1501 .await?,
1502 );
1503 }
1504 self.call_once_locked(
1505 state_guard.as_mut().expect("state should exist"),
1506 method,
1507 params,
1508 id,
1509 )
1510 .await
1511 }
1512
1513 async fn call(&self, method: &str, params: Value) -> Result<Value, String> {
1514 let timeout_secs = if method == "tool.call" { 120 } else { 30 };
1515 let id_str = self.id.clone();
1516 let method_str = method.to_string();
1517
1518 let result = tokio::time::timeout(
1519 std::time::Duration::from_secs(timeout_secs),
1520 self.call_inner(method, params),
1521 )
1522 .await;
1523
1524 match result {
1525 Ok(inner) => inner,
1526 Err(_) => Err(format!(
1527 "Extension '{}' method '{}' timed out after {}s",
1528 id_str, method_str, timeout_secs
1529 )),
1530 }
1531 }
1532
1533 async fn call_inner(&self, method: &str, params: Value) -> Result<Value, String> {
1534 let _call_guard = self.call_lock.lock().await;
1535 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1536 let mut state_guard = self.state.lock().await;
1537 if state_guard.is_none() {
1538 self.restart_locked(&mut state_guard).await?;
1539 }
1540
1541 let result = self
1542 .call_once_locked(
1543 state_guard
1544 .as_mut()
1545 .expect("state should exist after restart"),
1546 method,
1547 params.clone(),
1548 id,
1549 )
1550 .await;
1551
1552 match result {
1553 Ok(value) => {
1554 self.restart_count.store(0, Ordering::Relaxed);
1558 Ok(value)
1559 }
1560 Err(first_error) => {
1561 self.restart_locked(&mut state_guard).await?;
1562 let retry_id = self.next_id.fetch_add(1, Ordering::Relaxed);
1563 self.call_once_locked(
1564 state_guard
1565 .as_mut()
1566 .expect("state should exist after restart"),
1567 method,
1568 params,
1569 retry_id,
1570 )
1571 .await
1572 .inspect(|_| {
1573 self.restart_count.store(0, Ordering::Relaxed);
1575 })
1576 .map_err(|retry_error| {
1577 format!(
1578 "{}; retry after restart failed: {}",
1579 first_error, retry_error
1580 )
1581 })
1582 }
1583 }
1584 }
1585
1586 #[doc(hidden)]
1604 pub async fn subscribe_notifications(
1605 &self,
1606 ) -> (usize, mpsc::UnboundedReceiver<NotificationFrame>) {
1607 let (tx, rx) = mpsc::unbounded_channel();
1608 let id = self
1609 .inbox
1610 .next_sink_id
1611 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1612 let mut sinks = self.inbox.notification_sinks.lock().await;
1613 sinks.retain(|(_, tx)| !tx.is_closed());
1617 sinks.push((id, tx));
1618 (id, rx)
1619 }
1620
1621 #[doc(hidden)]
1626 pub async fn unsubscribe_notifications(&self, id: usize) {
1627 let mut sinks = self.inbox.notification_sinks.lock().await;
1628 sinks.retain(|(sub_id, tx)| *sub_id != id && !tx.is_closed());
1629 }
1630
1631 pub(crate) fn forward_invoke_command_frame(
1641 extension_id: &str,
1642 request_id: &str,
1643 sink: &mpsc::UnboundedSender<crate::extensions::runtime::InvokeCommandEvent>,
1644 sink_open: &mut bool,
1645 frame: NotificationFrame,
1646 ) -> bool {
1647 use crate::extensions::commands::parse_command_output;
1648 use crate::extensions::runtime::InvokeCommandEvent;
1649 use crate::extensions::tasks::{is_task_method, parse_task_event};
1650
1651 let mut saw_done = false;
1652 if frame.method == "command.output" {
1653 match parse_command_output(&frame.params) {
1654 Ok(parsed) if parsed.request_id == request_id => {
1655 if matches!(
1656 parsed.event,
1657 crate::extensions::commands::CommandOutputEvent::Done
1658 ) {
1659 saw_done = true;
1660 }
1661 if *sink_open && sink.send(InvokeCommandEvent::Output(parsed.event)).is_err() {
1662 *sink_open = false;
1663 }
1664 }
1665 Ok(_) => {
1666 tracing::trace!(
1668 extension = %extension_id,
1669 "Ignoring command.output for unrelated request_id",
1670 );
1671 }
1672 Err(error) => {
1673 tracing::warn!(
1674 extension = %extension_id,
1675 error = %error,
1676 params = %frame.params,
1677 "Skipping malformed command.output notification",
1678 );
1679 }
1680 }
1681 } else if is_task_method(&frame.method) {
1682 match parse_task_event(&frame.method, &frame.params) {
1683 Ok(event) => {
1684 if *sink_open && sink.send(InvokeCommandEvent::Task(event)).is_err() {
1685 *sink_open = false;
1686 }
1687 }
1688 Err(error) => {
1689 tracing::warn!(
1690 extension = %extension_id,
1691 method = %frame.method,
1692 error = %error,
1693 params = %frame.params,
1694 "Skipping malformed task notification",
1695 );
1696 }
1697 }
1698 } else {
1699 tracing::trace!(
1700 extension = %extension_id,
1701 method = %frame.method,
1702 "Ignoring non-command/task notification during command.invoke",
1703 );
1704 }
1705 saw_done
1706 }
1707
1708 fn forward_provider_stream_frame(
1715 extension_id: &str,
1716 sink: &mpsc::UnboundedSender<ProviderStreamEvent>,
1717 sink_open: &mut bool,
1718 frame: NotificationFrame,
1719 ) {
1720 if frame.method != "provider.stream.event" {
1721 tracing::trace!(
1722 extension = %extension_id,
1723 method = %frame.method,
1724 "Ignoring non-stream notification during provider.stream",
1725 );
1726 return;
1727 }
1728 match parse_provider_stream_event(&frame.params) {
1729 Ok(event) => {
1730 if *sink_open && sink.send(event).is_err() {
1731 *sink_open = false;
1732 }
1733 }
1734 Err(error) => {
1735 tracing::warn!(
1736 extension = %extension_id,
1737 error = %error,
1738 params = %frame.params,
1739 "Skipping malformed provider.stream.event notification",
1740 );
1741 }
1742 }
1743 }
1744}
1745
1746#[async_trait]
1747impl ExtensionHandler for ProcessExtension {
1748 fn id(&self) -> &str {
1749 &self.id
1750 }
1751
1752 async fn call_tool(&self, name: &str, input: Value) -> Result<Value, String> {
1753 self.call(
1754 "tool.call",
1755 serde_json::json!({
1756 "name": name,
1757 "input": input,
1758 }),
1759 )
1760 .await
1761 }
1762
1763 async fn provider_complete(
1764 &self,
1765 params: ProviderCompleteParams,
1766 ) -> Result<ProviderCompleteResult, String> {
1767 let value = tokio::time::timeout(
1768 std::time::Duration::from_secs(60),
1769 self.call(
1770 "provider.complete",
1771 serde_json::to_value(params).map_err(|e| e.to_string())?,
1772 ),
1773 )
1774 .await
1775 .map_err(|_| format!("Extension '{}' provider.complete timed out", self.id))??;
1776 let result: ProviderCompleteResult = serde_json::from_value(value).map_err(|e| {
1777 format!(
1778 "Invalid provider.complete response from extension '{}': {}",
1779 self.id, e
1780 )
1781 })?;
1782 if result.content.is_empty() {
1783 return Err(format!(
1784 "Extension '{}' provider.complete returned empty content",
1785 self.id
1786 ));
1787 }
1788 Ok(result)
1789 }
1790
1791 async fn provider_stream(
1792 &self,
1793 params: ProviderCompleteParams,
1794 sink: tokio::sync::mpsc::UnboundedSender<ProviderStreamEvent>,
1795 ) -> Result<ProviderCompleteResult, String> {
1796 let (sub_id, mut rx) = self.subscribe_notifications().await;
1799 let params_value = serde_json::to_value(params).map_err(|e| e.to_string())?;
1800
1801 let extension_id = self.id.clone();
1802 let stream_future = async {
1803 let mut call_fut = Box::pin(self.call("provider.stream", params_value));
1804 let mut sink_open = true;
1805 let response = loop {
1806 tokio::select! {
1807 response = &mut call_fut => break response,
1808 Some(frame) = rx.recv() => {
1809 Self::forward_provider_stream_frame(
1810 &extension_id, &sink, &mut sink_open, frame,
1811 );
1812 }
1813 }
1814 };
1815 self.unsubscribe_notifications(sub_id).await;
1820 while let Some(frame) = rx.recv().await {
1821 Self::forward_provider_stream_frame(&extension_id, &sink, &mut sink_open, frame);
1822 }
1823 response
1824 };
1825
1826 let outcome = tokio::time::timeout(std::time::Duration::from_secs(60), stream_future).await;
1827
1828 self.unsubscribe_notifications(sub_id).await;
1831
1832 let value =
1833 outcome.map_err(|_| format!("Extension '{}' provider.stream timed out", self.id))??;
1834
1835 let result: ProviderCompleteResult = serde_json::from_value(value).map_err(|e| {
1836 format!(
1837 "Invalid provider.stream response from extension '{}': {}",
1838 self.id, e
1839 )
1840 })?;
1841 Ok(result)
1844 }
1845
1846 async fn invoke_command(
1847 &self,
1848 command: &str,
1849 args: Vec<String>,
1850 request_id: &str,
1851 sink: tokio::sync::mpsc::UnboundedSender<crate::extensions::runtime::InvokeCommandEvent>,
1852 ) -> Result<Value, String> {
1853 let (sub_id, mut rx) = self.subscribe_notifications().await;
1855 let params = serde_json::json!({
1856 "command": command,
1857 "args": args,
1858 "request_id": request_id,
1859 });
1860
1861 let extension_id = self.id.clone();
1862 let request_id_owned = request_id.to_string();
1863 let invoke_future = async {
1864 let mut call_fut = Box::pin(self.call("command.invoke", params));
1865 let mut sink_open = true;
1866 let response = loop {
1867 tokio::select! {
1868 response = &mut call_fut => break response,
1869 Some(frame) = rx.recv() => {
1870 let _ = Self::forward_invoke_command_frame(
1871 &extension_id, &request_id_owned, &sink, &mut sink_open, frame,
1872 );
1873 }
1874 }
1875 };
1876 self.unsubscribe_notifications(sub_id).await;
1880 while let Ok(frame) = rx.try_recv() {
1881 let _ = Self::forward_invoke_command_frame(
1882 &extension_id,
1883 &request_id_owned,
1884 &sink,
1885 &mut sink_open,
1886 frame,
1887 );
1888 }
1889 response
1890 };
1891
1892 let outcome =
1893 tokio::time::timeout(std::time::Duration::from_secs(120), invoke_future).await;
1894
1895 self.unsubscribe_notifications(sub_id).await;
1898
1899 outcome.map_err(|_| format!("Extension '{}' command.invoke timed out", self.id))?
1900 }
1901
1902 async fn handle(&self, event: &HookEvent) -> HookResult {
1903 let params = serde_json::to_value(event).unwrap_or(Value::Null);
1904 match tokio::time::timeout(
1905 std::time::Duration::from_secs(5),
1906 self.call("hook.handle", params),
1907 )
1908 .await
1909 {
1910 Ok(Ok(value)) => match serde_json::from_value(value.clone()) {
1911 Ok(result) => result,
1912 Err(error) => {
1913 tracing::warn!(
1914 extension = %self.id,
1915 error = %error,
1916 response = %value,
1917 "Extension hook handler returned invalid result",
1918 );
1919 match value.get("action").and_then(Value::as_str) {
1920 Some("modify") => HookResult::Block {
1923 reason: "Extension returned malformed modify result".to_string(),
1924 },
1925 Some("replace") => HookResult::Continue,
1930 _ => HookResult::Continue,
1931 }
1932 }
1933 },
1934 Ok(Err(e)) => {
1935 tracing::warn!(
1936 extension = %self.id,
1937 error = %e,
1938 "Extension hook handler failed — continuing",
1939 );
1940 HookResult::Continue
1941 }
1942 Err(_) => {
1943 tracing::warn!(
1944 extension = %self.id,
1945 timeout_secs = 5,
1946 "Extension hook handler timed out — continuing",
1947 );
1948 HookResult::Continue
1949 }
1950 }
1951 }
1952
1953 async fn get_info(&self) -> Result<crate::extensions::info::PluginInfo, String> {
1954 let value = tokio::time::timeout(
1955 std::time::Duration::from_secs(5),
1956 self.call("info.get", Value::Null),
1957 )
1958 .await
1959 .map_err(|_| format!("Extension '{}' info.get timed out", self.id))??;
1960 serde_json::from_value(value).map_err(|e| {
1961 format!(
1962 "Invalid info.get response from extension '{}': {}",
1963 self.id, e
1964 )
1965 })
1966 }
1967
1968 async fn sidecar_spawn_args(&self) -> Result<crate::sidecar::spawn::SidecarSpawnArgs, String> {
1969 let value = tokio::time::timeout(
1970 std::time::Duration::from_secs(5),
1971 self.call("sidecar.spawn_args", Value::Null),
1972 )
1973 .await
1974 .map_err(|_| format!("Extension '{}' sidecar.spawn_args timed out", self.id))??;
1975 serde_json::from_value(value).map_err(|e| {
1976 format!(
1977 "Invalid sidecar.spawn_args response from extension '{}': {}",
1978 self.id, e
1979 )
1980 })
1981 }
1982
1983 async fn settings_editor_open(&self, category: &str, field: &str) -> Result<Value, String> {
1984 let params = crate::extensions::settings_editor::SettingsEditorOpenParams {
1985 category: category.to_string(),
1986 field: field.to_string(),
1987 };
1988 tokio::time::timeout(
1989 std::time::Duration::from_secs(5),
1990 self.call(
1991 "settings.editor.open",
1992 serde_json::to_value(params).map_err(|e| e.to_string())?,
1993 ),
1994 )
1995 .await
1996 .map_err(|_| format!("Extension '{}' settings.editor.open timed out", self.id))?
1997 }
1998
1999 async fn settings_editor_key(
2000 &self,
2001 category: &str,
2002 field: &str,
2003 key: &str,
2004 ) -> Result<Value, String> {
2005 let mut params = serde_json::to_value(
2006 crate::extensions::settings_editor::SettingsEditorKeyParams {
2007 key: key.to_string(),
2008 },
2009 )
2010 .map_err(|e| e.to_string())?;
2011 if let Some(obj) = params.as_object_mut() {
2012 obj.insert("category".to_string(), Value::String(category.to_string()));
2013 obj.insert("field".to_string(), Value::String(field.to_string()));
2014 }
2015 tokio::time::timeout(
2016 std::time::Duration::from_secs(5),
2017 self.call("settings.editor.key", params),
2018 )
2019 .await
2020 .map_err(|_| format!("Extension '{}' settings.editor.key timed out", self.id))?
2021 }
2022
2023 async fn settings_editor_commit(
2024 &self,
2025 category: &str,
2026 field: &str,
2027 value: Value,
2028 ) -> Result<Value, String> {
2029 let params = serde_json::json!({
2030 "category": category,
2031 "field": field,
2032 "value": value,
2033 });
2034 tokio::time::timeout(
2035 std::time::Duration::from_secs(5),
2036 self.call("settings.editor.commit", params),
2037 )
2038 .await
2039 .map_err(|_| format!("Extension '{}' settings.editor.commit timed out", self.id))?
2040 }
2041
2042 async fn shutdown(&self) {
2043 let _ = tokio::time::timeout(
2044 std::time::Duration::from_millis(500),
2045 self.call("shutdown", Value::Null),
2046 )
2047 .await;
2048
2049 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2050 let mut state_guard = self.state.lock().await;
2051 if let Some(state) = state_guard.take() {
2052 state.reader_handle.abort();
2053 let mut child = state.child;
2054 let _ = child.kill().await;
2055 }
2056 self.inbox.notification_sinks.lock().await.clear();
2058 self.inbox
2059 .fail_all_pending("transport closed: extension shutdown")
2060 .await;
2061 }
2062
2063 async fn subscribe_notifications(
2064 &self,
2065 ) -> (
2066 usize,
2067 tokio::sync::mpsc::UnboundedReceiver<NotificationFrame>,
2068 ) {
2069 ProcessExtension::subscribe_notifications(self).await
2070 }
2071
2072 async fn restart_count(&self) -> usize {
2073 self.restart_count()
2074 }
2075
2076 async fn health(&self) -> ExtensionHealth {
2077 let consecutive = self.restart_count.load(Ordering::Relaxed);
2081 let lifetime = self.total_restarts.load(Ordering::Relaxed);
2082 let max = self.restart_policy.max_attempts as usize;
2083 if consecutive >= max {
2084 ExtensionHealth::Failed
2085 } else if lifetime > 0 {
2086 let state_alive = self.state.try_lock().map(|g| g.is_some()).unwrap_or(true);
2090 if state_alive {
2091 ExtensionHealth::Degraded
2092 } else {
2093 ExtensionHealth::Restarting
2094 }
2095 } else {
2096 ExtensionHealth::Running
2097 }
2098 }
2099}
2100
2101#[cfg(test)]
2102mod stream_event_tests {
2103 use super::*;
2104 use serde_json::json;
2105
2106 #[test]
2107 fn parses_text_delta_with_delta_key() {
2108 let v = json!({"type": "text", "delta": "hi"});
2109 assert_eq!(
2110 parse_provider_stream_event(&v).unwrap(),
2111 ProviderStreamEvent::TextDelta { text: "hi".into() }
2112 );
2113 }
2114
2115 #[test]
2116 fn parses_text_delta_with_text_key() {
2117 let v = json!({"type": "text", "text": "hi"});
2118 assert_eq!(
2119 parse_provider_stream_event(&v).unwrap(),
2120 ProviderStreamEvent::TextDelta { text: "hi".into() }
2121 );
2122 }
2123
2124 #[test]
2125 fn parses_thinking_delta() {
2126 let v = json!({"type": "thinking", "delta": "hmm"});
2127 assert_eq!(
2128 parse_provider_stream_event(&v).unwrap(),
2129 ProviderStreamEvent::ThinkingDelta { text: "hmm".into() }
2130 );
2131 let v2 = json!({"type": "thinking", "text": "hmm"});
2132 assert_eq!(
2133 parse_provider_stream_event(&v2).unwrap(),
2134 ProviderStreamEvent::ThinkingDelta { text: "hmm".into() }
2135 );
2136 }
2137
2138 #[test]
2139 fn parses_tool_use() {
2140 let v = json!({
2141 "type": "tool_use",
2142 "id": "t1",
2143 "name": "echo",
2144 "input": {"x": 1}
2145 });
2146 assert_eq!(
2147 parse_provider_stream_event(&v).unwrap(),
2148 ProviderStreamEvent::ToolUse {
2149 id: "t1".into(),
2150 name: "echo".into(),
2151 input: json!({"x": 1}),
2152 }
2153 );
2154 }
2155
2156 #[test]
2157 fn tool_use_input_defaults_to_empty_object() {
2158 let v = json!({"type": "tool_use", "id": "t1", "name": "echo"});
2159 assert_eq!(
2160 parse_provider_stream_event(&v).unwrap(),
2161 ProviderStreamEvent::ToolUse {
2162 id: "t1".into(),
2163 name: "echo".into(),
2164 input: json!({}),
2165 }
2166 );
2167 }
2168
2169 #[test]
2170 fn parses_usage_strips_type() {
2171 let v = json!({"type": "usage", "input_tokens": 5, "output_tokens": 7});
2172 assert_eq!(
2173 parse_provider_stream_event(&v).unwrap(),
2174 ProviderStreamEvent::Usage {
2175 usage: json!({"input_tokens": 5, "output_tokens": 7})
2176 }
2177 );
2178 }
2179
2180 #[test]
2181 fn parses_error() {
2182 let v = json!({"type": "error", "message": "boom"});
2183 assert_eq!(
2184 parse_provider_stream_event(&v).unwrap(),
2185 ProviderStreamEvent::Error {
2186 message: "boom".into()
2187 }
2188 );
2189 }
2190
2191 #[test]
2192 fn parses_done() {
2193 let v = json!({"type": "done"});
2194 assert_eq!(
2195 parse_provider_stream_event(&v).unwrap(),
2196 ProviderStreamEvent::Done
2197 );
2198 }
2199
2200 #[test]
2201 fn nested_event_shape_matches_flat() {
2202 let flat = json!({"type": "text", "delta": "hi"});
2203 let nested = json!({"event": {"type": "text", "delta": "hi"}});
2204 assert_eq!(
2205 parse_provider_stream_event(&flat).unwrap(),
2206 parse_provider_stream_event(&nested).unwrap()
2207 );
2208 }
2209
2210 #[test]
2211 fn missing_type_errors() {
2212 let v = json!({"delta": "hi"});
2213 let err = parse_provider_stream_event(&v).unwrap_err();
2214 assert!(err.contains("missing type"), "got: {err}");
2215 }
2216
2217 #[test]
2218 fn unknown_type_errors_with_type() {
2219 let v = json!({"type": "wat"});
2220 let err = parse_provider_stream_event(&v).unwrap_err();
2221 assert!(err.contains("wat"), "got: {err}");
2222 }
2223
2224 #[test]
2225 fn tool_use_missing_id_errors() {
2226 let v = json!({"type": "tool_use", "name": "echo"});
2227 let err = parse_provider_stream_event(&v).unwrap_err();
2228 assert!(err.contains("id"), "got: {err}");
2229 }
2230
2231 #[test]
2232 fn tool_use_missing_name_errors() {
2233 let v = json!({"type": "tool_use", "id": "t1"});
2234 let err = parse_provider_stream_event(&v).unwrap_err();
2235 assert!(err.contains("name"), "got: {err}");
2236 }
2237
2238 #[test]
2239 fn tool_use_empty_id_errors() {
2240 let v = json!({"type": "tool_use", "id": "", "name": "echo"});
2241 assert!(parse_provider_stream_event(&v).is_err());
2242 }
2243
2244 #[test]
2245 fn tool_use_empty_name_errors() {
2246 let v = json!({"type": "tool_use", "id": "t1", "name": ""});
2247 assert!(parse_provider_stream_event(&v).is_err());
2248 }
2249
2250 #[test]
2251 fn tool_use_non_object_input_errors() {
2252 let v = json!({"type": "tool_use", "id": "t1", "name": "echo", "input": "nope"});
2253 let err = parse_provider_stream_event(&v).unwrap_err();
2254 assert!(err.contains("input"), "got: {err}");
2255 }
2256
2257 #[test]
2258 fn text_missing_delta_and_text_errors() {
2259 let v = json!({"type": "text"});
2260 let err = parse_provider_stream_event(&v).unwrap_err();
2261 assert!(err.contains("delta") || err.contains("text"), "got: {err}");
2262 }
2263
2264 #[test]
2265 fn error_missing_message_errors() {
2266 let v = json!({"type": "error"});
2267 assert!(parse_provider_stream_event(&v).is_err());
2268 }
2269
2270 #[test]
2271 fn error_empty_message_errors() {
2272 let v = json!({"type": "error", "message": ""});
2273 assert!(parse_provider_stream_event(&v).is_err());
2274 }
2275}
2276
2277#[cfg(test)]
2278mod restart_policy_tests {
2279 use super::*;
2280
2281 #[tokio::test]
2282 async fn restart_policy_default_max_attempts_is_3() {
2283 let ext = ProcessExtension::spawn("policy-test", "/bin/cat", &[])
2288 .await
2289 .expect("spawn /bin/cat");
2290 assert_eq!(ext.restart_policy.max_attempts, 3);
2291 ext.shutdown().await;
2292 }
2293
2294 #[tokio::test]
2295 async fn with_restart_policy_overrides_default() {
2296 let ext = ProcessExtension::spawn("policy-test-override", "/bin/cat", &[])
2297 .await
2298 .expect("spawn /bin/cat");
2299 let custom = RestartPolicy {
2300 max_attempts: 7,
2301 ..RestartPolicy::default()
2302 };
2303 let ext = ext.with_restart_policy(custom);
2304 assert_eq!(ext.restart_policy.max_attempts, 7);
2305 ext.shutdown().await;
2306 }
2307}
2308
2309#[cfg(test)]
2310mod capture_validator_tests {
2311 use super::*;
2312 use crate::extensions::permissions::{Permission, PermissionSet};
2313
2314 fn perms_with(grants: &[Permission]) -> PermissionSet {
2315 let mut p = PermissionSet::new();
2316 for g in grants {
2317 p.grant(*g);
2318 }
2319 p
2320 }
2321
2322 fn cap(kind: &str, name: &str, perms: &[&str]) -> CapabilityDeclaration {
2323 CapabilityDeclaration {
2324 kind: kind.to_string(),
2325 name: name.to_string(),
2326 permissions: perms.iter().map(|p| p.to_string()).collect(),
2327 params: serde_json::Value::Null,
2328 }
2329 }
2330
2331 #[test]
2332 fn capability_validator_rejects_empty_kind() {
2333 let d = cap(" ", "Sample", &["audio.input"]);
2334 let perms = perms_with(&[Permission::AudioInput]);
2335 let err = validate_capability(&d, &perms).unwrap_err();
2336 assert!(err.contains("kind"), "got: {}", err);
2337 }
2338
2339 #[test]
2340 fn capability_validator_rejects_empty_name() {
2341 let d = cap("capture", " ", &["audio.input"]);
2342 let perms = perms_with(&[Permission::AudioInput]);
2343 let err = validate_capability(&d, &perms).unwrap_err();
2344 assert!(err.contains("name"), "got: {}", err);
2345 }
2346
2347 #[test]
2348 fn capability_validator_rejects_unknown_permission_string() {
2349 let d = cap("capture", "Sample", &["audio.telepathy"]);
2350 let perms = perms_with(&[Permission::AudioInput, Permission::AudioOutput]);
2351 let err = validate_capability(&d, &perms).unwrap_err();
2352 assert!(
2353 err.contains("unknown permission") && err.contains("audio.telepathy"),
2354 "got: {}",
2355 err,
2356 );
2357 }
2358
2359 #[test]
2360 fn capability_validator_requires_every_declared_permission() {
2361 let d = cap("capture", "Sample", &["audio.input"]);
2362 let perms = perms_with(&[]);
2363 let err = validate_capability(&d, &perms).unwrap_err();
2364 assert!(
2365 err.contains("audio.input") && err.contains("not granted"),
2366 "got: {}",
2367 err,
2368 );
2369 }
2370
2371 #[test]
2372 fn capability_validator_accepts_when_all_permissions_granted() {
2373 let d = cap("capture", "Sample", &["audio.input", "audio.output"]);
2374 let perms = perms_with(&[Permission::AudioInput, Permission::AudioOutput]);
2375 validate_capability(&d, &perms).expect("should validate");
2376 }
2377
2378 #[test]
2379 fn capability_validator_accepts_no_permissions() {
2380 let d = cap("ocr", "Tesseract", &[]);
2384 let perms = perms_with(&[]);
2385 validate_capability(&d, &perms).expect("should validate");
2386 }
2387
2388 #[test]
2389 fn capability_validator_does_not_branch_on_kind() {
2390 let perms = perms_with(&[Permission::AudioInput]);
2394 for kind in ["capture", "ocr", "agent", "foot_pedal", "eeg"] {
2395 let d = cap(kind, "Anything", &["audio.input"]);
2396 validate_capability(&d, &perms).expect("should validate");
2397 }
2398 }
2399}
2400
2401#[cfg(test)]
2402mod invoke_command_dispatch_tests {
2403 use super::*;
2408 use crate::extensions::commands::CommandOutputEvent;
2409 use crate::extensions::runtime::InvokeCommandEvent;
2410 use crate::extensions::tasks::{TaskEvent, TaskKind};
2411 use serde_json::json;
2412 use tokio::sync::mpsc;
2413
2414 fn frame(method: &str, params: serde_json::Value) -> NotificationFrame {
2415 NotificationFrame {
2416 method: method.to_string(),
2417 params,
2418 }
2419 }
2420
2421 #[test]
2422 fn forwards_mixed_event_stream_in_order() {
2423 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2424 let mut open = true;
2425 let frames = vec![
2426 frame(
2427 "command.output",
2428 json!({"request_id":"r1","event":{"kind":"text","content":"A"}}),
2429 ),
2430 frame(
2431 "task.start",
2432 json!({"id":"dl","label":"Downloading","kind":"download"}),
2433 ),
2434 frame("task.update", json!({"id":"dl","current":50,"total":100})),
2435 frame(
2436 "command.output",
2437 json!({"request_id":"r1","event":{"kind":"system","content":"working"}}),
2438 ),
2439 frame("task.done", json!({"id":"dl"})),
2440 frame(
2441 "command.output",
2442 json!({"request_id":"r1","event":{"kind":"done"}}),
2443 ),
2444 ];
2445
2446 let mut saw_done = false;
2447 for f in frames {
2448 saw_done |=
2449 ProcessExtension::forward_invoke_command_frame("ext-test", "r1", &tx, &mut open, f);
2450 }
2451 drop(tx);
2452 assert!(saw_done, "should have observed the command Done marker");
2453
2454 let mut events = Vec::new();
2455 while let Ok(ev) = rx.try_recv() {
2456 events.push(ev);
2457 }
2458 assert_eq!(events.len(), 6);
2459 assert_eq!(
2460 events[0],
2461 InvokeCommandEvent::Output(CommandOutputEvent::Text {
2462 content: "A".into()
2463 })
2464 );
2465 assert!(matches!(
2466 events[1],
2467 InvokeCommandEvent::Task(TaskEvent::Start {
2468 kind: TaskKind::Download,
2469 ..
2470 })
2471 ));
2472 assert!(matches!(
2473 events[2],
2474 InvokeCommandEvent::Task(TaskEvent::Update { .. })
2475 ));
2476 assert!(matches!(
2477 events[3],
2478 InvokeCommandEvent::Output(CommandOutputEvent::System { .. })
2479 ));
2480 assert!(matches!(
2481 events[4],
2482 InvokeCommandEvent::Task(TaskEvent::Done { error: None, .. })
2483 ));
2484 assert_eq!(
2485 events[5],
2486 InvokeCommandEvent::Output(CommandOutputEvent::Done)
2487 );
2488 }
2489
2490 #[test]
2491 fn ignores_command_output_for_unrelated_request_id() {
2492 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2493 let mut open = true;
2494 ProcessExtension::forward_invoke_command_frame(
2495 "ext",
2496 "r1",
2497 &tx,
2498 &mut open,
2499 frame(
2500 "command.output",
2501 json!({"request_id":"other","event":{"kind":"text","content":"x"}}),
2502 ),
2503 );
2504 drop(tx);
2505 assert!(rx.try_recv().is_err());
2506 }
2507
2508 #[test]
2509 fn skips_malformed_command_output_without_aborting() {
2510 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2511 let mut open = true;
2512 ProcessExtension::forward_invoke_command_frame(
2514 "ext",
2515 "r1",
2516 &tx,
2517 &mut open,
2518 frame("command.output", json!({"request_id":"r1","event":{}})),
2519 );
2520 ProcessExtension::forward_invoke_command_frame(
2522 "ext",
2523 "r1",
2524 &tx,
2525 &mut open,
2526 frame(
2527 "command.output",
2528 json!({"request_id":"r1","event":{"kind":"done"}}),
2529 ),
2530 );
2531 drop(tx);
2532 let ev = rx.try_recv().unwrap();
2533 assert_eq!(ev, InvokeCommandEvent::Output(CommandOutputEvent::Done));
2534 assert!(rx.try_recv().is_err());
2535 }
2536
2537 #[test]
2538 fn task_events_pass_through_regardless_of_request_id() {
2539 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2540 let mut open = true;
2541 ProcessExtension::forward_invoke_command_frame(
2542 "ext",
2543 "r1",
2544 &tx,
2545 &mut open,
2546 frame("task.log", json!({"id":"abc","line":"..."})),
2547 );
2548 drop(tx);
2549 match rx.try_recv().unwrap() {
2550 InvokeCommandEvent::Task(TaskEvent::Log { id, line }) => {
2551 assert_eq!(id, "abc");
2552 assert_eq!(line, "...");
2553 }
2554 other => panic!("unexpected: {other:?}"),
2555 }
2556 }
2557
2558 #[test]
2559 fn unrelated_methods_are_dropped() {
2560 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2561 let mut open = true;
2562 ProcessExtension::forward_invoke_command_frame(
2563 "ext",
2564 "r1",
2565 &tx,
2566 &mut open,
2567 frame("provider.stream.event", json!({"type":"text","delta":"x"})),
2568 );
2569 drop(tx);
2570 assert!(rx.try_recv().is_err());
2571 }
2572}