Ajoute le chat assistant IA attaché à un ticket (#8). Backend Rust : - use cases OpenTicketAssistant/CloseTicketAssistant + tests - politique d'outils par agent (domain/agent_tool_policy) et policy MCP - store de contexte assistant + gabarit default_ticket_assistant.md + tests - events TicketAssistantOpened/Closed - commandes Tauri open_ticket_chat/close_ticket_chat et câblage state/lib/events Frontend : - gateway (ports, adapters ticket + mock, domain) - hook useTicketAssistant + composant TicketAssistantPanel - intégration dans TicketDetail Tests verts : vitest tickets.test.tsx (23), cargo test application::ticket_assistant (1), infrastructure::assistant_context_store (2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
114 lines
3.5 KiB
Rust
114 lines
3.5 KiB
Rust
//! MCP tool policy for constrained assistant sessions.
|
|
//!
|
|
//! This is a pure domain value. It models what a requester may do through the
|
|
//! MCP surface; it does not know how the MCP server enforces it.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::issue::IssueRef;
|
|
|
|
/// Tool policy attached to one agent/requester.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct AgentToolPolicy {
|
|
/// Explicitly allowed MCP tool names.
|
|
pub allow: Vec<String>,
|
|
/// Optional issue boundary for ticket mutations.
|
|
pub bound_issue: Option<IssueRef>,
|
|
/// Whether tools outside [`Self::allow`] are denied.
|
|
pub deny_others: bool,
|
|
}
|
|
|
|
impl AgentToolPolicy {
|
|
/// Builds a tool policy.
|
|
#[must_use]
|
|
pub fn new(allow: Vec<String>, bound_issue: Option<IssueRef>, deny_others: bool) -> Self {
|
|
Self {
|
|
allow,
|
|
bound_issue,
|
|
deny_others,
|
|
}
|
|
}
|
|
|
|
/// Returns true when `tool` is allowed by the allowlist/default rule.
|
|
#[must_use]
|
|
pub fn permits(&self, tool: &str) -> bool {
|
|
self.allow.iter().any(|allowed| allowed == tool) || !self.deny_others
|
|
}
|
|
|
|
/// Returns true when `tool` may mutate the supplied ticket reference.
|
|
///
|
|
/// The method first applies the tool allowlist. Then, for known ticket mutation
|
|
/// tools, an optional [`Self::bound_issue`] fences the mutation to that single
|
|
/// issue reference.
|
|
#[must_use]
|
|
pub fn permits_ticket_mutation(&self, tool: &str, issue_ref: IssueRef) -> bool {
|
|
if !self.permits(tool) {
|
|
return false;
|
|
}
|
|
if !is_ticket_mutation_tool(tool) {
|
|
return true;
|
|
}
|
|
self.bound_issue.map_or(true, |bound| bound == issue_ref)
|
|
}
|
|
}
|
|
|
|
fn is_ticket_mutation_tool(tool: &str) -> bool {
|
|
matches!(
|
|
tool,
|
|
"idea_ticket_update"
|
|
| "idea_ticket_update_status"
|
|
| "idea_ticket_update_priority"
|
|
| "idea_ticket_update_carnet"
|
|
| "idea_ticket_link"
|
|
| "idea_ticket_unlink"
|
|
| "idea_ticket_create"
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::str::FromStr;
|
|
|
|
use super::*;
|
|
|
|
fn issue_ref(raw: &str) -> IssueRef {
|
|
IssueRef::from_str(raw).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn permits_allowlisted_tool_when_deny_others_is_true() {
|
|
let policy = AgentToolPolicy::new(vec!["idea_ticket_read".to_owned()], None, true);
|
|
assert!(policy.permits("idea_ticket_read"));
|
|
assert!(!policy.permits("idea_ask_agent"));
|
|
}
|
|
|
|
#[test]
|
|
fn deny_others_false_allows_non_listed_tools() {
|
|
let policy = AgentToolPolicy::new(vec!["idea_ticket_read".to_owned()], None, false);
|
|
assert!(policy.permits("idea_ticket_read"));
|
|
assert!(policy.permits("idea_ask_agent"));
|
|
}
|
|
|
|
#[test]
|
|
fn ticket_mutation_is_bound_to_the_configured_issue() {
|
|
let policy = AgentToolPolicy::new(
|
|
vec!["idea_ticket_update".to_owned()],
|
|
Some(issue_ref("#7")),
|
|
true,
|
|
);
|
|
assert!(policy.permits_ticket_mutation("idea_ticket_update", issue_ref("#7")));
|
|
assert!(!policy.permits_ticket_mutation("idea_ticket_update", issue_ref("#8")));
|
|
}
|
|
|
|
#[test]
|
|
fn ticket_mutation_requires_the_tool_itself_to_be_allowed() {
|
|
let policy = AgentToolPolicy::new(
|
|
vec!["idea_ticket_read".to_owned()],
|
|
Some(issue_ref("#7")),
|
|
true,
|
|
);
|
|
assert!(!policy.permits_ticket_mutation("idea_ticket_update", issue_ref("#7")));
|
|
}
|
|
}
|