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:
@ -98,7 +98,26 @@ pub fn catalogue() -> Vec<ToolDef> {
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"content": { "type": "string" },
|
||||
"defaultProfileId": { "type": "string", "format": "uuid" }
|
||||
"defaultProfileId": { "type": "string", "format": "uuid" },
|
||||
"defaultAgentName": { "type": ["string", "null"] },
|
||||
"defaultSystemPermissions": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"network": { "type": "string", "enum": ["allow", "deny", "ask"] }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"defaultMcpToolPermissions": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"allowedTools": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["allowedTools"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "defaultProfileId"],
|
||||
"additionalProperties": false
|
||||
@ -106,14 +125,35 @@ pub fn catalogue() -> Vec<ToolDef> {
|
||||
},
|
||||
ToolDef {
|
||||
name: "idea_template_update",
|
||||
description: "Update a global IdeA agent template's Markdown content.",
|
||||
description: "Update a global IdeA agent template.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"templateId": template_id.clone(),
|
||||
"content": { "type": "string" }
|
||||
"content": { "type": ["string", "null"] },
|
||||
"name": { "type": "string" },
|
||||
"defaultProfileId": { "type": "string", "format": "uuid" },
|
||||
"defaultAgentName": { "type": ["string", "null"] },
|
||||
"defaultSystemPermissions": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"network": { "type": "string", "enum": ["allow", "deny", "ask"] }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"defaultMcpToolPermissions": {
|
||||
"type": ["object", "null"],
|
||||
"properties": {
|
||||
"allowedTools": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["allowedTools"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["templateId", "content"],
|
||||
"required": ["templateId"],
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
|
||||
@ -28,6 +28,7 @@ use domain::ids::{ProfileId, TemplateId};
|
||||
use domain::markdown::MarkdownDoc;
|
||||
use domain::ports::{FileSystem, RemotePath, StoreError, TemplateStore};
|
||||
use domain::template::{AgentTemplate, TemplateVersion};
|
||||
use domain::{McpToolPolicy, SystemPermissionSet};
|
||||
|
||||
/// Directory (under app-data) holding the templates store.
|
||||
const TEMPLATES_DIR: &str = "templates";
|
||||
@ -47,6 +48,17 @@ struct IndexEntry {
|
||||
version: TemplateVersion,
|
||||
content_hash: String,
|
||||
default_profile_id: ProfileId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
default_agent_name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
default_system_permissions: Option<SystemPermissionSet>,
|
||||
#[serde(
|
||||
default,
|
||||
rename = "defaultMcpToolPermissions",
|
||||
alias = "defaultMcpToolPolicy",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
default_mcp_tool_policy: Option<McpToolPolicy>,
|
||||
}
|
||||
|
||||
/// On-disk shape of `templates/index.json`.
|
||||
@ -155,6 +167,9 @@ impl FsTemplateStore {
|
||||
content_md: MarkdownDoc::new(content),
|
||||
version: entry.version,
|
||||
default_profile_id: entry.default_profile_id,
|
||||
default_agent_name: entry.default_agent_name.clone(),
|
||||
default_system_permissions: entry.default_system_permissions.clone(),
|
||||
default_mcp_tool_policy: entry.default_mcp_tool_policy.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -202,6 +217,9 @@ impl TemplateStore for FsTemplateStore {
|
||||
version: template.version,
|
||||
content_hash: content_hash(&template.content_md),
|
||||
default_profile_id: template.default_profile_id,
|
||||
default_agent_name: template.default_agent_name.clone(),
|
||||
default_system_permissions: template.default_system_permissions.clone(),
|
||||
default_mcp_tool_policy: template.default_mcp_tool_policy.clone(),
|
||||
};
|
||||
if let Some(slot) = index.templates.iter_mut().find(|e| e.id == template.id) {
|
||||
*slot = row;
|
||||
|
||||
@ -9,6 +9,7 @@ use domain::ids::{ProfileId, TemplateId};
|
||||
use domain::markdown::MarkdownDoc;
|
||||
use domain::ports::{FileSystem, RemotePath, StoreError, TemplateStore};
|
||||
use domain::template::AgentTemplate;
|
||||
use domain::{McpToolPolicy, NetworkPolicy, SystemPermissionSet};
|
||||
use infrastructure::{FsTemplateStore, LocalFileSystem};
|
||||
use uuid::Uuid;
|
||||
|
||||
@ -60,6 +61,17 @@ async fn save_then_get_and_list_roundtrip() {
|
||||
let store = store(&tmp);
|
||||
|
||||
let t = template(tid(1), "Backend", "# Backend template");
|
||||
let t = t
|
||||
.with_metadata(
|
||||
"Backend".to_owned(),
|
||||
pid(2),
|
||||
Some("Backend Agent".to_owned()),
|
||||
Some(SystemPermissionSet::new(Some(NetworkPolicy::Allow))),
|
||||
Some(McpToolPolicy {
|
||||
allowed_tools: vec!["idea_template_read".to_owned()],
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
store.save(&t).await.unwrap();
|
||||
|
||||
assert_eq!(store.get(tid(1)).await.unwrap(), t);
|
||||
@ -116,10 +128,18 @@ async fn delete_removes_from_index() {
|
||||
async fn index_is_camelcase_with_content_hash() {
|
||||
let tmp = TempDir::new();
|
||||
let store = store(&tmp);
|
||||
store
|
||||
.save(&template(tid(1), "Backend", "hello"))
|
||||
.await
|
||||
let template = template(tid(1), "Backend", "hello")
|
||||
.with_metadata(
|
||||
"Backend".to_owned(),
|
||||
pid(2),
|
||||
Some("Backend Agent".to_owned()),
|
||||
Some(SystemPermissionSet::new(Some(NetworkPolicy::Allow))),
|
||||
Some(McpToolPolicy {
|
||||
allowed_tools: vec!["idea_template_read".to_owned()],
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
store.save(&template).await.unwrap();
|
||||
|
||||
let fs = LocalFileSystem::new();
|
||||
let bytes = fs.read(&tmp.child("templates/index.json")).await.unwrap();
|
||||
@ -131,6 +151,26 @@ async fn index_is_camelcase_with_content_hash() {
|
||||
"camelCase contentHash present"
|
||||
);
|
||||
assert!(entry.get("defaultProfileId").is_some());
|
||||
assert_eq!(
|
||||
entry.get("defaultAgentName").and_then(|v| v.as_str()),
|
||||
Some("Backend Agent")
|
||||
);
|
||||
assert_eq!(
|
||||
entry
|
||||
.get("defaultSystemPermissions")
|
||||
.and_then(|v| v.get("network"))
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("allow")
|
||||
);
|
||||
assert_eq!(
|
||||
entry
|
||||
.get("defaultMcpToolPermissions")
|
||||
.and_then(|v| v.get("allowedTools"))
|
||||
.and_then(|v| v.as_array())
|
||||
.and_then(|values| values.first())
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("idea_template_read")
|
||||
);
|
||||
assert!(entry.get("content_hash").is_none(), "no snake_case leak");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user