1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6const MAX_DESCRIPTION_CHARS: usize = 4000;
8
9use super::validation::{paths, present, script, text, timeout};
10
11#[derive(Debug, Clone, Serialize)]
12#[serde(untagged)]
13pub enum AgentTool {
14 Command(CommandTool),
15 Builtin(BuiltinTool),
16}
17
18impl<'de> Deserialize<'de> for AgentTool {
19 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
20 use serde::de::Error;
21 let value = Value::deserialize(deserializer)?;
22 if value.get("builtin").is_some() {
23 serde_json::from_value(value).map(Self::Builtin)
24 } else {
25 serde_json::from_value(value).map(Self::Command)
26 }
27 .map_err(D::Error::custom)
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(
33 rename_all(serialize = "camelCase", deserialize = "snake_case"),
34 deny_unknown_fields,
35 try_from = "CommandToolInput"
36)]
37pub struct CommandTool {
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub session: Option<super::ToolOperationName>,
40 pub description: String,
41 #[serde(default = "empty_schema")]
43 pub input_schema: Arc<Value>,
44 pub protocol: ToolProtocol,
45 #[serde(serialize_with = "crate::platform::serialize_program")]
46 pub command: String,
47 pub args: Vec<crate::scope::Argument>,
48 #[serde(
49 default,
50 deserialize_with = "timeout",
51 serialize_with = "super::validation::milliseconds::serialize",
52 skip_serializing_if = "Option::is_none"
53 )]
54 pub timeout_ms: Option<std::time::Duration>,
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
56 pub execution_paths: Vec<super::LogicalPath>,
57 #[serde(skip)]
58 pub(crate) plain_args: Vec<super::plain::PlainArgument>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(
63 rename_all(serialize = "camelCase", deserialize = "snake_case"),
64 deny_unknown_fields
65)]
66struct CommandToolInput {
67 #[serde(default)]
68 pub session: Option<super::ToolOperationName>,
69 pub description: String,
70 #[serde(default = "empty_schema")]
71 pub input_schema: Value,
72 pub protocol: ToolProtocol,
73 pub command: String,
74 pub args: Vec<crate::scope::Argument>,
75 #[serde(
76 default,
77 deserialize_with = "timeout",
78 serialize_with = "super::validation::milliseconds::serialize",
79 skip_serializing_if = "Option::is_none"
80 )]
81 pub timeout_ms: Option<std::time::Duration>,
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
83 pub execution_paths: Vec<super::LogicalPath>,
84}
85
86impl TryFrom<CommandToolInput> for CommandTool {
87 type Error = String;
88 fn try_from(input: CommandToolInput) -> Result<Self, Self::Error> {
89 let plain_args = if input.protocol == ToolProtocol::Plain {
90 input
91 .args
92 .iter()
93 .map(|arg| {
94 super::plain::PlainArgument::parse(&arg.to_string(), &input.input_schema)
95 })
96 .collect::<Result<_, _>>()?
97 } else {
98 Vec::new()
99 };
100 Ok(Self {
101 session: input.session,
102 description: input.description,
103 input_schema: Arc::new(input.input_schema),
104 protocol: input.protocol,
105 command: input.command,
106 args: input.args,
107 timeout_ms: input.timeout_ms,
108 execution_paths: input.execution_paths,
109 plain_args,
110 })
111 }
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115#[serde(deny_unknown_fields)]
116pub struct ToolSession {
117 pub start: SessionCommand,
118 pub stop: SessionCommand,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(
123 rename_all(serialize = "camelCase", deserialize = "snake_case"),
124 deny_unknown_fields
125)]
126pub struct SessionCommand {
127 #[serde(serialize_with = "crate::platform::serialize_program")]
128 pub command: String,
129 pub args: Vec<crate::scope::Argument>,
130 #[serde(default, skip_serializing_if = "Vec::is_empty")]
131 pub execution_paths: Vec<super::LogicalPath>,
132}
133
134impl SessionCommand {
135 pub(super) fn validate(&self) -> Result<(), String> {
136 script(&self.command, &self.args)?;
137 if self.command.contains(['{', '}']) {
138 return Err("Session command must not contain placeholders.".into());
139 }
140 paths(&self.execution_paths, "execution_paths")
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "lowercase")]
146pub enum ToolProtocol {
147 Json,
148 Plain,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(deny_unknown_fields)]
153pub struct BuiltinTool {
154 pub builtin: Builtin,
155 #[serde(
156 default,
157 deserialize_with = "present",
158 skip_serializing_if = "Option::is_none"
159 )]
160 pub args: Option<Vec<String>>,
161 #[serde(
162 default,
163 deserialize_with = "present",
164 skip_serializing_if = "Option::is_none"
165 )]
166 pub description: Option<String>,
167}
168
169pub use artifactize_tools::Builtin;
170
171impl AgentTool {
172 pub(super) fn validate(&self, schemas: &mut super::schemas::Schemas) -> Result<(), String> {
174 match self {
175 Self::Command(tool) => {
176 description(&tool.description)?;
177 script(&tool.command, &tool.args)?;
178 if tool.command.contains(['{', '}']) {
179 return Err("Tool command must not contain placeholders.".into());
180 }
181 paths(&tool.execution_paths, "execution_paths")?;
182 schemas.check(&tool.input_schema, |schema| {
183 crate::tools::schema::compile(schema).map(drop)
184 })
185 }
186 Self::Builtin(tool) => {
187 if let Some(value) = &tool.description {
188 description(value)?;
189 }
190 artifactize_tools::builtin::validate_args(
191 tool.builtin,
192 tool.args.as_deref(),
193 false,
194 )?;
195 if let Some(args) = &tool.args {
196 validate_agent_target(tool.builtin, args)?;
197 }
198 Ok(())
199 }
200 }
201 }
202 pub fn input_schema(&self) -> Value {
203 match self {
204 Self::Command(tool) => Value::clone(&tool.input_schema),
205 Self::Builtin(tool) => match &tool.args {
206 Some(args) => crate::tools::builtin::fixed_schema(tool.builtin, args),
207 None => crate::tools::builtin::input_schema(tool.builtin),
208 },
209 }
210 }
211}
212
213#[derive(Debug, Clone, Serialize)]
214#[serde(untagged)]
215pub enum HumanTool {
216 Command(HumanCommandTool),
217 Builtin(HumanBuiltinTool),
218}
219
220impl<'de> Deserialize<'de> for HumanTool {
221 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
222 use serde::de::Error;
223 let value = Value::deserialize(deserializer)?;
224 if value.get("builtin").is_some() {
225 serde_json::from_value(value).map(Self::Builtin)
226 } else {
227 serde_json::from_value(value).map(Self::Command)
228 }
229 .map_err(D::Error::custom)
230 }
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(
235 rename_all(serialize = "camelCase", deserialize = "snake_case"),
236 deny_unknown_fields
237)]
238pub struct HumanCommandTool {
239 pub description: String,
240 pub kind: HumanToolKind,
241 #[serde(serialize_with = "crate::platform::serialize_program")]
242 pub command: String,
243 #[serde(deserialize_with = "crate::scope::human_arguments")]
244 pub args: Vec<crate::scope::Argument>,
245 #[serde(
246 default,
247 deserialize_with = "timeout",
248 serialize_with = "super::validation::milliseconds::serialize",
249 skip_serializing_if = "Option::is_none"
250 )]
251 pub timeout_ms: Option<std::time::Duration>,
252}
253
254#[derive(Debug, Clone)]
255pub struct HumanBuiltinTool {
256 pub builtin: Builtin,
257 pub description: String,
258 pub args: Vec<crate::scope::Argument>,
259}
260
261impl<'de> Deserialize<'de> for HumanBuiltinTool {
262 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
263 use serde::de::Error;
264 #[derive(Deserialize)]
265 #[serde(deny_unknown_fields)]
266 struct Declaration {
267 builtin: Builtin,
268 #[serde(default, deserialize_with = "present")]
269 description: Option<String>,
270 #[serde(default, deserialize_with = "present")]
271 kind: Option<HumanToolKind>,
272 #[serde(deserialize_with = "crate::scope::human_arguments")]
273 args: Vec<crate::scope::Argument>,
274 }
275 let declaration = Declaration::deserialize(deserializer)?;
276 let args: Vec<String> = declaration.args.iter().map(ToString::to_string).collect();
277 artifactize_tools::builtin::validate_args(declaration.builtin, Some(&args), true)
278 .map_err(D::Error::custom)?;
279 let kind = if matches!(declaration.builtin, Builtin::Open | Builtin::Graph) {
280 HumanToolKind::Launch
281 } else {
282 HumanToolKind::Output
283 };
284 if declaration.kind.is_some_and(|declared| declared != kind) {
285 return Err(D::Error::custom(
286 "Human builtin kind disagrees with its tool.",
287 ));
288 }
289 Ok(Self {
290 builtin: declaration.builtin,
291 description: declaration.description.unwrap_or_else(|| {
292 crate::tools::builtin::fixed_description(declaration.builtin, &args).into()
293 }),
294 args: declaration.args,
295 })
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
300#[serde(rename_all = "lowercase")]
301pub enum HumanToolKind {
302 Launch,
303 Output,
304}
305
306impl HumanTool {
307 pub fn description(&self) -> &str {
308 match self {
309 Self::Command(tool) => &tool.description,
310 Self::Builtin(tool) => &tool.description,
311 }
312 }
313 pub fn kind(&self) -> HumanToolKind {
314 match self {
315 Self::Command(tool) => tool.kind,
316 Self::Builtin(tool) => tool.kind(),
317 }
318 }
319 pub(super) fn validate(&self) -> Result<(), String> {
320 description(self.description())?;
321 match self {
322 Self::Command(tool) => {
323 script(&tool.command, &tool.args)?;
324 if tool.command.contains(['{', '}']) {
325 return Err("Tool command must not contain placeholders.".into());
326 }
327 for argument in &tool.args {
328 crate::scope::validate_human_argument(argument)
329 .map_err(|error| error.to_string())?;
330 }
331 }
332 Self::Builtin(tool) => {
333 for argument in &tool.args {
334 crate::scope::validate_human_argument(argument)
335 .map_err(|error| error.to_string())?;
336 }
337 if !matches!(tool.builtin, Builtin::Help | Builtin::Graph)
338 && let crate::scope::Argument::Literal(target) = &tool.args[0]
339 && !(tool.builtin == Builtin::Open
340 && artifactize_tools::builtin::is_url(target))
341 {
342 artifactize_tools::scope::logical_path(target)
343 .map_err(|error| error.to_string())?;
344 }
345 if tool.builtin == Builtin::Help
346 && tool
347 .args
348 .iter()
349 .any(|arg| matches!(arg, crate::scope::Argument::Reference { .. }))
350 {
351 return Err(
352 "Help args are literal program/subcommand names, not path placeholders."
353 .into(),
354 );
355 }
356 }
357 }
358 Ok(())
359 }
360}
361
362fn validate_agent_target(builtin: Builtin, args: &[String]) -> Result<(), String> {
363 if builtin == Builtin::Graph {
364 return Ok(());
365 }
366 if builtin == Builtin::Help {
367 if args.iter().any(|arg| arg.contains(['{', '}'])) {
368 return Err("Help args are literal program/subcommand names.".into());
369 }
370 return Ok(());
371 }
372 artifactize_tools::scope::logical_path(&args[0]).map_err(|error| error.to_string())
373}
374
375fn empty_schema() -> Value {
376 serde_json::json!({"type":"object", "additionalProperties":false})
377}
378
379pub(super) fn description(value: &str) -> Result<(), String> {
380 text(value, "Tool description")?;
381 if value.encode_utf16().count() > MAX_DESCRIPTION_CHARS
382 || value.replace("{artifactName}", "").contains(['{', '}'])
383 {
384 return Err(
385 "Tool description is limited to 4000 characters and supports only {artifactName}."
386 .into(),
387 );
388 }
389 Ok(())
390}
391
392impl HumanBuiltinTool {
393 pub fn kind(&self) -> HumanToolKind {
394 if matches!(self.builtin, Builtin::Open | Builtin::Graph) {
395 HumanToolKind::Launch
396 } else {
397 HumanToolKind::Output
398 }
399 }
400}
401impl Serialize for HumanBuiltinTool {
402 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
403 #[derive(Serialize)]
404 struct Stored<'a> {
405 builtin: Builtin,
406 description: &'a str,
407 kind: HumanToolKind,
408 args: &'a [crate::scope::Argument],
409 }
410 Stored {
411 builtin: self.builtin,
412 description: &self.description,
413 kind: self.kind(),
414 args: &self.args,
415 }
416 .serialize(serializer)
417 }
418}