From 961a2cadc4b91e1c8b1527cb68d20bfdf3d926b7 Mon Sep 17 00:00:00 2001 From: Blomios Date: Tue, 7 Jul 2026 17:28:30 +0200 Subject: [PATCH] fix(assistant): retire le plan sandbox project_read_only de l'assistant de ticket (#25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La surface « assistant de ticket » imposait un SessionPlan Landlock restrictif (project_read_only) qui gouvernait la classe exec de la sandbox OS et empêchait le spawn de la CLI (EACCES / os error 13). factory.start reçoit désormais SessionPlan::None + sandbox absent : l'assistant n'impose plus ce plan OS-sandbox. La classe exec n'est plus verrouillée, le spawn de la CLI aboutit. Co-Authored-By: Claude Opus 4.8 --- crates/application/src/ticket_assistant.rs | 30 ++-------------- crates/application/tests/ticket_assistant.rs | 5 +-- crates/domain/src/sandbox.rs | 38 -------------------- 3 files changed, 4 insertions(+), 69 deletions(-) diff --git a/crates/application/src/ticket_assistant.rs b/crates/application/src/ticket_assistant.rs index 5c8cb11..1373ee8 100644 --- a/crates/application/src/ticket_assistant.rs +++ b/crates/application/src/ticket_assistant.rs @@ -3,10 +3,10 @@ use std::sync::Arc; use domain::ports::{AgentSessionFactory, SessionPlan}; -use domain::{AgentProfile, ProjectPath}; +use domain::AgentProfile; use domain::{ AgentToolPolicy, AgentToolPolicyStore, AssistantContextProvider, DomainEvent, EventBus, - IssueRef, IssueStore, ProfileId, ProfileStore, Project, SandboxPlan, SessionId, + IssueRef, IssueStore, ProfileId, ProfileStore, Project, SessionId, }; use crate::terminal::StructuredSessions; @@ -94,7 +94,6 @@ impl OpenTicketAssistant { .prepare_ticket_assistant_context(&input.project, &issue) .await .map_err(|e| AppError::Store(e.to_string()))?; - let sandbox = ticket_assistant_sandbox(&input.project.root, input.issue_ref); let session = self .factory .start( @@ -102,7 +101,7 @@ impl OpenTicketAssistant { &prepared, &input.project.root, &SessionPlan::None, - Some(&sandbox), + None, ) .await .map_err(AppError::from)?; @@ -197,26 +196,3 @@ async fn find_profile( .find(|profile| profile.id == profile_id) .ok_or_else(|| AppError::NotFound(format!("profile {profile_id}"))) } - -fn ticket_assistant_sandbox(project_root: &ProjectPath, issue_ref: IssueRef) -> SandboxPlan { - let run_dir = format!( - "{}/.ideai/run/ticket-assistant-{}", - project_root.as_str().trim_end_matches(['/', '\\']), - issue_ref.number().get() - ); - SandboxPlan::project_read_only(project_root.as_str().to_owned(), run_dir) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn sandbox_uses_project_read_only_preset() { - let root = ProjectPath::new("/tmp/project").unwrap(); - let issue_ref = IssueRef::from(domain::IssueNumber::new(7).unwrap()); - let plan = ticket_assistant_sandbox(&root, issue_ref); - assert_eq!(plan.allowed[0].abs_root, "/tmp/project"); - assert_eq!(plan.default_posture, domain::permission::Posture::Deny); - } -} diff --git a/crates/application/tests/ticket_assistant.rs b/crates/application/tests/ticket_assistant.rs index 58216f8..d5cbfdd 100644 --- a/crates/application/tests/ticket_assistant.rs +++ b/crates/application/tests/ticket_assistant.rs @@ -319,10 +319,7 @@ async fn open_then_close_ticket_assistant_sets_policy_injects_context_and_emits_ assert!(prepared.content.as_str().contains("Ticket carnet")); let starts = factory.starts.lock().unwrap(); assert!(matches!(starts[0].1, SessionPlan::None)); - assert!(starts[0] - .2 - .as_ref() - .is_some_and(|p| { p.default_posture == domain::permission::Posture::Deny })); + assert!(starts[0].2.is_none()); drop(starts); close diff --git a/crates/domain/src/sandbox.rs b/crates/domain/src/sandbox.rs index c054c16..13eaabc 100644 --- a/crates/domain/src/sandbox.rs +++ b/crates/domain/src/sandbox.rs @@ -145,31 +145,6 @@ 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, run_dir: impl Into) -> 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> { @@ -490,19 +465,6 @@ 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(