1use std::collections::{HashMap, HashSet};
7use std::path::PathBuf;
8use std::process::Stdio;
9use std::sync::atomic::{AtomicBool, 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, ExtensionLifecycle, 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 session_tools: &crate::tools::activation::SessionToolSet,
219 hook_bus: &Arc<crate::extensions::hooks::HookBus>,
220 tool_use: ProviderToolUse,
221 ctx: crate::ToolContext,
222 max_tool_output: usize,
223) -> Value {
224 let tool_id = tool_use.id;
225 let tool_name = tool_use.name;
226 let input = tool_use.input;
227
228 let authorized = match crate::tools::activation::ExecutionGate::authorize_wire_call(
240 registry,
241 session_tools,
242 &tool_name,
243 ) {
244 Ok(authorized) => authorized,
245 Err(denial) => {
246 return serde_json::json!({
247 "type": "tool_result",
248 "tool_use_id": tool_id,
249 "content": denial.to_string(),
250 "is_error": true,
251 });
252 }
253 };
254 let tool = authorized.implementation();
255
256 let runtime_name = authorized.runtime_name().to_string();
257 let input = registry.translate_input_for_api_tool(&tool_name, input);
258 let decision = crate::runtime::resolve_before_tool_call_decision(
259 input.clone(),
260 crate::runtime::emit_before_tool_call(
261 hook_bus,
262 &tool_name,
263 Some(&runtime_name),
264 input.clone(),
265 None,
268 )
269 .await,
270 ctx.capabilities.secret_prompt.as_ref(),
271 false,
272 )
273 .await;
274
275 let crate::runtime::BeforeToolCallDecision::Continue { input } = decision else {
276 let crate::runtime::BeforeToolCallDecision::Block { reason } = decision else {
277 unreachable!()
278 };
279 return serde_json::json!({
280 "type": "tool_result",
281 "tool_use_id": tool_id,
282 "content": format!("Tool call blocked by extension: {}", reason),
283 "is_error": true,
284 });
285 };
286
287 let input_for_hook = input.clone();
288 let (result, is_error) = match tool.execute(input, ctx).await {
289 Ok(output) => (output, false),
290 Err(error) => (format!("Tool execution failed: {}", error), true),
291 };
292 let result = crate::runtime::emit_after_tool_call(
293 hook_bus,
294 &tool_name,
295 Some(&runtime_name),
296 input_for_hook,
297 result,
298 max_tool_output,
299 None,
300 )
301 .await;
302
303 let mut response = serde_json::json!({
304 "type": "tool_result",
305 "tool_use_id": tool_id,
306 "content": crate::truncate_str(&result, max_tool_output).to_string(),
307 });
308 if is_error {
309 response["is_error"] = serde_json::json!(true);
310 }
311 response
312}
313
314#[allow(clippy::too_many_arguments)] pub async fn complete_provider_with_tools<F>(
321 handler: Arc<dyn ExtensionHandler>,
322 mut params: ProviderCompleteParams,
323 registry: &crate::ToolRegistry,
324 session_tools: &crate::tools::activation::SessionToolSet,
325 hook_bus: &Arc<crate::extensions::hooks::HookBus>,
326 mut context_factory: F,
327 max_tool_output: usize,
328 max_iterations: usize,
329 tools_requested: &mut u32,
334) -> Result<ProviderCompleteResult, String>
335where
336 F: FnMut() -> crate::ToolContext,
337{
338 let max_iterations = max_iterations.max(1);
339 *tools_requested = 0;
340 for iteration in 0..max_iterations {
341 let result = handler.provider_complete(params.clone()).await?;
342 let tool_uses = extract_provider_tool_uses(&result.content)?;
343 if tool_uses.is_empty() {
344 return Ok(result);
345 }
346 *tools_requested = tools_requested.saturating_add(tool_uses.len() as u32);
349 if iteration + 1 == max_iterations {
350 return Err(format!(
351 "extension provider '{}' exceeded provider tool-use iteration limit ({})",
352 handler.id(),
353 max_iterations,
354 ));
355 }
356
357 let assistant_content = result.content.clone();
358 params.messages.push(std::sync::Arc::new(serde_json::json!({
359 "role": "assistant",
360 "content": assistant_content,
361 })));
362
363 let mut tool_results = Vec::with_capacity(tool_uses.len());
364 for tool_use in tool_uses {
365 tool_results.push(
366 execute_provider_tool_use(
367 registry,
368 session_tools,
369 hook_bus,
370 tool_use,
371 context_factory(),
372 max_tool_output,
373 )
374 .await,
375 );
376 }
377 params.messages.push(std::sync::Arc::new(serde_json::json!({
378 "role": "user",
379 "content": tool_results,
380 })));
381 }
382 Err(format!(
383 "extension provider '{}' exceeded provider tool-use iteration limit ({})",
384 handler.id(),
385 max_iterations,
386 ))
387}
388
389pub fn extract_provider_tool_uses(content: &[Value]) -> Result<Vec<ProviderToolUse>, String> {
390 let mut tool_uses = Vec::new();
391 for block in content {
392 if block.get("type").and_then(Value::as_str) != Some("tool_use") {
393 continue;
394 }
395 let id = block
396 .get("id")
397 .and_then(Value::as_str)
398 .ok_or_else(|| "provider tool_use missing id".to_string())?;
399 let name = block
400 .get("name")
401 .and_then(Value::as_str)
402 .ok_or_else(|| "provider tool_use missing name".to_string())?;
403 if id.trim().is_empty() {
404 return Err("provider tool_use id is empty".to_string());
405 }
406 if name.trim().is_empty() {
407 return Err("provider tool_use name is empty".to_string());
408 }
409 let input = block
410 .get("input")
411 .cloned()
412 .unwrap_or_else(|| serde_json::json!({}));
413 if !input.is_object() {
414 return Err(format!(
415 "provider tool_use '{}' input must be a JSON object",
416 id
417 ));
418 }
419 tool_uses.push(ProviderToolUse {
420 id: id.to_string(),
421 name: name.to_string(),
422 input,
423 });
424 }
425 Ok(tool_uses)
426}
427
428#[derive(Debug, Clone, Default, PartialEq)]
429pub struct InitializeCapabilitiesResult {
430 pub tools: Vec<RegisteredExtensionToolSpec>,
431 pub providers: Vec<RegisteredProviderSpec>,
432 pub capabilities: Vec<CapabilityDeclaration>,
438}
439
440#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
449pub struct CapabilityDeclaration {
450 pub kind: String,
454 pub name: String,
457 #[serde(default)]
462 pub permissions: Vec<String>,
463 #[serde(default, skip_serializing_if = "is_null_value")]
467 pub params: serde_json::Value,
468}
469
470fn is_null_value(v: &serde_json::Value) -> bool {
471 v.is_null()
472}
473
474pub fn validate_capability(
484 decl: &CapabilityDeclaration,
485 granted: &crate::extensions::permissions::PermissionSet,
486) -> Result<(), String> {
487 use crate::extensions::permissions::Permission;
488 if decl.kind.trim().is_empty() {
489 return Err("capability 'kind' must be non-empty".to_string());
490 }
491 if decl.name.trim().is_empty() {
492 return Err("capability 'name' must be non-empty".to_string());
493 }
494 for perm_name in &decl.permissions {
495 let parsed = Permission::parse(perm_name).ok_or_else(|| {
496 format!(
497 "capability '{}' declares unknown permission '{}'",
498 decl.kind, perm_name
499 )
500 })?;
501 if !granted.has(parsed) {
502 return Err(format!(
503 "capability '{}' requires permission '{}' but it is not granted",
504 decl.kind, perm_name
505 ));
506 }
507 }
508 Ok(())
509}
510
511#[derive(Deserialize)]
512struct InitializeResult {
513 protocol_version: u32,
514 #[serde(default)]
515 capabilities: InitializeCapabilities,
516}
517
518#[derive(Default, Deserialize)]
519struct InitializeCapabilities {
520 #[serde(default)]
521 tools: Vec<RegisteredExtensionToolSpec>,
522 #[serde(default)]
523 providers: Vec<RegisteredProviderSpec>,
524 #[serde(default)]
526 capabilities: Vec<CapabilityDeclaration>,
527}
528
529#[doc(hidden)]
535#[derive(Debug, Clone)]
536pub struct NotificationFrame {
537 pub method: String,
538 pub params: Value,
539}
540
541pub const NOTIFICATION_QUEUE_CAPACITY: usize = 8;
547pub const PROVIDER_EVENT_QUEUE_CAPACITY: usize = 8;
550
551struct Inbox {
558 pending: Mutex<HashMap<u64, oneshot::Sender<Result<Value, String>>>>,
559 notification_sinks: Mutex<Vec<(usize, mpsc::Sender<NotificationFrame>)>>,
567 next_sink_id: std::sync::atomic::AtomicUsize,
571 closed: std::sync::atomic::AtomicBool,
574 lifecycle: Mutex<Option<Arc<ProcessLifecycle>>>,
578 next_generation: AtomicU64,
581 permissions: RwLock<Option<crate::extensions::permissions::PermissionSet>>,
584 inbound_stdin: Mutex<Option<Arc<Mutex<ChildStdin>>>>,
588 extension_id: String,
590 exclusive_memory: bool,
591}
592
593impl Inbox {
594 fn new(extension_id: String) -> Self {
595 Self {
596 pending: Mutex::new(HashMap::new()),
597 notification_sinks: Mutex::new(Vec::new()),
598 next_sink_id: std::sync::atomic::AtomicUsize::new(0),
599 closed: std::sync::atomic::AtomicBool::new(false),
600 lifecycle: Mutex::new(None),
601 next_generation: AtomicU64::new(2),
602 permissions: RwLock::new(None),
603 inbound_stdin: Mutex::new(None),
604 exclusive_memory: crate::memory_backend::MemoryBinding::configured_current()
605 .exclusive(),
606 extension_id,
607 }
608 }
609
610 async fn invalidate_lifecycle(&self) {
611 if let Some(lifecycle) = self.lifecycle.lock().await.as_ref() {
612 lifecycle.invalidate();
613 }
614 }
615
616 async fn fail_all_pending(&self, reason: &str) {
619 self.invalidate_lifecycle().await;
620 self.closed
621 .store(true, std::sync::atomic::Ordering::Release);
622 let drained: Vec<_> = {
623 let mut pending = self.pending.lock().await;
624 pending.drain().collect()
625 };
626 for (_, tx) in drained {
627 let _ = tx.send(Err(reason.to_string()));
628 }
629 }
630}
631
632struct ProcessLifecycle {
635 child: Mutex<Child>,
636 generation: AtomicU64,
637 initialized: AtomicBool,
638}
639
640impl ProcessLifecycle {
641 fn invalidate(&self) {
642 self.generation.fetch_or(1, Ordering::AcqRel);
645 }
646
647 fn snapshot(&self, recovered: bool) -> Option<ExtensionLifecycle> {
648 if self.generation.load(Ordering::Acquire) & 1 == 0 {
649 let mut child = self.child.try_lock().ok()?;
650 match child.try_wait() {
651 Ok(None) => {}
652 Ok(Some(_)) | Err(_) => self.invalidate(),
653 }
654 }
655 let generation = self.generation.load(Ordering::Acquire);
656 let health = if generation & 1 != 0 {
657 ExtensionHealth::Failed
658 } else if !self.initialized.load(Ordering::Acquire) {
659 ExtensionHealth::Loaded
660 } else if recovered {
661 ExtensionHealth::Degraded
662 } else {
663 ExtensionHealth::Running
664 };
665 Some(ExtensionLifecycle { generation, health })
666 }
667}
668
669struct ProcessState {
670 lifecycle: Arc<ProcessLifecycle>,
671 stdin: Arc<Mutex<ChildStdin>>,
672 reader_handle: JoinHandle<()>,
673}
674
675impl Drop for ProcessState {
676 fn drop(&mut self) {
677 self.lifecycle.invalidate();
678 self.reader_handle.abort();
679 if let Ok(mut child) = self.lifecycle.child.try_lock() {
682 let _ = child.start_kill();
683 }
684 }
685}
686
687pub struct ProcessExtension {
689 id: String,
690 command: String,
691 args: Vec<String>,
692 cwd: Option<PathBuf>,
693 state: Arc<Mutex<Option<ProcessState>>>,
694 call_lock: Arc<Mutex<()>>,
696 next_id: AtomicU64,
697 restart_count: AtomicUsize,
698 initialize_config: Mutex<Value>,
702 total_restarts: AtomicUsize,
708 pub(crate) restart_policy: RestartPolicy,
710 inbox: Arc<Inbox>,
714}
715
716impl ProcessExtension {
717 pub async fn force_shutdown(&self) {
721 self.inbox.invalidate_lifecycle().await;
722 let mut state_guard = self.state.lock().await;
723 if let Some(state) = state_guard.take() {
724 state.reader_handle.abort();
725 state.lifecycle.invalidate();
726 let _ = state.lifecycle.child.lock().await.kill().await;
727 }
728 drop(state_guard);
729 self.inbox.closed.store(true, Ordering::Release);
730 self.inbox.notification_sinks.lock().await.clear();
731 self.inbox
732 .fail_all_pending("transport closed: extension cancelled")
733 .await;
734 }
735
736 pub async fn spawn(id: &str, command: &str, args: &[String]) -> Result<Self, String> {
737 Self::spawn_with_cwd(id, command, args, None).await
738 }
739
740 pub async fn spawn_with_cwd(
745 id: &str,
746 command: &str,
747 args: &[String],
748 cwd: Option<PathBuf>,
749 ) -> Result<Self, String> {
750 let exclusive = crate::memory_backend::MemoryBinding::configured_current().exclusive();
751 Self::spawn_with_memory_policy(id, command, args, cwd, exclusive).await
752 }
753
754 pub(crate) async fn spawn_with_memory_policy(
755 id: &str,
756 command: &str,
757 args: &[String],
758 cwd: Option<PathBuf>,
759 exclusive: bool,
760 ) -> Result<Self, String> {
761 if exclusive && id == "axel-memory-manager" {
762 return Err(
763 "independent Axel plugin is disabled by the host-selected memory backend".into(),
764 );
765 }
766 let mut inbox = Inbox::new(id.to_string());
767 inbox.exclusive_memory = exclusive;
768 let inbox = Arc::new(inbox);
769 let state = Self::spawn_state(id, command, args, cwd.as_ref(), inbox.clone()).await?;
770 Ok(Self {
771 id: id.to_string(),
772 command: command.to_string(),
773 args: args.to_vec(),
774 cwd,
775 state: Arc::new(Mutex::new(Some(state))),
776 call_lock: Arc::new(Mutex::new(())),
777 next_id: AtomicU64::new(1),
778 restart_count: AtomicUsize::new(0),
779 initialize_config: Mutex::new(Value::Object(Default::default())),
780 total_restarts: AtomicUsize::new(0),
781 restart_policy: RestartPolicy::default(),
782 inbox,
783 })
784 }
785
786 pub fn with_restart_policy(mut self, policy: RestartPolicy) -> Self {
788 self.restart_policy = policy;
789 self
790 }
791
792 async fn spawn_state(
793 id: &str,
794 command: &str,
795 args: &[String],
796 cwd: Option<&PathBuf>,
797 inbox: Arc<Inbox>,
798 ) -> Result<ProcessState, String> {
799 let mut cmd = Command::new(command);
800 cmd.args(args)
801 .stdin(Stdio::piped())
802 .stdout(Stdio::piped())
803 .stderr(Stdio::piped());
804 if let Some(cwd) = cwd {
805 cmd.current_dir(cwd);
806 }
807
808 cmd.env_clear();
814 for var in &["PATH", "HOME", "LANG", "TERM", "XDG_RUNTIME_DIR"] {
815 if let Ok(val) = std::env::var(var) {
816 cmd.env(var, val);
817 }
818 }
819
820 cmd.kill_on_drop(true);
821
822 let mut child = cmd
823 .spawn()
824 .map_err(|e| format!("Failed to spawn extension '{}': {}", id, e))?;
825
826 let stdin = child
827 .stdin
828 .take()
829 .ok_or_else(|| format!("No stdin for extension '{}'", id))?;
830 let stdout = child
831 .stdout
832 .take()
833 .ok_or_else(|| format!("No stdout for extension '{}'", id))?;
834 if let Some(stderr) = child.stderr.take() {
835 let extension_id = id.to_string();
836 tokio::spawn(async move {
837 const MAX_STDERR_LINE: usize = 8 * 1024;
842 const MAX_STDERR_FORWARD_TOTAL: u64 = 128 * 1024;
843 let mut reader = BufReader::new(stderr);
844 let mut forwarded: u64 = 0;
845 let mut suppressed: u64 = 0;
846 loop {
847 let mut buf: Vec<u8> = Vec::new();
848 let mut limited =
849 tokio::io::AsyncReadExt::take(&mut reader, MAX_STDERR_LINE as u64 + 1);
850 match tokio::io::AsyncBufReadExt::read_until(&mut limited, b'\n', &mut buf)
851 .await
852 {
853 Ok(0) => break,
854 Ok(n) => {
855 if forwarded < MAX_STDERR_FORWARD_TOTAL {
856 forwarded = forwarded.saturating_add(n as u64);
857 let truncated = n > MAX_STDERR_LINE;
858 let end = buf.len().min(MAX_STDERR_LINE);
859 let line = String::from_utf8_lossy(&buf[..end]);
860 let line = line.trim_end_matches(['\r', '\n']);
861 tracing::debug!(
862 extension = %extension_id,
863 truncated,
864 stderr = %line,
865 );
866 } else {
867 suppressed = suppressed.saturating_add(n as u64);
868 }
869 }
870 Err(error) => {
871 tracing::debug!(
872 extension = %extension_id,
873 error = %error,
874 "Failed to read extension stderr",
875 );
876 break;
877 }
878 }
879 }
880 if suppressed > 0 {
881 tracing::debug!(
882 extension = %extension_id,
883 suppressed_bytes = suppressed,
884 "Extension stderr exceeded the forwarding budget; further bytes counted only",
885 );
886 }
887 });
888 }
889
890 let generation = inbox
891 .next_generation
892 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |n| n.checked_add(2))
893 .map_err(|_| "extension lifecycle generation exhausted".to_string())?;
894 let lifecycle = Arc::new(ProcessLifecycle {
895 child: Mutex::new(child),
896 generation: AtomicU64::new(generation),
897 initialized: AtomicBool::new(false),
898 });
899 {
900 let mut slot = inbox.lifecycle.lock().await;
901 if let Some(old) = slot.as_ref() {
902 old.invalidate();
903 }
904 *slot = Some(lifecycle.clone());
905 }
906 inbox.closed.store(false, Ordering::Release);
908 let reader_handle =
909 Self::spawn_reader(stdout, inbox.clone(), id.to_string(), lifecycle.clone());
910
911 let stdin_arc = Arc::new(Mutex::new(stdin));
912 *inbox.inbound_stdin.lock().await = Some(stdin_arc.clone());
915
916 Ok(ProcessState {
917 lifecycle,
918 stdin: stdin_arc,
919 reader_handle,
920 })
921 }
922
923 fn spawn_reader(
928 stdout: ChildStdout,
929 inbox: Arc<Inbox>,
930 extension_id: String,
931 lifecycle: Arc<ProcessLifecycle>,
932 ) -> JoinHandle<()> {
933 tokio::spawn(async move {
934 let mut reader = BufReader::new(stdout);
935 loop {
936 match Self::read_one_frame(&mut reader, &extension_id).await {
937 Ok(Some(value)) => {
938 Self::dispatch_frame(value, &inbox, &extension_id).await;
939 }
940 Ok(None) => {
941 lifecycle.invalidate();
942 tracing::debug!(
943 extension = %extension_id,
944 "Extension stdout closed (EOF); failing pending requests",
945 );
946 inbox.fail_all_pending("transport closed: EOF").await;
947 inbox.notification_sinks.lock().await.clear();
949 return;
950 }
951 Err(error) => {
952 lifecycle.invalidate();
953 tracing::debug!(
954 extension = %extension_id,
955 error = %error,
956 "Extension transport read error",
957 );
958 inbox
959 .fail_all_pending(&format!("transport error: {}", error))
960 .await;
961 inbox.notification_sinks.lock().await.clear();
962 return;
963 }
964 }
965 }
966 })
967 }
968
969 async fn read_one_frame(
973 reader: &mut BufReader<ChildStdout>,
974 extension_id: &str,
975 ) -> Result<Option<Value>, String> {
976 let mut content_length: Option<usize> = None;
977 let mut saw_any_header = false;
978 loop {
979 let mut header_line = String::new();
980 let n = {
983 let mut limited = tokio::io::AsyncReadExt::take(&mut *reader, 1025);
984 limited
985 .read_line(&mut header_line)
986 .await
987 .map_err(|e| format!("Read header error: {}", e))?
988 };
989 if n == 0 {
990 if saw_any_header {
991 return Err("Unexpected EOF while reading response headers".into());
992 }
993 return Ok(None);
994 }
995 saw_any_header = true;
996 if header_line.len() > 1024 {
997 return Err(format!(
998 "Extension '{}' header line too long ({} bytes)",
999 extension_id,
1000 header_line.len()
1001 ));
1002 }
1003 let trimmed = header_line.trim();
1004 if trimmed.is_empty() {
1005 break;
1006 }
1007 if let Some((name, value)) = trimmed.split_once(':') {
1008 if name.trim().eq_ignore_ascii_case("Content-Length") {
1009 content_length = Some(value.trim().parse().map_err(|_| {
1010 format!("Invalid Content-Length value: {:?}", value.trim())
1011 })?);
1012 }
1013 }
1014 }
1015 let content_length = content_length.ok_or_else(|| {
1016 format!(
1017 "Extension '{}' frame missing Content-Length header",
1018 extension_id
1019 )
1020 })?;
1021 const MAX_RESPONSE_SIZE: usize = 4 * 1024 * 1024;
1022 if content_length > MAX_RESPONSE_SIZE {
1023 return Err(format!(
1024 "Extension '{}' frame too large: {} bytes (max {})",
1025 extension_id, content_length, MAX_RESPONSE_SIZE
1026 ));
1027 }
1028 let mut buf = vec![0u8; content_length];
1029 tokio::io::AsyncReadExt::read_exact(reader, &mut buf)
1030 .await
1031 .map_err(|e| format!("Read body error: {}", e))?;
1032 let value: Value =
1033 serde_json::from_slice(&buf).map_err(|e| format!("Parse frame error: {}", e))?;
1034 Ok(Some(value))
1035 }
1036
1037 async fn dispatch_frame(value: Value, inbox: &Arc<Inbox>, extension_id: &str) {
1043 let id_field = value.get("id");
1044 let id_is_present = !matches!(id_field, None | Some(Value::Null));
1045 let method_field = value
1046 .get("method")
1047 .and_then(Value::as_str)
1048 .map(str::to_string);
1049
1050 if id_is_present && method_field.is_some() {
1051 let id = match id_field {
1063 Some(id @ (Value::String(_) | Value::Number(_))) => id.clone(),
1064 _ => {
1065 tracing::warn!(
1066 extension = %extension_id,
1067 frame = %value,
1068 "Discarding inbound request: JSON-RPC id must be a string or a number",
1069 );
1070 return;
1071 }
1072 };
1073 let Some(method) = method_field else { return };
1074 let params = value.get("params").cloned().unwrap_or(Value::Null);
1075 let inbox = inbox.clone();
1076 let extension_id = extension_id.to_string();
1077 tokio::spawn(async move {
1078 let outcome = Self::handle_inbound_request(&inbox, &method, params).await;
1079 let payload = match outcome {
1080 Ok(result) => serde_json::json!({
1081 "jsonrpc": "2.0",
1082 "id": id,
1083 "result": result,
1084 }),
1085 Err((code, message)) => serde_json::json!({
1086 "jsonrpc": "2.0",
1087 "id": id,
1088 "error": {"code": code, "message": message},
1089 }),
1090 };
1091 let stdin_handle = inbox.inbound_stdin.lock().await.clone();
1092 if let Some(stdin) = stdin_handle {
1093 let body = match serde_json::to_string(&payload) {
1094 Ok(s) => s,
1095 Err(error) => {
1096 tracing::warn!(
1097 extension = %extension_id,
1098 error = %error,
1099 "Failed to serialize inbound response",
1100 );
1101 return;
1102 }
1103 };
1104 let frame = format!("Content-Length: {}\r\n\r\n{}", body.len(), body);
1105 let mut stdin = stdin.lock().await;
1106 if let Err(error) = stdin.write_all(frame.as_bytes()).await {
1107 inbox.invalidate_lifecycle().await;
1108 tracing::warn!(
1109 extension = %extension_id,
1110 error = %error,
1111 "Failed to write inbound response",
1112 );
1113 return;
1114 }
1115 if let Err(error) = stdin.flush().await {
1116 inbox.invalidate_lifecycle().await;
1117 tracing::warn!(
1118 extension = %extension_id,
1119 error = %error,
1120 "Failed to flush inbound response",
1121 );
1122 }
1123 } else {
1124 tracing::warn!(
1125 extension = %extension_id,
1126 "No stdin available to reply to inbound request",
1127 );
1128 }
1129 });
1130 return;
1131 }
1132
1133 if id_is_present {
1134 let id = match id_field.and_then(Value::as_u64) {
1139 Some(id) => id,
1140 None => {
1141 tracing::warn!(
1142 extension = %extension_id,
1143 frame = %value,
1144 "Discarding response frame: id does not match any request \
1145 the runtime issued (runtime request ids are always numeric)",
1146 );
1147 return;
1148 }
1149 };
1150 let sender = inbox.pending.lock().await.remove(&id);
1151 match sender {
1152 Some(tx) => {
1153 let payload = if let Some(err) = value.get("error") {
1154 let message = err
1155 .get("message")
1156 .and_then(Value::as_str)
1157 .unwrap_or("unknown extension error")
1158 .to_string();
1159 Err(format!("Extension error: {}", message))
1160 } else {
1161 Ok(value.get("result").cloned().unwrap_or(Value::Null))
1162 };
1163 let _ = tx.send(payload);
1164 }
1165 None => {
1166 tracing::trace!(
1167 extension = %extension_id,
1168 id = id,
1169 "Response with unknown id (no pending request); dropping",
1170 );
1171 }
1172 }
1173 } else if let Some(method) = value.get("method").and_then(Value::as_str) {
1174 let params = value.get("params").cloned().unwrap_or(Value::Null);
1175 let frame = NotificationFrame {
1176 method: method.to_string(),
1177 params,
1178 };
1179 let targets: Vec<(usize, mpsc::Sender<NotificationFrame>)> = {
1183 let sinks = inbox.notification_sinks.lock().await;
1184 sinks.to_vec()
1185 };
1186 if targets.is_empty() {
1187 tracing::trace!(
1188 extension = %extension_id,
1189 method = %method,
1190 "Notification with no active subscribers; dropping",
1191 );
1192 } else {
1193 let mut dead = Vec::new();
1198 for (id, tx) in &targets {
1199 if tx.send(frame.clone()).await.is_err() {
1200 dead.push(*id);
1201 }
1202 }
1203 if !dead.is_empty() {
1204 let mut sinks = inbox.notification_sinks.lock().await;
1205 sinks.retain(|(id, _)| !dead.contains(id));
1206 }
1207 }
1208 } else {
1209 tracing::trace!(
1210 extension = %extension_id,
1211 frame = %value,
1212 "Unrecognized frame; dropping",
1213 );
1214 }
1215 }
1216
1217 pub fn restart_count(&self) -> usize {
1218 self.total_restarts.load(Ordering::Relaxed)
1221 }
1222
1223 pub async fn set_permissions(&self, perms: crate::extensions::permissions::PermissionSet) {
1226 *self.inbox.permissions.write().await = Some(perms);
1227 }
1228
1229 #[allow(clippy::doc_lazy_continuation)]
1237 async fn handle_inbound_request(
1238 inbox: &Arc<Inbox>,
1239 method: &str,
1240 params: Value,
1241 ) -> Result<Value, (i32, String)> {
1242 use crate::extensions::permissions::Permission;
1243 use crate::memory::store::{self, MemoryQuery};
1244
1245 if inbox.exclusive_memory && matches!(method, "memory.append" | "memory.query") {
1246 return Err((
1247 -32000,
1248 "legacy extension memory is disabled by the host-selected backend".into(),
1249 ));
1250 }
1251
1252 match method {
1253 "memory.append" => {
1254 Self::require_permission(inbox, Permission::MemoryWrite, "memory.write").await?;
1255 let namespace = Self::param_str(¶ms, "namespace")?;
1256 Self::require_namespace_matches(inbox, &namespace).await?;
1257 let content = Self::param_str(¶ms, "content")?;
1258 let tags = match params.get("tags") {
1259 None | Some(Value::Null) => Vec::new(),
1260 Some(Value::Array(arr)) => {
1261 let mut out = Vec::with_capacity(arr.len());
1262 for v in arr {
1263 match v.as_str() {
1264 Some(s) => out.push(s.to_string()),
1265 None => {
1266 return Err((
1267 -32602,
1268 "tags must be an array of strings".to_string(),
1269 ))
1270 }
1271 }
1272 }
1273 out
1274 }
1275 _ => return Err((-32602, "tags must be an array of strings".to_string())),
1276 };
1277 let meta = match params.get("meta") {
1278 None | Some(Value::Null) => None,
1279 Some(v) => Some(v.clone()),
1280 };
1281 let record = store::new_record(namespace, content, tags, meta);
1282 let timestamp_ms = record.timestamp_ms;
1283 store::append(&record).map_err(|e| (-32000, e.to_string()))?;
1284 Ok(serde_json::json!({"ok": true, "timestamp_ms": timestamp_ms}))
1285 }
1286 "memory.query" => {
1287 Self::require_permission(inbox, Permission::MemoryRead, "memory.read").await?;
1288 let namespace = Self::param_str(¶ms, "namespace")?;
1289 Self::require_namespace_matches(inbox, &namespace).await?;
1290 let q = MemoryQuery {
1291 content_contains: params
1292 .get("content_contains")
1293 .and_then(Value::as_str)
1294 .map(str::to_string),
1295 tag_prefix: params
1296 .get("tag_prefix")
1297 .and_then(Value::as_str)
1298 .map(str::to_string),
1299 since_ms: params.get("since_ms").and_then(Value::as_u64),
1300 until_ms: params.get("until_ms").and_then(Value::as_u64),
1301 limit: params
1302 .get("limit")
1303 .and_then(Value::as_u64)
1304 .map(|n| n as usize),
1305 };
1306 let records = store::query(&namespace, &q).map_err(|e| (-32000, e.to_string()))?;
1307 Ok(serde_json::json!({"records": records}))
1308 }
1309 "config.get" => {
1310 let key = Self::param_str(¶ms, "key")?;
1311 Self::validate_config_key(&key)?;
1312 let value =
1313 crate::extensions::config_store::read_plugin_config(&inbox.extension_id, &key);
1314 Ok(serde_json::json!({"value": value}))
1315 }
1316 "config.set" => {
1317 Self::require_permission(inbox, Permission::ConfigWrite, "config.write").await?;
1318 let key = Self::param_str(¶ms, "key")?;
1319 Self::validate_config_key(&key)?;
1320 let value = Self::param_str(¶ms, "value")?;
1321 crate::extensions::config_store::write_plugin_config(
1322 &inbox.extension_id,
1323 &key,
1324 &value,
1325 )
1326 .map_err(|e| (-32000, e.to_string()))?;
1327 Ok(serde_json::json!({"ok": true}))
1328 }
1329 "config.subscribe" => {
1330 Self::require_permission(inbox, Permission::ConfigSubscribe, "config.subscribe")
1331 .await?;
1332 Ok(serde_json::json!({"ok": true}))
1336 }
1337 other => Err((-32601, format!("method not found: {other}"))),
1338 }
1339 }
1340
1341 async fn require_permission(
1342 inbox: &Arc<Inbox>,
1343 perm: crate::extensions::permissions::Permission,
1344 wire: &str,
1345 ) -> Result<(), (i32, String)> {
1346 let guard = inbox.permissions.read().await;
1347 match guard.as_ref() {
1348 Some(set) if set.has(perm) => Ok(()),
1349 _ => Err((-32602, format!("permission denied: {wire} required"))),
1350 }
1351 }
1352
1353 async fn require_namespace_matches(
1354 inbox: &Arc<Inbox>,
1355 namespace: &str,
1356 ) -> Result<(), (i32, String)> {
1357 if namespace == inbox.extension_id {
1358 Ok(())
1359 } else {
1360 Err((
1361 -32602,
1362 format!(
1363 "namespace must equal extension id '{}' (got '{}')",
1364 inbox.extension_id, namespace
1365 ),
1366 ))
1367 }
1368 }
1369
1370 fn param_str(params: &Value, name: &str) -> Result<String, (i32, String)> {
1371 params
1372 .get(name)
1373 .and_then(Value::as_str)
1374 .map(str::to_string)
1375 .ok_or_else(|| (-32602, format!("missing or invalid '{name}' parameter")))
1376 }
1377
1378 fn validate_config_key(key: &str) -> Result<(), (i32, String)> {
1379 let trimmed = key.trim();
1380 if trimmed.is_empty() {
1381 return Err((-32602, "config key must be non-empty".to_string()));
1382 }
1383 if trimmed.contains('.') || trimmed.contains('/') || trimmed.contains(' ') {
1384 return Err((
1385 -32602,
1386 "config key must not contain dots, slashes, or spaces".to_string(),
1387 ));
1388 }
1389 Ok(())
1390 }
1391
1392 pub async fn initialize(
1393 &self,
1394 plugin_root: Option<PathBuf>,
1395 config: Value,
1396 ) -> Result<InitializeCapabilitiesResult, String> {
1397 *self.initialize_config.lock().await = config.clone();
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: plugin_root
1405 .or_else(|| self.cwd.clone())
1406 .map(|path| path.to_string_lossy().to_string()),
1407 config,
1408 };
1409 let value = self
1410 .call_no_restart(
1411 "initialize",
1412 serde_json::to_value(params).map_err(|e| e.to_string())?,
1413 )
1414 .await?;
1415 Self::parse_initialize_result(&self.id, value)
1416 }
1417
1418 fn parse_initialize_result(
1419 id: &str,
1420 value: Value,
1421 ) -> Result<InitializeCapabilitiesResult, String> {
1422 let result: InitializeResult = serde_json::from_value(value)
1423 .map_err(|e| format!("Invalid initialize response from extension '{}': {}", id, e))?;
1424 if result.protocol_version != CURRENT_EXTENSION_PROTOCOL_VERSION {
1425 return Err(format!(
1426 "Extension '{}' initialize returned unsupported protocol_version {} (supported: {})",
1427 id, result.protocol_version, CURRENT_EXTENSION_PROTOCOL_VERSION,
1428 ));
1429 }
1430 Self::validate_registered_tool_specs(id, &result.capabilities.tools)?;
1431 Self::validate_registered_provider_specs(id, &result.capabilities.providers)?;
1432 Ok(InitializeCapabilitiesResult {
1433 tools: result.capabilities.tools,
1434 providers: result.capabilities.providers,
1435 capabilities: result.capabilities.capabilities,
1436 })
1437 }
1438
1439 fn validate_registered_tool_specs(
1440 id: &str,
1441 tools: &[RegisteredExtensionToolSpec],
1442 ) -> Result<(), String> {
1443 use crate::extensions::validation::{validate_id_segment, IdValidationError};
1444 let mut names = HashSet::new();
1445 for tool in tools {
1446 let name = tool.name.trim();
1447 if let Err(err) = validate_id_segment(name) {
1448 return Err(match err {
1449 IdValidationError::Empty => format!(
1450 "Extension '{}' registered a tool with an empty tool name",
1451 id
1452 ),
1453 IdValidationError::ContainsReserved { ch } => format!(
1454 "Extension '{}' registered tool '{}' with invalid tool name: '{}' is reserved",
1455 id, name, ch
1456 ),
1457 IdValidationError::TooLong { len, max } => format!(
1458 "Extension '{}' registered tool '{}' with invalid tool name: must be at most {} chars (got {})",
1459 id, name, max, len
1460 ),
1461 IdValidationError::ContainsWhitespace => format!(
1462 "Extension '{}' registered tool '{}' with invalid tool name: must not contain whitespace",
1463 id, name
1464 ),
1465 IdValidationError::ContainsControl { ch } => format!(
1466 "Extension '{}' registered tool '{}' with invalid tool name: contains control character U+{:04X}",
1467 id, name, ch as u32
1468 ),
1469 });
1470 }
1471 if !names.insert(name.to_string()) {
1472 return Err(format!(
1473 "Extension '{}' registered duplicate tool name '{}'",
1474 id, name
1475 ));
1476 }
1477 if tool.description.trim().is_empty() {
1478 return Err(format!(
1479 "Extension '{}' registered tool '{}' with an empty description",
1480 id, name,
1481 ));
1482 }
1483 if !tool.input_schema.is_object() {
1484 return Err(format!(
1485 "Extension '{}' registered tool '{}' with invalid input_schema: input_schema must be a JSON object",
1486 id, name,
1487 ));
1488 }
1489 }
1490 Ok(())
1491 }
1492
1493 fn validate_registered_provider_specs(
1494 id: &str,
1495 providers: &[RegisteredProviderSpec],
1496 ) -> Result<(), String> {
1497 use crate::extensions::validation::{validate_id_segment, IdValidationError};
1498 for provider in providers {
1499 let provider_id = provider.id.trim();
1500 match validate_id_segment(provider_id) {
1501 Ok(()) => {
1502 if !Self::is_safe_provider_id(provider_id) {
1503 return Err(format!(
1504 "Extension '{}' registered provider '{}' with invalid provider id",
1505 id, provider_id
1506 ));
1507 }
1508 }
1509 Err(IdValidationError::Empty) => {
1510 return Err(format!(
1511 "Extension '{}' registered provider with empty provider id",
1512 id
1513 ));
1514 }
1515 Err(err) => {
1516 return Err(format!(
1517 "Extension '{}' registered provider '{}' with invalid provider id: {}",
1518 id, provider_id, err
1519 ));
1520 }
1521 }
1522 if provider.display_name.trim().is_empty() {
1523 return Err(format!(
1524 "Extension '{}' registered provider '{}' with empty display_name",
1525 id, provider_id,
1526 ));
1527 }
1528 if provider.description.trim().is_empty() {
1529 return Err(format!(
1530 "Extension '{}' registered provider '{}' with empty description",
1531 id, provider_id,
1532 ));
1533 }
1534 if provider.models.is_empty() {
1535 return Err(format!(
1536 "Extension '{}' registered provider '{}' must declare at least one model",
1537 id, provider_id,
1538 ));
1539 }
1540 let mut model_ids = HashSet::new();
1541 for model in &provider.models {
1542 let model_id = model.id.trim();
1543 if let Err(err) = validate_id_segment(model_id) {
1544 return Err(match err {
1545 IdValidationError::Empty => format!(
1546 "Extension '{}' registered provider '{}' with empty model id",
1547 id, provider_id
1548 ),
1549 IdValidationError::ContainsReserved { ch } => format!(
1550 "Extension '{}' registered provider '{}' with invalid model id '{}': '{}' is reserved",
1551 id, provider_id, model_id, ch
1552 ),
1553 IdValidationError::TooLong { len, max } => format!(
1554 "Extension '{}' registered provider '{}' with invalid model id '{}': must be at most {} chars (got {})",
1555 id, provider_id, model_id, max, len
1556 ),
1557 IdValidationError::ContainsWhitespace => format!(
1558 "Extension '{}' registered provider '{}' with invalid model id '{}': must not contain whitespace",
1559 id, provider_id, model_id
1560 ),
1561 IdValidationError::ContainsControl { ch } => format!(
1562 "Extension '{}' registered provider '{}' with invalid model id '{}': contains control character U+{:04X}",
1563 id, provider_id, model_id, ch as u32
1564 ),
1565 });
1566 }
1567 if !model_ids.insert(model_id.to_string()) {
1568 return Err(format!(
1569 "Extension '{}' registered provider '{}' with duplicate model id '{}'",
1570 id, provider_id, model_id,
1571 ));
1572 }
1573 }
1574 if let Some(config_schema) = &provider.config_schema {
1575 if !config_schema.is_object() {
1576 return Err(format!(
1577 "Extension '{}' registered provider '{}' with invalid config_schema: config_schema must be a JSON object",
1578 id, provider_id,
1579 ));
1580 }
1581 }
1582 }
1583 Ok(())
1584 }
1585
1586 fn is_safe_provider_id(id: &str) -> bool {
1587 !id.is_empty()
1588 && !id.contains(':')
1589 && id
1590 .chars()
1591 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
1592 }
1593
1594 #[doc(hidden)]
1595 pub async fn initialize_for_test(&self, plugin_root: Option<PathBuf>) -> Result<(), String> {
1596 self.initialize(plugin_root, Value::Object(Default::default()))
1597 .await
1598 .map(|_| ())
1599 }
1600
1601 async fn restart_locked(&self, state: &mut Option<ProcessState>) -> Result<(), String> {
1602 self.inbox.invalidate_lifecycle().await;
1603 let attempted = self.restart_count.fetch_add(1, Ordering::Relaxed) + 1;
1604 self.total_restarts.fetch_add(1, Ordering::Relaxed);
1605 let max_attempts = self.restart_policy.max_attempts;
1606 if attempted > max_attempts as usize {
1607 *state = None;
1608 return Err(format!(
1609 "Extension '{}' exceeded restart limit ({})",
1610 self.id, max_attempts,
1611 ));
1612 }
1613
1614 if let Some(old) = state.take() {
1615 old.reader_handle.abort();
1616 old.lifecycle.invalidate();
1617 let _ = old.lifecycle.child.lock().await.kill().await;
1618 }
1619 self.inbox
1621 .fail_all_pending("transport closed: process restarting")
1622 .await;
1623
1624 let delay = self
1625 .restart_policy
1626 .delay_for_attempt(attempted as u32)
1627 .unwrap_or_default();
1628
1629 tracing::warn!(
1630 extension = %self.id,
1631 attempt = attempted,
1632 max_attempts = max_attempts,
1633 delay_ms = delay.as_millis() as u64,
1634 "Restarting extension process after transport failure",
1635 );
1636
1637 if !delay.is_zero() {
1638 tokio::time::sleep(delay).await;
1639 }
1640
1641 *state = Some(
1642 Self::spawn_state(
1643 &self.id,
1644 &self.command,
1645 &self.args,
1646 self.cwd.as_ref(),
1647 self.inbox.clone(),
1648 )
1649 .await?,
1650 );
1651 self.initialize_locked(state).await?;
1652 Ok(())
1659 }
1660
1661 async fn initialize_locked(&self, state: &mut Option<ProcessState>) -> Result<(), String> {
1662 let config = self.initialize_config.lock().await.clone();
1663 let params = InitializeParams {
1664 synaps_version: env!("CARGO_PKG_VERSION"),
1665 extension_protocol_version: CURRENT_EXTENSION_PROTOCOL_VERSION,
1666 plugin_id: self.id.clone(),
1667 plugin_root: self
1668 .cwd
1669 .clone()
1670 .map(|path| path.to_string_lossy().to_string()),
1671 config,
1672 };
1673 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1674 let value = tokio::time::timeout(
1675 std::time::Duration::from_secs(10),
1676 self.call_once_locked(
1677 state.as_mut().expect("state should exist for initialize"),
1678 "initialize",
1679 serde_json::to_value(params).map_err(|e| e.to_string())?,
1680 id,
1681 ),
1682 )
1683 .await
1684 .map_err(|_| format!("Extension '{}' initialize timed out after 10s", self.id))??;
1685 let result = Self::parse_initialize_result(&self.id, value).map(|_| ());
1686 if let Some(state) = state.as_ref() {
1687 if result.is_ok() {
1688 state.lifecycle.initialized.store(true, Ordering::Release);
1689 } else {
1690 state.lifecycle.invalidate();
1691 }
1692 }
1693 result
1694 }
1695
1696 async fn call_once_locked(
1700 &self,
1701 state: &mut ProcessState,
1702 method: &str,
1703 params: Value,
1704 id: u64,
1705 ) -> Result<Value, String> {
1706 let body = serde_json::to_string(&JsonRpcRequest {
1707 jsonrpc: "2.0",
1708 method: method.to_string(),
1709 params,
1710 id,
1711 })
1712 .map_err(|e| format!("Serialize error: {}", e))?;
1713
1714 let (tx, rx) = oneshot::channel::<Result<Value, String>>();
1715 if self.inbox.closed.load(std::sync::atomic::Ordering::Acquire) {
1718 return Err("transport closed: inbox is shut down".to_string());
1719 }
1720
1721 self.inbox.pending.lock().await.insert(id, tx);
1724
1725 if self.inbox.closed.load(std::sync::atomic::Ordering::Acquire) {
1728 self.inbox.pending.lock().await.remove(&id);
1729 return Err("transport closed: inbox shut down during registration".to_string());
1730 }
1731
1732 let frame = format!("Content-Length: {}\r\n\r\n{}", body.len(), body);
1733 let write_result = {
1734 let mut stdin = state.stdin.lock().await;
1735 match stdin.write_all(frame.as_bytes()).await {
1736 Ok(()) => stdin.flush().await,
1737 Err(e) => Err(e),
1738 }
1739 };
1740 if let Err(e) = write_result {
1741 state.lifecycle.invalidate();
1742 self.inbox.pending.lock().await.remove(&id);
1744 return Err(format!("Write error: {}", e));
1745 }
1746
1747 match rx.await {
1748 Ok(payload) => payload,
1749 Err(_) => {
1750 state.lifecycle.invalidate();
1751 self.inbox.pending.lock().await.remove(&id);
1756 Err("transport closed: response channel dropped".to_string())
1757 }
1758 }
1759 }
1760
1761 async fn call_no_restart(&self, method: &str, params: Value) -> Result<Value, String> {
1762 let _call_guard = self.call_lock.lock().await;
1763 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1764 let mut state_guard = self.state.lock().await;
1765 if state_guard.is_none() {
1766 *state_guard = Some(
1767 Self::spawn_state(
1768 &self.id,
1769 &self.command,
1770 &self.args,
1771 self.cwd.as_ref(),
1772 self.inbox.clone(),
1773 )
1774 .await?,
1775 );
1776 }
1777 let state = state_guard.as_mut().expect("state should exist");
1778 let result = self.call_once_locked(state, method, params, id).await;
1779 if method == "initialize" {
1780 match result.as_ref() {
1783 Ok(value) if Self::parse_initialize_result(&self.id, value.clone()).is_ok() => {
1784 state.lifecycle.initialized.store(true, Ordering::Release);
1785 }
1786 _ => state.lifecycle.invalidate(),
1787 }
1788 }
1789 result
1790 }
1791
1792 async fn call_no_spawn(&self, method: &str, params: Value) -> Result<Value, String> {
1796 let _call_guard = self.call_lock.lock().await;
1797 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1798 let mut state_guard = self.state.lock().await;
1799 let Some(state) = state_guard.as_mut() else {
1800 return Err(format!(
1801 "Extension '{}' process is not running; '{}' refused (no respawn)",
1802 self.id, method
1803 ));
1804 };
1805 self.call_once_locked(state, method, params, id).await
1806 }
1807
1808 pub(crate) async fn call_tool_once_no_spawn(
1814 &self,
1815 name: &str,
1816 input: Value,
1817 ) -> Result<Value, String> {
1818 let fut = async {
1819 let _call_guard = self.call_lock.lock().await;
1820 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1821 let mut state = {
1826 let mut state_guard = self.state.lock().await;
1827 state_guard.take().ok_or_else(|| {
1828 format!(
1829 "Extension '{}' process is not running; leased call refused (no respawn)",
1830 self.id
1831 )
1832 })?
1833 };
1834 let result = self
1835 .call_once_locked(
1836 &mut state,
1837 "tool.call",
1838 serde_json::json!({"name": name, "input": input}),
1839 id,
1840 )
1841 .await;
1842 if !self.inbox.closed.load(std::sync::atomic::Ordering::Acquire) {
1843 *self.state.lock().await = Some(state);
1844 } else {
1845 state.reader_handle.abort();
1846 state.lifecycle.invalidate();
1847 let _ = state.lifecycle.child.lock().await.kill().await;
1848 }
1849 result
1850 };
1851 match tokio::time::timeout(std::time::Duration::from_secs(120), fut).await {
1852 Ok(result) => result,
1853 Err(_) => Err(format!(
1854 "Extension '{}' leased tool.call timed out after 120s",
1855 self.id
1856 )),
1857 }
1858 }
1859
1860 async fn call(&self, method: &str, params: Value) -> Result<Value, String> {
1861 let timeout_secs = if method == "tool.call" { 120 } else { 30 };
1862 let id_str = self.id.clone();
1863 let method_str = method.to_string();
1864
1865 let result = tokio::time::timeout(
1866 std::time::Duration::from_secs(timeout_secs),
1867 self.call_inner(method, params),
1868 )
1869 .await;
1870
1871 match result {
1872 Ok(inner) => inner,
1873 Err(_) => Err(format!(
1874 "Extension '{}' method '{}' timed out after {}s",
1875 id_str, method_str, timeout_secs
1876 )),
1877 }
1878 }
1879
1880 async fn call_inner(&self, method: &str, params: Value) -> Result<Value, String> {
1881 let _call_guard = self.call_lock.lock().await;
1882 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
1883 let mut state_guard = self.state.lock().await;
1884 if state_guard.is_none() {
1885 self.restart_locked(&mut state_guard).await?;
1886 }
1887
1888 let result = self
1889 .call_once_locked(
1890 state_guard
1891 .as_mut()
1892 .expect("state should exist after restart"),
1893 method,
1894 params.clone(),
1895 id,
1896 )
1897 .await;
1898
1899 match result {
1900 Ok(value) => {
1901 self.restart_count.store(0, Ordering::Relaxed);
1905 Ok(value)
1906 }
1907 Err(first_error) => {
1908 self.restart_locked(&mut state_guard).await?;
1909 let retry_id = self.next_id.fetch_add(1, Ordering::Relaxed);
1910 self.call_once_locked(
1911 state_guard
1912 .as_mut()
1913 .expect("state should exist after restart"),
1914 method,
1915 params,
1916 retry_id,
1917 )
1918 .await
1919 .inspect(|_| {
1920 self.restart_count.store(0, Ordering::Relaxed);
1922 })
1923 .map_err(|retry_error| {
1924 format!(
1925 "{}; retry after restart failed: {}",
1926 first_error, retry_error
1927 )
1928 })
1929 }
1930 }
1931 }
1932
1933 #[doc(hidden)]
1951 pub async fn subscribe_notifications(&self) -> (usize, mpsc::Receiver<NotificationFrame>) {
1952 let (tx, rx) = mpsc::channel(NOTIFICATION_QUEUE_CAPACITY);
1953 let id = self
1954 .inbox
1955 .next_sink_id
1956 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1957 let mut sinks = self.inbox.notification_sinks.lock().await;
1958 sinks.retain(|(_, tx)| !tx.is_closed());
1962 sinks.push((id, tx));
1963 (id, rx)
1964 }
1965
1966 #[doc(hidden)]
1971 pub async fn unsubscribe_notifications(&self, id: usize) {
1972 let mut sinks = self.inbox.notification_sinks.lock().await;
1973 sinks.retain(|(sub_id, tx)| *sub_id != id && !tx.is_closed());
1974 }
1975
1976 pub(crate) async fn forward_invoke_command_frame(
1995 extension_id: &str,
1996 request_id: &str,
1997 sink: &crate::extensions::invoke_output::InvokeEventSink,
1998 sink_open: &mut bool,
1999 frame: NotificationFrame,
2000 ) -> bool {
2001 use crate::extensions::commands::parse_command_output;
2002 use crate::extensions::runtime::InvokeCommandEvent;
2003 use crate::extensions::tasks::{is_task_method, parse_task_event};
2004
2005 let mut saw_done = false;
2006 if frame.method == "command.output" {
2007 match parse_command_output(&frame.params) {
2008 Ok(parsed) if parsed.request_id == request_id => {
2009 if matches!(
2010 parsed.event,
2011 crate::extensions::commands::CommandOutputEvent::Done
2012 ) {
2013 saw_done = true;
2014 }
2015 if *sink_open
2016 && sink
2017 .send(InvokeCommandEvent::Output(parsed.event))
2018 .await
2019 .is_err()
2020 {
2021 *sink_open = false;
2022 }
2023 }
2024 Ok(_) => {
2025 tracing::trace!(
2027 extension = %extension_id,
2028 "Ignoring command.output for unrelated request_id",
2029 );
2030 }
2031 Err(error) => {
2032 tracing::warn!(
2033 extension = %extension_id,
2034 error = %error,
2035 params = %frame.params,
2036 "Skipping malformed command.output notification",
2037 );
2038 }
2039 }
2040 } else if is_task_method(&frame.method) {
2041 match parse_task_event(&frame.method, &frame.params) {
2042 Ok(event) => {
2043 if *sink_open && sink.send(InvokeCommandEvent::Task(event)).await.is_err() {
2044 *sink_open = false;
2045 }
2046 }
2047 Err(error) => {
2048 tracing::warn!(
2049 extension = %extension_id,
2050 method = %frame.method,
2051 error = %error,
2052 params = %frame.params,
2053 "Skipping malformed task notification",
2054 );
2055 }
2056 }
2057 } else {
2058 tracing::trace!(
2059 extension = %extension_id,
2060 method = %frame.method,
2061 "Ignoring non-command/task notification during command.invoke",
2062 );
2063 }
2064 saw_done
2065 }
2066
2067 async fn forward_provider_stream_frame(
2077 extension_id: &str,
2078 sink: &mpsc::Sender<ProviderStreamEvent>,
2079 sink_open: &mut bool,
2080 frame: NotificationFrame,
2081 ) {
2082 if frame.method != "provider.stream.event" {
2083 tracing::trace!(
2084 extension = %extension_id,
2085 method = %frame.method,
2086 "Ignoring non-stream notification during provider.stream",
2087 );
2088 return;
2089 }
2090 match parse_provider_stream_event(&frame.params) {
2091 Ok(event) => {
2092 if *sink_open && sink.send(event).await.is_err() {
2093 *sink_open = false;
2094 }
2095 }
2096 Err(error) => {
2097 tracing::warn!(
2098 extension = %extension_id,
2099 error = %error,
2100 params = %frame.params,
2101 "Skipping malformed provider.stream.event notification",
2102 );
2103 }
2104 }
2105 }
2106}
2107
2108#[async_trait]
2109impl ExtensionHandler for ProcessExtension {
2110 fn id(&self) -> &str {
2111 &self.id
2112 }
2113
2114 fn lifecycle_snapshot(&self) -> Option<ExtensionLifecycle> {
2115 let lifecycle = self.inbox.lifecycle.try_lock().ok()?.clone()?;
2116 lifecycle.snapshot(self.total_restarts.load(Ordering::Relaxed) > 0)
2117 }
2118
2119 async fn call_tool(&self, name: &str, input: Value) -> Result<Value, String> {
2120 self.call(
2121 "tool.call",
2122 serde_json::json!({
2123 "name": name,
2124 "input": input,
2125 }),
2126 )
2127 .await
2128 }
2129
2130 async fn provider_complete(
2131 &self,
2132 params: ProviderCompleteParams,
2133 ) -> Result<ProviderCompleteResult, String> {
2134 let value = tokio::time::timeout(
2135 std::time::Duration::from_secs(60),
2136 self.call(
2137 "provider.complete",
2138 serde_json::to_value(params).map_err(|e| e.to_string())?,
2139 ),
2140 )
2141 .await
2142 .map_err(|_| format!("Extension '{}' provider.complete timed out", self.id))??;
2143 let result: ProviderCompleteResult = serde_json::from_value(value).map_err(|e| {
2144 format!(
2145 "Invalid provider.complete response from extension '{}': {}",
2146 self.id, e
2147 )
2148 })?;
2149 if result.content.is_empty() {
2150 return Err(format!(
2151 "Extension '{}' provider.complete returned empty content",
2152 self.id
2153 ));
2154 }
2155 Ok(result)
2156 }
2157
2158 async fn provider_stream(
2159 &self,
2160 params: ProviderCompleteParams,
2161 sink: tokio::sync::mpsc::Sender<ProviderStreamEvent>,
2162 ) -> Result<ProviderCompleteResult, String> {
2163 let (sub_id, mut rx) = self.subscribe_notifications().await;
2166 let params_value = serde_json::to_value(params).map_err(|e| e.to_string())?;
2167
2168 let extension_id = self.id.clone();
2169 let stream_future = async {
2170 let mut call_fut = Box::pin(self.call("provider.stream", params_value));
2171 let mut sink_open = true;
2172 let response = loop {
2173 tokio::select! {
2174 response = &mut call_fut => break response,
2175 Some(frame) = rx.recv() => {
2176 Self::forward_provider_stream_frame(
2177 &extension_id, &sink, &mut sink_open, frame,
2178 ).await;
2179 }
2180 }
2181 };
2182 self.unsubscribe_notifications(sub_id).await;
2187 while let Some(frame) = rx.recv().await {
2188 Self::forward_provider_stream_frame(&extension_id, &sink, &mut sink_open, frame)
2189 .await;
2190 }
2191 response
2192 };
2193
2194 let outcome = tokio::time::timeout(std::time::Duration::from_secs(60), stream_future).await;
2195
2196 self.unsubscribe_notifications(sub_id).await;
2199
2200 let value =
2201 outcome.map_err(|_| format!("Extension '{}' provider.stream timed out", self.id))??;
2202
2203 let result: ProviderCompleteResult = serde_json::from_value(value).map_err(|e| {
2204 format!(
2205 "Invalid provider.stream response from extension '{}': {}",
2206 self.id, e
2207 )
2208 })?;
2209 Ok(result)
2212 }
2213
2214 async fn invoke_command(
2215 &self,
2216 command: &str,
2217 args: Vec<String>,
2218 request_id: &str,
2219 sink: crate::extensions::invoke_output::InvokeEventSink,
2220 ) -> Result<Value, String> {
2221 let (sub_id, mut rx) = self.subscribe_notifications().await;
2223 let params = serde_json::json!({
2224 "command": command,
2225 "args": args,
2226 "request_id": request_id,
2227 });
2228
2229 let extension_id = self.id.clone();
2230 let request_id_owned = request_id.to_string();
2231 let invoke_future = async {
2232 let mut call_fut = Box::pin(self.call("command.invoke", params));
2233 let mut sink_open = true;
2234 let response = loop {
2235 tokio::select! {
2236 response = &mut call_fut => break response,
2237 Some(frame) = rx.recv() => {
2238 let _ = Self::forward_invoke_command_frame(
2244 &extension_id, &request_id_owned, &sink, &mut sink_open, frame,
2245 ).await;
2246 }
2247 }
2248 };
2249 self.unsubscribe_notifications(sub_id).await;
2253 while let Ok(frame) = rx.try_recv() {
2254 let _ = Self::forward_invoke_command_frame(
2255 &extension_id,
2256 &request_id_owned,
2257 &sink,
2258 &mut sink_open,
2259 frame,
2260 )
2261 .await;
2262 }
2263 response
2264 };
2265
2266 let outcome =
2267 tokio::time::timeout(std::time::Duration::from_secs(120), invoke_future).await;
2268
2269 self.unsubscribe_notifications(sub_id).await;
2274
2275 outcome.map_err(|_| format!("Extension '{}' command.invoke timed out", self.id))?
2276 }
2277
2278 async fn handle(&self, event: &HookEvent) -> HookResult {
2279 let params = serde_json::to_value(event).unwrap_or(Value::Null);
2280 match tokio::time::timeout(
2281 std::time::Duration::from_secs(5),
2282 self.call("hook.handle", params),
2283 )
2284 .await
2285 {
2286 Ok(Ok(value)) => match serde_json::from_value(value.clone()) {
2287 Ok(result) => result,
2288 Err(error) => {
2289 tracing::warn!(
2290 extension = %self.id,
2291 error = %error,
2292 response = %value,
2293 "Extension hook handler returned invalid result",
2294 );
2295 match value.get("action").and_then(Value::as_str) {
2296 Some("modify") => HookResult::Block {
2299 reason: "Extension returned malformed modify result".to_string(),
2300 },
2301 Some("replace") => HookResult::Continue,
2306 _ => HookResult::Continue,
2307 }
2308 }
2309 },
2310 Ok(Err(e)) => {
2311 tracing::warn!(
2312 extension = %self.id,
2313 error = %e,
2314 "Extension hook handler failed — continuing",
2315 );
2316 HookResult::Continue
2317 }
2318 Err(_) => {
2319 tracing::warn!(
2320 extension = %self.id,
2321 timeout_secs = 5,
2322 "Extension hook handler timed out — continuing",
2323 );
2324 HookResult::Continue
2325 }
2326 }
2327 }
2328
2329 async fn get_info(&self) -> Result<crate::extensions::info::PluginInfo, String> {
2330 let value = tokio::time::timeout(
2331 std::time::Duration::from_secs(5),
2332 self.call("info.get", Value::Null),
2333 )
2334 .await
2335 .map_err(|_| format!("Extension '{}' info.get timed out", self.id))??;
2336 serde_json::from_value(value).map_err(|e| {
2337 format!(
2338 "Invalid info.get response from extension '{}': {}",
2339 self.id, e
2340 )
2341 })
2342 }
2343
2344 async fn sidecar_spawn_args(&self) -> Result<crate::sidecar::spawn::SidecarSpawnArgs, String> {
2345 let value = tokio::time::timeout(
2346 std::time::Duration::from_secs(5),
2347 self.call("sidecar.spawn_args", Value::Null),
2348 )
2349 .await
2350 .map_err(|_| format!("Extension '{}' sidecar.spawn_args timed out", self.id))??;
2351 serde_json::from_value(value).map_err(|e| {
2352 format!(
2353 "Invalid sidecar.spawn_args response from extension '{}': {}",
2354 self.id, e
2355 )
2356 })
2357 }
2358
2359 async fn settings_editor_open(&self, category: &str, field: &str) -> Result<Value, String> {
2360 let params = crate::extensions::settings_editor::SettingsEditorOpenParams {
2361 category: category.to_string(),
2362 field: field.to_string(),
2363 };
2364 tokio::time::timeout(
2365 std::time::Duration::from_secs(5),
2366 self.call(
2367 "settings.editor.open",
2368 serde_json::to_value(params).map_err(|e| e.to_string())?,
2369 ),
2370 )
2371 .await
2372 .map_err(|_| format!("Extension '{}' settings.editor.open timed out", self.id))?
2373 }
2374
2375 async fn settings_editor_key(
2376 &self,
2377 category: &str,
2378 field: &str,
2379 key: &str,
2380 ) -> Result<Value, String> {
2381 let mut params = serde_json::to_value(
2382 crate::extensions::settings_editor::SettingsEditorKeyParams {
2383 key: key.to_string(),
2384 },
2385 )
2386 .map_err(|e| e.to_string())?;
2387 if let Some(obj) = params.as_object_mut() {
2388 obj.insert("category".to_string(), Value::String(category.to_string()));
2389 obj.insert("field".to_string(), Value::String(field.to_string()));
2390 }
2391 tokio::time::timeout(
2392 std::time::Duration::from_secs(5),
2393 self.call("settings.editor.key", params),
2394 )
2395 .await
2396 .map_err(|_| format!("Extension '{}' settings.editor.key timed out", self.id))?
2397 }
2398
2399 async fn settings_editor_commit(
2400 &self,
2401 category: &str,
2402 field: &str,
2403 value: Value,
2404 ) -> Result<Value, String> {
2405 let params = serde_json::json!({
2406 "category": category,
2407 "field": field,
2408 "value": value,
2409 });
2410 tokio::time::timeout(
2411 std::time::Duration::from_secs(5),
2412 self.call("settings.editor.commit", params),
2413 )
2414 .await
2415 .map_err(|_| format!("Extension '{}' settings.editor.commit timed out", self.id))?
2416 }
2417
2418 async fn shutdown(&self) {
2419 self.inbox.invalidate_lifecycle().await;
2420 let _ = tokio::time::timeout(
2426 std::time::Duration::from_millis(500),
2427 self.call_no_spawn("shutdown", Value::Null),
2428 )
2429 .await;
2430
2431 tokio::time::sleep(std::time::Duration::from_millis(500)).await;
2432 let mut state_guard = self.state.lock().await;
2433 if let Some(state) = state_guard.take() {
2434 state.reader_handle.abort();
2435 state.lifecycle.invalidate();
2436 let _ = state.lifecycle.child.lock().await.kill().await;
2437 }
2438 self.inbox.notification_sinks.lock().await.clear();
2440 self.inbox
2441 .fail_all_pending("transport closed: extension shutdown")
2442 .await;
2443 }
2444
2445 async fn subscribe_notifications(
2446 &self,
2447 ) -> (usize, tokio::sync::mpsc::Receiver<NotificationFrame>) {
2448 ProcessExtension::subscribe_notifications(self).await
2449 }
2450
2451 async fn restart_count(&self) -> usize {
2452 self.restart_count()
2453 }
2454
2455 async fn health(&self) -> ExtensionHealth {
2456 let consecutive = self.restart_count.load(Ordering::Relaxed);
2460 let lifetime = self.total_restarts.load(Ordering::Relaxed);
2461 let max = self.restart_policy.max_attempts as usize;
2462 if consecutive >= max {
2463 ExtensionHealth::Failed
2464 } else if lifetime > 0 {
2465 let state_alive = self.state.try_lock().map(|g| g.is_some()).unwrap_or(true);
2469 if state_alive {
2470 ExtensionHealth::Degraded
2471 } else {
2472 ExtensionHealth::Restarting
2473 }
2474 } else {
2475 ExtensionHealth::Running
2476 }
2477 }
2478}
2479
2480#[cfg(test)]
2481mod exclusive_memory_tests {
2482 use super::*;
2483 #[tokio::test]
2484 async fn exclusive_inbox_refuses_legacy_memory_without_writes() {
2485 let mut inbox = Inbox::new("fixture".into());
2486 inbox.exclusive_memory = true;
2487 let inbox = Arc::new(inbox);
2488 for method in ["memory.append", "memory.query"] {
2489 let result = ProcessExtension::handle_inbound_request(
2490 &inbox,
2491 method,
2492 serde_json::json!({"namespace":"fixture","content":"must not persist"}),
2493 )
2494 .await;
2495 assert!(result.unwrap_err().1.contains("disabled"));
2496 }
2497 }
2498}
2499
2500#[cfg(test)]
2501mod stream_event_tests {
2502 use super::*;
2503 use serde_json::json;
2504
2505 #[test]
2506 fn parses_text_delta_with_delta_key() {
2507 let v = json!({"type": "text", "delta": "hi"});
2508 assert_eq!(
2509 parse_provider_stream_event(&v).unwrap(),
2510 ProviderStreamEvent::TextDelta { text: "hi".into() }
2511 );
2512 }
2513
2514 #[test]
2515 fn parses_text_delta_with_text_key() {
2516 let v = json!({"type": "text", "text": "hi"});
2517 assert_eq!(
2518 parse_provider_stream_event(&v).unwrap(),
2519 ProviderStreamEvent::TextDelta { text: "hi".into() }
2520 );
2521 }
2522
2523 #[test]
2524 fn parses_thinking_delta() {
2525 let v = json!({"type": "thinking", "delta": "hmm"});
2526 assert_eq!(
2527 parse_provider_stream_event(&v).unwrap(),
2528 ProviderStreamEvent::ThinkingDelta { text: "hmm".into() }
2529 );
2530 let v2 = json!({"type": "thinking", "text": "hmm"});
2531 assert_eq!(
2532 parse_provider_stream_event(&v2).unwrap(),
2533 ProviderStreamEvent::ThinkingDelta { text: "hmm".into() }
2534 );
2535 }
2536
2537 #[test]
2538 fn parses_tool_use() {
2539 let v = json!({
2540 "type": "tool_use",
2541 "id": "t1",
2542 "name": "echo",
2543 "input": {"x": 1}
2544 });
2545 assert_eq!(
2546 parse_provider_stream_event(&v).unwrap(),
2547 ProviderStreamEvent::ToolUse {
2548 id: "t1".into(),
2549 name: "echo".into(),
2550 input: json!({"x": 1}),
2551 }
2552 );
2553 }
2554
2555 #[test]
2556 fn tool_use_input_defaults_to_empty_object() {
2557 let v = json!({"type": "tool_use", "id": "t1", "name": "echo"});
2558 assert_eq!(
2559 parse_provider_stream_event(&v).unwrap(),
2560 ProviderStreamEvent::ToolUse {
2561 id: "t1".into(),
2562 name: "echo".into(),
2563 input: json!({}),
2564 }
2565 );
2566 }
2567
2568 #[test]
2569 fn parses_usage_strips_type() {
2570 let v = json!({"type": "usage", "input_tokens": 5, "output_tokens": 7});
2571 assert_eq!(
2572 parse_provider_stream_event(&v).unwrap(),
2573 ProviderStreamEvent::Usage {
2574 usage: json!({"input_tokens": 5, "output_tokens": 7})
2575 }
2576 );
2577 }
2578
2579 #[test]
2580 fn parses_error() {
2581 let v = json!({"type": "error", "message": "boom"});
2582 assert_eq!(
2583 parse_provider_stream_event(&v).unwrap(),
2584 ProviderStreamEvent::Error {
2585 message: "boom".into()
2586 }
2587 );
2588 }
2589
2590 #[test]
2591 fn parses_done() {
2592 let v = json!({"type": "done"});
2593 assert_eq!(
2594 parse_provider_stream_event(&v).unwrap(),
2595 ProviderStreamEvent::Done
2596 );
2597 }
2598
2599 #[test]
2600 fn nested_event_shape_matches_flat() {
2601 let flat = json!({"type": "text", "delta": "hi"});
2602 let nested = json!({"event": {"type": "text", "delta": "hi"}});
2603 assert_eq!(
2604 parse_provider_stream_event(&flat).unwrap(),
2605 parse_provider_stream_event(&nested).unwrap()
2606 );
2607 }
2608
2609 #[test]
2610 fn missing_type_errors() {
2611 let v = json!({"delta": "hi"});
2612 let err = parse_provider_stream_event(&v).unwrap_err();
2613 assert!(err.contains("missing type"), "got: {err}");
2614 }
2615
2616 #[test]
2617 fn unknown_type_errors_with_type() {
2618 let v = json!({"type": "wat"});
2619 let err = parse_provider_stream_event(&v).unwrap_err();
2620 assert!(err.contains("wat"), "got: {err}");
2621 }
2622
2623 #[test]
2624 fn tool_use_missing_id_errors() {
2625 let v = json!({"type": "tool_use", "name": "echo"});
2626 let err = parse_provider_stream_event(&v).unwrap_err();
2627 assert!(err.contains("id"), "got: {err}");
2628 }
2629
2630 #[test]
2631 fn tool_use_missing_name_errors() {
2632 let v = json!({"type": "tool_use", "id": "t1"});
2633 let err = parse_provider_stream_event(&v).unwrap_err();
2634 assert!(err.contains("name"), "got: {err}");
2635 }
2636
2637 #[test]
2638 fn tool_use_empty_id_errors() {
2639 let v = json!({"type": "tool_use", "id": "", "name": "echo"});
2640 assert!(parse_provider_stream_event(&v).is_err());
2641 }
2642
2643 #[test]
2644 fn tool_use_empty_name_errors() {
2645 let v = json!({"type": "tool_use", "id": "t1", "name": ""});
2646 assert!(parse_provider_stream_event(&v).is_err());
2647 }
2648
2649 #[test]
2650 fn tool_use_non_object_input_errors() {
2651 let v = json!({"type": "tool_use", "id": "t1", "name": "echo", "input": "nope"});
2652 let err = parse_provider_stream_event(&v).unwrap_err();
2653 assert!(err.contains("input"), "got: {err}");
2654 }
2655
2656 #[test]
2657 fn text_missing_delta_and_text_errors() {
2658 let v = json!({"type": "text"});
2659 let err = parse_provider_stream_event(&v).unwrap_err();
2660 assert!(err.contains("delta") || err.contains("text"), "got: {err}");
2661 }
2662
2663 #[test]
2664 fn error_missing_message_errors() {
2665 let v = json!({"type": "error"});
2666 assert!(parse_provider_stream_event(&v).is_err());
2667 }
2668
2669 #[test]
2670 fn error_empty_message_errors() {
2671 let v = json!({"type": "error", "message": ""});
2672 assert!(parse_provider_stream_event(&v).is_err());
2673 }
2674}
2675
2676#[cfg(test)]
2677mod restart_policy_tests {
2678 use super::*;
2679
2680 #[tokio::test]
2681 async fn restart_policy_default_max_attempts_is_3() {
2682 let ext = ProcessExtension::spawn("policy-test", "/bin/cat", &[])
2687 .await
2688 .expect("spawn /bin/cat");
2689 assert_eq!(ext.restart_policy.max_attempts, 3);
2690 ext.shutdown().await;
2691 }
2692
2693 #[tokio::test]
2694 async fn with_restart_policy_overrides_default() {
2695 let ext = ProcessExtension::spawn("policy-test-override", "/bin/cat", &[])
2696 .await
2697 .expect("spawn /bin/cat");
2698 let custom = RestartPolicy {
2699 max_attempts: 7,
2700 ..RestartPolicy::default()
2701 };
2702 let ext = ext.with_restart_policy(custom);
2703 assert_eq!(ext.restart_policy.max_attempts, 7);
2704 ext.shutdown().await;
2705 }
2706}
2707
2708#[cfg(test)]
2709mod capture_validator_tests {
2710 use super::*;
2711 use crate::extensions::permissions::{Permission, PermissionSet};
2712
2713 fn perms_with(grants: &[Permission]) -> PermissionSet {
2714 let mut p = PermissionSet::new();
2715 for g in grants {
2716 p.grant(*g);
2717 }
2718 p
2719 }
2720
2721 fn cap(kind: &str, name: &str, perms: &[&str]) -> CapabilityDeclaration {
2722 CapabilityDeclaration {
2723 kind: kind.to_string(),
2724 name: name.to_string(),
2725 permissions: perms.iter().map(|p| p.to_string()).collect(),
2726 params: serde_json::Value::Null,
2727 }
2728 }
2729
2730 #[test]
2731 fn capability_validator_rejects_empty_kind() {
2732 let d = cap(" ", "Sample", &["audio.input"]);
2733 let perms = perms_with(&[Permission::AudioInput]);
2734 let err = validate_capability(&d, &perms).unwrap_err();
2735 assert!(err.contains("kind"), "got: {}", err);
2736 }
2737
2738 #[test]
2739 fn capability_validator_rejects_empty_name() {
2740 let d = cap("capture", " ", &["audio.input"]);
2741 let perms = perms_with(&[Permission::AudioInput]);
2742 let err = validate_capability(&d, &perms).unwrap_err();
2743 assert!(err.contains("name"), "got: {}", err);
2744 }
2745
2746 #[test]
2747 fn capability_validator_rejects_unknown_permission_string() {
2748 let d = cap("capture", "Sample", &["audio.telepathy"]);
2749 let perms = perms_with(&[Permission::AudioInput, Permission::AudioOutput]);
2750 let err = validate_capability(&d, &perms).unwrap_err();
2751 assert!(
2752 err.contains("unknown permission") && err.contains("audio.telepathy"),
2753 "got: {}",
2754 err,
2755 );
2756 }
2757
2758 #[test]
2759 fn capability_validator_requires_every_declared_permission() {
2760 let d = cap("capture", "Sample", &["audio.input"]);
2761 let perms = perms_with(&[]);
2762 let err = validate_capability(&d, &perms).unwrap_err();
2763 assert!(
2764 err.contains("audio.input") && err.contains("not granted"),
2765 "got: {}",
2766 err,
2767 );
2768 }
2769
2770 #[test]
2771 fn capability_validator_accepts_when_all_permissions_granted() {
2772 let d = cap("capture", "Sample", &["audio.input", "audio.output"]);
2773 let perms = perms_with(&[Permission::AudioInput, Permission::AudioOutput]);
2774 validate_capability(&d, &perms).expect("should validate");
2775 }
2776
2777 #[test]
2778 fn capability_validator_accepts_no_permissions() {
2779 let d = cap("ocr", "Tesseract", &[]);
2783 let perms = perms_with(&[]);
2784 validate_capability(&d, &perms).expect("should validate");
2785 }
2786
2787 #[test]
2788 fn capability_validator_does_not_branch_on_kind() {
2789 let perms = perms_with(&[Permission::AudioInput]);
2793 for kind in ["capture", "ocr", "agent", "foot_pedal", "eeg"] {
2794 let d = cap(kind, "Anything", &["audio.input"]);
2795 validate_capability(&d, &perms).expect("should validate");
2796 }
2797 }
2798}
2799
2800#[cfg(test)]
2801mod invoke_command_dispatch_tests {
2802 use super::*;
2807 use crate::extensions::commands::CommandOutputEvent;
2808 use crate::extensions::invoke_output::{invoke_event_channel, InvokeOutputBudget};
2809 use crate::extensions::runtime::InvokeCommandEvent;
2810 use crate::extensions::tasks::{TaskEvent, TaskKind};
2811 use serde_json::json;
2812
2813 fn frame(method: &str, params: serde_json::Value) -> NotificationFrame {
2814 NotificationFrame {
2815 method: method.to_string(),
2816 params,
2817 }
2818 }
2819
2820 async fn dispatch(frames: Vec<NotificationFrame>) -> (Vec<InvokeCommandEvent>, bool) {
2825 let (tx, collector) = invoke_event_channel(InvokeOutputBudget::default());
2826 let mut open = true;
2827 let mut saw_done = false;
2828 for f in frames {
2829 saw_done |=
2830 ProcessExtension::forward_invoke_command_frame("ext-test", "r1", &tx, &mut open, f)
2831 .await;
2832 }
2833 drop(tx);
2834 let report = collector.collect().await;
2835 (report.events, saw_done)
2836 }
2837
2838 #[tokio::test]
2839 async fn forwards_mixed_event_stream_in_order() {
2840 let frames = vec![
2841 frame(
2842 "command.output",
2843 json!({"request_id":"r1","event":{"kind":"text","content":"A"}}),
2844 ),
2845 frame(
2846 "task.start",
2847 json!({"id":"dl","label":"Downloading","kind":"download"}),
2848 ),
2849 frame("task.update", json!({"id":"dl","current":50,"total":100})),
2850 frame(
2851 "command.output",
2852 json!({"request_id":"r1","event":{"kind":"system","content":"working"}}),
2853 ),
2854 frame("task.done", json!({"id":"dl"})),
2855 frame(
2856 "command.output",
2857 json!({"request_id":"r1","event":{"kind":"done"}}),
2858 ),
2859 ];
2860
2861 let (events, saw_done) = dispatch(frames).await;
2862 assert!(saw_done, "should have observed the command Done marker");
2863 assert_eq!(events.len(), 6);
2864 assert_eq!(
2865 events[0],
2866 InvokeCommandEvent::Output(CommandOutputEvent::Text {
2867 content: "A".into()
2868 })
2869 );
2870 assert!(matches!(
2871 events[1],
2872 InvokeCommandEvent::Task(TaskEvent::Start {
2873 kind: TaskKind::Download,
2874 ..
2875 })
2876 ));
2877 assert!(matches!(
2878 events[2],
2879 InvokeCommandEvent::Task(TaskEvent::Update { .. })
2880 ));
2881 assert!(matches!(
2882 events[3],
2883 InvokeCommandEvent::Output(CommandOutputEvent::System { .. })
2884 ));
2885 assert!(matches!(
2886 events[4],
2887 InvokeCommandEvent::Task(TaskEvent::Done { error: None, .. })
2888 ));
2889 assert_eq!(
2890 events[5],
2891 InvokeCommandEvent::Output(CommandOutputEvent::Done)
2892 );
2893 }
2894
2895 #[tokio::test]
2896 async fn ignores_command_output_for_unrelated_request_id() {
2897 let (events, saw_done) = dispatch(vec![frame(
2898 "command.output",
2899 json!({"request_id":"other","event":{"kind":"text","content":"x"}}),
2900 )])
2901 .await;
2902 assert!(!saw_done);
2903 assert!(events.is_empty());
2904 }
2905
2906 #[tokio::test]
2907 async fn skips_malformed_command_output_without_aborting() {
2908 let (events, saw_done) = dispatch(vec![
2909 frame("command.output", json!({"request_id":"r1","event":{}})),
2911 frame(
2913 "command.output",
2914 json!({"request_id":"r1","event":{"kind":"done"}}),
2915 ),
2916 ])
2917 .await;
2918 assert!(saw_done);
2919 assert_eq!(
2920 events,
2921 vec![InvokeCommandEvent::Output(CommandOutputEvent::Done)]
2922 );
2923 }
2924
2925 #[tokio::test]
2926 async fn task_events_pass_through_regardless_of_request_id() {
2927 let (events, _) = dispatch(vec![frame("task.log", json!({"id":"abc","line":"..."}))]).await;
2928 match &events[..] {
2929 [InvokeCommandEvent::Task(TaskEvent::Log { id, line })] => {
2930 assert_eq!(id, "abc");
2931 assert_eq!(line, "...");
2932 }
2933 other => panic!("unexpected: {other:?}"),
2934 }
2935 }
2936
2937 #[tokio::test]
2938 async fn unrelated_methods_are_dropped() {
2939 let (events, saw_done) = dispatch(vec![frame(
2940 "provider.stream.event",
2941 json!({"type":"text","delta":"x"}),
2942 )])
2943 .await;
2944 assert!(!saw_done);
2945 assert!(events.is_empty());
2946 }
2947
2948 #[tokio::test]
2951 async fn closed_sink_latches_open_false_without_blocking() {
2952 let (tx, collector) = invoke_event_channel(InvokeOutputBudget::default());
2953 drop(collector);
2954 let mut open = true;
2955 let saw_done = ProcessExtension::forward_invoke_command_frame(
2956 "ext-test",
2957 "r1",
2958 &tx,
2959 &mut open,
2960 frame(
2961 "command.output",
2962 json!({"request_id":"r1","event":{"kind":"done"}}),
2963 ),
2964 )
2965 .await;
2966 assert!(saw_done);
2968 assert!(!open);
2969 }
2970}
2971
2972#[cfg(all(test, unix))]
2973mod lifecycle_tests {
2974 use super::*;
2975 use std::time::Duration;
2976
2977 async fn fixture() -> ProcessExtension {
2980 let result = serde_json::json!({
2981 "protocol_version": CURRENT_EXTENSION_PROTOCOL_VERSION,
2982 "capabilities": {}
2983 })
2984 .to_string();
2985 let script = format!(
2986 r#"while IFS= read -r header; do
2987 length=${{header#Content-Length: }}
2988 length=${{length%?}}
2989 IFS= read -r blank || exit 1
2990 body=$(dd bs=1 count="$length" 2>/dev/null) || exit 1
2991 id=${{body##*\"id\":}}
2992 id=${{id%%[!0-9]*}}
2993 case "$body" in
2994 *'"method":"pending"'*) IFS= read -r hold; continue ;;
2995 esac
2996 reply='{{"jsonrpc":"2.0","id":'"$id"',"result":{result}}}'
2997 printf 'Content-Length: %s\r\n\r\n%s' "${{#reply}}" "$reply"
2998done"#
2999 );
3000 let ext = ProcessExtension::spawn("lifecycle-fixture", "/bin/sh", &["-c".into(), script])
3001 .await
3002 .expect("spawn offline lifecycle fixture");
3003 tokio::time::timeout(Duration::from_secs(3), ext.initialize_for_test(None))
3004 .await
3005 .expect("bounded initialize")
3006 .expect("initialize fixture");
3007 ext
3008 }
3009
3010 async fn wait_failed(ext: &ProcessExtension) -> ExtensionLifecycle {
3011 tokio::time::timeout(Duration::from_secs(3), async {
3012 loop {
3013 if let Some(snapshot) = ext.lifecycle_snapshot() {
3014 if snapshot.health == ExtensionHealth::Failed {
3015 return snapshot;
3016 }
3017 }
3018 tokio::time::sleep(Duration::from_millis(5)).await;
3019 }
3020 })
3021 .await
3022 .expect("lifecycle must detect failure without another RPC")
3023 }
3024
3025 #[tokio::test]
3026 async fn actual_child_exit_is_detected_while_rpc_holds_state_and_reader_is_stopped() {
3027 let ext = fixture().await;
3028 let before = ext.lifecycle_snapshot().expect("live snapshot");
3029 assert_eq!(before.health, ExtensionHealth::Running);
3030 let call_guard = ext.call_lock.lock().await;
3031 let mut state_guard = ext.state.lock().await;
3032 let state = state_guard.as_mut().unwrap();
3033 let lifecycle = state.lifecycle.clone();
3034 state.reader_handle.abort();
3035 tokio::time::timeout(Duration::from_secs(3), async {
3037 while !state.reader_handle.is_finished() {
3038 tokio::task::yield_now().await;
3039 }
3040 })
3041 .await
3042 .expect("reader cancellation");
3043 {
3044 let rpc = ext.call_once_locked(state, "pending", Value::Null, 900);
3045 tokio::pin!(rpc);
3046 tokio::select! {
3047 biased;
3048 result = &mut rpc => panic!("RPC must remain pending: {result:?}"),
3049 _ = tokio::time::sleep(Duration::from_millis(20)) => {}
3050 }
3051 assert!(ext.inbox.pending.lock().await.contains_key(&900));
3052 assert_eq!(ext.lifecycle_snapshot(), Some(before));
3053 lifecycle.child.lock().await.start_kill().unwrap();
3054 let dead = wait_failed(&ext).await;
3055 assert_ne!(dead.generation, before.generation);
3056 assert_eq!(ext.lifecycle_snapshot(), Some(dead));
3057 assert!(!ext.inbox.closed.load(Ordering::Acquire));
3058 assert!(ext.inbox.pending.lock().await.contains_key(&900));
3059 }
3060 drop(state_guard);
3061 drop(call_guard);
3062 ext.force_shutdown().await;
3063 }
3064
3065 #[tokio::test]
3066 async fn shutdown_invalidates_generation_before_waiting_for_rpc_lock() {
3067 let ext = Arc::new(fixture().await);
3068 let before = ext.lifecycle_snapshot().unwrap();
3069 let call_guard = ext.call_lock.lock().await;
3070 let shutdown_ext = ext.clone();
3071 let shutdown = tokio::spawn(async move { shutdown_ext.shutdown().await });
3072 let dead = wait_failed(&ext).await;
3073 assert_ne!(dead.generation, before.generation);
3074 assert!(!shutdown.is_finished());
3075 drop(call_guard);
3076 tokio::time::timeout(Duration::from_secs(3), shutdown)
3077 .await
3078 .expect("bounded shutdown")
3079 .expect("shutdown task");
3080 assert_eq!(ext.lifecycle_snapshot(), Some(dead));
3081 }
3082
3083 #[tokio::test]
3084 async fn in_place_restart_changes_generation_and_old_invalidation_is_isolated() {
3085 let ext = fixture().await;
3086 let before = ext.lifecycle_snapshot().unwrap();
3087 let mut state = ext.state.lock().await;
3088 let old = state.as_ref().unwrap().lifecycle.clone();
3089 {
3090 let child_guard = old.child.lock().await;
3093 let restart = ext.restart_locked(&mut state);
3094 tokio::pin!(restart);
3095 tokio::select! {
3096 biased;
3097 result = &mut restart => panic!("restart must await child lock: {result:?}"),
3098 _ = tokio::time::sleep(Duration::from_millis(20)) => {}
3099 }
3100 let replacing = ext.lifecycle_snapshot().unwrap();
3101 assert_ne!(replacing.generation, before.generation);
3102 assert_eq!(replacing.health, ExtensionHealth::Failed);
3103 drop(child_guard);
3104 tokio::time::timeout(Duration::from_secs(5), restart)
3105 .await
3106 .expect("bounded restart")
3107 .expect("restart fixture");
3108 }
3109 let after = ext.lifecycle_snapshot().unwrap();
3110 assert_ne!(after.generation, before.generation);
3111 assert_eq!(after.health, ExtensionHealth::Degraded);
3112 assert_eq!(old.snapshot(false).unwrap().health, ExtensionHealth::Failed);
3113 old.invalidate();
3114 assert_eq!(ext.lifecycle_snapshot(), Some(after));
3115 drop(state);
3116 ext.force_shutdown().await;
3117 let stopped = ext.lifecycle_snapshot().unwrap();
3118 assert_ne!(stopped.generation, after.generation);
3119 assert_eq!(stopped.health, ExtensionHealth::Failed);
3120 }
3121
3122 #[tokio::test]
3123 async fn transport_eof_invalidates_generation_even_while_child_is_alive() {
3124 let ext = ProcessExtension::spawn(
3126 "lifecycle-eof",
3127 "/bin/sh",
3128 &[
3129 "-c".into(),
3130 "IFS= read -r trigger; exec 1>&-; IFS= read -r hold".into(),
3131 ],
3132 )
3133 .await
3134 .unwrap();
3135 let mut state_guard = ext.state.lock().await;
3136 let state = state_guard.as_mut().unwrap();
3137 state.lifecycle.initialized.store(true, Ordering::Release);
3138 let before = ext.lifecycle_snapshot().unwrap();
3139 state
3140 .stdin
3141 .lock()
3142 .await
3143 .write_all(b"close\n")
3144 .await
3145 .unwrap();
3146 let dead = wait_failed(&ext).await;
3147 assert_ne!(dead.generation, before.generation);
3148 assert!(state
3149 .lifecycle
3150 .child
3151 .lock()
3152 .await
3153 .try_wait()
3154 .unwrap()
3155 .is_none());
3156 drop(state_guard);
3157 ext.force_shutdown().await;
3158 }
3159
3160 #[tokio::test]
3161 async fn contended_child_probe_is_unavailable_without_changing_generation() {
3162 let ext = fixture().await;
3163 let before = ext.lifecycle_snapshot().unwrap();
3164 let lifecycle = ext.inbox.lifecycle.lock().await.clone().unwrap();
3165 let child_guard = lifecycle.child.lock().await;
3166 assert_eq!(ext.lifecycle_snapshot(), None);
3167 drop(child_guard);
3168 assert_eq!(ext.lifecycle_snapshot(), Some(before));
3169 let slot_guard = ext.inbox.lifecycle.lock().await;
3170 assert_eq!(ext.lifecycle_snapshot(), None);
3171 drop(slot_guard);
3172 assert_eq!(ext.lifecycle_snapshot(), Some(before));
3173 ext.force_shutdown().await;
3174 }
3175}