diff --git a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json index 8228b26..f8a9bf0 100644 --- a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json +++ b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json @@ -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 diff --git a/.ideai/memory/MEMORY.md b/.ideai/memory/MEMORY.md index b0c7b91..a8f8587 100644 --- a/.ideai/memory/MEMORY.md +++ b/.ideai/memory/MEMORY.md @@ -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 diff --git a/.ideai/memory/sandbox-eperm-bind-false-green-web-server.md b/.ideai/memory/sandbox-eperm-bind-false-green-web-server.md new file mode 100644 index 0000000..a685572 --- /dev/null +++ b/.ideai/memory/sandbox-eperm-bind-false-green-web-server.md @@ -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]]. \ No newline at end of file diff --git a/.ideai/tickets/68/carnet.md b/.ideai/tickets/68/carnet.md index fcaf7a7..9796242 100644 --- a/.ideai/tickets/68/carnet.md +++ b/.ideai/tickets/68/carnet.md @@ -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)`. 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 `/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. diff --git a/.ideai/tickets/68/issue.md b/.ideai/tickets/68/issue.md index 6e9bdc0..821a523 100644 --- a/.ideai/tickets/68/issue.md +++ b/.ideai/tickets/68/issue.md @@ -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 \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index 47189e1..1db32f2 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -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",