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    /// **Advisory only in v1 — NOT enforced by dispatch.** The only
87    /// enforced concurrency control is `max_concurrent` (a per-tool
88    /// semaphore in the `atd-runtime` registry). Adopters needing real
89    /// per-minute rate limiting compose their own limiter (e.g. the
90    /// `governor` crate) outside dispatch. A future SP may make this
91    /// field enforced; adopters relying on advisory-only behaviour should
92    /// re-audit when that lands. See architecture §10.7.
93    pub rate_limit_per_min: Option<u32>,
94    pub estimated_tokens: Option<u32>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
99pub struct ToolTrust {
100    pub publisher: String,
101    /// **Publisher self-declared in v1 — ATD does NOT verify trust level.**
102    /// `L4Certified` means "the publisher claims certification", not "ATD
103    /// verified it". Use only as a hint to higher layers; do NOT base a
104    /// security decision on this field alone. A future SP may add
105    /// publisher-key PKI verification. See architecture §6.1 / §10.3.
106    pub trust_level: TrustLevel,
107    pub signature: Option<Vec<u8>>,
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    fn sample() -> ToolDefinition {
115        ToolDefinition {
116            id: "anos:fs.read".into(),
117            name: "Read File".into(),
118            description: "Read a file from disk.".into(),
119            version: "0.1.0".into(),
120            capability: ToolCapability {
121                domain: "fs".into(),
122                actions: vec!["read".into()],
123                tags: vec!["filesystem".into()],
124                intent_examples: vec!["read config.toml".into()],
125            },
126            input_schema: serde_json::json!({
127                "type": "object",
128                "properties": {"path": {"type": "string"}},
129                "required": ["path"]
130            }),
131            output_schema: serde_json::json!({"type": "string"}),
132            bindings: vec![ToolBinding {
133                protocol: BindingProtocol::Cli,
134                config: serde_json::json!({"cmd": "cat"}),
135            }],
136            safety: ToolSafety {
137                level: SafetyLevel::Read,
138                dry_run: false,
139                side_effects: vec![],
140                data_sensitivity: None,
141            },
142            resources: ToolResources {
143                timeout_ms: 5_000,
144                max_concurrent: 8,
145                rate_limit_per_min: None,
146                estimated_tokens: Some(100),
147            },
148            trust: ToolTrust {
149                publisher: "anos".into(),
150                trust_level: TrustLevel::L3Verified,
151                signature: None,
152            },
153            visibility: ToolVisibility::Read,
154            required_capabilities: vec![],
155            tier: None,
156            errors: vec![],
157        }
158    }
159
160    #[test]
161    fn tool_definition_roundtrip() {
162        let t = sample();
163        let json = serde_json::to_string(&t).unwrap();
164        let back: ToolDefinition = serde_json::from_str(&json).unwrap();
165        assert_eq!(back.id, t.id);
166        assert_eq!(back.capability.domain, "fs");
167        assert_eq!(back.safety.level, SafetyLevel::Read);
168    }
169
170    #[test]
171    fn visibility_defaults_when_missing_in_json() {
172        let mut v = serde_json::to_value(sample()).unwrap();
173        v.as_object_mut().unwrap().remove("visibility");
174        let back: ToolDefinition = serde_json::from_value(v).unwrap();
175        assert_eq!(back.visibility, ToolVisibility::Read);
176    }
177
178    #[test]
179    fn required_capabilities_defaults_to_empty_when_missing() {
180        // Pre-SP-12 serialized definitions omit the field; must round-trip.
181        let mut v = serde_json::to_value(sample()).unwrap();
182        v.as_object_mut().unwrap().remove("required_capabilities");
183        let back: ToolDefinition = serde_json::from_value(v).unwrap();
184        assert!(back.required_capabilities.is_empty());
185    }
186
187    #[test]
188    fn required_capabilities_roundtrip_when_set() {
189        let mut t = sample();
190        t.required_capabilities = vec!["exec".into(), "read".into()];
191        let j = serde_json::to_string(&t).unwrap();
192        let back: ToolDefinition = serde_json::from_str(&j).unwrap();
193        assert_eq!(back.required_capabilities, vec!["exec", "read"]);
194    }
195
196    #[test]
197    fn tier_omitted_when_none() {
198        let j = serde_json::to_string(&sample()).unwrap();
199        assert!(
200            !j.contains("\"tier\""),
201            "tier should be skipped when None: {j}"
202        );
203    }
204}