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;
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
41pub 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.";
50pub 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
57pub const DEFAULT_MAX_OUTPUT_TOKENS: usize = 10_000;
59
60pub 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}
69pub const MIN_MAX_OUTPUT_TOKENS: usize = 1;
71pub const MAX_MAX_OUTPUT_TOKENS: usize = 50_000;
73pub const MAX_OUTPUT_TOKENS_FIELD: &str = "max_output_tokens";
78
79#[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
169pub 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
233pub 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 "monitor_filter": {"type": "string", "description": "Optional case-sensitive substring filter for pipe runs; rejected for tty runs. Matching output lines accumulate into a bounded monitor_digest on run/wait/poll responses so long quiet commands cost no full-output polls. At most 256 characters."},
248 "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."},
249 "workdir": {"type": "string", "description": "Working directory."},
250 "tty": {"type": "boolean", "description": "Run the command in PTY mode for interactive or terminal-sensitive commands.", "default": false},
251 "sandbox_permissions": {
252 "type": "string",
253 "enum": ["use_default", "with_additional_permissions", "require_escalated", "bypass_sandbox"],
254 "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`.",
255 "default": "use_default"
256 },
257 "additional_permissions": {
258 "type": "object",
259 "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.",
260 "properties": {
261 "fs_read": {"type": "array", "items": {"type": "string"}},
262 "fs_write": {"type": "array", "items": {"type": "string"}}
263 },
264 "additionalProperties": false
265 },
266 "justification": {"type": "string", "description": "Short approval question for expanded sandbox scope. Required when sandbox_permissions is `require_escalated` or `bypass_sandbox`."}
267 },
268 "additionalProperties": false
269 })
270}
271
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.";
274
275#[must_use]
276pub fn write_stdin_parameters() -> Value {
277 json!({
278 "type": "object",
279 "required": ["session_id"],
280 "properties": {
281 "session_id": {"type": "string", "description": "Active execution session id copied verbatim from the run response."},
282 "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."},
283 "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)."},
284 "yield_time_ms": {"type": "integer", "description": "Wait before returning fresh session output (ms).", "default": 1000},
285 "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."},
286 "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."}
287 },
288 "anyOf": [
289 {"required": ["chars"]},
290 {"required": ["action"], "properties": {"action": {"enum": ["poll", "wait", "inspect", "terminate", "close"]}}}
291 ],
292 "additionalProperties": false
293 })
294}
295
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.";
298
299#[must_use]
300pub fn search_tools_parameters() -> Value {
301 with_max_output_tokens_parameter(json!({
302 "type": "object",
303 "required": ["query"],
304 "properties": {
305 "query": {
306 "type": "string",
307 "minLength": 1,
308 "description": "Natural-language description of the capability to discover."
309 },
310 "limit": {
311 "type": "integer",
312 "minimum": 1,
313 "maximum": 25,
314 "default": 5,
315 "description": "Maximum number of ranked matches to return (default: 5, max: 25)."
316 },
317 "detail_level": {
318 "type": "string",
319 "enum": ["name", "name_description", "full"],
320 "default": "name_description",
321 "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."
322 }
323 },
324 "additionalProperties": false
325 }))
326}
327
328#[must_use]
329pub fn code_search_parameters() -> Value {
330 json!({
331 "type": "object",
332 "required": ["query"],
333 "additionalProperties": false,
334 "properties": {
335 "query": {
336 "type": "string",
337 "minLength": 1,
338 "pattern": "\\S",
339 "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."
340 },
341 "path": {
342 "type": "string",
343 "minLength": 1,
344 "pattern": "\\S",
345 "description": "Workspace-relative file or directory to search. Omit to search the workspace root."
346 },
347 "file_types": {
348 "type": "array",
349 "minItems": 1,
350 "items": {
351 "type": "string",
352 "minLength": 1,
353 "pattern": "\\S"
354 },
355 "description": "Language names or common file extensions, with or without one leading dot."
356 },
357 "result_types": {
358 "type": "array",
359 "minItems": 1,
360 "items": {
361 "type": "string",
362 "enum": ["definition", "usage", "text", "path"]
363 },
364 "description": "Result categories to include. Omit to include all four categories."
365 },
366 "max_results": {
367 "type": "integer",
368 "minimum": 1,
369 "maximum": 100,
370 "description": "Maximum number of merged results to return. Omit for 20."
371 }
372 }
373 })
374}
375
376#[must_use]
377pub fn list_files_parameters() -> Value {
378 json!({
379 "type": "object",
380 "properties": {
381 "path": {"type": "string", "description": "Directory or file path to inspect.", "default": "."},
382 "mode": {
383 "type": "string",
384 "enum": ["list", "recursive", "tree", "find_name", "find_content", "largest", "file", "files"],
385 "description": "Listing mode. Use page/per_page to continue paginated results.",
386 "default": "list"
387 },
388 "pattern": {"type": "string", "description": "Optional glob-style path filter."},
389 "name_pattern": {"type": "string", "description": "Optional name filter for list/find_name modes."},
390 "content_pattern": {"type": "string", "description": "Content query for find_content mode."},
391 "page": {"type": "integer", "description": "1-indexed results page.", "minimum": 1},
392 "per_page": {"type": "integer", "description": "Items per page.", "minimum": 1},
393 "max_results": {"type": "integer", "description": "Maximum total results to consider before pagination.", "minimum": 1},
394 "include_hidden": {"type": "boolean", "description": "Include dotfiles and hidden entries.", "default": false},
395 "response_format": {"type": "string", "enum": ["concise", "detailed"], "description": "Verbosity of the listing output.", "default": "concise"},
396 "case_sensitive": {"type": "boolean", "description": "Case-sensitive name matching.", "default": false}
397 }
398 })
399}
400
401#[cfg(test)]
402mod tests {
403 use serde_json::json;
404
405 use super::*;
406
407 #[test]
408 fn apply_patch_parameter_schema_keeps_alias_and_guidance_consistent() {
409 let schema = apply_patch_parameter_schema("Patch in VT Code format");
410
411 assert_eq!(
415 schema["properties"]["patch"]["description"],
416 with_semantic_anchor_guidance(APPLY_PATCH_ALIAS_DESCRIPTION)
417 );
418 let patch_description = schema["properties"]["patch"]["description"]
419 .as_str()
420 .expect("patch description");
421 assert!(patch_description.contains("*** Begin Patch"));
422 assert!(patch_description.contains("unified diff"));
423 assert!(patch_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
424
425 let input_description = schema["properties"]["input"]["description"]
426 .as_str()
427 .expect("input description");
428 assert!(input_description.contains(SEMANTIC_ANCHOR_GUIDANCE));
429 }
430
431 #[test]
432 fn search_tools_schema_documents_limit_and_detail_level_defaults() {
433 let schema = search_tools_parameters();
434 let limit = &schema["properties"]["limit"];
435 assert_eq!(limit["default"], json!(5));
436 assert_eq!(limit["maximum"], json!(25));
437 let limit_description = limit["description"].as_str().expect("limit description");
438 assert!(limit_description.contains("default: 5"));
439 assert!(limit_description.contains("max: 25"));
440
441 let detail_level = &schema["properties"]["detail_level"];
442 assert_eq!(detail_level["default"], json!("name_description"));
443 let detail_description = detail_level["description"].as_str().expect("detail_level description");
444 for level in ["name", "name_description", "full"] {
445 assert!(detail_description.contains(level), "{level}");
446 }
447
448 assert!(SEARCH_TOOLS_DESCRIPTION.contains("deferred"));
449 assert!(SEARCH_TOOLS_DESCRIPTION.contains("next request"));
450 assert!(SEARCH_TOOLS_DESCRIPTION.contains("MCP"));
451 assert!(SEARCH_TOOLS_DESCRIPTION.contains("not needed"));
452 }
453
454 #[test]
455 fn apply_patch_tool_description_leads_with_format_and_stays_calm() {
456 assert!(APPLY_PATCH_TOOL_DESCRIPTION.starts_with("Apply a patch in VT Code format (*** Begin Patch"));
457 assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("unified diffs"));
458 assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("workspace-relative"));
459 assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("permission checks"));
460 assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("JSON calls use input (patch is an alias)"));
461 assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("preserving internal whitespace"));
462 assert!(APPLY_PATCH_TOOL_DESCRIPTION.contains("one fresh bounded file read range"));
463 for description in [
464 APPLY_PATCH_TOOL_DESCRIPTION,
465 APPLY_PATCH_ALIAS_DESCRIPTION,
466 DEFAULT_APPLY_PATCH_INPUT_DESCRIPTION,
467 ] {
468 assert!(!description.contains("IMPORTANT"), "{description}");
469 assert!(!description.contains("NOT"), "{description}");
470 assert!(!description.contains("never"), "{description}");
471 }
472 }
473
474 #[test]
475 fn common_output_limit_parameter_preserves_strict_schema() {
476 let schema = with_max_output_tokens_parameter(json!({
477 "type": "object",
478 "properties": {"query": {"type": "string"}},
479 "additionalProperties": false
480 }));
481 assert_eq!(schema["additionalProperties"], json!(false));
482 assert_eq!(schema["properties"]["max_output_tokens"]["default"], json!(DEFAULT_MAX_OUTPUT_TOKENS));
483 assert_eq!(schema["properties"]["max_output_tokens"]["minimum"], json!(MIN_MAX_OUTPUT_TOKENS));
484 assert_eq!(schema["properties"]["max_output_tokens"]["maximum"], json!(MAX_MAX_OUTPUT_TOKENS));
485 }
486
487 #[test]
488 fn codex_baseline_exec_schemas_use_public_names_shape() {
489 let exec_params = exec_command_parameters();
490 assert_eq!(exec_params["required"], json!(["cmd"]));
491 assert!(exec_params["properties"]["cmd"].is_object());
492 assert!(exec_params["properties"]["workdir"].is_object());
493 assert!(
494 exec_params["properties"]["yield_time_ms"]["description"]
495 .as_str()
496 .expect("exec yield description")
497 .contains("session_id")
498 );
499 assert!(
500 exec_params["properties"]["yield_time_ms"]["description"]
501 .as_str()
502 .expect("exec yield description")
503 .contains("single-call long run")
504 );
505 assert!(
506 exec_params["properties"]["max_output_tokens"]["description"]
507 .as_str()
508 .expect("exec max output description")
509 .contains("spool_path")
510 );
511 assert_eq!(exec_params["properties"]["tty"]["type"], "boolean");
512 assert_eq!(exec_params["properties"]["tty"]["default"], false);
513 assert_eq!(exec_params["properties"]["background"]["type"], "boolean");
514 assert_eq!(exec_params["properties"]["background"]["default"], false);
515 assert_eq!(exec_params["properties"]["monitor_filter"]["type"], "string");
516 assert!(
517 exec_params["properties"]["monitor_filter"]["description"]
518 .as_str()
519 .expect("monitor_filter description")
520 .contains("monitor_digest")
521 );
522 assert_eq!(exec_params["properties"]["yield_time_ms"]["default"], 10000);
523 assert_eq!(
524 exec_params["properties"]["sandbox_permissions"]["enum"],
525 json!([
526 "use_default",
527 "with_additional_permissions",
528 "require_escalated",
529 "bypass_sandbox"
530 ])
531 );
532 assert_eq!(exec_params["properties"]["sandbox_permissions"]["default"], "use_default");
533 assert_eq!(
534 exec_params["properties"]["additional_permissions"]["properties"]["fs_read"]["items"]["type"],
535 "string"
536 );
537 assert_eq!(
538 exec_params["properties"]["additional_permissions"]["properties"]["fs_write"]["items"]["type"],
539 "string"
540 );
541 assert_eq!(exec_params["properties"]["additional_permissions"]["additionalProperties"], false);
542 assert_eq!(exec_params["properties"]["justification"]["type"], "string");
543 assert!(
544 exec_params["properties"]["sandbox_permissions"]["description"]
545 .as_str()
546 .expect("sandbox_permissions description")
547 .contains("normalized to `with_additional_permissions`")
548 );
549 assert!(
550 exec_params["properties"]["additional_permissions"]["description"]
551 .as_str()
552 .expect("additional_permissions description")
553 .contains("allowed workspace or temp roots")
554 );
555 assert!(
556 exec_params["properties"]["justification"]["description"]
557 .as_str()
558 .expect("justification description")
559 .contains("Required when sandbox_permissions is `require_escalated` or `bypass_sandbox`")
560 );
561 assert_eq!(exec_params["additionalProperties"], false);
562 assert!(
566 exec_params["properties"]["cmd"]["description"]
567 .as_str()
568 .expect("cmd description")
569 .contains("tool description lists covered tools"),
570 "cmd description must defer the example list to the tool description"
571 );
572 for command in ["ls", "rg", "find", "cat", "sed", "awk"] {
573 assert!(
574 EXEC_COMMAND_DESCRIPTION.contains(command),
575 "{command} should be described as an exec_command example"
576 );
577 assert!(
578 exec_params["properties"].get(command).is_none(),
579 "{command} must not be modelled as a separate exec_command field"
580 );
581 }
582
583 let stdin_params = write_stdin_parameters();
584 assert_eq!(stdin_params["required"], json!(["session_id"]));
585 assert!(stdin_params["properties"]["session_id"].is_object());
586 assert_eq!(stdin_params["properties"]["chars"]["type"], "string");
587 assert_eq!(
588 stdin_params["properties"]["action"]["enum"],
589 json!(["write", "poll", "wait", "inspect", "terminate", "close"])
590 );
591 assert_eq!(exec_params["properties"]["stdin"]["default"], false);
592 assert!(stdin_params["properties"]["wait_timeout_seconds"].is_object());
593 assert!(
594 stdin_params["properties"].get("timeout_seconds").is_none(),
595 "write_stdin schema advertises only wait_timeout_seconds"
596 );
597 assert_eq!(stdin_params["anyOf"][1]["required"], json!(["action"]));
598 assert_eq!(
599 stdin_params["anyOf"][1]["properties"]["action"]["enum"],
600 json!(["poll", "wait", "inspect", "terminate", "close"])
601 );
602 assert!(
603 stdin_params["properties"]["chars"]["description"]
604 .as_str()
605 .is_some_and(|description| description.contains("empty string"))
606 );
607 assert!(stdin_params["properties"]["chars"].is_object());
608 assert!(
609 stdin_params["properties"]["yield_time_ms"]["description"]
610 .as_str()
611 .expect("stdin yield description")
612 .contains("fresh session output")
613 );
614 assert!(
615 stdin_params["properties"]["max_output_tokens"]["description"]
616 .as_str()
617 .expect("stdin max output description")
618 .contains("spool_path")
619 );
620 assert_eq!(stdin_params["additionalProperties"], false);
621 }
622
623 #[test]
624 fn mcp_description_distinguishes_server_search_from_catalog_search() {
625 assert!(MCP_DESCRIPTION.starts_with("Discover and manage Model Context Protocol capabilities."));
626 assert!(MCP_DESCRIPTION.contains("action=search_tools searches only tools exposed by configured MCP servers"));
627 assert!(MCP_DESCRIPTION.contains("the separate search_tools tool searches the whole session catalog"));
628 }
629
630 #[test]
631 fn agent_description_routes_shell_processes_to_exec_command() {
632 assert!(AGENT_DESCRIPTION.starts_with("Spawn and steer delegated child agents."));
633 assert!(AGENT_DESCRIPTION.contains("spawn_subprocess runs a subagent defined with background: true"));
634 assert!(AGENT_DESCRIPTION.contains("go through exec_command, with background=true"));
635 let action = agent_parameters()["properties"]["action"]["description"]
636 .as_str()
637 .unwrap_or_default()
638 .to_string();
639 assert!(!action.contains("daemons"), "{action}");
640 }
641
642 #[test]
643 fn exec_command_description_states_edit_routing_and_escalation_rules() {
644 assert!(EXEC_COMMAND_DESCRIPTION.starts_with("Run a shell command through the active sandbox policy"));
645 assert!(EXEC_COMMAND_DESCRIPTION.contains("For file edits, use apply_patch"));
646 assert!(EXEC_COMMAND_DESCRIPTION.contains("approval check"));
647 assert!(
651 !EXEC_COMMAND_DESCRIPTION.contains("non-empty justification"),
652 "justification requirement belongs to the field description"
653 );
654 for mode in ["require_escalated", "bypass_sandbox"] {
655 assert!(
656 exec_command_parameters()["properties"]["sandbox_permissions"]["enum"]
657 .as_array()
658 .expect("sandbox_permissions enum")
659 .iter()
660 .any(|value| value == mode),
661 "{mode} must stay a real sandbox_permissions value"
662 );
663 }
664 }
665
666 #[test]
667 fn code_search_schema_exposes_exact_five_property_contract() {
668 let params = code_search_parameters();
669 let properties = params["properties"].as_object().expect("properties");
670 let mut property_names = properties.keys().map(String::as_str).collect::<Vec<_>>();
671 property_names.sort_unstable();
672
673 assert_eq!(params["required"], json!(["query"]));
674 assert_eq!(property_names, ["file_types", "max_results", "path", "query", "result_types"]);
675 assert_eq!(params["additionalProperties"], false);
676 assert_eq!(params["properties"]["query"]["pattern"], "\\S");
677 assert_eq!(params["properties"]["file_types"]["minItems"], 1);
678 assert_eq!(params["properties"]["result_types"]["minItems"], 1);
679 assert_eq!(
680 params["properties"]["result_types"]["items"]["enum"],
681 json!(["definition", "usage", "text", "path"])
682 );
683 assert_eq!(params["properties"]["max_results"]["minimum"], 1);
684 assert_eq!(params["properties"]["max_results"]["maximum"], 100);
685 assert!(params.get("anyOf").is_none());
686 }
687
688 #[test]
689 fn legacy_list_files_schema_exposes_pagination_fields() {
690 let list_params = list_files_parameters();
691 assert!(list_params["properties"]["page"].is_object());
692 assert!(list_params["properties"]["per_page"].is_object());
693 assert!(
694 list_params["properties"]["mode"]["enum"]
695 .as_array()
696 .expect("mode enum")
697 .iter()
698 .any(|value| value == "recursive")
699 );
700 }
701
702 #[test]
703 fn semantic_anchor_guidance_is_appended_once() {
704 let base = "Patch in VT Code format.";
705 let with_guidance = with_semantic_anchor_guidance(base);
706
707 assert!(with_guidance.contains(SEMANTIC_ANCHOR_GUIDANCE));
708 assert_eq!(with_semantic_anchor_guidance(&with_guidance), with_guidance);
709 }
710
711 #[test]
712 fn default_apply_patch_parameters_keep_expected_alias_shape() {
713 let schema = apply_patch_parameters();
714
715 assert_eq!(
716 schema["anyOf"],
717 json!([
718 {"required": ["input"]},
719 {"required": ["patch"]}
720 ])
721 );
722 }
723}