feat(backend): MCP d'édition de templates (#81)

Ajoute le MCP dédié à l'édition de templates : catalogue et classification
des templates (mcp/templates.rs infrastructure + app-tauri), use cases et
provider (application/template), enforcement de la policy des tools (mod.rs,
server.rs, tools.rs), avec la parité côté chemin OpenAI-compatible
(openai_tools.rs x2).

Lots B1 (catalogue/classification), B2 (use cases/provider) et B3
(enforcement policy) livrés en un seul commit cohérent.

QA vert (seul l'échec de bind loopback #80, connu et non-régression, écarté).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 18:45:02 +02:00
parent 448edfc364
commit ef84d5cc49
17 changed files with 1171 additions and 49 deletions

View File

@ -32,7 +32,7 @@ use application::{
OpenTerminal, OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory,
ReadMemoryIndex, ReadProjectContext, ReadSkill, RecallMemory, ReconcileLayouts,
ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
ReconcileLiveState, ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles,
RenameDevice, RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal,
ResolveAgentPermissions, ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask,
@ -83,9 +83,10 @@ use infrastructure::{
InMemoryPairAttemptLimiter, LlamaCppRuntime, LocalFileSystem, LocalManagedProcess,
LocalProcessSpawner, McpServer, MediatedInbox, NaiveMemoryRecall, OrchestratorWatchHandle,
PortablePtyAdapter, RwFileGuard, StructuredSessionFactory, SystemClock, SystemMillisClock,
TicketAssistantEnvironmentPreparer, TicketToolProvider, TokioBroadcastEventBus, TokioScheduler,
ToolPolicyRegistry, UuidGenerator, VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL,
ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
TemplateToolProvider, TicketAssistantEnvironmentPreparer, TicketToolProvider,
TokioBroadcastEventBus, TokioScheduler, ToolPolicyRegistry, UuidGenerator, VectorMemoryRecall,
DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED,
VECTOR_ONNX_ENABLED,
};
pub mod dto;
@ -174,6 +175,48 @@ impl TicketToolProvider for LateBoundTicketToolProvider {
}
}
#[derive(Clone, Default)]
pub struct LateBoundTemplateToolProvider {
inner: Arc<Mutex<Option<Arc<dyn TemplateToolProvider>>>>,
}
impl LateBoundTemplateToolProvider {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn bind(&self, inner: Arc<dyn TemplateToolProvider>) {
*self.inner.lock().expect("template tool mutex poisoned") = Some(inner);
}
}
#[async_trait]
impl TemplateToolProvider for LateBoundTemplateToolProvider {
async fn handle_template_tool(
&self,
project: &Project,
requester: &str,
name: &str,
arguments: Value,
) -> Result<Value, infrastructure::TemplateToolError> {
let inner = self
.inner
.lock()
.expect("template tool mutex poisoned")
.clone()
.ok_or_else(|| {
infrastructure::TemplateToolError::new(
"UNAVAILABLE",
"template tool provider not bound".to_owned(),
)
})?;
inner
.handle_template_tool(project, requester, name, arguments)
.await
}
}
/// Implémente [`RecordTurnProvider`] (lot P6b) en matérialisant un [`RecordTurn`]
/// ciblant le **project root** du tour en cours.
///
@ -937,6 +980,8 @@ pub struct BackendCore {
pub unassign_ticket_from_sprint: Arc<UnassignTicketFromSprint>,
/// MCP provider for the public `idea_ticket_*` tools.
pub(crate) ticket_tool_provider: Arc<dyn TicketToolProvider>,
/// MCP provider for the public `idea_template_*` tools.
pub(crate) template_tool_provider: Arc<dyn TemplateToolProvider>,
/// Open an ephemeral AI assistant chat bound to one ticket.
pub open_ticket_assistant: Arc<OpenTicketAssistant>,
/// Close an ephemeral AI assistant chat bound to one ticket.
@ -1017,6 +1062,8 @@ pub struct BackendCore {
// --- Templates & sync (L7) ---
/// Create a template in the global store.
pub create_template: Arc<CreateTemplate>,
/// Read a template in the global store.
pub read_template: Arc<ReadTemplate>,
/// Update a template's content (bumps version).
pub update_template: Arc<UpdateTemplate>,
/// List all templates in the global store.
@ -1138,6 +1185,8 @@ pub struct BackendCore {
pub home_dir: String,
/// Late-bound concrete ticket MCP provider supplied by the driving adapter.
pub ticket_tool_binder: Arc<LateBoundTicketToolProvider>,
/// Late-bound concrete template MCP provider supplied by the driving adapter.
pub template_tool_binder: Arc<LateBoundTemplateToolProvider>,
}
impl BackendCore {
@ -1474,6 +1523,8 @@ impl BackendCore {
));
let ticket_tool_binder = Arc::new(LateBoundTicketToolProvider::new());
let ticket_tool_provider: Arc<dyn TicketToolProvider> = ticket_tool_binder.clone();
let template_tool_binder = Arc::new(LateBoundTemplateToolProvider::new());
let template_tool_provider: Arc<dyn TemplateToolProvider> = template_tool_binder.clone();
let tool_policy_registry = Arc::new(ToolPolicyRegistry::new());
let tool_policy_store = Arc::clone(&tool_policy_registry) as Arc<dyn AgentToolPolicyStore>;
let assistant_context_provider = Arc::new(FsAssistantContextStore::new(
@ -1841,6 +1892,7 @@ impl BackendCore {
Arc::clone(&template_store_port),
Arc::clone(&ids) as Arc<dyn IdGenerator>,
));
let read_template = Arc::new(ReadTemplate::new(Arc::clone(&template_store_port)));
let update_template = Arc::new(UpdateTemplate::new(
Arc::clone(&template_store_port),
Arc::clone(&events_port),
@ -2384,6 +2436,7 @@ impl BackendCore {
Arc::clone(&tool_policy_registry) as Arc<dyn AgentToolPolicyStore>,
Arc::clone(&mcp_tool_permission_store_port),
Arc::clone(&ticket_tool_provider),
Arc::clone(&template_tool_provider),
)) as Arc<dyn ToolInvoker>);
let stop_live_agent = Arc::new(
@ -2472,6 +2525,7 @@ impl BackendCore {
assign_ticket_to_sprint,
unassign_ticket_from_sprint,
ticket_tool_provider,
template_tool_provider,
open_ticket_assistant,
close_ticket_assistant,
tool_policy_registry,
@ -2500,6 +2554,7 @@ impl BackendCore {
update_agent_permissions,
resolve_agent_permissions,
create_template,
read_template,
update_template,
list_templates,
delete_template,
@ -2552,6 +2607,7 @@ impl BackendCore {
retry_background_task,
background_task_store: Arc::clone(&background_tasks_port),
ticket_tool_binder,
template_tool_binder,
}
}
@ -2640,6 +2696,7 @@ impl BackendCore {
.with_events(events)
.with_ready_sink(ready_sink)
.with_ticket_tools(Arc::clone(&self.ticket_tool_provider))
.with_template_tools(Arc::clone(&self.template_tool_provider))
.with_tool_policies(Arc::clone(&self.tool_policy_registry))
.with_mcp_tool_permissions(Arc::clone(&self.mcp_tool_permission_store)),
endpoint,
@ -4569,6 +4626,12 @@ mod mcp_serve_peer_tests {
"idea_ticket_link",
"idea_ticket_unlink",
"idea_sprint_list",
// Public template tools.
"idea_template_list",
"idea_template_read",
"idea_template_create",
"idea_template_update",
"idea_template_delete",
] {
assert!(
names.contains(&expected),
@ -4578,8 +4641,8 @@ mod mcp_serve_peer_tests {
assert!(!names.contains(&"idea_reply"));
assert_eq!(
tools.len(),
25,
"exactly the twenty-five exposed idea_* tools; got {names:?}"
30,
"exactly the thirty exposed idea_* tools; got {names:?}"
);
drop(client); // EOF ⇒ serve loop ends