fix(application): writable roots Codex corrects sur projets non-IdeA quel que soit le mode de lancement — #309 (QA verte)
cause: la politique de lancement Codex divergeait selon le chemin de composition. compile_codex_launch_policy ajoutait les roots extra (temp géré) AVANT de calculer le mode de sandbox, et le builder PTY (append_codex_pty_managed_temp_roots) n'appliquait QUE ces roots temp, ignorant project root et politique compilée ; sans permissions explicites, codex_permission_writable_roots renvoyait une liste vide, donc le premier lancement TUI partait sans --add-dir project root et rejetait les roots additionnels (« Error adding directories: Ignoring --add-dir … because the effective permissions do not allow additional writable roots ») ; un --add-dir <project root> préexistant dans spec.args n'était pas réconcilié, d'où des comportements différents entre first-run, stop/relaunch, CLI custom et délégation headless — invisible sur le projet IdeA lui-même du fait de ses permissions déjà configurées. fix: codex_permission_writable_roots retourne par défaut le project root quand aucune permission explicite n'est configurée (jamais vide si root non vide) ; compile_codex_launch_policy n'ajoute les roots extra qu'en mode workspace-write et reste vide en read-only (deny explicite ne gagne jamais project root ni temp géré) ; nouveau apply_codex_pty_launch_policy unique source de vérité PTY Codex — réécrit la valeur --sandbox existante ou l'ajoute, draine le --add-dir <project root> préexistant puis pose les roots compilés dédupliqués, composition identique pour first-run, relaunch, custom CLI et headless ; unconfigured_policy_writes_project_and_deduplicated_managed_temp, explicit_write_deny_never_gains_project_or_managed_temp_root, partial_allow_exposes_only_its_computed_rw_grant + tests lifecycle de réécriture d'args ajoutés. QA verte: cargo targeted tests OK, npm targeted/typecheck OK — reconfirmée par Git: git diff --check OK, cargo fmt --all -- --check OK, cargo test -p application --lib codex_permissions 3/3, cargo test -p application --lib lifecycle 48/48. fichiers .ideai/* (agents/test.md, chat-drafts.json, system-permissions.json, tickets/counter.json, tickets/index.json, tickets/308/, tickets/309/), attachments/ et scripts/ hors périmètre et préservés en worktree ; aucun push. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -37,9 +37,7 @@ use domain::{
|
||||
|
||||
use domain::live_state::WorkStatus;
|
||||
|
||||
use crate::codex_permissions::{
|
||||
codex_permission_writable_roots, codex_sandbox_mode, compile_codex_launch_policy,
|
||||
};
|
||||
use crate::codex_permissions::{codex_permission_writable_roots, compile_codex_launch_policy};
|
||||
use crate::error::AppError;
|
||||
use crate::layout::{persist_doc, resolve_doc};
|
||||
use crate::managed_temp::{
|
||||
@ -1649,19 +1647,56 @@ fn append_codex_pty_model_overrides(profile: &AgentProfile, spec: &mut SpawnSpec
|
||||
}
|
||||
}
|
||||
|
||||
fn append_codex_pty_managed_temp_roots(
|
||||
fn apply_codex_pty_launch_policy(
|
||||
profile: &AgentProfile,
|
||||
permissions: Option<&EffectivePermissions>,
|
||||
network: Option<NetworkPolicy>,
|
||||
project_root: &str,
|
||||
run_dir: &str,
|
||||
managed_temp_roots: &[String],
|
||||
spec: &mut SpawnSpec,
|
||||
) {
|
||||
if managed_temp_roots.is_empty() || !is_codex_profile(profile) {
|
||||
if !is_codex_profile(profile) {
|
||||
return;
|
||||
}
|
||||
if codex_sandbox_mode(permissions, managed_temp_roots) != "workspace-write" {
|
||||
return;
|
||||
|
||||
let StructuredProviderLaunchPolicy::Codex {
|
||||
sandbox_mode,
|
||||
writable_roots,
|
||||
..
|
||||
} = compile_codex_launch_policy(
|
||||
project_root,
|
||||
run_dir,
|
||||
permissions,
|
||||
network,
|
||||
managed_temp_roots,
|
||||
);
|
||||
|
||||
let mut index = 0;
|
||||
let mut sandbox_seen = false;
|
||||
while index < spec.args.len() {
|
||||
if spec.args[index] == "--sandbox" && index + 1 < spec.args.len() {
|
||||
spec.args[index + 1] = sandbox_mode.clone();
|
||||
sandbox_seen = true;
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if spec.args[index] == "--add-dir"
|
||||
&& spec
|
||||
.args
|
||||
.get(index + 1)
|
||||
.is_some_and(|root| root == project_root)
|
||||
{
|
||||
spec.args.drain(index..=index + 1);
|
||||
continue;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
for root in managed_temp_roots {
|
||||
if !sandbox_seen {
|
||||
spec.args.push("--sandbox".to_owned());
|
||||
spec.args.push(sandbox_mode);
|
||||
}
|
||||
for root in writable_roots {
|
||||
if spec
|
||||
.args
|
||||
.windows(2)
|
||||
@ -1670,7 +1705,7 @@ fn append_codex_pty_managed_temp_roots(
|
||||
continue;
|
||||
}
|
||||
spec.args.push("--add-dir".to_owned());
|
||||
spec.args.push(root.clone());
|
||||
spec.args.push(root);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2626,9 +2661,12 @@ impl LaunchAgent {
|
||||
spec.sandbox =
|
||||
enrich_sandbox_plan_with_managed_temp_roots(spec.sandbox, &managed_temp_writable_roots);
|
||||
spec.sandbox = enrich_sandbox_plan_with_plugin_mcp_roots(spec.sandbox, &plugin_mcp_servers);
|
||||
append_codex_pty_managed_temp_roots(
|
||||
apply_codex_pty_launch_policy(
|
||||
&profile,
|
||||
effective_permissions.as_ref(),
|
||||
network_permission,
|
||||
input.project.root.as_str(),
|
||||
run_dir.as_str(),
|
||||
&managed_temp_writable_roots,
|
||||
&mut spec,
|
||||
);
|
||||
@ -4630,6 +4668,95 @@ mod tests {
|
||||
use super::*;
|
||||
use domain::Skill;
|
||||
|
||||
fn codex_profile() -> AgentProfile {
|
||||
crate::agent::reference_profiles()
|
||||
.into_iter()
|
||||
.find(|profile| profile.projector == Some(ProjectorKey::Codex))
|
||||
.expect("reference Codex profile")
|
||||
}
|
||||
|
||||
fn spawn_spec(args: Vec<&str>) -> SpawnSpec {
|
||||
SpawnSpec {
|
||||
command: "codex".to_owned(),
|
||||
args: args.into_iter().map(str::to_owned).collect(),
|
||||
cwd: ProjectPath::new("/project/.ideai/run/main").unwrap(),
|
||||
env: Vec::new(),
|
||||
context_plan: None,
|
||||
sandbox: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_pty_first_launch_pairs_workspace_mode_with_project_and_temp_roots() {
|
||||
let mut spec = spawn_spec(Vec::new());
|
||||
apply_codex_pty_launch_policy(
|
||||
&codex_profile(),
|
||||
None,
|
||||
None,
|
||||
"/project",
|
||||
"/project/.ideai/run/main",
|
||||
&["/managed/tmp".to_owned(), "/project".to_owned()],
|
||||
&mut spec,
|
||||
);
|
||||
|
||||
assert!(spec
|
||||
.args
|
||||
.windows(2)
|
||||
.any(|pair| pair == ["--sandbox", "workspace-write"]));
|
||||
assert_eq!(
|
||||
spec.args
|
||||
.windows(2)
|
||||
.filter(|pair| pair == &["--add-dir", "/project"])
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
spec.args
|
||||
.windows(2)
|
||||
.filter(|pair| pair == &["--add-dir", "/managed/tmp"])
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codex_pty_deny_replaces_advisory_workspace_root_without_temp_escape() {
|
||||
use domain::permission::{
|
||||
resolve, Capability, Effect, PathScope, PermissionRule, PermissionSet, Posture,
|
||||
};
|
||||
|
||||
let deny = PermissionRule::file(
|
||||
Capability::Delete,
|
||||
Effect::Deny,
|
||||
PathScope::new(["**".to_owned()]).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let permissions =
|
||||
resolve(Some(&PermissionSet::new(vec![deny], Posture::Ask)), None).unwrap();
|
||||
let mut spec = spawn_spec(vec![
|
||||
"--sandbox",
|
||||
"workspace-write",
|
||||
"--add-dir",
|
||||
"/project",
|
||||
]);
|
||||
|
||||
apply_codex_pty_launch_policy(
|
||||
&codex_profile(),
|
||||
Some(&permissions),
|
||||
None,
|
||||
"/project",
|
||||
"/project/.ideai/run/main",
|
||||
&["/managed/tmp".to_owned()],
|
||||
&mut spec,
|
||||
);
|
||||
|
||||
assert!(spec
|
||||
.args
|
||||
.windows(2)
|
||||
.any(|pair| pair == ["--sandbox", "read-only"]));
|
||||
assert!(!spec.args.iter().any(|arg| arg == "--add-dir"));
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn compose_convention_file(
|
||||
project_root: &str,
|
||||
|
||||
Reference in New Issue
Block a user