agentlink-domain 0.0.2

Domain model for agentlink: one brain for every AI coding agent
Documentation
//! The set of agents agentlink knows how to serve.
//!
//! Manifests shipped in this crate's `providers/` directory are embedded at
//! build time — see `build.rs` for why that directory must live inside the
//! crate rather than at the workspace root — so the binary
//! has no data files to install and no runtime lookup path to get wrong. A
//! workspace may add or override providers by dropping manifests into
//! `.agentlink/providers/`, which is how someone tries out support for a new
//! agent before sending a pull request.

use crate::layout::Layout;
use crate::provider::{ManifestError, Provider};

// Generated by `build.rs`, which enumerates this crate's `providers/*.toml`.
// New agents are
// picked up by adding a file — there is no list to remember to update.
include!(concat!(env!("OUT_DIR"), "/builtin_manifests.rs"));

/// A validated collection of providers, keyed by id.
#[derive(Debug, Clone, Default)]
pub struct Registry {
    providers: Vec<Provider>,
}

/// A provider id that no manifest defines.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unknown provider `{id}` (known: {})", known.join(", "))]
pub struct UnknownProvider {
    pub id: String,
    pub known: Vec<String>,
}

impl Registry {
    /// Loads the manifests embedded in this binary.
    pub fn builtin(layout: &Layout) -> Result<Self, ManifestError> {
        Self::load(
            layout,
            BUILTIN_MANIFESTS.iter().map(|(name, text)| (*name, *text)),
        )
    }

    /// Loads manifests, with later entries replacing earlier ones by id.
    ///
    /// Workspace-local manifests are passed after the built-ins so a user can
    /// correct a path that changed upstream without waiting for a release.
    pub fn load<'a>(
        layout: &Layout,
        sources: impl IntoIterator<Item = (&'a str, &'a str)>,
    ) -> Result<Self, ManifestError> {
        let mut registry = Self::default();
        for (name, text) in sources {
            let provider =
                crate::provider::parse(name, text, |kind| layout.canonical(kind).clone())?;
            registry.insert(provider);
        }
        registry.providers.sort_by(|a, b| a.id.cmp(&b.id));
        Ok(registry)
    }

    /// Loads the embedded manifests, then applies workspace-local ones on top.
    ///
    /// A local manifest with the same id wins. This is the escape hatch for a
    /// user whose agent changed its paths upstream: they can fix it in their own
    /// repository today and send the same file as a pull request afterwards.
    pub fn with_local<'a>(
        layout: &Layout,
        locals: impl IntoIterator<Item = (&'a str, &'a str)>,
    ) -> Result<Self, ManifestError> {
        let mut registry = Self::builtin(layout)?;
        for (name, text) in locals {
            let provider =
                crate::provider::parse(name, text, |kind| layout.canonical(kind).clone())?;
            registry.insert(provider);
        }
        registry.providers.sort_by(|a, b| a.id.cmp(&b.id));
        Ok(registry)
    }

    fn insert(&mut self, provider: Provider) {
        self.providers.retain(|existing| existing.id != provider.id);
        self.providers.push(provider);
    }

    pub fn all(&self) -> &[Provider] {
        &self.providers
    }

    pub fn get(&self, id: &str) -> Option<&Provider> {
        self.providers.iter().find(|provider| provider.id == id)
    }

    pub fn ids(&self) -> Vec<String> {
        self.providers
            .iter()
            .map(|provider| provider.id.clone())
            .collect()
    }

    /// Resolves a selection, where `None` means every known provider.
    pub fn select(&self, ids: Option<&[String]>) -> Result<Vec<&Provider>, UnknownProvider> {
        match ids {
            None => Ok(self.providers.iter().collect()),
            Some(ids) => ids
                .iter()
                .map(|id| {
                    self.get(id).ok_or_else(|| UnknownProvider {
                        id: id.clone(),
                        known: self.ids(),
                    })
                })
                .collect(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::{ResourceKind, Strategy};

    #[test]
    fn every_shipped_manifest_is_valid() {
        // This is the guard rail for provider contributions: a malformed or
        // dishonest manifest fails the build's test suite, not a user's machine.
        let registry = Registry::builtin(&Layout::default()).expect("shipped manifests must parse");
        assert!(!registry.all().is_empty(), "no providers were embedded");
    }

    #[test]
    fn shipped_manifests_have_unique_ids() {
        let registry = Registry::builtin(&Layout::default()).unwrap();
        let mut ids = registry.ids();
        let total = ids.len();
        ids.sort();
        ids.dedup();
        assert_eq!(
            ids.len(),
            total,
            "duplicate provider ids among shipped manifests"
        );
    }

    #[test]
    fn the_reference_providers_are_present_and_correct() {
        let registry = Registry::builtin(&Layout::default()).unwrap();

        // Claude Code is the one major agent that does not read AGENTS.md, so it
        // must declare a materialised capability, never `native`.
        let claude = registry.get("claude-code").expect("claude-code manifest");
        let instructions = claude.capability(ResourceKind::Instructions).unwrap();
        assert_eq!(instructions.strategy, Strategy::Link);
        assert_eq!(instructions.path.as_str(), "CLAUDE.md");
        assert!(
            instructions.has_import_fallback(),
            "CLAUDE.md cannot be symlinked on unprivileged Windows, so a fallback is required"
        );

        // Antigravity defines the canonical layout, so both capabilities are free.
        let agy = registry.get("antigravity").expect("antigravity manifest");
        for &resource in ResourceKind::ALL {
            assert_eq!(
                agy.capability(resource).unwrap().strategy,
                Strategy::Native,
                "antigravity should need no work for {resource}"
            );
        }
    }

    #[test]
    fn most_capabilities_need_no_writes_at_all() {
        // The project's central claim, asserted rather than merely documented:
        // because the canonical layout matches what the ecosystem converged on,
        // the majority of shipped capabilities are `native`.
        let registry = Registry::builtin(&Layout::default()).unwrap();
        let (native, total) = registry.all().iter().flat_map(|p| &p.capabilities).fold(
            (0, 0),
            |(native, total), cap| {
                (
                    native + usize::from(cap.strategy == Strategy::Native),
                    total + 1,
                )
            },
        );
        assert!(
            native * 2 > total,
            "expected a majority of native capabilities, got {native}/{total}"
        );
    }

    #[test]
    fn local_manifests_override_shipped_ones_by_id() {
        let layout = Layout::default();
        let registry = Registry::load(
            &layout,
            [
                ("a.toml", "schema = 1\nid = \"dup\"\nname = \"First\"\n"),
                ("b.toml", "schema = 1\nid = \"dup\"\nname = \"Second\"\n"),
            ],
        )
        .unwrap();
        assert_eq!(registry.all().len(), 1);
        assert_eq!(registry.get("dup").unwrap().name, "Second");
    }

    #[test]
    fn selecting_an_unknown_provider_lists_the_valid_ones() {
        let registry = Registry::builtin(&Layout::default()).unwrap();
        let err = registry.select(Some(&["nope".to_string()])).unwrap_err();
        assert_eq!(err.id, "nope");
        assert!(err.known.contains(&"claude-code".to_string()));
    }

    #[test]
    fn selecting_none_yields_every_provider() {
        let registry = Registry::builtin(&Layout::default()).unwrap();
        assert_eq!(registry.select(None).unwrap().len(), registry.all().len());
    }
}