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