fix(sandbox): accès Git minimal pour Codex — writable roots restreints au .git résolu, sans blanket grant project (#195, QA verte)
Codex ne pouvait pas accéder aux métadonnées Git sous sandbox workspace-write. À l'inverse, la factory relayait jusqu'ici le project_root entier en --add-dir, soit un grant large non défendable. Change pour n'accorder en écriture (Codex --add-dir ET SandboxPlan OS) que le .git réellement résolu : - resolve_git_dir gère .git/ directory ET fichier worktree 'gitdir: <path>' (absolu ou relatif au parent du marker), puis canonicalize. - enrich_sandbox_plan_with_codex_git_roots ajoute/déduplique les roots en RW sur le SandboxPlan existant (préserve l'autorité EffectivePermissions). - factory: défaut writable_roots = Vec::new() en l'absence de policy explicite. Invariants conservés : - project_root n'est jamais injecté comme writable root. - aucun élargissement au parent commun (canonicalize + BTreeSet). - permissions MCP/globales inchangées (seul le plan runtime est enrichi). Tests (crates/application) : - standard .git/ dir -> root unique. - projet non-Git -> aucun root. - worktree (.git file gitdir:) -> résolution + canonicalization. - symlink -> dédup, pas de grant parent. - factory: défaut sans policy n'émet plus --add-dir project_root. Réserve QA: launch_conventionfile_injects_assigned_skills_in_order (agent_lifecycle) arbitré hors-scope/non bloquant pour #195.
This commit is contained in:
@ -11,7 +11,8 @@
|
||||
//! [`AgentRuntime`], [`PtyPort`], [`FileSystem`], [`EventBus`]); none knows about
|
||||
//! a concrete adapter or Tauri.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use domain::ports::{
|
||||
@ -24,7 +25,7 @@ use domain::profile::{
|
||||
resolve_effort, EffortSelection, McpConfigStrategy, OpenCodeProviderConfig, StructuredAdapter,
|
||||
CODEX_CODE_MODE_FEATURES_TABLE, CODEX_CODE_MODE_FEATURES_TOML,
|
||||
};
|
||||
use domain::sandbox::{compile_sandbox_plan, SandboxContext, SandboxPlan};
|
||||
use domain::sandbox::{compile_sandbox_plan, PathAccess, PathGrant, SandboxContext, SandboxPlan};
|
||||
use domain::{
|
||||
bound_handoff_summary, Agent, AgentId, AgentManifest, AgentOrigin, AgentProfile,
|
||||
ContextInjection, ConversationId, ConversationParty, DomainEvent, EffectivePermissions,
|
||||
@ -1339,7 +1340,7 @@ fn build_structured_launch_policy(
|
||||
profile: &AgentProfile,
|
||||
permissions: Option<&EffectivePermissions>,
|
||||
network: Option<NetworkPolicy>,
|
||||
project_root: &ProjectPath,
|
||||
codex_git_writable_roots: &[String],
|
||||
) -> Option<StructuredProviderLaunchPolicy> {
|
||||
if !matches!(profile.structured_adapter, Some(StructuredAdapter::Codex)) {
|
||||
return None;
|
||||
@ -1349,8 +1350,8 @@ fn build_structured_launch_policy(
|
||||
}
|
||||
|
||||
let sandbox_mode = codex_structured_sandbox_mode(permissions).to_owned();
|
||||
let writable_roots = if sandbox_mode == "workspace-write" && !project_root.as_str().is_empty() {
|
||||
vec![project_root.as_str().to_owned()]
|
||||
let writable_roots = if sandbox_mode == "workspace-write" {
|
||||
codex_git_writable_roots.to_vec()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@ -1362,6 +1363,77 @@ fn build_structured_launch_policy(
|
||||
})
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut plan = plan?;
|
||||
if codex_git_writable_roots.is_empty() {
|
||||
return Some(plan);
|
||||
}
|
||||
|
||||
for root in codex_git_writable_roots {
|
||||
match plan
|
||||
.allowed
|
||||
.iter_mut()
|
||||
.find(|grant| grant.abs_root == *root)
|
||||
{
|
||||
Some(grant) => grant.access.insert(PathAccess::RW),
|
||||
None => plan.allowed.push(PathGrant {
|
||||
abs_root: root.clone(),
|
||||
access: PathAccess::RW,
|
||||
}),
|
||||
}
|
||||
}
|
||||
plan.allowed.sort_by(|a, b| a.abs_root.cmp(&b.abs_root));
|
||||
Some(plan)
|
||||
}
|
||||
|
||||
fn projection_model(profile: &AgentProfile) -> Option<&str> {
|
||||
profile.model.as_deref()
|
||||
}
|
||||
@ -2157,6 +2229,12 @@ 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()
|
||||
};
|
||||
spec.sandbox = compile_sandbox_plan(
|
||||
effective_permissions.as_ref(),
|
||||
&SandboxContext {
|
||||
@ -2164,11 +2242,13 @@ impl LaunchAgent {
|
||||
run_dir: run_dir.as_str(),
|
||||
},
|
||||
);
|
||||
spec.sandbox =
|
||||
enrich_sandbox_plan_with_codex_git_roots(spec.sandbox, &codex_git_writable_roots);
|
||||
let structured_policy = build_structured_launch_policy(
|
||||
&profile,
|
||||
effective_permissions.as_ref(),
|
||||
network_permission,
|
||||
&input.project.root,
|
||||
&codex_git_writable_roots,
|
||||
);
|
||||
let resolved_effort = resolve_effort(
|
||||
profile.model_reasoning_effort.as_deref(),
|
||||
|
||||
Reference in New Issue
Block a user