Skip to content

Commit 8d2900a

Browse files
committed
refactor: implement schema command for runtime tool introspection and add input validation
1 parent f7251f5 commit 8d2900a

File tree

16 files changed

+448
-14
lines changed

16 files changed

+448
-14
lines changed

AGENTS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,13 @@ cargo clippy --workspace --all-targets -- -D warnings && cargo fmt --check && ca
125125

126126
**Performance**: Single codegen unit, strict Clippy, no `expect_used`/`unwrap_used`.
127127

128+
## Agent-First CLI Invariants
129+
130+
- Prefer machine-readable output for automation (`vtcode ask --output-format json`, `vtcode exec --json`).
131+
- Introspect tool schemas at runtime before generating tool calls (`vtcode schema tools`).
132+
- Use `vtcode exec --dry-run` before mutation-oriented autonomous tasks.
133+
- Assume agent-generated input can be adversarial; keep input validation strict for CLI/MCP fields.
134+
128135
**Pitfalls**:
129136

130137
1. Don't assume paths — validate boundaries

CONTEXT.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# VT Code Agent Context
2+
3+
This file captures runtime invariants for automation-oriented usage of VT Code.
4+
5+
## Output Contracts
6+
7+
- Use `vtcode ask --output-format json` when downstream tooling needs structured replies.
8+
- Use `vtcode exec --json` for JSONL event streams.
9+
- Use `vtcode exec --events <path>` to persist machine-readable transcripts.
10+
11+
## Runtime Introspection
12+
13+
- Discover built-in tool contracts from the binary at runtime:
14+
- `vtcode schema tools`
15+
- `vtcode schema tools --mode minimal`
16+
- `vtcode schema tools --name unified_search --name unified_file`
17+
18+
## Safety Defaults
19+
20+
- For autonomous mutation-oriented work, run a read-only rehearsal first:
21+
- `vtcode exec --dry-run "<task>"`
22+
- Treat prompts and MCP CLI values as untrusted input.
23+
- Prefer small, explicit tool arguments over broad/unbounded payloads.
24+
25+
## Context Discipline
26+
27+
- Limit request scope in prompts.
28+
- Prefer focused tool calls and incremental execution.
29+
- Avoid requesting full-file/full-workspace payloads unless required.

docs/modules/vtcode_docs_map.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ This document serves as an index of all VT Code documentation. When users ask qu
153153

154154
- **File**: `docs/user-guide/exec-mode.md`
155155
- **Content**: Exec Mode Automation
156-
- **Topics**: Launching exec mode, Structured event stream, Resuming sessions
157-
- **User Questions**: "What can you tell me about Exec Mode Automation?", "How does Launching exec mode work?", "How does Structured event stream work?"
156+
- **Topics**: Launching exec mode, Dry-run mode, Structured event stream, Resuming sessions
157+
- **User Questions**: "What can you tell me about Exec Mode Automation?", "How does Launching exec mode work?", "How does Dry-run mode work?"
158158

159159
- **File**: `docs/user-guide/getting-started.md`
160160
- **Content**: Getting Started with VT Code
@@ -878,7 +878,7 @@ This document serves as an index of all VT Code documentation. When users ask qu
878878

879879
- **File**: `docs/user-guide/commands.md`
880880
- **Content**: Command Reference
881-
- **Topics**: grep_file (ripgrep-like), File operations, Quick Actions in Chat Input, stats (session metrics), update (binary updates)
881+
- **Topics**: grep_file (ripgrep-like), File operations, Quick Actions in Chat Input, stats (session metrics), schema (runtime tool introspection)
882882
- **User Questions**: "What can you tell me about Command Reference?", "How does grep_file (ripgrep-like) work?", "How does File operations work?"
883883

884884
- **File**: `docs/user-guide/interactive-mode.md`

docs/user-guide/commands.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,33 @@ Display current configuration, available tools, and live performance metrics for
9393
session. Use `--format` to choose `text`, `json`, or `html` output and `--detailed` to list each
9494
tool.
9595

