use std::collections::HashMap;
use std::sync::Arc;
use typesec_core::ResourceId;
use typesec_core::policy::{PolicyEngine, PolicyResult, RequestContext, SubjectId};
use super::call::{GuardedToolCall, ToolBinding, ToolCallRequest, ToolCallVerdict};
use crate::tool::ToolRegistry;
pub struct ToolCallGuard {
engine: Arc<dyn PolicyEngine>,
bindings: HashMap<String, ToolBinding>,
}
impl ToolCallGuard {
pub fn new(engine: Arc<dyn PolicyEngine>) -> Self {
Self {
engine,
bindings: HashMap::new(),
}
}
#[must_use]
pub fn bind(mut self, binding: ToolBinding) -> Self {
self.bindings.insert(binding.tool_name.clone(), binding);
self
}
#[must_use]
pub fn bind_registry(mut self, registry: &ToolRegistry) -> Self {
for spec in registry.list_specs() {
let binding = ToolBinding::from_spec(&spec);
self.bindings.insert(binding.tool_name.clone(), binding);
}
self
}
pub fn binding(&self, tool_name: &str) -> Option<&ToolBinding> {
self.bindings.get(tool_name)
}
pub fn allows_listing(
&self,
subject: &SubjectId,
tool_name: &str,
ctx: &RequestContext,
) -> bool {
let Some(binding) = self.bindings.get(tool_name) else {
return false;
};
if binding.resource_arg.is_some() {
return true;
}
matches!(
self.engine.check_with_context(
subject,
&binding.action,
&ResourceId::from(binding.resource.as_str()),
ctx,
),
PolicyResult::Allow
)
}
pub fn check(
&self,
subject: &SubjectId,
request: ToolCallRequest,
ctx: &RequestContext,
) -> GuardedToolCall {
let (request, action, resource) = match self.resolve(request) {
Ok(bound) => bound,
Err(denied) => return denied,
};
let result = self.engine.check_with_context(
subject,
&action,
&ResourceId::from(resource.as_str()),
ctx,
);
self.finish(subject, request, action, resource, result)
}
pub async fn check_async(
&self,
subject: &SubjectId,
request: ToolCallRequest,
ctx: &RequestContext,
) -> GuardedToolCall {
let (request, action, resource) = match self.resolve(request) {
Ok(bound) => bound,
Err(denied) => return denied,
};
let result = self
.engine
.check_with_context_async(subject, &action, &ResourceId::from(resource.as_str()), ctx)
.await;
self.finish(subject, request, action, resource, result)
}
pub fn check_all(
&self,
subject: &SubjectId,
requests: impl IntoIterator<Item = ToolCallRequest>,
ctx: &RequestContext,
) -> Vec<GuardedToolCall> {
requests
.into_iter()
.map(|request| self.check(subject, request, ctx))
.collect()
}
#[allow(clippy::result_large_err)]
fn resolve(
&self,
request: ToolCallRequest,
) -> Result<(ToolCallRequest, String, String), GuardedToolCall> {
let Some(binding) = self.bindings.get(&request.tool_name) else {
let reason = format!(
"tool '{}' has no typesec binding (deny by default)",
request.tool_name
);
return Err(GuardedToolCall {
request,
action: None,
resource: None,
verdict: ToolCallVerdict::Deny { reason },
});
};
if let Err(reason) = binding.validate_arguments(&request.arguments) {
return Err(GuardedToolCall {
action: Some(binding.action.clone()),
request,
resource: None,
verdict: ToolCallVerdict::Deny { reason },
});
}
match binding.resolve_resource(&request.arguments) {
Ok(resource) => Ok((request, binding.action.clone(), resource)),
Err(reason) => Err(GuardedToolCall {
action: Some(binding.action.clone()),
request,
resource: None,
verdict: ToolCallVerdict::Deny { reason },
}),
}
}
fn finish(
&self,
subject: &SubjectId,
request: ToolCallRequest,
action: String,
resource: String,
result: PolicyResult,
) -> GuardedToolCall {
let verdict = match result {
PolicyResult::Allow => ToolCallVerdict::Allow,
PolicyResult::Deny(reason) => ToolCallVerdict::Deny { reason },
PolicyResult::Delegate(reason) => ToolCallVerdict::Delegate {
reason: reason.to_string(),
},
_ => ToolCallVerdict::Deny {
reason: "unknown policy result".to_string(),
},
};
tracing::info!(
subject = %subject,
tool = %request.tool_name,
action = %action,
resource = %resource,
allowed = verdict.is_allowed(),
"guarded tool call"
);
GuardedToolCall {
request,
action: Some(action),
resource: Some(resource),
verdict,
}
}
}