feat(app-tauri): cycle de vie du serveur embarqué et persistance de l'exposition (#68 B2)

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>
This commit is contained in:
2026-07-16 23:43:24 +02:00
parent 7fbaa8eba6
commit 49e1d5a157
4 changed files with 843 additions and 1 deletions

View File

@ -16,6 +16,7 @@
pub mod chat;
pub mod commands;
pub mod dto;
pub mod embedded_server;
pub mod events;
pub mod mcp_bridge;
pub mod mcp_endpoint;
@ -129,6 +130,7 @@ pub fn run() {
let snapshot = std::sync::Arc::clone(&state.snapshot_running_agents);
let model_servers =
std::sync::Arc::clone(&state.ensure_local_model_server);
let embedded_server = std::sync::Arc::clone(&state.embedded_server);
let open_projects = state.open_project_ids();
let handles = state.terminal_sessions.handles();
tauri::async_runtime::block_on(async move {
@ -148,6 +150,7 @@ pub fn run() {
let _ = pty.kill(&h).await;
}
let _ = model_servers.stop_on_app_exit().await;
let _ = embedded_server.stop().await;
});
}
@ -282,6 +285,12 @@ pub fn run() {
commands::cancel_background_task,
commands::retry_background_task,
commands::list_background_tasks,
commands::get_server_exposure_settings,
commands::save_server_exposure_settings,
commands::preview_server_exposure_settings,
commands::embedded_server_status,
commands::embedded_server_start,
commands::embedded_server_stop,
])
.run(tauri::generate_context!())
.expect("error while running IdeA Tauri application");