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:
@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user