feat(orchestrator): modèle de désignation d'orchestrateur + sink de diagnostic

Introduit le modèle AgentManifest { version, entries, orchestrator } et la
garde d'écriture directe may_write_directly(..., &OrchestratorDesignation) :
seul l'orchestrateur désigné peut écrire directement, les autres passent par
le rendez-vous médié. Câble la désignation à travers domain → application →
infrastructure → app-tauri (context_guard, service, lifecycle, ports).

Ajoute crates/application/src/diag.rs : sink de diagnostic best-effort, sans
dépendance, qui miroite les traces du rendez-vous inter-agents de
l'orchestrateur vers un fichier de log persistant (utile au lancement via
AppImage où stderr est jeté), avec la même discipline « zéro dépendance,
ne casse jamais le rendez-vous ».

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 08:56:17 +02:00
parent 40982d44da
commit 287681c198
57 changed files with 1758 additions and 420 deletions

View File

@ -422,6 +422,12 @@ pub struct McpServerWiring {
}
impl McpServerWiring {
/// Codex's MCP client defaults tool calls to a short interactive timeout (120s on
/// the versions observed in IdeA). `idea_ask_agent` is a synchronous rendezvous
/// that can legitimately span a full delegated dev/test turn, so the generated
/// server config must widen the per-tool timeout.
pub const IDEA_TOOL_TIMEOUT_SEC: u32 = 24 * 60 * 60;
/// Construit le wiring depuis ses parties.
#[must_use]
pub const fn new(command: String, args: Vec<String>, transport: McpTransport) -> Self {
@ -475,9 +481,9 @@ impl McpServerWiring {
}
/// Encode la table **`[mcp_servers.idea]`** d'un `config.toml` Codex :
/// `command`, `args` (tableau TOML), `transport`. Chaque chaîne est échappée en
/// chaîne basique TOML (équivalent du `json_string` : espaces/backslash/quotes
/// restent valides).
/// `command`, `args` (tableau TOML), `transport`, et l'approbation automatique
/// des outils IdeA. Chaque chaîne est échappée en chaîne basique TOML
/// (équivalent du `json_string` : espaces/backslash/quotes restent valides).
#[must_use]
pub fn to_config_toml(&self) -> String {
let command = toml_string(&self.command);
@ -488,8 +494,9 @@ impl McpServerWiring {
.collect::<Vec<_>>()
.join(", ");
let transport = self.transport_label();
let tool_timeout_sec = Self::IDEA_TOOL_TIMEOUT_SEC;
format!(
"[mcp_servers.idea]\ncommand = {command}\nargs = [{args}]\ntransport = \"{transport}\"\n"
"[mcp_servers.idea]\ncommand = {command}\nargs = [{args}]\ntransport = \"{transport}\"\ndefault_tools_approval_mode = \"approve\"\ntool_timeout_sec = {tool_timeout_sec}\n"
)
}
}
@ -910,11 +917,16 @@ impl AgentProfile {
/// ([`crate`] côté application) et la matérialisation ne puissent pas diverger.
#[must_use]
pub fn materializes_idea_bridge(&self) -> bool {
match (self.structured_adapter, self.mcp.as_ref().map(|c| &c.config)) {
match (
self.structured_adapter,
self.mcp.as_ref().map(|c| &c.config),
) {
(Some(StructuredAdapter::Claude), Some(McpConfigStrategy::ConfigFile { target })) => {
target == ".mcp.json"
}
(Some(StructuredAdapter::Codex), Some(McpConfigStrategy::TomlConfigHome { .. })) => true,
(Some(StructuredAdapter::Codex), Some(McpConfigStrategy::TomlConfigHome { .. })) => {
true
}
_ => false,
}
}
@ -1262,7 +1274,10 @@ mod mcp_tests {
!json.contains("stallAfterMs"),
"an unset stall threshold must be omitted; got: {json}"
);
assert!(json.contains("turnTimeoutMs"), "set threshold present: {json}");
assert!(
json.contains("turnTimeoutMs"),
"set threshold present: {json}"
);
}
#[test]
@ -1351,8 +1366,7 @@ mod mcp_tests {
assert!(codex.materializes_idea_bridge());
// Codex WITHOUT any MCP capability ⇒ no bridge.
let codex_no_mcp =
profile_without_mcp().with_structured_adapter(StructuredAdapter::Codex);
let codex_no_mcp = profile_without_mcp().with_structured_adapter(StructuredAdapter::Codex);
assert!(!codex_no_mcp.materializes_idea_bridge());
// Codex + wrong strategy (`.mcp.json` ConfigFile, Claude's shape) ⇒ no bridge.
@ -1448,6 +1462,16 @@ mod mcp_tests {
toml.contains("transport = \"stdio\""),
"transport expected; got: {toml}"
);
// IdeA-owned MCP tools are pre-approved, matching Claude's non-prompting
// `.mcp.json` path.
assert!(
toml.contains("default_tools_approval_mode = \"approve\""),
"IdeA MCP tools should not prompt for approval; got: {toml}"
);
assert!(
toml.contains("tool_timeout_sec = 86400"),
"IdeA MCP tools must outlive Codex's short default tool timeout; got: {toml}"
);
}
// -- §21 : rate_limit_pattern (détection de limite par motif, niveau 2) ------
@ -1519,8 +1543,14 @@ mod mcp_tests {
let json = serde_json::to_string(&profile).expect("serialise");
assert!(json.contains("rateLimitPattern"), "key present: {json}");
// camelCase respecté sur les champs de RateLimitPattern.
assert!(json.contains("resetCapture"), "camelCase field resetCapture: {json}");
assert!(json.contains("timeFormat"), "camelCase field timeFormat: {json}");
assert!(
json.contains("resetCapture"),
"camelCase field resetCapture: {json}"
);
assert!(
json.contains("timeFormat"),
"camelCase field timeFormat: {json}"
);
let back: AgentProfile = serde_json::from_str(&json).expect("deserialise");
assert_eq!(profile, back);
@ -1531,7 +1561,10 @@ mod mcp_tests {
// reset_capture / time_format à None ⇒ leurs clés sont omises.
let pattern = RateLimitPattern::new("rate limited", None, None).expect("valid pattern");
let json = serde_json::to_string(&pattern).expect("serialise");
assert!(json.contains("\"pattern\""), "pattern field present: {json}");
assert!(
json.contains("\"pattern\""),
"pattern field present: {json}"
);
assert!(
!json.contains("resetCapture"),
"an unset resetCapture must be omitted; got: {json}"