fix(sandbox): writable roots Codex dérivées des permissions effectives du menu agent — #197 (QA verte)

This commit is contained in:
2026-08-14 15:05:15 +02:00
parent 6a98752cb0
commit a60fb3ed92
6 changed files with 482 additions and 146 deletions

View File

@ -11,8 +11,7 @@
//! [`AgentRuntime`], [`PtyPort`], [`FileSystem`], [`EventBus`]); none knows about
//! a concrete adapter or Tauri.
use std::collections::{BTreeSet, HashMap};
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use std::sync::Arc;
use domain::ports::{
@ -30,13 +29,16 @@ use domain::{
bound_handoff_summary, Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile,
ContextInjection, ConversationId, ConversationParty, DomainEvent, EffectivePermissions,
Handoff, HandoffStore, ManifestEntry, MarkdownDoc, MemoryIndexEntry, MemoryType, NetworkPolicy,
NodeId, OrchestrationCapabilitySnapshot, PermissionProjector, Posture, ProfileId, Project,
ProjectPath, ProjectedFile, ProjectionContext, ProjectorKey, ProviderSessionStore, PtySize,
SessionId, SessionKind, SessionStatus, SkillKind, TerminalSession, HANDOFF_SUMMARY_MAX_CHARS,
NodeId, OrchestrationCapabilitySnapshot, PermissionProjector, ProfileId, Project, ProjectPath,
ProjectedFile, ProjectionContext, ProjectorKey, ProviderSessionStore, PtySize, SessionId,
SessionKind, SessionStatus, SkillKind, TerminalSession, HANDOFF_SUMMARY_MAX_CHARS,
};
use domain::live_state::WorkStatus;
use crate::codex_permissions::{
codex_permission_writable_roots, codex_sandbox_mode, compile_codex_launch_policy,
};
use crate::error::AppError;
use crate::layout::{persist_doc, resolve_doc};
use crate::managed_temp::{ResolveManagedTemp, ResolveManagedTempInput};
@ -1338,22 +1340,13 @@ fn select_projector_key(profile: &AgentProfile) -> Option<ProjectorKey> {
None
}
fn codex_structured_sandbox_mode(permissions: Option<&EffectivePermissions>) -> &'static str {
match permissions.map(EffectivePermissions::fallback) {
Some(Posture::Deny) => "read-only",
Some(Posture::Ask | Posture::Allow) | None => "workspace-write",
}
}
fn codex_network_access(network: Option<NetworkPolicy>) -> bool {
matches!(network, Some(NetworkPolicy::Allow))
}
fn build_structured_launch_policy(
profile: &AgentProfile,
permissions: Option<&EffectivePermissions>,
network: Option<NetworkPolicy>,
codex_git_writable_roots: &[String],
project_root: &str,
run_dir: &str,
extra_writable_roots: &[String],
) -> Option<StructuredProviderLaunchPolicy> {
if !matches!(profile.structured_adapter, Some(StructuredAdapter::Codex)) {
return None;
@ -1362,70 +1355,13 @@ fn build_structured_launch_policy(
return None;
}
let sandbox_mode = codex_structured_sandbox_mode(permissions).to_owned();
let writable_roots = if sandbox_mode == "workspace-write" {
codex_git_writable_roots.to_vec()
} else {
Vec::new()
};
Some(StructuredProviderLaunchPolicy::Codex {
sandbox_mode,
writable_roots,
network_access: codex_network_access(network),
})
}
fn codex_git_writable_roots(project_root: &ProjectPath) -> Vec<String> {
git_writable_roots_for_project(Path::new(project_root.as_str()))
}
fn git_writable_roots_for_project(project_root: &Path) -> Vec<String> {
let git_marker = project_root.join(".git");
let Some(git_dir) = resolve_git_dir(&git_marker) else {
return Vec::new();
};
let Ok(canonical) = git_dir.canonicalize() else {
return Vec::new();
};
if !canonical.is_dir() {
return Vec::new();
}
let mut roots = BTreeSet::new();
roots.insert(canonical.to_string_lossy().into_owned());
roots.into_iter().collect()
}
fn resolve_git_dir(git_marker: &Path) -> Option<PathBuf> {
if git_marker.is_dir() {
return Some(git_marker.to_path_buf());
}
if !git_marker.is_file() {
return None;
}
let contents = std::fs::read_to_string(git_marker).ok()?;
let gitdir = contents
.lines()
.find_map(|line| line.trim().strip_prefix("gitdir:").map(str::trim))?;
if gitdir.is_empty() {
return None;
}
let path = PathBuf::from(gitdir);
if path.is_absolute() {
Some(path)
} else {
git_marker.parent().map(|parent| parent.join(path))
}
}
fn enrich_sandbox_plan_with_codex_git_roots(
plan: Option<SandboxPlan>,
codex_git_writable_roots: &[String],
) -> Option<SandboxPlan> {
enrich_sandbox_plan_with_rw_roots(plan, codex_git_writable_roots)
Some(compile_codex_launch_policy(
project_root,
run_dir,
permissions,
network,
extra_writable_roots,
))
}
fn enrich_sandbox_plan_with_managed_temp_roots(
@ -1461,14 +1397,6 @@ fn enrich_sandbox_plan_with_rw_roots(
Some(plan)
}
fn append_unique_paths(target: &mut Vec<String>, roots: &[String]) {
for root in roots {
if !target.iter().any(|existing| existing == root) {
target.push(root.clone());
}
}
}
fn dedupe_paths(paths: impl IntoIterator<Item = LocalPath>) -> Vec<String> {
let mut roots = Vec::new();
for path in paths {
@ -1533,7 +1461,7 @@ fn append_codex_pty_managed_temp_roots(
if managed_temp_roots.is_empty() || !is_codex_profile(profile) {
return;
}
if codex_structured_sandbox_mode(permissions) != "workspace-write" {
if codex_sandbox_mode(permissions, managed_temp_roots) != "workspace-write" {
return;
}
for root in managed_temp_roots {
@ -2338,17 +2266,9 @@ impl LaunchAgent {
// remplir `spec.sandbox`. `eff == None` (rien posé) ⇒ `None` : aucun plan,
// comportement natif conservé (invariant produit). L'enforcer concret
// (Landlock/Noop) est injecté côté PTY au composition root.
let codex_git_writable_roots =
if matches!(profile.structured_adapter, Some(StructuredAdapter::Codex)) {
codex_git_writable_roots(&input.project.root)
} else {
Vec::new()
};
let managed_temp_writable_roots = self
.resolve_managed_temp_exposure(&input.managed_temp_exposure)
.await?;
let mut codex_writable_roots = codex_git_writable_roots.clone();
append_unique_paths(&mut codex_writable_roots, &managed_temp_writable_roots);
spec.sandbox = compile_sandbox_plan(
effective_permissions.as_ref(),
&SandboxContext {
@ -2356,8 +2276,13 @@ impl LaunchAgent {
run_dir: run_dir.as_str(),
},
);
let codex_permission_writable_roots = codex_permission_writable_roots(
input.project.root.as_str(),
run_dir.as_str(),
effective_permissions.as_ref(),
);
spec.sandbox =
enrich_sandbox_plan_with_codex_git_roots(spec.sandbox, &codex_git_writable_roots);
enrich_sandbox_plan_with_rw_roots(spec.sandbox, &codex_permission_writable_roots);
spec.sandbox =
enrich_sandbox_plan_with_managed_temp_roots(spec.sandbox, &managed_temp_writable_roots);
append_codex_pty_managed_temp_roots(
@ -2370,7 +2295,9 @@ impl LaunchAgent {
&profile,
effective_permissions.as_ref(),
network_permission,
&codex_writable_roots,
input.project.root.as_str(),
run_dir.as_str(),
&managed_temp_writable_roots,
);
let resolved_effort = resolve_effort(
profile.model_reasoning_effort.as_deref(),