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.
This commit is contained in:
@ -6,6 +6,7 @@
|
||||
//! TypeScript side sees idiomatic camelCase. This matches the persisted-domain
|
||||
//! JSON convention already used in the domain (`agents.json` etc.).
|
||||
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
@ -4289,6 +4290,19 @@ pub struct CreateTemplateRequestDto {
|
||||
pub content: String,
|
||||
/// Default runtime profile id for agents created from this template.
|
||||
pub default_profile_id: String,
|
||||
/// Optional default agent name for agents created from this template.
|
||||
#[serde(default)]
|
||||
pub default_agent_name: Option<String>,
|
||||
/// Optional agent-level system permission override for created agents.
|
||||
#[serde(default)]
|
||||
pub default_system_permissions: Option<SystemPermissionSet>,
|
||||
/// Optional agent-level MCP tool policy override for created agents.
|
||||
#[serde(
|
||||
default,
|
||||
rename = "defaultMcpToolPermissions",
|
||||
alias = "defaultMcpToolPolicy"
|
||||
)]
|
||||
pub default_mcp_tool_policy: Option<McpToolPolicy>,
|
||||
}
|
||||
|
||||
impl CreateTemplateRequestDto {
|
||||
@ -4301,18 +4315,73 @@ impl CreateTemplateRequestDto {
|
||||
name: self.name,
|
||||
content: self.content,
|
||||
default_profile_id: parse_profile_id(&self.default_profile_id)?,
|
||||
default_agent_name: self.default_agent_name,
|
||||
default_system_permissions: self.default_system_permissions,
|
||||
default_mcp_tool_policy: self.default_mcp_tool_policy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `update_template`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UpdateTemplateRequestDto {
|
||||
/// Id of the template to update.
|
||||
pub template_id: String,
|
||||
/// New Markdown content.
|
||||
pub content: String,
|
||||
/// New Markdown content. Missing/null leaves it unchanged.
|
||||
pub content: Option<String>,
|
||||
/// New display name. Missing leaves it unchanged.
|
||||
pub name: Option<String>,
|
||||
/// New default runtime profile. Missing leaves it unchanged.
|
||||
pub default_profile_id: Option<String>,
|
||||
/// New default agent name. Missing leaves it unchanged; null clears it.
|
||||
pub default_agent_name: Option<Option<String>>,
|
||||
/// New default system permissions. Missing leaves it unchanged; null clears it.
|
||||
pub default_system_permissions: Option<Option<SystemPermissionSet>>,
|
||||
/// New default MCP tool policy. Missing leaves it unchanged; null clears it.
|
||||
pub default_mcp_tool_policy: Option<Option<McpToolPolicy>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for UpdateTemplateRequestDto {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let mut value = Value::deserialize(deserializer)?;
|
||||
let object = value
|
||||
.as_object_mut()
|
||||
.ok_or_else(|| serde::de::Error::custom("expected update_template object"))?;
|
||||
let template_id =
|
||||
take_required_string(object, "templateId").map_err(serde::de::Error::custom)?;
|
||||
let content = take_optional_value::<String>(object, "content")
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.flatten();
|
||||
let name = take_optional_value::<String>(object, "name")
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.flatten();
|
||||
let default_profile_id = take_optional_value::<String>(object, "defaultProfileId")
|
||||
.map_err(serde::de::Error::custom)?
|
||||
.flatten();
|
||||
let default_agent_name = take_optional_value::<String>(object, "defaultAgentName")
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
let default_system_permissions =
|
||||
take_optional_value::<SystemPermissionSet>(object, "defaultSystemPermissions")
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
let default_mcp_tool_policy = take_optional_value_alias::<McpToolPolicy>(
|
||||
object,
|
||||
"defaultMcpToolPermissions",
|
||||
"defaultMcpToolPolicy",
|
||||
)
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
Ok(Self {
|
||||
template_id,
|
||||
content,
|
||||
name,
|
||||
default_profile_id,
|
||||
default_agent_name,
|
||||
default_system_permissions,
|
||||
default_mcp_tool_policy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateTemplateRequestDto {
|
||||
@ -4324,10 +4393,66 @@ impl UpdateTemplateRequestDto {
|
||||
Ok(UpdateTemplateInput {
|
||||
template_id: parse_template_id(&self.template_id)?,
|
||||
content: self.content,
|
||||
name: self.name,
|
||||
default_profile_id: self
|
||||
.default_profile_id
|
||||
.as_deref()
|
||||
.map(parse_profile_id)
|
||||
.transpose()?,
|
||||
default_agent_name: self.default_agent_name,
|
||||
default_system_permissions: self.default_system_permissions,
|
||||
default_mcp_tool_policy: self.default_mcp_tool_policy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn take_required_string(
|
||||
object: &mut serde_json::Map<String, Value>,
|
||||
key: &str,
|
||||
) -> Result<String, String> {
|
||||
match object.remove(key) {
|
||||
Some(Value::String(value)) => Ok(value),
|
||||
Some(_) => Err(format!("{key} must be a string")),
|
||||
None => Err(format!("missing {key}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_optional_value<T>(
|
||||
object: &mut serde_json::Map<String, Value>,
|
||||
key: &str,
|
||||
) -> Result<Option<Option<T>>, String>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
match object.remove(key) {
|
||||
None => Ok(None),
|
||||
Some(Value::Null) => Ok(Some(None)),
|
||||
Some(value) => serde_json::from_value(value)
|
||||
.map(Some)
|
||||
.map(Some)
|
||||
.map_err(|err| format!("invalid {key}: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_optional_value_alias<T>(
|
||||
object: &mut serde_json::Map<String, Value>,
|
||||
primary_key: &str,
|
||||
legacy_key: &str,
|
||||
) -> Result<Option<Option<T>>, String>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let primary = take_optional_value(object, primary_key)?;
|
||||
let legacy = take_optional_value(object, legacy_key)?;
|
||||
match (primary, legacy) {
|
||||
(Some(value), None) | (None, Some(value)) => Ok(Some(value)),
|
||||
(None, None) => Ok(None),
|
||||
(Some(_), Some(_)) => Err(format!(
|
||||
"{primary_key} and {legacy_key} cannot both be provided"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a template-id string (UUID) coming from the frontend.
|
||||
///
|
||||
/// # Errors
|
||||
|
||||
@ -2392,12 +2392,16 @@ impl BackendCore {
|
||||
));
|
||||
let list_templates = Arc::new(ListTemplates::new(Arc::clone(&template_store_port)));
|
||||
let delete_template = Arc::new(DeleteTemplate::new(Arc::clone(&template_store_port)));
|
||||
let create_agent_from_template = Arc::new(CreateAgentFromTemplate::new(
|
||||
Arc::clone(&template_store_port),
|
||||
Arc::clone(&contexts_port),
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let create_agent_from_template = Arc::new(
|
||||
CreateAgentFromTemplate::new(
|
||||
Arc::clone(&template_store_port),
|
||||
Arc::clone(&contexts_port),
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
Arc::clone(&events_port),
|
||||
)
|
||||
.with_system_permission_store(Arc::clone(&system_permission_store_port))
|
||||
.with_mcp_tool_permission_store(Arc::clone(&mcp_tool_permission_store_port)),
|
||||
);
|
||||
let detect_agent_drift = Arc::new(DetectAgentDrift::new(
|
||||
Arc::clone(&template_store_port),
|
||||
Arc::clone(&contexts_port),
|
||||
|
||||
Reference in New Issue
Block a user