1use remote::Interactive;
2use std::{
3 any::Any,
4 borrow::Borrow,
5 path::{Path, PathBuf},
6 sync::Arc,
7 time::Duration,
8};
9
10use anyhow::{Context as _, Result, bail};
11use collections::HashMap;
12use fs::Fs;
13use gpui::{AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task};
14use http_client::{HttpClient, github::AssetKind};
15use node_runtime::NodeRuntime;
16use remote::RemoteClient;
17use rpc::{
18 AnyProtoClient, TypedEnvelope,
19 proto::{self, ExternalExtensionAgent},
20};
21use schemars::JsonSchema;
22use serde::{Deserialize, Serialize};
23use settings::{RegisterSetting, SettingsStore};
24use task::Shell;
25use util::{ResultExt as _, debug_panic};
26
27use crate::ProjectEnvironment;
28use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig};
29
30#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
31pub struct AgentServerCommand {
32 #[serde(rename = "command")]
33 pub path: PathBuf,
34 #[serde(default)]
35 pub args: Vec<String>,
36 pub env: Option<HashMap<String, String>>,
37}
38
39impl std::fmt::Debug for AgentServerCommand {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 let filtered_env = self.env.as_ref().map(|env| {
42 env.iter()
43 .map(|(k, v)| {
44 (
45 k,
46 if util::redact::should_redact(k) {
47 "[REDACTED]"
48 } else {
49 v
50 },
51 )
52 })
53 .collect::<Vec<_>>()
54 });
55
56 f.debug_struct("AgentServerCommand")
57 .field("path", &self.path)
58 .field("args", &self.args)
59 .field("env", &filtered_env)
60 .finish()
61 }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
65pub struct ExternalAgentServerName(pub SharedString);
66
67impl std::fmt::Display for ExternalAgentServerName {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(f, "{}", self.0)
70 }
71}
72
73impl From<&'static str> for ExternalAgentServerName {
74 fn from(value: &'static str) -> Self {
75 ExternalAgentServerName(value.into())
76 }
77}
78
79impl From<ExternalAgentServerName> for SharedString {
80 fn from(value: ExternalAgentServerName) -> Self {
81 value.0
82 }
83}
84
85impl Borrow<str> for ExternalAgentServerName {
86 fn borrow(&self) -> &str {
87 &self.0
88 }
89}
90
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
92pub enum ExternalAgentSource {
93 #[default]
94 Custom,
95 Extension,
96 Registry,
97}
98
99pub trait ExternalAgentServer {
100 fn get_command(
101 &mut self,
102 extra_env: HashMap<String, String>,
103 status_tx: Option<watch::Sender<SharedString>>,
104 new_version_available_tx: Option<watch::Sender<Option<String>>>,
105 cx: &mut AsyncApp,
106 ) -> Task<Result<AgentServerCommand>>;
107
108 fn as_any_mut(&mut self) -> &mut dyn Any;
109}
110
111impl dyn ExternalAgentServer {
112 fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> {
113 self.as_any_mut().downcast_mut()
114 }
115}
116
117enum AgentServerStoreState {
118 Local {
119 node_runtime: NodeRuntime,
120 fs: Arc<dyn Fs>,
121 project_environment: Entity<ProjectEnvironment>,
122 downstream_client: Option<(u64, AnyProtoClient)>,
123 settings: Option<AllAgentServersSettings>,
124 http_client: Arc<dyn HttpClient>,
125 extension_agents: Vec<(
126 Arc<str>,
127 String,
128 HashMap<String, extension::TargetConfig>,
129 HashMap<String, String>,
130 Option<String>,
131 Option<SharedString>,
132 )>,
133 _subscriptions: Vec<Subscription>,
134 },
135 Remote {
136 project_id: u64,
137 upstream_client: Entity<RemoteClient>,
138 },
139 Collab,
140}
141
142pub struct ExternalAgentEntry {
143 server: Box<dyn ExternalAgentServer>,
144 icon: Option<SharedString>,
145 display_name: Option<SharedString>,
146 pub source: ExternalAgentSource,
147}
148
149impl ExternalAgentEntry {
150 pub fn new(
151 server: Box<dyn ExternalAgentServer>,
152 source: ExternalAgentSource,
153 icon: Option<SharedString>,
154 display_name: Option<SharedString>,
155 ) -> Self {
156 Self {
157 server,
158 icon,
159 display_name,
160 source,
161 }
162 }
163}
164
165pub struct AgentServerStore {
166 state: AgentServerStoreState,
167 pub external_agents: HashMap<ExternalAgentServerName, ExternalAgentEntry>,
168}
169
170pub struct AgentServersUpdated;
171
172impl EventEmitter<AgentServersUpdated> for AgentServerStore {}
173
174impl AgentServerStore {
175 /// Synchronizes extension-provided agent servers with the store.
176 pub fn sync_extension_agents<'a, I>(
177 &mut self,
178 manifests: I,
179 extensions_dir: PathBuf,
180 cx: &mut Context<Self>,
181 ) where
182 I: IntoIterator<Item = (&'a str, &'a extension::ExtensionManifest)>,
183 {
184 // Collect manifests first so we can iterate twice
185 let manifests: Vec<_> = manifests.into_iter().collect();
186
187 // Remove all extension-provided agents
188 // (They will be re-added below if they're in the currently installed extensions)
189 self.external_agents
190 .retain(|_, entry| entry.source != ExternalAgentSource::Extension);
191
192 // Insert agent servers from extension manifests
193 match &mut self.state {
194 AgentServerStoreState::Local {
195 extension_agents, ..
196 } => {
197 extension_agents.clear();
198 for (ext_id, manifest) in manifests {
199 for (agent_name, agent_entry) in &manifest.agent_servers {
200 let display_name = SharedString::from(agent_entry.name.clone());
201 let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
202 resolve_extension_icon_path(&extensions_dir, ext_id, icon)
203 });
204
205 extension_agents.push((
206 agent_name.clone(),
207 ext_id.to_owned(),
208 agent_entry.targets.clone(),
209 agent_entry.env.clone(),
210 icon_path,
211 Some(display_name),
212 ));
213 }
214 }
215 self.reregister_agents(cx);
216 }
217 AgentServerStoreState::Remote {
218 project_id,
219 upstream_client,
220 } => {
221 let mut agents = vec![];
222 for (ext_id, manifest) in manifests {
223 for (agent_name, agent_entry) in &manifest.agent_servers {
224 let display_name = SharedString::from(agent_entry.name.clone());
225 let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
226 resolve_extension_icon_path(&extensions_dir, ext_id, icon)
227 });
228 let icon_shared = icon_path
229 .as_ref()
230 .map(|path| SharedString::from(path.clone()));
231 let icon = icon_path;
232 let agent_server_name = ExternalAgentServerName(agent_name.clone().into());
233 self.external_agents
234 .entry(agent_server_name.clone())
235 .and_modify(|entry| {
236 entry.icon = icon_shared.clone();
237 entry.display_name = Some(display_name.clone());
238 entry.source = ExternalAgentSource::Extension;
239 })
240 .or_insert_with(|| {
241 ExternalAgentEntry::new(
242 Box::new(RemoteExternalAgentServer {
243 project_id: *project_id,
244 upstream_client: upstream_client.clone(),
245 name: agent_server_name.clone(),
246 status_tx: None,
247 new_version_available_tx: None,
248 })
249 as Box<dyn ExternalAgentServer>,
250 ExternalAgentSource::Extension,
251 icon_shared.clone(),
252 Some(display_name.clone()),
253 )
254 });
255
256 agents.push(ExternalExtensionAgent {
257 name: agent_name.to_string(),
258 icon_path: icon,
259 extension_id: ext_id.to_string(),
260 targets: agent_entry
261 .targets
262 .iter()
263 .map(|(k, v)| (k.clone(), v.to_proto()))
264 .collect(),
265 env: agent_entry
266 .env
267 .iter()
268 .map(|(k, v)| (k.clone(), v.clone()))
269 .collect(),
270 });
271 }
272 }
273 upstream_client
274 .read(cx)
275 .proto_client()
276 .send(proto::ExternalExtensionAgentsUpdated {
277 project_id: *project_id,
278 agents,
279 })
280 .log_err();
281 }
282 AgentServerStoreState::Collab => {
283 // Do nothing
284 }
285 }
286
287 cx.emit(AgentServersUpdated);
288 }
289
290 pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
291 self.external_agents
292 .get(name)
293 .and_then(|entry| entry.icon.clone())
294 }
295
296 pub fn agent_source(&self, name: &ExternalAgentServerName) -> Option<ExternalAgentSource> {
297 self.external_agents.get(name).map(|entry| entry.source)
298 }
299}
300
301/// Safely resolves an extension icon path, ensuring it stays within the extension directory.
302/// Returns `None` if the path would escape the extension directory (path traversal attack).
303pub fn resolve_extension_icon_path(
304 extensions_dir: &Path,
305 extension_id: &str,
306 icon_relative_path: &str,
307) -> Option<String> {
308 let extension_root = extensions_dir.join(extension_id);
309 let icon_path = extension_root.join(icon_relative_path);
310
311 // Canonicalize both paths to resolve symlinks and normalize the paths.
312 // For the extension root, we need to handle the case where it might be a symlink
313 // (common for dev extensions).
314 let canonical_extension_root = extension_root.canonicalize().unwrap_or(extension_root);
315 let canonical_icon_path = match icon_path.canonicalize() {
316 Ok(path) => path,
317 Err(err) => {
318 log::warn!(
319 "Failed to canonicalize icon path for extension '{}': {} (path: {})",
320 extension_id,
321 err,
322 icon_relative_path
323 );
324 return None;
325 }
326 };
327
328 // Verify the resolved icon path is within the extension directory
329 if canonical_icon_path.starts_with(&canonical_extension_root) {
330 Some(canonical_icon_path.to_string_lossy().to_string())
331 } else {
332 log::warn!(
333 "Icon path '{}' for extension '{}' escapes extension directory, ignoring for security",
334 icon_relative_path,
335 extension_id
336 );
337 None
338 }
339}
340
341impl AgentServerStore {
342 pub fn agent_display_name(&self, name: &ExternalAgentServerName) -> Option<SharedString> {
343 self.external_agents
344 .get(name)
345 .and_then(|entry| entry.display_name.clone())
346 }
347
348 pub fn init_remote(session: &AnyProtoClient) {
349 session.add_entity_message_handler(Self::handle_external_agents_updated);
350 session.add_entity_message_handler(Self::handle_loading_status_updated);
351 session.add_entity_message_handler(Self::handle_new_version_available);
352 }
353
354 pub fn init_headless(session: &AnyProtoClient) {
355 session.add_entity_message_handler(Self::handle_external_extension_agents_updated);
356 session.add_entity_request_handler(Self::handle_get_agent_server_command);
357 }
358
359 fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) {
360 let AgentServerStoreState::Local {
361 settings: old_settings,
362 ..
363 } = &mut self.state
364 else {
365 debug_panic!(
366 "should not be subscribed to agent server settings changes in non-local project"
367 );
368 return;
369 };
370
371 let new_settings = cx
372 .global::<SettingsStore>()
373 .get::<AllAgentServersSettings>(None)
374 .clone();
375 if Some(&new_settings) == old_settings.as_ref() {
376 return;
377 }
378
379 self.reregister_agents(cx);
380 }
381
382 fn reregister_agents(&mut self, cx: &mut Context<Self>) {
383 let AgentServerStoreState::Local {
384 node_runtime,
385 fs,
386 project_environment,
387 downstream_client,
388 settings: old_settings,
389 http_client,
390 extension_agents,
391 ..
392 } = &mut self.state
393 else {
394 debug_panic!("Non-local projects should never attempt to reregister. This is a bug!");
395
396 return;
397 };
398
399 let new_settings = cx
400 .global::<SettingsStore>()
401 .get::<AllAgentServersSettings>(None)
402 .clone();
403
404 // If we don't have agents from the registry loaded yet, trigger a
405 // refresh, which will cause this function to be called again
406 let registry_store = AgentRegistryStore::try_global(cx);
407 if new_settings.has_registry_agents()
408 && let Some(registry) = registry_store.as_ref()
409 {
410 registry.update(cx, |registry, cx| registry.refresh_if_stale(cx));
411 }
412
413 let registry_agents_by_id = registry_store
414 .as_ref()
415 .map(|store| {
416 store
417 .read(cx)
418 .agents()
419 .iter()
420 .cloned()
421 .map(|agent| (agent.id().to_string(), agent))
422 .collect::<HashMap<_, _>>()
423 })
424 .unwrap_or_default();
425
426 self.external_agents.clear();
427
428 // Insert extension agents before custom/registry so registry entries override extensions.
429 for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {
430 let name = ExternalAgentServerName(agent_name.clone().into());
431 let mut env = env.clone();
432 if let Some(settings_env) =
433 new_settings
434 .get(agent_name.as_ref())
435 .and_then(|settings| match settings {
436 CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),
437 _ => None,
438 })
439 {
440 env.extend(settings_env);
441 }
442 let icon = icon_path
443 .as_ref()
444 .map(|path| SharedString::from(path.clone()));
445
446 self.external_agents.insert(
447 name.clone(),
448 ExternalAgentEntry::new(
449 Box::new(LocalExtensionArchiveAgent {
450 fs: fs.clone(),
451 http_client: http_client.clone(),
452 node_runtime: node_runtime.clone(),
453 project_environment: project_environment.clone(),
454 extension_id: Arc::from(&**ext_id),
455 targets: targets.clone(),
456 env,
457 agent_id: agent_name.clone(),
458 }) as Box<dyn ExternalAgentServer>,
459 ExternalAgentSource::Extension,
460 icon,
461 display_name.clone(),
462 ),
463 );
464 }
465
466 for (name, settings) in new_settings.iter() {
467 match settings {
468 CustomAgentServerSettings::Custom { command, .. } => {
469 let agent_name = ExternalAgentServerName(name.clone().into());
470 self.external_agents.insert(
471 agent_name.clone(),
472 ExternalAgentEntry::new(
473 Box::new(LocalCustomAgent {
474 command: command.clone(),
475 project_environment: project_environment.clone(),
476 }) as Box<dyn ExternalAgentServer>,
477 ExternalAgentSource::Custom,
478 None,
479 None,
480 ),
481 );
482 }
483 CustomAgentServerSettings::Registry { env, .. } => {
484 let Some(agent) = registry_agents_by_id.get(name) else {
485 if registry_store.is_some() {
486 log::debug!("Registry agent '{}' not found in ACP registry", name);
487 }
488 continue;
489 };
490
491 let agent_name = ExternalAgentServerName(name.clone().into());
492 match agent {
493 RegistryAgent::Binary(agent) => {
494 if !agent.supports_current_platform {
495 log::warn!(
496 "Registry agent '{}' has no compatible binary for this platform",
497 name
498 );
499 continue;
500 }
501
502 self.external_agents.insert(
503 agent_name.clone(),
504 ExternalAgentEntry::new(
505 Box::new(LocalRegistryArchiveAgent {
506 fs: fs.clone(),
507 http_client: http_client.clone(),
508 node_runtime: node_runtime.clone(),
509 project_environment: project_environment.clone(),
510 registry_id: Arc::from(name.as_str()),
511 targets: agent.targets.clone(),
512 env: env.clone(),
513 })
514 as Box<dyn ExternalAgentServer>,
515 ExternalAgentSource::Registry,
516 agent.metadata.icon_path.clone(),
517 Some(agent.metadata.name.clone()),
518 ),
519 );
520 }
521 RegistryAgent::Npx(agent) => {
522 self.external_agents.insert(
523 agent_name.clone(),
524 ExternalAgentEntry::new(
525 Box::new(LocalRegistryNpxAgent {
526 node_runtime: node_runtime.clone(),
527 project_environment: project_environment.clone(),
528 package: agent.package.clone(),
529 args: agent.args.clone(),
530 distribution_env: agent.env.clone(),
531 settings_env: env.clone(),
532 })
533 as Box<dyn ExternalAgentServer>,
534 ExternalAgentSource::Registry,
535 agent.metadata.icon_path.clone(),
536 Some(agent.metadata.name.clone()),
537 ),
538 );
539 }
540 }
541 }
542 CustomAgentServerSettings::Extension { .. } => {}
543 }
544 }
545
546 *old_settings = Some(new_settings);
547
548 if let Some((project_id, downstream_client)) = downstream_client {
549 downstream_client
550 .send(proto::ExternalAgentsUpdated {
551 project_id: *project_id,
552 names: self
553 .external_agents
554 .keys()
555 .map(|name| name.to_string())
556 .collect(),
557 })
558 .log_err();
559 }
560 cx.emit(AgentServersUpdated);
561 }
562
563 pub fn node_runtime(&self) -> Option<NodeRuntime> {
564 match &self.state {
565 AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),
566 _ => None,
567 }
568 }
569
570 pub fn local(
571 node_runtime: NodeRuntime,
572 fs: Arc<dyn Fs>,
573 project_environment: Entity<ProjectEnvironment>,
574 http_client: Arc<dyn HttpClient>,
575 cx: &mut Context<Self>,
576 ) -> Self {
577 let mut subscriptions = vec![cx.observe_global::<SettingsStore>(|this, cx| {
578 this.agent_servers_settings_changed(cx);
579 })];
580 if let Some(registry_store) = AgentRegistryStore::try_global(cx) {
581 subscriptions.push(cx.observe(®istry_store, |this, _, cx| {
582 this.reregister_agents(cx);
583 }));
584 }
585 let mut this = Self {
586 state: AgentServerStoreState::Local {
587 node_runtime,
588 fs,
589 project_environment,
590 http_client,
591 downstream_client: None,
592 settings: None,
593 extension_agents: vec![],
594 _subscriptions: subscriptions,
595 },
596 external_agents: HashMap::default(),
597 };
598 if let Some(_events) = extension::ExtensionEvents::try_global(cx) {}
599 this.agent_servers_settings_changed(cx);
600 this
601 }
602
603 pub(crate) fn remote(project_id: u64, upstream_client: Entity<RemoteClient>) -> Self {
604 Self {
605 state: AgentServerStoreState::Remote {
606 project_id,
607 upstream_client,
608 },
609 external_agents: HashMap::default(),
610 }
611 }
612
613 pub fn collab() -> Self {
614 Self {
615 state: AgentServerStoreState::Collab,
616 external_agents: HashMap::default(),
617 }
618 }
619
620 pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
621 match &mut self.state {
622 AgentServerStoreState::Local {
623 downstream_client, ..
624 } => {
625 *downstream_client = Some((project_id, client.clone()));
626 // Send the current list of external agents downstream, but only after a delay,
627 // to avoid having the message arrive before the downstream project's agent server store
628 // sets up its handlers.
629 cx.spawn(async move |this, cx| {
630 cx.background_executor().timer(Duration::from_secs(1)).await;
631 let names = this.update(cx, |this, _| {
632 this.external_agents()
633 .map(|name| name.to_string())
634 .collect()
635 })?;
636 client
637 .send(proto::ExternalAgentsUpdated { project_id, names })
638 .log_err();
639 anyhow::Ok(())
640 })
641 .detach();
642 }
643 AgentServerStoreState::Remote { .. } => {
644 debug_panic!(
645 "external agents over collab not implemented, remote project should not be shared"
646 );
647 }
648 AgentServerStoreState::Collab => {
649 debug_panic!("external agents over collab not implemented, should not be shared");
650 }
651 }
652 }
653
654 pub fn get_external_agent(
655 &mut self,
656 name: &ExternalAgentServerName,
657 ) -> Option<&mut (dyn ExternalAgentServer + 'static)> {
658 self.external_agents
659 .get_mut(name)
660 .map(|entry| entry.server.as_mut())
661 }
662
663 pub fn no_browser(&self) -> bool {
664 match &self.state {
665 AgentServerStoreState::Local {
666 downstream_client, ..
667 } => downstream_client
668 .as_ref()
669 .is_some_and(|(_, client)| !client.has_wsl_interop()),
670 _ => false,
671 }
672 }
673
674 pub fn external_agents(&self) -> impl Iterator<Item = &ExternalAgentServerName> {
675 self.external_agents.keys()
676 }
677
678 async fn handle_get_agent_server_command(
679 this: Entity<Self>,
680 envelope: TypedEnvelope<proto::GetAgentServerCommand>,
681 mut cx: AsyncApp,
682 ) -> Result<proto::AgentServerCommand> {
683 let command = this
684 .update(&mut cx, |this, cx| {
685 let AgentServerStoreState::Local {
686 downstream_client, ..
687 } = &this.state
688 else {
689 debug_panic!("should not receive GetAgentServerCommand in a non-local project");
690 bail!("unexpected GetAgentServerCommand request in a non-local project");
691 };
692 let no_browser = this.no_browser();
693 let agent = this
694 .external_agents
695 .get_mut(&*envelope.payload.name)
696 .map(|entry| entry.server.as_mut())
697 .with_context(|| format!("agent `{}` not found", envelope.payload.name))?;
698 let (status_tx, new_version_available_tx) = downstream_client
699 .clone()
700 .map(|(project_id, downstream_client)| {
701 let (status_tx, mut status_rx) = watch::channel(SharedString::from(""));
702 let (new_version_available_tx, mut new_version_available_rx) =
703 watch::channel(None);
704 cx.spawn({
705 let downstream_client = downstream_client.clone();
706 let name = envelope.payload.name.clone();
707 async move |_, _| {
708 while let Some(status) = status_rx.recv().await.ok() {
709 downstream_client.send(
710 proto::ExternalAgentLoadingStatusUpdated {
711 project_id,
712 name: name.clone(),
713 status: status.to_string(),
714 },
715 )?;
716 }
717 anyhow::Ok(())
718 }
719 })
720 .detach_and_log_err(cx);
721 cx.spawn({
722 let name = envelope.payload.name.clone();
723 async move |_, _| {
724 if let Some(version) =
725 new_version_available_rx.recv().await.ok().flatten()
726 {
727 downstream_client.send(
728 proto::NewExternalAgentVersionAvailable {
729 project_id,
730 name: name.clone(),
731 version,
732 },
733 )?;
734 }
735 anyhow::Ok(())
736 }
737 })
738 .detach_and_log_err(cx);
739 (status_tx, new_version_available_tx)
740 })
741 .unzip();
742 let mut extra_env = HashMap::default();
743 if no_browser {
744 extra_env.insert("NO_BROWSER".to_owned(), "1".to_owned());
745 }
746 anyhow::Ok(agent.get_command(
747 extra_env,
748 status_tx,
749 new_version_available_tx,
750 &mut cx.to_async(),
751 ))
752 })?
753 .await?;
754 Ok(proto::AgentServerCommand {
755 path: command.path.to_string_lossy().into_owned(),
756 args: command.args,
757 env: command
758 .env
759 .map(|env| env.into_iter().collect())
760 .unwrap_or_default(),
761 // root_dir and login are no longer used, but returned for backwards compatibility
762 root_dir: paths::home_dir().to_string_lossy().to_string(),
763 login: None,
764 })
765 }
766
767 async fn handle_external_agents_updated(
768 this: Entity<Self>,
769 envelope: TypedEnvelope<proto::ExternalAgentsUpdated>,
770 mut cx: AsyncApp,
771 ) -> Result<()> {
772 this.update(&mut cx, |this, cx| {
773 let AgentServerStoreState::Remote {
774 project_id,
775 upstream_client,
776 } = &this.state
777 else {
778 debug_panic!(
779 "handle_external_agents_updated should not be called for a non-remote project"
780 );
781 bail!("unexpected ExternalAgentsUpdated message")
782 };
783
784 let mut previous_entries = std::mem::take(&mut this.external_agents);
785 let mut status_txs = HashMap::default();
786 let mut new_version_available_txs = HashMap::default();
787 let mut metadata = HashMap::default();
788
789 for (name, mut entry) in previous_entries.drain() {
790 if let Some(agent) = entry.server.downcast_mut::<RemoteExternalAgentServer>() {
791 status_txs.insert(name.clone(), agent.status_tx.take());
792 new_version_available_txs
793 .insert(name.clone(), agent.new_version_available_tx.take());
794 }
795
796 metadata.insert(name, (entry.icon, entry.display_name, entry.source));
797 }
798
799 this.external_agents = envelope
800 .payload
801 .names
802 .into_iter()
803 .map(|name| {
804 let agent_name = ExternalAgentServerName(name.into());
805 let (icon, display_name, source) = metadata
806 .remove(&agent_name)
807 .or_else(|| {
808 AgentRegistryStore::try_global(cx)
809 .and_then(|store| store.read(cx).agent(&agent_name.0))
810 .map(|s| {
811 (
812 s.icon_path().cloned(),
813 Some(s.name().clone()),
814 ExternalAgentSource::Registry,
815 )
816 })
817 })
818 .unwrap_or((None, None, ExternalAgentSource::default()));
819 let agent = RemoteExternalAgentServer {
820 project_id: *project_id,
821 upstream_client: upstream_client.clone(),
822 name: agent_name.clone(),
823 status_tx: status_txs.remove(&agent_name).flatten(),
824 new_version_available_tx: new_version_available_txs
825 .remove(&agent_name)
826 .flatten(),
827 };
828 (
829 agent_name,
830 ExternalAgentEntry::new(
831 Box::new(agent) as Box<dyn ExternalAgentServer>,
832 source,
833 icon,
834 display_name,
835 ),
836 )
837 })
838 .collect();
839 cx.emit(AgentServersUpdated);
840 Ok(())
841 })
842 }
843
844 async fn handle_external_extension_agents_updated(
845 this: Entity<Self>,
846 envelope: TypedEnvelope<proto::ExternalExtensionAgentsUpdated>,
847 mut cx: AsyncApp,
848 ) -> Result<()> {
849 this.update(&mut cx, |this, cx| {
850 let AgentServerStoreState::Local {
851 extension_agents, ..
852 } = &mut this.state
853 else {
854 panic!(
855 "handle_external_extension_agents_updated \
856 should not be called for a non-remote project"
857 );
858 };
859
860 for ExternalExtensionAgent {
861 name,
862 icon_path,
863 extension_id,
864 targets,
865 env,
866 } in envelope.payload.agents
867 {
868 extension_agents.push((
869 Arc::from(&*name),
870 extension_id,
871 targets
872 .into_iter()
873 .map(|(k, v)| (k, extension::TargetConfig::from_proto(v)))
874 .collect(),
875 env.into_iter().collect(),
876 icon_path,
877 None,
878 ));
879 }
880
881 this.reregister_agents(cx);
882 cx.emit(AgentServersUpdated);
883 Ok(())
884 })
885 }
886
887 async fn handle_loading_status_updated(
888 this: Entity<Self>,
889 envelope: TypedEnvelope<proto::ExternalAgentLoadingStatusUpdated>,
890 mut cx: AsyncApp,
891 ) -> Result<()> {
892 this.update(&mut cx, |this, _| {
893 if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
894 && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
895 && let Some(status_tx) = &mut agent.status_tx
896 {
897 status_tx.send(envelope.payload.status.into()).ok();
898 }
899 });
900 Ok(())
901 }
902
903 async fn handle_new_version_available(
904 this: Entity<Self>,
905 envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
906 mut cx: AsyncApp,
907 ) -> Result<()> {
908 this.update(&mut cx, |this, _| {
909 if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
910 && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
911 && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
912 {
913 new_version_available_tx
914 .send(Some(envelope.payload.version))
915 .ok();
916 }
917 });
918 Ok(())
919 }
920
921 pub fn get_extension_id_for_agent(
922 &mut self,
923 name: &ExternalAgentServerName,
924 ) -> Option<Arc<str>> {
925 self.external_agents.get_mut(name).and_then(|entry| {
926 entry
927 .server
928 .as_any_mut()
929 .downcast_ref::<LocalExtensionArchiveAgent>()
930 .map(|ext_agent| ext_agent.extension_id.clone())
931 })
932 }
933}
934
935struct RemoteExternalAgentServer {
936 project_id: u64,
937 upstream_client: Entity<RemoteClient>,
938 name: ExternalAgentServerName,
939 status_tx: Option<watch::Sender<SharedString>>,
940 new_version_available_tx: Option<watch::Sender<Option<String>>>,
941}
942
943impl ExternalAgentServer for RemoteExternalAgentServer {
944 fn get_command(
945 &mut self,
946 extra_env: HashMap<String, String>,
947 status_tx: Option<watch::Sender<SharedString>>,
948 new_version_available_tx: Option<watch::Sender<Option<String>>>,
949 cx: &mut AsyncApp,
950 ) -> Task<Result<AgentServerCommand>> {
951 let project_id = self.project_id;
952 let name = self.name.to_string();
953 let upstream_client = self.upstream_client.downgrade();
954 self.status_tx = status_tx;
955 self.new_version_available_tx = new_version_available_tx;
956 cx.spawn(async move |cx| {
957 let mut response = upstream_client
958 .update(cx, |upstream_client, _| {
959 upstream_client
960 .proto_client()
961 .request(proto::GetAgentServerCommand {
962 project_id,
963 name,
964 root_dir: None,
965 })
966 })?
967 .await?;
968 let root_dir = response.root_dir;
969 response.env.extend(extra_env);
970 let command = upstream_client.update(cx, |client, _| {
971 client.build_command_with_options(
972 Some(response.path),
973 &response.args,
974 &response.env.into_iter().collect(),
975 Some(root_dir.clone()),
976 None,
977 Interactive::No,
978 )
979 })??;
980 Ok(AgentServerCommand {
981 path: command.program.into(),
982 args: command.args,
983 env: Some(command.env),
984 })
985 })
986 }
987
988 fn as_any_mut(&mut self) -> &mut dyn Any {
989 self
990 }
991}
992
993pub struct LocalExtensionArchiveAgent {
994 pub fs: Arc<dyn Fs>,
995 pub http_client: Arc<dyn HttpClient>,
996 pub node_runtime: NodeRuntime,
997 pub project_environment: Entity<ProjectEnvironment>,
998 pub extension_id: Arc<str>,
999 pub agent_id: Arc<str>,
1000 pub targets: HashMap<String, extension::TargetConfig>,
1001 pub env: HashMap<String, String>,
1002}
1003
1004impl ExternalAgentServer for LocalExtensionArchiveAgent {
1005 fn get_command(
1006 &mut self,
1007 extra_env: HashMap<String, String>,
1008 _status_tx: Option<watch::Sender<SharedString>>,
1009 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1010 cx: &mut AsyncApp,
1011 ) -> Task<Result<AgentServerCommand>> {
1012 let fs = self.fs.clone();
1013 let http_client = self.http_client.clone();
1014 let node_runtime = self.node_runtime.clone();
1015 let project_environment = self.project_environment.downgrade();
1016 let extension_id = self.extension_id.clone();
1017 let agent_id = self.agent_id.clone();
1018 let targets = self.targets.clone();
1019 let base_env = self.env.clone();
1020
1021 cx.spawn(async move |cx| {
1022 // Get project environment
1023 let mut env = project_environment
1024 .update(cx, |project_environment, cx| {
1025 project_environment.local_directory_environment(
1026 &Shell::System,
1027 paths::home_dir().as_path().into(),
1028 cx,
1029 )
1030 })?
1031 .await
1032 .unwrap_or_default();
1033
1034 // Merge manifest env and extra env
1035 env.extend(base_env);
1036 env.extend(extra_env);
1037
1038 let cache_key = format!("{}/{}", extension_id, agent_id);
1039 let dir = paths::external_agents_dir().join(&cache_key);
1040 fs.create_dir(&dir).await?;
1041
1042 // Determine platform key
1043 let os = if cfg!(target_os = "macos") {
1044 "darwin"
1045 } else if cfg!(target_os = "linux") {
1046 "linux"
1047 } else if cfg!(target_os = "windows") {
1048 "windows"
1049 } else {
1050 anyhow::bail!("unsupported OS");
1051 };
1052
1053 let arch = if cfg!(target_arch = "aarch64") {
1054 "aarch64"
1055 } else if cfg!(target_arch = "x86_64") {
1056 "x86_64"
1057 } else {
1058 anyhow::bail!("unsupported architecture");
1059 };
1060
1061 let platform_key = format!("{}-{}", os, arch);
1062 let target_config = targets.get(&platform_key).with_context(|| {
1063 format!(
1064 "no target specified for platform '{}'. Available platforms: {}",
1065 platform_key,
1066 targets
1067 .keys()
1068 .map(|k| k.as_str())
1069 .collect::<Vec<_>>()
1070 .join(", ")
1071 )
1072 })?;
1073
1074 let archive_url = &target_config.archive;
1075
1076 // Use URL as version identifier for caching
1077 // Hash the URL to get a stable directory name
1078 use std::collections::hash_map::DefaultHasher;
1079 use std::hash::{Hash, Hasher};
1080 let mut hasher = DefaultHasher::new();
1081 archive_url.hash(&mut hasher);
1082 let url_hash = hasher.finish();
1083 let version_dir = dir.join(format!("v_{:x}", url_hash));
1084
1085 if !fs.is_dir(&version_dir).await {
1086 // Determine SHA256 for verification
1087 let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1088 // Use provided SHA256
1089 Some(provided_sha.clone())
1090 } else if archive_url.starts_with("https://github.com/") {
1091 // Try to fetch SHA256 from GitHub API
1092 // Parse URL to extract repo and tag/file info
1093 // Format: https://github.com/owner/repo/releases/download/tag/file.zip
1094 if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1095 let parts: Vec<&str> = caps.split('/').collect();
1096 if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1097 let repo = format!("{}/{}", parts[0], parts[1]);
1098 let tag = parts[4];
1099 let filename = parts[5..].join("/");
1100
1101 // Try to get release info from GitHub
1102 if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1103 &repo,
1104 tag,
1105 http_client.clone(),
1106 )
1107 .await
1108 {
1109 // Find matching asset
1110 if let Some(asset) =
1111 release.assets.iter().find(|a| a.name == filename)
1112 {
1113 // Strip "sha256:" prefix if present
1114 asset.digest.as_ref().map(|d| {
1115 d.strip_prefix("sha256:")
1116 .map(|s| s.to_string())
1117 .unwrap_or_else(|| d.clone())
1118 })
1119 } else {
1120 None
1121 }
1122 } else {
1123 None
1124 }
1125 } else {
1126 None
1127 }
1128 } else {
1129 None
1130 }
1131 } else {
1132 None
1133 };
1134
1135 // Determine archive type from URL
1136 let asset_kind = if archive_url.ends_with(".zip") {
1137 AssetKind::Zip
1138 } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1139 AssetKind::TarGz
1140 } else {
1141 anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1142 };
1143
1144 // Download and extract
1145 ::http_client::github_download::download_server_binary(
1146 &*http_client,
1147 archive_url,
1148 sha256.as_deref(),
1149 &version_dir,
1150 asset_kind,
1151 )
1152 .await?;
1153 }
1154
1155 // Validate and resolve cmd path
1156 let cmd = &target_config.cmd;
1157
1158 let cmd_path = if cmd == "node" {
1159 // Use Zed's managed Node.js runtime
1160 node_runtime.binary_path().await?
1161 } else {
1162 if cmd.contains("..") {
1163 anyhow::bail!("command path cannot contain '..': {}", cmd);
1164 }
1165
1166 if cmd.starts_with("./") || cmd.starts_with(".\\") {
1167 // Relative to extraction directory
1168 let cmd_path = version_dir.join(&cmd[2..]);
1169 anyhow::ensure!(
1170 fs.is_file(&cmd_path).await,
1171 "Missing command {} after extraction",
1172 cmd_path.to_string_lossy()
1173 );
1174 cmd_path
1175 } else {
1176 // On PATH
1177 anyhow::bail!("command must be relative (start with './'): {}", cmd);
1178 }
1179 };
1180
1181 let command = AgentServerCommand {
1182 path: cmd_path,
1183 args: target_config.args.clone(),
1184 env: Some(env),
1185 };
1186
1187 Ok(command)
1188 })
1189 }
1190
1191 fn as_any_mut(&mut self) -> &mut dyn Any {
1192 self
1193 }
1194}
1195
1196struct LocalRegistryArchiveAgent {
1197 fs: Arc<dyn Fs>,
1198 http_client: Arc<dyn HttpClient>,
1199 node_runtime: NodeRuntime,
1200 project_environment: Entity<ProjectEnvironment>,
1201 registry_id: Arc<str>,
1202 targets: HashMap<String, RegistryTargetConfig>,
1203 env: HashMap<String, String>,
1204}
1205
1206impl ExternalAgentServer for LocalRegistryArchiveAgent {
1207 fn get_command(
1208 &mut self,
1209 extra_env: HashMap<String, String>,
1210 _status_tx: Option<watch::Sender<SharedString>>,
1211 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1212 cx: &mut AsyncApp,
1213 ) -> Task<Result<AgentServerCommand>> {
1214 let fs = self.fs.clone();
1215 let http_client = self.http_client.clone();
1216 let node_runtime = self.node_runtime.clone();
1217 let project_environment = self.project_environment.downgrade();
1218 let registry_id = self.registry_id.clone();
1219 let targets = self.targets.clone();
1220 let settings_env = self.env.clone();
1221
1222 cx.spawn(async move |cx| {
1223 let mut env = project_environment
1224 .update(cx, |project_environment, cx| {
1225 project_environment.local_directory_environment(
1226 &Shell::System,
1227 paths::home_dir().as_path().into(),
1228 cx,
1229 )
1230 })?
1231 .await
1232 .unwrap_or_default();
1233
1234 let dir = paths::external_agents_dir()
1235 .join("registry")
1236 .join(registry_id.as_ref());
1237 fs.create_dir(&dir).await?;
1238
1239 let os = if cfg!(target_os = "macos") {
1240 "darwin"
1241 } else if cfg!(target_os = "linux") {
1242 "linux"
1243 } else if cfg!(target_os = "windows") {
1244 "windows"
1245 } else {
1246 anyhow::bail!("unsupported OS");
1247 };
1248
1249 let arch = if cfg!(target_arch = "aarch64") {
1250 "aarch64"
1251 } else if cfg!(target_arch = "x86_64") {
1252 "x86_64"
1253 } else {
1254 anyhow::bail!("unsupported architecture");
1255 };
1256
1257 let platform_key = format!("{}-{}", os, arch);
1258 let target_config = targets.get(&platform_key).with_context(|| {
1259 format!(
1260 "no target specified for platform '{}'. Available platforms: {}",
1261 platform_key,
1262 targets
1263 .keys()
1264 .map(|k| k.as_str())
1265 .collect::<Vec<_>>()
1266 .join(", ")
1267 )
1268 })?;
1269
1270 env.extend(target_config.env.clone());
1271 env.extend(extra_env);
1272 env.extend(settings_env);
1273
1274 let archive_url = &target_config.archive;
1275
1276 use std::collections::hash_map::DefaultHasher;
1277 use std::hash::{Hash, Hasher};
1278 let mut hasher = DefaultHasher::new();
1279 archive_url.hash(&mut hasher);
1280 let url_hash = hasher.finish();
1281 let version_dir = dir.join(format!("v_{:x}", url_hash));
1282
1283 if !fs.is_dir(&version_dir).await {
1284 let sha256 = if let Some(provided_sha) = &target_config.sha256 {
1285 Some(provided_sha.clone())
1286 } else if archive_url.starts_with("https://github.com/") {
1287 if let Some(caps) = archive_url.strip_prefix("https://github.com/") {
1288 let parts: Vec<&str> = caps.split('/').collect();
1289 if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" {
1290 let repo = format!("{}/{}", parts[0], parts[1]);
1291 let tag = parts[4];
1292 let filename = parts[5..].join("/");
1293
1294 if let Ok(release) = ::http_client::github::get_release_by_tag_name(
1295 &repo,
1296 tag,
1297 http_client.clone(),
1298 )
1299 .await
1300 {
1301 if let Some(asset) =
1302 release.assets.iter().find(|a| a.name == filename)
1303 {
1304 asset.digest.as_ref().and_then(|d| {
1305 d.strip_prefix("sha256:")
1306 .map(|s| s.to_string())
1307 .or_else(|| Some(d.clone()))
1308 })
1309 } else {
1310 None
1311 }
1312 } else {
1313 None
1314 }
1315 } else {
1316 None
1317 }
1318 } else {
1319 None
1320 }
1321 } else {
1322 None
1323 };
1324
1325 let asset_kind = if archive_url.ends_with(".zip") {
1326 AssetKind::Zip
1327 } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") {
1328 AssetKind::TarGz
1329 } else {
1330 anyhow::bail!("unsupported archive type in URL: {}", archive_url);
1331 };
1332
1333 ::http_client::github_download::download_server_binary(
1334 &*http_client,
1335 archive_url,
1336 sha256.as_deref(),
1337 &version_dir,
1338 asset_kind,
1339 )
1340 .await?;
1341 }
1342
1343 let cmd = &target_config.cmd;
1344
1345 let cmd_path = if cmd == "node" {
1346 node_runtime.binary_path().await?
1347 } else {
1348 if cmd.contains("..") {
1349 anyhow::bail!("command path cannot contain '..': {}", cmd);
1350 }
1351
1352 if cmd.starts_with("./") || cmd.starts_with(".\\") {
1353 let cmd_path = version_dir.join(&cmd[2..]);
1354 anyhow::ensure!(
1355 fs.is_file(&cmd_path).await,
1356 "Missing command {} after extraction",
1357 cmd_path.to_string_lossy()
1358 );
1359 cmd_path
1360 } else {
1361 anyhow::bail!("command must be relative (start with './'): {}", cmd);
1362 }
1363 };
1364
1365 let command = AgentServerCommand {
1366 path: cmd_path,
1367 args: target_config.args.clone(),
1368 env: Some(env),
1369 };
1370
1371 Ok(command)
1372 })
1373 }
1374
1375 fn as_any_mut(&mut self) -> &mut dyn Any {
1376 self
1377 }
1378}
1379
1380struct LocalRegistryNpxAgent {
1381 node_runtime: NodeRuntime,
1382 project_environment: Entity<ProjectEnvironment>,
1383 package: SharedString,
1384 args: Vec<String>,
1385 distribution_env: HashMap<String, String>,
1386 settings_env: HashMap<String, String>,
1387}
1388
1389impl ExternalAgentServer for LocalRegistryNpxAgent {
1390 fn get_command(
1391 &mut self,
1392 extra_env: HashMap<String, String>,
1393 _status_tx: Option<watch::Sender<SharedString>>,
1394 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1395 cx: &mut AsyncApp,
1396 ) -> Task<Result<AgentServerCommand>> {
1397 let node_runtime = self.node_runtime.clone();
1398 let project_environment = self.project_environment.downgrade();
1399 let package = self.package.clone();
1400 let args = self.args.clone();
1401 let distribution_env = self.distribution_env.clone();
1402 let settings_env = self.settings_env.clone();
1403
1404 cx.spawn(async move |cx| {
1405 let mut env = project_environment
1406 .update(cx, |project_environment, cx| {
1407 project_environment.local_directory_environment(
1408 &Shell::System,
1409 paths::home_dir().as_path().into(),
1410 cx,
1411 )
1412 })?
1413 .await
1414 .unwrap_or_default();
1415
1416 let mut exec_args = Vec::new();
1417 exec_args.push("--yes".to_string());
1418 exec_args.push(package.to_string());
1419 if !args.is_empty() {
1420 exec_args.push("--".to_string());
1421 exec_args.extend(args);
1422 }
1423
1424 let npm_command = node_runtime
1425 .npm_command(
1426 "exec",
1427 &exec_args.iter().map(|a| a.as_str()).collect::<Vec<_>>(),
1428 )
1429 .await?;
1430
1431 env.extend(npm_command.env);
1432 env.extend(distribution_env);
1433 env.extend(extra_env);
1434 env.extend(settings_env);
1435
1436 let command = AgentServerCommand {
1437 path: npm_command.path,
1438 args: npm_command.args,
1439 env: Some(env),
1440 };
1441
1442 Ok(command)
1443 })
1444 }
1445
1446 fn as_any_mut(&mut self) -> &mut dyn Any {
1447 self
1448 }
1449}
1450
1451struct LocalCustomAgent {
1452 project_environment: Entity<ProjectEnvironment>,
1453 command: AgentServerCommand,
1454}
1455
1456impl ExternalAgentServer for LocalCustomAgent {
1457 fn get_command(
1458 &mut self,
1459 extra_env: HashMap<String, String>,
1460 _status_tx: Option<watch::Sender<SharedString>>,
1461 _new_version_available_tx: Option<watch::Sender<Option<String>>>,
1462 cx: &mut AsyncApp,
1463 ) -> Task<Result<AgentServerCommand>> {
1464 let mut command = self.command.clone();
1465 let project_environment = self.project_environment.downgrade();
1466 cx.spawn(async move |cx| {
1467 let mut env = project_environment
1468 .update(cx, |project_environment, cx| {
1469 project_environment.local_directory_environment(
1470 &Shell::System,
1471 paths::home_dir().as_path().into(),
1472 cx,
1473 )
1474 })?
1475 .await
1476 .unwrap_or_default();
1477 env.extend(command.env.unwrap_or_default());
1478 env.extend(extra_env);
1479 command.env = Some(env);
1480 Ok(command)
1481 })
1482 }
1483
1484 fn as_any_mut(&mut self) -> &mut dyn Any {
1485 self
1486 }
1487}
1488
1489pub const GEMINI_NAME: &str = "gemini";
1490pub const CLAUDE_AGENT_NAME: &str = "claude-acp";
1491pub const CODEX_NAME: &str = "codex-acp";
1492
1493#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)]
1494pub struct AllAgentServersSettings(pub HashMap<String, CustomAgentServerSettings>);
1495
1496impl std::ops::Deref for AllAgentServersSettings {
1497 type Target = HashMap<String, CustomAgentServerSettings>;
1498
1499 fn deref(&self) -> &Self::Target {
1500 &self.0
1501 }
1502}
1503
1504impl std::ops::DerefMut for AllAgentServersSettings {
1505 fn deref_mut(&mut self) -> &mut Self::Target {
1506 &mut self.0
1507 }
1508}
1509
1510impl AllAgentServersSettings {
1511 pub fn has_registry_agents(&self) -> bool {
1512 self.values()
1513 .any(|s| matches!(s, CustomAgentServerSettings::Registry { .. }))
1514 }
1515}
1516
1517#[derive(Clone, JsonSchema, Debug, PartialEq)]
1518pub enum CustomAgentServerSettings {
1519 Custom {
1520 command: AgentServerCommand,
1521 /// The default mode to use for this agent.
1522 ///
1523 /// Note: Not only all agents support modes.
1524 ///
1525 /// Default: None
1526 default_mode: Option<String>,
1527 /// The default model to use for this agent.
1528 ///
1529 /// This should be the model ID as reported by the agent.
1530 ///
1531 /// Default: None
1532 default_model: Option<String>,
1533 /// The favorite models for this agent.
1534 ///
1535 /// Default: []
1536 favorite_models: Vec<String>,
1537 /// Default values for session config options.
1538 ///
1539 /// This is a map from config option ID to value ID.
1540 ///
1541 /// Default: {}
1542 default_config_options: HashMap<String, String>,
1543 /// Favorited values for session config options.
1544 ///
1545 /// This is a map from config option ID to a list of favorited value IDs.
1546 ///
1547 /// Default: {}
1548 favorite_config_option_values: HashMap<String, Vec<String>>,
1549 },
1550 Extension {
1551 /// Additional environment variables to pass to the agent.
1552 ///
1553 /// Default: {}
1554 env: HashMap<String, String>,
1555 /// The default mode to use for this agent.
1556 ///
1557 /// Note: Not only all agents support modes.
1558 ///
1559 /// Default: None
1560 default_mode: Option<String>,
1561 /// The default model to use for this agent.
1562 ///
1563 /// This should be the model ID as reported by the agent.
1564 ///
1565 /// Default: None
1566 default_model: Option<String>,
1567 /// The favorite models for this agent.
1568 ///
1569 /// Default: []
1570 favorite_models: Vec<String>,
1571 /// Default values for session config options.
1572 ///
1573 /// This is a map from config option ID to value ID.
1574 ///
1575 /// Default: {}
1576 default_config_options: HashMap<String, String>,
1577 /// Favorited values for session config options.
1578 ///
1579 /// This is a map from config option ID to a list of favorited value IDs.
1580 ///
1581 /// Default: {}
1582 favorite_config_option_values: HashMap<String, Vec<String>>,
1583 },
1584 Registry {
1585 /// Additional environment variables to pass to the agent.
1586 ///
1587 /// Default: {}
1588 env: HashMap<String, String>,
1589 /// The default mode to use for this agent.
1590 ///
1591 /// Note: Not only all agents support modes.
1592 ///
1593 /// Default: None
1594 default_mode: Option<String>,
1595 /// The default model to use for this agent.
1596 ///
1597 /// This should be the model ID as reported by the agent.
1598 ///
1599 /// Default: None
1600 default_model: Option<String>,
1601 /// The favorite models for this agent.
1602 ///
1603 /// Default: []
1604 favorite_models: Vec<String>,
1605 /// Default values for session config options.
1606 ///
1607 /// This is a map from config option ID to value ID.
1608 ///
1609 /// Default: {}
1610 default_config_options: HashMap<String, String>,
1611 /// Favorited values for session config options.
1612 ///
1613 /// This is a map from config option ID to a list of favorited value IDs.
1614 ///
1615 /// Default: {}
1616 favorite_config_option_values: HashMap<String, Vec<String>>,
1617 },
1618}
1619
1620impl CustomAgentServerSettings {
1621 pub fn command(&self) -> Option<&AgentServerCommand> {
1622 match self {
1623 CustomAgentServerSettings::Custom { command, .. } => Some(command),
1624 CustomAgentServerSettings::Extension { .. }
1625 | CustomAgentServerSettings::Registry { .. } => None,
1626 }
1627 }
1628
1629 pub fn default_mode(&self) -> Option<&str> {
1630 match self {
1631 CustomAgentServerSettings::Custom { default_mode, .. }
1632 | CustomAgentServerSettings::Extension { default_mode, .. }
1633 | CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
1634 }
1635 }
1636
1637 pub fn default_model(&self) -> Option<&str> {
1638 match self {
1639 CustomAgentServerSettings::Custom { default_model, .. }
1640 | CustomAgentServerSettings::Extension { default_model, .. }
1641 | CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
1642 }
1643 }
1644
1645 pub fn favorite_models(&self) -> &[String] {
1646 match self {
1647 CustomAgentServerSettings::Custom {
1648 favorite_models, ..
1649 }
1650 | CustomAgentServerSettings::Extension {
1651 favorite_models, ..
1652 }
1653 | CustomAgentServerSettings::Registry {
1654 favorite_models, ..
1655 } => favorite_models,
1656 }
1657 }
1658
1659 pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
1660 match self {
1661 CustomAgentServerSettings::Custom {
1662 default_config_options,
1663 ..
1664 }
1665 | CustomAgentServerSettings::Extension {
1666 default_config_options,
1667 ..
1668 }
1669 | CustomAgentServerSettings::Registry {
1670 default_config_options,
1671 ..
1672 } => default_config_options.get(config_id).map(|s| s.as_str()),
1673 }
1674 }
1675
1676 pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
1677 match self {
1678 CustomAgentServerSettings::Custom {
1679 favorite_config_option_values,
1680 ..
1681 }
1682 | CustomAgentServerSettings::Extension {
1683 favorite_config_option_values,
1684 ..
1685 }
1686 | CustomAgentServerSettings::Registry {
1687 favorite_config_option_values,
1688 ..
1689 } => favorite_config_option_values
1690 .get(config_id)
1691 .map(|v| v.as_slice()),
1692 }
1693 }
1694}
1695
1696impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
1697 fn from(value: settings::CustomAgentServerSettings) -> Self {
1698 match value {
1699 settings::CustomAgentServerSettings::Custom {
1700 path,
1701 args,
1702 env,
1703 default_mode,
1704 default_model,
1705 favorite_models,
1706 default_config_options,
1707 favorite_config_option_values,
1708 } => CustomAgentServerSettings::Custom {
1709 command: AgentServerCommand {
1710 path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
1711 args,
1712 env: Some(env),
1713 },
1714 default_mode,
1715 default_model,
1716 favorite_models,
1717 default_config_options,
1718 favorite_config_option_values,
1719 },
1720 settings::CustomAgentServerSettings::Extension {
1721 env,
1722 default_mode,
1723 default_model,
1724 default_config_options,
1725 favorite_models,
1726 favorite_config_option_values,
1727 } => CustomAgentServerSettings::Extension {
1728 env,
1729 default_mode,
1730 default_model,
1731 default_config_options,
1732 favorite_models,
1733 favorite_config_option_values,
1734 },
1735 settings::CustomAgentServerSettings::Registry {
1736 env,
1737 default_mode,
1738 default_model,
1739 default_config_options,
1740 favorite_models,
1741 favorite_config_option_values,
1742 } => CustomAgentServerSettings::Registry {
1743 env,
1744 default_mode,
1745 default_model,
1746 default_config_options,
1747 favorite_models,
1748 favorite_config_option_values,
1749 },
1750 }
1751 }
1752}
1753
1754impl settings::Settings for AllAgentServersSettings {
1755 fn from_settings(content: &settings::SettingsContent) -> Self {
1756 let agent_settings = content.agent_servers.clone().unwrap();
1757 Self(
1758 agent_settings
1759 .0
1760 .into_iter()
1761 .map(|(k, v)| (k, v.into()))
1762 .collect(),
1763 )
1764 }
1765}