Skip to main content

tea_tools/
source.rs

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/// Provider-neutral origin category for a registered tool.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ToolSourceKind {
13    /// Product-owned in-process or host-native implementation.
14    Native,
15    /// Tool exposed through the Model Context Protocol.
16    Mcp,
17    /// Tool executed by another remote adapter.
18    Remote,
19}
20
21/// Host-assigned trust class for a tool source.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ToolTrust {
25    /// Source shipped and controlled by the product.
26    Product,
27    /// Source configured directly by the current user.
28    User,
29    /// Source enabled by trusted workspace configuration.
30    Workspace,
31    /// Source has no affirmative trust assignment.
32    Untrusted,
33}
34
35/// Stable, bounded provenance for one frozen tool descriptor.
36#[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    /// Creates validated tool-source provenance.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error unless the source ID is canonical lowercase ASCII and
51    /// the descriptor digest is exactly one lowercase hexadecimal SHA-256.
52    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    /// Returns the stable default provenance used by native product tools.
87    #[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    /// Returns whether this is the stable legacy-compatible product source.
98    #[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    /// Returns the provider-neutral source category.
107    #[must_use]
108    pub const fn kind(&self) -> ToolSourceKind {
109        self.kind
110    }
111
112    /// Returns the stable canonical source identifier.
113    #[must_use]
114    pub fn source_id(&self) -> &str {
115        &self.source_id
116    }
117
118    /// Returns the host-assigned trust class.
119    #[must_use]
120    pub const fn trust(&self) -> ToolTrust {
121        self.trust
122    }
123
124    /// Returns the lowercase SHA-256 descriptor digest.
125    #[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/// Error returned when validating tool-source provenance.
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
153pub enum ToolSourceError {
154    /// Source ID is empty, oversized, or not canonical lowercase ASCII.
155    #[error("tool source ID is not canonical")]
156    InvalidSourceId,
157    /// Descriptor digest is not lowercase hexadecimal SHA-256 text.
158    #[error("tool source descriptor digest is not lowercase SHA-256")]
159    InvalidDescriptorDigest,
160}