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:
@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user