diff --git a/codex-rs/core/src/tools/multi_agent_tool.rs b/codex-rs/core/src/tools/multi_agent_tool.rs index 1bfe8e5413..3e8e77410c 100644 --- a/codex-rs/core/src/tools/multi_agent_tool.rs +++ b/codex-rs/core/src/tools/multi_agent_tool.rs @@ -4,6 +4,7 @@ use crate::session::session::Session; use crate::tools::context::ToolInvocation; use crate::tools::registry::CoreToolRuntime; +use codex_protocol::openai_models::GuardianScope; use codex_tools::JsonSchema; use codex_tools::ResponsesApiNamespace; use codex_tools::ResponsesApiNamespaceTool; @@ -123,10 +124,8 @@ impl CoreToolRuntime for MultiAgentV2ToolOverrides { .config .features .enabled(codex_features::Feature::GuardianNativeMultiAgentHistory) - && matches!( - self.handler.tool_name().name.as_str(), - "spawn_agent" | "send_message" | "followup_task" | "interrupt_agent" - ) + && GuardianScope::for_multi_agent_tool(&self.tool_name(), self.namespace.as_deref()) + .is_some() } fn wait_until_ready<'a>(&'a self, session: &'a Arc) -> Option> { diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/observation.rs b/codex-rs/ext/guardian-v2/src/async_scorer/observation.rs index 00ffcf9cfa..103355650e 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/observation.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/observation.rs @@ -77,21 +77,16 @@ impl GuardianV2Extension { GuardianScope::for_mcp_connector(&info.server_name, info.connector_id.as_deref()) }) .or_else(|| { - if guardian_config.review_multi_agent_calls - && match guardian_config.multi_agent_namespace.as_deref() { - Some(namespace) => input.tool_name.namespace.as_deref() == Some(namespace), - None => input.tool_name.is_default_namespace(), - } - && matches!( - input.tool_name.name.as_str(), - "spawn_agent" | "send_message" | "followup_task" | "interrupt_agent" + if guardian_config.review_multi_agent_calls { + GuardianScope::for_multi_agent_tool( + input.tool_name, + guardian_config.multi_agent_namespace.as_deref(), ) - { - Some(GuardianScope::NativeTool) } else { - GuardianScope::for_tool(input.tool_name) + None } - }); + }) + .or_else(|| GuardianScope::for_tool(input.tool_name)); // Model policies review nested actions; the Code Mode wrapper leaves their scores alone. // The legacy all-tools policy still scores wrappers through `other_tools`. if scope.is_none() diff --git a/codex-rs/protocol/src/openai_models/guardian.rs b/codex-rs/protocol/src/openai_models/guardian.rs index a09c57212a..ea9c65e36d 100644 --- a/codex-rs/protocol/src/openai_models/guardian.rs +++ b/codex-rs/protocol/src/openai_models/guardian.rs @@ -138,6 +138,25 @@ impl GuardianScope { } } + /// Classifies state-changing MAv2 calls in the configured namespace. + /// An absent namespace accepts the default namespace spellings; callers gate this lookup. + pub fn for_multi_agent_tool(tool: &ToolName, namespace: Option<&str>) -> Option { + let matches_namespace = match namespace { + Some(namespace) => tool.namespace.as_deref() == Some(namespace), + None => tool.is_default_namespace(), + }; + if matches_namespace + && matches!( + tool.name.as_str(), + "spawn_agent" | "send_message" | "followup_task" | "interrupt_agent" + ) + { + Some(Self::NativeTool) + } else { + None + } + } + pub fn for_tool(tool: &ToolName) -> Option { if is_node_repl_backed_tool(&tool.name, tool.namespace.as_deref()) { return Some(Self::ComputerUse);