//! 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, /// 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, /// 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, } 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, content_md: MarkdownDoc, default_profile_id: ProfileId, ) -> Result { 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, default_system_permissions: Option, default_mcp_tool_policy: Option, ) -> Result { 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() }) } }