fix(domain,application,infrastructure): borne le namespace MCP plugin à 64 chars pour Codex/OpenAI — #333 (QA verte)

Cause racine: PluginMcpServerSpec::client_name() encodait chaque octet
non alphanumérique en _XX, produisant pour UnitySoundTool
`idea_plugin__com_2eidea_2eunity_2ddeveloper_2dtools__unity_2ddeveloper_2dtools_2etools`
(86 chars). Codex préfixe `mcp__` => 91 chars, limite API 64 => HTTP 400
`Invalid input.namespace string too long` sur chaque `codex exec resume`
dont l'historique contenait un outil Unity, aucun agent_message, UI en
fallback générique « réponse non finalisée ».

- domain: client_name() borné à 59 bytes ASCII (=> 64 avec mcp__) —
  stem lisible (minuscules, non-alnum => '_', tronqué) + suffixe
  déterministe = 16 premiers hex de SHA-256(plugin_id + NUL + server_id)
  pour conserver la résistance aux collisions après troncature; tests
  Unity (59 exact), stabilité, slug ponctuation, collisions sur stems
  tronqués identiques
- application: tests de projection TOML/JSON Codex et Claude adaptés au
  vrai namespace legacy encodé (86 chars) — vérifie que les tables
  utilisateur (mcp_servers.user_server, user-server) sont préservées
  pendant le retrait des tables managées périmées
- infrastructure: parse_event Codex traite turn.failed/error comme
  ReplyEvent::Error avec le message fournisseur (fini le fallback
  générique muet), classification lisible MCP_NAMESPACE_TOO_LONG: pour
  les erreurs de longueur de namespace; tests

Après déploiement: nouvelle conversation/restart requis pour les agents
Codex existants — les historiques contiennent déjà le namespace
invalide. Aucun push.
This commit is contained in:
2026-09-12 07:57:03 +02:00
parent 25506e160a
commit a94167ada0
5 changed files with 144 additions and 42 deletions

View File

@ -63,6 +63,9 @@ pub struct ParsedLine {
/// (Codex CLI ≥ 0.147).
/// - `{"type":"turn.completed","usage":{…}}` ⇒ [`ReplyEvent::Heartbeat`] (le `Final`
/// vient de l'`agent_message`, pas de `turn.completed`).
/// - `{"type":"turn.failed","error":{…}}` / `{"type":"error","message":…}` ⇒
/// [`ReplyEvent::Error`] avec le message fournisseur. Une erreur de namespace MCP
/// trop long reçoit en plus le code lisible `MCP_NAMESPACE_TOO_LONG`.
///
/// Ligne vide ⇒ ignorée ; type inconnu ⇒ ignoré sans erreur ; JSON illisible ⇒
/// [`AgentSessionError::Decode`] (jamais de JSON brut propagé).
@ -113,6 +116,11 @@ pub fn parse_event(line: &str) -> Result<ParsedLine, AgentSessionError> {
});
events.push(ReplyEvent::Heartbeat);
}
Some("turn.failed" | "error") => {
events.push(ReplyEvent::Error {
message: codex_terminal_error_message(&value),
});
}
Some("item.started" | "conversation.item.added" | "response.output_item.added") => {
if let Some(item) = value.get("item") {
append_item_progress(
@ -335,13 +343,35 @@ fn error_value_message(value: Option<&Value>) -> Option<String> {
}
fn codex_error_message(root: &Value, item: &Value) -> String {
non_blank_string(item.get("text"))
let message = non_blank_string(item.get("text"))
.or_else(|| non_blank_string(item.get("message")))
.or_else(|| error_value_message(item.get("error")))
.or_else(|| non_blank_string(root.get("stderr")))
.or_else(|| non_blank_string(root.get("message")))
.or_else(|| error_value_message(root.get("error")))
.unwrap_or_else(|| CODEX_ERROR_FALLBACK.to_owned())
.unwrap_or_else(|| CODEX_ERROR_FALLBACK.to_owned());
classify_codex_error(message)
}
fn codex_terminal_error_message(root: &Value) -> String {
let message = non_blank_string(root.get("message"))
.or_else(|| error_value_message(root.get("error")))
.or_else(|| non_blank_string(root.get("stderr")))
.unwrap_or_else(|| CODEX_ERROR_FALLBACK.to_owned());
classify_codex_error(message)
}
fn classify_codex_error(message: String) -> String {
let lowercase = message.to_ascii_lowercase();
if lowercase.contains("namespace")
&& (lowercase.contains("string_above_max_length")
|| lowercase.contains("maximum length 64")
|| lowercase.contains("max length 64"))
{
format!("MCP_NAMESPACE_TOO_LONG: {message}")
} else {
message
}
}
/// Adapter de session structurée Codex.

View File

@ -1167,6 +1167,32 @@ mod tests {
);
}
#[test]
fn codex_turn_failed_surfaces_namespace_error_with_stable_code() {
let r = codex::parse_event(
r#"{"type":"turn.failed","error":{"message":"Invalid 'input[106].namespace': string too long. Expected a string with maximum length 64, but got a string with length 91 instead. [string_above_max_length]"}}"#,
)
.unwrap();
assert!(matches!(
r.events.as_slice(),
[ReplyEvent::Error { message }]
if message.starts_with("MCP_NAMESPACE_TOO_LONG: ")
&& message.contains("input[106].namespace")
));
}
#[test]
fn codex_top_level_error_surfaces_provider_message() {
let r =
codex::parse_event(r#"{"type":"error","message":"provider request failed"}"#).unwrap();
assert_eq!(
r.events,
vec![ReplyEvent::Error {
message: "provider request failed".into()
}]
);
}
/// `agent_message` ⇒ Final (porte le texte de réponse).
#[test]
fn codex_agent_message_is_final() {