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:
2026-08-23 10:59:55 +02:00
parent ed5c5f73d6
commit d6c80be56d
11 changed files with 809 additions and 47 deletions

View File

@ -7,14 +7,16 @@ use std::sync::Arc;
use application::{
CreateTemplate, CreateTemplateInput, DeleteTemplate, DeleteTemplateInput, ListTemplates,
ReadTemplate, ReadTemplateInput, UpdateTemplate, UpdateTemplateInput,
ReadTemplate, ReadTemplateInput, UpdateTemplate,
};
use async_trait::async_trait;
use domain::{Project, TemplateId};
use infrastructure::{TemplateToolError, TemplateToolProvider};
use serde_json::{json, Value};
use crate::dto::{parse_profile_id, parse_template_id, TemplateDto, TemplateListDto};
use crate::dto::{
parse_profile_id, parse_template_id, TemplateDto, TemplateListDto, UpdateTemplateRequestDto,
};
/// Concrete provider for the public MCP template tools.
#[derive(Clone)]
@ -65,18 +67,30 @@ impl TemplateToolProvider for AppTemplateToolProvider {
"defaultProfileId",
)?)
.map_err(dto_tool_error)?,
default_agent_name: optional_string(&arguments, "defaultAgentName")?
.map(str::to_owned),
default_system_permissions: optional_json_value(
&arguments,
"defaultSystemPermissions",
)?,
default_mcp_tool_policy: optional_json_value_alias(
&arguments,
"defaultMcpToolPermissions",
"defaultMcpToolPolicy",
)?,
})
.await
.map_err(template_error)?;
json!(TemplateDto::from(template))
}
"idea_template_update" => {
let request: UpdateTemplateRequestDto =
serde_json::from_value(arguments).map_err(|err| {
TemplateToolError::new("invalid", format!("invalid update payload: {err}"))
})?;
let template = self
.update
.execute(UpdateTemplateInput {
template_id: parse_json_template_id(&arguments)?,
content: required_str(&arguments, "content")?.to_owned(),
})
.execute(request.into_input().map_err(dto_tool_error)?)
.await
.map_err(template_error)?;
json!(TemplateDto::from(template))
@ -133,6 +147,51 @@ fn required_str<'a>(arguments: &'a Value, key: &str) -> Result<&'a str, Template
.ok_or_else(|| TemplateToolError::new("invalid", format!("missing {key}")))
}
fn optional_string<'a>(
arguments: &'a Value,
key: &str,
) -> Result<Option<&'a str>, TemplateToolError> {
match arguments.get(key) {
None | Some(Value::Null) => Ok(None),
Some(value) => value
.as_str()
.map(Some)
.ok_or_else(|| TemplateToolError::new("invalid", format!("{key} must be a string"))),
}
}
fn optional_json_value<T>(arguments: &Value, key: &str) -> Result<Option<T>, TemplateToolError>
where
T: serde::de::DeserializeOwned,
{
match arguments.get(key) {
None | Some(Value::Null) => Ok(None),
Some(value) => serde_json::from_value(value.clone())
.map(Some)
.map_err(|err| TemplateToolError::new("invalid", format!("invalid {key}: {err}"))),
}
}
fn optional_json_value_alias<T>(
arguments: &Value,
primary_key: &str,
legacy_key: &str,
) -> Result<Option<T>, TemplateToolError>
where
T: serde::de::DeserializeOwned,
{
let primary = optional_json_value(arguments, primary_key)?;
let legacy = optional_json_value(arguments, legacy_key)?;
match (primary, legacy) {
(Some(value), None) | (None, Some(value)) => Ok(Some(value)),
(None, None) => Ok(None),
(Some(_), Some(_)) => Err(TemplateToolError::new(
"invalid",
format!("{primary_key} and {legacy_key} cannot both be provided"),
)),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
@ -340,7 +399,7 @@ mod tests {
}
#[test]
fn template_provider_preserves_content_only_b4_boundary() {
fn template_provider_metadata_updates_preserve_version_without_content_change() {
let template = AgentTemplate::new(
TemplateId::from_uuid(Uuid::from_u128(7)),
"Base",
@ -348,10 +407,22 @@ mod tests {
ProfileId::from_uuid(Uuid::from_u128(9)),
)
.unwrap();
let updated = template.with_updated_content(MarkdownDoc::new("v2"));
let updated = template
.with_metadata(
"Renamed".to_owned(),
ProfileId::from_uuid(Uuid::from_u128(10)),
Some("Agent".to_owned()),
None,
None,
)
.unwrap();
assert_eq!(updated.name, "Base");
assert_eq!(updated.default_profile_id, template.default_profile_id);
assert_eq!(updated.version, TemplateVersion(2));
assert_eq!(updated.name, "Renamed");
assert_eq!(
updated.default_profile_id,
ProfileId::from_uuid(Uuid::from_u128(10))
);
assert_eq!(updated.default_agent_name.as_deref(), Some("Agent"));
assert_eq!(updated.version, TemplateVersion::INITIAL);
}
}