fix(codex): preserve project root on structured resume

Recalcule la policy structured Codex lors des reprises custom/headless afin que le project root et le managed tmp soient transmis en writable_roots. Fail-closed si une session Codex structured est lancée sans policy explicite.
This commit is contained in:
2026-09-10 10:53:43 +02:00
parent ee5373e57e
commit eb863664fa
4 changed files with 122 additions and 39 deletions

View File

@ -189,7 +189,11 @@ impl AgentSessionFactory for StructuredSessionFactory {
writable_roots.clone(),
Some(*network_access),
),
None => ("workspace-write".to_owned(), Vec::new(), None),
None => {
return Err(AgentSessionError::Start(
"structured Codex launch requires an explicit launch policy".to_owned(),
));
}
};
Arc::new(CodexExecSession::new_with_policy_and_overrides(
id,

View File

@ -51,7 +51,7 @@ mod tests {
use domain::ports::{
AgentSession, AgentSessionError, AgentSessionFactory, ContextInjectionPlan,
PreparedContext, ReplyEvent, ReplyProgressKind, ReplyProgressSource, ReplyProgressStage,
SessionPlan,
SessionPlan, StructuredProviderLaunchPolicy,
};
use domain::profile::{AgentProfile, ContextInjection, HttpChatConfig, StructuredAdapter};
use domain::project::ProjectPath;
@ -589,6 +589,11 @@ mod tests {
// Codex : routé vers l'adapter Codex (id de session distinct, démarrage ok).
let fake_cx = FakeCli::printing(&codex_script());
let codex = structured_profile(StructuredAdapter::Codex, &fake_cx.command());
let codex_policy = StructuredProviderLaunchPolicy::Codex {
sandbox_mode: "read-only".to_owned(),
writable_roots: Vec::new(),
network_access: false,
};
let session_cx = factory
.start(
&codex,
@ -598,7 +603,7 @@ mod tests {
None,
&[],
None,
None,
Some(&codex_policy),
)
.await
.expect("start Codex ok");
@ -647,7 +652,7 @@ mod tests {
}
#[tokio::test]
async fn factory_does_not_pass_project_root_to_codex_add_dir_without_policy() {
async fn factory_rejects_codex_launch_without_explicit_policy() {
let factory = StructuredSessionFactory::new();
let (cmd, argv) = make_recording_fake(&[
r#"{"type":"thread.started","thread_id":"cx-new"}"#,
@ -660,7 +665,7 @@ mod tests {
project_root: "/project/root".to_owned(),
};
let session = factory
let err = match factory
.start(
&codex,
&ctx,
@ -672,23 +677,18 @@ mod tests {
None,
)
.await
.expect("start Codex ok");
let content = drain_final(session.as_ref()).await;
assert_eq!(content, "ok");
let recorded = std::fs::read_to_string(&argv).expect("argv");
let args: Vec<&str> = recorded.lines().collect();
{
Ok(_) => panic!("Codex launch without a policy must fail closed"),
Err(err) => err,
};
assert!(
!args.windows(2).any(|w| w == ["--add-dir", "/project/root"]),
"factory must not invent --add-dir project root without an explicit policy, got: {args:?}"
err.to_string()
.contains("requires an explicit launch policy"),
"unexpected error: {err}"
);
assert!(
!args.contains(&"--add-dir"),
"default Codex adapter launch must not carry any implicit --add-dir, got: {args:?}"
);
assert!(
!args.contains(&"--ask-for-approval"),
"codex exec must not receive unsupported approval flags, got: {args:?}"
!argv.exists(),
"factory must reject the launch before spawning Codex"
);
let _ = std::fs::remove_file(&cmd);
@ -710,6 +710,11 @@ mod tests {
relative_path: "AGENTS.md".to_owned(),
project_root: "/project/root".to_owned(),
};
let policy = StructuredProviderLaunchPolicy::Codex {
sandbox_mode: "workspace-write".to_owned(),
writable_roots: vec!["/project/root".to_owned()],
network_access: false,
};
let session = factory
.start(
@ -720,7 +725,7 @@ mod tests {
None,
&[],
None,
None,
Some(&policy),
)
.await
.expect("start Codex ok");
@ -1803,6 +1808,11 @@ mod tests {
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"ok"}}"#,
]);
let codex = structured_profile(StructuredAdapter::Codex, &fake.command());
let policy = StructuredProviderLaunchPolicy::Codex {
sandbox_mode: "read-only".to_owned(),
writable_roots: Vec::new(),
network_access: false,
};
let session = factory
.start(
&codex,
@ -1814,7 +1824,7 @@ mod tests {
None,
&[],
None,
None,
Some(&policy),
)
.await
.expect("start resume codex");
@ -2102,7 +2112,7 @@ mod tests {
cmd.clone(),
"/",
Some("cx-id".to_owned()),
vec!["/project/root".to_owned()],
vec!["/project/root".to_owned(), "/managed/tmp".to_owned()],
Vec::new(),
None,
None,
@ -2121,6 +2131,8 @@ mod tests {
"workspace-write",
"--add-dir",
"/project/root",
"--add-dir",
"/managed/tmp",
"resume",
"cx-id",
"vas-y",