Skip to main content

agentlink_domain/
registry.rs

1//! The set of agents agentlink knows how to serve.
2//!
3//! Manifests shipped in this crate's `providers/` directory are embedded at
4//! build time — see `build.rs` for why that directory must live inside the
5//! crate rather than at the workspace root — so the binary
6//! has no data files to install and no runtime lookup path to get wrong. A
7//! workspace may add or override providers by dropping manifests into
8//! `.agentlink/providers/`, which is how someone tries out support for a new
9//! agent before sending a pull request.
10
11use crate::layout::Layout;
12use crate::provider::{ManifestError, Provider};
13
14// Generated by `build.rs`, which enumerates this crate's `providers/*.toml`.
15// New agents are
16// picked up by adding a file — there is no list to remember to update.
17include!(concat!(env!("OUT_DIR"), "/builtin_manifests.rs"));
18
19/// A validated collection of providers, keyed by id.
20#[derive(Debug, Clone, Default)]
21pub struct Registry {
22    providers: Vec<Provider>,
23}
24
25/// A provider id that no manifest defines.
26#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
27#[error("unknown provider `{id}` (known: {})", known.join(", "))]
28pub struct UnknownProvider {
29    pub id: String,
30    pub known: Vec<String>,
31}
32
33impl Registry {
34    /// Loads the manifests embedded in this binary.
35    pub fn builtin(layout: &Layout) -> Result<Self, ManifestError> {
36        Self::load(
37            layout,
38            BUILTIN_MANIFESTS.iter().map(|(name, text)| (*name, *text)),
39        )
40    }
41
42    /// Loads manifests, with later entries replacing earlier ones by id.
43    ///
44    /// Workspace-local manifests are passed after the built-ins so a user can
45    /// correct a path that changed upstream without waiting for a release.
46    pub fn load<'a>(
47        layout: &Layout,
48        sources: impl IntoIterator<Item = (&'a str, &'a str)>,
49    ) -> Result<Self, ManifestError> {
50        let mut registry = Self::default();
51        for (name, text) in sources {
52            let provider =
53                crate::provider::parse(name, text, |kind| layout.canonical(kind).clone())?;
54            registry.insert(provider);
55        }
56        registry.providers.sort_by(|a, b| a.id.cmp(&b.id));
57        Ok(registry)
58    }
59
60    /// Loads the embedded manifests, then applies workspace-local ones on top.
61    ///
62    /// A local manifest with the same id wins. This is the escape hatch for a
63    /// user whose agent changed its paths upstream: they can fix it in their own
64    /// repository today and send the same file as a pull request afterwards.
65    pub fn with_local<'a>(
66        layout: &Layout,
67        locals: impl IntoIterator<Item = (&'a str, &'a str)>,
68    ) -> Result<Self, ManifestError> {
69        let mut registry = Self::builtin(layout)?;
70        for (name, text) in locals {
71            let provider =
72                crate::provider::parse(name, text, |kind| layout.canonical(kind).clone())?;
73            registry.insert(provider);
74        }
75        registry.providers.sort_by(|a, b| a.id.cmp(&b.id));
76        Ok(registry)
77    }
78
79    fn insert(&mut self, provider: Provider) {
80        self.providers.retain(|existing| existing.id != provider.id);
81        self.providers.push(provider);
82    }
83
84    pub fn all(&self) -> &[Provider] {
85        &self.providers
86    }
87
88    pub fn get(&self, id: &str) -> Option<&Provider> {
89        self.providers.iter().find(|provider| provider.id == id)
90    }
91
92    pub fn ids(&self) -> Vec<String> {
93        self.providers
94            .iter()
95            .map(|provider| provider.id.clone())
96            .collect()
97    }
98
99    /// Resolves a selection, where `None` means every known provider.
100    pub fn select(&self, ids: Option<&[String]>) -> Result<Vec<&Provider>, UnknownProvider> {
101        match ids {
102            None => Ok(self.providers.iter().collect()),
103            Some(ids) => ids
104                .iter()
105                .map(|id| {
106                    self.get(id).ok_or_else(|| UnknownProvider {
107                        id: id.clone(),
108                        known: self.ids(),
109                    })
110                })
111                .collect(),
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::model::{ResourceKind, Strategy};
120
121    #[test]
122    fn every_shipped_manifest_is_valid() {
123        // This is the guard rail for provider contributions: a malformed or
124        // dishonest manifest fails the build's test suite, not a user's machine.
125        let registry = Registry::builtin(&Layout::default()).expect("shipped manifests must parse");
126        assert!(!registry.all().is_empty(), "no providers were embedded");
127    }
128
129    #[test]
130    fn shipped_manifests_have_unique_ids() {
131        let registry = Registry::builtin(&Layout::default()).unwrap();
132        let mut ids = registry.ids();
133        let total = ids.len();
134        ids.sort();
135        ids.dedup();
136        assert_eq!(
137            ids.len(),
138            total,
139            "duplicate provider ids among shipped manifests"
140        );
141    }
142
143    #[test]
144    fn the_reference_providers_are_present_and_correct() {
145        let registry = Registry::builtin(&Layout::default()).unwrap();
146
147        // Claude Code is the one major agent that does not read AGENTS.md, so it
148        // must declare a materialised capability, never `native`.
149        let claude = registry.get("claude-code").expect("claude-code manifest");
150        let instructions = claude.capability(ResourceKind::Instructions).unwrap();
151        assert_eq!(instructions.strategy, Strategy::Link);
152        assert_eq!(instructions.path.as_str(), "CLAUDE.md");
153        assert!(
154            instructions.has_import_fallback(),
155            "CLAUDE.md cannot be symlinked on unprivileged Windows, so a fallback is required"
156        );
157
158        // Antigravity defines the canonical layout, so both capabilities are free.
159        let agy = registry.get("antigravity").expect("antigravity manifest");
160        for &resource in ResourceKind::ALL {
161            assert_eq!(
162                agy.capability(resource).unwrap().strategy,
163                Strategy::Native,
164                "antigravity should need no work for {resource}"
165            );
166        }
167    }
168
169    #[test]
170    fn most_capabilities_need_no_writes_at_all() {
171        // The project's central claim, asserted rather than merely documented:
172        // because the canonical layout matches what the ecosystem converged on,
173        // the majority of shipped capabilities are `native`.
174        let registry = Registry::builtin(&Layout::default()).unwrap();
175        let (native, total) = registry.all().iter().flat_map(|p| &p.capabilities).fold(
176            (0, 0),
177            |(native, total), cap| {
178                (
179                    native + usize::from(cap.strategy == Strategy::Native),
180                    total + 1,
181                )
182            },
183        );
184        assert!(
185            native * 2 > total,
186            "expected a majority of native capabilities, got {native}/{total}"
187        );
188    }
189
190    #[test]
191    fn local_manifests_override_shipped_ones_by_id() {
192        let layout = Layout::default();
193        let registry = Registry::load(
194            &layout,
195            [
196                ("a.toml", "schema = 1\nid = \"dup\"\nname = \"First\"\n"),
197                ("b.toml", "schema = 1\nid = \"dup\"\nname = \"Second\"\n"),
198            ],
199        )
200        .unwrap();
201        assert_eq!(registry.all().len(), 1);
202        assert_eq!(registry.get("dup").unwrap().name, "Second");
203    }
204
205    #[test]
206    fn selecting_an_unknown_provider_lists_the_valid_ones() {
207        let registry = Registry::builtin(&Layout::default()).unwrap();
208        let err = registry.select(Some(&["nope".to_string()])).unwrap_err();
209        assert_eq!(err.id, "nope");
210        assert!(err.known.contains(&"claude-code".to_string()));
211    }
212
213    #[test]
214    fn selecting_none_yields_every_provider() {
215        let registry = Registry::builtin(&Layout::default()).unwrap();
216        assert_eq!(registry.select(None).unwrap().len(), registry.all().len());
217    }
218}