Écoute l'event Tauri `app-exit-work-guard` (émis par le backend quand la
fermeture de la fenêtre main est interceptée) et affiche une popup modale
"Du travail est encore en cours" avec le résumé pluralisé exact du carnet
#83, le détail agents/tâches capé à 5 lignes, et les deux actions Annuler
(focus par défaut, no-op local) / Quitter quand même (danger, appelle
confirm_app_exit). Pas d'option "ne plus demander".
- domain/ports/adapters (Tauri listen+invoke, HTTP desktop-only stub, mock
avec helpers de test) : onAppExitWorkGuard/confirmAppExit sur
SystemGateway, suivant le patron déjà utilisé pour focused-project et les
domain events.
- AppExitConfirmDialog : mounted une fois près de la racine (App.tsx), à
côté d'AnnouncementsProvider — role="alertdialog", focus trap, Échap =
Annuler, pas de fermeture au clic extérieur, ne se referme jamais
automatiquement (un event pendant l'ouverture rafraîchit juste le résumé).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ajoute le MCP dédié à l'édition de templates : catalogue et classification
des templates (mcp/templates.rs infrastructure + app-tauri), use cases et
provider (application/template), enforcement de la policy des tools (mod.rs,
server.rs, tools.rs), avec la parité côté chemin OpenAI-compatible
(openai_tools.rs x2).
Lots B1 (catalogue/classification), B2 (use cases/provider) et B3
(enforcement policy) livrés en un seul commit cohérent.
QA vert (seul l'échec de bind loopback #80, connu et non-régression, écarté).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ticket #82, lot UX/Frontend : surface de gestion des permissions des tools
MCP par agent (Permissions panel), consommant l'API Tauri livrée en B4. QA
vert (858/858, tsc propre, stable sur shuffle hormis le flake #85, hors
périmètre).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the "Tools MCP IdeA" tab to the project Permissions panel, alongside
the existing "Système" (file/command) tab. Lets the user grant/revoke MCP
tool capabilities per agent or project-wide default, grouped by domain
(Lecture projet, Lecture tickets, Délégation agents, Contexte et mémoire,
Tickets, Travail et exécution, Skills) instead of a flat 25-checkbox list,
per the UX conception in carnet #82.
- domain/ports/adapters (Tauri, HTTP, mock): wire get_mcp_tool_permissions,
update_project_mcp_tool_permissions, update_agent_mcp_tool_permissions
(already merged backend API, #82 lots B1-B4) onto PermissionGateway.
- useMcpToolPermissions: view-model owning the durable MCP tool policy
document, distinct from the file/command permissions in usePermissions.
- McpToolPermissionsPanel: target selector (Défaut projet + agents with
Hérité/Override badges) and grouped editor — inherited agents are
read-only until "Créer un override" (prefilled with the effective
allowlist), per-row Ajouté/Retiré diffing against the project default,
inline confirmation before granting a write tool at project-default
level, and unsaved-draft protection on target change.
- mcpToolGroups.ts: presentational-only domain grouping and short French
labels — the read/write classification itself always comes from the
backend-provided catalogue, never hardcoded here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deux reformatages sans changement de comportement (device.rs, web-server/lib.rs),
laissés de côté hors périmètre au fil de plusieurs tickets précédents.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ticket #82 : catalogue et permissions des tools MCP, backend complet en 4
lots — B1 domaine/store durable, B2 enforcement au serveur MCP stdio, B3
parité enforcement sur le chemin OpenAI-compatible, B4 API Tauri pour la
future UI de gestion. QA vert sur chaque lot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Expose au niveau application/DTO/commandes Tauri le catalogue et les
permissions des tools MCP (application/mcp_tool_permissions.rs, dto.rs,
commands.rs) pour une future UI de gestion.
Lot B4 du ticket #82, dernier lot backend : ferme la boucle sur B1
(domaine/store) + B2 (enforcement MCP stdio) + B3 (parité
OpenAI-compatible).
QA vert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
backend/src/openai_tools.rs et app-tauri/src/openai_tools.rs appliquent
désormais les mêmes règles de permission du domaine mcp_tool_permissions
(B1) et le même enforcement que le serveur MCP natif (B2), pour fermer
l'écart de parité sur le chemin OpenAI-compatible.
Lot B3 du ticket #82 : parité posée sur B1+B2, l'API backend pour la future
UI suit en B4 sur la même branche.
QA vert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
orchestrator/mcp/server.rs applique désormais les règles de permission du
domaine mcp_tool_permissions (lot B1) à l'invocation d'un tool, y compris
le cas requester vide.
Lot B2 du ticket #82 : ferme la boucle enforcement sur le socle B1, la
parité OpenAI-compatible suit en B3 sur la même branche.
QA vert (32 tests dont le nouveau cas requester vide).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduit le domaine mcp_tool_permissions (règles de permission par tool
MCP) et étend le port de store correspondant. Le catalogue read/write des
tools MCP (orchestrator/mcp/tools.rs) s'appuie désormais sur ces règles, et
un store durable (mcp_tool_permission.rs) persiste les permissions au-delà
d'une session.
Lot B1 du ticket #82 : pose le socle domaine/store, l'enforcement suit en
B2 sur la même branche.
QA vert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ticket #62 : identité requester explicite pour les sessions structurées et
policy des tools OpenAI-compatible (ToolPolicyRegistry branché sur
AppOpenAiToolInvoker). QA vert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AgentSessionFactory::start propage désormais l'identité du requester aux
sessions structurées ; OpenTicketAssistant et LaunchAgent la portent
correctement de bout en bout. ToolPolicyRegistry est branché sur
AppOpenAiToolInvoker pour combler le trou de parité : TicketToolProvider
n'appliquait pas la policy des tools sur le chemin OpenAI-compatible,
contrairement au chemin structuré natif.
QA vert (échecs de bind loopback écartés comme non-régression préexistante,
tracés en #80).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ticket #79 : fix du flake PermissionsPanel > saves project defaults
(PermissionEditor draft resync clobbering in-flight edits). QA vert sur le
périmètre du ticket (2/2 stable + suite complète non-shuffled) ; le rouge
observé en shuffle vient d'un autre test préexistant sans rapport
(setCellAgent.test.tsx, tracé séparément en #85).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ticket #78 : alignement des libellés Settings desktop sur le français, QA
vert (850/850 tests, tsc propre).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
UX decision (carnet #78): the human UI of IdeA is French by default,
uniform per surface — proper nouns and technical acronyms (URL, LAN,
IP/CIDR, HTTPS, API, CLI…) stay as-is. Settings mixed English (AI
Profiles, Deployment) with French (Appareils, frozen by UX for #77).
Renames the Settings menu/nav, the Profils IA and Déploiement panels
(titles, actions, states, help text) to French, per the carnet's
exhaustive list. Updates the affected tests accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The draft-resync useEffect fired on every new `draft` object (initial load,
refresh, another save), even when its content matched `local`. If it flushed
after the user started editing, it silently discarded the edit — flaky in
tests where a passive effect can settle after a synchronous fireEvent
sequence, and a real risk in production if a fetch lands mid-edit. Now it
only resyncs when `local` still matches the last-adopted draft.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ticket #61 : refit différé des cellules terminal après split/merge, QA vert
(850/850 tests, tsc propre, test refitSignal dédié).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Le ResizeObserver de TerminalView ne déclenche pas toujours un événement
utile quand une cellule voisine apparaît/disparaît (split/merge), forçant
l'utilisateur à redimensionner la fenêtre pour rafraîchir le scaling xterm.
useLayout expose un layoutVersion bumpé à chaque commit d'arbre (chargement
initial inclus), relayé par LayoutGrid comme refitSignal à chaque
TerminalView survivant pour déclencher fit.fit() sans rouvrir le PTY.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Intègre le ticket #77 : l'appairage repose désormais sur des appareils
persistants, nommés et révocables, derrière un code éphémère à usage
unique (TTL 10 min) au lieu d'un code permanent imprimé sur stdout.
Ferme aussi la dette #76 : la normalisation du code passe côté serveur,
là où #75 ne pouvait que la masquer depuis le frontend.
Suites rejouées avant merge, toutes vertes :
- cargo test -p domain -p application -p infrastructure -p web-server :
exit 0, 89 suites, 1686 tests.
- frontend : 91 fichiers, 848 tests, tsc exit 0.
- garde-fou de bundle : dist = tauri, dist-web = http.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enregistre l'état du registre de tickets : #77 entre en QA, #78 ouvre la
dette UX de langue de l'écran Settings et #79 le test flaky relevé en
cours de route.
État runtime uniquement : aucun code de feature n'est touché.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose la gestion des appareils appairés introduite en B1-B4, sur une
surface unique partagée par le web et le desktop.
- Écran Appareils : liste, renommage, révocation unitaire ou globale,
activité formatée, panneau de code éphémère.
- Le parcours d'appairage demande un nom d'appareil, pour qu'une
révocation porte sur quelque chose d'identifiable par l'utilisateur.
- Gateways DeviceGateway en trois adapters (Tauri, HTTP, Mock), le port
restant le seul contrat connu de la feature.
Les erreurs sont mappées localement et le message du serveur n'est jamais
affiché tel quel : un échec d'appairage ne doit pas devenir un oracle pour
qui teste des codes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L'appairage ne survivait pas au redémarrage et son code, permanent, était
imprimé sur la sortie standard. Un appareil appairé devient une entité
persistante, nommée et révocable, derrière un code désormais éphémère.
- B1 : port DeviceSessionStore et adapter FsDeviceSessionStore, entités de
domaine (PairedDevice, DeviceId, SessionTokenHash, DeviceName). Les tokens
sont hachés en SHA-256 et comparés en temps constant (subtle) : le store
ne peut pas rejouer une session qu'il a servie. Cookie Max-Age 400 j à
renouvellement glissant, lastSeenAtMs throttlé.
- B2 : code éphémère en mémoire, TTL 10 min et usage unique, toute
génération invalidant la précédente. POST /api/pairing-code authentifiée,
flag --new-code. Le code est retiré du boot et l'eprintln! qui l'imprimait
est supprimé.
- B3 : endpoints devices (list/rename/revoke/revoke-all/logout), event
DeviceRevoked et ActiveConnectionRegistry par device_id, fermant sans
délai les WebSockets d'un appareil révoqué.
- B4 : port PairAttemptLimiter et adapter mémoire, rate-limit par origine et
global sur horloge injectée, donc testable sans attente réelle.
La normalisation du code passe côté serveur : elle absorbe la dette #76, que
la seule normalisation frontend de #75 ne faisait que masquer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Intègre le ticket #75 : l'écran d'appairage web appelait le pavé
numérique des mobiles alors que le code d'appairage est de
l'hexadécimal majuscule, rendant les lettres A-F non saisissables.
Le champ passe en clavier texte, annonce le format réel et normalise
la saisie avant envoi. Le correctif revient sur le choix de #69, qui
avait supposé un code purement numérique.
Suite verte rejouée avant merge : 88 fichiers, 801 tests, tsc exit 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enregistre l'état du registre de tickets : #74 et #68 passent en closed
après la validation live de l'intégration web, #75 (ce fix) entre en QA
et #76 ouvre la dette de casse du code d'appairage côté serveur.
État runtime uniquement : aucun code de feature n'est touché.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le code d'appairage est de l'hexadécimal majuscule (p. ex. 3F7A9C21),
pas une suite de chiffres. Le champ appelait pourtant le pavé numérique
des mobiles, rendant les lettres A-F non saisissables.
Le champ passe donc en inputMode="text" avec autoCapitalize="characters",
ce qui revient sur le choix inverse fait en #69 : ce ticket avait supposé
un code purement numérique. Le placeholder et l'aide du champ décrivent
désormais le format réel.
La saisie est normalisée (majuscules, espaces retirés) avant pair() car
le serveur compare le code strictement ; la dette correspondante côté
backend est suivie en #76.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Intègre les tickets #68 (packaging des assets web en ressource Tauri) et
#74 (le serveur embarqué servait le bundle desktop au navigateur).
Le frontend produit désormais deux artefacts Vite distincts : dist
(transport tauri, build.frontendDist) et dist-web (transport http,
packagé en ressource « web/ »). Le garde-fou de build échoue si un
bundle résout le mauvais transport.
Les deux tickets ferment ensemble : le packaging #68 servait le bundle
cassé, il ne pouvait pas être déclaré vert sans le fix#74. Validation
live utilisateur verte derrière reverse proxy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enregistre le passage de #74 en statut QA (issue, carnet, index) et
l'accusé de complétion des rendez-vous headless du lot.
État runtime uniquement : aucun code de feature n'est touché.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vérifie sur les artefacts réels que `dist` résout `tauri` et `dist-web`
`http`, en lisant le marqueur `__IDEA_TRANSPORT__` des assets JS émis. La
régression de #74 était invisible aux tests unitaires : elle vivait dans la
conf de build et de packaging, pas dans le code testé.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`bundle.resources` packageait `frontend/dist` — le bundle desktop — sous
`web/`, que `resolve_web_root()` sert au navigateur. La ressource pointe
désormais sur `frontend/dist-web` (transport http), et `beforeBuildCommand`
appelle `build:bundle` pour que les deux bundles existent au packaging.
`frontendDist` reste sur `dist` : le desktop est inchangé.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Le transport est figé au build par Vite : un seul `dist` ne peut pas servir
à la fois le desktop (IPC Tauri) et le navigateur (HTTP+WS). Le serveur
embarqué servait donc un bundle desktop, d'où `__TAURI_INTERNALS__ is
undefined` côté navigateur.
- `vite.config.ts` devient une factory `({ mode })` : le mode par défaut émet
le bundle desktop (`dist`), `--mode web` lit `.env.web` et émet le bundle
navigateur (`dist-web`).
- `.env.web` porte `VITE_TRANSPORT=http` dans un fichier de mode plutôt qu'en
préfixe de commande, syntaxe qui n'existe pas sous Windows (bundle NSIS).
- `transport.ts` extrait le prédicat `transportFromEnv()`, partagé par l'app et
la config de build : le constant `__IDEA_TRANSPORT__` et le transport résolu
dérivent de la même variable via le même prédicat, ils ne peuvent pas diverger.
- `main.tsx` publie `__IDEA_TRANSPORT__` sur `window` : l'affectation est un
effet de bord, elle survit à la minification et rend un bundle identifiable
sans grep d'un symbole minifié. Les deux jeux d'adapters étant présents dans
les deux bundles, leur présence ne prouve rien.
Le chemin desktop est inchangé : le web reste opt-in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L'AppImage ne pouvait pas servir d'assets web : `resolve_web_root` ne
connaissait que `IDEA_WEB_ROOT`, le dossier de l'exécutable et le cwd —
aucun ne pointe vers un bundle packagé.
- `bundle.resources` embarque les assets sous `web/`, `beforeBuildCommand`
déclenche le build du frontend.
- Le resource dir Tauri descend de `lib.rs` jusqu'à `EmbeddedServerController`
via `AppState::build_with_resource_dir`, en gardant les constructeurs
historiques comme façade.
- `resolve_web_root` insère le candidat packagé juste après `IDEA_WEB_ROOT`,
qui garde donc la priorité pour le développement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
#72 passé en `closed` : mergé dans `develop` via `ae01297`, vert hors sandbox,
branche supprimée. Carnet complété avec le périmètre livré et les arbitrages.
État `.ideai/` indépendant du code de #68, committé directement sur `develop`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ferme #68 : le serveur web s'active désormais depuis l'app desktop, via un
panneau Settings > Deployment. C'est la fonctionnalité demandée à l'origine du
ticket, et l'aboutissement de la chaîne #65 (extraction d'idea-serve) → #68 B1
(seam shared-core) → #72 (durcissement proxy) → ce lot.
Backend : `EmbeddedServerController` sur `AppState`, commandes start/stop/status,
store `<app-data-dir>/deployment/server-exposure.json` (config de sécurité, pas
préférence d'UI), arrêt câblé sur la sortie d'app. Le serveur embarqué consomme
`run_embedded_with_core` avec le `BackendCore` du desktop — une seule
composition root, ce pour quoi le seam de B1 existait.
Frontend : `features/settings/` avec `SettingsView`/`DeploymentSettings`,
gateway `DesktopServerGateway`, et `ProjectsView` dont le booléen `showSettings`
devient une vraie navigation `AI Profiles` / `Deployment`.
Vert avant merge, ré-exécuté par Git HORS SANDBOX — impératif sur ces crates,
le sandbox bloque `bind` et a déjà produit un faux vert sur #68 B1 :
`cargo test -p app-tauri` 43 passed / 1 ignored · `-p web-server` 66 passed ·
`npm run typecheck` exit 0 · `npx vitest run` 87 files / 789 passed. Le test
ignoré est une garde d'environnement préexistante, vérifiée passante hors
sandbox avec `--ignored`.
Invariants vérifiés par Git avant merge : `run_embedded_with_core` bien
l'appelant, aucune fuite des types d'exposition dans `domain`/`application`,
`0600` posé avant le `rename`, `stop()` appelé à la fermeture.
Dette consignée, non bloquante : `preview_settings` valide avant de
prévisualiser (verrouille la liste des candidats LAN, contourné côté frontend),
et le warning `missingTrustedProxy` est inatteignable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Donne à l'utilisateur la surface pour activer le serveur depuis l'app.
- `features/settings/` : `SettingsView`, `DeploymentSettings`, `useDeployment`.
- `adapters/desktopServer.ts` + port `DesktopServerGateway` et DTO associés ;
adapters mock et http/unsupported alignés (le mode web n'expose pas le
contrôle du serveur qui l'héberge).
- `ProjectsView` : le `showSettings: boolean` devient une navigation interne
`AI Profiles` / `Deployment`. Le libellé alternant « Close AI Profiles »
disparaît — Settings existait déjà dans cette vue, la surface évolue au lieu
d'ajouter un `PanelId`.
CORRECTION D'UN BRIEF FAUX, remontée spontanément par DevFrontend et qui mérite
de survivre : le cadrage décrivait le mode `remoteProxyOtherMachine` avec deux
champs. `validate_settings` en exige un troisième, `lanBindAddress`, et rejette
loopback comme unspecified. Construit selon la spec, chaque save et chaque start
en mode 3 aurait échoué — le lot serait parti vert et cassé. Le panneau expose
donc un select alimenté par `candidateLanAddresses` fourni par le backend :
la règle « le frontend n'invente jamais une IP » tient.
QA ré-exécutée par Git avant merge : `npm run typecheck` exit 0 ·
`npx vitest run` 87 files / 789 passed, 0 échec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Permet d'activer le serveur web depuis l'app desktop — la fonctionnalité
demandée à l'origine du ticket.
- `embedded_server.rs` : `EmbeddedServerController` porté par `AppState`, avec
les commandes `embedded_server_start` / `stop` / `status` et
`get` / `save` / `preview_server_exposure_settings`.
- `FsServerExposureSettingsStore` : écrit `<app-data-dir>/deployment/server-exposure.json`.
C'est de la config de sécurité, pas une préférence d'UI — d'où l'app-data-dir
global plutôt que `localStorage`. Écriture atomique : temporaire, `0600` posé
AVANT le `rename`, donc le fichier final n'est jamais brièvement lisible par
tous.
- Arrêt du serveur câblé sur la sortie de l'app (`lib.rs`), aux côtés des autres
nettoyages.
Le serveur embarqué appelle `run_embedded_with_core` avec le `BackendCore`
partagé du desktop, jamais `run_embedded` — c'est tout l'intérêt du seam ouvert
par B1 : une seule composition root dans le processus, pas deux.
DETTE CONNUE, consignée au carnet, non bloquante :
- `preview_settings` valide avant de prévisualiser (`embedded_server.rs:211`),
ce qui verrouille la liste des candidats : un brouillon en mode
`remoteProxyOtherMachine` ne peut pas être prévisualisé tant qu'il n'a pas le
`lanBindAddress` que cette liste doit justement fournir. Contourné côté
frontend par une sonde `localOnly` toujours valide. Défaut d'ordonnancement,
pas une fatalité.
- Le warning `missingTrustedProxy` (`embedded_server.rs:452`) est du code mort :
`validate_settings` rejette les `trustedProxies` vides en mode 3 avant que
`preview_settings` ne puisse le produire.
QA ré-exécutée par Git HORS SANDBOX (DevBackend et QA sont bloqués par EPERM sur
`bind` ; un vert sandboxé ne prouve rien sur ces crates) : `cargo test -p
app-tauri` 43 passed, 1 ignored · `-p web-server` 66 passed, 0 échec.
Le test ignoré `mcp_bridge::tests::end_to_end_over_real_loopback` est
PRÉEXISTANT sur `develop` et non touché par ce lot ; lancé explicitement hors
sandbox avec `--ignored`, il passe. C'est une garde d'environnement, pas un faux
vert — distinction qui nous a déjà coûté deux fois aujourd'hui.
Invariants vérifiés par Git : appelant `run_embedded_with_core` confirmé, aucune
fuite d'`EmbeddedServer*`/`ServerExposure*` dans `domain` ni `application`,
ordre chmod/rename correct, `stop()` bien appelé à la fermeture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#73 ouvert en priorité haute : intégrer TLS à `idea-serve` pour supprimer la
cause racine de la cérémonie reverse proxy, dont #72 vient de durcir les
garde-fous. Lié à #72, #66, #68 et #71.
État `.ideai/` indépendant du code de #72, committé directement sur `develop`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rend réelle la confiance accordée au reverse proxy. Jusqu'ici
`--trust-reverse-proxy` n'était lu que pour exiger sa propre présence : le
serveur ne vérifiait ni qui se connectait, ni ce que le proxy annonçait. Le pair
TCP est désormais vérifié avant toute confiance aux headers, `X-Forwarded-Proto:
https` est exigé en mode proxy, et `X-Forwarded-For` n'autorise jamais rien.
Corrige aussi B0, le pendant CLI du bug de port de #68 B1 : `idea-serve --listen
127.0.0.1:0` ne compare plus l'origine à `http://127.0.0.1:0`.
CHANGEMENT DE COMPORTEMENT ASSUMÉ : un bind non-loopback distant sans
`--trusted-proxy` est refusé au démarrage. Les configurations existantes de cette
forme cassent volontairement, avec un message qui dit quoi ajouter.
Vert avant merge, ré-exécuté par Git HORS SANDBOX — impératif sur ce crate, le
sandbox interdit `TcpListener::bind` et a déjà produit un faux vert sur #68 B1 :
`cargo test -p web-server` 66 passed · `-p backend` · `-p app-tauri` verts.
Revue de sécurité : guard câblé sur les 3 surfaces (statique, /api/*, /api/ws),
vrai `peer_addr` propagé sur tout le chemin de production.
Étape 2 du plan d'intégration séquentiel. Suit : #68 (panneau Settings
Deployment), sur arbitrage utilisateur qui a refusé de le reléguer derrière #71
et #73.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Aligne la doc du mode distant sur le durcissement de #72 : exemple complet du
cas proxy sur une autre machine avec `--trusted-proxy`, mention que tout bind
non-loopback exige au moins un proxy autorisé, et obligation faite au proxy
d'envoyer `X-Forwarded-Proto: https`.
Précise que transmettre le host public est recommandé pour le diagnostic mais
n'est pas la vérification d'accès : celle-ci reste l'égalité stricte d'`Origin`
contre `--public-origin`.
Bascule les IP d'exemple vers la plage de documentation RFC 5737 (`192.0.2.0/24`)
au lieu d'un réseau privé plausible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`--trust-reverse-proxy` était un drapeau creux : il n'était lu que dans
`ServerConfig::validate()` pour exiger sa propre présence. Le serveur n'a jamais
vérifié qui se connectait ni ce que le proxy annonçait. Ce lot lui donne un
effet.
- `trusted_proxies: Vec<TrustedProxy>` dans `ServerConfig` + `--trusted-proxy`
(répétable, IP ou CIDR v4/v6). `validate()` refuse désormais au démarrage un
bind non-loopback en mode remote sans au moins un proxy autorisé.
- Guard runtime sur les trois surfaces (statique, `/api/*`, `/api/ws`) : le
`peer_addr` réel est vérifié AVANT toute confiance accordée aux headers. Sur
bind loopback, seul un pair loopback est accepté ; sinon le pair doit tomber
dans un `--trusted-proxy`.
- `X-Forwarded-Proto: https` obligatoire en mode proxy, avec un message de refus
actionnable (la commande nginx exacte à ajouter).
- `X-Forwarded-For` n'autorise jamais rien : il n'est que journalisé. Un header
ne donne aucun droit, seul le pair TCP en donne.
- Diagnostics : `UntrustedProxyPeer`, `ForwardedProtoRejected`,
`ForwardedHostMismatch`.
B0 — même bug de port que celui corrigé dans #68 B1, sur le chemin CLI cette
fois : `run_server` construisait son `ServerState` AVANT le bind, donc
`idea-serve --listen 127.0.0.1:0` comparait l'origine à `http://127.0.0.1:0` et
rejetait tout en 403. La réconciliation du port effectif est factorisée dans
`config_with_effective_listen`, partagée avec le chemin embarqué.
`X-Forwarded-Host` ne provoque PAS de rejet : Architect l'a jugé redondant avec
la vérification stricte d'`Origin`, et il cassait nginx en configuration par
défaut. Il reste un warning de diagnostic. La vérification d'accès stricte
demeure l'égalité d'`Origin` contre `--public-origin`.
CHANGEMENT DE COMPORTEMENT ASSUMÉ — ce lot casse volontairement les
configurations existantes : un bind non-loopback distant sans `--trusted-proxy`
est désormais refusé au démarrage. C'est le prix d'un drapeau qui ne mentait
plus. Le message d'erreur dit quoi ajouter.
Origine du ticket : #72 est né d'une trouvaille de l'agent Git à la revue de la
doc de #65 — c'est en vérifiant une phrase de sécurité qu'il a établi que
`--trust-reverse-proxy` n'avait aucun effet runtime.
QA ré-exécutée par Git HORS SANDBOX avant merge (DevBackend et QA sont tous deux
bloqués par EPERM sur `TcpListener::bind` ; un vert sandboxé ne vaut rien ici, on
s'est déjà fait avoir sur #68 B1) : `cargo test -p web-server` 66 passed, 0
échec · `-p backend` · `-p app-tauri` verts.
Revue de sécurité par Git : guard câblé sur les 3 routes, chemin de production
propageant toujours le vrai `peer_addr` (le repli `listen.ip()` est
`#[cfg(test)]`, inatteignable en production), CIDR correct y compris `prefix 0`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Corrige une documentation qui a réellement induit l'utilisateur en erreur :
l'exemple du cas « Remote HTTPS » bindait `127.0.0.1` en supposant sans le dire
que le reverse proxy est co-localisé. Suivi tel quel avec un proxy sur une autre
machine, il produit un serveur injoignable.
Précise le cas co-localisé vs le cas distant, la configuration exigée pour tout
bind non-loopback, l'égalité stricte de `--public-origin` contre l'`Origin`
(ouvrir l'UI par le domaine, pas par l'IP, sinon 403), et le pare-feu hôte comme
cause de timeout depuis les autres machines.
Conserve l'obligation normative « ne pas exposer sans proxy TLS » que la
première rédaction avait perdue au profit d'une garantie que le code n'offre
pas : `ServerConfig::validate()` valide la configuration, pas la présence réelle
d'un proxy.
Indépendant de #68 et #71 : n'a attendu aucun de leurs lots.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
La réécriture précédente avait remplacé « Do not expose the backend on a public
non-loopback address without TLS proxying » par une formulation adossée à
`ServerConfig::validate()`. Littéralement vraie, mais pas équivalente : elle
troque une OBLIGATION faite à l'opérateur contre une garantie automatique qui
n'existe pas.
`validate()` valide une configuration, pas la réalité. Rien ne vérifie qu'un
proxy est réellement devant le serveur : passer les trois drapeaux en bindant
`0.0.0.0` sans aucun proxy satisfait `validate()` et sert du HTTP en clair.
La phrase normative est donc rétablie, avec cette limite dite explicitement.
Voir #72 : `--trust-reverse-proxy` n'a par ailleurs aucun effet à l'exécution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- #72 ouvert en priorité haute, bloquant #68 : `--trust-reverse-proxy` n'est lu
que dans `ServerConfig::validate()` pour exiger sa propre présence — le serveur
ne parse jamais `X-Forwarded-*`. C'est un drapeau d'intention pur, qui donne un
faux sentiment de protection. Le ticket porte aussi B0 (le bug de port effectif
sur le chemin CLI `run_server`), `trusted_proxies` + `--trusted-proxy`, le
guard runtime et les événements de diagnostic.
- Journal des tâches de fond : complétions enregistrées.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Déclare UX comme propriétaire de la conception des surfaces (navigation,
libellés, hiérarchie de l'information, formulation des erreurs, parcours) et
l'insère dans le cycle : avant Architect quand la forme conditionne les contrats,
avec lui quand une contrainte technique borne la conception. UX ne bloque pas les
lots sans surface utilisateur.
Ajoute aussi l'avertissement que la liste des rôles de ce document n'est pas la
source de vérité des agents déclarés : `idea_list_agents` fait autorité.
Contexte projet, indépendant des chantiers en cours — committé directement sur
`develop`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Livre la fondation backend du serveur embarqué desktop : le seam
`run_embedded_with_core(config, Arc<BackendCore>)`, qui permet à l'app Tauri de
faire tourner le serveur web en partageant son composition root plutôt qu'en
dupliquant un backend dans le même processus.
Apporte aussi le fix du port effectif : un serveur embarqué sur port éphémère
rejetait toutes les requêtes API en 403 (origine comparée à un `listen` resté à
port 0). Et comble le trou de couverture sur `run_embedded().stop()` signalé au
merge de #65.
Vert avant merge, ré-exécuté par Git HORS SANDBOX — impératif ici : le sandbox
interdit `TcpListener::bind` et avait produit un faux vert sur ce lot précis.
`cargo test -p web-server` 57 passed (les 2 tests embedded réellement exécutés) ·
`-p backend` 41 · `-p app-tauri` 35, 0 échec.
Étape 1 du plan d'intégration séquentiel. Suit : #72 (durcissement bind/origine/
proxy, dont B0 = le même bug de port sur le chemin CLI `run_server`), puis #71
lot 1 (diagnostics), puis le Settings Deployment de #68.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ouvre le point d'intégration du serveur embarqué desktop : `run_embedded_with_core`
reçoit un `Arc<BackendCore>` déjà construit, pour que l'adapter HTTP partage le
composition root de l'adapter Tauri au lieu d'en bâtir un second dans le même
processus. `ServerState` porte désormais `Arc<BackendCore>` ; `run_embedded` est
conservé en compat et délègue en construisant son propre core.
Corrige au passage un vrai bug produit, découvert en rejouant les tests hors
sandbox : `run_embedded` ne réconciliait pas le port effectif avec la config. Sur
un bind éphémère (`127.0.0.1:0`), l'état du serveur gardait `listen` à port 0,
donc `origin_allowed` comparait l'Origin entrante à `http://127.0.0.1:0` et le
serveur embarqué rejetait **toutes** les requêtes API en 403. La config reçoit
maintenant `local_addr` lu sur le listener **avant** la construction du state.
Comble le trou signalé au merge de #65 : `run_embedded().stop()` est enfin
couvert.
ATTENTION — le premier « 57 passed » de ce lot était un FAUX VERT. Le sandbox
d'exécution interdit `TcpListener::bind` ; les tests sortaient en silence sur
EPERM, dont précisément le test `stop()`. Les replis EPERM sont supprimés : un
bind refusé fait désormais échouer le test au lieu de le peindre en vert.
QA ré-exécutée par Git HORS SANDBOX avant merge (sinon on reproduit le faux
vert) : `cargo test -p web-server` 57 passed, 0 échec, avec
`run_embedded_stop_shuts_down_accept_loop` et
`run_embedded_with_core_uses_injected_core_for_http_invokes` réellement exécutés
sur le vrai chemin réseau. `-p backend` 41 · `-p app-tauri` 35, 0 échec.
DETTE CONNUE, tracée en #72 B0 : le chemin CLI standalone `run_server` garde la
même hypothèse fausse — il reçoit un `ServerState` construit AVANT le bind, donc
`idea-serve --listen 127.0.0.1:0` reste cassé à l'identique.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
L'exemple du cas « Remote HTTPS » bindait `127.0.0.1` en supposant sans le dire
que le reverse proxy tourne sur la même machine que le serveur. Suivi tel quel
avec un proxy sur une autre machine, il produit un serveur injoignable — ce qui
a réellement induit l'utilisateur en erreur aujourd'hui.
Précise donc : le cas co-localisé vs le cas proxy distant (binder une adresse
joignable par le proxy, lien proxy→serveur en HTTP clair sur le LAN), la
configuration exigée pour tout bind non-loopback, l'égalité stricte de
`--public-origin` contre l'`Origin` de la requête (ouvrir l'UI par le domaine et
non par l'IP, sinon 403), et le pare-feu hôte comme cause de timeout depuis les
autres machines.
Indépendant de #68 et #71 : valeur immédiate, n'attend aucun de leurs lots.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>