vtcode_auth/credentials/mode.rs
1//! Storage backend selection for credentials.
2
3/// Preferred storage backend for credentials.
4///
5/// - `Keyring`: Use OS-specific secure storage (macOS Keychain, Windows Credential Manager,
6/// Linux Secret Service). This is the default as it's the most secure option.
7/// - `File`: Use AES-256-GCM encrypted file (requires the `file-storage` feature or
8/// custom implementation)
9/// - `Auto`: Try keyring first, fall back to file if unavailable
10#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12#[serde(rename_all = "lowercase")]
13pub enum AuthCredentialsStoreMode {
14 /// Use OS-specific keyring service.
15 /// This is the most secure option as credentials are managed by the OS
16 /// and are not accessible to other users or applications.
17 Keyring,
18 /// Persist credentials in an encrypted file.
19 /// The file is encrypted with AES-256-GCM using a machine-derived key.
20 File,
21 /// Use keyring when available; otherwise, fall back to file.
22 Auto,
23}
24
25impl Default for AuthCredentialsStoreMode {
26 /// Platform-aware default:
27 ///
28 /// - **macOS**: `File` — AES-256-GCM encrypted file with machine-derived key.
29 /// Avoids macOS Keychain authorization popups that trigger on every new
30 /// binary (including each release update). Users can opt into `Keyring`
31 /// via `credential_storage_mode` in `vtcode.toml`.
32 ///
33 /// - **Linux / Windows / others**: `Auto` — try OS keyring (Secret Service
34 /// / Windows Credential Manager, no popups), fall back to encrypted file.
35 fn default() -> Self {
36 #[cfg(target_os = "macos")]
37 {
38 Self::File
39 }
40 #[cfg(not(target_os = "macos"))]
41 {
42 Self::Auto
43 }
44 }
45}
46
47impl AuthCredentialsStoreMode {
48 /// Resolve `Auto` to the best available concrete backend.
49 ///
50 /// Follows the "parse, don't validate" pattern: the return type is
51 /// [`ResolvedStoreMode`], which has no `Auto` variant, so downstream
52 /// code matches exhaustively and never needs an `unreachable!` arm.
53 pub(crate) fn effective_mode(self) -> ResolvedStoreMode {
54 match self {
55 Self::Auto => {
56 if super::keyring::is_functional() {
57 ResolvedStoreMode::Keyring
58 } else {
59 tracing::debug!("Keyring not available, falling back to file storage");
60 ResolvedStoreMode::File
61 }
62 }
63 Self::Keyring => ResolvedStoreMode::Keyring,
64 Self::File => ResolvedStoreMode::File,
65 }
66 }
67}
68
69/// A concrete credential backend, with [`AuthCredentialsStoreMode::Auto`] resolved.
70///
71/// [`AuthCredentialsStoreMode::effective_mode`] parses the possibly-`Auto`
72/// configuration into this type once; every consumer then handles both
73/// backends exhaustively without a runtime `unreachable!` guard.
74#[derive(Debug, Copy, Clone, PartialEq, Eq)]
75pub(crate) enum ResolvedStoreMode {
76 /// Use the OS-specific keyring service.
77 Keyring,
78 /// Persist credentials in an encrypted file.
79 File,
80}
81
82#[cfg(test)]
83mod tests {
84 use super::{AuthCredentialsStoreMode, ResolvedStoreMode};
85
86 #[test]
87 fn explicit_modes_pass_through_unchanged() {
88 assert_eq!(AuthCredentialsStoreMode::Keyring.effective_mode(), ResolvedStoreMode::Keyring);
89 assert_eq!(AuthCredentialsStoreMode::File.effective_mode(), ResolvedStoreMode::File);
90 }
91
92 #[test]
93 fn auto_resolves_to_a_concrete_backend() {
94 // `Auto` must never survive resolution: the match is exhaustive over
95 // the concrete variants, so there is no `Auto` case to assert against.
96 match AuthCredentialsStoreMode::Auto.effective_mode() {
97 ResolvedStoreMode::Keyring | ResolvedStoreMode::File => {}
98 }
99 }
100}