96+
## schema (runtime tool introspection)
97+
98+
Inspect VT Code's built-in tool schemas at runtime so automation can discover exact tool names and
99+
input parameters without relying on stale docs.
100+
101+
### Usage
102+
103+
```bash
104+
# Full JSON document (default)
105+
vtcode schema tools
106+
107+
# Compact schema descriptions for tighter context windows
108+
vtcode schema tools --mode minimal
109+
110+
# NDJSON output for streaming parsers
111+
vtcode schema tools --format ndjson
112+
113+
# Filter to specific tools
114+
vtcode schema tools --name unified_search --name unified_file
115+
```
116+
117+
### Options
118+
119+
- `--mode``minimal`, `progressive` (default), or `full`
120+
- `--format``json` (default) or `ndjson`
121+
- `--name` — repeatable exact tool-name filter
122+
96123
## update (binary updates)
97124

98125
Check for and install binary updates of VT Code from GitHub Releases. Updates are downloaded and verified against checksums for security.

docs/user-guide/exec-mode.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ allowances or confirmation dialogs, so ensure the allow list covers every tool i
1717
`--last-message-file` or persist the raw JSON stream with `--events`. Use `--json` when you want to see the live JSONL feed on
1818
stdout.
1919

20+
## Dry-run mode
21+
22+
Use `--dry-run` to force read-only execution while still allowing the agent to analyze and plan:
23+
24+
```bash
25+
vtcode exec --dry-run "identify required code changes for adding OAuth refresh token support"
26+
```
27+
28+
In dry-run mode, VT Code enables plan/read-only enforcement for tool calls. Mutating operations are blocked, and the run reports what
29+
would be changed rather than applying edits.
30+
2031
## Structured event stream
2132

2233
Use `vtcode exec --json` to emit JSON Lines that match the non-interactive Codex schema:

src/cli/exec.rs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use anyhow::{Context, Result, bail};
22
use std::io::{self, Read};
33
use std::path::PathBuf;
44
use std::str::FromStr;
5+
use vtcode_core::cli::input_hardening::validate_agent_safe_text;
56
use vtcode_core::config::VTCodeConfig;
67
use vtcode_core::config::WorkspaceTrustLevel;
78
use vtcode_core::config::models::ModelId;
@@ -21,17 +22,27 @@ const EXEC_SESSION_PREFIX: &str = "exec-task";
2122
const EXEC_TASK_ID: &str = "exec-task";
2223
const EXEC_TASK_TITLE: &str = "Exec Task";
2324
const EXEC_TASK_INSTRUCTIONS: &str = "You are running vtcode in non-interactive exec mode. Complete the task autonomously using the configured full-auto tool allowlist. Do not request additional user input, confirmations, or allowances—operate solely with the provided information and available tools. Provide a concise summary of the outcome when finished.";
25+
const EXEC_TASK_INSTRUCTIONS_DRY_RUN: &str = "You are running vtcode in non-interactive exec dry-run mode. Plan and validate the approach in read-only mode without mutating files, running mutating commands, or requesting additional user input. If the task requires mutations, explain what would be changed and why.";
2426

2527
#[derive(Debug, Clone)]
2628
pub struct ExecCommandOptions {
2729
pub json: bool,
30+
pub dry_run: bool,
2831
pub events_path: Option<PathBuf>,
2932
pub last_message_file: Option<PathBuf>,
3033
}
3134

