Skip to main content

agentic_core/tool/
codex.rs

1use std::collections::HashMap;
2
3use serde_json::{Map, Value};
4
5use crate::events::WireEvent;
6use crate::types::io::{FunctionTool, FunctionToolCall, OutputItem, ToolChoice};
7use crate::types::tools::{CodexNamespaceMember, CodexNamespaceToolParam, NonEmptyToolName, ResponsesTool};
8use crate::utils::common::serialize_to_value_or_custom_default;
9
10use super::handler::{ToolError, ToolHandler};
11use super::registry::{ToolEntry, ToolType};
12
13// Upstream Responses-compatible backends only see flat function names. Prefix
14// flattened Codex namespace members so generated names are recognizable,
15// unlikely to collide with user functions, and can be restored to
16// `{ namespace, name }` on the way back to the client.
17pub const MODEL_VISIBLE_NAMESPACE_MEMBER_PREFIX: &str = "agentic_ns__";
18pub const MAX_MODEL_VISIBLE_TOOL_NAME_LEN: usize = 64;
19
20const HASHED_NAMESPACE_MEMBER_SUFFIX_LEN: usize = 18;
21
22fn stable_name_hash(value: &str) -> u64 {
23    const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
24    const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
25
26    value.bytes().fold(FNV_OFFSET_BASIS, |hash, byte| {
27        (hash ^ u64::from(byte)).wrapping_mul(FNV_PRIME)
28    })
29}
30
31#[must_use]
32pub fn model_visible_namespace_member_name(namespace: &str, member: &str) -> String {
33    let full_name = format!("{MODEL_VISIBLE_NAMESPACE_MEMBER_PREFIX}{namespace}__{member}");
34    if full_name.chars().count() <= MAX_MODEL_VISIBLE_TOOL_NAME_LEN {
35        return full_name;
36    }
37
38    let hash = stable_name_hash(&full_name);
39    let readable_len = MAX_MODEL_VISIBLE_TOOL_NAME_LEN - HASHED_NAMESPACE_MEMBER_SUFFIX_LEN;
40    let readable_prefix = full_name.chars().take(readable_len).collect::<String>();
41    format!("{readable_prefix}__{hash:016x}")
42}
43
44/// Registers one `ToolEntry` per `Function` member of `p`, keyed by the
45/// member's already-flattened, model-visible name — callers must resolve
46/// namespace members to those flat names first (see
47/// [`CodexNamespaceHandler::resolve_namespace_members`]).
48pub(crate) fn insert_namespace_entries(entries: &mut HashMap<String, ToolEntry>, p: &CodexNamespaceToolParam) {
49    serialize_to_value_or_custom_default(
50        p,
51        "namespace tool config serialization failed",
52        |config| {
53            for member in &p.tools {
54                let CodexNamespaceMember::Function(function) = member else {
55                    continue;
56                };
57                let name = function.name.as_str().to_owned();
58                if entries
59                    .insert(
60                        name.clone(),
61                        ToolEntry {
62                            tool_type: ToolType::CodexNamespace,
63                            config: config.clone(),
64                            server_label: Some(p.name.clone()),
65                            handler: None,
66                        },
67                    )
68                    .is_some()
69                {
70                    tracing::warn!(name = %name, namespace = %p.name, "duplicate tool name - previous definition overwritten");
71                }
72            }
73        },
74        (),
75    );
76}
77
78#[derive(Clone, Debug, Eq, Hash, PartialEq)]
79struct NamespaceMemberName {
80    namespace: String,
81    name: String,
82}
83
84#[derive(Clone, Debug)]
85struct NamespaceCallMapping {
86    member: NamespaceMemberName,
87    upstream_name: String,
88}
89
90/// A pre-built, reusable namespace rename map, computed once per request from
91/// the declared tools via [`CodexNamespaceHandler::build_namespace_map`].
92///
93/// Passing this into [`CodexNamespaceHandler::restore_output_items`],
94/// [`CodexNamespaceHandler::restore_response_value`], and
95/// [`CodexNamespaceHandler::resolve_tool_choice`] avoids
96/// rebuilding the map on every call — important for streaming responses,
97/// which call the restore path once per SSE line.
98#[derive(Clone, Debug, Default)]
99pub struct NamespaceMap {
100    calls: HashMap<String, NamespaceCallMapping>,
101    members: HashMap<NamespaceMemberName, String>,
102}
103
104impl NamespaceMap {
105    fn mapping_for_call(&self, name: &str) -> Option<&NamespaceCallMapping> {
106        self.calls.get(name)
107    }
108
109    fn mapping_for_member(&self, namespace: &str, name: &str) -> Option<&NamespaceCallMapping> {
110        let member = NamespaceMemberName {
111            namespace: namespace.to_string(),
112            name: name.to_string(),
113        };
114        self.members
115            .get(&member)
116            .and_then(|upstream_name| self.calls.get(upstream_name))
117    }
118}
119
120#[derive(Default)]
121struct NamespaceMapBuilder {
122    top_level_registry_keys: HashMap<String, ToolType>,
123    map: NamespaceMap,
124}
125
126impl NamespaceMapBuilder {
127    fn new(top_level_registry_keys: HashMap<String, ToolType>) -> Self {
128        Self {
129            top_level_registry_keys,
130            ..Self::default()
131        }
132    }
133
134    fn validate_and_record_flat_member(
135        &mut self,
136        namespace_name: &str,
137        member_name: &str,
138    ) -> Result<String, ToolError> {
139        let flat_name = model_visible_namespace_member_name(namespace_name, member_name);
140        if let Some(tool_kind) = self.top_level_registry_keys.get(&flat_name) {
141            return Err(ToolError::Config(format!(
142                "codex namespace member {namespace_name}.{member_name} generates name {flat_name}, which collides with a declared {}",
143                tool_kind.description()
144            )));
145        }
146        if let Some(existing) = self.map.calls.get(&flat_name) {
147            if existing.member.namespace != namespace_name || existing.member.name != member_name {
148                return Err(ToolError::Config(format!(
149                    "codex namespace member {namespace_name}.{member_name} collides with {}.{} at generated name {flat_name}",
150                    existing.member.namespace, existing.member.name
151                )));
152            }
153        }
154        Ok(self.record_flat_member_with_flat_name(namespace_name, member_name, flat_name))
155    }
156
157    fn record_flat_member_with_flat_name(
158        &mut self,
159        namespace_name: &str,
160        member_name: &str,
161        flat_name: String,
162    ) -> String {
163        let member = NamespaceMemberName {
164            namespace: namespace_name.to_string(),
165            name: member_name.to_string(),
166        };
167        if let Some(existing) = self.map.calls.get(&flat_name) {
168            debug_assert!(
169                existing.member == member,
170                "namespace collisions must be validated before recording namespace members"
171            );
172            if existing.member != member {
173                tracing::warn!(
174                    upstream_name = %flat_name,
175                    namespace = %namespace_name,
176                    member = %member_name,
177                    existing_namespace = %existing.member.namespace,
178                    existing_member = %existing.member.name,
179                    "generated codex namespace member name collides with another namespace member"
180                );
181            }
182        }
183        let mapping = NamespaceCallMapping {
184            member: member.clone(),
185            upstream_name: flat_name.clone(),
186        };
187
188        self.map.members.insert(member, flat_name.clone());
189        self.map.calls.insert(flat_name.clone(), mapping);
190        flat_name
191    }
192
193    fn finish(self) -> NamespaceMap {
194        self.map
195    }
196}
197
198/// Handler for Codex `type: "namespace"` tools.
199///
200/// Namespace tools are client-owned, like plain function tools, but need a
201/// request-scoped normalization pass to flatten members into model-visible
202/// function names and restore model calls back to the public namespace shape.
203#[derive(Debug)]
204pub struct CodexNamespaceHandler;
205
206impl CodexNamespaceHandler {
207    /// Rewrites every `Namespace` tool's function members to their flat,
208    /// model-visible names (see [`model_visible_namespace_member_name`]),
209    /// with collision detection against sibling function-call registry keys.
210    ///
211    /// Tools stay `ResponsesTool::Namespace` — only the nested members'
212    /// `name` fields change — so [`ResponsesTool::to_function_tools`] and
213    /// [`super::registry::ToolRegistry::build_with_handlers`] can read each
214    /// member's already-flat name directly, with no further namespace logic.
215    ///
216    /// Request execution must handle the result so ambiguous declarations fail
217    /// instead of being normalized into an irreversible flat shape.
218    ///
219    /// # Errors
220    ///
221    /// Returns [`ToolError::Config`] when a generated namespace member name
222    /// collides with another declared function-call tool or with another
223    /// namespace member.
224    pub fn resolve_namespace_members(&self, tools: &[ResponsesTool]) -> Result<Vec<ResponsesTool>, ToolError> {
225        let mut builder = NamespaceMapBuilder::new(typed_top_level_registry_keys(tools));
226        tools
227            .iter()
228            .map(|tool| match tool {
229                ResponsesTool::Namespace(namespace) => {
230                    rename_namespace_members(namespace, &mut builder).map(ResponsesTool::Namespace)
231                }
232                other => Ok(other.clone()),
233            })
234            .collect()
235    }
236
237    /// Builds a [`NamespaceMap`] once from a request's declared tools, for
238    /// reuse across every subsequent restore/rewrite call on that request —
239    /// see [`NamespaceMap`]'s docs for why this matters.
240    /// # Errors
241    ///
242    /// Returns [`ToolError::Config`] when a generated namespace member name
243    /// collides with another declared function-call tool or with another
244    /// namespace member.
245    pub fn build_namespace_map(&self, tools: Option<&[ResponsesTool]>) -> Result<Option<NamespaceMap>, ToolError> {
246        namespace_map_from_tools(tools)
247    }
248
249    /// Rejects namespace declarations that cannot be represented
250    /// unambiguously by the gateway-owned flat model-visible naming scheme.
251    ///
252    /// This must run before building the upstream request or request-scoped
253    /// registry. Otherwise the gateway could silently drop a namespace member
254    /// or restore a flat model call to the wrong public `{ namespace, name }`.
255    ///
256    /// # Errors
257    ///
258    /// Returns [`ToolError::Config`] when a generated namespace member name
259    /// collides with another declared function-call tool or with another
260    /// namespace member.
261    pub fn validate_namespace_collisions(&self, tools: Option<&[ResponsesTool]>) -> Result<(), ToolError> {
262        let Some(tools) = tools else {
263            return Ok(());
264        };
265        let mut builder = NamespaceMapBuilder::new(typed_top_level_registry_keys(tools));
266        for tool in tools {
267            let ResponsesTool::Namespace(namespace) = tool else {
268                continue;
269            };
270            for member_name in typed_function_member_names(namespace) {
271                builder.validate_and_record_flat_member(&namespace.name, &member_name)?;
272            }
273        }
274        Ok(())
275    }
276
277    /// Resolves the request's `tool_choice` (defaulting to `ToolChoice::Auto`
278    /// when absent) and, if it's a namespaced `ToolChoice::Function {
279    /// namespace, name }`, rewrites it to the flattened, model-visible name
280    /// that [`ResponsesTool::to_function_tools`] produces for the matching
281    /// namespace member — so `tool_choice` agrees with the tool names
282    /// actually sent upstream.
283    ///
284    /// A no-op for `ToolChoice` variants other than `Function`, and for a
285    /// `Function` choice that doesn't match any declared namespace member.
286    #[must_use]
287    pub fn resolve_tool_choice(&self, map: Option<&NamespaceMap>, tool_choice: Option<&ToolChoice>) -> ToolChoice {
288        let tool_choice = tool_choice.unwrap_or(&ToolChoice::Auto);
289        let Some(map) = map else {
290            return tool_choice.clone();
291        };
292        rewrite_tool_choice_with_map(tool_choice, map)
293    }
294
295    pub fn restore_output_items(&self, output: &mut [OutputItem], map: Option<&NamespaceMap>) {
296        let Some(map) = map else {
297            return;
298        };
299        for item in output {
300            if let OutputItem::FunctionCall(call) = item {
301                restore_function_call_with_map(call, map);
302            }
303        }
304    }
305
306    #[must_use]
307    pub fn restore_response_value(&self, value: &mut Value, map: Option<&NamespaceMap>) -> bool {
308        let Some(map) = map else {
309            return false;
310        };
311        restore_response_value_with_map(value, map)
312    }
313
314    #[must_use]
315    pub fn restore_response_wire(&self, wire: &mut WireEvent, map: Option<&NamespaceMap>) -> bool {
316        let Some(map) = map else {
317            return false;
318        };
319        restore_response_map_with_map(&mut wire.rest, map)
320    }
321}
322
323impl ToolHandler for CodexNamespaceHandler {
324    fn tool_type(&self) -> ToolType {
325        ToolType::CodexNamespace
326    }
327
328    fn validate(&self, param: &Value) -> Result<(), ToolError> {
329        serde_json::from_value::<CodexNamespaceToolParam>(param.clone())
330            .map(|_| ())
331            .map_err(|e| ToolError::Config(format!("invalid codex namespace tool config: {e}")))
332    }
333
334    /// Converts an already-renamed namespace's function members straight to
335    /// `FunctionTool`s. Callers must rename members to their flat, model-visible
336    /// names first via [`CodexNamespaceHandler::resolve_namespace_members`] —
337    /// this method has no sibling-tool context to do that itself.
338    fn normalize(&self, param: &Value) -> Vec<FunctionTool> {
339        let Ok(namespace) = serde_json::from_value::<CodexNamespaceToolParam>(param.clone()) else {
340            tracing::warn!("normalize() called with invalid codex namespace param - validate() must be called first");
341            return vec![];
342        };
343        namespace
344            .tools
345            .iter()
346            .filter_map(|member| match member {
347                CodexNamespaceMember::Function(function) => Some(FunctionTool::from(function)),
348                CodexNamespaceMember::Unknown => None,
349            })
350            .collect()
351    }
352}
353
354fn namespace_map_from_tools(tools: Option<&[ResponsesTool]>) -> Result<Option<NamespaceMap>, ToolError> {
355    let Some(tools) = tools else {
356        return Ok(None);
357    };
358    let mut builder = NamespaceMapBuilder::new(typed_top_level_registry_keys(tools));
359    for tool in tools {
360        if let ResponsesTool::Namespace(namespace) = tool {
361            let _ = rename_namespace_members(namespace, &mut builder)?;
362        }
363    }
364    Ok(Some(builder.finish()))
365}
366
367/// Returns `namespace` with its function members' names rewritten to their
368/// flat, model-visible form, recording each rename in `builder` along the
369/// way.
370///
371/// # Errors
372///
373/// Returns [`ToolError::Config`] when a generated namespace member name
374/// collides with another declared function-call tool or with another namespace
375/// member.
376fn rename_namespace_members(
377    namespace: &CodexNamespaceToolParam,
378    builder: &mut NamespaceMapBuilder,
379) -> Result<CodexNamespaceToolParam, ToolError> {
380    let function_member_names = typed_function_member_names(namespace);
381    if function_member_names.is_empty() {
382        tracing::debug!(
383            namespace = %namespace.name,
384            "namespace tool has no function members to rename for upstream"
385        );
386        return Ok(namespace.clone());
387    }
388    let tools = namespace
389        .tools
390        .iter()
391        .map(|member| {
392            let CodexNamespaceMember::Function(function) = member else {
393                return Ok(member.clone());
394            };
395            let flat_name_text = builder.validate_and_record_flat_member(&namespace.name, function.name.as_str())?;
396            let flat_name = NonEmptyToolName::try_from(flat_name_text.clone())
397                .expect("generated namespace member names include a non-empty prefix");
398            tracing::debug!(
399                namespace = %namespace.name,
400                member = %function.name.as_str(),
401                upstream_name = %flat_name_text,
402                "renamed namespace tool member for upstream"
403            );
404            let mut function = function.clone();
405            function.name = flat_name;
406            Ok(CodexNamespaceMember::Function(function))
407        })
408        .collect::<Result<Vec<_>, ToolError>>()?;
409
410    Ok(CodexNamespaceToolParam {
411        tools,
412        ..namespace.clone()
413    })
414}
415
416fn typed_top_level_registry_keys(tools: &[ResponsesTool]) -> HashMap<String, ToolType> {
417    tools
418        .iter()
419        .filter_map(|tool| {
420            let registry_key = match tool {
421                ResponsesTool::Function(function) => function.name.as_str().to_owned(),
422                ResponsesTool::WebSearch(_) => "web_search".to_owned(),
423                ResponsesTool::FileSearch(_) => "file_search".to_owned(),
424                ResponsesTool::CodeInterpreter(_) => "code_interpreter".to_owned(),
425                ResponsesTool::Mcp(_)
426                | ResponsesTool::Namespace(_)
427                | ResponsesTool::Custom(_)
428                | ResponsesTool::Unknown => return None,
429            };
430            tool.tool_type().map(|tool_type| (registry_key, tool_type))
431        })
432        .collect()
433}
434
435fn typed_function_member_names(namespace: &CodexNamespaceToolParam) -> Vec<String> {
436    namespace
437        .tools
438        .iter()
439        .filter_map(|member| match member {
440            CodexNamespaceMember::Function(function) => Some(function.name.as_str().to_string()),
441            CodexNamespaceMember::Unknown => None,
442        })
443        .collect()
444}
445
446fn restore_function_call_with_map(call: &mut FunctionToolCall, map: &NamespaceMap) -> bool {
447    if call.namespace.is_some() {
448        return false;
449    }
450    let Some(mapping) = map.mapping_for_call(&call.name) else {
451        return false;
452    };
453    let original_name = call.name.clone();
454
455    call.namespace = Some(mapping.member.namespace.clone());
456    call.name.clone_from(&mapping.member.name);
457    tracing::debug!(
458        upstream_name = %original_name,
459        namespace = %mapping.member.namespace,
460        member = %mapping.member.name,
461        "restored upstream namespace function call"
462    );
463    true
464}
465
466fn rewrite_tool_choice_with_map(choice: &ToolChoice, map: &NamespaceMap) -> ToolChoice {
467    let ToolChoice::Function { namespace, name } = choice else {
468        return choice.clone();
469    };
470    let mapping = namespace
471        .as_deref()
472        .and_then(|namespace| map.mapping_for_member(namespace, name.as_str()))
473        .or_else(|| {
474            namespace
475                .is_none()
476                .then(|| map.mapping_for_call(name.as_str()))
477                .flatten()
478        });
479    let Some(mapping) = mapping else {
480        return choice.clone();
481    };
482    let Ok(name) = NonEmptyToolName::try_from(mapping.upstream_name.clone()) else {
483        return choice.clone();
484    };
485
486    ToolChoice::Function { namespace: None, name }
487}
488
489fn restore_response_value_with_map(value: &mut Value, map: &NamespaceMap) -> bool {
490    let mut changed = false;
491
492    if let Some(item) = value.as_object_mut().and_then(|object| object.get_mut("item")) {
493        changed |= restore_call_value_with_map(item, map);
494    }
495
496    changed |= restore_call_value_with_map(value, map);
497
498    for key in ["response", "payload"] {
499        if let Some(nested) = value.as_object_mut().and_then(|object| object.get_mut(key)) {
500            changed |= restore_response_value_with_map(nested, map);
501        }
502    }
503
504    if let Some(Value::Array(items)) = value.as_object_mut().and_then(|object| object.get_mut("output")) {
505        for item in items {
506            changed |= restore_call_value_with_map(item, map);
507        }
508    }
509
510    changed
511}
512
513fn restore_call_value_with_map(value: &mut Value, map: &NamespaceMap) -> bool {
514    let Some(object) = value.as_object_mut() else {
515        return false;
516    };
517    if object.get("type").and_then(Value::as_str) != Some("function_call") {
518        return false;
519    }
520    if object.get("namespace").and_then(Value::as_str).is_some() {
521        return false;
522    }
523    let Some(name) = object.get("name").and_then(Value::as_str) else {
524        return false;
525    };
526    let Some(mapping) = map.mapping_for_call(name) else {
527        return false;
528    };
529    let original_name = name.to_string();
530
531    object.insert("namespace".to_string(), Value::String(mapping.member.namespace.clone()));
532    object.insert("name".to_string(), Value::String(mapping.member.name.clone()));
533    tracing::debug!(
534        upstream_name = %original_name,
535        namespace = %mapping.member.namespace,
536        member = %mapping.member.name,
537        "restored upstream namespace function call"
538    );
539    true
540}
541
542fn restore_response_map_with_map(object: &mut Map<String, Value>, map: &NamespaceMap) -> bool {
543    let mut changed = false;
544    if let Some(item) = object.get_mut("item") {
545        changed |= restore_call_value_with_map(item, map);
546    }
547    for key in ["response", "payload"] {
548        if let Some(nested) = object.get_mut(key) {
549            changed |= restore_response_value_with_map(nested, map);
550        }
551    }
552    if let Some(Value::Array(items)) = object.get_mut("output") {
553        for item in items {
554            changed |= restore_call_value_with_map(item, map);
555        }
556    }
557    changed
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563    use crate::types::event::MessageStatus;
564
565    fn completed_call(name: &str, arguments: &str) -> OutputItem {
566        OutputItem::FunctionCall(FunctionToolCall {
567            id: "fc_1".to_string(),
568            call_id: "call_1".to_string(),
569            name: name.to_string(),
570            namespace: None,
571            arguments: arguments.to_string(),
572            status: MessageStatus::Completed,
573        })
574    }
575
576    #[test]
577    fn unqualified_function_tool_choice_is_not_rewritten_to_namespace_member() {
578        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
579            {
580                "type": "namespace",
581                "name": "mcp__shell",
582                "tools": [{"type": "function", "name": "run"}]
583            }
584        ]))
585        .unwrap();
586        let choice = ToolChoice::Function {
587            namespace: None,
588            name: NonEmptyToolName::try_from("run").unwrap(),
589        };
590
591        let map = CodexNamespaceHandler
592            .build_namespace_map(Some(&tools))
593            .expect("valid namespace map");
594        let rewritten = CodexNamespaceHandler.resolve_tool_choice(map.as_ref(), Some(&choice));
595
596        assert_eq!(
597            rewritten,
598            ToolChoice::Function {
599                namespace: None,
600                name: NonEmptyToolName::try_from("run").unwrap()
601            }
602        );
603        let resolved = CodexNamespaceHandler
604            .resolve_namespace_members(&tools)
605            .expect("valid namespace members");
606        assert!(matches!(
607            resolved.as_slice(),
608            [ResponsesTool::Namespace(namespace)]
609                if matches!(&namespace.tools[0], CodexNamespaceMember::Function(f) if f.name.as_str() == "agentic_ns__mcp__shell__run")
610        ));
611    }
612
613    #[test]
614    fn namespaced_function_tool_choice_flattens_exact_member() {
615        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
616            {
617                "type": "namespace",
618                "name": "mcp__shell",
619                "tools": [{"type": "function", "name": "run"}]
620            },
621            {
622                "type": "namespace",
623                "name": "mcp__git",
624                "tools": [{"type": "function", "name": "run"}]
625            }
626        ]))
627        .unwrap();
628        let choice: ToolChoice = serde_json::from_value(serde_json::json!({
629            "type": "function",
630            "namespace": "mcp__git",
631            "name": "run"
632        }))
633        .unwrap();
634
635        let map = CodexNamespaceHandler
636            .build_namespace_map(Some(&tools))
637            .expect("valid namespace map");
638        let rewritten = CodexNamespaceHandler.resolve_tool_choice(map.as_ref(), Some(&choice));
639
640        assert_eq!(
641            rewritten,
642            ToolChoice::Function {
643                namespace: None,
644                name: NonEmptyToolName::try_from("agentic_ns__mcp__git__run").unwrap()
645            }
646        );
647    }
648
649    #[test]
650    fn long_namespace_member_name_is_stable_and_within_upstream_limit() {
651        let namespace = "mcp__codex_apps__github";
652        let member = "_remove_reaction_from_pr_review_comment";
653
654        let shortened = model_visible_namespace_member_name(namespace, member);
655
656        assert_eq!(shortened.chars().count(), MAX_MODEL_VISIBLE_TOOL_NAME_LEN);
657        assert_eq!(
658            shortened,
659            "agentic_ns__mcp__codex_apps__github___remove_r__2e989f39f22daf41"
660        );
661        assert_eq!(shortened, model_visible_namespace_member_name(namespace, member));
662        assert_ne!(
663            shortened,
664            model_visible_namespace_member_name(namespace, "_remove_reaction_from_issue_comment")
665        );
666    }
667
668    #[test]
669    fn namespace_member_name_preserves_exact_limit_and_shortens_next_character() {
670        let namespace = "n";
671        let fixed_len = MODEL_VISIBLE_NAMESPACE_MEMBER_PREFIX.chars().count() + namespace.chars().count() + 2;
672        let member_at_limit = "m".repeat(MAX_MODEL_VISIBLE_TOOL_NAME_LEN - fixed_len);
673        let full_name_at_limit = format!("{MODEL_VISIBLE_NAMESPACE_MEMBER_PREFIX}{namespace}__{member_at_limit}");
674
675        assert_eq!(full_name_at_limit.chars().count(), MAX_MODEL_VISIBLE_TOOL_NAME_LEN);
676        assert_eq!(
677            model_visible_namespace_member_name(namespace, &member_at_limit),
678            full_name_at_limit
679        );
680
681        let member_over_limit = format!("{member_at_limit}m");
682        let shortened = model_visible_namespace_member_name(namespace, &member_over_limit);
683        assert_eq!(shortened.chars().count(), MAX_MODEL_VISIBLE_TOOL_NAME_LEN);
684        assert_ne!(
685            shortened,
686            format!("{MODEL_VISIBLE_NAMESPACE_MEMBER_PREFIX}{namespace}__{member_over_limit}")
687        );
688    }
689
690    #[test]
691    fn long_unicode_namespace_member_name_stays_valid_utf8() {
692        let namespace = "工具箱";
693        let member = "工具".repeat(30);
694
695        let shortened = model_visible_namespace_member_name(namespace, &member);
696
697        assert_eq!(shortened.chars().count(), MAX_MODEL_VISIBLE_TOOL_NAME_LEN);
698        assert!(shortened.starts_with("agentic_ns__工具箱__"));
699    }
700
701    #[test]
702    fn long_namespace_member_round_trips_through_shortened_name() {
703        let namespace = "mcp__codex_apps__github";
704        let member = "_remove_reaction_from_pr_review_comment";
705        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
706            {
707                "type": "namespace",
708                "name": namespace,
709                "tools": [{"type": "function", "name": member}]
710            }
711        ]))
712        .unwrap();
713        let upstream_name = model_visible_namespace_member_name(namespace, member);
714        let mut output = vec![completed_call(&upstream_name, "{}")];
715
716        let resolved = CodexNamespaceHandler
717            .resolve_namespace_members(&tools)
718            .expect("valid namespace members");
719        assert!(matches!(
720            resolved.as_slice(),
721            [ResponsesTool::Namespace(namespace)]
722                if matches!(&namespace.tools[0], CodexNamespaceMember::Function(function)
723                    if function.name.as_str() == upstream_name)
724        ));
725
726        let map = CodexNamespaceHandler
727            .build_namespace_map(Some(&tools))
728            .expect("valid namespace map");
729        let choice = ToolChoice::Function {
730            namespace: Some(namespace.to_string()),
731            name: NonEmptyToolName::try_from(member).unwrap(),
732        };
733        assert_eq!(
734            CodexNamespaceHandler.resolve_tool_choice(map.as_ref(), Some(&choice)),
735            ToolChoice::Function {
736                namespace: None,
737                name: NonEmptyToolName::try_from(upstream_name).unwrap(),
738            }
739        );
740        CodexNamespaceHandler.restore_output_items(&mut output, map.as_ref());
741
742        let OutputItem::FunctionCall(call) = &output[0] else {
743            panic!("expected function call");
744        };
745        assert_eq!(call.namespace.as_deref(), Some(namespace));
746        assert_eq!(call.name, member);
747    }
748
749    #[test]
750    fn validate_namespace_collisions_rejects_top_level_flat_name_collision() {
751        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
752            {"type": "function", "name": "agentic_ns__mcp__shell__run"},
753            {
754                "type": "namespace",
755                "name": "mcp__shell",
756                "tools": [{"type": "function", "name": "run"}]
757            }
758        ]))
759        .unwrap();
760
761        let err = CodexNamespaceHandler
762            .validate_namespace_collisions(Some(&tools))
763            .unwrap_err();
764
765        assert!(err.to_string().contains("collides with a declared function tool"));
766    }
767
768    #[test]
769    fn resolve_namespace_members_rejects_top_level_flat_name_collision() {
770        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
771            {"type": "function", "name": "agentic_ns__mcp__shell__run"},
772            {
773                "type": "namespace",
774                "name": "mcp__shell",
775                "tools": [{"type": "function", "name": "run"}]
776            }
777        ]))
778        .unwrap();
779
780        let err = CodexNamespaceHandler.resolve_namespace_members(&tools).unwrap_err();
781
782        assert!(err.to_string().contains("collides with a declared function tool"));
783    }
784
785    #[test]
786    fn resolve_namespace_members_accepts_native_mcp_without_static_registry_key() {
787        let namespace = "mcp__codex_apps__github";
788        let member = "_remove_reaction_from_pr_review_comment";
789        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
790            {
791                "type": "namespace",
792                "name": namespace,
793                "tools": [{"type": "function", "name": member}]
794            },
795            {
796                "type": "mcp",
797                "server_label": "fixture",
798                "server_url": "http://127.0.0.1:1/mcp"
799            }
800        ]))
801        .unwrap();
802
803        CodexNamespaceHandler
804            .resolve_namespace_members(&tools)
805            .expect("native MCP registry keys are derived after discovery");
806    }
807
808    #[test]
809    fn validate_namespace_collisions_rejects_generated_name_collision_between_namespace_members() {
810        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
811            {
812                "type": "namespace",
813                "name": "a__b",
814                "tools": [{"type": "function", "name": "c"}]
815            },
816            {
817                "type": "namespace",
818                "name": "a",
819                "tools": [{"type": "function", "name": "b__c"}]
820            }
821        ]))
822        .unwrap();
823
824        let err = CodexNamespaceHandler
825            .validate_namespace_collisions(Some(&tools))
826            .unwrap_err();
827
828        assert!(err.to_string().contains("generated name"));
829    }
830
831    #[test]
832    #[cfg(debug_assertions)]
833    #[should_panic(expected = "namespace collisions must be validated before recording namespace members")]
834    fn namespace_map_builder_debug_asserts_when_member_collision_validation_is_skipped() {
835        let mut builder = NamespaceMapBuilder::new(HashMap::new());
836
837        assert_eq!(
838            builder.record_flat_member_with_flat_name("a__b", "c", "agentic_ns__a__b__c".to_owned()),
839            "agentic_ns__a__b__c"
840        );
841        let _ = builder.record_flat_member_with_flat_name("a", "b__c", "agentic_ns__a__b__c".to_owned());
842    }
843
844    #[test]
845    fn flat_namespace_member_call_preserves_tools_argument() {
846        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
847            {
848                "type": "namespace",
849                "name": "mcp__agentic_fixture",
850                "tools": [{"type": "function", "name": "run"}]
851            }
852        ]))
853        .unwrap();
854        let mut output = vec![completed_call(
855            "agentic_ns__mcp__agentic_fixture__run",
856            "{\"tools\":\"legitimate\",\"cmd\":\"pwd\"}",
857        )];
858
859        let map = CodexNamespaceHandler
860            .build_namespace_map(Some(&tools))
861            .expect("valid namespace map");
862        CodexNamespaceHandler.restore_output_items(&mut output, map.as_ref());
863
864        let OutputItem::FunctionCall(call) = &output[0] else {
865            panic!("expected function call");
866        };
867        assert_eq!(call.namespace.as_deref(), Some("mcp__agentic_fixture"));
868        assert_eq!(call.name, "run");
869        assert_eq!(call.arguments, "{\"tools\":\"legitimate\",\"cmd\":\"pwd\"}");
870    }
871
872    #[test]
873    fn plain_function_call_round_trip() {
874        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
875            {
876                "type": "function",
877                "name": "get_weather",
878                "parameters": {"type": "object"}
879            }
880        ]))
881        .unwrap();
882        let resolved = CodexNamespaceHandler
883            .resolve_namespace_members(&tools)
884            .expect("valid namespace members");
885        let mut output = vec![completed_call("get_weather", "{\"city\":\"SF\"}")];
886
887        let map = CodexNamespaceHandler
888            .build_namespace_map(Some(&tools))
889            .expect("valid namespace map");
890        CodexNamespaceHandler.restore_output_items(&mut output, map.as_ref());
891
892        assert!(matches!(
893            resolved.as_slice(),
894            [ResponsesTool::Function(function)] if function.name.as_str() == "get_weather"
895        ));
896        let OutputItem::FunctionCall(call) = &output[0] else {
897            panic!("expected function call");
898        };
899        assert!(call.namespace.is_none());
900        assert_eq!(call.name, "get_weather");
901        assert_eq!(call.arguments, "{\"city\":\"SF\"}");
902    }
903
904    #[test]
905    fn response_value_normalizes_nested_function_call_item() {
906        let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
907            {
908                "type": "namespace",
909                "name": "mcp__agentic_fixture",
910                "tools": [{"type": "function", "name": "add_numbers"}]
911            }
912        ]))
913        .unwrap();
914        let mut value = serde_json::json!({
915            "type": "response.output_item.done",
916            "item": {
917                "type": "function_call",
918                "name": "agentic_ns__mcp__agentic_fixture__add_numbers",
919                "call_id": "call_1",
920                "arguments": "{\"numbers\":[8,0]}"
921            }
922        });
923
924        let map = CodexNamespaceHandler
925            .build_namespace_map(Some(&tools))
926            .expect("valid namespace map");
927        assert!(CodexNamespaceHandler.restore_response_value(&mut value, map.as_ref()));
928        assert_eq!(value["item"]["namespace"], "mcp__agentic_fixture");
929        assert_eq!(value["item"]["name"], "add_numbers");
930        assert_eq!(value["item"]["arguments"], "{\"numbers\":[8,0]}");
931    }
932}