Skip to main content

vtcode_utility_tool_specs/
lib.rs

1#![allow(
2    missing_docs,
3    dead_code,
4    unused_imports,
5    reason = "Intentional compatibility, platform, or test-only suppression."
6)]
7
8//! Passive JSON schemas for utility, file, scheduling, and collaboration tool surfaces.
9
10#![recursion_limit = "256"]
11use serde_json::{Value, json};
12
13mod collaboration;
14mod json_schema;
15#[cfg(feature = "mcp")]
16mod mcp_tool;
17mod responses_api;
18mod tool_kind;
19
20pub use collaboration::{
21    AGENT_DESCRIPTION, SUBAGENT_REASONING_EFFORT_VALUES, agent_parameters, close_agent_parameters,
22    request_user_input_description, request_user_input_parameters, resume_agent_parameters, send_input_parameters,
23    spawn_agent_parameters, spawn_background_subprocess_parameters, wait_agent_parameters,
24};
25pub use json_schema::{AdditionalProperties, JsonSchema, parse_tool_input_schema};
26#[cfg(feature = "mcp")]
27pub use mcp_tool::{ParsedMcpTool, parse_mcp_tool};
28pub use responses_api::{FreeformTool, FreeformToolFormat, ResponsesApiTool};
29pub(crate) use tool_kind::{CanonicalToolMeta, TokenBucket, ToolKind, ToolNamespace};
30
31pub const SEMANTIC_ANCHOR_GUIDANCE: &str =
32    "Prefer stable semantic @@ anchors such as function, class, method, or impl names.";
33
34/// Explicit, format-bearing description for the `patch` alias field. The old
35/// value ("Alias for input") gave the model no format guidance, so it often
36/// placed a standard unified diff (`---`/`+++`) there — which `apply_patch`
37/// rejects. This mirrors the `input` description so both alias fields carry
38/// identical, complete format guidance (see checkpoint turn_615 for the
39/// failure this prevents).
40pub const APPLY_PATCH_ALIAS_DESCRIPTION: &str = "Patch in VT Code format (*** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch). Same envelope as 'input'; standard unified diffs (--- /+++ format) are rejected. Every patch path must be workspace-relative; absolute paths, `..`, and traversal-like forms are rejected.";
41pub const DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION: &str = "Patch in VT Code format: *** Begin Patch, *** Update File: path, @@ hunk, -/+ lines, *** End Patch. Every patch path must be workspace-relative; absolute paths, `..`, and traversal-like forms are rejected.";
42/// Model-visible description of the `apply_patch` tool. It leads with the
43/// accepted envelope so the model writes the right format on the first try,
44/// and states the unified-diff rejection and path rules as plain facts
45/// instead of shouted warnings. Registration sites append
46/// [`SEMANTIC_ANCHOR_GUIDANCE`] via [`with_semantic_anchor_guidance`].
47pub const APPLY_PATCH_TOOL_DESCRIPTION: &str = "Apply a patch in VT Code format (*** Begin Patch / *** Update File: path / @@ hunks with -/+ lines / *** End Patch); standard unified diffs (---/+++ format) are rejected. *** Add File: path, *** Delete File: path, and *** Move to: path (after *** Update File) are also supported. Every patch path must be workspace-relative; absolute paths, `..`, and traversal-like forms are rejected. Changes are applied after permission checks.";
48
49/// Default model-visible preview budget for function-tool results.
50pub const DEFAULT_MAX_OUTPUT_TOKENS: usize = 10_000;
51/// Smallest valid model-visible preview budget for a function-tool result.
52pub const MIN_MAX_OUTPUT_TOKENS: usize = 1;
53/// Largest valid model-visible preview budget for a function-tool result.
54pub const MAX_MAX_OUTPUT_TOKENS: usize = 50_000;
55/// Canonical JSON property name for the common result-preview budget field.
56///
57/// Centralized so the strict dedicated validator in `vtcode-core` and any
58/// compatibility coercion both reference one name and cannot drift.
59pub const MAX_OUTPUT_TOKENS_FIELD: &str = "max_output_tokens";
60
61/// Adds the common result-preview budget field to an object-shaped tool schema.
62///
63/// The returned schema deliberately preserves every existing constraint,
64/// including `additionalProperties`, so callers can use it for legacy schemas
65/// without weakening their argument validation.
66#[must_use]
67pub fn with_max_output_tokens_parameter(mut schema: Value) -> Value {
68    let Some(schema_object) = schema.as_object_mut() else {
69        return schema;
70    };
71    let properties = schema_object
72        .entry("properties")
73        .or_insert_with(|| Value::Object(serde_json::Map::new()));
74    let Some(properties_object) = properties.as_object_mut() else {
75        return schema;
76    };
77    {
78        let _entry = properties_object.entry(MAX_OUTPUT_TOKENS_FIELD).or_insert_with(|| {
79            json!({
80                "type": "integer",
81                "minimum": MIN_MAX_OUTPUT_TOKENS,
82                "maximum": MAX_MAX_OUTPUT_TOKENS,
83                "default": DEFAULT_MAX_OUTPUT_TOKENS,
84                "description": "Maximum number of result tokens returned to the model. Full oversized output is spooled when available."
85            })
86        });
87    }
88    schema
89}
90
91#[must_use]
92pub fn with_semantic_anchor_guidance(base: &str) -> String {
93    let trimmed = base.trim_end();
94    if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
95        trimmed.to_string()
96    } else if trimmed.ends_with('.') {
97        format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
98    } else {
99        format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
100    }
101}
102
103#[must_use]
104pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
105    json!({
106        "type": "object",
107        "properties": {
108            "input": {
109                "type": "string",
110                "description": with_semantic_anchor_guidance(input_description)
111            },
112            "patch": {
113                "type": "string",
114                "description": with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
115            }
116        },
117        "anyOf": [
118            {"required": ["input"]},
119            {"required": ["patch"]}
120        ]
121    })
122}
123
124#[must_use]
125pub fn apply_patch_parameters() -> Value {
126    apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
127}
128
129#[must_use]
130pub fn cron_parameters() -> Value {
131    json!({
132        "type": "object",
133        "required": ["action"],
134        "additionalProperties": false,
135        "properties": {
136            "action": {
137                "type": "string",
138                "enum": ["create", "list", "delete"],
139                "description": "create: schedule a prompt (requires prompt and exactly one of cron, delay_minutes, or run_at). list: show scheduled prompts. delete: remove one by id."
140            },
141            "prompt": {"type": "string", "description": "create: prompt to run when the task fires."},
142            "name": {"type": "string", "description": "create: optional short label for the task."},
143            "cron": {"type": "string", "description": "create: five-field cron expression for recurring tasks."},
144            "delay_minutes": {"type": "integer", "description": "create: fixed recurring interval in minutes."},
145            "run_at": {"type": "string", "description": "create: one-shot fire time in RFC3339 or local datetime form."},
146            "id": {"type": "string", "description": "delete: session scheduled task id to delete."}
147        }
148    })
149}
150
151/// Model-visible description of the `mcp` tool.
152pub const MCP_DESCRIPTION: &str = "Discover and manage Model Context Protocol capabilities. Use action=search_tools to find tools, action=get_tool_details to fetch one schema, action=list_servers to inspect configured servers, or action=connect and action=disconnect to manage a named server. action=search_tools searches only tools exposed by configured MCP servers; the separate search_tools tool searches the whole session catalog, including deferred built-in tools. Do not disconnect a server while one of its tool calls is active.";
153
154#[must_use]
155pub fn mcp_parameters() -> Value {
156    json!({
157        "type": "object",
158        "required": ["action"],
159        "properties": {
160            "action": {
161                "type": "string",
162                "enum": ["search_tools", "get_tool_details", "list_servers", "connect", "disconnect"],
163                "description": "search_tools: find MCP tools by natural-language query. get_tool_details: fetch the full input schema for one MCP tool name. list_servers: list configured servers and their connection state. connect or disconnect: manage one configured MCP server by name."
164            },
165            "query": {"type": "string", "description": "search_tools: natural language query describing the MCP capability to find."},
166            "detail_level": {"type": "string", "enum": ["name", "name_description", "full"], "description": "search_tools: response detail level."},
167            "limit": {"type": "integer", "minimum": 1, "maximum": 25, "description": "search_tools: maximum number of results to return."},
168            "name": {"type": "string", "description": "get_tool_details: exact MCP tool name. connect or disconnect: configured MCP server name."}
169        },
170        "additionalProperties": false
171    })
172}
173
174#[must_use]
175pub fn cron_create_parameters() -> Value {
176    json!({
177        "type": "object",
178        "required": ["prompt"],
179        "additionalProperties": false,
180        "properties": {
181            "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
182            "name": {"type": "string", "description": "Optional short label for the task."},
183            "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
184            "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
185            "run_at": {
186                "type": "string",
187                "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
188            }
189        }
190    })
191}
192
193#[must_use]
194pub fn cron_list_parameters() -> Value {
195    json!({
196        "type": "object",
197        "properties": {},
198        "additionalProperties": false
199    })
200}
201
202#[must_use]
203pub fn cron_delete_parameters() -> Value {
204    json!({
205        "type": "object",
206        "required": ["id"],
207        "properties": {
208            "id": {"type": "string", "description": "Session scheduled task id to delete."}
209        }
210    })
211}
212
213/// Model-visible description of the `exec_command` tool.
214pub const EXEC_COMMAND_DESCRIPTION: &str = "Run a shell command through the active sandbox policy and permission checks. Put normal shell tools such as ls, rg, find, cat, sed, awk, build tools, and test tools in cmd. Returns output, exit status, and a reusable session id when the command is still running. For file edits, use apply_patch instead of shell redirection or in-place editors such as `sed -i`. Expanded sandbox_permissions modes trigger an approval check before the command runs; `require_escalated` and `bypass_sandbox` also need a non-empty justification.";
215
216#[must_use]
217pub fn exec_command_parameters() -> Value {
218    json!({
219        "type": "object",
220        "required": ["cmd"],
221        "properties": {
222            "cmd": {"type": "string", "description": "Shell command to execute, subject to command policy. Examples include `ls`, `rg`, `find`, `cat`, `sed`, `awk`, build tools, and test tools."},
223            "yield_time_ms": {"type": "integer", "description": "Wait before returning output (ms). If the command is still running, the response includes a session_id for write_stdin. Values above 10000 turn this into a single-call long run: no outer timeout applies and the response returns after the yield window or command exit, whichever is first.", "default": 10000},
224            "background": {"type": "boolean", "description": "Start a retained background process and return after a bounded initial output window. At most three live background processes are allowed per VT Code runtime; use the returned session_id with write_stdin to wait, poll, write, inspect, terminate, or close.", "default": false},
225            "max_output_tokens": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 10000, "description": "Output token cap. Large or truncated output can return a spool_path; an active session may set spool_complete=false for a readable partial snapshot, while an exited pending spool is withheld until a later wait."},
226            "workdir": {"type": "string", "description": "Working directory."},
227            "tty": {"type": "boolean", "description": "Run the command in PTY mode for interactive or terminal-sensitive commands.", "default": false},
228            "sandbox_permissions": {
229                "type": "string",
230                "enum": ["use_default", "with_additional_permissions", "require_escalated", "bypass_sandbox"],
231                "description": "Sandbox permission mode for this command. Omit it or use `use_default` for the normal sandbox. Non-empty `additional_permissions` is normalized to `with_additional_permissions`. `require_escalated` and `bypass_sandbox` require a non-empty `justification` and cannot be combined with `additional_permissions`.",
232                "default": "use_default"
233            },
234            "additional_permissions": {
235                "type": "object",
236                "description": "Optional extra filesystem roots to grant inside the sandbox. Non-empty `additional_permissions` implicitly requests `with_additional_permissions`; every path is normalized and must stay within allowed workspace or temp roots.",
237                "properties": {
238                    "fs_read": {"type": "array", "items": {"type": "string"}},
239                    "fs_write": {"type": "array", "items": {"type": "string"}}
240                },
241                "additionalProperties": false
242            },
243            "justification": {"type": "string", "description": "Short approval question for expanded sandbox scope. Required when sandbox_permissions is `require_escalated` or `bypass_sandbox`."}
244        },
245        "additionalProperties": false
246    })
247}
248
249#[must_use]
250pub fn write_stdin_parameters() -> Value {
251    json!({
252        "type": "object",
253        "required": ["session_id"],
254        "properties": {
255            "session_id": {"type": "string", "description": "Active execution session id."},
256            "action": {"type": "string", "enum": ["write", "poll", "wait"], "description": "Use wait to block until the command exits or wait_timeout_seconds expires; wait never kills the session."},
257            "chars": {"type": "string", "description": "Bytes to write to stdin. Pass an empty string to poll without sending input."},
258            "yield_time_ms": {"type": "integer", "description": "Wait before returning fresh session output (ms).", "default": 1000},
259            "wait_timeout_seconds": {"type": "integer", "minimum": 1, "description": "Explicit wait deadline in seconds. A deadline returns an in-progress session that can be waited on again."},
260            "max_output_tokens": {"type": "integer", "minimum": 1, "maximum": 50000, "default": 10000, "description": "Output token cap for the continuation response. Large or truncated output can return a spool_path; the response reports whether an active session has finished writing it."}
261        },
262        "anyOf": [
263            {"required": ["chars"]},
264            {"required": ["action"], "properties": {"action": {"const": "wait"}}}
265        ],
266        "additionalProperties": false
267    })
268}
269
270/// Model-visible description of the `search_tools` tool.
271pub const SEARCH_TOOLS_DESCRIPTION: &str = "Search the session tool catalog by capability and return ranked matches. Use it to find tools whose definitions are deferred and not yet sent to you, such as code_search, web_fetch, web_search, cron, and MCP server tools. Deferred matches are listed in `expanded_for_next_segment` and become callable on the next request. It is not needed for tools already defined in the current request; call those directly.";
272
273#[must_use]
274pub fn search_tools_parameters() -> Value {
275    with_max_output_tokens_parameter(json!({
276        "type": "object",
277        "required": ["query"],
278        "properties": {
279            "query": {
280                "type": "string",
281                "minLength": 1,
282                "description": "Natural-language description of the capability to discover."
283            },
284            "limit": {
285                "type": "integer",
286                "minimum": 1,
287                "maximum": 25,
288                "default": 5,
289                "description": "Maximum number of ranked matches to return (default: 5, max: 25)."
290            },
291            "detail_level": {
292                "type": "string",
293                "enum": ["name", "name_description", "full"],
294                "default": "name_description",
295                "description": "Fields returned per match (default: name_description). name returns the tool name and score; name_description adds the description; full also adds the parameter schema."
296            }
297        },
298        "additionalProperties": false
299    }))
300}
301
302#[must_use]
303pub fn code_search_parameters() -> Value {
304    json!({
305        "type": "object",
306        "required": ["query"],
307        "additionalProperties": false,
308        "properties": {
309            "query": {
310                "type": "string",
311                "minLength": 1,
312                "pattern": "\\S",
313                "description": "Literal code or path query. Smart-case applies to content and exact symbol-name matching: a wholly lower-case query matches case-insensitively, while an upper-case character makes matching case-sensitive. Path matching remains fuzzy and case-insensitive."
314            },
315            "path": {
316                "type": "string",
317                "minLength": 1,
318                "pattern": "\\S",
319                "description": "Workspace-relative file or directory to search. Omit to search the workspace root."
320            },
321            "file_types": {
322                "type": "array",
323                "minItems": 1,
324                "items": {
325                    "type": "string",
326                    "minLength": 1,
327                    "pattern": "\\S"
328                },
329                "description": "Language names or common file extensions, with or without one leading dot."
330            },
331            "result_types": {
332                "type": "array",
333                "minItems": 1,
334                "items": {
335                    "type": "string",
336                    "enum": ["definition", "usage", "text", "path"]
337                },
338                "description": "Result categories to include. Omit to include all four categories."
339            },
340            "max_results": {
341                "type": "integer",
342                "minimum": 1,
343                "maximum": 100,
344                "description": "Maximum number of merged results to return. Omit for 20."
345            }
346        }
347    })
348}
349
350#[must_use]
351pub fn list_files_parameters() -> Value {
352    json!({
353        "type": "object",
354        "properties": {
355            "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
356            "mode": {
357                "type": "string",
358                "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
359                "description": "Listing mode. Use page/per_page to continue paginated results.",
360                "default": "list"
361            },
362            "pattern": {"type": "string", "description": "Optional glob-style path filter."},
363            "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
364            "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
365            "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
366            "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
367            "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
368            "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
369            "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
370            "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
371        }
372    })
373}
374
375#[cfg(test)]
376mod tests {
377    use serde_json::json;
378
379    use super::*;
380
381    #[test]
382    fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
383        let schema = apply_patch_parameter_schema("Patch in VT Code format");
384
385        // Both `input` and `patch` alias fields now carry the format
386        // description AND the semantic-anchor guidance, preventing the model
387        // from placing a unified diff in `patch` (see checkpoint turn_615).
388        assert_eq!(
389            schema["properties"]["patch"]["description"],
390            with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
391        );
392        let patch_description = schema["properties"]["patch"]["description"]
393            .as_str()
394            .expect("patch description");
395        assert!(patch_description.contains("*** Begin Patch"));
396        assert!(patch_description.contains("unified diff"));
397        assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
398
399        let input_description = schema["properties"]["input"]["description"]
400            .as_str()
401            .expect("input description");
402        assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
403    }
404
405    #[test]
406    fn search_tools_schema_documents_limit_and_detail_level_defaults() {
407        let schema = search_tools_parameters();
408        let limit = &schema["properties"]["limit"];
409        assert_eq!(limit["default"], json!(5));
410        assert_eq!(limit["maximum"], json!(25));
411        let limit_description = limit["description"].as_str().expect("limit description");
412        assert!(limit_description.contains("default: 5"));
413        assert!(limit_description.contains("max: 25"));
414
415        let detail_level = &schema["properties"]["detail_level"];
416        assert_eq!(detail_level["default"], json!("name_description"));
417        let detail_description = detail_level["description"].as_str().expect("detail_level description");
418        for level in ["name", "name_description", "full"] {
419            assert!(detail_description.contains(level), "{level}");
420        }
421
422        assert!(SEARCH_TOOLS_DESCRIPTION.contains("deferred"));
423        assert!(SEARCH_TOOLS_DESCRIPTION.contains("next request"));
424        assert!(SEARCH_TOOLS_DESCRIPTION.contains("MCP"));
425        assert!(SEARCH_TOOLS_DESCRIPTION.contains("not needed"));
426    }
427
428    #[test]
429    fn apply_patch_tool_description_leads_with_format_and_stays_calm() {
430        assert!(APPLY_PATCH_TOOL_DESCRIPTION.starts_with("Apply a patch in VT Code format (*** Begin Patch"));
431        assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("unified diffs"));
432        assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("workspace-relative"));
433        assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("permission checks"));
434        for description in [
435            APPLY_PATCH_TOOL_DESCRIPTION,
436            APPLY_PATCH_ALIAS_DESCRIPTION,
437            DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION,
438        ] {
439            assert!(!description.contains("IMPORTANT"), "{description}");
440            assert!(!description.contains("NOT"), "{description}");
441            assert!(!description.contains("never"), "{description}");
442        }
443    }
444
445    #[test]
446    fn common_output_limit_parameter_preserves_strict_schema() {
447        let schema = with_max_output_tokens_parameter(json!({
448            "type": "object",
449            "properties": {"query": {"type": "string"}},
450            "additionalProperties": false
451        }));
452        assert_eq!(schema["additionalProperties"], json!(false));
453        assert_eq!(schema["properties"]["max_output_tokens"]["default"], json!(DEFAULT_MAX_OUTPUT_TOKENS));
454        assert_eq!(schema["properties"]["max_output_tokens"]["minimum"], json!(MIN_MAX_OUTPUT_TOKENS));
455        assert_eq!(schema["properties"]["max_output_tokens"]["maximum"], json!(MAX_MAX_OUTPUT_TOKENS));
456    }
457
458    #[test]
459    fn codex_baseline_exec_schemas_use_public_names_shape() {
460        let exec_params = exec_command_parameters();
461        assert_eq!(exec_params["required"], json!(["cmd"]));
462        assert!(exec_params["properties"]["cmd"].is_object());
463        assert!(exec_params["properties"]["workdir"].is_object());
464        assert!(
465            exec_params["properties"]["yield_time_ms"]["description"]
466                .as_str()
467                .expect("exec yield description")
468                .contains("session_id")
469        );
470        assert!(
471            exec_params["properties"]["yield_time_ms"]["description"]
472                .as_str()
473                .expect("exec yield description")
474                .contains("single-call long run")
475        );
476        assert!(
477            exec_params["properties"]["max_output_tokens"]["description"]
478                .as_str()
479                .expect("exec max output description")
480                .contains("spool_path")
481        );
482        assert_eq!(exec_params["properties"]["tty"]["type"], "boolean");
483        assert_eq!(exec_params["properties"]["tty"]["default"], false);
484        assert_eq!(exec_params["properties"]["background"]["type"], "boolean");
485        assert_eq!(exec_params["properties"]["background"]["default"], false);
486        assert_eq!(exec_params["properties"]["yield_time_ms"]["default"], 10000);
487        assert_eq!(
488            exec_params["properties"]["sandbox_permissions"]["enum"],
489            json!([
490                "use_default",
491                "with_additional_permissions",
492                "require_escalated",
493                "bypass_sandbox"
494            ])
495        );
496        assert_eq!(exec_params["properties"]["sandbox_permissions"]["default"], "use_default");
497        assert_eq!(
498            exec_params["properties"]["additional_permissions"]["properties"]["fs_read"]["items"]["type"],
499            "string"
500        );
501        assert_eq!(
502            exec_params["properties"]["additional_permissions"]["properties"]["fs_write"]["items"]["type"],
503            "string"
504        );
505        assert_eq!(exec_params["properties"]["additional_permissions"]["additionalProperties"], false);
506        assert_eq!(exec_params["properties"]["justification"]["type"], "string");
507        assert!(
508            exec_params["properties"]["sandbox_permissions"]["description"]
509                .as_str()
510                .expect("sandbox_permissions description")
511                .contains("normalized to `with_additional_permissions`")
512        );
513        assert!(
514            exec_params["properties"]["additional_permissions"]["description"]
515                .as_str()
516                .expect("additional_permissions description")
517                .contains("allowed workspace or temp roots")
518        );
519        assert!(
520            exec_params["properties"]["justification"]["description"]
521                .as_str()
522                .expect("justification description")
523                .contains("Required when sandbox_permissions is `require_escalated` or `bypass_sandbox`")
524        );
525        assert_eq!(exec_params["additionalProperties"], false);
526        for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
527            assert!(
528                exec_params["properties"]["cmd"]["description"]
529                    .as_str()
530                    .expect("cmd description")
531                    .contains(command),
532                "{command} should be described as an exec_command.cmd example"
533            );
534            assert!(
535                exec_params["properties"].get(command).is_none(),
536                "{command} must not be modelled as a separate exec_command field"
537            );
538        }
539
540        let stdin_params = write_stdin_parameters();
541        assert_eq!(stdin_params["required"], json!(["session_id"]));
542        assert!(stdin_params["properties"]["session_id"].is_object());
543        assert_eq!(stdin_params["properties"]["chars"]["type"], "string");
544        assert_eq!(stdin_params["properties"]["action"]["enum"], json!(["write", "poll", "wait"]));
545        assert!(stdin_params["properties"]["wait_timeout_seconds"].is_object());
546        assert!(
547            stdin_params["properties"].get("timeout_seconds").is_none(),
548            "write_stdin schema advertises only wait_timeout_seconds"
549        );
550        assert_eq!(stdin_params["anyOf"][1]["required"], json!(["action"]));
551        assert_eq!(stdin_params["anyOf"][1]["properties"]["action"]["const"], "wait");
552        assert!(
553            stdin_params["properties"]["chars"]["description"]
554                .as_str()
555                .is_some_and(|description| description.contains("empty string"))
556        );
557        assert!(stdin_params["properties"]["chars"].is_object());
558        assert!(
559            stdin_params["properties"]["yield_time_ms"]["description"]
560                .as_str()
561                .expect("stdin yield description")
562                .contains("fresh session output")
563        );
564        assert!(
565            stdin_params["properties"]["max_output_tokens"]["description"]
566                .as_str()
567                .expect("stdin max output description")
568                .contains("spool_path")
569        );
570        assert_eq!(stdin_params["additionalProperties"], false);
571    }
572
573    #[test]
574    fn mcp_description_distinguishes_server_search_from_catalog_search() {
575        assert!(MCP_DESCRIPTION.starts_with("Discover and manage Model Context Protocol capabilities."));
576        assert!(MCP_DESCRIPTION.contains("action=search_tools searches only tools exposed by configured MCP servers"));
577        assert!(MCP_DESCRIPTION.contains("the separate search_tools tool searches the whole session catalog"));
578    }
579
580    #[test]
581    fn agent_description_routes_shell_processes_to_exec_command() {
582        assert!(AGENT_DESCRIPTION.starts_with("Spawn and steer delegated child agents."));
583        assert!(AGENT_DESCRIPTION.contains("spawn_subprocess runs a subagent defined with background: true"));
584        assert!(AGENT_DESCRIPTION.contains("go through exec_command, with background=true"));
585        let action = agent_parameters()["properties"]["action"]["description"]
586            .as_str()
587            .unwrap_or_default()
588            .to_string();
589        assert!(!action.contains("daemons"), "{action}");
590    }
591
592    #[test]
593    fn exec_command_description_states_edit_routing_and_escalation_rules() {
594        assert!(EXEC_COMMAND_DESCRIPTION.starts_with("Run a shell command through the active sandbox policy"));
595        assert!(EXEC_COMMAND_DESCRIPTION.contains("For file edits, use apply_patch"));
596        assert!(EXEC_COMMAND_DESCRIPTION.contains("approval check"));
597        assert!(EXEC_COMMAND_DESCRIPTION.contains("non-empty justification"));
598        for mode in ["require_escalated", "bypass_sandbox"] {
599            assert!(EXEC_COMMAND_DESCRIPTION.contains(mode), "{mode}");
600            assert!(
601                exec_command_parameters()["properties"]["sandbox_permissions"]["enum"]
602                    .as_array()
603                    .expect("sandbox_permissions enum")
604                    .iter()
605                    .any(|value| value == mode),
606                "{mode} must stay a real sandbox_permissions value"
607            );
608        }
609    }
610
611    #[test]
612    fn code_search_schema_exposes_exact_five_property_contract() {
613        let params = code_search_parameters();
614        let properties = params["properties"].as_object().expect("properties");
615        let mut property_names = properties.keys().map(String::as_str).collect::<Vec<_>>();
616        property_names.sort_unstable();
617
618        assert_eq!(params["required"], json!(["query"]));
619        assert_eq!(property_names, ["file_types", "max_results", "path", "query", "result_types"]);
620        assert_eq!(params["additionalProperties"], false);
621        assert_eq!(params["properties"]["query"]["pattern"], "\\S");
622        assert_eq!(params["properties"]["file_types"]["minItems"], 1);
623        assert_eq!(params["properties"]["result_types"]["minItems"], 1);
624        assert_eq!(
625            params["properties"]["result_types"]["items"]["enum"],
626            json!(["definition", "usage", "text", "path"])
627        );
628        assert_eq!(params["properties"]["max_results"]["minimum"], 1);
629        assert_eq!(params["properties"]["max_results"]["maximum"], 100);
630        assert!(params.get("anyOf").is_none());
631    }
632
633    #[test]
634    fn legacy_list_files_schema_exposes_pagination_fields() {
635        let list_params = list_files_parameters();
636        assert!(list_params["properties"]["page"].is_object());
637        assert!(list_params["properties"]["per_page"].is_object());
638        assert!(
639            list_params["properties"]["mode"]["enum"]
640                .as_array()
641                .expect("mode enum")
642                .iter()
643                .any(|value| value == "recursive")
644        );
645    }
646
647    #[test]
648    fn semantic_anchor_guidance_is_appended_once() {
649        let base = "Patch in VT Code format.";
650        let with_guidance = with_semantic_anchor_guidance(base);
651
652        assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
653        assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
654    }
655
656    #[test]
657    fn default_apply_patch_parameters_keep_expected_alias_shape() {
658        let schema = apply_patch_parameters();
659
660        assert_eq!(
661            schema["anyOf"],
662            json!([
663                {"required": ["input"]},
664                {"required": ["patch"]}
665            ])
666        );
667    }
668}