chore(ideai): carnet de #68, passage en QA et note du faux vert sandbox

Dernier état `.ideai/` du sprint serveur embarqué, séparé du code applicatif
conformément aux précédents `f965f64` / `7fbaa8e` / `5ee25d1`.

- #68 passé en `qa`, pas en `closed` : mergé dans `develop` (`b30c9c7`) et vert,
  mais RIEN n'a été cliqué dans l'app réelle. « Mergé » ne veut pas dire
  « validé » — même distinction que pour #69 et son lot 3.
- Carnet de #68 : périmètre livré, réserve « aucune validation live », et la
  dette identifiée (ordre de `preview_settings` qui verrouille la liste des
  candidats LAN, warning `missingTrustedProxy` inatteignable).
- Mémoire projet : note `sandbox-eperm-bind-false-green-web-server`. Elle
  capitalise le piège qui s'est refermé DEUX FOIS le 2026-07-16 : les sandboxes
  de DevBackend et QA refusent `TcpListener::bind`, donc tout `cargo test` sur
  `web-server`/`app-tauri` y rend un vert qui ne prouve rien. La première fois,
  ce faux vert masquait un vrai bug produit — un serveur embarqué sur port
  éphémère rejetait toutes les requêtes API en 403. La règle qui en sort : aucun
  repli EPERM silencieux, et tout vert sur ces crates doit être produit hors
  sandbox.
- Journal des tâches de fond : complétions enregistrées.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 06:03:11 +02:00
parent f965f64b07
commit a13a6c1801
6 changed files with 161 additions and 9 deletions

View File

