Skip to main content

atd_protocol/
tool.rs

1use serde::{Deserialize, Serialize};
2
3use crate::enums::{BindingProtocol, SafetyLevel, ToolVisibility, TrustLevel};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7pub struct ToolDefinition {
8    pub id: String,
9    pub name: String,
10    pub description: String,
11    pub version: String,
12
13    pub capability: ToolCapability,
14    pub input_schema: serde_json::Value,
15    pub output_schema: serde_json::Value,
16
17    pub bindings: Vec<ToolBinding>,
18    pub safety: ToolSafety,
19    pub resources: ToolResources,
20    pub trust: ToolTrust,
21
22    #[serde(default)]
23    pub visibility: ToolVisibility,
24
25    /// Capabilities a caller must hold for this tool to be invoked.
26    /// Enforced by the server's dispatch layer (SP-12). Empty = unrestricted.
27    /// The existing `capability` field above is a *descriptor* (domain, actions,
28    /// intent examples); this is the *enforcement* list — intentionally
29    /// separate to avoid overloading the schema.
30    #[serde(default)]
31    pub required_capabilities: Vec<String>,
32
33    /// Execution tier hint used by the dispatch layer to derive deadline and
34    /// max-output budgets. Absent / unknown values default to `Warm` on the
35    /// server side (SP-12 back-compat). The client-facing `ToolSummary` carries
36    /// an equivalent field.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub tier: Option<crate::enums::ToolTier>,
39
40    /// Domain errors this tool may emit. Optional; missing on the wire =
41    /// empty. Surfaces only via `describe`, never via `discover` (kept off
42    /// `ToolSummary`).
43    #[serde(default)]
44    pub errors: Vec<ToolErrorDef>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
49pub struct ToolErrorDef {
50    /// SCREAMING_SNAKE error code, e.g. "FILE_NOT_FOUND".
51    pub code: String,
52    pub description: String,
53    pub retryable: bool,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
58pub struct ToolCapability {
59    pub domain: String,
60    pub actions: Vec<String>,
61    pub tags: Vec<String>,
62    pub intent_examples: Vec<String>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
67pub struct ToolBinding {
68    pub protocol: BindingProtocol,
69    pub config: serde_json::Value,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
74pub struct ToolSafety {
75    pub level: SafetyLevel,
76    pub dry_run: bool,
77    pub side_effects: Vec<String>,
78    pub data_sensitivity: Option<String>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
83pub struct ToolResources {
84    pub timeout_ms: u64,
85    pub max_concurrent: u32,
86    pub rate_limit_per_min: Option<u32>,
87    pub estimated_tokens: Option<u32>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
92pub struct ToolTrust {
93    pub publisher: String,
94    pub trust_level: TrustLevel,
95    pub signature: Option<Vec<u8>>,
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn sample() -> ToolDefinition {
103        ToolDefinition {
104            id: "anos:fs.read".into(),
105            name: "Read File".into(),
106            description: "Read a file from disk.".into(),
107            version: "0.1.0".into(),
108            capability: ToolCapability {
109                domain: "fs".into(),
110                actions: vec!["read".into()],
111                tags: vec!["filesystem".into()],
112                intent_examples: vec!["read config.toml".into()],
113            },
114            input_schema: serde_json::json!({
115                "type": "object",
116                "properties": {"path": {"type": "string"}},
117                "required": ["path"]
118            }),
119            output_schema: serde_json::json!({"type": "string"}),
120            bindings: vec![ToolBinding {
121                protocol: BindingProtocol::Cli,
122                config: serde_json::json!({"cmd": "cat"}),
123            }],
124            safety: ToolSafety {
125                level: SafetyLevel::Read,
126                dry_run: false,
127                side_effects: vec![],
128                data_sensitivity: None,
129            },
130            resources: ToolResources {
131                timeout_ms: 5_000,
132                max_concurrent: 8,
133                rate_limit_per_min: None,
134                estimated_tokens: Some(100),
135            },
136            trust: ToolTrust {
137                publisher: "anos".into(),
138                trust_level: TrustLevel::L3Verified,
139                signature: None,
140            },
141            visibility: ToolVisibility::Read,
142            required_capabilities: vec![],
143            tier: None,
144            errors: vec![],
145        }
146    }
147
148    #[test]
149    fn tool_definition_roundtrip() {
150        let t = sample();
151        let json = serde_json::to_string(&t).unwrap();
152        let back: ToolDefinition = serde_json::from_str(&json).unwrap();
153        assert_eq!(back.id, t.id);
154        assert_eq!(back.capability.domain, "fs");
155        assert_eq!(back.safety.level, SafetyLevel::Read);
156    }
157
158    #[test]
159    fn visibility_defaults_when_missing_in_json() {
160        let mut v = serde_json::to_value(sample()).unwrap();
161        v.as_object_mut().unwrap().remove("visibility");
162        let back: ToolDefinition = serde_json::from_value(v).unwrap();
163        assert_eq!(back.visibility, ToolVisibility::Read);
164    }
165
166    #[test]
167    fn required_capabilities_defaults_to_empty_when_missing() {
168        // Pre-SP-12 serialized definitions omit the field; must round-trip.
169        let mut v = serde_json::to_value(sample()).unwrap();
170        v.as_object_mut().unwrap().remove("required_capabilities");
171        let back: ToolDefinition = serde_json::from_value(v).unwrap();
172        assert!(back.required_capabilities.is_empty());
173    }
174
175    #[test]
176    fn required_capabilities_roundtrip_when_set() {
177        let mut t = sample();
178        t.required_capabilities = vec!["exec".into(), "read".into()];
179        let j = serde_json::to_string(&t).unwrap();
180        let back: ToolDefinition = serde_json::from_str(&j).unwrap();
181        assert_eq!(back.required_capabilities, vec!["exec", "read"]);
182    }
183
184    #[test]
185    fn tier_omitted_when_none() {
186        let j = serde_json::to_string(&sample()).unwrap();
187        assert!(
188            !j.contains("\"tier\""),
189            "tier should be skipped when None: {j}"
190        );
191    }
192}