|
|
|
|
@ -512,6 +512,11 @@ impl ChangeAgentProfile {
|
|
|
|
|
// Conversation id discarded: the previous one belonged to the old
|
|
|
|
|
// engine; the new profile starts (or assigns) a fresh one.
|
|
|
|
|
conversation_id: None,
|
|
|
|
|
// Internal relaunch (no app-tauri composition root in scope) ⇒ the
|
|
|
|
|
// MCP runtime is not injected here; `apply_mcp_config` falls back to
|
|
|
|
|
// the minimal declaration. A profile-hot-swap that needs the real
|
|
|
|
|
// endpoint is re-driven through the app-tauri launch path.
|
|
|
|
|
mcp_runtime: None,
|
|
|
|
|
})
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(Some(output.session))
|
|
|
|
|
@ -637,6 +642,50 @@ pub struct LaunchAgentInput {
|
|
|
|
|
/// when the profile supports it). The caller (which owns the layout) reads this
|
|
|
|
|
/// from the leaf's [`domain::layout::LeafCell::conversation_id`].
|
|
|
|
|
pub conversation_id: Option<String>,
|
|
|
|
|
/// Runtime facts needed to write the **real** IdeA MCP server declaration
|
|
|
|
|
/// (M5d). These are **OS/runtime data** (the IdeA executable path, the
|
|
|
|
|
/// project's loopback endpoint) that live in `app-tauri` — they are *injected
|
|
|
|
|
/// as data* from the composition root, never computed in `application` (which
|
|
|
|
|
/// must not depend on `app-tauri`, cadrage v5 §0.3 / §7).
|
|
|
|
|
///
|
|
|
|
|
/// `None` ⇒ no runtime injected (launches issued from inside `application`:
|
|
|
|
|
/// the orchestrator's `spawn_agent`/`ask_agent`, a profile hot-swap relaunch,
|
|
|
|
|
/// or tests). In that case [`apply_mcp_config`](LaunchAgent::apply_mcp_config)
|
|
|
|
|
/// still honours the profile's `McpConfigStrategy`, but falls back to a
|
|
|
|
|
/// **coherent minimal** declaration (the `idea mcp-server` command without the
|
|
|
|
|
/// endpoint/project/requester args) rather than a project-bound one — see
|
|
|
|
|
/// [`mcp_server_declaration`].
|
|
|
|
|
pub mcp_runtime: Option<McpRuntime>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// OS/runtime facts injected by the composition root (`app-tauri`) to materialise
|
|
|
|
|
/// the **real** IdeA MCP server declaration in an agent's `.mcp.json` (cadrage v5
|
|
|
|
|
/// §2). Carrying these as **plain data** on [`LaunchAgentInput`] keeps
|
|
|
|
|
/// `application` free of any `app-tauri` / `current_exe` / `mcp_endpoint`
|
|
|
|
|
/// dependency: the endpoint stays computed by the *single source of truth*
|
|
|
|
|
/// (`app-tauri::mcp_endpoint`) and only its **string** crosses the layer boundary.
|
|
|
|
|
///
|
|
|
|
|
/// All four fields are the exact strings the spawned bridge expects on its command
|
|
|
|
|
/// line (`<exe> mcp-server --endpoint <endpoint> --project <project_id> --requester
|
|
|
|
|
/// <requester>`); the `project_id` must already be in the **hyphen-free 32-hex
|
|
|
|
|
/// `simple` form** consumed by the M5c handshake guard (`serve_peer`).
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub struct McpRuntime {
|
|
|
|
|
/// Absolute path to the IdeA executable (`std::env::current_exe()`), used as the
|
|
|
|
|
/// declaration's `command` — the CLI spawns *this* binary in its `mcp-server`
|
|
|
|
|
/// bridge mode.
|
|
|
|
|
pub exe: String,
|
|
|
|
|
/// The project's loopback endpoint string (`mcp_endpoint(project).as_cli_arg()`),
|
|
|
|
|
/// passed as `--endpoint`. **Same source of truth** as the listener bound by
|
|
|
|
|
/// `ensure_mcp_server` (cadrage v5 §2 coherence invariant).
|
|
|
|
|
pub endpoint: String,
|
|
|
|
|
/// The project id in the **hyphen-free 32-hex `simple` form** consumed by the
|
|
|
|
|
/// M5c handshake guard, passed as `--project`.
|
|
|
|
|
pub project_id: String,
|
|
|
|
|
/// The launching agent's id, passed as `--requester` so the server tags
|
|
|
|
|
/// `OrchestratorRequestProcessed.requester_id` with the real agent (cadrage v5
|
|
|
|
|
/// §1.4) instead of the frozen `"mcp"` placeholder.
|
|
|
|
|
pub requester: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Descripteur d'une session **structurée** (IA, cellule chat) démarrée par
|
|
|
|
|
@ -1042,7 +1091,8 @@ impl LaunchAgent {
|
|
|
|
|
// IdeA matérialise SA config MCP au format de CETTE CLI, dans le **même**
|
|
|
|
|
// run dir isolé que le convention file et le seed de permissions. `None`
|
|
|
|
|
// ⇒ aucun write/flag/env (chemin actuel inchangé, zéro régression).
|
|
|
|
|
self.apply_mcp_config(&profile, &run_dir, &mut spec).await;
|
|
|
|
|
self.apply_mcp_config(&profile, &run_dir, input.mcp_runtime.as_ref(), &mut spec)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
// 5b. ── POINT DE ROUTAGE §17.4 : IA structuré vs terminal brut ──
|
|
|
|
|
// Le convention file (CLAUDE.md / AGENTS.md) vient d'être écrit dans le
|
|
|
|
|
@ -1331,14 +1381,18 @@ impl LaunchAgent {
|
|
|
|
|
/// `profile.mcp == None` ⇒ no-op: no write, no flag, no env (current path, zero
|
|
|
|
|
/// regression).
|
|
|
|
|
///
|
|
|
|
|
/// Note (M1): the embedded server declaration (command + transport) is a coherent
|
|
|
|
|
/// **placeholder** — the real IdeA MCP server and its exact launch command land in
|
|
|
|
|
/// M2/M3. The strategy plumbing here is stable; only the declaration content will
|
|
|
|
|
/// be finalised then.
|
|
|
|
|
/// MCP runtime (M5d): when the composition root injects a [`McpRuntime`], the
|
|
|
|
|
/// declaration written for a `ConfigFile` strategy is the **real** one — it points
|
|
|
|
|
/// the spawned `idea mcp-server` bridge at the project's exact loopback endpoint
|
|
|
|
|
/// (same source of truth as `ensure_mcp_server`, cadrage v5 §2). When no runtime is
|
|
|
|
|
/// injected (launches issued from inside `application`), a coherent **minimal**
|
|
|
|
|
/// declaration is written instead (see [`mcp_server_declaration`]). `Flag`/`Env`
|
|
|
|
|
/// are unaffected by the runtime: they keep passing the run-dir config path.
|
|
|
|
|
async fn apply_mcp_config(
|
|
|
|
|
&self,
|
|
|
|
|
profile: &AgentProfile,
|
|
|
|
|
run_dir: &ProjectPath,
|
|
|
|
|
runtime: Option<&McpRuntime>,
|
|
|
|
|
spec: &mut SpawnSpec,
|
|
|
|
|
) {
|
|
|
|
|
let Some(mcp) = &profile.mcp else {
|
|
|
|
|
@ -1351,7 +1405,7 @@ impl LaunchAgent {
|
|
|
|
|
match self.fs.exists(&path).await {
|
|
|
|
|
Ok(true) => {}
|
|
|
|
|
Ok(false) => {
|
|
|
|
|
let declaration = mcp_server_declaration(mcp.transport);
|
|
|
|
|
let declaration = mcp_server_declaration(mcp.transport, runtime);
|
|
|
|
|
// Best-effort: a write failure must not fail the launch.
|
|
|
|
|
let _ = self.fs.write(&path, declaration.as_bytes()).await;
|
|
|
|
|
}
|
|
|
|
|
@ -1448,24 +1502,67 @@ fn reattach_decision(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Builds the IdeA MCP server declaration written into a CLI's `ConfigFile` MCP
|
|
|
|
|
/// config (cadrage v3, Décision 3). Coherent **placeholder** for M1: it describes a
|
|
|
|
|
/// stdio/socket server launched by the `idea` binary — the exact command and the
|
|
|
|
|
/// real server land in **M2/M3**. Kept pure (no I/O) so it is unit-testable.
|
|
|
|
|
/// config (cadrage v3 D3 ; cadrage v5 §2). Kept pure (no I/O) so it is unit-testable.
|
|
|
|
|
///
|
|
|
|
|
/// Two shapes, by whether the composition root injected a [`McpRuntime`]:
|
|
|
|
|
///
|
|
|
|
|
/// - **`Some(runtime)` — the real declaration (M5d, end of the placeholder)**:
|
|
|
|
|
/// `command = runtime.exe` (the IdeA executable), and `args` carry the
|
|
|
|
|
/// `mcp-server` subcommand plus the project's exact loopback `--endpoint`, its
|
|
|
|
|
/// `--project` (hyphen-free 32-hex form, consumed by the M5c guard) and the
|
|
|
|
|
/// launching agent as `--requester`. The endpoint string comes verbatim from the
|
|
|
|
|
/// **single source of truth** (`app-tauri::mcp_endpoint`), so the bridge connects
|
|
|
|
|
/// to exactly what `ensure_mcp_server` listens on.
|
|
|
|
|
///
|
|
|
|
|
/// - **`None` — coherent minimal declaration**: launches issued from inside
|
|
|
|
|
/// `application` (orchestrator/hot-swap/tests) have no OS/runtime facts to inject;
|
|
|
|
|
/// rather than fabricate a project-bound endpoint, we write the `idea mcp-server`
|
|
|
|
|
/// command without the endpoint/project/requester args. It stays a valid,
|
|
|
|
|
/// self-consistent `mcpServers/idea` entry (the bridge would resolve the endpoint
|
|
|
|
|
/// from its own project context), surfacing `transport` for the future socket path.
|
|
|
|
|
#[must_use]
|
|
|
|
|
fn mcp_server_declaration(transport: domain::profile::McpTransport) -> String {
|
|
|
|
|
fn mcp_server_declaration(
|
|
|
|
|
transport: domain::profile::McpTransport,
|
|
|
|
|
runtime: Option<&McpRuntime>,
|
|
|
|
|
) -> String {
|
|
|
|
|
// Common `.mcp.json`-style shape (Claude Code et CLIs apparentées). The transport
|
|
|
|
|
// is surfaced so a socket-based CLI can be wired in M2 without changing M1's seam.
|
|
|
|
|
// is surfaced so a socket-based CLI can be wired later without changing this seam.
|
|
|
|
|
let transport_label = match transport {
|
|
|
|
|
domain::profile::McpTransport::Stdio => "stdio",
|
|
|
|
|
domain::profile::McpTransport::Socket => "socket",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// `command` + extra args depend on whether OS/runtime facts were injected.
|
|
|
|
|
// Each `args` entry is emitted as an escaped JSON string so an exe path or
|
|
|
|
|
// endpoint with spaces/backslashes/quotes stays valid JSON.
|
|
|
|
|
let (command, extra_args) = match runtime {
|
|
|
|
|
Some(rt) => {
|
|
|
|
|
let arg = |label: &str, value: &str| {
|
|
|
|
|
format!(
|
|
|
|
|
",\n {},\n {}",
|
|
|
|
|
json_string(label),
|
|
|
|
|
json_string(value)
|
|
|
|
|
)
|
|
|
|
|
};
|
|
|
|
|
let extra = format!(
|
|
|
|
|
"{}{}{}",
|
|
|
|
|
arg("--endpoint", &rt.endpoint),
|
|
|
|
|
arg("--project", &rt.project_id),
|
|
|
|
|
arg("--requester", &rt.requester),
|
|
|
|
|
);
|
|
|
|
|
(rt.exe.as_str(), extra)
|
|
|
|
|
}
|
|
|
|
|
None => ("idea", String::new()),
|
|
|
|
|
};
|
|
|
|
|
let command = json_string(command);
|
|
|
|
|
|
|
|
|
|
format!(
|
|
|
|
|
r#"{{
|
|
|
|
|
"mcpServers": {{
|
|
|
|
|
"idea": {{
|
|
|
|
|
"command": "idea",
|
|
|
|
|
"command": {command},
|
|
|
|
|
"args": [
|
|
|
|
|
"mcp-server"
|
|
|
|
|
"mcp-server"{extra_args}
|
|
|
|
|
],
|
|
|
|
|
"transport": "{transport_label}"
|
|
|
|
|
}}
|
|
|
|
|
@ -1475,6 +1572,13 @@ fn mcp_server_declaration(transport: domain::profile::McpTransport) -> String {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Wraps a string as a JSON string literal (quotes + escaping), reusing the path
|
|
|
|
|
/// escaper so an exe path / endpoint with spaces, backslashes or quotes stays valid
|
|
|
|
|
/// JSON in the generated `.mcp.json`.
|
|
|
|
|
fn json_string(s: &str) -> String {
|
|
|
|
|
format!("\"{}\"", json_escape(s))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Builds an absolute path string by joining a [`ProjectPath`] with a relative
|
|
|
|
|
/// segment using a POSIX separator.
|
|
|
|
|
fn join(base: &ProjectPath, rel: &str) -> String {
|
|
|
|
|
|