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