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