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