1use serde::{Deserialize, Deserializer, Serialize};
2use serde_json::to_value;
3use tea_protocol::{ProtocolMetadata, ProtocolMetadataError};
4use thiserror::Error;
5
6use crate::{ToolEffect, ToolResourceAccess, ToolSource, ToolVersion};
7
8const MAX_AUDIT_EFFECTS: usize = 64;
9const MAX_AUDIT_RESOURCES: usize = 128;
10const MAX_RESOURCE_PRESENTATION_BYTES: usize = 2048;
11
12pub const TOOL_AUDIT_METADATA_NAMESPACE: &str = "dev.tea-rs.tool-audit";
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
17#[serde(rename_all = "camelCase")]
18pub struct ToolAuditResource {
19 scheme: String,
20 redacted_presentation: String,
21 access: ToolResourceAccess,
22}
23
24impl ToolAuditResource {
25 pub fn new(
32 scheme: impl Into<String>,
33 redacted_presentation: impl Into<String>,
34 access: ToolResourceAccess,
35 ) -> Result<Self, ToolAuditMetadataError> {
36 let scheme = scheme.into();
37 let redacted_presentation = redacted_presentation.into();
38 let mut bytes = scheme.bytes();
39 if scheme.len() > 64
40 || !bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
41 || !bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
42 {
43 return Err(ToolAuditMetadataError::InvalidResource);
44 }
45 if redacted_presentation.is_empty()
46 || redacted_presentation.len() > MAX_RESOURCE_PRESENTATION_BYTES
47 || redacted_presentation.chars().any(char::is_control)
48 {
49 return Err(ToolAuditMetadataError::InvalidResource);
50 }
51 Ok(Self {
52 scheme,
53 redacted_presentation,
54 access,
55 })
56 }
57
58 #[must_use]
60 pub fn scheme(&self) -> &str {
61 &self.scheme
62 }
63
64 #[must_use]
66 pub fn redacted_presentation(&self) -> &str {
67 &self.redacted_presentation
68 }
69
70 #[must_use]
72 pub const fn access(&self) -> ToolResourceAccess {
73 self.access
74 }
75}
76
77#[derive(Deserialize)]
78#[serde(rename_all = "camelCase", deny_unknown_fields)]
79struct RawToolAuditResource {
80 scheme: String,
81 redacted_presentation: String,
82 access: ToolResourceAccess,
83}
84
85impl<'de> Deserialize<'de> for ToolAuditResource {
86 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
87 where
88 D: Deserializer<'de>,
89 {
90 let raw = RawToolAuditResource::deserialize(deserializer)?;
91 Self::new(raw.scheme, raw.redacted_presentation, raw.access)
92 .map_err(serde::de::Error::custom)
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Serialize)]
98#[serde(rename_all = "camelCase")]
99pub struct ToolAuditMetadata {
100 tool_version: ToolVersion,
101 source: ToolSource,
102 effects: Vec<ToolEffect>,
103 resources: Vec<ToolAuditResource>,
104}
105
106impl ToolAuditMetadata {
107 pub fn new(
113 tool_version: ToolVersion,
114 source: ToolSource,
115 effects: impl IntoIterator<Item = ToolEffect>,
116 resources: impl IntoIterator<Item = ToolAuditResource>,
117 ) -> Result<Self, ToolAuditMetadataError> {
118 let mut effects = effects.into_iter().collect::<Vec<_>>();
119 effects.sort();
120 effects.dedup();
121 if effects.is_empty() || effects.len() > MAX_AUDIT_EFFECTS {
122 return Err(ToolAuditMetadataError::InvalidEffects);
123 }
124 let mut resources = resources.into_iter().collect::<Vec<_>>();
125 resources.sort();
126 resources.dedup();
127 if resources.len() > MAX_AUDIT_RESOURCES {
128 return Err(ToolAuditMetadataError::InvalidResources);
129 }
130 Ok(Self {
131 tool_version,
132 source,
133 effects,
134 resources,
135 })
136 }
137
138 pub fn to_protocol_metadata(&self) -> Result<ProtocolMetadata, ToolAuditMetadataError> {
144 let value = to_value(self).map_err(ToolAuditMetadataError::Serialization)?;
145 ProtocolMetadata::from_entries([(TOOL_AUDIT_METADATA_NAMESPACE, value)])
146 .map_err(ToolAuditMetadataError::ProtocolMetadata)
147 }
148
149 #[must_use]
151 pub const fn tool_version(&self) -> &ToolVersion {
152 &self.tool_version
153 }
154
155 #[must_use]
157 pub const fn source(&self) -> &ToolSource {
158 &self.source
159 }
160
161 #[must_use]
163 pub fn effects(&self) -> &[ToolEffect] {
164 &self.effects
165 }
166
167 #[must_use]
169 pub fn resources(&self) -> &[ToolAuditResource] {
170 &self.resources
171 }
172}
173
174#[derive(Deserialize)]
175#[serde(rename_all = "camelCase", deny_unknown_fields)]
176struct RawToolAuditMetadata {
177 tool_version: ToolVersion,
178 source: ToolSource,
179 effects: Vec<ToolEffect>,
180 resources: Vec<ToolAuditResource>,
181}
182
183impl<'de> Deserialize<'de> for ToolAuditMetadata {
184 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
185 where
186 D: Deserializer<'de>,
187 {
188 let raw = RawToolAuditMetadata::deserialize(deserializer)?;
189 Self::new(raw.tool_version, raw.source, raw.effects, raw.resources)
190 .map_err(serde::de::Error::custom)
191 }
192}
193
194#[derive(Debug, Error)]
196pub enum ToolAuditMetadataError {
197 #[error("tool audit effects are invalid")]
199 InvalidEffects,
200 #[error("tool audit resources are invalid")]
202 InvalidResources,
203 #[error("tool audit resource is invalid")]
205 InvalidResource,
206 #[error("tool audit metadata could not be encoded: {0}")]
208 Serialization(serde_json::Error),
209 #[error("tool audit metadata exceeds protocol bounds: {0}")]
211 ProtocolMetadata(ProtocolMetadataError),
212}