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