feat(server): sous-commande idea --serve avec serveur HTTP sécurisé pairing+cookie (#13)

Lot B3 du chantier server/client mode : socle serveur sécurisé exposant le
cœur backend en mode client/serveur, sans PTY (reporté B5).

- crates/app-tauri/src/server.rs : sous-commande --serve, serveur HTTP
  sécurisé — /api/invoke sur allowlist, /api/pair → cookie de session,
  Origin strict, refus du secret en query, gate de bind. 16 tests dont les
  2 refus de sécurité (pairing_rejects_wrong_code,
  invoke_rejects_invalid_session_cookie).
- crates/app-tauri/src/lib.rs : câblage de la sous-commande --serve.
- crates/app-tauri/src/commands.rs : adaptations.
- crates/app-tauri/Cargo.toml : deps bytes/cookie/http/http-body-util,
  feature tokio net. Cargo.lock synchronisé.

Validé : cargo check --workspace vert, app-tauri 267 tests verts, cœur
backend agnostique confirmé, desktop non régressé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 13:06:40 +02:00
parent e0cdb4aa56
commit 4ed0b168d2
5 changed files with 1164 additions and 6 deletions

View File

@ -113,6 +113,17 @@ pub async fn create_project(
pub async fn open_project(
project_id: String,
state: State<'_, AppState>,
) -> Result<ProjectDto, ErrorDto> {
open_project_for_adapter(project_id, &state).await
}
/// Shared `open_project` adapter body used by desktop IPC and the HTTP server.
///
/// Keeps the non-presentation side effects attached to project opening in one
/// place while B3 adds a second driving adapter.
pub(crate) async fn open_project_for_adapter(
project_id: String,
state: &AppState,
) -> Result<ProjectDto, ErrorDto> {
let id = parse_project_id(&project_id)?;
let output = state

View File

@ -21,6 +21,7 @@ pub mod mcp_bridge;
pub mod mcp_endpoint;
pub mod openai_tools;
pub mod pty;
pub mod server;
pub mod state;
pub mod stream;
pub mod tickets;
@ -42,14 +43,17 @@ use state::AppState;
/// The `argv[1]` subcommand token that switches the binary into the headless
/// `mcp-server` **bridge** mode (cadrage v5 §1.3) instead of launching Tauri.
pub const MCP_SERVER_SUBCOMMAND: &str = "mcp-server";
/// The `argv[1]` subcommand token that starts the secure HTTP server adapter.
pub const SERVE_SUBCOMMAND: &str = "--serve";
/// Process entry point: routes `argv` **before** anything Tauri/WebKit is touched.
///
/// When invoked as `<exe> mcp-server …` (an MCP CLI spawned us from the injected
/// `.mcp.json` declaration), we run the **stdio↔loopback bridge** headless and
/// **never** initialise the webview — see [`mcp_bridge::run_mcp_bridge`]. Any other
/// invocation is the normal IDE launch: [`run`] (which blocks until the window
/// closes and then exits the process itself).
/// **never** initialise the webview — see [`mcp_bridge::run_mcp_bridge`]. When
/// invoked as `<exe> --serve …`, we run the secure HTTP adapter headless. Any
/// other invocation is the normal IDE launch: [`run`] (which blocks until the
/// window closes and then exits the process itself).
///
/// Returns the [`ExitCode`] for the bridge path; the normal path does not return.
#[must_use]
@ -61,6 +65,11 @@ pub fn dispatch() -> ExitCode {
let rest: Vec<String> = args.map(|a| a.to_string_lossy().into_owned()).collect();
return mcp_bridge::run_mcp_bridge(rest);
}
let mut args = std::env::args_os().skip(1);
if args.next().is_some_and(|a| a == *SERVE_SUBCOMMAND) {
let rest: Vec<String> = args.map(|a| a.to_string_lossy().into_owned()).collect();
return server::run_from_args(rest);
}
run();
ExitCode::SUCCESS

File diff suppressed because it is too large Load Diff