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<crate::SharedMessage>,
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(std::sync::Arc::new(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(std::sync::Arc::new(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 initialize_config: Mutex<Value>,
581 total_restarts: AtomicUsize,
587 pub(crate) restart_policy: RestartPolicy,
589 inbox: Arc<Inbox>,
593}
594
595impl ProcessExtension {
596 pub async fn spawn(id: &str, command: &str, args: &[String]) -> Result<Self, String> {
597 Self::spawn_with_cwd(id, command, args, None).await
598 }
599
600 pub async fn spawn_with_cwd(
605 id: &str,
606 command: &str,
607 args: &[String],
608 cwd: Option<PathBuf>,
609 ) -> Result<Self, String> {
610 let inbox = Arc::new(Inbox::new(id.to_string()));
611 let state = Self::spawn_state(id, command, args, cwd.as_ref(), inbox.clone()).await?;
612 Ok(Self {
613 id: id.to_string(),
614 command: command.to_string(),
615 args: args.to_vec(),
616 cwd,
617 state: Arc::new(Mutex::new(Some(state))),
618 call_lock: Arc::new(Mutex::new(())),
619 next_id: AtomicU64::new(1),
620 restart_count: AtomicUsize::new(0),
621 initialize_config: Mutex::new(Value::Object(Default::default())),
622 total_restarts: AtomicUsize::new(0),
623 restart_policy: RestartPolicy::default(),
624 inbox,
625 })
626 }
627
628 pub fn with_restart_policy(mut self, policy: RestartPolicy) -> Self {
630 self.restart_policy = policy;
631 self
632 }
633
634 async fn spawn_state(
635 id: &str,
636 command: &str,
637 args: &[String],
638 cwd: Option<&PathBuf>,
639 inbox: Arc<Inbox>,
640 ) -> Result<ProcessState, String> {
641 let mut cmd = Command::new(command);
642 cmd.args(args)
643 .stdin(Stdio::piped())
644 .stdout(Stdio::piped())
645 .stderr(Stdio::piped());
646 if let Some(cwd) = cwd {
647 cmd.current_dir(cwd);
648 }
649
650 cmd.env_clear();
656 for var in &["PATH", "HOME", "LANG", "TERM", "XDG_RUNTIME_DIR"] {
657 if let Ok(val) = std::env::var(var) {
658 cmd.env(var, val);
659 }
660 }
661
662 cmd.kill_on_drop(true);
663
664 let mut child = cmd
665 .spawn()
666 .map_err(|e| format!("Failed to spawn extension '{}': {}", id, e))?;
667
668 let stdin = child
669 .stdin
670 .take()
671 .ok_or_else(|| format!("No stdin for extension '{}'", id))?;
672 let stdout = child
673 .stdout
674 .take()
675 .ok_or_else(|| format!("No stdout for extension '{}'", id))?;
676 if let Some(stderr) = child.stderr.take() {
677 let extension_id = id.to_string();
678 tokio::spawn(async move {
679 let mut lines = BufReader::new(stderr).lines();
680 loop {
681 match lines.next_line().await {
682 Ok(Some(line)) => {
683 tracing::debug!(extension = %extension_id, stderr = %line);
684 }
685 Ok(None) => break,
686 Err(error) => {
687 tracing::debug!(
688 extension = %extension_id,
689 error = %error,
690 "Failed to read extension stderr",
691 );
692 break;
693 }
694 }
695 }
696 });
697 }
698
699 let reader_handle = Self::spawn_reader(stdout, inbox.clone(), id.to_string());
700
701 let stdin_arc = Arc::new(Mutex::new(stdin));
702 *inbox.inbound_stdin.lock().await = Some(stdin_arc.clone());
705
706 Ok(ProcessState {
707 child,
708 stdin: stdin_arc,
709 reader_handle,
710 })
711 }
712
713 fn spawn_reader(
718 stdout: ChildStdout,
719 inbox: Arc<Inbox>,
720 extension_id: String,
721 ) -> JoinHandle<()> {
722 tokio::spawn(async move {
723 let mut reader = BufReader::new(stdout);
724 loop {
725 match Self::read_one_frame(&mut reader, &extension_id).await {
726 Ok(Some(value)) => {
727 Self::dispatch_frame(value, &inbox, &extension_id).await;
728 }
729 Ok(None) => {
730 tracing::debug!(
731 extension = %extension_id,
732 "Extension stdout closed (EOF); failing pending requests",
733 );
734 inbox.fail_all_pending("transport closed: EOF").await;
735 inbox.notification_sinks.lock().await.clear();
737 return;
738 }
739 Err(error) => {
740 tracing::debug!(
741 extension = %extension_id,
742 error = %error,
743 "Extension transport read error",
744 );
745 inbox
746 .fail_all_pending(&format!("transport error: {}", error))
747 .await;
748 inbox.notification_sinks.lock().await.clear();
749 return;
750 }
751 }
752 }
753 })
754 }
755
756 async fn read_one_frame(
760 reader: &mut BufReader<ChildStdout>,
761 extension_id: &str,
762 ) -> Result<Option<Value>, String> {
763 let mut content_length: Option<usize> = None;
764 let mut saw_any_header = false;
765 loop {
766 let mut header_line = String::new();
767 let n = reader
768 .read_line(&mut header_line)
769 .await
770 .map_err(|e| format!("Read header error: {}", e))?;
771 if n == 0 {
772 if saw_any_header {
773 return Err("Unexpected EOF while reading response headers".into());
774 }
775 return Ok(None);
776 }
777 saw_any_header = true;
778 if header_line.len() > 1024 {
779 return Err(format!(
780 "Extension '{}' header line too long ({} bytes)",
781 extension_id,
782 header_line.len()
783 ));
784 }
785 let trimmed = header_line.trim();
786 if trimmed.is_empty() {
787 break;
788 }
789 if let Some((name, value)) = trimmed.split_once(':') {
790 if name.trim().eq_ignore_ascii_case("Content-Length") {
791 content_length = Some(value.trim().parse().map_err(|_| {
792 format!("Invalid Content-Length value: {:?}", value.trim())
793 })?);
794 }
795 }
796 }
797 let content_length = content_length.ok_or_else(|| {
798 format!(
799 "Extension '{}' frame missing Content-Length header",
800 extension_id
801 )
802 })?;
803 const MAX_RESPONSE_SIZE: usize = 4 * 1024 * 1024;
804 if content_length > MAX_RESPONSE_SIZE {
805 return Err(format!(
806 "Extension '{}' frame too large: {} bytes (max {})",
807 extension_id, content_length, MAX_RESPONSE_SIZE
808 ));
809 }
810 let mut buf = vec![0u8; content_length];
811 tokio::io::AsyncReadExt::read_exact(reader, &mut buf)
812 .await
813 .map_err(|e| format!("Read body error: {}", e))?;
814 let value: Value =
815 serde_json::from_slice(&buf).map_err(|e| format!("Parse frame error: {}", e))?;
816 Ok(Some(value))
817 }
818
819 async fn dispatch_frame(value: Value, inbox: &Arc<Inbox>, extension_id: &str) {
825 let id_field = value.get("id");
826 let id_is_present = !matches!(id_field, None | Some(Value::Null));
827 let method_field = value
828 .get("method")
829 .and_then(Value::as_str)
830 .map(str::to_string);
831
832 if id_is_present && method_field.is_some() {
833 let id = match id_field.and_then(Value::as_u64) {
836 Some(id) => id,
837 None => {
838 tracing::trace!(
839 extension = %extension_id,
840 frame = %value,
841 "Discarding inbound request with non-numeric id",
842 );
843 return;
844 }
845 };
846 let Some(method) = method_field else { return };
847 let params = value.get("params").cloned().unwrap_or(Value::Null);
848 let inbox = inbox.clone();
849 let extension_id = extension_id.to_string();
850 tokio::spawn(async move {
851 let outcome = Self::handle_inbound_request(&inbox, &method, params).await;
852 let payload = match outcome {
853 Ok(result) => serde_json::json!({
854 "jsonrpc": "2.0",
855 "id": id,
856 "result": result,
857 }),
858 Err((code, message)) => serde_json::json!({
859 "jsonrpc": "2.0",
860 "id": id,
861 "error": {"code": code, "message": message},
862 }),
863 };
864 let stdin_handle = inbox.inbound_stdin.lock().await.clone();
865 if let Some(stdin) = stdin_handle {
866 let body = match serde_json::to_string(&payload) {
867 Ok(s) => s,
868 Err(error) => {
869 tracing::warn!(
870 extension = %extension_id,
871 error = %error,
872 "Failed to serialize inbound response",
873 );
874 return;
875 }
876 };
877 let frame = format!("Content-Length: {}\r\n\r\n{}", body.len(), body);
878 let mut stdin = stdin.lock().await;
879 if let Err(error) = stdin.write_all(frame.as_bytes()).await {
880 tracing::warn!(
881 extension = %extension_id,
882 error = %error,
883 "Failed to write inbound response",
884 );
885 return;
886 }
887 if let Err(error) = stdin.flush().await {
888 tracing::warn!(
889 extension = %extension_id,
890 error = %error,
891 "Failed to flush inbound response",
892 );
893 }
894 } else {
895 tracing::warn!(
896 extension = %extension_id,
897 "No stdin available to reply to inbound request",
898 );
899 }
900 });
901 return;
902 }
903
904 if id_is_present {
905 let id = match id_field.and_then(Value::as_u64) {
906 Some(id) => id,
907 None => {
908 tracing::trace!(
909 extension = %extension_id,
910 frame = %value,
911 "Discarding frame with non-numeric id",
912 );
913 return;
914 }
915 };
916 let sender = inbox.pending.lock().await.remove(&id);
917 match sender {
918 Some(tx) => {
919 let payload = if let Some(err) = value.get("error") {
920 let message = err
921 .get("message")
922 .and_then(Value::as_str)
923 .unwrap_or("unknown extension error")
924 .to_string();
925 Err(format!("Extension error: {}", message))
926 } else {
927 Ok(value.get("result").cloned().unwrap_or(Value::Null))
928 };
929 let _ = tx.send(payload);
930 }
931 None => {
932 tracing::trace!(
933 extension = %extension_id,
934 id = id,
935 "Response with unknown id (no pending request); dropping",
936 );
937 }
938 }
939 } else if let Some(method) = value.get("method").and_then(Value::as_str) {
940 let params = value.get("params").cloned().unwrap_or(Value::Null);
941 let frame = NotificationFrame {
942 method: method.to_string(),
943 params,
944 };
945 let mut sinks = inbox.notification_sinks.lock().await;
946 if sinks.is_empty() {
947 tracing::trace!(
948 extension = %extension_id,
949 method = %method,
950 "Notification with no active subscribers; dropping",
951 );
952 } else {
953 sinks.retain(|(_, tx)| tx.send(frame.clone()).is_ok());
957 }
958 } else {
959 tracing::trace!(
960 extension = %extension_id,
961 frame = %value,
962 "Unrecognized frame; dropping",
963 );
964 }
965 }
966
967 pub fn restart_count(&self) -> usize {
968 self.total_restarts.load(Ordering::Relaxed)
971 }
972
973 pub async fn set_permissions(&self, perms: crate::extensions::permissions::PermissionSet) {
976 *self.inbox.permissions.write().await = Some(perms);
977 }
978
979 #[allow(clippy::doc_lazy_continuation)]
987 async fn handle_inbound_request(
988 inbox: &Arc<Inbox>,
989 method: &str,
990 params: Value,
991 ) -> Result<Value, (i32, String)> {
992 use crate::extensions::permissions::Permission;
993 use crate::memory::store::{self, MemoryQuery};
994
995 match method {
996 "memory.append" => {
997 Self::require_permission(inbox, Permission::MemoryWrite, "memory.write").await?;
998 let namespace = Self::param_str(¶ms, "namespace")?;
999 Self::require_namespace_matches(inbox, &namespace).await?;
1000 let content = Self::param_str(¶ms, "content")?;
1001 let tags = match params.get("tags") {
1002 None | Some(Value::Null) => Vec::new(),
1003 Some(Value::Array(arr)) => {
1004 let mut out = Vec::with_capacity(arr.len());
1005 for v in arr {
1006 match v.as_str() {
1007 Some(s) => out.push(s.to_string()),
1008 None => {
1009 return Err((
1010 -32602,
1011 "tags must be an array of strings".to_string(),
1012 ))
1013 }
1014 }
1015 }
1016 out
1017 }
1018 _ => return Err((-32602, "tags must be an array of strings".to_string())),
1019 };
1020 let meta = match params.get("meta") {
1021 None | Some(Value::Null) => None,
1022 Some(v) => Some(v.clone()),
1023 };
1024 let record = store::new_record(namespace, content, tags, meta);
1025 let timestamp_ms = record.timestamp_ms;
1026 store::append(&record).map_err(|e| (-32000, e.to_string()))?;
1027 Ok(serde_json::json!({"ok": true, "timestamp_ms": timestamp_ms}))
1028 }
1029 "memory.query" => {
1030 Self::require_permission(inbox, Permission::MemoryRead, "memory.read").await?;
1031 let namespace = Self::param_str(¶ms, "namespace")?;
1032 Self::require_namespace_matches(inbox, &namespace).await?;
1033 let q = MemoryQuery {
1034 content_contains: params
1035 .get("content_contains")
1036 .and_then(Value::as_str)
1037 .map(str::to_string),
1038 tag_prefix: params
1039 .get("tag_prefix")
1040 .and_then(Value::as_str)
1041 .map(str::to_string),
1042 since_ms: params.get("since_ms").and_then(Value::as_u64),
1043 until_ms: params.get("until_ms").and_then(Value::as_u64),
1044 limit: params
1045 .get("limit")
1046 .and_then(Value::as_u64)
1047 .map(|n| n as usize),
1048 };
1049 let records = store::query(&namespace, &q).map_err(|e| (-32000, e.to_string()))?;
1050 Ok(serde_json::json!({"records": records}))
1051 }
1052 "config.get" => {
1053 let key = Self::param_str(¶ms, "key")?;
1054 Self::validate_config_key(&key)?;
1055 let value =
1056 crate::extensions::config_store::read_plugin_config(&inbox.extension_id, &key);
1057 Ok(serde_json::json!({"value": value}))
1058 }
1059 "config.set" => {
1060 Self::require_permission(inbox, Permission::ConfigWrite, "config.write").await?;
1061 let key = Self::param_str(¶ms, "key")?;
1062 Self::validate_config_key(&key)?;
1063 let value = Self::param_str(¶ms, "value")?;
1064 crate::extensions::config_store::write_plugin_config(
1065 &inbox.extension_id,
1066 &key,
1067 &value,
1068 )
1069 .map_err(|e| (-32000, e.to_string()))?;
1070 Ok(serde_json::json!({"ok": true}))
1071 }
1072 "config.subscribe" => {
1073 Self::require_permission(inbox, Permission::ConfigSubscribe, "config.subscribe")
1074 .await?;
1075 Ok(serde_json::json!({"ok": true}))
1079 }
1080 other => Err((-32601, format!("method not found: {other}"))),
1081 }
1082 }
1083
1084 async fn require_permission(
1085 inbox: &Arc<Inbox>,
1086 perm: crate::extensions::permissions::Permission,
1087 wire: &str,
1088 ) -> Result<(), (i32, String)> {
1089 let guard = inbox.permissions.read().await;
1090 match guard.as_ref() {
1091 Some(set) if set.has(perm) => Ok(()),
1092 _ => Err((-32602, format!("permission denied: {wire} required"))),
1093 }
1094 }
1095
1096 async fn require_namespace_matches(
1097 inbox: &Arc<Inbox>,
1098 namespace: &str,
1099 ) -> Result<(), (i32, String)> {
1100 if namespace == inbox.extension_id {
1101 Ok(())
1102 } else {
1103 Err((
1104 -32602,
1105 format!(
1106 "namespace must equal extension id '{}' (got '{}')",
1107 inbox.extension_id, namespace
1108 ),
1109 ))
1110 }
1111 }
1112
1113 fn param_str(params: &Value, name: &str) -> Result<String, (i32, String)> {
1114 params
1115 .get(name)
1116 .and_then(Value::as_str)
1117 .map(str::to_string)
1118 .ok_or_else(|| (-32602, format!("missing or invalid '{name}' parameter")))
1119 }
1120
1121 fn validate_config_key(key: &str) -> Result<(), (i32, String)> {
1122 let trimmed = key.trim();
1123 if trimmed.is_empty() {
1124 return Err((-32602, "config key must be non-empty".to_string()));
1125 }
1126 if trimmed.contains('.') || trimmed.contains('/') || trimmed.contains(' ') {
1127 return Err((
1128 -32602,
1129 "config key must not contain dots, slashes, or spaces".to_string(),
1130 ));
1131 }
1132 Ok(())
1133 }
1134
1135 pub async fn initialize(
1136 &self,
1137 plugin_root: Option<PathBuf>,
1138 config: Value,
1139 ) -> Result<InitializeCapabilitiesResult, String> {
1140 *self.initialize_config.lock().await = config.clone();
1143 let params = InitializeParams {
1144 synaps_version: env!("CARGO_PKG_VERSION"),
1145 extension_protocol_version: CURRENT_EXTENSION_PROTOCOL_VERSION,
1146 plugin_id: self.id.clone(),
1147 plugin_root: plugin_root
1148 .or_else(|| self.cwd.clone())
1149 .map(|path| path.to_string_lossy().to_string()),
1150 config,
1151 };
1152 let value = self
1153 .call_no_restart(
1154 "initialize",
1155 serde_json::to_value(params).map_err(|e| e.to_string())?,
1156 )
1157 .await?;
1158 Self::parse_initialize_result(&self.id, value)
1159 }
1160
1161 fn parse_initialize_result(
1162 id: &str,
1163 value: Value,
1164 ) -> Result<InitializeCapabilitiesResult, String> {
1165 let result: InitializeResult = serde_json::from_value(value)
1166 .map_err(|e| format!("Invalid initialize response from extension '{}': {}", id, e))?;
1167 if result.protocol_version != CURRENT_EXTENSION_PROTOCOL_VERSION {
1168 return Err(format!(
1169 "Extension '{}' initialize returned unsupported protocol_version {} (supported: {})",
1170 id, result.protocol_version, CURRENT_EXTENSION_PROTOCOL_VERSION,
1171 ));
1172 }
1173 Self::validate_registered_tool_specs(id, &result.capabilities.tools)?;
1174 Self::validate_registered_provider_specs(id, &result.capabilities.providers)?;
1175 Ok(InitializeCapabilitiesResult {
1176 tools: result.capabilities.tools,
1177 providers: result.capabilities.providers,
1178 capabilities: result.capabilities.capabilities,
1179 })
1180 }
1181
1182 fn validate_registered_tool_specs(
1183 id: &str,
1184 tools: &[RegisteredExtensionToolSpec],
1185 ) -> Result<(), String> {
1186 use crate::extensions::validation::{validate_id_segment, IdValidationError};
1187 let mut names = HashSet::new();
1188 for tool in tools {
1189 let name = tool.name.trim();
1190 if let Err(err) = validate_id_segment(name) {
1191 return Err(match err {
1192 IdValidationError::Empty => format!(
1193 "Extension '{}' registered a tool with an empty tool name",
1194 id
1195 ),
1196 IdValidationError::ContainsReserved { ch } => format!(
1197 "Extension '{}' registered tool '{}' with invalid tool name: '{}' is reserved",
1198 id, name, ch
1199 ),
1200 IdValidationError::TooLong { len, max } => format!(
1201 "Extension '{}' registered tool '{}' with invalid tool name: must be at most {} chars (got {})",
1202 id, name, max, len
1203 ),
1204 IdValidationError::ContainsWhitespace => format!(
1205 "Extension '{}' registered tool '{}' with invalid tool name: must not contain whitespace",
1206 id, name
1207 ),
1208 IdValidationError::ContainsControl { ch } => format!(
1209 "Extension '{}' registered tool '{}' with invalid tool name: contains control character U+{:04X}",
1210 id, name, ch as u32
1211 ),
1212 });
1213 }
1214 if !names.insert(name.to_string()) {
1215 return Err(format!(
1216 "Extension '{}' registered duplicate tool name '{}'",
1217 id, name
1218 ));
1219 }
1220 if tool.description.trim().is_empty() {
1221 return Err(format!(
1222 "Extension '{}' registered tool '{}' with an empty description",
1223 id, name,
1224 ));
1225 }
1226 if !tool.input_schema.is_object() {
1227 return Err(format!(
1228 "Extension '{}' registered tool '{}' with invalid input_schema: input_schema must be a JSON object",
1229 id, name,
1230 ));
1231 }
1232 }
1233 Ok(())
1234 }
1235
1236 fn validate_registered_provider_specs(
1237 id: &str,
1238 providers: &[RegisteredProviderSpec],
1239 ) -> Result<(), String> {
1240 use crate::extensions::validation::{validate_id_segment, IdValidationError};
1241 for provider in providers {
1242 let provider_id = provider.id.trim();
1243 match validate_id_segment(provider_id) {
1244 Ok(()) => {
1245 if !Self::is_safe_provider_id(provider_id) {
1246 return Err(format!(
1247 "Extension '{}' registered provider '{}' with invalid provider id",
1248 id, provider_id
1249 ));
1250 }
1251 }
1252 Err(IdValidationError::Empty) => {
1253 return Err(format!(
1254 "Extension '{}' registered provider with empty provider id",
1255 id
1256 ));
1257 }
1258 Err(err) => {
1259 return Err(format!(
1260 "Extension '{}' registered provider '{}' with invalid provider id: {}",
1261 id, provider_id, err
1262 ));
1263 }
1264 }
1265 if provider.display_name.trim().is_empty() {
1266 return Err(format!(
1267 "Extension '{}' registered provider '{}' with empty display_name",
1268 id, provider_id,
1269 ));
1270 }
1271 if provider.description.trim().is_empty() {
1272 return Err(format!(
1273 "Extension '{}' registered provider '{}' with empty description",
1274 id, provider_id,
1275 ));
1276 }
1277 if provider.models.is_empty() {
1278 return Err(format!(
1279 "Extension '{}' registered provider '{}' must declare at least one model",
1280 id, provider_id,
1281 ));
1282 }
1283 let mut model_ids = HashSet::new();
1284 for model in &provider.models {
1285 let model_id = model.id.trim();
1286 if let Err(err) = validate_id_segment(model_id) {
1287 return Err(match err {
1288 IdValidationError::Empty => format!(
1289 "Extension '{}' registered provider '{}' with empty model id",
1290 id, provider_id
1291 ),
1292 IdValidationError::ContainsReserved { ch } => format!(
1293 "Extension '{}' registered provider '{}' with invalid model id '{}': '{}' is reserved",
1294 id, provider_id, model_id, ch
1295 ),
1296 IdValidationError::TooLong { len, max } => format!(
1297 "Extension '{}' registered provider '{}' with invalid model id '{}': must be at most {} chars (got {})",
1298 id, provider_id, model_id, max, len
1299 ),
1300 IdValidationError::ContainsWhitespace => format!(
1301 "Extension '{}' registered provider '{}' with invalid model id '{}': must not contain whitespace",
1302 id, provider_id, model_id
1303 ),
1304 IdValidationError::ContainsControl { ch } => format!(
1305 "Extension '{}' registered provider '{}' with invalid model id '{}': contains control character U+{:04X}",
1306 id, provider_id, model_id, ch as u32
1307 ),
1308 });
1309 }
1310 if !model_ids.insert(model_id.to_string()) {
1311 return Err(format!(
1312 "Extension '{}' registered provider '{}' with duplicate model id '{}'",
1313 id, provider_id, model_id,
1314 ));
1315 }
1316 }
1317 if let Some(config_schema) = &provider.config_schema {
1318 if !config_schema.is_object() {
1319 return Err(format!(
1320 "Extension '{}' registered provider '{}' with invalid config_schema: config_schema must be a JSON object",
1321 id, provider_id,
1322 ));
1323 }
1324 }
1325 }
1326 Ok(())
1327 }
1328
1329 fn is_safe_provider_id(id: &str) -> bool {
1330 !id.is_empty()
1331 && !id.contains(':')
1332 && id
1333 .chars()
1334 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
1335 }
1336
1337 #[doc(hidden)]
1338 pub async fn initialize_for_test(&self, plugin_root: Option<PathBuf>) -> Result<(), String> {
1339 self.initialize(plugin_root, Value::Object(Default::default()))
1340 .await
1341 .map(|_| ())
1342 }
1343
1344 async fn restart_locked(&self, state: &mut Option<ProcessState>) -> Result<(), String> {
1345 let attempted = self.restart_count.fetch_add(1, Ordering::Relaxed) + 1;
1346 self.total_restarts.fetch_add(1, Ordering::Relaxed);
1347 let max_attempts = self.restart_policy.max_attempts;
1348 if attempted > max_attempts as usize {
1349 *state = None;
1350 return Err(format!(
1351 "Extension '{}' exceeded restart limit ({})",
1352 self.id, max_attempts,
1353 ));
1354 }
1355
1356 if let Some(old) = state.take() {
1357 old.reader_handle.abort();
1358 let mut child = old.child;
1359 let _ = child.kill().await;
1360 }
1361 self.inbox
1363 .fail_all_pending("transport closed: process restarting")
1364 .await;
1365
1366 let delay = self
1367 .restart_policy
1368 .delay_for_attempt(attempted as u32)
1369 .unwrap_or_default();
1370
1371 tracing::warn!(
1372 extension = %self.id,
1373 attempt = attempted,
1374 max_attempts = max_attempts,
1375 delay_ms = delay.as_millis() as u64,
1376 "Restarting extension process after transport failure",
1377 );
1378
1379 if !delay.is_zero() {
1380 tokio::time::sleep(delay).await;
1381 }
1382
1383 *state = Some(
1384 Self::spawn_state(
1385 &self.id,
1386 &self.command,
1387 &self.args,
1388 self.cwd.as_ref(),
1389 self.inbox.clone(),
1390 )
1391 .await?,
1392 );
1393 self.inbox
1395 .closed
1396 .store(false, std::sync::atomic::Ordering::Release);
1397 self.initialize_locked(state).await?;
1398 Ok(())
1405 }
1406
1407 async fn initialize_locked(&self, state: &mut Option<ProcessState>) -> Result<(), String> {
1408 let config = self.initialize_config.lock().await.clone();
1409 let params = InitializeParams {
1410 synaps_version: env!("CARGO_PKG_VERSION"),
1411 extension_protocol_version: CURRENT_EXTENSION_PROTOCOL_VERSION,
1412 plugin_id: self.id.clone(),
1413 plugin_root: self
1414 .cwd
1415 .clone()
1416 .map(|path| path.to_string_lossy().to_string()),
1417 config,
1418 };
1419 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1420 let value = tokio::time::timeout(
1421 std::time::Duration::from_secs(10),
1422 self.call_once_locked(
1423 state.as_mut().expect("state should exist for initialize"),
1424 "initialize",
1425 serde_json::to_value(params).map_err(|e| e.to_string())?,
1426 id,
1427 ),
1428 )
1429 .await
1430 .map_err(|_| format!("Extension '{}' initialize timed out after 10s", self.id))??;
1431 Self::parse_initialize_result(&self.id, value).map(|_| ())
1432 }
1433
1434 async fn call_once_locked(
1438 &self,
1439 state: &mut ProcessState,
1440 method: &str,
1441 params: Value,
1442 id: u64,
1443 ) -> Result<Value, String> {
1444 let body = serde_json::to_string(&JsonRpcRequest {
1445 jsonrpc: "2.0",
1446 method: method.to_string(),
1447 params,
1448 id,
1449 })
1450 .map_err(|e| format!("Serialize error: {}", e))?;
1451
1452 let (tx, rx) = oneshot::channel::<Result<Value, String>>();
1453 if self.inbox.closed.load(std::sync::atomic::Ordering::Acquire) {
1456 return Err("transport closed: inbox is shut down".to_string());
1457 }
1458
1459 self.inbox.pending.lock().await.insert(id, tx);
1462
1463 if self.inbox.closed.load(std::sync::atomic::Ordering::Acquire) {
1466 self.inbox.pending.lock().await.remove(&id);
1467 return Err("transport closed: inbox shut down during registration".to_string());
1468 }
1469
1470 let frame = format!("Content-Length: {}\r\n\r\n{}", body.len(), body);
1471 let write_result = {
1472 let mut stdin = state.stdin.lock().await;
1473 match stdin.write_all(frame.as_bytes()).await {
1474 Ok(()) => stdin.flush().await,
1475 Err(e) => Err(e),
1476 }
1477 };
1478 if let Err(e) = write_result {
1479 self.inbox.pending.lock().await.remove(&id);
1481 return Err(format!("Write error: {}", e));
1482 }
1483
1484 match rx.await {
1485 Ok(payload) => payload,
1486 Err(_) => {
1487 self.inbox.pending.lock().await.remove(&id);
1492 Err("transport closed: response channel dropped".to_string())
1493 }
1494 }
1495 }
1496
1497 async fn call_no_restart(&self, method: &str, params: Value) -> Result<Value, String> {
1498 let _call_guard = self.call_lock.lock().await;
1499 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1500 let mut state_guard = self.state.lock().await;
1501 if state_guard.is_none() {
1502 *state_guard = Some(
1503 Self::spawn_state(
1504 &self.id,
1505 &self.command,
1506 &self.args,
1507 self.cwd.as_ref(),
1508 self.inbox.clone(),
1509 )
1510 .await?,
1511 );
1512 }
1513 self.call_once_locked(
1514 state_guard.as_mut().expect("state should exist"),
1515 method,
1516 params,
1517 id,
1518 )
1519 .await
1520 }
1521
1522 async fn call(&self, method: &str, params: Value) -> Result<Value, String> {
1523 let timeout_secs = if method == "tool.call" { 120 } else { 30 };
1524 let id_str = self.id.clone();
1525 let method_str = method.to_string();
1526
1527 let result = tokio::time::timeout(
1528 std::time::Duration::from_secs(timeout_secs),
1529 self.call_inner(method, params),
1530 )
1531 .await;
1532
1533 match result {
1534 Ok(inner) => inner,
1535 Err(_) => Err(format!(
1536 "Extension '{}' method '{}' timed out after {}s",
1537 id_str, method_str, timeout_secs
1538 )),
1539 }
1540 }
1541
1542 async fn call_inner(&self, method: &str, params: Value) -> Result<Value, String> {
1543 let _call_guard = self.call_lock.lock().await;
1544 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1545 let mut state_guard = self.state.lock().await;
1546 if state_guard.is_none() {
1547 self.restart_locked(&mut state_guard).await?;
1548 }
1549
1550 let result = self
1551 .call_once_locked(
1552 state_guard
1553 .as_mut()
1554 .expect("state should exist after restart"),
1555 method,
1556 params.clone(),
1557 id,
1558 )
1559 .await;
1560
1561 match result {
1562 Ok(value) => {
1563 self.restart_count.store(0, Ordering::Relaxed);
1567 Ok(value)
1568 }
1569 Err(first_error) => {
1570 self.restart_locked(&mut state_guard).await?;
1571 let retry_id = self.next_id.fetch_add(1, Ordering::Relaxed);
1572 self.call_once_locked(
1573 state_guard
1574 .as_mut()
1575 .expect("state should exist after restart"),
1576 method,
1577 params,
1578 retry_id,
1579 )
1580 .await
1581 .inspect(|_| {
1582 self.restart_count.store(0, Ordering::Relaxed);
1584 })
1585 .map_err(|retry_error| {
1586 format!(
1587 "{}; retry after restart failed: {}",
1588 first_error, retry_error
1589 )
1590 })
1591 }
1592 }
1593 }
1594
1595 #[doc(hidden)]
1613 pub async fn subscribe_notifications(
1614 &self,
1615 ) -> (usize, mpsc::UnboundedReceiver<NotificationFrame>) {
1616 let (tx, rx) = mpsc::unbounded_channel();
1617 let id = self
1618 .inbox
1619 .next_sink_id
1620 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1621 let mut sinks = self.inbox.notification_sinks.lock().await;
1622 sinks.retain(|(_, tx)| !tx.is_closed());
1626 sinks.push((id, tx));
1627 (id, rx)
1628 }
1629
1630 #[doc(hidden)]
1635 pub async fn unsubscribe_notifications(&self, id: usize) {
1636 let mut sinks = self.inbox.notification_sinks.lock().await;
1637 sinks.retain(|(sub_id, tx)| *sub_id != id && !tx.is_closed());
1638 }
1639
1640 pub(crate) fn forward_invoke_command_frame(
1650 extension_id: &str,
1651 request_id: &str,
1652 sink: &mpsc::UnboundedSender<crate::extensions::runtime::InvokeCommandEvent>,
1653 sink_open: &mut bool,
1654 frame: NotificationFrame,
1655 ) -> bool {
1656 use crate::extensions::commands::parse_command_output;
1657 use crate::extensions::runtime::InvokeCommandEvent;
1658 use crate::extensions::tasks::{is_task_method, parse_task_event};
1659
1660 let mut saw_done = false;
1661 if frame.method == "command.output" {
1662 match parse_command_output(&frame.params) {
1663 Ok(parsed) if parsed.request_id == request_id => {
1664 if matches!(
1665 parsed.event,
1666 crate::extensions::commands::CommandOutputEvent::Done
1667 ) {
1668 saw_done = true;
1669 }
1670 if *sink_open && sink.send(InvokeCommandEvent::Output(parsed.event)).is_err() {
1671 *sink_open = false;
1672 }
1673 }
1674 Ok(_) => {
1675 tracing::trace!(
1677 extension = %extension_id,
1678 "Ignoring command.output for unrelated request_id",
1679 );
1680 }
1681 Err(error) => {
1682 tracing::warn!(
1683 extension = %extension_id,
1684 error = %error,
1685 params = %frame.params,
1686 "Skipping malformed command.output notification",
1687 );
1688 }
1689 }
1690 } else if is_task_method(&frame.method) {
1691 match parse_task_event(&frame.method, &frame.params) {
1692 Ok(event) => {
1693 if *sink_open && sink.send(InvokeCommandEvent::Task(event)).is_err() {
1694 *sink_open = false;
1695 }
1696 }
1697 Err(error) => {
1698 tracing::warn!(
1699 extension = %extension_id,
1700 method = %frame.method,
1701 error = %error,
1702 params = %frame.params,
1703 "Skipping malformed task notification",
1704 );
1705 }
1706 }
1707 } else {
1708 tracing::trace!(
1709 extension = %extension_id,
1710 method = %frame.method,
1711 "Ignoring non-command/task notification during command.invoke",
1712 );
1713 }
1714 saw_done
1715 }
1716
1717 fn forward_provider_stream_frame(
1724 extension_id: &str,
1725 sink: &mpsc::UnboundedSender<ProviderStreamEvent>,
1726 sink_open: &mut bool,
1727 frame: NotificationFrame,
1728 ) {
1729 if frame.method != "provider.stream.event" {
1730 tracing::trace!(
1731 extension = %extension_id,
1732 method = %frame.method,
1733 "Ignoring non-stream notification during provider.stream",
1734 );
1735 return;
1736 }
1737 match parse_provider_stream_event(&frame.params) {
1738 Ok(event) => {
1739 if *sink_open && sink.send(event).is_err() {
1740 *sink_open = false;
1741 }
1742 }
1743 Err(error) => {
1744 tracing::warn!(
1745 extension = %extension_id,
1746 error = %error,
1747 params = %frame.params,
1748 "Skipping malformed provider.stream.event notification",
1749 );
1750 }
1751 }
1752 }
1753}
1754
1755#[async_trait]
1756impl ExtensionHandler for ProcessExtension {
1757 fn id(&self) -> &str {
1758 &self.id
1759 }
1760
1761 async fn call_tool(&self, name: &str, input: Value) -> Result<Value, String> {
1762 self.call(
1763 "tool.call",
1764 serde_json::json!({
1765 "name": name,
1766 "input": input,
1767 }),
1768 )
1769 .await
1770 }
1771
1772 async fn provider_complete(
1773 &self,
1774 params: ProviderCompleteParams,
1775 ) -> Result<ProviderCompleteResult, String> {
1776 let value = tokio::time::timeout(
1777 std::time::Duration::from_secs(60),
1778 self.call(
1779 "provider.complete",
1780 serde_json::to_value(params).map_err(|e| e.to_string())?,
1781 ),
1782 )
1783 .await
1784 .map_err(|_| format!("Extension '{}' provider.complete timed out", self.id))??;
1785 let result: ProviderCompleteResult = serde_json::from_value(value).map_err(|e| {
1786 format!(
1787 "Invalid provider.complete response from extension '{}': {}",
1788 self.id, e
1789 )
1790 })?;
1791 if result.content.is_empty() {
1792 return Err(format!(
1793 "Extension '{}' provider.complete returned empty content",
1794 self.id
1795 ));
1796 }
1797 Ok(result)
1798 }
1799
1800 async fn provider_stream(
1801 &self,
1802 params: ProviderCompleteParams,
1803 sink: tokio::sync::mpsc::UnboundedSender<ProviderStreamEvent>,
1804 ) -> Result<ProviderCompleteResult, String> {
1805 let (sub_id, mut rx) = self.subscribe_notifications().await;
1808 let params_value = serde_json::to_value(params).map_err(|e| e.to_string())?;
1809
1810 let extension_id = self.id.clone();
1811 let stream_future = async {
1812 let mut call_fut = Box::pin(self.call("provider.stream", params_value));
1813 let mut sink_open = true;
1814 let response = loop {
1815 tokio::select! {
1816 response = &mut call_fut => break response,
1817 Some(frame) = rx.recv() => {
1818 Self::forward_provider_stream_frame(
1819 &extension_id, &sink, &mut sink_open, frame,
1820 );
1821 }
1822 }
1823 };
1824 self.unsubscribe_notifications(sub_id).await;
1829 while let Some(frame) = rx.recv().await {
1830 Self::forward_provider_stream_frame(&extension_id, &sink, &mut sink_open, frame);
1831 }
1832 response
1833 };
1834
1835 let outcome = tokio::time::timeout(std::time::Duration::from_secs(60), stream_future).await;
1836
1837 self.unsubscribe_notifications(sub_id).await;
1840
1841 let value =
1842 outcome.map_err(|_| format!("Extension '{}' provider.stream timed out", self.id))??;
1843
1844 let result: ProviderCompleteResult = serde_json::from_value(value).map_err(|e| {
1845 format!(
1846 "Invalid provider.stream response from extension '{}': {}",
1847 self.id, e
1848 )
1849 })?;
1850 Ok(result)
1853 }
1854
1855 async fn invoke_command(
1856 &self,
1857 command: &str,
1858 args: Vec<String>,
1859 request_id: &str,
1860 sink: tokio::sync::mpsc::UnboundedSender<crate::extensions::runtime::InvokeCommandEvent>,
1861 ) -> Result<Value, String> {
1862 let (sub_id, mut rx) = self.subscribe_notifications().await;
1864 let params = serde_json::json!({
1865 "command": command,
1866 "args": args,
1867 "request_id": request_id,
1868 });
1869
1870 let extension_id = self.id.clone();
1871 let request_id_owned = request_id.to_string();
1872 let invoke_future = async {
1873 let mut call_fut = Box::pin(self.call("command.invoke", params));
1874 let mut sink_open = true;
1875 let response = loop {
1876 tokio::select! {
1877 response = &mut call_fut => break response,
1878 Some(frame) = rx.recv() => {
1879 let _ = Self::forward_invoke_command_frame(
1880 &extension_id, &request_id_owned, &sink, &mut sink_open, frame,
1881 );
1882 }
1883 }
1884 };
1885 self.unsubscribe_notifications(sub_id).await;
1889 while let Ok(frame) = rx.try_recv() {
1890 let _ = Self::forward_invoke_command_frame(
1891 &extension_id,
1892 &request_id_owned,
1893 &sink,
1894 &mut sink_open,
1895 frame,
1896 );
1897 }
1898 response
1899 };
1900
1901 let outcome =
1902 tokio::time::timeout(std::time::Duration::from_secs(120), invoke_future).await;
1903
1904 self.unsubscribe_notifications(sub_id).await;
1907
1908 outcome.map_err(|_| format!("Extension '{}' command.invoke timed out", self.id))?
1909 }
1910
1911 async fn handle(&self, event: &HookEvent) -> HookResult {
1912 let params = serde_json::to_value(event).unwrap_or(Value::Null);
1913 match tokio::time::timeout(
1914 std::time::Duration::from_secs(5),
1915 self.call("hook.handle", params),
1916 )
1917 .await
1918 {
1919 Ok(Ok(value)) => match serde_json::from_value(value.clone()) {
1920 Ok(result) => result,
1921 Err(error) => {
1922 tracing::warn!(
1923 extension = %self.id,
1924 error = %error,
1925 response = %value,
1926 "Extension hook handler returned invalid result",
1927 );
1928 match value.get("action").and_then(Value::as_str) {
1929 Some("modify") => HookResult::Block {
1932 reason: "Extension returned malformed modify result".to_string(),
1933 },
1934 Some("replace") => HookResult::Continue,
1939 _ => HookResult::Continue,
1940 }
1941 }
1942 },
1943 Ok(Err(e)) => {
1944 tracing::warn!(
1945 extension = %self.id,
1946 error = %e,
1947 "Extension hook handler failed — continuing",
1948 );
1949 HookResult::Continue
1950 }
1951 Err(_) => {
1952 tracing::warn!(
1953 extension = %self.id,
1954 timeout_secs = 5,
1955 "Extension hook handler timed out — continuing",
1956 );
1957 HookResult::Continue
1958 }
1959 }
1960 }
1961
1962 async fn get_info(&self) -> Result<crate::extensions::info::PluginInfo, String> {
1963 let value = tokio::time::timeout(
1964 std::time::Duration::from_secs(5),
1965 self.call("info.get", Value::Null),
1966 )
1967 .await
1968 .map_err(|_| format!("Extension '{}' info.get timed out", self.id))??;
1969 serde_json::from_value(value).map_err(|e| {
1970 format!(
1971 "Invalid info.get response from extension '{}': {}",
1972 self.id, e
1973 )
1974 })
1975 }
1976
1977 async fn sidecar_spawn_args(&self) -> Result<crate::sidecar::spawn::SidecarSpawnArgs, String> {
1978 let value = tokio::time::timeout(
1979 std::time::Duration::from_secs(5),
1980 self.call("sidecar.spawn_args", Value::Null),
1981 )
1982 .await
1983 .map_err(|_| format!("Extension '{}' sidecar.spawn_args timed out", self.id))??;
1984 serde_json::from_value(value).map_err(|e| {
1985 format!(
1986 "Invalid sidecar.spawn_args response from extension '{}': {}",
1987 self.id, e
1988 )
1989 })
1990 }
1991
1992 async fn settings_editor_open(&self, category: &str, field: &str) -> Result<Value, String> {
1993 let params = crate::extensions::settings_editor::SettingsEditorOpenParams {
1994 category: category.to_string(),
1995 field: field.to_string(),
1996 };
1997 tokio::time::timeout(
1998 std::time::Duration::from_secs(5),
1999 self.call(
2000 "settings.editor.open",
2001 serde_json::to_value(params).map_err(|e| e.to_string())?,
2002 ),
2003 )
2004 .await
2005 .map_err(|_| format!("Extension '{}' settings.editor.open timed out", self.id))?
2006 }
2007
2008 async fn settings_editor_key(
2009 &self,
2010 category: &str,
2011 field: &str,
2012 key: &str,
2013 ) -> Result<Value, String> {
2014 let mut params = serde_json::to_value(
2015 crate::extensions::settings_editor::SettingsEditorKeyParams {
2016 key: key.to_string(),
2017 },
2018 )
2019 .map_err(|e| e.to_string())?;
2020 if let Some(obj) = params.as_object_mut() {
2021 obj.insert("category".to_string(), Value::String(category.to_string()));
2022 obj.insert("field".to_string(), Value::String(field.to_string()));
2023 }
2024 tokio::time::timeout(
2025 std::time::Duration::from_secs(5),
2026 self.call("settings.editor.key", params),
2027 )
2028 .await
2029 .map_err(|_| format!("Extension '{}' settings.editor.key timed out", self.id))?
2030 }
2031
2032 async fn settings_editor_commit(
2033 &self,
2034 category: &str,
2035 field: &str,
2036 value: Value,
2037 ) -> Result<Value, String> {
2038 let params = serde_json::json!({
2039 "category": category,
2040 "field": field,
2041 "value": value,
2042 });
2043 tokio::time::timeout(
2044 std::time::Duration::from_secs(5),
2045 self.call("settings.editor.commit", params),
2046 )
2047 .await
2048 .map_err(|_| format!("Extension '{}' settings.editor.commit timed out", self.id))?
2049 }
2050
2051 async fn shutdown(&self) {
2052 let _ = tokio::time::timeout(
2053 std::time::Duration::from_millis(500),
2054 self.call("shutdown", Value::Null),
2055 )
2056 .await;
2057
2058 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2059 let mut state_guard = self.state.lock().await;
2060 if let Some(state) = state_guard.take() {
2061 state.reader_handle.abort();
2062 let mut child = state.child;
2063 let _ = child.kill().await;
2064 }
2065 self.inbox.notification_sinks.lock().await.clear();
2067 self.inbox
2068 .fail_all_pending("transport closed: extension shutdown")
2069 .await;
2070 }
2071
2072 async fn subscribe_notifications(
2073 &self,
2074 ) -> (
2075 usize,
2076 tokio::sync::mpsc::UnboundedReceiver<NotificationFrame>,
2077 ) {
2078 ProcessExtension::subscribe_notifications(self).await
2079 }
2080
2081 async fn restart_count(&self) -> usize {
2082 self.restart_count()
2083 }
2084
2085 async fn health(&self) -> ExtensionHealth {
2086 let consecutive = self.restart_count.load(Ordering::Relaxed);
2090 let lifetime = self.total_restarts.load(Ordering::Relaxed);
2091 let max = self.restart_policy.max_attempts as usize;
2092 if consecutive >= max {
2093 ExtensionHealth::Failed
2094 } else if lifetime > 0 {
2095 let state_alive = self.state.try_lock().map(|g| g.is_some()).unwrap_or(true);
2099 if state_alive {
2100 ExtensionHealth::Degraded
2101 } else {
2102 ExtensionHealth::Restarting
2103 }
2104 } else {
2105 ExtensionHealth::Running
2106 }
2107 }
2108}
2109
2110#[cfg(test)]
2111mod stream_event_tests {
2112 use super::*;
2113 use serde_json::json;
2114
2115 #[test]
2116 fn parses_text_delta_with_delta_key() {
2117 let v = json!({"type": "text", "delta": "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_text_delta_with_text_key() {
2126 let v = json!({"type": "text", "text": "hi"});
2127 assert_eq!(
2128 parse_provider_stream_event(&v).unwrap(),
2129 ProviderStreamEvent::TextDelta { text: "hi".into() }
2130 );
2131 }
2132
2133 #[test]
2134 fn parses_thinking_delta() {
2135 let v = json!({"type": "thinking", "delta": "hmm"});
2136 assert_eq!(
2137 parse_provider_stream_event(&v).unwrap(),
2138 ProviderStreamEvent::ThinkingDelta { text: "hmm".into() }
2139 );
2140 let v2 = json!({"type": "thinking", "text": "hmm"});
2141 assert_eq!(
2142 parse_provider_stream_event(&v2).unwrap(),
2143 ProviderStreamEvent::ThinkingDelta { text: "hmm".into() }
2144 );
2145 }
2146
2147 #[test]
2148 fn parses_tool_use() {
2149 let v = json!({
2150 "type": "tool_use",
2151 "id": "t1",
2152 "name": "echo",
2153 "input": {"x": 1}
2154 });
2155 assert_eq!(
2156 parse_provider_stream_event(&v).unwrap(),
2157 ProviderStreamEvent::ToolUse {
2158 id: "t1".into(),
2159 name: "echo".into(),
2160 input: json!({"x": 1}),
2161 }
2162 );
2163 }
2164
2165 #[test]
2166 fn tool_use_input_defaults_to_empty_object() {
2167 let v = json!({"type": "tool_use", "id": "t1", "name": "echo"});
2168 assert_eq!(
2169 parse_provider_stream_event(&v).unwrap(),
2170 ProviderStreamEvent::ToolUse {
2171 id: "t1".into(),
2172 name: "echo".into(),
2173 input: json!({}),
2174 }
2175 );
2176 }
2177
2178 #[test]
2179 fn parses_usage_strips_type() {
2180 let v = json!({"type": "usage", "input_tokens": 5, "output_tokens": 7});
2181 assert_eq!(
2182 parse_provider_stream_event(&v).unwrap(),
2183 ProviderStreamEvent::Usage {
2184 usage: json!({"input_tokens": 5, "output_tokens": 7})
2185 }
2186 );
2187 }
2188
2189 #[test]
2190 fn parses_error() {
2191 let v = json!({"type": "error", "message": "boom"});
2192 assert_eq!(
2193 parse_provider_stream_event(&v).unwrap(),
2194 ProviderStreamEvent::Error {
2195 message: "boom".into()
2196 }
2197 );
2198 }
2199
2200 #[test]
2201 fn parses_done() {
2202 let v = json!({"type": "done"});
2203 assert_eq!(
2204 parse_provider_stream_event(&v).unwrap(),
2205 ProviderStreamEvent::Done
2206 );
2207 }
2208
2209 #[test]
2210 fn nested_event_shape_matches_flat() {
2211 let flat = json!({"type": "text", "delta": "hi"});
2212 let nested = json!({"event": {"type": "text", "delta": "hi"}});
2213 assert_eq!(
2214 parse_provider_stream_event(&flat).unwrap(),
2215 parse_provider_stream_event(&nested).unwrap()
2216 );
2217 }
2218
2219 #[test]
2220 fn missing_type_errors() {
2221 let v = json!({"delta": "hi"});
2222 let err = parse_provider_stream_event(&v).unwrap_err();
2223 assert!(err.contains("missing type"), "got: {err}");
2224 }
2225
2226 #[test]
2227 fn unknown_type_errors_with_type() {
2228 let v = json!({"type": "wat"});
2229 let err = parse_provider_stream_event(&v).unwrap_err();
2230 assert!(err.contains("wat"), "got: {err}");
2231 }
2232
2233 #[test]
2234 fn tool_use_missing_id_errors() {
2235 let v = json!({"type": "tool_use", "name": "echo"});
2236 let err = parse_provider_stream_event(&v).unwrap_err();
2237 assert!(err.contains("id"), "got: {err}");
2238 }
2239
2240 #[test]
2241 fn tool_use_missing_name_errors() {
2242 let v = json!({"type": "tool_use", "id": "t1"});
2243 let err = parse_provider_stream_event(&v).unwrap_err();
2244 assert!(err.contains("name"), "got: {err}");
2245 }
2246
2247 #[test]
2248 fn tool_use_empty_id_errors() {
2249 let v = json!({"type": "tool_use", "id": "", "name": "echo"});
2250 assert!(parse_provider_stream_event(&v).is_err());
2251 }
2252
2253 #[test]
2254 fn tool_use_empty_name_errors() {
2255 let v = json!({"type": "tool_use", "id": "t1", "name": ""});
2256 assert!(parse_provider_stream_event(&v).is_err());
2257 }
2258
2259 #[test]
2260 fn tool_use_non_object_input_errors() {
2261 let v = json!({"type": "tool_use", "id": "t1", "name": "echo", "input": "nope"});
2262 let err = parse_provider_stream_event(&v).unwrap_err();
2263 assert!(err.contains("input"), "got: {err}");
2264 }
2265
2266 #[test]
2267 fn text_missing_delta_and_text_errors() {
2268 let v = json!({"type": "text"});
2269 let err = parse_provider_stream_event(&v).unwrap_err();
2270 assert!(err.contains("delta") || err.contains("text"), "got: {err}");
2271 }
2272
2273 #[test]
2274 fn error_missing_message_errors() {
2275 let v = json!({"type": "error"});
2276 assert!(parse_provider_stream_event(&v).is_err());
2277 }
2278
2279 #[test]
2280 fn error_empty_message_errors() {
2281 let v = json!({"type": "error", "message": ""});
2282 assert!(parse_provider_stream_event(&v).is_err());
2283 }
2284}
2285
2286#[cfg(test)]
2287mod restart_policy_tests {
2288 use super::*;
2289
2290 #[tokio::test]
2291 async fn restart_policy_default_max_attempts_is_3() {
2292 let ext = ProcessExtension::spawn("policy-test", "/bin/cat", &[])
2297 .await
2298 .expect("spawn /bin/cat");
2299 assert_eq!(ext.restart_policy.max_attempts, 3);
2300 ext.shutdown().await;
2301 }
2302
2303 #[tokio::test]
2304 async fn with_restart_policy_overrides_default() {
2305 let ext = ProcessExtension::spawn("policy-test-override", "/bin/cat", &[])
2306 .await
2307 .expect("spawn /bin/cat");
2308 let custom = RestartPolicy {
2309 max_attempts: 7,
2310 ..RestartPolicy::default()
2311 };
2312 let ext = ext.with_restart_policy(custom);
2313 assert_eq!(ext.restart_policy.max_attempts, 7);
2314 ext.shutdown().await;
2315 }
2316}
2317
2318#[cfg(test)]
2319mod capture_validator_tests {
2320 use super::*;
2321 use crate::extensions::permissions::{Permission, PermissionSet};
2322
2323 fn perms_with(grants: &[Permission]) -> PermissionSet {
2324 let mut p = PermissionSet::new();
2325 for g in grants {
2326 p.grant(*g);
2327 }
2328 p
2329 }
2330
2331 fn cap(kind: &str, name: &str, perms: &[&str]) -> CapabilityDeclaration {
2332 CapabilityDeclaration {
2333 kind: kind.to_string(),
2334 name: name.to_string(),
2335 permissions: perms.iter().map(|p| p.to_string()).collect(),
2336 params: serde_json::Value::Null,
2337 }
2338 }
2339
2340 #[test]
2341 fn capability_validator_rejects_empty_kind() {
2342 let d = cap(" ", "Sample", &["audio.input"]);
2343 let perms = perms_with(&[Permission::AudioInput]);
2344 let err = validate_capability(&d, &perms).unwrap_err();
2345 assert!(err.contains("kind"), "got: {}", err);
2346 }
2347
2348 #[test]
2349 fn capability_validator_rejects_empty_name() {
2350 let d = cap("capture", " ", &["audio.input"]);
2351 let perms = perms_with(&[Permission::AudioInput]);
2352 let err = validate_capability(&d, &perms).unwrap_err();
2353 assert!(err.contains("name"), "got: {}", err);
2354 }
2355
2356 #[test]
2357 fn capability_validator_rejects_unknown_permission_string() {
2358 let d = cap("capture", "Sample", &["audio.telepathy"]);
2359 let perms = perms_with(&[Permission::AudioInput, Permission::AudioOutput]);
2360 let err = validate_capability(&d, &perms).unwrap_err();
2361 assert!(
2362 err.contains("unknown permission") && err.contains("audio.telepathy"),
2363 "got: {}",
2364 err,
2365 );
2366 }
2367
2368 #[test]
2369 fn capability_validator_requires_every_declared_permission() {
2370 let d = cap("capture", "Sample", &["audio.input"]);
2371 let perms = perms_with(&[]);
2372 let err = validate_capability(&d, &perms).unwrap_err();
2373 assert!(
2374 err.contains("audio.input") && err.contains("not granted"),
2375 "got: {}",
2376 err,
2377 );
2378 }
2379
2380 #[test]
2381 fn capability_validator_accepts_when_all_permissions_granted() {
2382 let d = cap("capture", "Sample", &["audio.input", "audio.output"]);
2383 let perms = perms_with(&[Permission::AudioInput, Permission::AudioOutput]);
2384 validate_capability(&d, &perms).expect("should validate");
2385 }
2386
2387 #[test]
2388 fn capability_validator_accepts_no_permissions() {
2389 let d = cap("ocr", "Tesseract", &[]);
2393 let perms = perms_with(&[]);
2394 validate_capability(&d, &perms).expect("should validate");
2395 }
2396
2397 #[test]
2398 fn capability_validator_does_not_branch_on_kind() {
2399 let perms = perms_with(&[Permission::AudioInput]);
2403 for kind in ["capture", "ocr", "agent", "foot_pedal", "eeg"] {
2404 let d = cap(kind, "Anything", &["audio.input"]);
2405 validate_capability(&d, &perms).expect("should validate");
2406 }
2407 }
2408}
2409
2410#[cfg(test)]
2411mod invoke_command_dispatch_tests {
2412 use super::*;
2417 use crate::extensions::commands::CommandOutputEvent;
2418 use crate::extensions::runtime::InvokeCommandEvent;
2419 use crate::extensions::tasks::{TaskEvent, TaskKind};
2420 use serde_json::json;
2421 use tokio::sync::mpsc;
2422
2423 fn frame(method: &str, params: serde_json::Value) -> NotificationFrame {
2424 NotificationFrame {
2425 method: method.to_string(),
2426 params,
2427 }
2428 }
2429
2430 #[test]
2431 fn forwards_mixed_event_stream_in_order() {
2432 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2433 let mut open = true;
2434 let frames = vec![
2435 frame(
2436 "command.output",
2437 json!({"request_id":"r1","event":{"kind":"text","content":"A"}}),
2438 ),
2439 frame(
2440 "task.start",
2441 json!({"id":"dl","label":"Downloading","kind":"download"}),
2442 ),
2443 frame("task.update", json!({"id":"dl","current":50,"total":100})),
2444 frame(
2445 "command.output",
2446 json!({"request_id":"r1","event":{"kind":"system","content":"working"}}),
2447 ),
2448 frame("task.done", json!({"id":"dl"})),
2449 frame(
2450 "command.output",
2451 json!({"request_id":"r1","event":{"kind":"done"}}),
2452 ),
2453 ];
2454
2455 let mut saw_done = false;
2456 for f in frames {
2457 saw_done |=
2458 ProcessExtension::forward_invoke_command_frame("ext-test", "r1", &tx, &mut open, f);
2459 }
2460 drop(tx);
2461 assert!(saw_done, "should have observed the command Done marker");
2462
2463 let mut events = Vec::new();
2464 while let Ok(ev) = rx.try_recv() {
2465 events.push(ev);
2466 }
2467 assert_eq!(events.len(), 6);
2468 assert_eq!(
2469 events[0],
2470 InvokeCommandEvent::Output(CommandOutputEvent::Text {
2471 content: "A".into()
2472 })
2473 );
2474 assert!(matches!(
2475 events[1],
2476 InvokeCommandEvent::Task(TaskEvent::Start {
2477 kind: TaskKind::Download,
2478 ..
2479 })
2480 ));
2481 assert!(matches!(
2482 events[2],
2483 InvokeCommandEvent::Task(TaskEvent::Update { .. })
2484 ));
2485 assert!(matches!(
2486 events[3],
2487 InvokeCommandEvent::Output(CommandOutputEvent::System { .. })
2488 ));
2489 assert!(matches!(
2490 events[4],
2491 InvokeCommandEvent::Task(TaskEvent::Done { error: None, .. })
2492 ));
2493 assert_eq!(
2494 events[5],
2495 InvokeCommandEvent::Output(CommandOutputEvent::Done)
2496 );
2497 }
2498
2499 #[test]
2500 fn ignores_command_output_for_unrelated_request_id() {
2501 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2502 let mut open = true;
2503 ProcessExtension::forward_invoke_command_frame(
2504 "ext",
2505 "r1",
2506 &tx,
2507 &mut open,
2508 frame(
2509 "command.output",
2510 json!({"request_id":"other","event":{"kind":"text","content":"x"}}),
2511 ),
2512 );
2513 drop(tx);
2514 assert!(rx.try_recv().is_err());
2515 }
2516
2517 #[test]
2518 fn skips_malformed_command_output_without_aborting() {
2519 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2520 let mut open = true;
2521 ProcessExtension::forward_invoke_command_frame(
2523 "ext",
2524 "r1",
2525 &tx,
2526 &mut open,
2527 frame("command.output", json!({"request_id":"r1","event":{}})),
2528 );
2529 ProcessExtension::forward_invoke_command_frame(
2531 "ext",
2532 "r1",
2533 &tx,
2534 &mut open,
2535 frame(
2536 "command.output",
2537 json!({"request_id":"r1","event":{"kind":"done"}}),
2538 ),
2539 );
2540 drop(tx);
2541 let ev = rx.try_recv().unwrap();
2542 assert_eq!(ev, InvokeCommandEvent::Output(CommandOutputEvent::Done));
2543 assert!(rx.try_recv().is_err());
2544 }
2545
2546 #[test]
2547 fn task_events_pass_through_regardless_of_request_id() {
2548 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2549 let mut open = true;
2550 ProcessExtension::forward_invoke_command_frame(
2551 "ext",
2552 "r1",
2553 &tx,
2554 &mut open,
2555 frame("task.log", json!({"id":"abc","line":"..."})),
2556 );
2557 drop(tx);
2558 match rx.try_recv().unwrap() {
2559 InvokeCommandEvent::Task(TaskEvent::Log { id, line }) => {
2560 assert_eq!(id, "abc");
2561 assert_eq!(line, "...");
2562 }
2563 other => panic!("unexpected: {other:?}"),
2564 }
2565 }
2566
2567 #[test]
2568 fn unrelated_methods_are_dropped() {
2569 let (tx, mut rx) = mpsc::unbounded_channel::<InvokeCommandEvent>();
2570 let mut open = true;
2571 ProcessExtension::forward_invoke_command_frame(
2572 "ext",
2573 "r1",
2574 &tx,
2575 &mut open,
2576 frame("provider.stream.event", json!({"type":"text","delta":"x"})),
2577 );
2578 drop(tx);
2579 assert!(rx.try_recv().is_err());
2580 }
2581}