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 hashbrown::HashMap;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
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
247/// Model information
248#[derive(Debug, Clone, Serialize, Deserialize)]
249#[serde(rename_all = "camelCase")]
250pub struct ModelInfo {
251    /// Model identifier
252    pub id: String,
253
254    /// Model name
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub name: Option<String>,
257
258    /// Provider name
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub provider: Option<String>,
261
262    /// Context window size
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub context_window: Option<u32>,
265}
266
267// ============================================================================
268// Client/Agent Info
269// ============================================================================
270
271/// Information about the client (IDE/host)
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct ClientInfo {
274    /// Client name
275    pub name: String,
276
277    /// Client version
278    pub version: String,
279
280    /// Additional metadata
281    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
282    pub metadata: HashMap<String, Value>,
283}
284
285impl Default for ClientInfo {
286    fn default() -> Self {
287        Self {
288            name: "vtcode".to_string(),
289            version: env!("CARGO_PKG_VERSION").to_string(),
290            metadata: HashMap::new(),
291        }
292    }
293}
294
295/// Information about the agent
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct AgentInfo {
298    /// Agent name
299    pub name: String,
300
301    /// Agent version
302    pub version: String,
303
304    /// Agent description
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub description: Option<String>,
307
308    /// Additional metadata
309    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
310    pub metadata: HashMap<String, Value>,
311}
312
313impl Default for AgentInfo {
314    fn default() -> Self {
315        Self {
316            name: "vtcode-agent".to_string(),
317            version: env!("CARGO_PKG_VERSION").to_string(),
318            description: Some("VT Code AI coding agent".to_string()),
319            metadata: HashMap::new(),
320        }
321    }
322}
323
324// ============================================================================
325// Authentication
326// ============================================================================
327
328/// Authentication requirements
329#[derive(Debug, Clone, Serialize, Deserialize)]
330#[serde(rename_all = "camelCase")]
331pub struct AuthRequirements {
332    /// Whether authentication is required
333    pub required: bool,
334
335    /// Supported authentication methods
336    #[serde(default, skip_serializing_if = "Vec::is_empty")]
337    pub methods: Vec<AuthMethod>,
338}
339
340/// Supported authentication methods
341///
342/// Follows ACP authentication specification:
343/// https://agentclientprotocol.com/protocol/auth
344#[derive(Debug, Clone, Serialize, Deserialize)]
345#[serde(tag = "type", rename_all = "snake_case")]
346pub enum AuthMethod {
347    /// Agent handles authentication itself (default/backward-compatible)
348    #[serde(rename = "agent")]
349    Agent {
350        /// Unique identifier for this auth method
351        id: String,
352        /// Human-readable name
353        name: String,
354        /// Description of the auth method
355        #[serde(skip_serializing_if = "Option::is_none")]
356        description: Option<String>,
357    },
358
359    /// Environment variable-based authentication
360    /// User provides a key/credential that client passes as environment variable
361    #[serde(rename = "env_var")]
362    EnvVar {
363        /// Unique identifier for this auth method
364        id: String,
365        /// Human-readable name
366        name: String,
367        /// Description of the auth method
368        #[serde(skip_serializing_if = "Option::is_none")]
369        description: Option<String>,
370        /// Environment variable name to set
371        var_name: String,
372        /// Optional link to page where user can get their key
373        #[serde(skip_serializing_if = "Option::is_none")]
374        link: Option<String>,
375    },
376
377    /// Terminal/TUI-based interactive authentication
378    /// Client launches interactive terminal for user to login
379    #[serde(rename = "terminal")]
380    Terminal {
381        /// Unique identifier for this auth method
382        id: String,
383        /// Human-readable name
384        name: String,
385        /// Description of the auth method
386        #[serde(skip_serializing_if = "Option::is_none")]
387        description: Option<String>,
388        /// Additional arguments to pass to agent command
389        #[serde(default, skip_serializing_if = "Vec::is_empty")]
390        args: Vec<String>,
391        /// Additional environment variables to set
392        #[serde(default, skip_serializing_if = "HashMap::is_empty")]
393        env: HashMap<String, String>,
394    },
395
396    /// Legacy: API key authentication (deprecated, use EnvVar instead)
397    #[serde(rename = "api_key")]
398    ApiKey,
399
400    /// Legacy: OAuth 2.0 (use Terminal for interactive flows)
401    #[serde(rename = "oauth2")]
402    OAuth2,
403
404    /// Legacy: Bearer token authentication
405    #[serde(rename = "bearer")]
406    Bearer,
407
408    /// Custom authentication (agent-specific)
409    #[serde(rename = "custom")]
410    Custom(String),
411}
412
413/// Parameters for authenticate method
414#[derive(Debug, Clone, Serialize, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct AuthenticateParams {
417    /// Authentication method being used
418    pub method: AuthMethod,
419
420    /// Authentication credentials
421    pub credentials: AuthCredentials,
422}
423
424/// Authentication credentials
425#[derive(Debug, Clone, Serialize, Deserialize)]
426#[serde(tag = "type", rename_all = "snake_case")]
427pub enum AuthCredentials {
428    /// API key
429    ApiKey { key: String },
430
431    /// Bearer token
432    Bearer { token: String },
433
434    /// OAuth2 token
435    OAuth2 {
436        access_token: String,
437        #[serde(skip_serializing_if = "Option::is_none")]
438        refresh_token: Option<String>,
439    },
440}
441
442/// Result of authenticate method
443#[derive(Debug, Clone, Serialize, Deserialize)]
444#[serde(rename_all = "camelCase")]
445pub struct AuthenticateResult {
446    /// Whether authentication succeeded
447    pub authenticated: bool,
448
449    /// Session token (if applicable)
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub session_token: Option<String>,
452
453    /// Token expiration (ISO 8601)
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub expires_at: Option<String>,
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    #[test]
463    fn test_initialize_params_default() {
464        let params = InitializeParams::default();
465        assert!(!params.protocol_versions.is_empty());
466        assert!(
467            params
468                .protocol_versions
469                .contains(&PROTOCOL_VERSION.to_string())
470        );
471    }
472
473    #[test]
474    fn test_client_info_default() {
475        let info = ClientInfo::default();
476        assert_eq!(info.name, "vtcode");
477        assert!(!info.version.is_empty());
478    }
479
480    #[test]
481    fn test_capabilities_serialization() {
482        let caps = ClientCapabilities {
483            filesystem: FilesystemCapabilities {
484                read: true,
485                write: true,
486                list: true,
487                search: true,
488                watch: false,
489            },
490            terminal: TerminalCapabilities {
491                create: true,
492                input: true,
493                output: true,
494                pty: true,
495            },
496            ..Default::default()
497        };
498
499        let json = serde_json::to_value(&caps).unwrap();
500        assert_eq!(json["filesystem"]["read"], true);
501        assert_eq!(json["terminal"]["pty"], true);
502    }
503
504    #[test]
505    fn test_auth_credentials() {
506        let creds = AuthCredentials::ApiKey {
507            key: "sk-test123".to_string(),
508        };
509        let json = serde_json::to_value(&creds).unwrap();
510        assert_eq!(json["type"], "api_key");
511        assert_eq!(json["key"], "sk-test123");
512    }
513
514    #[test]
515    fn test_auth_method_agent() {
516        let method = AuthMethod::Agent {
517            id: "agent_auth".to_string(),
518            name: "Agent Authentication".to_string(),
519            description: Some("Let agent handle authentication".to_string()),
520        };
521        let json = serde_json::to_value(&method).unwrap();
522        assert_eq!(json["type"], "agent");
523        assert_eq!(json["id"], "agent_auth");
524        assert_eq!(json["name"], "Agent Authentication");
525    }
526
527    #[test]
528    fn test_auth_method_env_var() {
529        let method = AuthMethod::EnvVar {
530            id: "openai_key".to_string(),
531            name: "OpenAI API Key".to_string(),
532            description: Some("Provide your OpenAI API key".to_string()),
533            var_name: "OPENAI_API_KEY".to_string(),
534            link: Some("https://platform.openai.com/api-keys".to_string()),
535        };
536        let json = serde_json::to_value(&method).unwrap();
537        assert_eq!(json["type"], "env_var");
538        assert_eq!(json["id"], "openai_key");
539        assert_eq!(json["name"], "OpenAI API Key");
540        assert_eq!(json["var_name"], "OPENAI_API_KEY");
541        assert_eq!(json["link"], "https://platform.openai.com/api-keys");
542    }
543
544    #[test]
545    fn test_auth_method_terminal() {
546        let mut env = HashMap::new();
547        env.insert("VAR1".to_string(), "value1".to_string());
548
549        let method = AuthMethod::Terminal {
550            id: "terminal_login".to_string(),
551            name: "Terminal Login".to_string(),
552            description: Some("Login via interactive terminal".to_string()),
553            args: vec!["--login".to_string(), "--interactive".to_string()],
554            env,
555        };
556        let json = serde_json::to_value(&method).unwrap();
557        assert_eq!(json["type"], "terminal");
558        assert_eq!(json["args"][0], "--login");
559        assert_eq!(json["env"]["VAR1"], "value1");
560    }
561
562    #[test]
563    fn test_auth_method_serialization_roundtrip() {
564        let method = AuthMethod::EnvVar {
565            id: "test_id".to_string(),
566            name: "Test".to_string(),
567            description: None,
568            var_name: "TEST_VAR".to_string(),
569            link: None,
570        };
571
572        let json = serde_json::to_value(&method).unwrap();
573        let deserialized: AuthMethod = serde_json::from_value(json).unwrap();
574
575        match deserialized {
576            AuthMethod::EnvVar {
577                id, name, var_name, ..
578            } => {
579                assert_eq!(id, "test_id");
580                assert_eq!(name, "Test");
581                assert_eq!(var_name, "TEST_VAR");
582            }
583            _ => panic!("Unexpected auth method variant"),
584        }
585    }
586
587    #[test]
588    fn test_legacy_auth_methods() {
589        // Ensure backward compatibility
590        let json = serde_json::json!({"type": "api_key"});
591        let method: AuthMethod = serde_json::from_value(json).unwrap();
592        matches!(method, AuthMethod::ApiKey);
593
594        let json = serde_json::json!({"type": "oauth2"});
595        let method: AuthMethod = serde_json::from_value(json).unwrap();
596        matches!(method, AuthMethod::OAuth2);
597
598        let json = serde_json::json!({"type": "bearer"});
599        let method: AuthMethod = serde_json::from_value(json).unwrap();
600        matches!(method, AuthMethod::Bearer);
601    }
602}