//! Reference profile **catalogue** — the pre-filled, *editable* profiles offered //! by the first-run wizard (CONTEXT §9, ARCHITECTURE §6 `ConfigureProfiles`). //! //! These are **data, not domain code**: the catalogue lives in the application //! layer (a product decision about *which* AIs to suggest), built from the //! domain's validating constructors. Nothing is imposed — the user picks, edits //! the pre-filled commands, and may add custom profiles. The single //! [`domain::ports::AgentRuntime`] adapter consumes whatever profiles result. //! //! Reference set (CONTEXT §9): //! - **Claude Code** — `claude`, context via `CLAUDE.md` (convention file), //! - **OpenAI Codex CLI** — `codex`, context via `AGENTS.md`, //! - **Gemini CLI** — `gemini`, context via `GEMINI.md`, //! - **Aider** — `aider`, context passed as an argument (`--message-file {path}`). //! //! The ids are **stable, deterministic UUIDs** (derived from a fixed namespace) //! so re-deriving the catalogue yields the same id for "claude" every time, //! making the reference profiles addressable across runs without a registry. use domain::ids::ProfileId; use domain::permission::ProjectorKey; use domain::profile::{ AgentProfile, ContextInjection, McpCapability, McpConfigStrategy, McpTransport, StructuredAdapter, }; /// A fixed UUID namespace used to derive stable ids for reference profiles. /// (Random-looking but constant; only its stability matters.) const REFERENCE_NAMESPACE: uuid::Uuid = uuid::uuid!("6f9b1d2a-7c34-4e58-9a1b-2c3d4e5f6a7b"); /// Derives a stable [`ProfileId`] for a reference profile from its slug. #[must_use] fn reference_id(slug: &str) -> ProfileId { ProfileId::from_uuid(uuid::Uuid::new_v5(&REFERENCE_NAMESPACE, slug.as_bytes())) } /// Returns the stable id a reference profile slug maps to (exposed for tests and /// callers that need to address a reference profile). #[must_use] pub fn reference_profile_id(slug: &str) -> ProfileId { reference_id(slug) } /// Builds the pre-filled, editable reference profiles (CONTEXT §9). /// /// # Panics /// Never in practice: every literal here satisfies the domain invariants, so the /// constructors cannot fail; the `expect`s document that. #[must_use] pub fn reference_profiles() -> Vec { vec![ AgentProfile::new( reference_id("claude"), "Claude Code", "claude", Vec::new(), ContextInjection::convention_file("CLAUDE.md") .expect("CLAUDE.md is a valid convention target"), Some("claude --version".to_owned()), "{agentRunDir}", None, ) .expect("claude reference profile is valid") .with_structured_adapter(StructuredAdapter::Claude) .with_projector(ProjectorKey::Claude) .with_mcp(McpCapability::new( McpConfigStrategy::config_file(".mcp.json") .expect(".mcp.json is a valid relative MCP config target"), McpTransport::Stdio, )), AgentProfile::new( reference_id("codex"), "OpenAI Codex CLI", "codex", Vec::new(), ContextInjection::convention_file("AGENTS.md") .expect("AGENTS.md is a valid convention target"), Some("codex --version".to_owned()), "{agentRunDir}", None, ) .expect("codex reference profile is valid") .with_structured_adapter(StructuredAdapter::Codex) .with_projector(ProjectorKey::Codex) .with_mcp(McpCapability::new( // Codex lit ses serveurs MCP dans `$CODEX_HOME/config.toml`, pas `.mcp.json` : // IdeA écrit ce TOML DANS le run dir et pointe `CODEX_HOME` dessus pour // isoler l'agent du `~/.codex` global (miroir du `.mcp.json` de Claude). McpConfigStrategy::toml_config_home(".codex/config.toml", "CODEX_HOME") .expect(".codex/config.toml + CODEX_HOME is a valid MCP config target"), McpTransport::Stdio, )), AgentProfile::new( reference_id("gemini"), "Gemini CLI", "gemini", Vec::new(), ContextInjection::convention_file("GEMINI.md") .expect("GEMINI.md is a valid convention target"), Some("gemini --version".to_owned()), "{agentRunDir}", None, ) .expect("gemini reference profile is valid"), AgentProfile::new( reference_id("aider"), "Aider", "aider", Vec::new(), ContextInjection::flag("--message-file {path}") .expect("aider flag template is non-empty"), Some("aider --version".to_owned()), "{agentRunDir}", None, ) .expect("aider reference profile is valid"), ] } /// Returns the **selectable** subset of [`reference_profiles`] — the profiles the /// first-run wizard and the agent-creation menu are allowed to offer (§17.3, /// lot D7). /// /// A profile is selectable iff it can be driven in **structured** mode /// ([`AgentProfile::is_selectable`] = it carries a `structured_adapter`). Today /// that is Claude + Codex; Gemini/Aider stay in [`reference_profiles`] (the data /// catalogue is untouched) but are **not** proposed for selection. There is no /// custom-profile entry here either: the selection path offers only profiles we /// know how to pilot. /// /// This filter is the single selection gate; `is_selectable` is the same /// predicate the `AgentSessionFactory` uses to decide it `supports` a profile, so /// the menu and the runtime can never disagree. #[must_use] pub fn selectable_reference_profiles() -> Vec { reference_profiles() .into_iter() .filter(AgentProfile::is_selectable) .collect() } #[cfg(test)] mod mcp_tests { use super::*; fn profile(slug: &str) -> AgentProfile { let id = reference_id(slug); reference_profiles() .into_iter() .find(|p| p.id == id) .unwrap_or_else(|| panic!("reference profile `{slug}` exists")) } #[test] fn claude_and_codex_expose_mcp_capability() { for slug in ["claude", "codex"] { let p = profile(slug); assert!( p.mcp.is_some(), "structured profile `{slug}` must carry an MCP capability" ); } } #[test] fn claude_mcp_uses_config_file_mcp_json() { let mcp = profile("claude").mcp.expect("mcp present"); assert_eq!( mcp.config, McpConfigStrategy::ConfigFile { target: ".mcp.json".to_owned() }, "Claude should declare `.mcp.json`" ); assert_eq!(mcp.transport, McpTransport::Stdio); } #[test] fn codex_mcp_uses_toml_config_home_codex() { // Codex lit `$CODEX_HOME/config.toml`, pas `.mcp.json` : le seed doit déclarer // la stratégie TOML isolée par `CODEX_HOME` (pendant Codex de Claude). let mcp = profile("codex").mcp.expect("mcp present"); assert_eq!( mcp.config, McpConfigStrategy::TomlConfigHome { target: ".codex/config.toml".to_owned(), home_env: "CODEX_HOME".to_owned(), }, "Codex should declare `.codex/config.toml` + CODEX_HOME" ); assert_eq!(mcp.transport, McpTransport::Stdio); assert!( profile("codex").materializes_idea_bridge(), "the Codex seed must materialise the idea bridge" ); } #[test] fn gemini_and_aider_have_no_mcp_capability() { for slug in ["gemini", "aider"] { assert!( profile(slug).mcp.is_none(), "non-structured profile `{slug}` must NOT carry MCP (file fallback)" ); } } // -- Lot LP3 : projector (clé du projecteur de permissions par-CLI) ---------- #[test] fn claude_and_codex_seed_their_projector_key() { assert_eq!( profile("claude").projector, Some(ProjectorKey::Claude), "the Claude seed must pose the Claude projector" ); assert_eq!( profile("codex").projector, Some(ProjectorKey::Codex), "the Codex seed must pose the Codex projector" ); } #[test] fn gemini_and_aider_have_no_projector() { for slug in ["gemini", "aider"] { assert!( profile(slug).projector.is_none(), "non-structured profile `{slug}` must NOT carry a projector (native prompting)" ); } } }