1use std::collections::{HashMap, HashSet};
2
3use serde_json::Value;
4
5use crate::types::io::{FunctionTool, FunctionToolCall, OutputItem, ToolChoice};
6use crate::types::tools::{CodexNamespaceMember, CodexNamespaceToolParam, NonEmptyToolName, ResponsesTool};
7
8use super::handler::{ToolError, ToolHandler};
9use super::registry::ToolType;
10
11pub const MODEL_VISIBLE_NAMESPACE_MEMBER_PREFIX: &str = "agentic_ns__";
16
17#[must_use]
18pub fn model_visible_namespace_member_name(namespace: &str, member: &str) -> String {
19 format!("{MODEL_VISIBLE_NAMESPACE_MEMBER_PREFIX}{namespace}__{member}")
20}
21
22#[derive(Clone, Debug, Eq, Hash, PartialEq)]
23struct NamespaceMemberName {
24 namespace: String,
25 name: String,
26}
27
28#[derive(Clone, Debug)]
29struct NamespaceCallMapping {
30 member: NamespaceMemberName,
31 upstream_name: String,
32}
33
34#[derive(Clone, Debug, Default)]
43pub struct NamespaceMap {
44 calls: HashMap<String, NamespaceCallMapping>,
45 members: HashMap<NamespaceMemberName, String>,
46}
47
48impl NamespaceMap {
49 fn mapping_for_call(&self, name: &str) -> Option<&NamespaceCallMapping> {
50 self.calls.get(name)
51 }
52
53 fn mapping_for_member(&self, namespace: &str, name: &str) -> Option<&NamespaceCallMapping> {
54 let member = NamespaceMemberName {
55 namespace: namespace.to_string(),
56 name: name.to_string(),
57 };
58 self.members
59 .get(&member)
60 .and_then(|upstream_name| self.calls.get(upstream_name))
61 }
62}
63
64#[derive(Default)]
65struct NamespaceMapBuilder {
66 top_level_names: HashSet<String>,
67 map: NamespaceMap,
68}
69
70impl NamespaceMapBuilder {
71 fn new(top_level_names: HashSet<String>) -> Self {
72 Self {
73 top_level_names,
74 ..Self::default()
75 }
76 }
77
78 fn validate_and_record_flat_member(
79 &mut self,
80 namespace_name: &str,
81 member_name: &str,
82 ) -> Result<String, ToolError> {
83 let flat_name = model_visible_namespace_member_name(namespace_name, member_name);
84 if self.top_level_names.contains(&flat_name) {
85 return Err(ToolError::Config(format!(
86 "codex namespace member {namespace_name}.{member_name} collides with top-level function {flat_name}"
87 )));
88 }
89 if let Some(existing) = self.map.calls.get(&flat_name) {
90 if existing.member.namespace != namespace_name || existing.member.name != member_name {
91 return Err(ToolError::Config(format!(
92 "codex namespace member {namespace_name}.{member_name} collides with {}.{} at generated name {flat_name}",
93 existing.member.namespace, existing.member.name
94 )));
95 }
96 }
97 Ok(self.record_flat_member_with_flat_name(namespace_name, member_name, flat_name))
98 }
99
100 fn record_flat_member_with_flat_name(
101 &mut self,
102 namespace_name: &str,
103 member_name: &str,
104 flat_name: String,
105 ) -> String {
106 let member = NamespaceMemberName {
107 namespace: namespace_name.to_string(),
108 name: member_name.to_string(),
109 };
110 if let Some(existing) = self.map.calls.get(&flat_name) {
111 debug_assert!(
112 existing.member == member,
113 "namespace collisions must be validated before recording namespace members"
114 );
115 if existing.member != member {
116 tracing::warn!(
117 upstream_name = %flat_name,
118 namespace = %namespace_name,
119 member = %member_name,
120 existing_namespace = %existing.member.namespace,
121 existing_member = %existing.member.name,
122 "generated codex namespace member name collides with another namespace member"
123 );
124 }
125 }
126 let mapping = NamespaceCallMapping {
127 member: member.clone(),
128 upstream_name: flat_name.clone(),
129 };
130
131 self.map.members.insert(member, flat_name.clone());
132 self.map.calls.insert(flat_name.clone(), mapping);
133 flat_name
134 }
135
136 fn finish(self) -> NamespaceMap {
137 self.map
138 }
139}
140
141#[derive(Debug)]
147pub struct CodexNamespaceHandler;
148
149impl CodexNamespaceHandler {
150 pub fn resolve_namespace_members(&self, tools: &[ResponsesTool]) -> Result<Vec<ResponsesTool>, ToolError> {
168 let mut builder = NamespaceMapBuilder::new(typed_top_level_tool_names(tools));
169 tools
170 .iter()
171 .map(|tool| match tool {
172 ResponsesTool::Namespace(namespace) => {
173 rename_namespace_members(namespace, &mut builder).map(ResponsesTool::Namespace)
174 }
175 other => Ok(other.clone()),
176 })
177 .collect()
178 }
179
180 pub fn build_namespace_map(&self, tools: Option<&[ResponsesTool]>) -> Result<Option<NamespaceMap>, ToolError> {
189 namespace_map_from_tools(tools)
190 }
191
192 pub fn validate_namespace_collisions(&self, tools: Option<&[ResponsesTool]>) -> Result<(), ToolError> {
205 let Some(tools) = tools else {
206 return Ok(());
207 };
208 let mut builder = NamespaceMapBuilder::new(typed_top_level_tool_names(tools));
209 for tool in tools {
210 let ResponsesTool::Namespace(namespace) = tool else {
211 continue;
212 };
213 for member_name in typed_function_member_names(namespace) {
214 builder.validate_and_record_flat_member(&namespace.name, &member_name)?;
215 }
216 }
217 Ok(())
218 }
219
220 #[must_use]
230 pub fn resolve_tool_choice(&self, map: Option<&NamespaceMap>, tool_choice: Option<&ToolChoice>) -> ToolChoice {
231 let tool_choice = tool_choice.unwrap_or(&ToolChoice::Auto);
232 let Some(map) = map else {
233 return tool_choice.clone();
234 };
235 rewrite_tool_choice_with_map(tool_choice, map)
236 }
237
238 pub fn restore_output_items(&self, output: &mut [OutputItem], map: Option<&NamespaceMap>) {
239 let Some(map) = map else {
240 return;
241 };
242 for item in output {
243 if let OutputItem::FunctionCall(call) = item {
244 restore_function_call_with_map(call, map);
245 }
246 }
247 }
248
249 #[must_use]
250 pub fn restore_response_value(&self, value: &mut Value, map: Option<&NamespaceMap>) -> bool {
251 let Some(map) = map else {
252 return false;
253 };
254 restore_response_value_with_map(value, map)
255 }
256}
257
258impl ToolHandler for CodexNamespaceHandler {
259 fn tool_type(&self) -> ToolType {
260 ToolType::CodexNamespace
261 }
262
263 fn validate(&self, param: &Value) -> Result<(), ToolError> {
264 serde_json::from_value::<CodexNamespaceToolParam>(param.clone())
265 .map(|_| ())
266 .map_err(|e| ToolError::Config(format!("invalid codex namespace tool config: {e}")))
267 }
268
269 fn normalize(&self, param: &Value) -> Vec<FunctionTool> {
274 let Ok(namespace) = serde_json::from_value::<CodexNamespaceToolParam>(param.clone()) else {
275 tracing::warn!("normalize() called with invalid codex namespace param - validate() must be called first");
276 return vec![];
277 };
278 namespace
279 .tools
280 .iter()
281 .filter_map(|member| match member {
282 CodexNamespaceMember::Function(function) => Some(FunctionTool::from(function)),
283 CodexNamespaceMember::Unknown => None,
284 })
285 .collect()
286 }
287}
288
289fn namespace_map_from_tools(tools: Option<&[ResponsesTool]>) -> Result<Option<NamespaceMap>, ToolError> {
290 let Some(tools) = tools else {
291 return Ok(None);
292 };
293 let mut builder = NamespaceMapBuilder::new(typed_top_level_tool_names(tools));
294 for tool in tools {
295 if let ResponsesTool::Namespace(namespace) = tool {
296 let _ = rename_namespace_members(namespace, &mut builder)?;
297 }
298 }
299 Ok(Some(builder.finish()))
300}
301
302fn rename_namespace_members(
311 namespace: &CodexNamespaceToolParam,
312 builder: &mut NamespaceMapBuilder,
313) -> Result<CodexNamespaceToolParam, ToolError> {
314 let function_member_names = typed_function_member_names(namespace);
315 if function_member_names.is_empty() {
316 tracing::debug!(
317 namespace = %namespace.name,
318 "namespace tool has no function members to rename for upstream"
319 );
320 return Ok(namespace.clone());
321 }
322 let tools = namespace
323 .tools
324 .iter()
325 .map(|member| {
326 let CodexNamespaceMember::Function(function) = member else {
327 return Ok(member.clone());
328 };
329 let flat_name_text = builder.validate_and_record_flat_member(&namespace.name, function.name.as_str())?;
330 let flat_name = NonEmptyToolName::try_from(flat_name_text.clone())
331 .expect("generated namespace member names include a non-empty prefix");
332 tracing::debug!(
333 namespace = %namespace.name,
334 member = %function.name.as_str(),
335 upstream_name = %flat_name_text,
336 "renamed namespace tool member for upstream"
337 );
338 let mut function = function.clone();
339 function.name = flat_name;
340 Ok(CodexNamespaceMember::Function(function))
341 })
342 .collect::<Result<Vec<_>, ToolError>>()?;
343
344 Ok(CodexNamespaceToolParam {
345 tools,
346 ..namespace.clone()
347 })
348}
349
350fn typed_top_level_tool_names(tools: &[ResponsesTool]) -> HashSet<String> {
351 tools
352 .iter()
353 .filter_map(|tool| match tool {
354 ResponsesTool::Function(function) => Some(function.name.as_str().to_string()),
355 ResponsesTool::Mcp(_)
356 | ResponsesTool::WebSearch(_)
357 | ResponsesTool::FileSearch(_)
358 | ResponsesTool::CodeInterpreter(_)
359 | ResponsesTool::Namespace(_)
360 | ResponsesTool::Unknown => None,
361 })
362 .collect()
363}
364
365fn typed_function_member_names(namespace: &CodexNamespaceToolParam) -> Vec<String> {
366 namespace
367 .tools
368 .iter()
369 .filter_map(|member| match member {
370 CodexNamespaceMember::Function(function) => Some(function.name.as_str().to_string()),
371 CodexNamespaceMember::Unknown => None,
372 })
373 .collect()
374}
375
376fn restore_function_call_with_map(call: &mut FunctionToolCall, map: &NamespaceMap) -> bool {
377 if call.namespace.is_some() {
378 return false;
379 }
380 let Some(mapping) = map.mapping_for_call(&call.name) else {
381 return false;
382 };
383 let original_name = call.name.clone();
384
385 call.namespace = Some(mapping.member.namespace.clone());
386 call.name.clone_from(&mapping.member.name);
387 tracing::debug!(
388 upstream_name = %original_name,
389 namespace = %mapping.member.namespace,
390 member = %mapping.member.name,
391 "restored upstream namespace function call"
392 );
393 true
394}
395
396fn rewrite_tool_choice_with_map(choice: &ToolChoice, map: &NamespaceMap) -> ToolChoice {
397 let ToolChoice::Function { namespace, name } = choice else {
398 return choice.clone();
399 };
400 let mapping = namespace
401 .as_deref()
402 .and_then(|namespace| map.mapping_for_member(namespace, name.as_str()))
403 .or_else(|| {
404 namespace
405 .is_none()
406 .then(|| map.mapping_for_call(name.as_str()))
407 .flatten()
408 });
409 let Some(mapping) = mapping else {
410 return choice.clone();
411 };
412 let Ok(name) = NonEmptyToolName::try_from(mapping.upstream_name.clone()) else {
413 return choice.clone();
414 };
415
416 ToolChoice::Function { namespace: None, name }
417}
418
419fn restore_response_value_with_map(value: &mut Value, map: &NamespaceMap) -> bool {
420 let mut changed = false;
421
422 if let Some(item) = value.as_object_mut().and_then(|object| object.get_mut("item")) {
423 changed |= restore_call_value_with_map(item, map);
424 }
425
426 changed |= restore_call_value_with_map(value, map);
427
428 for key in ["response", "payload"] {
429 if let Some(nested) = value.as_object_mut().and_then(|object| object.get_mut(key)) {
430 changed |= restore_response_value_with_map(nested, map);
431 }
432 }
433
434 if let Some(Value::Array(items)) = value.as_object_mut().and_then(|object| object.get_mut("output")) {
435 for item in items {
436 changed |= restore_call_value_with_map(item, map);
437 }
438 }
439
440 changed
441}
442
443fn restore_call_value_with_map(value: &mut Value, map: &NamespaceMap) -> bool {
444 let Some(object) = value.as_object_mut() else {
445 return false;
446 };
447 if object.get("type").and_then(Value::as_str) != Some("function_call") {
448 return false;
449 }
450 if object.get("namespace").and_then(Value::as_str).is_some() {
451 return false;
452 }
453 let Some(name) = object.get("name").and_then(Value::as_str) else {
454 return false;
455 };
456 let Some(mapping) = map.mapping_for_call(name) else {
457 return false;
458 };
459 let original_name = name.to_string();
460
461 object.insert("namespace".to_string(), Value::String(mapping.member.namespace.clone()));
462 object.insert("name".to_string(), Value::String(mapping.member.name.clone()));
463 tracing::debug!(
464 upstream_name = %original_name,
465 namespace = %mapping.member.namespace,
466 member = %mapping.member.name,
467 "restored upstream namespace function call"
468 );
469 true
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475 use crate::types::event::MessageStatus;
476
477 fn completed_call(name: &str, arguments: &str) -> OutputItem {
478 OutputItem::FunctionCall(FunctionToolCall {
479 id: "fc_1".to_string(),
480 call_id: "call_1".to_string(),
481 name: name.to_string(),
482 namespace: None,
483 arguments: arguments.to_string(),
484 status: MessageStatus::Completed,
485 })
486 }
487
488 #[test]
489 fn unqualified_function_tool_choice_is_not_rewritten_to_namespace_member() {
490 let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
491 {
492 "type": "namespace",
493 "name": "mcp__shell",
494 "tools": [{"type": "function", "name": "run"}]
495 }
496 ]))
497 .unwrap();
498 let choice = ToolChoice::Function {
499 namespace: None,
500 name: NonEmptyToolName::try_from("run").unwrap(),
501 };
502
503 let map = CodexNamespaceHandler
504 .build_namespace_map(Some(&tools))
505 .expect("valid namespace map");
506 let rewritten = CodexNamespaceHandler.resolve_tool_choice(map.as_ref(), Some(&choice));
507
508 assert_eq!(
509 rewritten,
510 ToolChoice::Function {
511 namespace: None,
512 name: NonEmptyToolName::try_from("run").unwrap()
513 }
514 );
515 let resolved = CodexNamespaceHandler
516 .resolve_namespace_members(&tools)
517 .expect("valid namespace members");
518 assert!(matches!(
519 resolved.as_slice(),
520 [ResponsesTool::Namespace(namespace)]
521 if matches!(&namespace.tools[0], CodexNamespaceMember::Function(f) if f.name.as_str() == "agentic_ns__mcp__shell__run")
522 ));
523 }
524
525 #[test]
526 fn namespaced_function_tool_choice_flattens_exact_member() {
527 let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
528 {
529 "type": "namespace",
530 "name": "mcp__shell",
531 "tools": [{"type": "function", "name": "run"}]
532 },
533 {
534 "type": "namespace",
535 "name": "mcp__git",
536 "tools": [{"type": "function", "name": "run"}]
537 }
538 ]))
539 .unwrap();
540 let choice: ToolChoice = serde_json::from_value(serde_json::json!({
541 "type": "function",
542 "namespace": "mcp__git",
543 "name": "run"
544 }))
545 .unwrap();
546
547 let map = CodexNamespaceHandler
548 .build_namespace_map(Some(&tools))
549 .expect("valid namespace map");
550 let rewritten = CodexNamespaceHandler.resolve_tool_choice(map.as_ref(), Some(&choice));
551
552 assert_eq!(
553 rewritten,
554 ToolChoice::Function {
555 namespace: None,
556 name: NonEmptyToolName::try_from("agentic_ns__mcp__git__run").unwrap()
557 }
558 );
559 }
560
561 #[test]
562 fn validate_namespace_collisions_rejects_top_level_flat_name_collision() {
563 let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
564 {"type": "function", "name": "agentic_ns__mcp__shell__run"},
565 {
566 "type": "namespace",
567 "name": "mcp__shell",
568 "tools": [{"type": "function", "name": "run"}]
569 }
570 ]))
571 .unwrap();
572
573 let err = CodexNamespaceHandler
574 .validate_namespace_collisions(Some(&tools))
575 .unwrap_err();
576
577 assert!(err.to_string().contains("collides with top-level function"));
578 }
579
580 #[test]
581 fn resolve_namespace_members_rejects_top_level_flat_name_collision() {
582 let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
583 {"type": "function", "name": "agentic_ns__mcp__shell__run"},
584 {
585 "type": "namespace",
586 "name": "mcp__shell",
587 "tools": [{"type": "function", "name": "run"}]
588 }
589 ]))
590 .unwrap();
591
592 let err = CodexNamespaceHandler.resolve_namespace_members(&tools).unwrap_err();
593
594 assert!(err.to_string().contains("collides with top-level function"));
595 }
596
597 #[test]
598 fn validate_namespace_collisions_rejects_generated_name_collision_between_namespace_members() {
599 let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
600 {
601 "type": "namespace",
602 "name": "a__b",
603 "tools": [{"type": "function", "name": "c"}]
604 },
605 {
606 "type": "namespace",
607 "name": "a",
608 "tools": [{"type": "function", "name": "b__c"}]
609 }
610 ]))
611 .unwrap();
612
613 let err = CodexNamespaceHandler
614 .validate_namespace_collisions(Some(&tools))
615 .unwrap_err();
616
617 assert!(err.to_string().contains("generated name"));
618 }
619
620 #[test]
621 #[cfg(debug_assertions)]
622 #[should_panic(expected = "namespace collisions must be validated before recording namespace members")]
623 fn namespace_map_builder_debug_asserts_when_member_collision_validation_is_skipped() {
624 let mut builder = NamespaceMapBuilder::new(HashSet::new());
625
626 assert_eq!(
627 builder.record_flat_member_with_flat_name("a__b", "c", "agentic_ns__a__b__c".to_owned()),
628 "agentic_ns__a__b__c"
629 );
630 let _ = builder.record_flat_member_with_flat_name("a", "b__c", "agentic_ns__a__b__c".to_owned());
631 }
632
633 #[test]
634 fn flat_namespace_member_call_preserves_tools_argument() {
635 let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
636 {
637 "type": "namespace",
638 "name": "mcp__agentic_fixture",
639 "tools": [{"type": "function", "name": "run"}]
640 }
641 ]))
642 .unwrap();
643 let mut output = vec![completed_call(
644 "agentic_ns__mcp__agentic_fixture__run",
645 "{\"tools\":\"legitimate\",\"cmd\":\"pwd\"}",
646 )];
647
648 let map = CodexNamespaceHandler
649 .build_namespace_map(Some(&tools))
650 .expect("valid namespace map");
651 CodexNamespaceHandler.restore_output_items(&mut output, map.as_ref());
652
653 let OutputItem::FunctionCall(call) = &output[0] else {
654 panic!("expected function call");
655 };
656 assert_eq!(call.namespace.as_deref(), Some("mcp__agentic_fixture"));
657 assert_eq!(call.name, "run");
658 assert_eq!(call.arguments, "{\"tools\":\"legitimate\",\"cmd\":\"pwd\"}");
659 }
660
661 #[test]
662 fn plain_function_call_round_trip() {
663 let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
664 {
665 "type": "function",
666 "name": "get_weather",
667 "parameters": {"type": "object"}
668 }
669 ]))
670 .unwrap();
671 let resolved = CodexNamespaceHandler
672 .resolve_namespace_members(&tools)
673 .expect("valid namespace members");
674 let mut output = vec![completed_call("get_weather", "{\"city\":\"SF\"}")];
675
676 let map = CodexNamespaceHandler
677 .build_namespace_map(Some(&tools))
678 .expect("valid namespace map");
679 CodexNamespaceHandler.restore_output_items(&mut output, map.as_ref());
680
681 assert!(matches!(
682 resolved.as_slice(),
683 [ResponsesTool::Function(function)] if function.name.as_str() == "get_weather"
684 ));
685 let OutputItem::FunctionCall(call) = &output[0] else {
686 panic!("expected function call");
687 };
688 assert!(call.namespace.is_none());
689 assert_eq!(call.name, "get_weather");
690 assert_eq!(call.arguments, "{\"city\":\"SF\"}");
691 }
692
693 #[test]
694 fn response_value_normalizes_nested_function_call_item() {
695 let tools: Vec<ResponsesTool> = serde_json::from_value(serde_json::json!([
696 {
697 "type": "namespace",
698 "name": "mcp__agentic_fixture",
699 "tools": [{"type": "function", "name": "add_numbers"}]
700 }
701 ]))
702 .unwrap();
703 let mut value = serde_json::json!({
704 "type": "response.output_item.done",
705 "item": {
706 "type": "function_call",
707 "name": "agentic_ns__mcp__agentic_fixture__add_numbers",
708 "call_id": "call_1",
709 "arguments": "{\"numbers\":[8,0]}"
710 }
711 });
712
713 let map = CodexNamespaceHandler
714 .build_namespace_map(Some(&tools))
715 .expect("valid namespace map");
716 assert!(CodexNamespaceHandler.restore_response_value(&mut value, map.as_ref()));
717 assert_eq!(value["item"]["namespace"], "mcp__agentic_fixture");
718 assert_eq!(value["item"]["name"], "add_numbers");
719 assert_eq!(value["item"]["arguments"], "{\"numbers\":[8,0]}");
720 }
721}