1use serde::{Deserialize, Deserializer, Serialize};
2use thiserror::Error;
3
4const MAX_SOURCE_ID_BYTES: usize = 256;
5const SHA256_HEX_BYTES: usize = 64;
6const NATIVE_PRODUCT_DIGEST: &str =
7 "5c2fe6d5aa5a64dc09567ef61a02305036b9c96e3e2ad8fa6c4bd7eaeb234ecf";
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ToolSourceKind {
13 Native,
15 Mcp,
17 Remote,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ToolTrust {
25 Product,
27 User,
29 Workspace,
31 Untrusted,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct ToolSource {
39 kind: ToolSourceKind,
40 source_id: String,
41 trust: ToolTrust,
42 descriptor_digest: String,
43}
44
45impl ToolSource {
46 pub fn new(
53 kind: ToolSourceKind,
54 source_id: impl Into<String>,
55 trust: ToolTrust,
56 descriptor_digest: impl Into<String>,
57 ) -> Result<Self, ToolSourceError> {
58 let source_id = source_id.into();
59 let descriptor_digest = descriptor_digest.into();
60 let mut bytes = source_id.bytes();
61 if source_id.len() > MAX_SOURCE_ID_BYTES
62 || !bytes.next().is_some_and(|byte| byte.is_ascii_lowercase())
63 || !bytes.all(|byte| {
64 byte.is_ascii_lowercase()
65 || byte.is_ascii_digit()
66 || matches!(byte, b'.' | b'-' | b'_')
67 })
68 {
69 return Err(ToolSourceError::InvalidSourceId);
70 }
71 if descriptor_digest.len() != SHA256_HEX_BYTES
72 || !descriptor_digest
73 .bytes()
74 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
75 {
76 return Err(ToolSourceError::InvalidDescriptorDigest);
77 }
78 Ok(Self {
79 kind,
80 source_id,
81 trust,
82 descriptor_digest,
83 })
84 }
85
86 #[must_use]
88 pub fn native_product() -> Self {
89 Self {
90 kind: ToolSourceKind::Native,
91 source_id: "tea-rs.product.native".to_owned(),
92 trust: ToolTrust::Product,
93 descriptor_digest: NATIVE_PRODUCT_DIGEST.to_owned(),
94 }
95 }
96
97 #[must_use]
99 pub fn is_native_product(&self) -> bool {
100 self.kind == ToolSourceKind::Native
101 && self.source_id == "tea-rs.product.native"
102 && self.trust == ToolTrust::Product
103 && self.descriptor_digest == NATIVE_PRODUCT_DIGEST
104 }
105
106 #[must_use]
108 pub const fn kind(&self) -> ToolSourceKind {
109 self.kind
110 }
111
112 #[must_use]
114 pub fn source_id(&self) -> &str {
115 &self.source_id
116 }
117
118 #[must_use]
120 pub const fn trust(&self) -> ToolTrust {
121 self.trust
122 }
123
124 #[must_use]
126 pub fn descriptor_digest(&self) -> &str {
127 &self.descriptor_digest
128 }
129}
130
131#[derive(Deserialize)]
132#[serde(rename_all = "camelCase", deny_unknown_fields)]
133struct RawToolSource {
134 kind: ToolSourceKind,
135 source_id: String,
136 trust: ToolTrust,
137 descriptor_digest: String,
138}
139
140impl<'de> Deserialize<'de> for ToolSource {
141 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
142 where
143 D: Deserializer<'de>,
144 {
145 let raw = RawToolSource::deserialize(deserializer)?;
146 Self::new(raw.kind, raw.source_id, raw.trust, raw.descriptor_digest)
147 .map_err(serde::de::Error::custom)
148 }
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
153pub enum ToolSourceError {
154 #[error("tool source ID is not canonical")]
156 InvalidSourceId,
157 #[error("tool source descriptor digest is not lowercase SHA-256")]
159 InvalidDescriptorDigest,
160}