merge(agents): démarrage custom CLI web téléphone — sandbox OS non relayé aux providers structurés — #227 (QA verte)

This commit is contained in:
2026-08-20 14:07:51 +02:00
5 changed files with 65 additions and 60 deletions

View File

@ -1,6 +1,27 @@
--- ---
issueRef: "#227" issueRef: "#227"
version: 2 version: 6
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"}
updatedAt: 1787227116713 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).

View File

@ -2,16 +2,16 @@
id: "1cbd4308-7bed-45eb-9105-0434b28f7ec5" id: "1cbd4308-7bed-45eb-9105-0434b28f7ec5"
number: 227 number: 227
title: "Bug web téléphone: la CLI custom ne démarre pas" title: "Bug web téléphone: la CLI custom ne démarre pas"
status: "inProgress" status: "closed"
priority: "high" priority: "high"
sprint: null sprint: null
links: [] links: []
agentRefs: [] agentRefs: []
attachments: [] attachments: []
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} 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 createdAt: 1787227106844
updatedAt: 1787227116713 updatedAt: 1787227664912
version: 2 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. 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.

View File

@ -3047,7 +3047,7 @@
"issueRef": "#227", "issueRef": "#227",
"path": "227", "path": "227",
"title": "Bug web téléphone: la CLI custom ne démarre pas", "title": "Bug web téléphone: la CLI custom ne démarre pas",
"status": "inProgress", "status": "closed",
"priority": "high", "priority": "high",
"sprint": null, "sprint": null,
"assignedAgentIds": [], "assignedAgentIds": [],
@ -3055,7 +3055,7 @@
"kind": "agent", "kind": "agent",
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
}, },
"updatedAt": 1787227116713 "updatedAt": 1787227664912
} }
] ]
} }

View File

@ -2439,7 +2439,7 @@ impl LaunchAgent {
input.node_id, input.node_id,
size, size,
&spec.env, &spec.env,
spec.sandbox.as_ref(), None,
structured_policy.as_ref(), structured_policy.as_ref(),
launch_managed_temp, launch_managed_temp,
local_model_server_guard, local_model_server_guard,
@ -2547,8 +2547,9 @@ impl LaunchAgent {
launch_managed_temp: Option<LaunchManagedTemp>, launch_managed_temp: Option<LaunchManagedTemp>,
model_server_guard: Option<ModelServerUseGuard>, model_server_guard: Option<ModelServerUseGuard>,
) -> Result<LaunchAgentOutput, AppError> { ) -> Result<LaunchAgentOutput, AppError> {
// Relaie le plan de sandbox OS (lot LP4-4) à la fabrique : `spec.sandbox`, // Structured/headless providers manage their own CLI-compatible sandbox
// déjà compilé (pur, domaine) en step 5d. `None` ⇒ exécution native inchangée. // 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 let session = match factory
.start( .start(
profile, profile,

View File

@ -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" "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( 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" "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::<Vec<_>>();
assert_eq!( assert_eq!(
sandbox_roots, writable_roots, sandboxes,
"OS sandbox write roots must match Codex writable_roots" vec![None],
); "structured launch must not pass an OS SandboxPlan to the provider"
assert!(
sandbox
.allowed
.iter()
.all(|grant| grant.access == domain::sandbox::PathAccess::RW),
"Codex writable roots must be added as write grants"
); );
} }
@ -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" "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_eq!(
assert!( factory.sandboxes(),
sandbox.allowed.is_empty(), vec![None],
"OS sandbox must not grant a broad root that would include the denied subpath" "structured launch must not pass an OS SandboxPlan to the provider"
); );
let _ = std::fs::remove_dir_all(&root); let _ = std::fs::remove_dir_all(&root);
} }
@ -4628,7 +4620,7 @@ async fn codex_structured_disjoint_write_allow_survives_unrelated_subpath_deny()
} }
#[tokio::test] #[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 root = fresh_temp_dir("managed-temp-exposure");
let managed_root = root.join("tmp").join("agent-output"); let managed_root = root.join("tmp").join("agent-output");
std::fs::create_dir_all(&managed_root).unwrap(); 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" "Codex writable_roots must include permission roots plus the resolved managed-temp path"
); );
let sandbox = factory.sandboxes().remove(0).expect("sandbox plan");
assert_eq!( assert_eq!(
sandbox.allowed, factory.sandboxes(),
vec![ vec![None],
domain::sandbox::PathGrant { "structured launch must not pass an OS SandboxPlan to the provider"
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"
); );
let _ = std::fs::remove_dir_all(&root); 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] #[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 root = fresh_temp_dir("launch-tmp-structured-non-codex");
let tmp = root.join("managed-tmp-main").to_string_lossy().into_owned(); let tmp = root.join("managed-tmp-main").to_string_lossy().into_owned();
let store = Arc::new(FakeManagedTempStore::default()); 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("TMP"), Some(&tmp));
assert_eq!(env.get("TEMP"), Some(&tmp)); assert_eq!(env.get("TEMP"), Some(&tmp));
assert_eq!(factory.policies(), vec![None]); assert_eq!(factory.policies(), vec![None]);
let sandbox = factory.sandboxes().remove(0).expect("sandbox plan"); assert_eq!(
assert!(sandbox_has_rw_root(&sandbox, &tmp)); factory.sandboxes(),
vec![None],
"structured launch must not pass an OS SandboxPlan to the provider"
);
let _ = std::fs::remove_dir_all(&root); let _ = std::fs::remove_dir_all(&root);
} }
#[tokio::test] #[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 root = fresh_temp_dir("launch-tmp-structured");
let tmp = root.join("managed-tmp-main").to_string_lossy().into_owned(); let tmp = root.join("managed-tmp-main").to_string_lossy().into_owned();
let store = Arc::new(FakeManagedTempStore::default()); 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"); panic!("expected Codex policy");
}; };
assert!(writable_roots.contains(&tmp)); assert!(writable_roots.contains(&tmp));
let sandbox = factory.sandboxes().remove(0).expect("sandbox plan"); assert_eq!(
assert!(sandbox_has_rw_root(&sandbox, &tmp)); factory.sandboxes(),
vec![None],
"structured launch must not pass an OS SandboxPlan to the provider"
);
let _ = std::fs::remove_dir_all(&root); 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()], vec![root.to_string_lossy().into_owned(), managed_root.clone()],
"main launch temp and additive exposure path must be deduped" "main launch temp and additive exposure path must be deduped"
); );
let sandbox = factory.sandboxes().remove(0).expect("sandbox plan");
assert_eq!( assert_eq!(
sandbox factory.sandboxes(),
.allowed vec![None],
.iter() "structured launch must not pass an OS SandboxPlan to the provider"
.filter(|grant| grant.abs_root == managed_root)
.count(),
1,
"OS sandbox must also dedupe the shared path"
); );
let _ = std::fs::remove_dir_all(&root); let _ = std::fs::remove_dir_all(&root);
} }