Skip to main content

mentra/tool/
authorization.rs

1use std::path::PathBuf;
2use std::time::Duration;
3
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::{
9    runtime::RuntimeError,
10    tool::{
11        ToolApprovalCategory, ToolCapability, ToolDurability, ToolExecutionCategory,
12        ToolSideEffectLevel,
13    },
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ToolAuthorizationOutcome {
19    Allow,
20    Prompt,
21    Deny,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct ToolAuthorizationPreview {
26    pub working_directory: PathBuf,
27    pub capabilities: Vec<ToolCapability>,
28    pub side_effect_level: ToolSideEffectLevel,
29    pub durability: ToolDurability,
30    pub execution_category: ToolExecutionCategory,
31    pub approval_category: ToolApprovalCategory,
32    pub raw_input: Value,
33    pub structured_input: Value,
34}
35
36impl ToolAuthorizationPreview {
37    /// What this call was classified as, without its input.
38    ///
39    /// The classification is the part a policy reasons about, and it is the
40    /// part that survives past the authorizer — see [`ToolClassification`].
41    pub fn classification(&self) -> ToolClassification {
42        self.into()
43    }
44}
45
46/// What the runtime worked out about a tool call before anyone was asked to
47/// approve it: what it can touch, how far its effects reach, whether it can be
48/// replayed, which scheduler lane it will run in, and which approval group it
49/// falls under.
50///
51/// [`execution_category`](Self::execution_category) is the lane the scheduler
52/// will actually use, asked of the tool with this call's input and carrying
53/// the coercion a terminal tool gets -- not the category the tool's descriptor
54/// declares. The two differ: `files` declares an exclusive mutation and
55/// reports a parallel read for a batch that only reads.
56///
57/// This is [`ToolAuthorizationPreview`] with the call's input left off. The
58/// input already travels beside it wherever this type is carried, and the
59/// classification alone is what a policy matches on: "allow edits, refuse the
60/// network" is [`ToolSideEffectLevel::LocalState`] against
61/// [`ToolSideEffectLevel::External`], a distinction that no amount of reading
62/// tool names and parsing arguments recovers reliably.
63///
64/// Deliberately not [`Default`]: the two fields a policy leans on hardest
65/// zero out permissively — [`capabilities`](Self::capabilities) to empty and
66/// [`side_effect_level`](Self::side_effect_level) to
67/// [`ToolSideEffectLevel::None`] — so a classification conjured from nothing
68/// would read as a call that touches nothing, the one answer a policy must
69/// never be handed by accident. Where a classification may be absent, say so
70/// with an [`Option`].
71///
72/// # Wire format
73///
74/// The enums inside serialize in their derived form — PascalCase, externally
75/// tagged: `"ReplaySafe"`, `"External"`, `{"Custom":"network"}` — even though
76/// this type rides inside
77/// [`SessionEvent`](crate::session::SessionEvent), whose own tags and fields
78/// are snake_case. The mismatch is kept deliberately. The same five enums are
79/// shared with [`ToolAuthorizationPreview`], which has serialized this way
80/// since it shipped and is written verbatim into audit rows by the audit
81/// hook, so renaming their representation would change a released wire format
82/// and orphan recorded data — while reserializing them differently for this
83/// one field would have the same value print two ways in one API. One
84/// representation, consistent with itself, is the smaller surprise; a host
85/// parsing the event stream as JSON should expect PascalCase inside this
86/// field alone.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct ToolClassification {
89    pub capabilities: Vec<ToolCapability>,
90    pub side_effect_level: ToolSideEffectLevel,
91    pub durability: ToolDurability,
92    pub execution_category: ToolExecutionCategory,
93    pub approval_category: ToolApprovalCategory,
94}
95
96impl From<&ToolAuthorizationPreview> for ToolClassification {
97    fn from(preview: &ToolAuthorizationPreview) -> Self {
98        Self {
99            capabilities: preview.capabilities.clone(),
100            side_effect_level: preview.side_effect_level,
101            durability: preview.durability,
102            execution_category: preview.execution_category,
103            approval_category: preview.approval_category,
104        }
105    }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct ToolAuthorizationDecision {
110    pub outcome: ToolAuthorizationOutcome,
111    pub reason: Option<String>,
112}
113
114impl ToolAuthorizationDecision {
115    pub fn allow() -> Self {
116        Self {
117            outcome: ToolAuthorizationOutcome::Allow,
118            reason: None,
119        }
120    }
121
122    pub fn prompt(reason: impl Into<String>) -> Self {
123        Self {
124            outcome: ToolAuthorizationOutcome::Prompt,
125            reason: Some(reason.into()),
126        }
127    }
128
129    pub fn deny(reason: impl Into<String>) -> Self {
130        Self {
131            outcome: ToolAuthorizationOutcome::Deny,
132            reason: Some(reason.into()),
133        }
134    }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138pub struct ToolAuthorizationRequest {
139    pub agent_id: String,
140    pub agent_name: String,
141    pub model: String,
142    pub history_len: usize,
143    pub tool_call_id: String,
144    pub tool_name: String,
145    pub preview: ToolAuthorizationPreview,
146}
147
148/// Consulted after the pre-execution hooks and the schema check, and able to
149/// stop a call — but not to rewrite it.
150///
151/// The request's [`structured_input`](ToolAuthorizationPreview::structured_input)
152/// is the input the tool will execute with: every
153/// [`PreExecutionHook`](crate::runtime::control::PreExecutionHook) has already
154/// run, any [`HookDecision::Modify`](crate::runtime::control::HookDecision::Modify)
155/// has been applied and validated against the tool's `input_schema`, and
156/// nothing rewrites the call between this answer and execution. A host that
157/// remembers a decision as a rule keyed on that input can therefore replay it
158/// against future calls and know the rule describes what ran.
159///
160/// The authorizer is not consulted for a call a hook denied, nor for one whose
161/// input — the model's or a hook's — does not fit the schema.
162///
163/// A [`Session`](crate::session::Session) consults its current authorizer before
164/// considering remembered permission answers. `Allow` and `Deny` are
165/// authoritative; only `Prompt` may be answered by a remembered rule or sent
166/// through the session's live permission flow. With no authorizer installed,
167/// the effective answer remains `Allow` and remembered rules are not a separate
168/// policy layer.
169#[async_trait]
170pub trait ToolAuthorizer: Send + Sync {
171    async fn authorize(
172        &self,
173        request: &ToolAuthorizationRequest,
174    ) -> Result<ToolAuthorizationDecision, RuntimeError>;
175
176    fn timeout(&self) -> Option<Duration> {
177        None
178    }
179}
180
181/// Forwards to the authorizer inside.
182///
183/// Lets a caller hold an authorizer it chose at runtime — one of several, or
184/// none — and still hand it to anything taking `impl ToolAuthorizer`, without
185/// each caller writing this impl itself.
186#[async_trait]
187impl<T: ToolAuthorizer + ?Sized> ToolAuthorizer for Box<T> {
188    async fn authorize(
189        &self,
190        request: &ToolAuthorizationRequest,
191    ) -> Result<ToolAuthorizationDecision, RuntimeError> {
192        (**self).authorize(request).await
193    }
194
195    fn timeout(&self) -> Option<Duration> {
196        (**self).timeout()
197    }
198}
199
200/// Forwards to the authorizer inside, for a shared one.
201#[async_trait]
202impl<T: ToolAuthorizer + ?Sized> ToolAuthorizer for std::sync::Arc<T> {
203    async fn authorize(
204        &self,
205        request: &ToolAuthorizationRequest,
206    ) -> Result<ToolAuthorizationDecision, RuntimeError> {
207        (**self).authorize(request).await
208    }
209
210    fn timeout(&self) -> Option<Duration> {
211        (**self).timeout()
212    }
213}