use std::fmt;
use std::sync::Arc;
use serde_json::Value;
use thiserror::Error;
use typesec_core::GlobPattern;
use crate::tool::ToolSpec;
#[derive(Clone)]
pub(crate) struct ArgsSchema {
schema: Value,
validator: Arc<jsonschema::Validator>,
}
impl fmt::Debug for ArgsSchema {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ArgsSchema")
.field("schema", &self.schema)
.finish_non_exhaustive()
}
}
#[derive(Debug, Error)]
pub enum InteropError {
#[error("malformed {dialect} tool-call payload: {detail}")]
Malformed {
dialect: &'static str,
detail: String,
},
}
impl InteropError {
pub(crate) fn malformed(dialect: &'static str, detail: impl Into<String>) -> Self {
Self::Malformed {
dialect,
detail: detail.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolCallRequest {
pub call_id: Option<String>,
pub tool_name: String,
pub arguments: Value,
}
impl ToolCallRequest {
pub fn new(tool_name: impl Into<String>, arguments: Value) -> Self {
Self {
call_id: None,
tool_name: tool_name.into(),
arguments,
}
}
#[must_use]
pub fn with_call_id(mut self, call_id: impl Into<String>) -> Self {
self.call_id = Some(call_id.into());
self
}
}
#[derive(Debug, Clone)]
pub struct ToolBinding {
pub tool_name: String,
pub action: String,
pub resource: String,
pub resource_arg: Option<String>,
pub required_args: Vec<String>,
arg_globs: Vec<(String, GlobPattern)>,
args_schema: Option<ArgsSchema>,
}
impl ToolBinding {
pub fn new(
tool_name: impl Into<String>,
action: impl Into<String>,
resource: impl Into<String>,
) -> Self {
Self {
tool_name: tool_name.into(),
action: action.into(),
resource: resource.into(),
resource_arg: None,
required_args: Vec::new(),
arg_globs: Vec::new(),
args_schema: None,
}
}
#[must_use]
pub fn resource_from_arg(mut self, arg: impl Into<String>) -> Self {
self.resource_arg = Some(arg.into());
self
}
#[must_use]
pub fn require_args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.required_args.extend(args.into_iter().map(Into::into));
self
}
pub fn arg_glob(mut self, arg: impl Into<String>, pattern: &str) -> Result<Self, InteropError> {
let arg = arg.into();
let compiled =
GlobPattern::compile(pattern, "argument").map_err(|err| InteropError::Malformed {
dialect: "binding",
detail: err,
})?;
self.arg_globs.push((arg, compiled));
Ok(self)
}
pub fn args_schema(mut self, schema: Value) -> Result<Self, InteropError> {
let validator =
jsonschema::validator_for(&schema).map_err(|err| InteropError::Malformed {
dialect: "binding",
detail: format!("invalid args schema: {err}"),
})?;
self.args_schema = Some(ArgsSchema {
schema,
validator: Arc::new(validator),
});
Ok(self)
}
pub(crate) fn validate_arguments(&self, arguments: &Value) -> Result<(), String> {
if let Some(args_schema) = &self.args_schema
&& let Err(err) = args_schema.validator.validate(arguments)
{
return Err(format!(
"tool '{}' arguments failed schema validation: {err}",
self.tool_name
));
}
for required in &self.required_args {
if arguments.get(required).is_none() {
return Err(format!(
"tool '{}' requires argument '{required}'",
self.tool_name
));
}
}
for (arg, glob) in &self.arg_globs {
let Some(value) = arguments.get(arg).and_then(Value::as_str) else {
return Err(format!(
"tool '{}' requires string argument '{arg}' matching its declared pattern",
self.tool_name
));
};
if !glob.matches(value) {
return Err(format!(
"tool '{}' argument '{arg}' value '{value}' does not match the allowed pattern",
self.tool_name
));
}
}
Ok(())
}
pub fn from_spec(spec: &ToolSpec) -> Self {
Self::new(&spec.name, spec.required_permission, &spec.resource_id)
}
pub(crate) fn resolve_resource(&self, arguments: &Value) -> Result<String, String> {
match &self.resource_arg {
None => Ok(self.resource.clone()),
Some(arg) => arguments
.get(arg)
.and_then(Value::as_str)
.map(str::to_owned)
.ok_or_else(|| {
format!(
"tool '{}' requires string argument '{arg}' to name the resource",
self.tool_name
)
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolCallVerdict {
Allow,
Deny {
reason: String,
},
Delegate {
reason: String,
},
}
impl ToolCallVerdict {
pub fn is_allowed(&self) -> bool {
matches!(self, Self::Allow)
}
pub fn reason(&self) -> Option<&str> {
match self {
Self::Allow => None,
Self::Deny { reason } | Self::Delegate { reason } => Some(reason),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GuardedToolCall {
pub request: ToolCallRequest,
pub action: Option<String>,
pub resource: Option<String>,
pub verdict: ToolCallVerdict,
}
impl GuardedToolCall {
pub fn denial_message(&self) -> Option<String> {
match &self.verdict {
ToolCallVerdict::Allow => None,
ToolCallVerdict::Deny { reason } => Some(format!(
"Tool call '{}' was denied by security policy: {reason}",
self.request.tool_name
)),
ToolCallVerdict::Delegate { reason } => Some(format!(
"Tool call '{}' was not authorized (no policy engine decided): {reason}",
self.request.tool_name
)),
}
}
}