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