From f8cf86c6f5546d4a740d6a856c32a832ebd0b684 Mon Sep 17 00:00:00 2001 From: Blomios Date: Sat, 22 Aug 2026 09:50:00 +0200 Subject: [PATCH] =?UTF-8?q?fix(infra):=20temp=20IdeA=20g=C3=A9r=C3=A9=20al?= =?UTF-8?q?lou=C3=A9=20avant=20le=20convention=20file,=20expos=C3=A9=20via?= =?UTF-8?q?=20IDEA=5FMANAGED=5FTMP=20et=20instruction=20explicite=20privil?= =?UTF-8?q?=C3=A9giant=20le=20temp=20g=C3=A9r=C3=A9=20=C3=A0=20/tmp=20?= =?UTF-8?q?=E2=80=94=20#200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ideai/tickets/200/carnet.md | 14 ++- .ideai/tickets/200/issue.md | 6 +- crates/application/src/agent/lifecycle.rs | 128 ++++++++++++++------ crates/application/tests/agent_lifecycle.rs | 19 ++- 4 files changed, 120 insertions(+), 47 deletions(-) diff --git a/.ideai/tickets/200/carnet.md b/.ideai/tickets/200/carnet.md index 3f67869a..19cebaf7 100644 --- a/.ideai/tickets/200/carnet.md +++ b/.ideai/tickets/200/carnet.md @@ -1,8 +1,8 @@ --- issueRef: "#200" -version: 13 +version: 16 updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} -updatedAt: 1786745694791 +updatedAt: 1787384954575 --- 2026-08-14 Main - Reprise du ticket après livraison initiale du lot Codex-centric. @@ -11,4 +11,12 @@ updatedAt: 1786745694791 - Git: branche locale retenue `feature/200-managed-tmp-all-agents-purge` depuis `develop` @ `e6f6eb5b`. - DevBackend: implémentation terminée. Résumé: `ManagedTempStore::purge_all` + use case `PurgeManagedTemp`, câblage backend/app-tauri au shutdown, test structured non-Codex pour env+sandbox sans writable_roots provider-specific, conservation des garanties Codex/stop/release/union sans doublons. - QA: verdict VERT sur l'extension. Commandes validées: `cargo test -p application --test agent_lifecycle launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path`, `cargo test -p application --test agent_lifecycle launch_structured_non_codex_uses_managed_tmp_for_env_and_sandbox_without_writable_roots`, `cargo test -p application --test agent_lifecycle launch_structured_codex_uses_same_managed_tmp_for_env_sandbox_and_writable_roots`, `cargo test -p application --test agent_lifecycle stop_pty_session_releases_primary_launch_managed_tmp`, `cargo test -p application --test agent_lifecycle launch_managed_tmp_and_exposure_roots_are_union_without_duplicates`, `cargo test -p application --test managed_temp purge_managed_temp_calls_store_and_returns_report`, `cargo test -p infrastructure --test managed_temp purge`, `cargo test -p app-tauri --lib shutdown_managed_temp_purge_is_attempted_and_non_blocking_on_error`, `cargo test -p backend --lib --no-run`. -- Suite du cycle: commit/merge local par Git, puis fermeture du ticket. \ No newline at end of file +- Suite du cycle: commit/merge local par Git, puis fermeture du ticket. + +2026-08-22 Main +- Réouverture suite à un cas réel observé utilisateur: un agent a annoncé vouloir cloner Flutter dans `/tmp` malgré le lot livré. +- Vérification Main: le runtime de launch injecte bien `TMPDIR`, `TMP`, `TEMP` depuis le managed temp principal (`crates/application/src/agent/lifecycle.rs`, étape 5d) et les tests `agent_lifecycle` couvrent PTY, structured non-Codex et structured Codex. +- Analyse Architect: pas un gap de design; suspicion de chemin secondaire ou de comportement agent qui continue d'utiliser/mentionner `/tmp` explicitement au lieu de suivre l'env temp fourni. +- Suite proposée: reproduire le cas réel avec le provider/profil concerné, vérifier la valeur réellement vue dans le process agent, puis décider entre fix runtime (si env perdu/contourné) et renfort d'instruction/runtime API (si l'agent hardcode `/tmp`). +- Git a retenu une branche de fix dédiée `fix/200-managed-tmp-injection-regression` depuis `develop`, HEAD `3344361fc27bace7f68953f7a11623af1d8a7d86`. +- DevBackend: diagnostic retenu = l'env temp était déjà injecté, mais le convention file était généré avant l'allocation du temp, donc aucun signal textuel explicite ne guidait l'agent; correctif implémenté = allocation plus tôt dans le launch path, ajout de `IDEA_MANAGED_TMP`, et injection conditionnelle d'une instruction “Temporaire IdeA géré” avec le chemin concret; tests agent_lifecycle renforcés. \ No newline at end of file diff --git a/.ideai/tickets/200/issue.md b/.ideai/tickets/200/issue.md index a1af0a6d..58c99aed 100644 --- a/.ideai/tickets/200/issue.md +++ b/.ideai/tickets/200/issue.md @@ -2,7 +2,7 @@ id: "0268a6a4-2ceb-4332-a113-29b93a59caf1" number: 200 title: "Remplacer l'usage implicite de /tmp par un temp IdeA géré pour les agents" -status: "closed" +status: "inProgress" priority: "high" sprint: null links: [] @@ -11,7 +11,7 @@ attachments: [] createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} createdAt: 1786743649446 -updatedAt: 1786745694791 -version: 13 +updatedAt: 1787384954575 +version: 16 --- Extension du lot temp IdeA géré. Le temp principal launch-scoped existe déjà mais le comportement doit maintenant être généralisé à tous les agents, pas seulement Codex, afin que n'importe quel agent puisse utiliser le répertoire temporaire IdeA via ses variables d'environnement/runtime appropriées. En plus du release au stop de session déjà en place, IdeA doit purger le contenu de ce stockage temporaire à la fermeture de l'application pour éviter l'accumulation résiduelle. Le lot couvre donc: projection cross-agent du temp IdeA principal, cohérence sandbox/runtime selon les profils, et purge à l'extinction d'IdeA. \ No newline at end of file diff --git a/crates/application/src/agent/lifecycle.rs b/crates/application/src/agent/lifecycle.rs index c747f098..bf639e39 100644 --- a/crates/application/src/agent/lifecycle.rs +++ b/crates/application/src/agent/lifecycle.rs @@ -2278,18 +2278,42 @@ impl LaunchAgent { }) .await; } - self.apply_injection( - &input.project, - &agent.context_path, - &effective_context, - profile.mcp.is_some(), - &mut spec, - ) - .await?; - - let local_model_server_guard = self - .ensure_local_model_server_for_opencode(&input.project, &agent, &mut profile) + let launch_managed_temp = self + .allocate_launch_managed_temp(&input.project, &agent) .await?; + if let Some(temp) = &launch_managed_temp { + upsert_env(&mut spec.env, "TMPDIR", &temp.path); + upsert_env(&mut spec.env, "TMP", &temp.path); + upsert_env(&mut spec.env, "TEMP", &temp.path); + upsert_env(&mut spec.env, "IDEA_MANAGED_TMP", &temp.path); + } + if let Err(err) = self + .apply_injection( + &input.project, + &agent.context_path, + &effective_context, + profile.mcp.is_some(), + launch_managed_temp.as_ref().map(|temp| temp.path.as_str()), + &mut spec, + ) + .await + { + self.release_launch_managed_temp_best_effort(launch_managed_temp.as_ref()) + .await; + return Err(err); + } + + let local_model_server_guard = match self + .ensure_local_model_server_for_opencode(&input.project, &agent, &mut profile) + .await + { + Ok(guard) => guard, + Err(err) => { + self.release_launch_managed_temp_best_effort(launch_managed_temp.as_ref()) + .await; + return Err(err); + } + }; // 5a. ── INJECTION DE LA CONF MCP (cadrage v3, Décision 3) ── // Strictement APRÈS le convention file (étape 5) et AVANT le spawn / @@ -2297,15 +2321,21 @@ impl LaunchAgent { // IdeA matérialise SA config MCP au format de CETTE CLI, dans le **même** // run dir isolé que le convention file et le seed de permissions. `None` // ⇒ aucun write/flag/env (chemin actuel inchangé, zéro régression). - self.apply_mcp_config( - &profile, - &run_dir, - &input.project.root, - input.mcp_runtime.as_ref(), - effective_permissions.as_ref(), - &mut spec, - ) - .await?; + if let Err(err) = self + .apply_mcp_config( + &profile, + &run_dir, + &input.project.root, + input.mcp_runtime.as_ref(), + effective_permissions.as_ref(), + &mut spec, + ) + .await + { + self.release_launch_managed_temp_best_effort(launch_managed_temp.as_ref()) + .await; + return Err(err); + } // 5c. ── PROJECTION DES PERMISSIONS (lot LP3-3) ── // Strictement APRÈS le convention file (5) ET la conf MCP (5a), donc @@ -2313,15 +2343,21 @@ impl LaunchAgent { // héritent de la projection (fichiers du plan écrits dans le run dir, // `args`/`env` foldés dans `spec`). `None` registre / profil non // projetable / `eff == None` ⇒ no-op (prompting natif conservé). - self.apply_permission_projection( - &profile, - &run_dir, - &input.project.root, - effective_permissions.as_ref(), - network_permission, - &mut spec, - ) - .await?; + if let Err(err) = self + .apply_permission_projection( + &profile, + &run_dir, + &input.project.root, + effective_permissions.as_ref(), + network_permission, + &mut spec, + ) + .await + { + self.release_launch_managed_temp_best_effort(launch_managed_temp.as_ref()) + .await; + return Err(err); + } // 5d. ── PLAN DE SANDBOX OS (lot LP4-3) ── // Strictement APRÈS la résolution des permissions (3) et la projection @@ -2331,14 +2367,6 @@ impl LaunchAgent { // remplir `spec.sandbox`. `eff == None` (rien posé) ⇒ `None` : aucun plan, // comportement natif conservé (invariant produit). L'enforcer concret // (Landlock/Noop) est injecté côté PTY au composition root. - let launch_managed_temp = self - .allocate_launch_managed_temp(&input.project, &agent) - .await?; - if let Some(temp) = &launch_managed_temp { - upsert_env(&mut spec.env, "TMPDIR", &temp.path); - upsert_env(&mut spec.env, "TMP", &temp.path); - upsert_env(&mut spec.env, "TEMP", &temp.path); - } let exposed_managed_temp_roots = match self .resolve_managed_temp_exposure(&input.managed_temp_exposure) .await @@ -2916,6 +2944,7 @@ impl LaunchAgent { context_rel_path: &str, effective: &EffectiveAgentContext, mcp_enabled: bool, + launch_managed_temp_path: Option<&str>, spec: &mut SpawnSpec, ) -> Result<(), AppError> { match spec.context_plan.clone() { @@ -2927,8 +2956,12 @@ impl LaunchAgent { // composed: an absolute project-root header (so the agent knows // where to operate, since its cwd is *not* the root), the agent's // persona `.md`, then the bodies of its assigned skills (§14.2). - let document = - compose_convention_file(project.root.as_str(), effective, mcp_enabled); + let document = compose_convention_file( + project.root.as_str(), + effective, + mcp_enabled, + launch_managed_temp_path, + ); let path = RemotePath::new(join(&spec.cwd, &target)); self.fs.write(&path, document.as_bytes()).await?; } @@ -3864,6 +3897,7 @@ pub(crate) fn compose_convention_file( project_root: &str, effective: &EffectiveAgentContext, mcp_enabled: bool, + launch_managed_temp_path: Option<&str>, ) -> String { let mut out = String::new(); out.push_str("# Project root\n\n"); @@ -3949,6 +3983,12 @@ pub(crate) fn compose_convention_file( ); } out.push_str(skill_awareness(mcp_enabled)); + if let Some(path) = launch_managed_temp_path + .map(str::trim) + .filter(|path| !path.is_empty()) + { + out.push_str(&managed_temp_awareness(path)); + } out.push_str(memory_awareness()); out.push_str("---\n\n"); @@ -4087,6 +4127,14 @@ fn memory_awareness() -> &'static str { réutilise le même `slug` pour remplacer une note existante.\n\n" } +fn managed_temp_awareness(path: &str) -> String { + format!( + "**Temporaire IdeA géré** : utilise `{path}` pour les gros fichiers temporaires, \ + clones, caches et artefacts de travail. Ce chemin est aussi exposé via \ + `IDEA_MANAGED_TMP`, `TMPDIR`, `TMP` et `TEMP`; privilégie-le à `/tmp`.\n\n" + ) +} + /// Renders a [`MemoryType`] as its stable lowercase label for the convention-file /// memory section (`user`/`feedback`/`project`/`reference`). #[must_use] @@ -4182,7 +4230,7 @@ mod tests { handoff: handoff.cloned(), live_rows: live_rows.to_vec(), }; - super::compose_convention_file(project_root, &effective, mcp_enabled) + super::compose_convention_file(project_root, &effective, mcp_enabled, None) } #[test] diff --git a/crates/application/tests/agent_lifecycle.rs b/crates/application/tests/agent_lifecycle.rs index 9c626a8c..55b49277 100644 --- a/crates/application/tests/agent_lifecycle.rs +++ b/crates/application/tests/agent_lifecycle.rs @@ -4732,7 +4732,7 @@ async fn launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path() { ContextInjection::convention_file("CLAUDE.md").unwrap(), ) .with_projector(ProjectorKey::Claude); - let (launch, agent, _fs, pty, _sessions) = launch_with_projection( + let (launch, agent, fs, pty, _sessions) = launch_with_projection( profile, Some(ContextInjectionPlan::File { target: "CLAUDE.md".to_owned(), @@ -4752,6 +4752,21 @@ async fn launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path() { assert_eq!(env.get("TMPDIR"), Some(&tmp)); assert_eq!(env.get("TMP"), Some(&tmp)); assert_eq!(env.get("TEMP"), Some(&tmp)); + assert_eq!(env.get("IDEA_MANAGED_TMP"), Some(&tmp)); + let writes = fs.context_writes(); + let convention = String::from_utf8(writes[0].1.clone()).unwrap(); + assert!( + convention.contains("**Temporaire IdeA géré**"), + "convention file must explicitly surface the managed temp directory: {convention}" + ); + assert!( + convention.contains(&tmp), + "convention file must carry the concrete managed temp path: {convention}" + ); + assert!( + convention.contains("privilégie-le à `/tmp`"), + "convention file must steer agents away from literal /tmp usage: {convention}" + ); let sandbox = spawn.sandbox.expect("sandbox plan"); assert!(sandbox_has_rw_root(&sandbox, &tmp)); let _ = std::fs::remove_dir_all(&root); @@ -4796,6 +4811,7 @@ async fn launch_structured_non_codex_uses_managed_tmp_for_env_without_os_sandbox assert_eq!(env.get("TMPDIR"), Some(&tmp)); assert_eq!(env.get("TMP"), Some(&tmp)); assert_eq!(env.get("TEMP"), Some(&tmp)); + assert_eq!(env.get("IDEA_MANAGED_TMP"), Some(&tmp)); assert_eq!(factory.policies(), vec![None]); assert_eq!( factory.sandboxes(), @@ -4841,6 +4857,7 @@ async fn launch_structured_codex_uses_managed_tmp_for_env_and_writable_roots_wit assert_eq!(env.get("TMPDIR"), Some(&tmp)); assert_eq!(env.get("TMP"), Some(&tmp)); assert_eq!(env.get("TEMP"), Some(&tmp)); + assert_eq!(env.get("IDEA_MANAGED_TMP"), Some(&tmp)); let Some(domain::ports::StructuredProviderLaunchPolicy::Codex { writable_roots, .. }) = factory.policies().into_iter().next().unwrap() else {