fix(ticket-assistant): édition de ticket via les tools MCP idea_ticket_* (#27)

L'assistant IA d'édition de ticket éditait les fichiers du ticket en direct,
hors de tout contrôle. Il passe désormais par les tools MCP idea_ticket_* :
préparation d'un environnement structuré dédié et policy d'enforcement scopée
au ticket courant, de sorte que l'assistant ne peut agir que sur son ticket
via la surface MCP plutôt que sur le système de fichiers.

Couvert par de nouveaux tests QA (mcp_server, assistant_context_store).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 09:10:23 +02:00
parent e54ffae0c3
commit 8570adb8e0
10 changed files with 873 additions and 40 deletions

View File

@ -2,7 +2,7 @@
use std::sync::Arc;
use domain::ports::{AgentSessionFactory, SessionPlan};
use domain::ports::{AgentSessionFactory, SessionPlan, StructuredSessionEnvironmentPreparer};
use domain::AgentProfile;
use domain::{
AgentToolPolicy, AgentToolPolicyStore, AssistantContextProvider, DomainEvent, EventBus,
@ -17,6 +17,7 @@ pub struct OpenTicketAssistant {
issues: Arc<dyn IssueStore>,
profiles: Arc<dyn ProfileStore>,
contexts: Arc<dyn AssistantContextProvider>,
environment: Arc<dyn StructuredSessionEnvironmentPreparer>,
factory: Arc<dyn AgentSessionFactory>,
structured: Arc<StructuredSessions>,
policies: Arc<dyn AgentToolPolicyStore>,
@ -52,6 +53,7 @@ impl OpenTicketAssistant {
issues: Arc<dyn IssueStore>,
profiles: Arc<dyn ProfileStore>,
contexts: Arc<dyn AssistantContextProvider>,
environment: Arc<dyn StructuredSessionEnvironmentPreparer>,
factory: Arc<dyn AgentSessionFactory>,
structured: Arc<StructuredSessions>,
policies: Arc<dyn AgentToolPolicyStore>,
@ -61,6 +63,7 @@ impl OpenTicketAssistant {
issues,
profiles,
contexts,
environment,
factory,
structured,
policies,
@ -94,39 +97,74 @@ impl OpenTicketAssistant {
.prepare_ticket_assistant_context(&input.project, &issue)
.await
.map_err(|e| AppError::Store(e.to_string()))?;
let requester = ticket_assistant_requester(&input.project, input.issue_ref);
let had_existing = self
.structured
.ticket_assistant_requester(input.issue_ref)
.is_some();
let policy = AgentToolPolicy::new(
vec![
"idea_ticket_read".to_owned(),
"idea_ticket_update".to_owned(),
"idea_ticket_update_status".to_owned(),
"idea_ticket_update_priority".to_owned(),
"idea_ticket_read_carnet".to_owned(),
"idea_ticket_update_carnet".to_owned(),
"idea_ticket_link".to_owned(),
"idea_ticket_unlink".to_owned(),
],
Some(input.issue_ref),
true,
);
self.policies.set_policy(requester.clone(), policy);
let environment = match self
.environment
.prepare_ticket_assistant(
&input.project,
input.issue_ref,
&profile,
&prepared,
&requester,
)
.await
{
Ok(environment) => environment,
Err(err) => {
if !had_existing {
self.policies.clear_policy(&requester);
}
return Err(AppError::from(err));
}
};
let session = self
.factory
.start(
&profile,
&prepared,
&input.project.root,
&environment.cwd,
&SessionPlan::None,
&[],
&environment.env,
None,
)
.await
.map_err(AppError::from)?;
.map_err(|err| {
if !had_existing {
self.policies.clear_policy(&requester);
}
AppError::from(err)
})?;
let session_id = session.id();
let requester = session_id.to_string();
let policy = AgentToolPolicy::new(
vec![
"idea_ticket_read".to_owned(),
"idea_ticket_update".to_owned(),
"idea_ticket_update_carnet".to_owned(),
],
Some(input.issue_ref),
true,
);
if let Some(old_requester) = self.structured.ticket_assistant_requester(input.issue_ref) {
self.policies.clear_policy(&old_requester);
if old_requester != requester {
self.policies.clear_policy(&old_requester);
}
}
let replaced = self.structured.insert_ticket_assistant(
input.issue_ref,
requester.clone(),
Arc::clone(&session),
);
self.policies.set_policy(requester.clone(), policy);
if let Some(old) = replaced {
let _ = old.shutdown().await;
}
@ -143,6 +181,14 @@ impl OpenTicketAssistant {
}
}
fn ticket_assistant_requester(project: &Project, issue_ref: IssueRef) -> String {
format!(
"ticket-assistant:{}:{}",
project.id.as_uuid().simple(),
issue_ref.number().get()
)
}
/// Closes an ephemeral ticket assistant chat session.
pub struct CloseTicketAssistant {
structured: Arc<StructuredSessions>,