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/// Model-visible description of the `patch` alias field. The old value
35/// ("Alias for input") gave the model no format guidance, so it often placed
36/// a standard unified diff (`---`/`+++`) there — which `apply_patch` rejects
37/// (checkpoint turn_615). The full envelope and path rules live on the tool
38/// description and the `input` field; this alias keeps only the
39/// turn_615-critical rejection signal plus the alias relationship, so the
40/// same guidance is not sent three times per request.
41pub const APPLY_PATCH_ALIAS_DESCRIPTION: &str = "Alias for 'input': same VT Code patch envelope (*** Begin Patch … *** End Patch); unified diffs (---/+++ format) are rejected.";
42pub 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.";
43/// Model-visible description of the `apply_patch` tool. It leads with the
44/// accepted envelope so the model writes the right format on the first try,
45/// and states the unified-diff rejection and path rules as plain facts
46/// instead of shouted warnings. Registration sites append
47/// [`SEMANTIC_ANCHOR_GUIDANCE`] via [`with_semantic_anchor_guidance`].
48pub 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. Call this tool directly instead of through a shell; JSON calls use input (patch is an alias). Use complete current context/deletion lines, preserving internal whitespace; boundary whitespace and Unicode punctuation normalization are supported, but partial lines are rejected. A typed context mismatch permits one fresh bounded file read range per affected path per turn; other limits remain authoritative.";
49
50/// Default model-visible preview budget for function-tool results.
51pub const DEFAULT_MAX_OUTPUT_TOKENS: usize = 10_000;
52/// Smallest valid model-visible preview budget for a function-tool result.
53pub const MIN_MAX_OUTPUT_TOKENS: usize = 1;
54/// Largest valid model-visible preview budget for a function-tool result.
55pub const MAX_MAX_OUTPUT_TOKENS: usize = 50_000;
56/// Canonical JSON property name for the common result-preview budget field.
57///
58/// Centralized so the strict dedicated validator in `vtcode-core` and any
59/// compatibility coercion both reference one name and cannot drift.
60pub const MAX_OUTPUT_TOKENS_FIELD: &str = "max_output_tokens";
61
62/// Adds the common result-preview budget field to an object-shaped tool schema.
63///
64/// The returned schema deliberately preserves every existing constraint,
65/// including `additionalProperties`, so callers can use it for legacy schemas
66/// without weakening their argument validation.
67#[must_use]
68pub fn with_max_output_tokens_parameter(mut schema: Value) -> Value {
69    let Some(schema_object) = schema.as_object_mut() else {
70        return schema;
71    };
72    let properties = schema_object
73        .entry("properties")
74        .or_insert_with(|| Value::Object(serde_json::Map::new()));
75    let Some(properties_object) = properties.as_object_mut() else {
76        return schema;
77    };
78    {
79        let _entry = properties_object.entry(MAX_OUTPUT_TOKENS_FIELD).or_insert_with(|| {
80            json!({
81                "type": "integer",
82                "minimum": MIN_MAX_OUTPUT_TOKENS,
83                "maximum": MAX_MAX_OUTPUT_TOKENS,
84                "default": DEFAULT_MAX_OUTPUT_TOKENS,
85                "description": "Maximum number of result tokens returned to the model. Full oversized output is spooled when available."
86            })
87        });
88    }
89    schema
90}
91
92#[must_use]
93pub fn with_semantic_anchor_guidance(base: &str) -> String {
94    let trimmed = base.trim_end();
95    if trimmed.contains(SEMANTIC_ANCHOR_GUIDANCE) {
96        trimmed.to_string()
97    } else if trimmed.ends_with('.') {
98        format!("{trimmed} {SEMANTIC_ANCHOR_GUIDANCE}")
99    } else {
100        format!("{trimmed}. {SEMANTIC_ANCHOR_GUIDANCE}")
101    }
102}
103
104#[must_use]
105pub fn apply_patch_parameter_schema(input_description: &str) -> Value {
106    json!({
107        "type": "object",
108        "properties": {
109            "input": {
110                "type": "string",
111                "description": with_semantic_anchor_guidance(input_description)
112            },
113            "patch": {
114                "type": "string",
115                "description": with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
116            }
117        },
118        "anyOf": [
119            {"required": ["input"]},
120            {"required": ["patch"]}
121        ]
122    })
123}
124
125#[must_use]
126pub fn apply_patch_parameters() -> Value {
127    apply_patch_parameter_schema(DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION)
128}
129
130#[must_use]
131pub fn cron_parameters() -> Value {
132    json!({
133        "type": "object",
134        "required": ["action"],
135        "additionalProperties": false,
136        "properties": {
137            "action": {
138                "type": "string",
139                "enum": ["create", "list", "delete"],
140                "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."
141            },
142            "prompt": {"type": "string", "description": "create: prompt to run when the task fires."},
143            "name": {"type": "string", "description": "create: optional short label for the task."},
144            "cron": {"type": "string", "description": "create: five-field cron expression for recurring tasks."},
145            "delay_minutes": {"type": "integer", "description": "create: fixed recurring interval in minutes."},
146            "run_at": {"type": "string", "description": "create: one-shot fire time in RFC3339 or local datetime form."},
147            "id": {"type": "string", "description": "delete: session scheduled task id to delete."}
148        }
149    })
150}
151
152/// Model-visible description of the `mcp` tool.
153pub 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.";
154
155#[must_use]
156pub fn mcp_parameters() -> Value {
157    json!({
158        "type": "object",
159        "required": ["action"],
160        "properties": {
161            "action": {
162                "type": "string",
163                "enum": ["search_tools", "get_tool_details", "list_servers", "connect", "disconnect"],
164                "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."
165            },
166            "query": {"type": "string", "description": "search_tools: natural language query describing the MCP capability to find."},
167            "detail_level": {"type": "string", "enum": ["name", "name_description", "full"], "description": "search_tools: response detail level."},
168            "limit": {"type": "integer", "minimum": 1, "maximum": 25, "description": "search_tools: maximum number of results to return."},
169            "name": {"type": "string", "description": "get_tool_details: exact MCP tool name. connect or disconnect: configured MCP server name."}
170        },
171        "additionalProperties": false
172    })
173}
174
175#[must_use]
176pub fn cron_create_parameters() -> Value {
177    json!({
178        "type": "object",
179        "required": ["prompt"],
180        "additionalProperties": false,
181        "properties": {
182            "prompt": {"type": "string", "description": "Prompt to run when the task fires."},
183            "name": {"type": "string", "description": "Optional short label for the task."},
184            "cron": {"type": "string", "description": "Five-field cron expression for recurring tasks."},
185            "delay_minutes": {"type": "integer", "description": "Fixed recurring interval in minutes."},
186            "run_at": {
187                "type": "string",
188                "description": "One-shot fire time in RFC3339 or local datetime form. Use this instead of `cron` or `delay_minutes` for reminders."
189            }
190        }
191    })
192}
193
194#[must_use]
195pub fn cron_list_parameters() -> Value {
196    json!({
197        "type": "object",
198        "properties": {},
199        "additionalProperties": false
200    })
201}
202
203#[must_use]
204pub fn cron_delete_parameters() -> Value {
205    json!({
206        "type": "object",
207        "required": ["id"],
208        "properties": {
209            "id": {"type": "string", "description": "Session scheduled task id to delete."}
210        }
211    })
212}
213
214/// Model-visible description of the `exec_command` tool.
215pub 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.";
216
217#[must_use]
218pub fn exec_command_parameters() -> Value {
219    json!({
220        "type": "object",
221        "required": ["cmd"],
222        "properties": {
223            "cmd": {"type": "string", "description": "Shell command to execute, subject to command policy. The tool description lists covered tools."},
224            "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},
225            "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},
226            "stdin": {"type": "boolean", "description": "Keep pipe stdin open for later write_stdin input. Defaults to false (EOF); enable only for commands that need input. PTY input is always available.", "default": false},
227            "max_output_tokens": {"type": "integer", "minimum": MIN_MAX_OUTPUT_TOKENS, "maximum": MAX_MAX_OUTPUT_TOKENS, "default": DEFAULT_MAX_OUTPUT_TOKENS, "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."},
228            "workdir": {"type": "string", "description": "Working directory."},
229            "tty": {"type": "boolean", "description": "Run the command in PTY mode for interactive or terminal-sensitive commands.", "default": false},
230            "sandbox_permissions": {
231                "type": "string",
232                "enum": ["use_default", "with_additional_permissions", "require_escalated", "bypass_sandbox"],
233                "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`.",
234                "default": "use_default"
235            },
236            "additional_permissions": {
237                "type": "object",
238                "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.",
239                "properties": {
240                    "fs_read": {"type": "array", "items": {"type": "string"}},
241                    "fs_write": {"type": "array", "items": {"type": "string"}}
242                },
243                "additionalProperties": false
244            },
245            "justification": {"type": "string", "description": "Short approval question for expanded sandbox scope. Required when sandbox_permissions is `require_escalated` or `bypass_sandbox`."}
246        },
247        "additionalProperties": false
248    })
249}
250
251/// Shared model-facing description for execution session controls.
252pub const WRITE_STDIN_DESCRIPTION: &str = "Write input to an owned exec session (pipe runs require stdin: true at launch), poll for fresh output, wait until exit or deadline, inspect a bounded snapshot, terminate its process group, or close and release it. Wait never kills the process; use the returned exact session_id.";
253
254#[must_use]
255pub fn write_stdin_parameters() -> Value {
256    json!({
257        "type": "object",
258        "required": ["session_id"],
259        "properties": {
260            "session_id": {"type": "string", "description": "Active execution session id."},
261            "action": {"type": "string", "enum": ["write", "poll", "wait", "inspect", "terminate", "close"], "description": "wait blocks until exit or deadline without killing; inspect reads a bounded snapshot; terminate kills the process group and captures output; close cancels and releases the session. poll sends no input."},
262            "chars": {"type": "string", "description": "Bytes to write to stdin. Pass an empty string to poll without sending input."},
263            "yield_time_ms": {"type": "integer", "description": "Wait before returning fresh session output (ms).", "default": 1000},
264            "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."},
265            "max_output_tokens": {"type": "integer", "minimum": MIN_MAX_OUTPUT_TOKENS, "maximum": MAX_MAX_OUTPUT_TOKENS, "default": DEFAULT_MAX_OUTPUT_TOKENS, "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."}
266        },
267        "anyOf": [
268            {"required": ["chars"]},
269            {"required": ["action"], "properties": {"action": {"enum": ["poll", "wait", "inspect", "terminate", "close"]}}}
270        ],
271        "additionalProperties": false
272    })
273}
274
275/// Model-visible description of the `search_tools` tool.
276pub 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.";
277
278#[must_use]
279pub fn search_tools_parameters() -> Value {
280    with_max_output_tokens_parameter(json!({
281        "type": "object",
282        "required": ["query"],
283        "properties": {
284            "query": {
285                "type": "string",
286                "minLength": 1,
287                "description": "Natural-language description of the capability to discover."
288            },
289            "limit": {
290                "type": "integer",
291                "minimum": 1,
292                "maximum": 25,
293                "default": 5,
294                "description": "Maximum number of ranked matches to return (default: 5, max: 25)."
295            },
296            "detail_level": {
297                "type": "string",
298                "enum": ["name", "name_description", "full"],
299                "default": "name_description",
300                "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."
301            }
302        },
303        "additionalProperties": false
304    }))
305}
306
307#[must_use]
308pub fn code_search_parameters() -> Value {
309    json!({
310        "type": "object",
311        "required": ["query"],
312        "additionalProperties": false,
313        "properties": {
314            "query": {
315                "type": "string",
316                "minLength": 1,
317                "pattern": "\\S",
318                "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."
319            },
320            "path": {
321                "type": "string",
322                "minLength": 1,
323                "pattern": "\\S",
324                "description": "Workspace-relative file or directory to search. Omit to search the workspace root."
325            },
326            "file_types": {
327                "type": "array",
328                "minItems": 1,
329                "items": {
330                    "type": "string",
331                    "minLength": 1,
332                    "pattern": "\\S"
333                },
334                "description": "Language names or common file extensions, with or without one leading dot."
335            },
336            "result_types": {
337                "type": "array",
338                "minItems": 1,
339                "items": {
340                    "type": "string",
341                    "enum": ["definition", "usage", "text", "path"]
342                },
343                "description": "Result categories to include. Omit to include all four categories."
344            },
345            "max_results": {
346                "type": "integer",
347                "minimum": 1,
348                "maximum": 100,
349                "description": "Maximum number of merged results to return. Omit for 20."
350            }
351        }
352    })
353}
354
355#[must_use]
356pub fn list_files_parameters() -> Value {
357    json!({
358        "type": "object",
359        "properties": {
360            "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
361            "mode": {
362                "type": "string",
363                "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
364                "description": "Listing mode. Use page/per_page to continue paginated results.",
365                "default": "list"
366            },
367            "pattern": {"type": "string", "description": "Optional glob-style path filter."},
368            "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
369            "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
370            "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
371            "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
372            "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
373            "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
374            "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
375            "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
376        }
377    })
378}
379
380#[cfg(test)]
381mod tests {
382    use serde_json::json;
383
384    use super::*;
385
386    #[test]
387    fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
388        let schema = apply_patch_parameter_schema("Patch in VT Code format");
389
390        // Both `input` and `patch` alias fields now carry the format
391        // description AND the semantic-anchor guidance, preventing the model
392        // from placing a unified diff in `patch` (see checkpoint turn_615).
393        assert_eq!(
394            schema["properties"]["patch"]["description"],
395            with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
396        );
397        let patch_description = schema["properties"]["patch"]["description"]
398            .as_str()
399            .expect("patch description");
400        assert!(patch_description.contains("*** Begin Patch"));
401        assert!(patch_description.contains("unified diff"));
402        assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
403
404        let input_description = schema["properties"]["input"]["description"]
405            .as_str()
406            .expect("input description");
407        assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
408    }
409
410    #[test]
411    fn search_tools_schema_documents_limit_and_detail_level_defaults() {
412        let schema = search_tools_parameters();
413        let limit = &schema["properties"]["limit"];
414        assert_eq!(limit["default"], json!(5));
415        assert_eq!(limit["maximum"], json!(25));
416        let limit_description = limit["description"].as_str().expect("limit description");
417        assert!(limit_description.contains("default: 5"));
418        assert!(limit_description.contains("max: 25"));
419
420        let detail_level = &schema["properties"]["detail_level"];
421        assert_eq!(detail_level["default"], json!("name_description"));
422        let detail_description = detail_level["description"].as_str().expect("detail_level description");
423        for level in ["name", "name_description", "full"] {
424            assert!(detail_description.contains(level), "{level}");
425        }
426
427        assert!(SEARCH_TOOLS_DESCRIPTION.contains("deferred"));
428        assert!(SEARCH_TOOLS_DESCRIPTION.contains("next request"));
429        assert!(SEARCH_TOOLS_DESCRIPTION.contains("MCP"));
430        assert!(SEARCH_TOOLS_DESCRIPTION.contains("not needed"));
431    }
432
433    #[test]
434    fn apply_patch_tool_description_leads_with_format_and_stays_calm() {
435        assert!(APPLY_PATCH_TOOL_DESCRIPTION.starts_with("Apply a patch in VT Code format (*** Begin Patch"));
436        assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("unified diffs"));
437        assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("workspace-relative"));
438        assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("permission checks"));
439        assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("JSON calls use input (patch is an alias)"));
440        assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("preserving internal whitespace"));
441        assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("one fresh bounded file read range"));
442        for description in [
443            APPLY_PATCH_TOOL_DESCRIPTION,
444            APPLY_PATCH_ALIAS_DESCRIPTION,
445            DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION,
446        ] {
447            assert!(!description.contains("IMPORTANT"), "{description}");
448            assert!(!description.contains("NOT"), "{description}");
449            assert!(!description.contains("never"), "{description}");
450        }
451    }
452
453    #[test]
454    fn common_output_limit_parameter_preserves_strict_schema() {
455        let schema = with_max_output_tokens_parameter(json!({
456            "type": "object",
457            "properties": {"query": {"type": "string"}},
458            "additionalProperties": false
459        }));
460        assert_eq!(schema["additionalProperties"], json!(false));
461        assert_eq!(schema["properties"]["max_output_tokens"]["default"], json!(DEFAULT_MAX_OUTPUT_TOKENS));
462        assert_eq!(schema["properties"]["max_output_tokens"]["minimum"], json!(MIN_MAX_OUTPUT_TOKENS));
463        assert_eq!(schema["properties"]["max_output_tokens"]["maximum"], json!(MAX_MAX_OUTPUT_TOKENS));
464    }
465
466    #[test]
467    fn codex_baseline_exec_schemas_use_public_names_shape() {
468        let exec_params = exec_command_parameters();
469        assert_eq!(exec_params["required"], json!(["cmd"]));
470        assert!(exec_params["properties"]["cmd"].is_object());
471        assert!(exec_params["properties"]["workdir"].is_object());
472        assert!(
473            exec_params["properties"]["yield_time_ms"]["description"]
474                .as_str()
475                .expect("exec yield description")
476                .contains("session_id")
477        );
478        assert!(
479            exec_params["properties"]["yield_time_ms"]["description"]
480                .as_str()
481                .expect("exec yield description")
482                .contains("single-call long run")
483        );
484        assert!(
485            exec_params["properties"]["max_output_tokens"]["description"]
486                .as_str()
487                .expect("exec max output description")
488                .contains("spool_path")
489        );
490        assert_eq!(exec_params["properties"]["tty"]["type"], "boolean");
491        assert_eq!(exec_params["properties"]["tty"]["default"], false);
492        assert_eq!(exec_params["properties"]["background"]["type"], "boolean");
493        assert_eq!(exec_params["properties"]["background"]["default"], false);
494        assert_eq!(exec_params["properties"]["yield_time_ms"]["default"], 10000);
495        assert_eq!(
496            exec_params["properties"]["sandbox_permissions"]["enum"],
497            json!([
498                "use_default",
499                "with_additional_permissions",
500                "require_escalated",
501                "bypass_sandbox"
502            ])
503        );
504        assert_eq!(exec_params["properties"]["sandbox_permissions"]["default"], "use_default");
505        assert_eq!(
506            exec_params["properties"]["additional_permissions"]["properties"]["fs_read"]["items"]["type"],
507            "string"
508        );
509        assert_eq!(
510            exec_params["properties"]["additional_permissions"]["properties"]["fs_write"]["items"]["type"],
511            "string"
512        );
513        assert_eq!(exec_params["properties"]["additional_permissions"]["additionalProperties"], false);
514        assert_eq!(exec_params["properties"]["justification"]["type"], "string");
515        assert!(
516            exec_params["properties"]["sandbox_permissions"]["description"]
517                .as_str()
518                .expect("sandbox_permissions description")
519                .contains("normalized to `with_additional_permissions`")
520        );
521        assert!(
522            exec_params["properties"]["additional_permissions"]["description"]
523                .as_str()
524                .expect("additional_permissions description")
525                .contains("allowed workspace or temp roots")
526        );
527        assert!(
528            exec_params["properties"]["justification"]["description"]
529                .as_str()
530                .expect("justification description")
531                .contains("Required when sandbox_permissions is `require_escalated` or `bypass_sandbox`")
532        );
533        assert_eq!(exec_params["additionalProperties"], false);
534        // Example commands live once, in EXEC_COMMAND_DESCRIPTION; the `cmd`
535        // property defers to it instead of repeating the list on every
536        // request.
537        assert!(
538            exec_params["properties"]["cmd"]["description"]
539                .as_str()
540                .expect("cmd description")
541                .contains("tool description lists covered tools"),
542            "cmd description must defer the example list to the tool description"
543        );
544        for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
545            assert!(
546                EXEC_COMMAND_DESCRIPTION.contains(command),
547                "{command} should be described as an exec_command example"
548            );
549            assert!(
550                exec_params["properties"].get(command).is_none(),
551                "{command} must not be modelled as a separate exec_command field"
552            );
553        }
554
555        let stdin_params = write_stdin_parameters();
556        assert_eq!(stdin_params["required"], json!(["session_id"]));
557        assert!(stdin_params["properties"]["session_id"].is_object());
558        assert_eq!(stdin_params["properties"]["chars"]["type"], "string");
559        assert_eq!(
560            stdin_params["properties"]["action"]["enum"],
561            json!(["write", "poll", "wait", "inspect", "terminate", "close"])
562        );
563        assert_eq!(exec_params["properties"]["stdin"]["default"], false);
564        assert!(stdin_params["properties"]["wait_timeout_seconds"].is_object());
565        assert!(
566            stdin_params["properties"].get("timeout_seconds").is_none(),
567            "write_stdin schema advertises only wait_timeout_seconds"
568        );
569        assert_eq!(stdin_params["anyOf"][1]["required"], json!(["action"]));
570        assert_eq!(
571            stdin_params["anyOf"][1]["properties"]["action"]["enum"],
572            json!(["poll", "wait", "inspect", "terminate", "close"])
573        );
574        assert!(
575            stdin_params["properties"]["chars"]["description"]
576                .as_str()
577                .is_some_and(|description| description.contains("empty string"))
578        );
579        assert!(stdin_params["properties"]["chars"].is_object());
580        assert!(
581            stdin_params["properties"]["yield_time_ms"]["description"]
582                .as_str()
583                .expect("stdin yield description")
584                .contains("fresh session output")
585        );
586        assert!(
587            stdin_params["properties"]["max_output_tokens"]["description"]
588                .as_str()
589                .expect("stdin max output description")
590                .contains("spool_path")
591        );
592        assert_eq!(stdin_params["additionalProperties"], false);
593    }
594
595    #[test]
596    fn mcp_description_distinguishes_server_search_from_catalog_search() {
597        assert!(MCP_DESCRIPTION.starts_with("Discover and manage Model Context Protocol capabilities."));
598        assert!(MCP_DESCRIPTION.contains("action=search_tools searches only tools exposed by configured MCP servers"));
599        assert!(MCP_DESCRIPTION.contains("the separate search_tools tool searches the whole session catalog"));
600    }
601
602    #[test]
603    fn agent_description_routes_shell_processes_to_exec_command() {
604        assert!(AGENT_DESCRIPTION.starts_with("Spawn and steer delegated child agents."));
605        assert!(AGENT_DESCRIPTION.contains("spawn_subprocess runs a subagent defined with background: true"));
606        assert!(AGENT_DESCRIPTION.contains("go through exec_command, with background=true"));
607        let action = agent_parameters()["properties"]["action"]["description"]
608            .as_str()
609            .unwrap_or_default()
610            .to_string();
611        assert!(!action.contains("daemons"), "{action}");
612    }
613
614    #[test]
615    fn exec_command_description_states_edit_routing_and_escalation_rules() {
616        assert!(EXEC_COMMAND_DESCRIPTION.starts_with("Run a shell command through the active sandbox policy"));
617        assert!(EXEC_COMMAND_DESCRIPTION.contains("For file edits, use apply_patch"));
618        assert!(EXEC_COMMAND_DESCRIPTION.contains("approval check"));
619        assert!(EXEC_COMMAND_DESCRIPTION.contains("non-empty justification"));
620        for mode in ["require_escalated", "bypass_sandbox"] {
621            assert!(EXEC_COMMAND_DESCRIPTION.contains(mode), "{mode}");
622            assert!(
623                exec_command_parameters()["properties"]["sandbox_permissions"]["enum"]
624                    .as_array()
625                    .expect("sandbox_permissions enum")
626                    .iter()
627                    .any(|value| value == mode),
628                "{mode} must stay a real sandbox_permissions value"
629            );
630        }
631    }
632
633    #[test]
634    fn code_search_schema_exposes_exact_five_property_contract() {
635        let params = code_search_parameters();
636        let properties = params["properties"].as_object().expect("properties");
637        let mut property_names = properties.keys().map(String::as_str).collect::<Vec<_>>();
638        property_names.sort_unstable();
639
640        assert_eq!(params["required"], json!(["query"]));
641        assert_eq!(property_names, ["file_types", "max_results", "path", "query", "result_types"]);
642        assert_eq!(params["additionalProperties"], false);
643        assert_eq!(params["properties"]["query"]["pattern"], "\\S");
644        assert_eq!(params["properties"]["file_types"]["minItems"], 1);
645        assert_eq!(params["properties"]["result_types"]["minItems"], 1);
646        assert_eq!(
647            params["properties"]["result_types"]["items"]["enum"],
648            json!(["definition", "usage", "text", "path"])
649        );
650        assert_eq!(params["properties"]["max_results"]["minimum"], 1);
651        assert_eq!(params["properties"]["max_results"]["maximum"], 100);
652        assert!(params.get("anyOf").is_none());
653    }
654
655    #[test]
656    fn legacy_list_files_schema_exposes_pagination_fields() {
657        let list_params = list_files_parameters();
658        assert!(list_params["properties"]["page"].is_object());
659        assert!(list_params["properties"]["per_page"].is_object());
660        assert!(
661            list_params["properties"]["mode"]["enum"]
662                .as_array()
663                .expect("mode enum")
664                .iter()
665                .any(|value| value == "recursive")
666        );
667    }
668
669    #[test]
670    fn semantic_anchor_guidance_is_appended_once() {
671        let base = "Patch in VT Code format.";
672        let with_guidance = with_semantic_anchor_guidance(base);
673
674        assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
675        assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
676    }
677
678    #[test]
679    fn default_apply_patch_parameters_keep_expected_alias_shape() {
680        let schema = apply_patch_parameters();
681
682        assert_eq!(
683            schema["anyOf"],
684            json!([
685                {"required": ["input"]},
686                {"required": ["patch"]}
687            ])
688        );
689    }
690}