@ -7813,10 +7813,62 @@
"ticket_id": "e79249c2-3f08-4fb4-b917-c3d7d1f1ea40", "ticket_id": "e79249c2-3f08-4fb4-b917-c3d7d1f1ea40",
"conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4" "conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4"
}, },
"state": "running", "state": "completed",
"wakePolicy": "recordOnly", "wakePolicy": "recordOnly",
"createdAtMs": 1784238075328, "createdAtMs": 1784238075328,
"updatedAtMs": 1784238075328, "updatedAtMs": 1784238314757,
"deadlineMs": null,
"result": {
"outcome": "success",
"finished_at_ms": 1784238314757,
"exit_code": null,
"summary": "Headless rendezvous completed with Final",
"stdout_tail": "#68 est mergé, sans conflit. `develop` est sur **`f965f64`**, vert, arbre propre. La fonctionnalité demandée ce matin est dans `develop`.\n\n## Commits\n\n| Commit | |\n|---|---|\n| `49e1d5a` | `feat(app-tauri)` — controller, commandes, store B2 |\n| `7efa634` | `feat(frontend)` — gateway F1 + panneau Settings Deployment F2 |\n| `b30c9c7` | merge `--no-ff` dans `develop` |\n| `f965f64` | `chore(ideai)` — clôture de #72, à part |\n\nBackend et frontend en deux commits : une branche parce que c'est un lot, deux commits parce que ce sont deux intentions. Pas de rebase, `develop` n'avait pas bougé. Branche supprimée.\n\n## QA refaite hors sandbox\n\nTes quatre chiffres sont confirmés à l'identique : `app-tauri` 43/1 ignored, `web-server` 66, typecheck propre, `vitest` 87 fichiers / 789 tests. Puis re-vérifié sur `develop` après merge — 24 suites Rust, zéro échec, frontend toujours 789.\n\nJ'ai revérifié tes quatre invariants plutôt que de les prendre pour acquis, et ils tiennent tous. `run_embedded_with_core` est le seul appelant — aucune occurrence de `run_embedded` nu dans `app-tauri`, donc le seam de B1 sert bien à ce pour quoi il existe. Aucune fuite d'`EmbeddedServer*`/`ServerExposure*` dans `domain` ni `application`. L'ordre du store est correct : `write(tmp)` → `set_user_only_permissions(tmp)` → `rename`, le fichier final n'est jamais world-readable. Et `stop()` est bien appelé à la sortie d'app (`lib.rs:153`).\n\n**Sur le test `#[ignore]`, j'ai poussé un cran plus loin que ta vérification.** Il est bien préexistant — `#[ignore = \"requires local socket bind permission\"]` dans `mcp_bridge.rs`, présent sur `develop`, fichier que ce lot ne touche pas. Mais comme c'est exactement la classe de test qui nous a piégés deux fois aujourd'hui, je l'ai *lancé* hors sandbox avec `--ignored` plutôt que de raisonner dessus : il passe réellement. Garde d'environnement, confirmée par exécution.\n\nLes trois faits sont dans les messages de commit. Celui qui compte le plus est le brief faux : DevFrontend a intercepté un cadrage qui aurait produit un lot **vert et cassé** — chaque save et chaque start en mode 3 auraient échoué, sans qu'aucun test ne le voie. C'est écrit dans `7efa634`, avec la règle qui en sort : le frontend n'invente jamais une IP, il consomme `candidateLanAddresses`. La dette (ordre de `preview_settings`, code mort `missingTrustedProxy`) est dans `49e1d5a` et dans le merge.\n\n## Un point mineur, pour ton information\n\nLe `0600` est bien posé avant le `rename`, donc ta formulation est exacte. Il reste une fenêtre courte où le **temporaire** existe avec les permissions par défaut de l'umask avant le `chmod`. Le risque est faible — `server-exposure.json` contient des modes, ports, IP et proxies autorisés, aucun secret — et le fichier vit dans l'app-data-dir. Je ne le traite pas et je ne bloque rien ; à verser à la dette de #68 si quelqu'un veut un `OpenOptions::mode(0o600)` à la création.\n\n`develop` est en avance de 25 commits sur `origin/develop`. Aucune action sortante : pas de push, pas de PR.",
"stderr_tail": null
},
"completionDelivered": false
},
{
"id": "dd959500-f6ce-4d28-bcdd-5435da17f9f9",
"projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023",
"ownerAgentId": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5",
"kind": {
"kind": "headlessRendezvous",
"requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641",
"target_agent_id": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5",
"ticket_id": "71016dc7-ab66-46e9-8fbc-cb820e897b06",
"conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4"
},
"state": "completed",
"wakePolicy": "recordOnly",
"createdAtMs": 1784238493799,
"updatedAtMs": 1784238503624,
"deadlineMs": null,
"result": {
"outcome": "success",
"finished_at_ms": 1784238503624,
"exit_code": null,
"summary": "Headless rendezvous completed with Final",
"stdout_tail": "You've hit your session limit · resets 4am (Europe/Paris)",
"stderr_tail": null
},
"completionDelivered": false
},
{
"id": "bf76ce01-9c1c-415d-bfa4-30d4be5d86d9",
"projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023",
"ownerAgentId": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5",
"kind": {
"kind": "headlessRendezvous",
"requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641",
"target_agent_id": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5",
"ticket_id": "98635672-3215-46ed-806c-dd7de084c6eb",
"conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4"
},
"state": "running",
"wakePolicy": "recordOnly",
"createdAtMs": 1784260909376,
"updatedAtMs": 1784260909376,
"deadlineMs": null, "deadlineMs": null,
"result": null, "result": null,
"completionDelivered": false "completionDelivered": false

View File

@ -63,3 +63,4 @@
- [frontend-uses-npm-not-pnpm](frontend-uses-npm-not-pnpm.md) — memory note frontend-uses-npm-not-pnpm - [frontend-uses-npm-not-pnpm](frontend-uses-npm-not-pnpm.md) — memory note frontend-uses-npm-not-pnpm
- [idea-distribution-strategy-desktop-vs-docker](idea-distribution-strategy-desktop-vs-docker.md) — memory note idea-distribution-strategy-desktop-vs-docker - [idea-distribution-strategy-desktop-vs-docker](idea-distribution-strategy-desktop-vs-docker.md) — memory note idea-distribution-strategy-desktop-vs-docker
- [web-client-is-single-column-no-desktop-shell](web-client-is-single-column-no-desktop-shell.md) — memory note web-client-is-single-column-no-desktop-shell - [web-client-is-single-column-no-desktop-shell](web-client-is-single-column-no-desktop-shell.md) — memory note web-client-is-single-column-no-desktop-shell
- [sandbox-eperm-bind-false-green-web-server](sandbox-eperm-bind-false-green-web-server.md) — memory note sandbox-eperm-bind-false-green-web-server

View File

@ -0,0 +1,42 @@
---
name: sandbox-eperm-bind-false-green-web-server
description: memory note sandbox-eperm-bind-false-green-web-server
metadata:
type: project
---
---
name: sandbox-eperm-bind-false-green-web-server
description: Les sandboxes de DevBackend et QA bloquent TcpListener::bind (EPERM) ; tout `cargo test` sur web-server/app-tauri y rend un VERT QUI NE PROUVE RIEN. Vérifier hors sandbox.
metadata:
type: reference
---
# Faux vert : `TcpListener::bind` interdit dans les sandboxes agents
## Le fait
Les environnements d'exécution de **DevBackend et de QA** refusent `TcpListener::bind("127.0.0.1:0")` avec `Operation not permitted (os error 1)`. Tout test qui ouvre un socket y est structurellement inexécutable.
Crates concernés : `web-server`, `app-tauri` (pont MCP, serveur embarqué).
## Pourquoi c'est dangereux, pas juste gênant
Le 2026-07-16, ce piège s'est refermé **deux fois dans la même journée** :
1. **#68 B1** — DevBackend annonce « 57 passed ». En réalité le test `run_embedded_stop_shuts_down_accept_loop` sortait en silence sur EPERM via un repli, et le test du core partagé basculait sur un dispatch in-process. Rejoué **hors sandbox** : échec réel, révélant un **vrai bug produit**`run_embedded` ne réconciliait pas le port effectif avec la config, donc `origin_allowed` comparait l'origine à `http://127.0.0.1:0` et un serveur embarqué sur port éphémère **rejetait toutes les requêtes API en 403**. Le trou n'avait jamais été vu parce que rien ne consommait `run_embedded`.
2. **#72** — même schéma, cette fois signalé honnêtement par DevBackend (« 63 passed; 2 failed » sur EPERM) après consigne explicite.
Un repli silencieux sur EPERM transforme un test en décoration : il passe sans rien exercer, et masque la classe de bug qu'il était censé attraper.
## La règle
- **Aucun repli EPERM silencieux.** Un test qui ne peut pas s'exécuter doit **échouer visiblement** ou être `#[ignore]` explicite avec sa raison — jamais « passer ».
- **Tout vert sur `web-server`/`app-tauri` doit être produit hors sandbox** (`dangerouslyDisableSandbox: true` côté Main, qui n'a pas la restriction). Ne jamais accepter un vert sandboxé comme preuve sur ces crates.
- DevBackend et QA doivent **dire ce qu'ils ont pu exécuter et ce qu'ils n'ont pas pu**, plutôt que de rendre un chiffre global.
- Les `#[ignore = "requires local socket bind permission"]` légitimes (ex. `mcp_bridge::tests::end_to_end_over_real_loopback`) se vérifient en les **lançant** hors sandbox avec `--ignored`, pas en raisonnant dessus.
## Principe général
Ne pas confondre « les tests sont verts » et « le code marche ». Le vert d'un environnement contraint ne dit rien du produit. Cette leçon vaut au-delà du bind : un environnement qui ne peut pas exercer un chemin ne peut pas le valider.
Lien : [[appimage-build-no-strip-relr-dyn-fix]] (autre piège d'environnement de cette machine),
[[rendezvous-600s-cap-too-short-heavy-tasks]].

View File

@ -1,6 +1,63 @@
--- ---
issueRef: "#68" issueRef: "#68"
version: 5 version: 7
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
updatedAt: 1784193767292 updatedAt: 1784238387151
--- ---
# Ticket #68 — Serveur embarqué activable depuis le desktop : carnet de chantier
Statut : **livré et mergé dans `develop` (`f965f64`)** le 2026-07-16, en exécution autonome de nuit.
**Reste à faire : la validation live utilisateur.** Rien n'a été cliqué dans l'app réelle — voir la réserve plus bas.
## Livraison en deux temps
**B1 — seam shared-core** (mergé plus tôt, `b7d38f1` + `328c941`). `web_server::run_embedded_with_core(config, Arc<BackendCore>)`. Sans lui, le desktop aurait construit une **seconde composition root dans le même processus** — deux event bus, deux registres live, deux jeux de stores — et le conflit d'écriture app-data-dir serait réapparu à l'intérieur d'un seul process.
**B2/F1/F2 — le reste** (`49e1d5a` backend, `7efa634` frontend, merge `b30c9c7`).
- `crates/app-tauri/src/embedded_server.rs` : controller de cycle de vie sur `AppState`, commandes `embedded_server_start/stop/status` et `get/save/preview_server_exposure_settings`, `FsServerExposureSettingsStore`.
- `frontend/src/features/settings/` : `SettingsView`, `DeploymentSettings`, `useDeployment` ; `adapters/desktopServer.ts` ; port `DesktopServerGateway`.
- `ProjectsView` : `showSettings: boolean` → navigation interne `AI Profiles` / `Deployment`. Le libellé alternant « Close AI Profiles » a disparu.
## Décisions structurantes appliquées
- **Pas de `PanelId: "settings"`.** Architect s'est corrigé en cours de route : Settings existe déjà dans `ProjectsView` comme surface principale, hors modèle `viewPlacement`. C'est de la configuration globale persistée, pas une vue projet dockable/détachable.
- **Persistance dans `<app-data-dir>/deployment/server-exposure.json`**, pas `localStorage`/`uiPreferences` : falsifier cette config peut exposer le serveur, c'est de la config de sécurité. Écriture atomique, `0600` posé **avant** le `rename`.
- **Le pairing code n'est jamais persisté** — secret runtime, affiché seulement si `running`.
- **Le frontend n'invente jamais une IP** : `candidateLanAddresses` et `upstreamUrl` viennent du backend, d'où l'existence de `preview`.
- `EmbedderSettings` / `ModelServersPanel` **non rapatriés** : refonte latérale hors sprint. La liste de sections est le seam où ils s'inséreront.
## ⚠️ Le brief de Main était FAUX — intercepté par DevFrontend
Main décrivait le mode `remoteProxyOtherMachine` avec deux champs (origine publique + IP du proxy). **`validate_settings` en exige un troisième : `lanBindAddress`**, et rejette loopback/unspecified. Construit selon le brief, le lot aurait été **vert et cassé** : chaque `save` et chaque `start` en mode 3 auraient échoué, sans qu'aucun test ne le voie.
Corrigé par un select alimenté par `candidateLanAddresses` — la règle « le frontend n'invente jamais une IP » tient. Leçon consignée dans `7efa634`.
## Vérification réelle (Main puis Git, tous deux hors sandbox)
```
npm run typecheck → propre
npx vitest run → 87 files / 789 passed
cargo test -p app-tauri → 43 passed, 1 ignored
cargo test -p web-server → 66 passed
```
Re-vérifié sur `develop` après merge : 24 suites Rust, 0 échec.
**Invariants vérifiés par exécution, pas par lecture de rapport :** `run_embedded_with_core` est le seul appelant (aucun `run_embedded` nu dans `app-tauri`) ; aucune fuite d'`EmbeddedServer*`/`ServerExposure*` dans `domain` ni `application` ; ordre du store `write(tmp)``chmod 0600``rename` ; `stop()` appelé à la sortie d'app (`lib.rs:153`) ; `features/web` n'importe pas la surface Settings (épinglé par test) et le transport web rend `UNSUPPORTED_ON_WEB`.
**Piège d'environnement :** `app-tauri` et `web-server` ont un sandbox qui bloque `TcpListener::bind` (EPERM). Le test `end_to_end_over_real_loopback` est `#[ignore]` **préexistant** — lancé hors sandbox avec `--ignored`, il passe. Garde d'environnement confirmée par exécution, pas faux vert.
## RÉSERVE — aucune validation live
Tout est vert, **rien n'a été cliqué**. Le panneau n'a jamais été ouvert dans l'app réelle : ni le démarrage/arrêt, ni l'affichage du pairing code, ni les trois modes, ni la persistance entre deux lancements. jsdom ne prouve pas le rendu. Une AppImage a été construite pour que l'utilisateur teste — c'est le point de vérité qui manque.
## Dette identifiée, non traitée (ne bloque rien)
1. **`preview` valide avant de prévisualiser** (`embedded_server.rs:211`) : un brouillon mode 3 ne peut pas être prévisualisé tant qu'il n'a pas le `lanBindAddress` que la liste des candidats doit justement fournir. Contourné côté UI par une sonde `localOnly` toujours valide. Défaut d'ordonnancement backend.
2. **Code mort** : le warning `missingTrustedProxy` (`embedded_server.rs:452`) est inatteignable — `validate_settings` rejette les `trustedProxies` vides en mode 3 avant que `preview_settings` ne puisse le produire.
3. **Aucun événement de statut** : le backend n'émet rien, `onStatusChanged` est du polling **dans l'adapter Tauri**. La forme du port est prête pour un vrai événement, sans toucher l'UI.
4. **Fenêtre umask sur le temporaire** du store : le `0600` arrive après la création. Aucun secret dans ce fichier (modes, ports, IP), risque faible. Un `OpenOptions::mode(0o600)` à la création fermerait le sujet.
5. **Piège latent hors #68** : les stubs `unsupported.ts` préexistants (`WebRemoteGateway`, `WebWindowGateway`) *throwent de façon synchrone* depuis des méthodes qui rendent des Promise — `.then(ok, err)` ne les attraperait pas. Sans effet aujourd'hui (les appelants `await`), mais mérite un nettoyage.
## Contexte : pourquoi #68 est passé avant #71 et #73
Arbitrage **utilisateur**, contre la recommandation d'Architect. Ce dernier proposait #72#73#71#68, ce qui reléguait en dernier la fonctionnalité demandée le matin même. L'utilisateur a tranché : #72 ajusté, puis #68. #71 (diagnostics) et #73 (TLS intégré) suivent.

View File

@ -2,7 +2,7 @@
id: "13821b24-f566-402e-bc10-02ac6e8a5f7c" id: "13821b24-f566-402e-bc10-02ac6e8a5f7c"
number: 68 number: 68
title: "Ajouter dans l'app desktop, la possibilité d'activer le serveur" title: "Ajouter dans l'app desktop, la possibilité d'activer le serveur"
status: "open" status: "qa"
priority: "medium" priority: "medium"
sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6"
links: [{"target":"#65","kind":"dependsOn"}] links: [{"target":"#65","kind":"dependsOn"}]
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
createdAt: 1784193318419 createdAt: 1784193318419
updatedAt: 1784193767292 updatedAt: 1784238387151
version: 5 version: 7
--- ---
J'iamerais que depuis l'app desktop, on puisse quand même embarquer le serveur. Comme ça un utilisateur pourrait continuer son travail en cours en remote J'iamerais que depuis l'app desktop, on puisse quand même embarquer le serveur. Comme ça un utilisateur pourrait continuer son travail en cours en remote

View File

@ -707,13 +707,13 @@
"issueRef": "#68", "issueRef": "#68",
"path": "68", "path": "68",
"title": "Ajouter dans l'app desktop, la possibilité d'activer le serveur", "title": "Ajouter dans l'app desktop, la possibilité d'activer le serveur",
"status": "open", "status": "qa",
"priority": "medium", "priority": "medium",
"sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6",
"assignedAgentIds": [ "assignedAgentIds": [
"a6ced819-b893-4213-b003-9e9dc79b9641" "a6ced819-b893-4213-b003-9e9dc79b9641"
], ],
"updatedAt": 1784193767292 "updatedAt": 1784238387151
}, },
{ {
"issueRef": "#69", "issueRef": "#69",