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",
"conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4"
},
"state": "running",
"state": "completed",
"wakePolicy": "recordOnly",
"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,
"result": null,
"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
- [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
- [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"
version: 5
version: 7
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"
number: 68
title: "Ajouter dans l'app desktop, la possibilité d'activer le serveur"
status: "open"
status: "qa"
priority: "medium"
sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6"
links: [{"target":"#65","kind":"dependsOn"}]
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}
createdBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
createdAt: 1784193318419
updatedAt: 1784193767292
version: 5
updatedAt: 1784238387151
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

View File

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