Files
IdeA/crates/domain/src/agent_tool_policy.rs
Blomios 8158057b1d feat(tickets): ajoute les pièces jointes sur tickets (#108)
Stockage flat côté ticket + métadonnées d'attachments, lecture exposée côté
backend/MCP, et UI minimale de liste/ajout dans TicketDetail. Traverse le
domaine (Issue, ports), l'application (usecases + assistant de ticket), les
adaptateurs infra/MCP (issues store, orchestrateur), les DTO backend/web-
server/app-tauri, et le frontend (domain/ports/adapters/hooks/UI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 11:08:45 +02:00

130 lines
4.2 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_bulk_update_status"
| "idea_ticket_bulk_update_priority"
| "idea_ticket_bulk_delete"
| "idea_ticket_update_carnet"
| "idea_ticket_attachment_add"
| "idea_ticket_attachment_mark_summarized"
| "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_bulk_mutation_is_bound_to_the_configured_issue() {
let policy = AgentToolPolicy::new(
vec!["idea_ticket_bulk_delete".to_owned()],
Some(issue_ref("#7")),
true,
);
assert!(policy.permits_ticket_mutation("idea_ticket_bulk_delete", issue_ref("#7")));
assert!(!policy.permits_ticket_mutation("idea_ticket_bulk_delete", 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")));
}
}