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