1#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
2pub struct RemoteToolGrant {
3 pub protocol_version: u32,
4 #[serde(default, skip_serializing_if = "Option::is_none")]
5 pub id: Option<String>,
6 pub name: String,
7 #[serde(default, skip_serializing_if = "String::is_empty")]
8 pub description: String,
9 #[serde(default = "default_input_schema")]
10 pub input_schema: serde_json::Value,
11 #[serde(default)]
12 pub output_schema: serde_json::Value,
13 #[serde(default, skip_serializing_if = "Vec::is_empty")]
14 pub input_schema_projections: Vec<RemoteSchemaProjectionOverride>,
15 #[serde(default, skip_serializing_if = "Vec::is_empty")]
16 pub output_schema_projections: Vec<RemoteSchemaProjectionOverride>,
17 #[serde(default, skip_serializing_if = "RemoteToolOutputContract::is_static")]
18 pub output_contract: RemoteToolOutputContract,
19 #[serde(default, skip_serializing_if = "Vec::is_empty")]
20 pub examples: Vec<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub availability: Option<RemoteToolAvailability>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub activation: Option<RemoteToolActivation>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub argument_projection: Option<RemoteToolArgumentProjectionPolicy>,
27 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub scheduling: Option<RemoteToolScheduling>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub retry_policy: Option<RemoteToolRetryPolicy>,
31 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
32 pub bindings: BTreeMap<String, serde_json::Value>,
33}
34
35impl RemoteToolGrant {
36 pub fn binding_call_path(&self, binding_key: &str) -> Result<String, RemoteProtocolError> {
37 let binding = self.required_call_path_binding(binding_key)?;
38 Ok(format!(
39 "{}.{}",
40 binding.module_path.join("."),
41 binding.operation
42 ))
43 }
44
45 pub fn validate(&self) -> Result<(), RemoteProtocolError> {
46 ensure_protocol_version(self.protocol_version)?;
47 if self.name.trim().is_empty() {
48 return Err(RemoteProtocolError::InvalidToolGrant {
49 tool_name: self.name.clone(),
50 message: "tool grant name cannot be empty".to_string(),
51 });
52 }
53 for key in self.bindings.keys() {
54 if key.trim().is_empty() {
55 return Err(RemoteProtocolError::InvalidToolGrant {
56 tool_name: self.name.clone(),
57 message: "tool grant binding keys cannot be empty".to_string(),
58 });
59 }
60 }
61 Ok(())
62 }
63
64 pub fn validate_all(grants: &[Self]) -> Result<(), RemoteProtocolError> {
65 let mut seen = HashSet::new();
66 for grant in grants {
67 grant.validate()?;
68 for call_path in grant.call_path_bindings()? {
69 if !seen.insert(call_path.clone()) {
70 return Err(RemoteProtocolError::DuplicateRemoteCallPath { call_path });
71 }
72 }
73 }
74 Ok(())
75 }
76
77 pub fn call_path_bindings(&self) -> Result<Vec<String>, RemoteProtocolError> {
78 let mut paths = Vec::new();
79 for (key, value) in &self.bindings {
80 if let Some(binding) = RemoteCallPathBinding::from_value(value) {
81 validate_call_path_binding(&self.name, key, &binding)?;
82 paths.push(format!(
83 "{}.{}",
84 binding.module_path.join("."),
85 binding.operation
86 ));
87 }
88 }
89 Ok(paths)
90 }
91
92 fn required_call_path_binding(
93 &self,
94 binding_key: &str,
95 ) -> Result<RemoteCallPathBinding, RemoteProtocolError> {
96 let Some(value) = self.bindings.get(binding_key) else {
97 return Err(RemoteProtocolError::MissingToolBinding {
98 tool_name: self.name.clone(),
99 binding: binding_key.to_string(),
100 });
101 };
102 let Some(binding) = RemoteCallPathBinding::from_value(value) else {
103 return Err(RemoteProtocolError::InvalidToolGrant {
104 tool_name: self.name.clone(),
105 message: format!("tool binding `{binding_key}` does not expose a call path"),
106 });
107 };
108 validate_call_path_binding(&self.name, binding_key, &binding)?;
109 Ok(binding)
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
114pub struct RemoteCallPathBinding {
115 pub module_path: Vec<String>,
116 pub operation: String,
117}
118
119impl RemoteCallPathBinding {
120 fn from_value(value: &serde_json::Value) -> Option<Self> {
121 let module_path = value
122 .get("module_path")?
123 .as_array()?
124 .iter()
125 .map(|part| part.as_str().map(ToOwned::to_owned))
126 .collect::<Option<Vec<_>>>()?;
127 let operation = value.get("operation")?.as_str()?.to_string();
128 Some(Self {
129 module_path,
130 operation,
131 })
132 }
133}
134
135fn validate_call_path_binding(
136 tool_name: &str,
137 binding_key: &str,
138 binding: &RemoteCallPathBinding,
139) -> Result<(), RemoteProtocolError> {
140 if binding.module_path.is_empty() {
141 return Err(RemoteProtocolError::InvalidToolGrant {
142 tool_name: tool_name.to_string(),
143 message: format!("tool binding `{binding_key}` requires an explicit module path"),
144 });
145 }
146 if binding.module_path.iter().any(|part| part.trim().is_empty()) {
147 return Err(RemoteProtocolError::InvalidToolGrant {
148 tool_name: tool_name.to_string(),
149 message: format!(
150 "tool binding `{binding_key}` module path cannot contain empty segments"
151 ),
152 });
153 }
154 if binding.operation.trim().is_empty() {
155 return Err(RemoteProtocolError::InvalidToolGrant {
156 tool_name: tool_name.to_string(),
157 message: format!("tool binding `{binding_key}` requires an explicit operation"),
158 });
159 }
160 Ok(())
161}
162
163fn default_input_schema() -> serde_json::Value {
164 serde_json::json!({
165 "type": "object",
166 "properties": {},
167 "additionalProperties": true
168 })
169}
170
171#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
172pub struct RemoteSchemaProjectionOverride {
173 pub profile: String,
174 pub schema: serde_json::Value,
175}
176
177#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
178#[serde(rename_all = "snake_case")]
179pub enum RemoteToolAvailability {
180 Off,
181 Searchable,
182 Callable,
183 #[default]
184 Showcased,
185}
186
187#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
188#[serde(rename_all = "snake_case")]
189pub enum RemoteToolActivation {
190 #[default]
191 Always,
192 Internal,
193}
194
195#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
196#[serde(rename_all = "snake_case")]
197pub enum RemoteToolScheduling {
198 #[default]
199 Parallel,
200 Serial,
201}
202
203#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
204#[serde(tag = "kind", rename_all = "snake_case")]
205pub enum RemoteToolOutputContract {
206 #[default]
207 Static,
208 FromInputSchema {
209 input_field: String,
210 #[serde(default, skip_serializing_if = "Option::is_none")]
211 default_schema: Option<serde_json::Value>,
212 },
213}
214
215impl RemoteToolOutputContract {
216 fn is_static(&self) -> bool {
217 matches!(self, Self::Static)
218 }
219}
220
221#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
222#[serde(tag = "kind", rename_all = "snake_case")]
223pub enum RemoteToolArgumentProjectionPolicy {
224 #[default]
225 MaterializeProjectedValues,
226 PreserveProjectedRefsInField {
227 field: String,
228 },
229}
230
231#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
232#[serde(tag = "type", rename_all = "snake_case")]
233pub enum RemoteToolRetryPolicy {
234 #[default]
235 Never,
236 Safe {
237 max_attempts: u32,
238 base_delay_ms: u64,
239 max_delay_ms: u64,
240 },
241 Idempotent {
242 max_attempts: u32,
243 base_delay_ms: u64,
244 max_delay_ms: u64,
245 },
246}