35+
fn task_instructions(dry_run: bool) -> &'static str {
36+
if dry_run {
37+
EXEC_TASK_INSTRUCTIONS_DRY_RUN
38+
} else {
39+
EXEC_TASK_INSTRUCTIONS
40+
}
41+
}
42+
3243
fn resolve_prompt(prompt_arg: Option<String>, quiet: bool) -> Result<String> {
33-
match prompt_arg {
34-
Some(p) if p != "-" => Ok(p),
44+
let prompt = match prompt_arg {
45+
Some(p) if p != "-" => p,
3546
maybe_dash => {
3647
let force_stdin = matches!(maybe_dash.as_deref(), Some("-"));
3748
if io::stdin().is_tty_ext() && !force_stdin {
@@ -48,9 +59,12 @@ fn resolve_prompt(prompt_arg: Option<String>, quiet: bool) -> Result<String> {
4859
.read_to_string(&mut buffer)
4960
.context("Failed to read prompt from stdin")?;
5061
validate_non_empty(&buffer, "Prompt via stdin")?;
51-
Ok(buffer)
62+
buffer
5263
}
53-
}
64+
};
65+
66+
validate_agent_safe_text("prompt", &prompt)?;
67+
Ok(prompt)
5468
}
5569

5670
// OPTIMIZATION: Use inline hint for hot path
@@ -146,6 +160,9 @@ async fn handle_exec_command_impl(
146160
.await
147161
.context("Failed to apply workspace configuration to exec runner")?;
148162
runner.enable_full_auto(&automation_cfg.allowed_tools).await;
163+
if options.dry_run {
164+
runner.enable_plan_mode();
165+
}
149166
runner.set_quiet(options.json);
150167
if options.json {
151168
runner.set_event_handler(|event| match serde_json::to_string(event) {
@@ -159,7 +176,7 @@ async fn handle_exec_command_impl(
159176
id: EXEC_TASK_ID.into(),
160177
title: EXEC_TASK_TITLE.into(),
161178
description: prompt.trim().to_string(),
162-
instructions: Some(EXEC_TASK_INSTRUCTIONS.into()),
179+
instructions: Some(task_instructions(options.dry_run).into()),
163180
};
164181

165182
let max_retries = vt_cfg.agent.max_task_retries;
@@ -199,6 +216,7 @@ async fn handle_exec_command_impl(
199216

200217
eprintln!("{}", style("[OUTCOME]").magenta().bold());
201218
eprintln!(" {:16} {}", "outcome", result.outcome);
219+
eprintln!(" {:16} {}", "dry_run", options.dry_run);
202220
eprintln!(" {:16} {}", "turns", result.turns_executed);
203221
eprintln!(" {:16} {}", "duration_ms", result.total_duration_ms);
204222
eprintln!(" {:16} {}", "avg_turn_ms", avg_display);
@@ -252,3 +270,21 @@ async fn handle_exec_command_impl(
252270

253271
Ok(())
254272
}
273+
274+
#[cfg(test)]
275+
mod tests {
276+
use super::task_instructions;
277+
278+
#[test]
279+
fn dry_run_instructions_are_read_only_focused() {
280+
let instructions = task_instructions(true);
281+
assert!(instructions.contains("read-only"));
282+
assert!(instructions.contains("without mutating files"));
283+
}
284+
285+
#[test]
286+
fn normal_exec_instructions_do_not_use_dry_run_wording() {
287+
let instructions = task_instructions(false);
288+
assert!(!instructions.contains("dry-run mode"));
289+
}
290+
}

src/cli/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub use vtcode_core::mcp::cli::handle_mcp_command;
1818
pub mod analyze;
1919
pub mod benchmark;
2020
pub mod exec;
21+
pub mod schema;
2122
pub mod skills;
2223
pub mod skills_ref;
2324
pub mod update;
@@ -73,6 +74,10 @@ pub async fn handle_exec_command(
7374
exec::handle_exec_command(&core_cfg, cfg, options, prompt).await
7475
}
7576

77+
pub async fn handle_schema_command(command: vtcode_core::cli::args::SchemaCommands) -> Result<()> {
78+
schema::handle_schema_command(command).await
79+
}
80+
7681
pub async fn handle_analyze_command(
7782
core_cfg: vtcode_core::config::types::AgentConfig,
7883
analysis_type: analyze::AnalysisType,

0 commit comments

Comments
 (0)