Skip to main content

vtcode_acp_client/
capabilities.rs

1//! ACP capabilities and initialization types
2//!
3//! This module implements the capability negotiation as defined by ACP:
4//! - Protocol version negotiation
5//! - Feature capability exchange
6//! - Agent information structures
7//!
8//! Reference: https://agentclientprotocol.com/llms.txt
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::HashMap;
13
14/// Current ACP protocol version supported by this implementation
15pub const PROTOCOL_VERSION: &str = "2025-01-01";
16
17/// Supported protocol versions (newest first)
18pub const SUPPORTED_VERSIONS: &[&str] = &["2025-01-01", "2024-11-01"];
19
20// ============================================================================
21// Initialize Request/Response
22// ============================================================================
23
24/// Parameters for the initialize method
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct InitializeParams {
28    /// Protocol versions the client supports (newest first)
29    pub protocol_versions: Vec<String>,
30
31    /// Client capabilities
32    pub capabilities: ClientCapabilities,
33
34    /// Client information
35    pub client_info: ClientInfo,
36}
37
38impl Default for InitializeParams {
39    fn default() -> Self {
40        Self {
41            protocol_versions: SUPPORTED_VERSIONS.iter().map(|s| s.to_string()).collect(),
42            capabilities: ClientCapabilities::default(),
43            client_info: ClientInfo::default(),
44        }
45    }
46}
47
48/// Result of the initialize method
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct InitializeResult {
52    /// Negotiated protocol version
53    pub protocol_version: String,
54
55    /// Agent capabilities
56    pub capabilities: AgentCapabilities,
57
58    /// Agent information
59    pub agent_info: AgentInfo,
60
61    /// Authentication requirements (if any)
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub auth_requirements: Option<AuthRequirements>,
64}
65
66// ============================================================================
67// Client Capabilities
68// ============================================================================
69
70/// Capabilities the client (IDE/host) provides to the agent
71#[derive(Debug, Clone, Default, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73pub struct ClientCapabilities {
74    /// File system operations
75    #[serde(default)]
76    pub filesystem: FilesystemCapabilities,
77
78    /// Terminal/shell capabilities
79    #[serde(default)]
80    pub terminal: TerminalCapabilities,
81
82    /// UI/notification capabilities
83    #[serde(default)]
84    pub ui: UiCapabilities,
85
86    /// MCP server connections the client can provide
87    #[serde(default, skip_serializing_if = "Vec::is_empty")]
88    pub mcp_servers: Vec<McpServerCapability>,
89
90    /// Extension points for custom capabilities
91    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
92    pub extensions: HashMap<String, Value>,
93}
94
95/// File system operation capabilities
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct FilesystemCapabilities {
99    /// Can read files
100    #[serde(default)]
101    pub read: bool,
102
103    /// Can write files
104    #[serde(default)]
105    pub write: bool,
106
107    /// Can list directories
108    #[serde(default)]
109    pub list: bool,
110
111    /// Can search files (grep/find)
112    #[serde(default)]
113    pub search: bool,
114
115    /// Can watch for file changes
116    #[serde(default)]
117    pub watch: bool,
118}
119
120/// Terminal operation capabilities
121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
122#[serde(rename_all = "camelCase")]
123pub struct TerminalCapabilities {
124    /// Can create terminal sessions
125    #[serde(default)]
126    pub create: bool,
127
128    /// Can send input to terminals
129    #[serde(default)]
130    pub input: bool,
131
132    /// Can read terminal output
133    #[serde(default)]
134    pub output: bool,
135
136    /// Supports PTY (pseudo-terminal)
137    #[serde(default)]
138    pub pty: bool,
139}
140
141/// UI/notification capabilities
142#[derive(Debug, Clone, Default, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct UiCapabilities {
145    /// Can show notifications
146    #[serde(default)]
147    pub notifications: bool,
148
149    /// Can show progress indicators
150    #[serde(default)]
151    pub progress: bool,
152
153    /// Can prompt for user input
154    #[serde(default)]
155    pub input_prompt: bool,
156
157    /// Can show file diffs
158    #[serde(default)]
159    pub diff_view: bool,
160}
161
162/// MCP server connection capability
163#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct McpServerCapability {
166    /// Server name/identifier
167    pub name: String,
168
169    /// Server transport type (stdio, http, sse)
170    pub transport: String,
171
172    /// Tools this server provides
173    #[serde(default, skip_serializing_if = "Vec::is_empty")]
174    pub tools: Vec<String>,
175}
176
177// ============================================================================
178// Agent Capabilities
179// ============================================================================
180
181/// Capabilities the agent provides
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct AgentCapabilities {
185    /// Available tools
186    #[serde(default, skip_serializing_if = "Vec::is_empty")]
187    pub tools: Vec<ToolCapability>,
188
189    /// Supported features
190    #[serde(default)]
191    pub features: AgentFeatures,
192
193    /// Model information
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub model: Option<ModelInfo>,
196
197    /// Extension points
198    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
199    pub extensions: HashMap<String, Value>,
200}
201
202/// A tool the agent can execute
203#[derive(Debug, Clone, Serialize, Deserialize)]
204#[serde(rename_all = "camelCase")]
205pub struct ToolCapability {
206    /// Tool name
207    pub name: String,
208
209    /// Tool description
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub description: Option<String>,
212
213    /// Input schema (JSON Schema)
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub input_schema: Option<Value>,
216
217    /// Whether tool requires user confirmation
218    #[serde(default)]
219    pub requires_confirmation: bool,
220}
221
222/// Agent feature flags
223#[derive(Debug, Clone, Default, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct AgentFeatures {
226    /// Supports streaming responses
227    #[serde(default)]
228    pub streaming: bool,
229
230    /// Supports multi-turn conversations
231    #[serde(default)]
232    pub multi_turn: bool,
233
234    /// Supports session persistence
235    #[serde(default)]
236    pub session_persistence: bool,
237
238    /// Supports image/vision input
239    #[serde(default)]
240    pub vision: bool,
241
242    /// Supports code execution
243    #[serde(default)]
244    pub code_execution: bool,
245
246    /// Supports subagent spawning
247    #[serde(default)]
248    pub subagents: bool,
249}
250
251/// Model information
252#[derive(Debug, Clone, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct ModelInfo {
255    /// Model identifier
256    pub id: String,
257
258    /// Model name
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub name: Option<String>,
261
262    /// Provider name
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub provider: Option<String>,
265
266    /// Context window size
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub context_window: Option<u32>,
269}
270
271// ============================================================================
272// Client/Agent Info
273// ============================================================================
274
275/// Information about the client (IDE/host)
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct ClientInfo {
278    /// Client name
279    pub name: String,
280
281    /// Client version
282    pub version: String,
283
284    /// Additional metadata
285    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
286    pub metadata: HashMap<String, Value>,
287}
288
289impl Default for ClientInfo {
290    fn default() -> Self {
291        Self {
292            name: "vtcode".to_string(),
293            version: env!("CARGO_PKG_VERSION").to_string(),
294            metadata: HashMap::new(),
295        }
296    }
297}
298
299/// Information about the agent
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct AgentInfo {
302    /// Agent name
303    pub name: String,
304
305    /// Agent version
306    pub version: String,
307
308    /// Agent description
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub description: Option<String>,
311
312    /// Additional metadata
313    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
314    pub metadata: HashMap<String, Value>,
315}
316
317impl Default for AgentInfo {
318    fn default() -> Self {
319        Self {
320            name: "vtcode-agent".to_string(),
321            version: env!("CARGO_PKG_VERSION").to_string(),
322            description: Some("VT Code AI coding agent".to_string()),
323            metadata: HashMap::new(),
324        }
325    }
326}
327
328// ============================================================================
329// Authentication
330// ============================================================================
331
332/// Authentication requirements
333#[derive(Debug, Clone, Serialize, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct AuthRequirements {
336    /// Whether authentication is required
337    pub required: bool,
338
339    /// Supported authentication methods
340    #[serde(default, skip_serializing_if = "Vec::is_empty")]
341    pub methods: Vec<AuthMethod>,
342}
343
344/// Supported authentication methods
345/// 
346/// Follows ACP authentication specification:
347/// https://agentclientprotocol.com/protocol/auth
348#[derive(Debug, Clone, Serialize, Deserialize)]
349#[serde(tag = "type", rename_all = "snake_case")]
350pub enum AuthMethod {
351    /// Agent handles authentication itself (default/backward-compatible)
352    #[serde(rename = "agent")]
353    Agent {
354        /// Unique identifier for this auth method
355        id: String,
356        /// Human-readable name
357        name: String,
358        /// Description of the auth method
359        #[serde(skip_serializing_if = "Option::is_none")]
360        description: Option<String>,
361    },
362    
363    /// Environment variable-based authentication
364    /// User provides a key/credential that client passes as environment variable
365    #[serde(rename = "env_var")]
366    EnvVar {
367        /// Unique identifier for this auth method
368        id: String,
369        /// Human-readable name
370        name: String,
371        /// Description of the auth method
372        #[serde(skip_serializing_if = "Option::is_none")]
373        description: Option<String>,
374        /// Environment variable name to set
375        var_name: String,
376        /// Optional link to page where user can get their key
377        #[serde(skip_serializing_if = "Option::is_none")]
378        link: Option<String>,
379    },
380    
381    /// Terminal/TUI-based interactive authentication
382    /// Client launches interactive terminal for user to login
383    #[serde(rename = "terminal")]
384    Terminal {
385        /// Unique identifier for this auth method
386        id: String,
387        /// Human-readable name
388        name: String,
389        /// Description of the auth method
390        #[serde(skip_serializing_if = "Option::is_none")]
391        description: Option<String>,
392        /// Additional arguments to pass to agent command
393        #[serde(default, skip_serializing_if = "Vec::is_empty")]
394        args: Vec<String>,
395        /// Additional environment variables to set
396        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
397        env: HashMap<String, String>,
398    },
399    
400    /// Legacy: API key authentication (deprecated, use EnvVar instead)
401    #[serde(rename = "api_key")]
402    ApiKey,
403    
404    /// Legacy: OAuth 2.0 (use Terminal for interactive flows)
405    #[serde(rename = "oauth2")]
406    OAuth2,
407    
408    /// Legacy: Bearer token authentication
409    #[serde(rename = "bearer")]
410    Bearer,
411    
412    /// Custom authentication (agent-specific)
413    #[serde(rename = "custom")]
414    Custom(String),
415}
416
417/// Parameters for authenticate method
418#[derive(Debug, Clone, Serialize, Deserialize)]
419#[serde(rename_all = "camelCase")]
420pub struct AuthenticateParams {
421    /// Authentication method being used
422    pub method: AuthMethod,
423
424    /// Authentication credentials
425    pub credentials: AuthCredentials,
426}
427
428/// Authentication credentials
429#[derive(Debug, Clone, Serialize, Deserialize)]
430#[serde(tag = "type", rename_all = "snake_case")]
431pub enum AuthCredentials {
432    /// API key
433    ApiKey { key: String },
434
435    /// Bearer token
436    Bearer { token: String },
437
438    /// OAuth2 token
439    OAuth2 {
440        access_token: String,
441        #[serde(skip_serializing_if = "Option::is_none")]
442        refresh_token: Option<String>,
443    },
444}
445
446/// Result of authenticate method
447#[derive(Debug, Clone, Serialize, Deserialize)]
448#[serde(rename_all = "camelCase")]
449pub struct AuthenticateResult {
450    /// Whether authentication succeeded
451    pub authenticated: bool,
452
453    /// Session token (if applicable)
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub session_token: Option<String>,
456
457    /// Token expiration (ISO 8601)
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub expires_at: Option<String>,
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_initialize_params_default() {
468        let params = InitializeParams::default();
469        assert!(!params.protocol_versions.is_empty());
470        assert!(
471            params
472                .protocol_versions
473                .contains(&PROTOCOL_VERSION.to_string())
474        );
475    }
476
477    #[test]
478    fn test_client_info_default() {
479        let info = ClientInfo::default();
480        assert_eq!(info.name, "vtcode");
481        assert!(!info.version.is_empty());
482    }
483
484    #[test]
485    fn test_capabilities_serialization() {
486        let caps = ClientCapabilities {
487            filesystem: FilesystemCapabilities {
488                read: true,
489                write: true,
490                list: true,
491                search: true,
492                watch: false,
493            },
494            terminal: TerminalCapabilities {
495                create: true,
496                input: true,
497                output: true,
498                pty: true,
499            },
500            ..Default::default()
501        };
502
503        let json = serde_json::to_value(&caps).unwrap();
504        assert_eq!(json["filesystem"]["read"], true);
505        assert_eq!(json["terminal"]["pty"], true);
506    }
507
508    #[test]
509    fn test_auth_credentials() {
510        let creds = AuthCredentials::ApiKey {
511            key: "sk-test123".to_string(),
512        };
513        let json = serde_json::to_value(&creds).unwrap();
514        assert_eq!(json["type"], "api_key");
515        assert_eq!(json["key"], "sk-test123");
516    }
517
518    #[test]
519    fn test_auth_method_agent() {
520        let method = AuthMethod::Agent {
521            id: "agent_auth".to_string(),
522            name: "Agent Authentication".to_string(),
523            description: Some("Let agent handle authentication".to_string()),
524        };
525        let json = serde_json::to_value(&method).unwrap();
526        assert_eq!(json["type"], "agent");
527        assert_eq!(json["id"], "agent_auth");
528        assert_eq!(json["name"], "Agent Authentication");
529    }
530
531    #[test]
532    fn test_auth_method_env_var() {
533        let method = AuthMethod::EnvVar {
534            id: "openai_key".to_string(),
535            name: "OpenAI API Key".to_string(),
536            description: Some("Provide your OpenAI API key".to_string()),
537            var_name: "OPENAI_API_KEY".to_string(),
538            link: Some("https://platform.openai.com/api-keys".to_string()),
539        };
540        let json = serde_json::to_value(&method).unwrap();
541        assert_eq!(json["type"], "env_var");
542        assert_eq!(json["id"], "openai_key");
543        assert_eq!(json["name"], "OpenAI API Key");
544        assert_eq!(json["var_name"], "OPENAI_API_KEY");
545        assert_eq!(json["link"], "https://platform.openai.com/api-keys");
546    }
547
548    #[test]
549    fn test_auth_method_terminal() {
550        let mut env = HashMap::new();
551        env.insert("VAR1".to_string(), "value1".to_string());
552        
553        let method = AuthMethod::Terminal {
554            id: "terminal_login".to_string(),
555            name: "Terminal Login".to_string(),
556            description: Some("Login via interactive terminal".to_string()),
557            args: vec!["--login".to_string(), "--interactive".to_string()],
558            env,
559        };
560        let json = serde_json::to_value(&method).unwrap();
561        assert_eq!(json["type"], "terminal");
562        assert_eq!(json["args"][0], "--login");
563        assert_eq!(json["env"]["VAR1"], "value1");
564    }
565
566    #[test]
567    fn test_auth_method_serialization_roundtrip() {
568        let method = AuthMethod::EnvVar {
569            id: "test_id".to_string(),
570            name: "Test".to_string(),
571            description: None,
572            var_name: "TEST_VAR".to_string(),
573            link: None,
574        };
575        
576        let json = serde_json::to_value(&method).unwrap();
577        let deserialized: AuthMethod = serde_json::from_value(json).unwrap();
578        
579        match deserialized {
580            AuthMethod::EnvVar { id, name, var_name, .. } => {
581                assert_eq!(id, "test_id");
582                assert_eq!(name, "Test");
583                assert_eq!(var_name, "TEST_VAR");
584            }
585            _ => panic!("Unexpected auth method variant"),
586        }
587    }
588
589    #[test]
590    fn test_legacy_auth_methods() {
591        // Ensure backward compatibility
592        let json = serde_json::json!({"type": "api_key"});
593        let method: AuthMethod = serde_json::from_value(json).unwrap();
594        matches!(method, AuthMethod::ApiKey);
595
596        let json = serde_json::json!({"type": "oauth2"});
597        let method: AuthMethod = serde_json::from_value(json).unwrap();
598        matches!(method, AuthMethod::OAuth2);
599
600        let json = serde_json::json!({"type": "bearer"});
601        let method: AuthMethod = serde_json::from_value(json).unwrap();
602        matches!(method, AuthMethod::Bearer);
603    }
604}