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
13pub 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
44pub(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#[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#[derive(Debug)]
204pub struct CodexNamespaceHandler;
205
206impl CodexNamespaceHandler {
207 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 pub fn build_namespace_map(&self, tools: Option<&[ResponsesTool]>) -> Result<Option<NamespaceMap>, ToolError> {
246 namespace_map_from_tools(tools)
247 }
248
249 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 #[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 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
367fn 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}