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:
2026-08-12 16:41:25 +02:00
parent 8a66ccbc07
commit e2fc85b434
5 changed files with 295 additions and 21 deletions

View File

@ -451,18 +451,18 @@ impl CodexExecSession {
///
/// Format RÉEL vérifié 2026-06-10 (codex 0.137.0) :
/// - Conversation neuve : `codex exec --json --skip-git-repo-check
/// --sandbox workspace-write --add-dir <project-root> <prompt>`.
/// --sandbox workspace-write [--add-dir <explicit-root>...] <prompt>`.
/// - Reprise (id connu) : `codex exec --json
/// --skip-git-repo-check --sandbox workspace-write --add-dir <project-root> resume
/// <thread_id> <prompt>`.
/// --skip-git-repo-check --sandbox workspace-write [--add-dir <explicit-root>...]
/// resume <thread_id> <prompt>`.
///
/// **Autonomie d'écriture (D3)** : `--sandbox workspace-write` autorise l'agent à
/// écrire dans son workspace. `codex exec` est déjà non-interactif (aucun prompt
/// d'approbation possible), donc on ne passe **pas** `--ask-for-approval` : ce flag
/// appartient à la commande interactive `codex`, pas à la sous-commande `exec` qui
/// sort sur `error: unexpected argument '--ask-for-approval' found`. Comme l'agent
/// tourne depuis son run dir isolé, `--add-dir` expose explicitement le project root
/// à la sandbox Codex pour que les écritures Git touchent le vrai workspace.
/// tourne depuis son run dir isolé, `--add-dir` expose uniquement les roots
/// writable explicitement fournis par la launch policy.
fn build_spawn_line(&self, prompt: &str) -> SpawnLine {
let mut args = vec!["exec".to_owned()];
let conversation_id = self.conversation_id.lock().expect("mutex sain").clone();

View File

@ -189,11 +189,7 @@ impl AgentSessionFactory for StructuredSessionFactory {
writable_roots.clone(),
Some(*network_access),
),
None => (
"workspace-write".to_owned(),
vec![ctx.project_root.clone()],
None,
),
None => ("workspace-write".to_owned(), Vec::new(), None),
};
Arc::new(CodexExecSession::new_with_policy_and_overrides(
id,

View File

@ -647,7 +647,7 @@ mod tests {
}
#[tokio::test]
async fn factory_passes_project_root_to_codex_add_dir() {
async fn factory_does_not_pass_project_root_to_codex_add_dir_without_policy() {
let factory = StructuredSessionFactory::new();
let (cmd, argv) = make_recording_fake(&[
r#"{"type":"thread.started","thread_id":"cx-new"}"#,
@ -679,8 +679,12 @@ mod tests {
let recorded = std::fs::read_to_string(&argv).expect("argv");
let args: Vec<&str> = recorded.lines().collect();
assert!(
args.windows(2).any(|w| w == ["--add-dir", "/project/root"]),
"factory must relay PreparedContext.project_root to Codex --add-dir, got: {args:?}"
!args.windows(2).any(|w| w == ["--add-dir", "/project/root"]),
"factory must not invent --add-dir project root without an explicit policy, got: {args:?}"
);
assert!(
!args.contains(&"--add-dir"),
"default Codex adapter launch must not carry any implicit --add-dir, got: {args:?}"
);
assert!(
!args.contains(&"--ask-for-approval"),