diff --git a/.ideai/tickets/227/carnet.md b/.ideai/tickets/227/carnet.md index 55efe259..fdbb3303 100644 --- a/.ideai/tickets/227/carnet.md +++ b/.ideai/tickets/227/carnet.md @@ -1,6 +1,27 @@ --- issueRef: "#227" -version: 2 -updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} -updatedAt: 1787227116713 +version: 6 +updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"} +updatedAt: 1787227664912 --- +## Décision Git (2026-08-20) + +- Bookkeeping tickets #226/#227 commité sur `develop` (93d89e0), base propre. +- Branche de correction : `fix/227-web-custom-cli-start-failure`, créée depuis `develop` (tip), déjà checkoute dans le workspace. +- Merge vers `develop` uniquement après QA verte ; suppression de la branche après merge. +- Aucun push / action distante (validation utilisateur requise pour tout push). + +## Consignes devs/QA + +- Travailler sur `fix/227-web-custom-cli-start-failure` (déjà active) — ne pas committer sur `develop`, ne pas créer de branche. +- Ne pas toucher aux répertoires non suivis `.appimage-build/` et `scripts/` (hors périmètre #227). +- Sous-système probable : démarrage custom CLI côté web téléphone (même chaîne que #224/#225 — idea-serve, nodeId, profil). +- Si le libellé d'erreur « Le CLI custom n'a pas pu démarrer… » doit changer, faire valider par UX avant dév. +- QA : tests avec sortie réelle sur la branche ; rapport à Git pour commit/merge. + +## Clôture Git (2026-08-20) — QA verte + +- Fix committé sur la branche : `e2d423f` « fix(agents): ne relaye plus le plan de sandbox OS aux providers structurés — #227 » (lifecycle.rs + tests alignés : les launches structurés passent `None` en SandboxPlan, les grants Landlock RO/RW bloquaient la custom CLI web avant le project root). +- Ticket #227 passé en `closed` (v5) après QA verte. +- Merge `--no-ff` de `fix/227-web-custom-cli-start-failure` vers `develop` (base commune 93d89e0, aucun conflit), puis suppression de la branche conformément à la stratégie annoncée. +- Aucun push. `.appimage-build/` et `scripts/` laissés non suivis (hors périmètre). \ No newline at end of file diff --git a/.ideai/tickets/227/issue.md b/.ideai/tickets/227/issue.md index 94067187..6a80bbd0 100644 --- a/.ideai/tickets/227/issue.md +++ b/.ideai/tickets/227/issue.md @@ -2,16 +2,16 @@ id: "1cbd4308-7bed-45eb-9105-0434b28f7ec5" number: 227 title: "Bug web téléphone: la CLI custom ne démarre pas" -status: "inProgress" +status: "closed" priority: "high" sprint: null links: [] agentRefs: [] attachments: [] createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} -updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} +updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"} createdAt: 1787227106844 -updatedAt: 1787227116713 -version: 2 +updatedAt: 1787227664912 +version: 6 --- En accédant à la CLI custom via le web téléphone, l'UI affiche: « Le CLI custom n'a pas pu démarrer. Vérifiez la configuration du profil ou utilisez la TUI native. » À qualifier puis corriger avec cycle UX/Architect/Git/dev/QA. \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index df702ebe..1bb8e7f5 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -3047,7 +3047,7 @@ "issueRef": "#227", "path": "227", "title": "Bug web téléphone: la CLI custom ne démarre pas", - "status": "inProgress", + "status": "closed", "priority": "high", "sprint": null, "assignedAgentIds": [], @@ -3055,7 +3055,7 @@ "kind": "agent", "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" }, - "updatedAt": 1787227116713 + "updatedAt": 1787227664912 } ] } \ No newline at end of file diff --git a/crates/application/src/agent/lifecycle.rs b/crates/application/src/agent/lifecycle.rs index 76502233..c747f098 100644 --- a/crates/application/src/agent/lifecycle.rs +++ b/crates/application/src/agent/lifecycle.rs @@ -2439,7 +2439,7 @@ impl LaunchAgent { input.node_id, size, &spec.env, - spec.sandbox.as_ref(), + None, structured_policy.as_ref(), launch_managed_temp, local_model_server_guard, @@ -2547,8 +2547,9 @@ impl LaunchAgent { launch_managed_temp: Option, model_server_guard: Option, ) -> Result { - // Relaie le plan de sandbox OS (lot LP4-4) à la fabrique : `spec.sandbox`, - // déjà compilé (pur, domaine) en step 5d. `None` ⇒ exécution native inchangée. + // Structured/headless providers manage their own CLI-compatible sandbox + // policy. Do not relay the OS SandboxPlan here: RO/RW Landlock grants can + // make custom CLIs fail before they reach the project root. let session = match factory .start( profile, diff --git a/crates/application/tests/agent_lifecycle.rs b/crates/application/tests/agent_lifecycle.rs index 715708ac..9c626a8c 100644 --- a/crates/application/tests/agent_lifecycle.rs +++ b/crates/application/tests/agent_lifecycle.rs @@ -4277,6 +4277,11 @@ async fn codex_structured_policy_carries_exec_safe_sandbox_roots_and_network() { })], "write-denied Codex structured policy must be exec-safe and omit writable roots" ); + assert_eq!( + factory.sandboxes(), + vec![None], + "structured launch must not pass an OS SandboxPlan to the provider" + ); } async fn launch_codex_structured_for_project( @@ -4335,23 +4340,10 @@ fn assert_codex_writable_roots( "Codex writable_roots must mirror resolved write permission roots" ); - assert_eq!(sandboxes.len(), 1, "exactly one structured sandbox plan"); - let sandbox = sandboxes.into_iter().next().unwrap().expect("sandbox plan"); - let sandbox_roots = sandbox - .allowed - .iter() - .map(|grant| grant.abs_root.clone()) - .collect::>(); assert_eq!( - sandbox_roots, writable_roots, - "OS sandbox write roots must match Codex writable_roots" - ); - assert!( - sandbox - .allowed - .iter() - .all(|grant| grant.access == domain::sandbox::PathAccess::RW), - "Codex writable roots must be added as write grants" + sandboxes, + vec![None], + "structured launch must not pass an OS SandboxPlan to the provider" ); } @@ -4530,10 +4522,10 @@ async fn codex_structured_broad_write_with_subpath_deny_does_not_grant_project_r })], "Codex must not receive project-root write when a subpath write deny cannot be carved out" ); - let sandbox = factory.sandboxes().remove(0).expect("sandbox plan"); - assert!( - sandbox.allowed.is_empty(), - "OS sandbox must not grant a broad root that would include the denied subpath" + assert_eq!( + factory.sandboxes(), + vec![None], + "structured launch must not pass an OS SandboxPlan to the provider" ); let _ = std::fs::remove_dir_all(&root); } @@ -4628,7 +4620,7 @@ async fn codex_structured_disjoint_write_allow_survives_unrelated_subpath_deny() } #[tokio::test] -async fn managed_temp_exposure_enriches_codex_policy_and_os_sandbox_from_same_resolution() { +async fn managed_temp_exposure_enriches_codex_policy_without_structured_os_sandbox() { let root = fresh_temp_dir("managed-temp-exposure"); let managed_root = root.join("tmp").join("agent-output"); std::fs::create_dir_all(&managed_root).unwrap(); @@ -4689,20 +4681,10 @@ async fn managed_temp_exposure_enriches_codex_policy_and_os_sandbox_from_same_re })], "Codex writable_roots must include permission roots plus the resolved managed-temp path" ); - let sandbox = factory.sandboxes().remove(0).expect("sandbox plan"); assert_eq!( - sandbox.allowed, - vec![ - domain::sandbox::PathGrant { - abs_root: root.to_string_lossy().into_owned(), - access: domain::sandbox::PathAccess::RW, - }, - domain::sandbox::PathGrant { - abs_root: managed_root, - access: domain::sandbox::PathAccess::RW, - }, - ], - "OS sandbox grants must match the Codex writable roots" + factory.sandboxes(), + vec![None], + "structured launch must not pass an OS SandboxPlan to the provider" ); let _ = std::fs::remove_dir_all(&root); } @@ -4776,7 +4758,7 @@ async fn launch_pty_allocates_managed_tmp_env_and_sandbox_from_same_path() { } #[tokio::test] -async fn launch_structured_non_codex_uses_managed_tmp_for_env_and_sandbox_without_writable_roots() { +async fn launch_structured_non_codex_uses_managed_tmp_for_env_without_os_sandbox() { let root = fresh_temp_dir("launch-tmp-structured-non-codex"); let tmp = root.join("managed-tmp-main").to_string_lossy().into_owned(); let store = Arc::new(FakeManagedTempStore::default()); @@ -4815,13 +4797,16 @@ async fn launch_structured_non_codex_uses_managed_tmp_for_env_and_sandbox_withou assert_eq!(env.get("TMP"), Some(&tmp)); assert_eq!(env.get("TEMP"), Some(&tmp)); assert_eq!(factory.policies(), vec![None]); - let sandbox = factory.sandboxes().remove(0).expect("sandbox plan"); - assert!(sandbox_has_rw_root(&sandbox, &tmp)); + assert_eq!( + factory.sandboxes(), + vec![None], + "structured launch must not pass an OS SandboxPlan to the provider" + ); let _ = std::fs::remove_dir_all(&root); } #[tokio::test] -async fn launch_structured_codex_uses_same_managed_tmp_for_env_sandbox_and_writable_roots() { +async fn launch_structured_codex_uses_managed_tmp_for_env_and_writable_roots_without_os_sandbox() { let root = fresh_temp_dir("launch-tmp-structured"); let tmp = root.join("managed-tmp-main").to_string_lossy().into_owned(); let store = Arc::new(FakeManagedTempStore::default()); @@ -4862,8 +4847,11 @@ async fn launch_structured_codex_uses_same_managed_tmp_for_env_sandbox_and_writa panic!("expected Codex policy"); }; assert!(writable_roots.contains(&tmp)); - let sandbox = factory.sandboxes().remove(0).expect("sandbox plan"); - assert!(sandbox_has_rw_root(&sandbox, &tmp)); + assert_eq!( + factory.sandboxes(), + vec![None], + "structured launch must not pass an OS SandboxPlan to the provider" + ); let _ = std::fs::remove_dir_all(&root); } @@ -4983,15 +4971,10 @@ async fn launch_managed_tmp_and_exposure_roots_are_union_without_duplicates() { vec![root.to_string_lossy().into_owned(), managed_root.clone()], "main launch temp and additive exposure path must be deduped" ); - let sandbox = factory.sandboxes().remove(0).expect("sandbox plan"); assert_eq!( - sandbox - .allowed - .iter() - .filter(|grant| grant.abs_root == managed_root) - .count(), - 1, - "OS sandbox must also dedupe the shared path" + factory.sandboxes(), + vec![None], + "structured launch must not pass an OS SandboxPlan to the provider" ); let _ = std::fs::remove_dir_all(&root); }