vtcode 0.137.0

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
use std::env;
use std::io::{self, Write};
use std::path::Path;

use anstyle::Ansi256Color;
use anyhow::{Context, Result, bail};
use vtcode_commons::color256_theme::rgb_to_ansi256_for_theme;
use vtcode_core::utils::dot_config::{WorkspaceTrustLevel, load_workspace_trust_level, update_workspace_trust};
use vtcode_core::utils::style_helpers::{ColorPalette, render_styled};
use vtcode_core::utils::tty::TtyExt;
use vtcode_core::utils::{ansi_capabilities, ansi_capabilities::ColorScheme};

const WARNING_RGB: (u8, u8, u8) = (166, 51, 51);
const INFO_RGB: (u8, u8, u8) = (217, 154, 78);
const CHOICE_PROMPT: &str = "Enter your choice (a/q): ";
const INVALID_CHOICE_MESSAGE: &str = "Invalid selection. Please enter 'a' or 'q'.";

/// Environment variable that allows non-interactive (CI / scripted) callers
/// to grant or deny workspace trust without launching the interactive
/// VT Code shell. Recognised values (case-insensitive):
///
/// * `full-auto`, `full_auto`, `fullauto`, `trust`, `trusted`, `1`, `yes`,
///   `on` — grant full-auto trust for this workspace.
/// * `deny`, `0`, `no`, `off` — explicitly refuse trust; the calling command
///   will abort with a clear error instead of prompting.
pub(crate) const TRUST_OVERRIDE_ENV: &str = "VTCODE_TRUST_WORKSPACE";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TrustSelection {
    FullAuto,
    Quit,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EnvTrustOverride {
    FullAuto,
    Deny,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TrustOutcome {
    Granted,
    UserDeclined,
    EnvDenied,
    NonInteractive,
}

/// Non-interactive check: does this workspace already have full-auto trust
/// (either persisted or via env-var override)?  Never prompts.
pub(crate) async fn is_workspace_trusted(workspace: &Path) -> bool {
    if let Ok(Some(WorkspaceTrustLevel::FullAuto)) = load_workspace_trust_level(workspace).await {
        return true;
    }
    matches!(parse_env_trust_override().ok().flatten(), Some(EnvTrustOverride::FullAuto))
}

/// Grant full-auto trust for an already-running interactive TUI session.
///
/// The user is explicitly switching into an auto-permission agent, but the TUI
/// cannot safely show the normal blocking trust prompt while crossterm owns the
/// terminal. Persist the same trust bit the prompt would set so the switch can
/// complete instead of stopping the session at an unrecoverable warning.
pub(crate) async fn auto_grant_tui_full_auto_workspace_trust(workspace: &Path) -> Result<()> {
    if !vtcode_core::ui::is_tui_mode() || is_workspace_trusted(workspace).await {
        return Ok(());
    }

    if matches!(parse_env_trust_override().ok().flatten(), Some(EnvTrustOverride::Deny)) {
        bail!("Workspace trust denied via {TRUST_OVERRIDE_ENV}=deny; auto agent switch cannot continue.");
    }

    update_workspace_trust(workspace, WorkspaceTrustLevel::FullAuto)
        .await
        .context("Failed to persist full-auto workspace trust for auto agent switch")
}

/// Best-effort path used by `vtcode benchmark`: prompt the user interactively,
/// fall back to env-var overrides, otherwise return `Ok(false)` so the caller
/// can exit gracefully without a hard error.
pub(crate) async fn ensure_full_auto_workspace_trust(workspace: &Path) -> Result<bool> {
    let outcome = resolve_full_auto_workspace_trust(workspace, "benchmark").await?;
    let palette = ColorPalette::default();
    match outcome {
        TrustOutcome::Granted => Ok(true),
        TrustOutcome::UserDeclined => {
            let msg = "Workspace not trusted. Exiting benchmark command.";
            tui_safe_println(&render_styled(msg, palette.warning, None));
            Ok(false)
        }
        TrustOutcome::EnvDenied => {
            let msg = format!("Workspace trust denied via {TRUST_OVERRIDE_ENV}=deny. Exiting benchmark command.");
            tui_safe_println(&render_styled(&msg, palette.warning, None));
            Ok(false)
        }
        TrustOutcome::NonInteractive => {
            let msg = format!(
                "Workspace is not trusted and stdin is not a TTY. Re-run benchmark interactively or set {TRUST_OVERRIDE_ENV}=full-auto."
            );
            tui_safe_println(&render_styled(&msg, palette.warning, None));
            Ok(false)
        }
    }
}

/// Strict path used by `vtcode exec` and `--auto/--full-auto`: returns Ok only
/// when trust has been granted (either previously persisted, via env-var, or
/// freshly accepted at an interactive prompt). Any other outcome surfaces an
/// actionable error.
pub(crate) async fn require_full_auto_workspace_trust(
    workspace: &Path,
    denied_action: &str,
    command_name: &str,
) -> Result<()> {
    let outcome = resolve_full_auto_workspace_trust(workspace, command_name).await?;
    match outcome {
        TrustOutcome::Granted => Ok(()),
        TrustOutcome::UserDeclined => {
            bail!("Workspace trust declined. {denied_action} requires full-auto trust before continuing.")
        }
        TrustOutcome::EnvDenied => bail!(
            "Workspace trust denied via {TRUST_OVERRIDE_ENV}=deny; {denied_action} cannot continue. Unset the variable or set it to full-auto."
        ),
        TrustOutcome::NonInteractive => {
            let workspace_display = workspace.display();
            bail!(
                "Workspace '{workspace_display}' is not trusted; {denied_action} requires full-auto workspace trust.\nGrant trust by one of:\n  - Re-run {command_name} from a terminal (TTY) and accept the trust prompt\n  - Set {TRUST_OVERRIDE_ENV}=full-auto and re-run {command_name}\n  - Start VT Code interactively once and mark this workspace as full auto"
            );
        }
    }
}

async fn resolve_full_auto_workspace_trust(workspace: &Path, command_name: &str) -> Result<TrustOutcome> {
    let current_level = load_workspace_trust_level(workspace)
        .await
        .with_context(|| format!("Failed to determine workspace trust level for {command_name}"))?;

    if current_level == Some(WorkspaceTrustLevel::FullAuto) {
        return Ok(TrustOutcome::Granted);
    }

    if let Some(env_override) = parse_env_trust_override()? {
        return match env_override {
            EnvTrustOverride::FullAuto => {
                update_workspace_trust(workspace, WorkspaceTrustLevel::FullAuto)
                    .await
                    .context("Failed to persist full-auto workspace trust")?;
                if !quiet_env_overrides() {
                    let palette = ColorPalette::default();
                    let msg = format!("Workspace trusted via {TRUST_OVERRIDE_ENV}=full-auto for {command_name}.");
                    println!("{}", render_styled(&msg, palette.success, None));
                }
                Ok(TrustOutcome::Granted)
            }
            EnvTrustOverride::Deny => Ok(TrustOutcome::EnvDenied),
        };
    }

    if !prompt_capable() {
        return Ok(TrustOutcome::NonInteractive);
    }

    let require_full_auto_upgrade = current_level.is_some();
    render_prompt(workspace, require_full_auto_upgrade);

    match read_user_selection()? {
        TrustSelection::FullAuto => {
            update_workspace_trust(workspace, WorkspaceTrustLevel::FullAuto)
                .await
                .context("Failed to persist full-auto workspace trust")?;
            let palette = ColorPalette::default();
            let msg = "Workspace marked as trusted with full auto capabilities.";
            println!("{}", render_styled(msg, palette.success, None));
            Ok(TrustOutcome::Granted)
        }
        TrustSelection::Quit => Ok(TrustOutcome::UserDeclined),
    }
}

fn prompt_capable() -> bool {
    // When the TUI is active, crossterm owns the terminal.  Using raw
    // println! / stdin.read_line() would corrupt the display and block the
    // event loop, so treat the session as non-interactive here.  The caller
    // will surface the trust requirement through its own renderer.
    if vtcode_core::ui::is_tui_mode() {
        return false;
    }
    io::stdin().is_tty_ext() && io::stdout().is_tty_ext()
}

/// Print a line only when not in TUI mode.  In TUI mode crossterm owns the
/// terminal and raw println! would corrupt the display; the message is
/// logged via tracing instead.
fn tui_safe_println(msg: &str) {
    if vtcode_core::ui::is_tui_mode() {
        tracing::info!(target: "workspace_trust", "{msg}");
    } else {
        println!("{msg}");
    }
}

fn quiet_env_overrides() -> bool {
    matches!(env::var("VTCODE_TRUST_WORKSPACE_QUIET").ok().as_deref(), Some("1") | Some("true") | Some("yes"))
}

fn parse_env_trust_override() -> Result<Option<EnvTrustOverride>> {
    let raw = match env::var(TRUST_OVERRIDE_ENV) {
        Ok(value) => value,
        Err(env::VarError::NotPresent) => return Ok(None),
        Err(env::VarError::NotUnicode(_)) => {
            bail!("{TRUST_OVERRIDE_ENV} must be valid UTF-8");
        }
    };

    classify_env_trust_value(&raw)
        .map(Some)
        .with_context(|| format!("Invalid value for {TRUST_OVERRIDE_ENV}: '{raw}'"))
}

fn classify_env_trust_value(raw: &str) -> Result<EnvTrustOverride> {
    let normalized = raw.trim().to_ascii_lowercase();
    match normalized.as_str() {
        "" => bail!("value is empty. Use 'full-auto' to grant trust or 'deny' to refuse without prompting."),
        "full-auto" | "full_auto" | "fullauto" | "trust" | "trusted" | "1" | "yes" | "on" => {
            Ok(EnvTrustOverride::FullAuto)
        }
        "deny" | "denied" | "0" | "no" | "off" => Ok(EnvTrustOverride::Deny),
        _ => bail!("unsupported value. Use 'full-auto' to grant trust or 'deny' to refuse without prompting."),
    }
}

fn render_prompt(workspace: &Path, require_full_auto_upgrade: bool) {
    println!();
    print_prompt_line("[WARN] Workspace Trust Required", PromptTone::Heading);
    println!();
    print_prompt_line("VT Code can execute code and access files in your workspace.", PromptTone::Body);
    print_prompt_line("Trusting this workspace also trusts all MCP servers configured here.", PromptTone::Body);
    println!();
    print_prompt_line("Full-auto, exec, and autonomous runs require workspace trust.", PromptTone::Body);
    print_prompt_line(
        &format!("Set {TRUST_OVERRIDE_ENV}=full-auto to skip this prompt in CI / non-interactive runs."),
        PromptTone::Body,
    );
    println!();

    let workspace_display = workspace.display();
    print_prompt_line(&format!("Workspace: {workspace_display}"), PromptTone::Body);
    println!();

    let requirement_line = if require_full_auto_upgrade {
        "Full-auto permission review requires upgrading this workspace to full auto."
    } else {
        "Trust this workspace with full auto capabilities to continue:"
    };
    print_prompt_line(requirement_line, PromptTone::Body);
    println!();

    print_prompt_line("  [a] Trust with full auto capabilities", PromptTone::Body);
    print_prompt_line("  [q] Quit without trusting", PromptTone::Body);
    println!();
}

fn read_user_selection() -> Result<TrustSelection> {
    loop {
        print_prompt(CHOICE_PROMPT, PromptTone::Heading)?;
        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .context("Failed to read workspace trust selection")?;

        if let Some(selection) = parse_trust_selection(input.trim()) {
            return Ok(selection);
        }

        print_prompt_line(INVALID_CHOICE_MESSAGE, PromptTone::Heading);
    }
}

fn parse_trust_selection(input: &str) -> Option<TrustSelection> {
    if input.eq_ignore_ascii_case("a") {
        Some(TrustSelection::FullAuto)
    } else if input.eq_ignore_ascii_case("q") {
        Some(TrustSelection::Quit)
    } else {
        None
    }
}

enum PromptTone {
    Heading,
    Body,
}

fn print_prompt(message: &str, tone: PromptTone) -> Result<()> {
    print!("{}", styled_prompt_message(message, tone));
    io::stdout().flush().context("Failed to flush workspace trust prompt")
}

fn print_prompt_line(message: &str, tone: PromptTone) {
    println!("{}", styled_prompt_message(message, tone));
}

fn styled_prompt_message(message: &str, tone: PromptTone) -> String {
    use anstyle::Color;

    let is_light_theme = matches!(ansi_capabilities::detect_color_scheme(), ColorScheme::Light);
    let (rgb, is_heading) = match tone {
        PromptTone::Heading => (WARNING_RGB, true),
        PromptTone::Body => (INFO_RGB, false),
    };

    let color = Color::Ansi256(Ansi256Color(rgb_to_ansi256_for_theme(rgb.0, rgb.1, rgb.2, is_light_theme)));
    if is_heading {
        render_styled(message, color, Some("bold".to_string()))
    } else {
        render_styled(message, color, None)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_trust_selection_accepts_expected_choices() {
        assert_eq!(parse_trust_selection("a"), Some(TrustSelection::FullAuto));
        assert_eq!(parse_trust_selection("q"), Some(TrustSelection::Quit));
    }

    #[test]
    fn parse_trust_selection_rejects_unknown_choices() {
        assert_eq!(parse_trust_selection(""), None);
        assert_eq!(parse_trust_selection("w"), None);
    }

    #[test]
    fn classify_env_trust_value_recognises_grant_aliases() {
        for value in [
            "full-auto",
            "Full_Auto",
            "FULLAUTO",
            "trust",
            "trusted",
            "1",
            "yes",
            "on",
        ] {
            assert_eq!(
                classify_env_trust_value(value).expect("grant alias should parse"),
                EnvTrustOverride::FullAuto,
                "{value} should grant trust"
            );
        }
    }

    #[test]
    fn classify_env_trust_value_recognises_deny_aliases() {
        for value in ["deny", "DENIED", "0", "no", "off"] {
            assert_eq!(
                classify_env_trust_value(value).expect("deny alias should parse"),
                EnvTrustOverride::Deny,
                "{value} should deny trust"
            );
        }
    }

    #[test]
    fn classify_env_trust_value_rejects_unknown_values() {
        let err = classify_env_trust_value("maybe").expect_err("unknown value should fail");
        assert!(err.to_string().contains("unsupported value"));
    }

    #[test]
    fn classify_env_trust_value_rejects_empty() {
        let err = classify_env_trust_value("   ").expect_err("empty value should fail");
        assert!(err.to_string().contains("empty"));
    }

    #[tokio::test]
    async fn require_full_auto_workspace_trust_rejects_env_denied() {
        // An untrusted temp workspace with `VTCODE_TRUST_WORKSPACE=deny` must be
        // rejected before any full-auto session starts. This is the gate the
        // interactive codex-app-server path now shares with `--auto`.
        let temp = tempfile::TempDir::new().expect("tempdir");

        let env_guard = vtcode_commons::env_lock::lock();
        let previous = env::var_os(TRUST_OVERRIDE_ENV);
        env_guard.set_var(TRUST_OVERRIDE_ENV, "deny");

        let err = require_full_auto_workspace_trust(temp.path(), "interactive full-auto", "test")
            .await
            .expect_err("denied trust should error");

        env_guard.restore_var(TRUST_OVERRIDE_ENV, previous);

        assert!(err.to_string().contains("denied"), "expected a denial error, got: {err}");
    }

    #[tokio::test]
    async fn require_full_auto_workspace_trust_non_interactive_untrusted_errors() {
        // An untrusted temp workspace with no env override and no TTY (tests
        // run without one) yields a `NonInteractive` outcome that must surface
        // an actionable error rather than silently proceeding.
        let temp = tempfile::TempDir::new().expect("tempdir");

        let env_guard = vtcode_commons::env_lock::lock();
        let previous = env::var_os(TRUST_OVERRIDE_ENV);
        env_guard.remove_var(TRUST_OVERRIDE_ENV);

        let err = require_full_auto_workspace_trust(temp.path(), "interactive full-auto", "test")
            .await
            .expect_err("untrusted non-interactive should error");

        env_guard.restore_var(TRUST_OVERRIDE_ENV, previous);

        assert!(err.to_string().contains("not trusted"), "expected a not-trusted error, got: {err}");
    }

    #[tokio::test]
    async fn tui_auto_grant_persists_full_auto_workspace_trust() {
        let temp = tempfile::TempDir::new().expect("tempdir");
        let env_guard = vtcode_commons::env_lock::lock();
        let previous_env = env::var_os(TRUST_OVERRIDE_ENV);
        env_guard.remove_var(TRUST_OVERRIDE_ENV);
        let was_tui = vtcode_core::ui::is_tui_mode();
        vtcode_core::ui::set_tui_mode(true);

        auto_grant_tui_full_auto_workspace_trust(temp.path())
            .await
            .expect("TUI auto grant should persist trust");

        vtcode_core::ui::set_tui_mode(was_tui);
        env_guard.restore_var(TRUST_OVERRIDE_ENV, previous_env);

        assert_eq!(load_workspace_trust_level(temp.path()).await.unwrap(), Some(WorkspaceTrustLevel::FullAuto));
    }

    #[tokio::test]
    async fn tui_auto_grant_respects_explicit_env_deny() {
        let temp = tempfile::TempDir::new().expect("tempdir");
        let env_guard = vtcode_commons::env_lock::lock();
        let previous_env = env::var_os(TRUST_OVERRIDE_ENV);
        env_guard.set_var(TRUST_OVERRIDE_ENV, "deny");
        let was_tui = vtcode_core::ui::is_tui_mode();
        vtcode_core::ui::set_tui_mode(true);

        let err = auto_grant_tui_full_auto_workspace_trust(temp.path())
            .await
            .expect_err("explicit deny should block auto grant");

        vtcode_core::ui::set_tui_mode(was_tui);
        env_guard.restore_var(TRUST_OVERRIDE_ENV, previous_env);

        assert!(err.to_string().contains("denied"));
        assert_eq!(load_workspace_trust_level(temp.path()).await.unwrap(), None);
    }
}