feat(tickets): création d'un assistant IA de ticket (backend + frontend)

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>
This commit is contained in:
2026-07-06 06:06:24 +02:00
parent f1803768fd
commit 1fdf62c089
29 changed files with 2173 additions and 49 deletions

View File

@ -145,6 +145,31 @@ pub struct SandboxPlan {
pub default_posture: Posture,
}
impl SandboxPlan {
/// Builds the ticket-assistant preset: read-only access to the project root,
/// denied writes by default, while keeping the assistant run directory
/// writable so CLI session metadata/resume files can still function.
#[must_use]
pub fn project_read_only(project_root: impl Into<String>, run_dir: impl Into<String>) -> Self {
let project_root = project_root.into();
let run_dir = run_dir.into();
let mut allowed = vec![PathGrant {
abs_root: project_root,
access: PathAccess::RO,
}];
if !run_dir.trim().is_empty() {
allowed.push(PathGrant {
abs_root: run_dir,
access: PathAccess::RO | PathAccess::RW,
});
}
Self {
allowed,
default_posture: Posture::Deny,
}
}
}
/// Immutable inputs [`compile_sandbox_plan`] interpolates into the absolute roots
/// it emits. Borrowed: a plan is computed at the launch site, never stored.
pub struct SandboxContext<'a> {
@ -465,6 +490,19 @@ mod tests {
assert!(!g.access.contains(PathAccess::RW));
}
#[test]
fn project_read_only_preset_allows_project_read_and_run_dir_write_only() {
let plan = SandboxPlan::project_read_only(ROOT, "/home/anthony/.ideai/run/assistant");
let project = grant(&plan, ROOT).expect("project root granted");
assert_eq!(project.access, PathAccess::RO);
assert!(!project.access.contains(PathAccess::RW));
let run = grant(&plan, "/home/anthony/.ideai/run/assistant").expect("run dir granted");
assert!(run.access.contains(PathAccess::RO));
assert!(run.access.contains(PathAccess::RW));
assert_eq!(plan.default_posture, Posture::Deny);
}
#[test]
fn write_and_delete_map_to_rw() {
let e = eff(