use serde_json::Value;
use crate::associative_package::{default_package_store, PackagePermissionDecision};
use crate::engine::{SymbolicAnswer, ThinkingStep};
pub fn tool_call_refusal_answer() -> SymbolicAnswer {
SymbolicAnswer {
intent: String::from("tool_call_refused"),
answer: String::from(
"Tool calls and function execution are not allowed without explicit agent mode. \
Enable agent mode only for an isolated execution environment.",
),
execution_recipe: None,
confidence: 1.0,
evidence_links: vec![String::from("policy:agent_mode_required_for_tools")],
thinking_steps: policy_thinking_steps("agent_mode_required_for_tools"),
links_notation: String::from(
"tool_call_refusal\n policy \"agent_mode_required_for_tools\"\n thinking_step \"policy_refusal agent_mode_required_for_tools\"\n",
),
}
}
pub fn tool_permission_refusal_answer(decision: &PackagePermissionDecision) -> SymbolicAnswer {
let PackagePermissionDecision::Denied { capability, reason } = decision else {
return tool_call_refusal_answer();
};
SymbolicAnswer {
intent: String::from("tool_call_refused"),
answer: format!(
"Tool calls are not allowed for `{capability}`: {reason}. Install or import an \
associative package that grants this capability before enabling the tool."
),
execution_recipe: None,
confidence: 1.0,
evidence_links: vec![format!("policy:package_permission_required:{capability}")],
thinking_steps: policy_thinking_steps(format!("package_permission_required:{capability}")),
links_notation: format!(
"tool_call_refusal\n policy \"package_permission_required\"\n capability \"{capability}\"\n thinking_step \"policy_refusal package_permission_required:{capability}\"\n"
),
}
}
fn policy_thinking_steps(detail: impl Into<String>) -> Vec<ThinkingStep> {
vec![ThinkingStep::new(
0,
"policy_refusal",
detail,
"high",
"policy",
)]
}
#[must_use]
pub fn agentic_tool_permission_denial(names: &[String]) -> Option<PackagePermissionDecision> {
use crate::agentic_coding::planner::tool_capability;
let store = default_package_store();
if names.is_empty() {
let decision = store.permission_for_capability("tool:*");
return matches!(decision, PackagePermissionDecision::Denied { .. }).then_some(decision);
}
names.iter().find_map(|name| {
let capability = tool_capability(name)?;
let decision = store.permission_for_capability(capability.permission_key());
matches!(decision, PackagePermissionDecision::Denied { .. }).then_some(decision)
})
}
pub fn is_tool_choice_request(value: &Value) -> bool {
!matches_tool_choice_none(value)
}
pub fn tool_choice_function_name(value: &Value) -> Option<String> {
match value {
Value::Object(object) => object
.get("function")
.and_then(|function| function.get("name"))
.or_else(|| object.get("name"))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| hosted_tool_type_name(object).map(ToOwned::to_owned)),
_ => None,
}
}
pub fn tool_definition_name(value: &Value) -> Option<String> {
match value {
Value::Object(object) => object
.get("function")
.and_then(|function| function.get("name"))
.or_else(|| object.get("name"))
.and_then(Value::as_str)
.map(ToOwned::to_owned)
.or_else(|| hosted_tool_type_name(object).map(ToOwned::to_owned)),
_ => None,
}
}
pub fn tool_definition_names(value: &Value) -> Vec<String> {
let mut names = Vec::new();
append_qualified_tool_definition_names(value, None, &mut names);
names
}
pub fn find_tool_definition<'a>(definitions: &'a [Value], tool_name: &str) -> Option<&'a Value> {
definitions
.iter()
.find_map(|definition| find_qualified_tool_definition(definition, None, tool_name))
}
pub fn response_tool_call_identity(
definitions: &[Value],
tool_name: &str,
) -> (String, Option<String>) {
definitions
.iter()
.find_map(|definition| namespace_tool_call_identity(definition, None, tool_name))
.unwrap_or_else(|| (tool_name.to_owned(), None))
}
fn append_qualified_tool_definition_names(
value: &Value,
prefix: Option<&str>,
names: &mut Vec<String>,
) {
let Some(object) = value.as_object() else {
return;
};
if object.get("type").and_then(Value::as_str) == Some("namespace") {
let Some(namespace) = object.get("name").and_then(Value::as_str) else {
return;
};
let qualified_namespace = qualify_tool_name(prefix, namespace);
if let Some(children) = object.get("tools").and_then(Value::as_array) {
for child in children {
append_qualified_tool_definition_names(child, Some(&qualified_namespace), names);
}
}
return;
}
if let Some(name) = tool_definition_name(value) {
names.push(qualify_tool_name(prefix, &name));
}
}
fn find_qualified_tool_definition<'a>(
value: &'a Value,
prefix: Option<&str>,
tool_name: &str,
) -> Option<&'a Value> {
let object = value.as_object()?;
if object.get("type").and_then(Value::as_str) == Some("namespace") {
let namespace = object.get("name").and_then(Value::as_str)?;
let qualified_namespace = qualify_tool_name(prefix, namespace);
return object
.get("tools")
.and_then(Value::as_array)?
.iter()
.find_map(|child| {
find_qualified_tool_definition(child, Some(&qualified_namespace), tool_name)
});
}
let name = tool_definition_name(value)?;
(qualify_tool_name(prefix, &name) == tool_name).then_some(value)
}
fn namespace_tool_call_identity(
value: &Value,
prefix: Option<&str>,
tool_name: &str,
) -> Option<(String, Option<String>)> {
let object = value.as_object()?;
if object.get("type").and_then(Value::as_str) != Some("namespace") {
return None;
}
let namespace = object.get("name").and_then(Value::as_str)?;
let qualified_namespace = qualify_tool_name(prefix, namespace);
for child in object.get("tools").and_then(Value::as_array)? {
if let Some(identity) =
namespace_tool_call_identity(child, Some(&qualified_namespace), tool_name)
{
return Some(identity);
}
let Some(child_name) = tool_definition_name(child) else {
continue;
};
if qualify_tool_name(Some(&qualified_namespace), &child_name) == tool_name {
return Some((child_name, Some(qualified_namespace)));
}
}
None
}
fn qualify_tool_name(prefix: Option<&str>, name: &str) -> String {
prefix.map_or_else(
|| name.to_owned(),
|prefix| {
let separator = if prefix.ends_with("__") { "" } else { "__" };
if name.starts_with(&format!("{prefix}{separator}")) {
name.to_owned()
} else {
format!("{prefix}{separator}{name}")
}
},
)
}
fn hosted_tool_type_name(object: &serde_json::Map<String, Value>) -> Option<&'static str> {
match object.get("type").and_then(Value::as_str)? {
"web_search" | "web_search_preview" => Some("web_search"),
"file_search" => Some("file_search"),
"computer_use" | "computer_use_preview" => Some("computer_use"),
"code_interpreter" => Some("code_interpreter"),
kind if kind.starts_with("web_search_") => Some("web_search"),
_ => None,
}
}
#[must_use]
pub fn is_hosted_tool_definition(value: &Value, capability: &str) -> bool {
value
.as_object()
.and_then(hosted_tool_type_name)
.is_some_and(|name| name == capability)
}
pub fn matches_tool_choice_none(value: &Value) -> bool {
match value {
Value::Null => true,
Value::String(choice) => choice.eq_ignore_ascii_case("none"),
Value::Object(object) => object
.get("type")
.and_then(Value::as_str)
.is_some_and(|kind| kind.eq_ignore_ascii_case("none")),
_ => false,
}
}