Files
IdeA/crates/domain/src/template.rs
Blomios d6c80be56d feat(templates): nom d'agent par défaut + permissions système et MCP outils sur AgentTemplate — #242
AgentTemplate étendu (defaultAgentName, systemPermissions, mcpToolPermissions),
CreateTemplate/UpdateTemplate/CreateAgentFromTemplate projetés vers l'agent créé,
DTO/MCP tools et persistance store complétés. Correctif modèle par défaut:
UpdateTemplate ne persistait pas defaultProfileId (profil stale à la création
d'agent). Backfill best-effort des templates existants depuis les agents du
projet au open_project, overrides explicites uniquement.
2026-08-23 10:59:55 +02:00

143 lines
4.7 KiB
Rust

//! Agent templates (global IDE store) and their monotonic versioning.
use serde::{Deserialize, Serialize};
use crate::error::DomainError;
use crate::ids::{ProfileId, TemplateId};
use crate::markdown::MarkdownDoc;
use crate::mcp_tool_permissions::McpToolPolicy;
use crate::system_permissions::SystemPermissionSet;
/// Monotonically increasing template version.
///
/// Invariant (enforced via [`TemplateVersion::next`]): a version only ever
/// increases by one when the template content changes (ARCHITECTURE §8.1).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct TemplateVersion(pub u64);
impl TemplateVersion {
/// The initial version assigned to a freshly created template.
pub const INITIAL: Self = Self(1);
/// Returns the next version (current + 1).
#[must_use]
pub const fn next(self) -> Self {
Self(self.0 + 1)
}
/// Returns the raw version number.
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
}
impl Default for TemplateVersion {
fn default() -> Self {
Self::INITIAL
}
}
/// A reusable agent template stored in the global IDE store.
///
/// Invariants:
/// - `name` non-empty,
/// - `version` is monotonic; bumping is done via [`AgentTemplate::with_updated_content`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentTemplate {
/// Stable identifier.
pub id: TemplateId,
/// Display name.
pub name: String,
/// Markdown content.
pub content_md: MarkdownDoc,
/// Current version.
pub version: TemplateVersion,
/// Default runtime profile for agents created from this template.
pub default_profile_id: ProfileId,
/// Default agent name for agents created from this template. `None` falls
/// back to the template display name.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_agent_name: Option<String>,
/// Agent-level system permission override to apply at creation time.
/// `None` preserves project inheritance.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_system_permissions: Option<SystemPermissionSet>,
/// Agent-level MCP tool policy override to apply at creation time.
/// `None` preserves project inheritance.
#[serde(
default,
rename = "defaultMcpToolPermissions",
alias = "defaultMcpToolPolicy",
skip_serializing_if = "Option::is_none"
)]
pub default_mcp_tool_policy: Option<McpToolPolicy>,
}
impl AgentTemplate {
/// Builds a validated template at [`TemplateVersion::INITIAL`].
///
/// # Errors
/// Returns [`DomainError::EmptyField`] if `name` is empty.
pub fn new(
id: TemplateId,
name: impl Into<String>,
content_md: MarkdownDoc,
default_profile_id: ProfileId,
) -> Result<Self, DomainError> {
let name = name.into();
crate::validation::non_empty(&name, "template.name")?;
Ok(Self {
id,
name,
content_md,
version: TemplateVersion::INITIAL,
default_profile_id,
default_agent_name: None,
default_system_permissions: None,
default_mcp_tool_policy: None,
})
}
/// Returns a copy of this template with new content and the version bumped
/// by one, preserving the monotonic-version invariant.
#[must_use]
pub fn with_updated_content(&self, content_md: MarkdownDoc) -> Self {
Self {
content_md,
version: self.version.next(),
..self.clone()
}
}
/// Returns a copy with template metadata changed. The template version is
/// left unchanged because these defaults affect future agent creation only.
///
/// # Errors
/// Returns [`DomainError::EmptyField`] if `name` or a provided default agent
/// name is empty.
pub fn with_metadata(
&self,
name: String,
default_profile_id: ProfileId,
default_agent_name: Option<String>,
default_system_permissions: Option<SystemPermissionSet>,
default_mcp_tool_policy: Option<McpToolPolicy>,
) -> Result<Self, DomainError> {
crate::validation::non_empty(&name, "template.name")?;
if let Some(default_agent_name) = &default_agent_name {
crate::validation::non_empty(default_agent_name, "template.defaultAgentName")?;
}
Ok(Self {
name,
default_profile_id,
default_agent_name,
default_system_permissions,
default_mcp_tool_policy,
..self.clone()
})
}
}