capsule_runtime/
tool_names.rs1use std::collections::HashMap;
11
12use murmur_artifact::tool_name_of;
13
14use crate::errors::RuntimeError;
15use crate::types::StageRequest;
16
17pub fn check_unique_tool_names<'a, I>(names: I) -> Result<(), RuntimeError>
23where
24 I: IntoIterator<Item = &'a str>,
25{
26 let mut seen: HashMap<&str, &str> = HashMap::new();
27 for name in names {
28 let tool = tool_name_of(name);
29 if let Some(first) = seen.insert(tool, name) {
30 return Err(RuntimeError::DuplicateToolName {
31 first: first.to_string(),
32 second: name.to_string(),
33 tool: tool.to_string(),
34 });
35 }
36 }
37 Ok(())
38}
39
40pub(crate) fn localize_artifact_names(request: &mut StageRequest) -> HashMap<String, String> {
45 let mut full_names = HashMap::with_capacity(request.artifacts.len());
46 for artifact in &mut request.artifacts {
47 let tool = tool_name_of(&artifact.name).to_string();
48 full_names.insert(tool.clone(), std::mem::replace(&mut artifact.name, tool));
49 }
50 request.allowlisted_tools = request
51 .allowlisted_tools
52 .iter()
53 .map(|name| tool_name_of(name).to_string())
54 .collect();
55 if let Some(inference) = request.inference.as_mut() {
56 if let Some(driver) = inference.driver.as_mut() {
57 driver.artifact = tool_name_of(&driver.artifact).to_string();
58 }
59 for alternate in &mut inference.alternates {
60 alternate.driver = tool_name_of(&alternate.driver).to_string();
61 }
62 if let Some(artifact) = inference.system_prompt_artifact.as_mut() {
63 *artifact = tool_name_of(artifact).to_string();
64 }
65 }
66 full_names
67}
68
69pub fn spawn_target(spawn_allow: &[String], capsule: &str) -> String {
73 spawn_allow
74 .iter()
75 .find(|entry| tool_name_of(entry) == capsule)
76 .cloned()
77 .unwrap_or_else(|| capsule.to_string())
78}
79
80pub(crate) fn full_name_of<'a>(full_names: &'a HashMap<String, String>, tool: &'a str) -> &'a str {
83 full_names.get(tool).map_or(tool, String::as_str)
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 #[test]
91 fn unique_tool_names_refuses_two_scopes_with_one_name() {
92 let error = check_unique_tool_names(["murmur/tool-editor", "ana/tool-editor"]).unwrap_err();
93 let RuntimeError::DuplicateToolName {
94 first,
95 second,
96 tool,
97 } = &error
98 else {
99 panic!("expected DuplicateToolName, got {error:?}");
100 };
101 assert_eq!(first, "murmur/tool-editor");
102 assert_eq!(second, "ana/tool-editor");
103 assert_eq!(tool, "tool-editor");
104 let message = error.to_string();
105 for part in ["murmur/tool-editor", "ana/tool-editor", "'tool-editor'"] {
106 assert!(message.contains(part), "{message}");
107 }
108 }
109
110 #[test]
111 fn unique_tool_names_refuses_a_source_skill_beside_a_resolved_artifact() {
112 let error = check_unique_tool_names(["tool-editor", "murmur/tool-editor"]).unwrap_err();
113 assert!(matches!(error, RuntimeError::DuplicateToolName { .. }));
114 }
115
116 #[test]
117 fn delegate_task_spawn_target_maps_a_bare_name_to_its_entry() {
118 let allow = vec!["local/worker".to_string(), "ana/reviewer".to_string()];
119 assert_eq!(spawn_target(&allow, "worker"), "local/worker");
120 assert_eq!(spawn_target(&allow, "reviewer"), "ana/reviewer");
121 assert_eq!(spawn_target(&allow, "stranger"), "stranger");
122 }
123
124 #[test]
125 fn unique_tool_names_accepts_distinct_tool_names() {
126 check_unique_tool_names(["murmur/tool-editor", "ana/tool-x", "local-skill"]).unwrap();
127 }
128}