1use std::collections::HashMap;
4use std::sync::Arc;
5
6use typesec_core::ResourceId;
7use typesec_core::policy::{PolicyEngine, PolicyResult, RequestContext, SubjectId};
8
9use super::call::{GuardedToolCall, ToolBinding, ToolCallRequest, ToolCallVerdict};
10use crate::tool::ToolRegistry;
11
12pub struct ToolCallGuard {
19 engine: Arc<dyn PolicyEngine>,
20 bindings: HashMap<String, ToolBinding>,
21}
22
23impl ToolCallGuard {
24 pub fn new(engine: Arc<dyn PolicyEngine>) -> Self {
26 Self {
27 engine,
28 bindings: HashMap::new(),
29 }
30 }
31
32 #[must_use]
35 pub fn bind(mut self, binding: ToolBinding) -> Self {
36 self.bindings.insert(binding.tool_name.clone(), binding);
37 self
38 }
39
40 #[must_use]
43 pub fn bind_registry(mut self, registry: &ToolRegistry) -> Self {
44 for spec in registry.list_specs() {
45 let binding = ToolBinding::from_spec(&spec);
46 self.bindings.insert(binding.tool_name.clone(), binding);
47 }
48 self
49 }
50
51 pub fn binding(&self, tool_name: &str) -> Option<&ToolBinding> {
53 self.bindings.get(tool_name)
54 }
55
56 pub fn allows_listing(
64 &self,
65 subject: &SubjectId,
66 tool_name: &str,
67 ctx: &RequestContext,
68 ) -> bool {
69 let Some(binding) = self.bindings.get(tool_name) else {
70 return false;
71 };
72 if binding.resource_arg.is_some() {
73 return true;
74 }
75 matches!(
76 self.engine.check_with_context(
77 subject,
78 &binding.action,
79 &ResourceId::from(binding.resource.as_str()),
80 ctx,
81 ),
82 PolicyResult::Allow
83 )
84 }
85
86 pub fn check(
88 &self,
89 subject: &SubjectId,
90 request: ToolCallRequest,
91 ctx: &RequestContext,
92 ) -> GuardedToolCall {
93 let (request, action, resource) = match self.resolve(request) {
94 Ok(bound) => bound,
95 Err(denied) => return denied,
96 };
97 let result = self.engine.check_with_context(
98 subject,
99 &action,
100 &ResourceId::from(resource.as_str()),
101 ctx,
102 );
103 self.finish(subject, request, action, resource, result)
104 }
105
106 pub async fn check_async(
108 &self,
109 subject: &SubjectId,
110 request: ToolCallRequest,
111 ctx: &RequestContext,
112 ) -> GuardedToolCall {
113 let (request, action, resource) = match self.resolve(request) {
114 Ok(bound) => bound,
115 Err(denied) => return denied,
116 };
117 let result = self
118 .engine
119 .check_with_context_async(subject, &action, &ResourceId::from(resource.as_str()), ctx)
120 .await;
121 self.finish(subject, request, action, resource, result)
122 }
123
124 pub fn check_all(
126 &self,
127 subject: &SubjectId,
128 requests: impl IntoIterator<Item = ToolCallRequest>,
129 ctx: &RequestContext,
130 ) -> Vec<GuardedToolCall> {
131 requests
132 .into_iter()
133 .map(|request| self.check(subject, request, ctx))
134 .collect()
135 }
136
137 #[allow(clippy::result_large_err)]
140 fn resolve(
141 &self,
142 request: ToolCallRequest,
143 ) -> Result<(ToolCallRequest, String, String), GuardedToolCall> {
144 let Some(binding) = self.bindings.get(&request.tool_name) else {
145 let reason = format!(
146 "tool '{}' has no typesec binding (deny by default)",
147 request.tool_name
148 );
149 return Err(GuardedToolCall {
150 request,
151 action: None,
152 resource: None,
153 verdict: ToolCallVerdict::Deny { reason },
154 });
155 };
156 if let Err(reason) = binding.validate_arguments(&request.arguments) {
157 return Err(GuardedToolCall {
158 action: Some(binding.action.clone()),
159 request,
160 resource: None,
161 verdict: ToolCallVerdict::Deny { reason },
162 });
163 }
164 match binding.resolve_resource(&request.arguments) {
165 Ok(resource) => Ok((request, binding.action.clone(), resource)),
166 Err(reason) => Err(GuardedToolCall {
167 action: Some(binding.action.clone()),
168 request,
169 resource: None,
170 verdict: ToolCallVerdict::Deny { reason },
171 }),
172 }
173 }
174
175 fn finish(
176 &self,
177 subject: &SubjectId,
178 request: ToolCallRequest,
179 action: String,
180 resource: String,
181 result: PolicyResult,
182 ) -> GuardedToolCall {
183 let verdict = match result {
184 PolicyResult::Allow => ToolCallVerdict::Allow,
185 PolicyResult::Deny(reason) => ToolCallVerdict::Deny { reason },
186 PolicyResult::Delegate(reason) => ToolCallVerdict::Delegate {
187 reason: reason.to_string(),
188 },
189 _ => ToolCallVerdict::Deny {
190 reason: "unknown policy result".to_string(),
191 },
192 };
193 tracing::info!(
194 subject = %subject,
195 tool = %request.tool_name,
196 action = %action,
197 resource = %resource,
198 allowed = verdict.is_allowed(),
199 "guarded tool call"
200 );
201 GuardedToolCall {
202 request,
203 action: Some(action),
204 resource: Some(resource),
205 verdict,
206 }
207 }
208}