Merge feature/ticket25-assistant-sandbox-plan into develop (#25)
fix(assistant): retire le plan sandbox project_read_only qui provoquait l'EACCES/os error 13 au spawn de la CLI de l'assistant de ticket. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -3,10 +3,10 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use domain::ports::{AgentSessionFactory, SessionPlan};
|
use domain::ports::{AgentSessionFactory, SessionPlan};
|
||||||
use domain::{AgentProfile, ProjectPath};
|
use domain::AgentProfile;
|
||||||
use domain::{
|
use domain::{
|
||||||
AgentToolPolicy, AgentToolPolicyStore, AssistantContextProvider, DomainEvent, EventBus,
|
AgentToolPolicy, AgentToolPolicyStore, AssistantContextProvider, DomainEvent, EventBus,
|
||||||
IssueRef, IssueStore, ProfileId, ProfileStore, Project, SandboxPlan, SessionId,
|
IssueRef, IssueStore, ProfileId, ProfileStore, Project, SessionId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::terminal::StructuredSessions;
|
use crate::terminal::StructuredSessions;
|
||||||
@ -94,7 +94,6 @@ impl OpenTicketAssistant {
|
|||||||
.prepare_ticket_assistant_context(&input.project, &issue)
|
.prepare_ticket_assistant_context(&input.project, &issue)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| AppError::Store(e.to_string()))?;
|
.map_err(|e| AppError::Store(e.to_string()))?;
|
||||||
let sandbox = ticket_assistant_sandbox(&input.project.root, input.issue_ref);
|
|
||||||
let session = self
|
let session = self
|
||||||
.factory
|
.factory
|
||||||
.start(
|
.start(
|
||||||
@ -102,7 +101,7 @@ impl OpenTicketAssistant {
|
|||||||
&prepared,
|
&prepared,
|
||||||
&input.project.root,
|
&input.project.root,
|
||||||
&SessionPlan::None,
|
&SessionPlan::None,
|
||||||
Some(&sandbox),
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
@ -197,26 +196,3 @@ async fn find_profile(
|
|||||||
.find(|profile| profile.id == profile_id)
|
.find(|profile| profile.id == profile_id)
|
||||||
.ok_or_else(|| AppError::NotFound(format!("profile {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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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"));
|
assert!(prepared.content.as_str().contains("Ticket carnet"));
|
||||||
let starts = factory.starts.lock().unwrap();
|
let starts = factory.starts.lock().unwrap();
|
||||||
assert!(matches!(starts[0].1, SessionPlan::None));
|
assert!(matches!(starts[0].1, SessionPlan::None));
|
||||||
assert!(starts[0]
|
assert!(starts[0].2.is_none());
|
||||||
.2
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|p| { p.default_posture == domain::permission::Posture::Deny }));
|
|
||||||
drop(starts);
|
drop(starts);
|
||||||
|
|
||||||
close
|
close
|
||||||
|
|||||||
@ -145,31 +145,6 @@ pub struct SandboxPlan {
|
|||||||
pub default_posture: Posture,
|
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
|
/// Immutable inputs [`compile_sandbox_plan`] interpolates into the absolute roots
|
||||||
/// it emits. Borrowed: a plan is computed at the launch site, never stored.
|
/// it emits. Borrowed: a plan is computed at the launch site, never stored.
|
||||||
pub struct SandboxContext<'a> {
|
pub struct SandboxContext<'a> {
|
||||||
@ -490,19 +465,6 @@ mod tests {
|
|||||||
assert!(!g.access.contains(PathAccess::RW));
|
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]
|
#[test]
|
||||||
fn write_and_delete_map_to_rw() {
|
fn write_and_delete_map_to_rw() {
|
||||||
let e = eff(
|
let e = eff(
|
||||||
|
|||||||
Reference in New Issue
Block a user