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