Skip to main content

tea_tools/
resource.rs

1use std::fmt::Debug;
2
3use serde::{Deserialize, Deserializer, Serialize};
4use serde_json::Value;
5use thiserror::Error;
6
7use crate::ToolName;
8
9/// Maximum resolved resources for one invocation.
10pub const MAX_TOOL_RESOURCES: usize = 128;
11
12/// Access requested for a resolved resource.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ToolResourceAccess {
16    /// Read-only access.
17    Read,
18    /// Create or modify access.
19    Write,
20    /// Delete access.
21    Delete,
22    /// Execute or spawn access.
23    Execute,
24    /// Access semantics unknown to this runtime.
25    Unknown,
26}
27
28/// Canonical resource affected by a tool invocation.
29#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
30#[serde(rename_all = "camelCase")]
31pub struct ToolResource {
32    scheme: String,
33    locator: String,
34    access: ToolResourceAccess,
35}
36
37impl ToolResource {
38    /// Creates a bounded canonical resource.
39    ///
40    /// # Errors
41    ///
42    /// Rejects invalid schemes or empty/control/oversized locators.
43    pub fn new(
44        scheme: impl Into<String>,
45        locator: impl Into<String>,
46        access: ToolResourceAccess,
47    ) -> Result<Self, ToolResourceError> {
48        let scheme = scheme.into();
49        let locator = locator.into();
50        let mut bytes = scheme.bytes();
51        if scheme.len() > 64
52            || !bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
53            || !bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
54        {
55            return Err(ToolResourceError::InvalidScheme);
56        }
57        if locator.is_empty() || locator.len() > 2048 || locator.chars().any(char::is_control) {
58            return Err(ToolResourceError::InvalidLocator);
59        }
60        Ok(Self {
61            scheme,
62            locator,
63            access,
64        })
65    }
66
67    /// Returns the resource scheme.
68    #[must_use]
69    pub fn scheme(&self) -> &str {
70        &self.scheme
71    }
72
73    /// Returns the opaque bounded locator.
74    #[must_use]
75    pub fn locator(&self) -> &str {
76        &self.locator
77    }
78
79    /// Returns requested access.
80    #[must_use]
81    pub const fn access(&self) -> ToolResourceAccess {
82        self.access
83    }
84}
85
86#[derive(Deserialize)]
87#[serde(rename_all = "camelCase")]
88struct RawToolResource {
89    scheme: String,
90    locator: String,
91    access: ToolResourceAccess,
92}
93
94impl<'de> Deserialize<'de> for ToolResource {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: Deserializer<'de>,
98    {
99        let raw = RawToolResource::deserialize(deserializer)?;
100        Self::new(raw.scheme, raw.locator, raw.access).map_err(serde::de::Error::custom)
101    }
102}
103
104/// Pure resource resolver used before policy and execution.
105pub trait ToolResourceResolver: Debug + Send + Sync {
106    /// Resolves affected resources from schema-validated arguments.
107    ///
108    /// # Errors
109    ///
110    /// Returns a bounded resolution error without performing a side effect.
111    fn resolve(
112        &self,
113        tool_name: &ToolName,
114        arguments: &Value,
115    ) -> Result<Vec<ToolResource>, ToolResourceError>;
116}
117
118/// Resolver reading one string argument as a resource locator.
119#[derive(Debug, Clone)]
120pub struct ArgumentResourceResolver {
121    argument: String,
122    scheme: String,
123    access: ToolResourceAccess,
124}
125
126impl ArgumentResourceResolver {
127    /// Creates a resolver for one canonical top-level string argument.
128    ///
129    /// # Errors
130    ///
131    /// Returns an error for invalid argument names or resource schemes.
132    pub fn new(
133        argument: impl Into<String>,
134        scheme: impl Into<String>,
135        access: ToolResourceAccess,
136    ) -> Result<Self, ToolResourceError> {
137        let argument = argument.into();
138        let scheme = scheme.into();
139        if argument.is_empty()
140            || argument.len() > 128
141            || !argument
142                .bytes()
143                .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
144        {
145            return Err(ToolResourceError::Unresolved);
146        }
147        ToolResource::new(&scheme, "validation", access)?;
148        Ok(Self {
149            argument,
150            scheme,
151            access,
152        })
153    }
154}
155
156impl ToolResourceResolver for ArgumentResourceResolver {
157    fn resolve(
158        &self,
159        _tool_name: &ToolName,
160        arguments: &Value,
161    ) -> Result<Vec<ToolResource>, ToolResourceError> {
162        let locator = arguments
163            .get(&self.argument)
164            .and_then(Value::as_str)
165            .ok_or(ToolResourceError::Unresolved)?;
166        Ok(vec![ToolResource::new(&self.scheme, locator, self.access)?])
167    }
168}
169
170/// Resolver returning one deterministic static resource set.
171#[derive(Debug, Clone)]
172pub struct StaticResourceResolver {
173    resources: Vec<ToolResource>,
174}
175
176impl StaticResourceResolver {
177    /// Creates a sorted, deduplicated bounded static resolver.
178    ///
179    /// # Errors
180    ///
181    /// Returns an error when the deduplicated resource count exceeds bounds.
182    pub fn new(
183        resources: impl IntoIterator<Item = ToolResource>,
184    ) -> Result<Self, ToolResourceError> {
185        let mut resources = resources.into_iter().collect::<Vec<_>>();
186        resources.sort();
187        resources.dedup();
188        if resources.len() > MAX_TOOL_RESOURCES {
189            return Err(ToolResourceError::TooManyResources);
190        }
191        Ok(Self { resources })
192    }
193
194    /// Returns sorted static resources.
195    #[must_use]
196    pub fn resources(&self) -> &[ToolResource] {
197        &self.resources
198    }
199}
200
201impl ToolResourceResolver for StaticResourceResolver {
202    fn resolve(
203        &self,
204        _tool_name: &ToolName,
205        _arguments: &Value,
206    ) -> Result<Vec<ToolResource>, ToolResourceError> {
207        Ok(self.resources.clone())
208    }
209}
210
211/// Resource construction or resolution failure.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
213pub enum ToolResourceError {
214    /// Scheme is not canonical lowercase ASCII.
215    #[error("tool resource scheme is invalid")]
216    InvalidScheme,
217    /// Locator is empty, oversized, or contains controls.
218    #[error("tool resource locator is invalid")]
219    InvalidLocator,
220    /// Resolver returned too many resources.
221    #[error("tool invocation resolves too many resources")]
222    TooManyResources,
223    /// Arguments do not identify a required resource.
224    #[error("tool resource cannot be resolved from arguments")]
225    Unresolved,
226}