feat(agent): routage LaunchAgent structuré vs PTY + réconciliation A/B (D3) — §17
- LaunchAgent route sur profile.structured_adapter : Some ⇒ AgentSession via
factory + enregistrement StructuredSessions (aucun pty.spawn) ; None ⇒ PTY
inchangé. Dépendances structurées injectées par builders additifs
with_structured (signatures publiques inchangées ⇒ A/B legacy verts).
- LaunchAgentOutput étendu d'un champ optionnel structured (non cassant).
- A (ChangeAgentProfile) : kill polymorphe — session structurée ⇒ shutdown(),
PTY ⇒ kill ; détection sur les deux registres.
- B : resume_supported vrai pour profil structuré ; resolve_session_plan ⇒
Resume{conversation_id} (Claude --resume / Codex exec resume).
- Codex : build_spawn_line porte --sandbox workspace-write --ask-for-approval
never (autonomie d'écriture ; à terme piloté par les permissions).
Tests : structured_launch 8 + codex flags 2 ; application/infrastructure 0 échec.
Reste D4 : ChatBridge (Channel) + commandes agent_send/reattach + StructuredSessions
dans AppState + DTO cellKind/ReplyChunk.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -142,13 +142,18 @@ impl CodexExecSession {
|
||||
/// Compose la ligne de commande d'un tour.
|
||||
///
|
||||
/// Format RÉEL vérifié 2026-06-09 :
|
||||
/// - Conversation neuve : `codex exec --json --skip-git-repo-check <prompt>`.
|
||||
/// - Conversation neuve : `codex exec --json --skip-git-repo-check
|
||||
/// --sandbox workspace-write --ask-for-approval never <prompt>`.
|
||||
/// - Reprise (id connu) : `codex exec resume <thread_id> --json
|
||||
/// --skip-git-repo-check <prompt>`.
|
||||
/// --skip-git-repo-check --sandbox workspace-write --ask-for-approval never
|
||||
/// <prompt>`.
|
||||
///
|
||||
/// NOTE intégration (D3, hors parsing) : pour qu'un agent puisse **écrire**, le
|
||||
/// lancement devra ajouter `--sandbox workspace-write` + une politique
|
||||
/// d'approbation non bloquante. Ce n'est pas le rôle de cette composition.
|
||||
/// **Autonomie d'écriture (D3)** : `--sandbox workspace-write` autorise l'agent à
|
||||
/// écrire dans son workspace et `--ask-for-approval never` évite tout blocage sur
|
||||
/// une demande d'approbation interactive (un tour structuré non-interactif ne peut
|
||||
/// répondre à un prompt). Défaut raisonnable, aligné sur l'autonomie projet
|
||||
/// (CLAUDE.md §12) ; à terme **piloté par les permissions de l'agent**
|
||||
/// (`.ideai/permissions.json` + sandbox OS) — non implémenté ici.
|
||||
fn build_spawn_line(&self, prompt: &str) -> SpawnLine {
|
||||
let mut args = vec!["exec".to_owned()];
|
||||
if let Some(id) = self.conversation_id.lock().expect("mutex sain").as_ref() {
|
||||
@ -157,6 +162,10 @@ impl CodexExecSession {
|
||||
}
|
||||
args.push("--json".to_owned());
|
||||
args.push("--skip-git-repo-check".to_owned());
|
||||
args.push("--sandbox".to_owned());
|
||||
args.push("workspace-write".to_owned());
|
||||
args.push("--ask-for-approval".to_owned());
|
||||
args.push("never".to_owned());
|
||||
args.push(prompt.to_owned());
|
||||
SpawnLine {
|
||||
command: self.command.clone(),
|
||||
|
||||
@ -1127,4 +1127,93 @@ mod tests {
|
||||
let _ = std::fs::remove_file(&cmd);
|
||||
let _ = std::fs::remove_file(&argv);
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// DURCISSEMENT QA (lot D3, §17.9 D3) — autonomie d'écriture Codex :
|
||||
// la commande générée porte `--sandbox workspace-write` ET
|
||||
// `--ask-for-approval never`, conversation NEUVE comme en REPRISE.
|
||||
// Prouvé via le sidecar argv du fake enregistreur (jamais le vrai codex).
|
||||
// =====================================================================
|
||||
|
||||
/// Conversation NEUVE : `codex exec --json --skip-git-repo-check
|
||||
/// --sandbox workspace-write --ask-for-approval never <prompt>`. On prouve la
|
||||
/// présence des deux flags d'autonomie ET leur appariement valeur (sandbox=
|
||||
/// workspace-write, ask-for-approval=never), sans sous-commande `resume`.
|
||||
#[tokio::test]
|
||||
async fn codex_new_conversation_command_carries_autonomy_flags() {
|
||||
let (cmd, argv) = make_recording_fake(&[
|
||||
r#"{"type":"thread.started","thread_id":"cx-new"}"#,
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"ok"}}"#,
|
||||
]);
|
||||
let session = CodexExecSession::new(SessionId::new_random(), cmd.clone(), "/", None);
|
||||
let _ = session.send("salut").await.expect("send ok");
|
||||
let recorded = std::fs::read_to_string(&argv).expect("argv");
|
||||
let args: Vec<&str> = recorded.lines().collect();
|
||||
|
||||
// Conversation neuve : pas de sous-commande resume.
|
||||
assert!(
|
||||
!args.contains(&"resume"),
|
||||
"1er tour NEUF ne doit PAS porter resume, vu: {args:?}"
|
||||
);
|
||||
// Flags d'autonomie (D3), avec leur valeur appariée.
|
||||
assert!(args.contains(&"--sandbox"), "vu: {args:?}");
|
||||
assert!(args.contains(&"workspace-write"), "vu: {args:?}");
|
||||
assert!(args.contains(&"--ask-for-approval"), "vu: {args:?}");
|
||||
assert!(args.contains(&"never"), "vu: {args:?}");
|
||||
// `--sandbox` est immédiatement suivi de `workspace-write`.
|
||||
let sb = args.iter().position(|a| *a == "--sandbox").unwrap();
|
||||
assert_eq!(
|
||||
args.get(sb + 1),
|
||||
Some(&"workspace-write"),
|
||||
"--sandbox doit être suivi de workspace-write, vu: {args:?}"
|
||||
);
|
||||
// `--ask-for-approval` est immédiatement suivi de `never`.
|
||||
let af = args.iter().position(|a| *a == "--ask-for-approval").unwrap();
|
||||
assert_eq!(
|
||||
args.get(af + 1),
|
||||
Some(&"never"),
|
||||
"--ask-for-approval doit être suivi de never, vu: {args:?}"
|
||||
);
|
||||
// La base + le prompt restent présents.
|
||||
assert!(args.contains(&"--json") && args.contains(&"--skip-git-repo-check"));
|
||||
assert!(args.contains(&"salut"), "vu: {args:?}");
|
||||
let _ = std::fs::remove_file(&cmd);
|
||||
let _ = std::fs::remove_file(&argv);
|
||||
}
|
||||
|
||||
/// REPRISE (seed d'id) : `codex exec resume <id> --json --skip-git-repo-check
|
||||
/// --sandbox workspace-write --ask-for-approval never <prompt>`. Les flags
|
||||
/// d'autonomie sont AUSSI présents lors d'une reprise.
|
||||
#[tokio::test]
|
||||
async fn codex_resume_command_carries_autonomy_flags() {
|
||||
let (cmd, argv) = make_recording_fake(&[
|
||||
r#"{"type":"item.completed","item":{"id":"i0","type":"agent_message","text":"ok"}}"#,
|
||||
]);
|
||||
let session = CodexExecSession::new(
|
||||
SessionId::new_random(),
|
||||
cmd.clone(),
|
||||
"/",
|
||||
Some("cx-id".to_owned()),
|
||||
);
|
||||
let _ = session.send("vas-y").await.expect("send ok");
|
||||
let recorded = std::fs::read_to_string(&argv).expect("argv");
|
||||
let args: Vec<&str> = recorded.lines().collect();
|
||||
|
||||
// Reprise : sous-commande resume + id.
|
||||
assert!(args.contains(&"resume"), "vu: {args:?}");
|
||||
assert!(args.contains(&"cx-id"), "vu: {args:?}");
|
||||
// Flags d'autonomie présents même en reprise.
|
||||
let sb = args
|
||||
.iter()
|
||||
.position(|a| *a == "--sandbox")
|
||||
.expect("--sandbox présent en reprise");
|
||||
assert_eq!(args.get(sb + 1), Some(&"workspace-write"), "vu: {args:?}");
|
||||
let af = args
|
||||
.iter()
|
||||
.position(|a| *a == "--ask-for-approval")
|
||||
.expect("--ask-for-approval présent en reprise");
|
||||
assert_eq!(args.get(af + 1), Some(&"never"), "vu: {args:?}");
|
||||
let _ = std::fs::remove_file(&cmd);
|
||||
let _ = std::fs::remove_file(&argv);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user