Skip to main content

vtcode_acp/
capabilities.rs

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