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