diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index 4be03aa..b4d62a5 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -330,7 +330,6 @@ fn parse_tool_calls_from_json_value(value: &serde_json::Value) -> Vec", "", ""]; -const TOOL_CALL_CLOSE_TAGS: [&str; 3] = ["", "", ""]; fn find_first_tag<'a>(haystack: &str, tags: &'a [&'a str]) -> Option<(usize, &'a str)> { tags.iter() @@ -338,6 +337,15 @@ fn find_first_tag<'a>(haystack: &str, tags: &'a [&'a str]) -> Option<(usize, &'a .min_by_key(|(idx, _)| *idx) } +fn matching_tool_call_close_tag(open_tag: &str) -> Option<&'static str> { + match open_tag { + "" => Some(""), + "" => Some(""), + "" => Some(""), + _ => None, + } +} + /// Extract JSON values from a string. /// /// # Security Warning @@ -426,8 +434,12 @@ fn parse_tool_calls(response: &str) -> (String, Vec) { text_parts.push(before.trim().to_string()); } + let Some(close_tag) = matching_tool_call_close_tag(open_tag) else { + break; + }; + let after_open = &remaining[start + open_tag.len()..]; - if let Some((close_idx, close_tag)) = find_first_tag(after_open, &TOOL_CALL_CLOSE_TAGS) { + if let Some(close_idx) = after_open.find(close_tag) { let inner = &after_open[..close_idx]; let mut parsed_any = false; let json_values = extract_json_values(inner); @@ -454,7 +466,7 @@ fn parse_tool_calls(response: &str) -> (String, Vec) { // (e.g., in emails, files, or web pages) could include JSON that mimics a // tool call. Tool calls MUST be explicitly wrapped in either: // 1. OpenAI-style JSON with a "tool_calls" array - // 2. ZeroClaw ... tags + // 2. ZeroClaw tool-call tags (, , ) // This ensures only the LLM's intentional tool calls are executed. // Remaining text after last tool call @@ -1541,6 +1553,18 @@ I will now call the tool with this payload: ); } + #[test] + fn parse_tool_calls_does_not_cross_match_alias_tags() { + let response = r#" +{"name": "shell", "arguments": {"command": "date"}} +"#; + + let (text, calls) = parse_tool_calls(response); + assert!(calls.is_empty()); + assert!(text.contains("")); + assert!(text.contains("")); + } + #[test] fn parse_tool_calls_rejects_raw_tool_json_without_tags() { // SECURITY: Raw JSON without explicit wrappers should NOT be parsed