Jalon vert regroupant deux chantiers entrelacés dans le working tree, indissociables au niveau fichier mais tous deux verts (cargo test --workspace + tests frontend permissions au vert). Permissions — voie « projection CLI » (advisory), complète : - LP0 domaine pur : modèle PermissionSet/EffectivePermissions, resolve deny-wins + postures Allow<Ask<Deny (crates/domain/src/permission.rs). - LP1 store : FsPermissionStore (.ideai/permissions.json). - LP2 use cases : Get/Update project, Update agent override, Resolve. - LP3 projecteurs Claude/Codex (settings.local.json / config.toml), câblage launch-path + PermissionProjectorRegistry, nettoyage des fichiers Replace orphelins au swap de profil (LP3-4), composition root + commandes Tauri, UI PermissionsPanel (projet + override agent). - ports.rs : PermissionStore + FileSystem::remove_file (cleanup au swap). Reste ouvert (hors scope, marqué dans le code) : LP4 enforcement OS airtight (Landlock fichiers) + résumé de permissions injecté. Inclut aussi le chantier Codex/input/sessions structurées en cours (McpConfigStrategy, StructuredAdapter, gestion d'input) partageant les mêmes fichiers (lifecycle.rs, commands.rs, dto.rs, state.rs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
234 lines
8.6 KiB
Rust
234 lines
8.6 KiB
Rust
//! 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<AgentProfile> {
|
|
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<AgentProfile> {
|
|
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)"
|
|
);
|
|
}
|
|
}
|
|
}
|