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

@ -1100,6 +1100,28 @@ fn self_restart_launch_input(
}
}
fn automatic_resume_launch_input(
context: ResumeContext,
agent_id: AgentId,
node_id: domain::NodeId,
conversation_id: Option<String>,
mcp_runtime: Option<McpRuntime>,
) -> LaunchAgentInput {
LaunchAgentInput {
project: context.project,
agent_id,
rows: context.rows,
cols: context.cols,
node_id: Some(node_id),
conversation_id,
mcp_runtime,
allow_structured_alongside_pty: false,
require_structured: context.require_structured,
fresh_start: false,
managed_temp_exposure: application::ManagedTempExposureRequest::default(),
}
}
#[cfg(test)]
mod self_restart_contract_tests {
use super::*;
@ -1137,6 +1159,42 @@ mod self_restart_contract_tests {
assert!(!input.fresh_start);
}
#[test]
fn automatic_resume_preserves_custom_chat_structured_routing() {
let project = Project::new(
ProjectId::from_uuid(Uuid::from_u128(306)),
"resume",
domain::project::ProjectPath::new("/external/project-root").unwrap(),
domain::remote::RemoteRef::local(),
0,
)
.unwrap();
let agent = AgentId::from_uuid(Uuid::from_u128(1));
let node = domain::NodeId::from_uuid(Uuid::from_u128(2));
let input = automatic_resume_launch_input(
ResumeContext {
project,
rows: 24,
cols: 80,
node_id: Some(node),
conversation_id: Some("old-conversation".to_owned()),
require_structured: true,
},
agent,
node,
Some("current-conversation".to_owned()),
None,
);
assert!(input.require_structured);
assert_eq!(input.node_id, Some(node));
assert_eq!(
input.conversation_id.as_deref(),
Some("current-conversation")
);
assert!(!input.fresh_start);
}
#[tokio::test]
async fn human_turn_ignores_old_terminal_and_accepts_replacement() {
let mailbox = Arc::new(InMemoryMailbox::new());
@ -1682,19 +1740,13 @@ impl AgentResumer for AppAgentResumer {
let project_id = ctx.project.id;
self.launch_agent
.execute(LaunchAgentInput {
project: ctx.project,
.execute(automatic_resume_launch_input(
ctx,
agent_id,
rows: ctx.rows,
cols: ctx.cols,
node_id: Some(node_id),
node_id,
conversation_id,
mcp_runtime,
allow_structured_alongside_pty: false,
require_structured: false,
fresh_start: false,
managed_temp_exposure: application::ManagedTempExposureRequest::default(),
})
))
.await?;
// Premier tour de reprise par le **portail d'entrée** (pas de write brut, §20) :