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:
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -75,7 +75,11 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"backend",
|
"backend",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"cookie",
|
||||||
"domain",
|
"domain",
|
||||||
|
"http",
|
||||||
|
"http-body-util",
|
||||||
"infrastructure",
|
"infrastructure",
|
||||||
"interprocess",
|
"interprocess",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -26,14 +26,18 @@ infrastructure = { workspace = true }
|
|||||||
backend = { workspace = true }
|
backend = { workspace = true }
|
||||||
tauri = { workspace = true }
|
tauri = { workspace = true }
|
||||||
tauri-plugin-dialog = { workspace = true }
|
tauri-plugin-dialog = { workspace = true }
|
||||||
# `io-std` (on top of the workspace features) gives the headless `mcp-server`
|
# `io-std` gives the headless `mcp-server` bridge access to
|
||||||
# bridge access to `tokio::io::{stdin,stdout}` without widening tokio elsewhere.
|
# `tokio::io::{stdin,stdout}`; `net` is used only by the `--serve` HTTP adapter.
|
||||||
tokio = { workspace = true, features = ["io-std", "rt"] }
|
tokio = { workspace = true, features = ["io-std", "rt", "net"] }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
bytes = "1.11"
|
||||||
|
cookie = "0.18"
|
||||||
|
http = "1.4"
|
||||||
|
http-body-util = "0.1"
|
||||||
# `AppAgentResumer` implements the application's async `AgentResumer` port (LS7).
|
# `AppAgentResumer` implements the application's async `AgentResumer` port (LS7).
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
# Cross-OS local IPC for the MCP loopback transport (M5a): Unix domain socket
|
# Cross-OS local IPC for the MCP loopback transport (M5a): Unix domain socket
|
||||||
|
|||||||
@ -113,6 +113,17 @@ pub async fn create_project(
|
|||||||
pub async fn open_project(
|
pub async fn open_project(
|
||||||
project_id: String,
|
project_id: String,
|
||||||
state: State<'_, AppState>,
|
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> {
|
) -> Result<ProjectDto, ErrorDto> {
|
||||||
let id = parse_project_id(&project_id)?;
|
let id = parse_project_id(&project_id)?;
|
||||||
let output = state
|
let output = state
|
||||||
|
|||||||
@ -21,6 +21,7 @@ pub mod mcp_bridge;
|
|||||||
pub mod mcp_endpoint;
|
pub mod mcp_endpoint;
|
||||||
pub mod openai_tools;
|
pub mod openai_tools;
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
|
pub mod server;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod stream;
|
pub mod stream;
|
||||||
pub mod tickets;
|
pub mod tickets;
|
||||||
@ -42,14 +43,17 @@ use state::AppState;
|
|||||||
/// The `argv[1]` subcommand token that switches the binary into the headless
|
/// The `argv[1]` subcommand token that switches the binary into the headless
|
||||||
/// `mcp-server` **bridge** mode (cadrage v5 §1.3) instead of launching Tauri.
|
/// `mcp-server` **bridge** mode (cadrage v5 §1.3) instead of launching Tauri.
|
||||||
pub const MCP_SERVER_SUBCOMMAND: &str = "mcp-server";
|
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.
|
/// 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
|
/// 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
|
/// `.mcp.json` declaration), we run the **stdio↔loopback bridge** headless and
|
||||||
/// **never** initialise the webview — see [`mcp_bridge::run_mcp_bridge`]. Any other
|
/// **never** initialise the webview — see [`mcp_bridge::run_mcp_bridge`]. When
|
||||||
/// invocation is the normal IDE launch: [`run`] (which blocks until the window
|
/// invoked as `<exe> --serve …`, we run the secure HTTP adapter headless. Any
|
||||||
/// closes and then exits the process itself).
|
/// 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.
|
/// Returns the [`ExitCode`] for the bridge path; the normal path does not return.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@ -61,6 +65,11 @@ pub fn dispatch() -> ExitCode {
|
|||||||
let rest: Vec<String> = args.map(|a| a.to_string_lossy().into_owned()).collect();
|
let rest: Vec<String> = args.map(|a| a.to_string_lossy().into_owned()).collect();
|
||||||
return mcp_bridge::run_mcp_bridge(rest);
|
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();
|
run();
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
|
|||||||
1130
crates/app-tauri/src/server.rs
Normal file
1130
crates/app-tauri/src/server.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user