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