Skip to main content

vtcode_acp/
jsonrpc.rs

1//! JSON-RPC 2.0 types for ACP protocol compliance
2//!
3//! This module implements the JSON-RPC 2.0 specification as required by the
4//! Agent Client Protocol (ACP). All ACP methods use JSON-RPC 2.0 as the
5//! transport layer.
6//!
7//! Reference: <https://agentclientprotocol.com/llms.txt>
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12/// JSON-RPC 2.0 version string (always "2.0")
13pub(crate) const JSONRPC_VERSION: &str = "2.0";
14
15/// JSON-RPC 2.0 request object
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct JsonRpcRequest {
18    /// Protocol version (always "2.0")
19    pub(crate) jsonrpc: String,
20
21    /// Method name to invoke
22    pub(crate) method: String,
23
24    /// Method parameters (positional or named)
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub(crate) params: Option<Value>,
27
28    /// Request ID for correlation (null for notifications)
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub(crate) id: Option<JsonRpcId>,
31}
32
33/// JSON-RPC 2.0 response object
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct JsonRpcResponse {
36    /// Protocol version (always "2.0")
37    jsonrpc: String,
38
39    /// Result on success (mutually exclusive with error)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub(crate) result: Option<Value>,
42
43    /// Error on failure (mutually exclusive with result)
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub(crate) error: Option<JsonRpcError>,
46
47    /// Request ID this response corresponds to
48    id: Option<JsonRpcId>,
49}
50
51/// JSON-RPC 2.0 error object
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct JsonRpcError {
54    /// Error code (integer)
55    pub(crate) code: i32,
56
57    /// Short error description
58    pub(crate) message: String,
59
60    /// Additional error data
61    #[serde(skip_serializing_if = "Option::is_none")]
62    data: Option<Value>,
63}
64
65/// JSON-RPC 2.0 request/response ID
66///
67/// Per spec, ID can be a string, number, or null
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
69#[serde(untagged)]
70pub enum JsonRpcId {
71    /// String ID
72    String(String),
73    /// Numeric ID
74    Number(i64),
75}
76
77impl JsonRpcId {
78    /// Create a new string ID
79    fn string(s: impl Into<String>) -> Self {
80        Self::String(s.into())
81    }
82
83    /// Create a new numeric ID
84    fn number(n: i64) -> Self {
85        Self::Number(n)
86    }
87
88    /// Generate a new UUID-based string ID
89    pub fn new_uuid() -> Self {
90        Self::String(uuid::Uuid::new_v4().to_string())
91    }
92}
93
94impl std::fmt::Display for JsonRpcId {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            JsonRpcId::String(s) => write!(f, "{s}"),
98            JsonRpcId::Number(n) => write!(f, "{n}"),
99        }
100    }
101}
102
103/// Standard JSON-RPC 2.0 error codes
104mod error_codes {
105    /// Parse error: Invalid JSON was received
106    pub const PARSE_ERROR: i32 = -32700;
107
108    /// Invalid request: The JSON sent is not a valid Request object
109    pub const INVALID_REQUEST: i32 = -32600;
110
111    /// Method not found: The method does not exist / is not available
112    pub(crate) const METHOD_NOT_FOUND: i32 = -32601;
113
114    /// Invalid params: Invalid method parameter(s)
115    pub const INVALID_PARAMS: i32 = -32602;
116
117    /// Internal error: Internal JSON-RPC error
118    pub const INTERNAL_ERROR: i32 = -32603;
119
120    /// Server error range: -32000 to -32099 (reserved for implementation-defined server-errors)
121    pub const SERVER_ERROR_START: i32 = -32099;
122    pub const SERVER_ERROR_END: i32 = -32000;
123
124    // ACP-specific error codes (in server error range)
125
126    /// Authentication required
127    pub(crate) const AUTH_REQUIRED: i32 = -32001;
128
129    /// Permission denied
130    pub(crate) const PERMISSION_DENIED: i32 = -32002;
131
132    /// Session not found
133    pub(crate) const SESSION_NOT_FOUND: i32 = -32003;
134
135    /// Rate limited
136    pub(crate) const RATE_LIMITED: i32 = -32004;
137
138    /// Resource not found
139    pub(crate) const RESOURCE_NOT_FOUND: i32 = -32005;
140}
141
142impl JsonRpcRequest {
143    /// Create a new JSON-RPC 2.0 request
144    fn new(method: impl Into<String>, params: Option<Value>, id: Option<JsonRpcId>) -> Self {
145        Self {
146            jsonrpc: JSONRPC_VERSION.to_string(),
147            method: method.into(),
148            params,
149            id,
150        }
151    }
152
153    /// Create a request with auto-generated UUID ID
154    pub fn with_uuid(method: impl Into<String>, params: Option<Value>) -> Self {
155        Self::new(method, params, Some(JsonRpcId::new_uuid()))
156    }
157
158    /// Create a notification (request without ID, no response expected)
159    pub(crate) fn notification(method: impl Into<String>, params: Option<Value>) -> Self {
160        Self::new(method, params, None)
161    }
162
163    /// Check if this is a notification (no ID means no response expected)
164    fn is_notification(&self) -> bool {
165        self.id.is_none()
166    }
167}
168
169impl JsonRpcResponse {
170    /// Create a successful response
171    fn success(result: Value, id: Option<JsonRpcId>) -> Self {
172        Self {
173            jsonrpc: JSONRPC_VERSION.to_string(),
174            result: Some(result),
175            error: None,
176            id,
177        }
178    }
179
180    /// Create an error response
181    fn error(error: JsonRpcError, id: Option<JsonRpcId>) -> Self {
182        Self {
183            jsonrpc: JSONRPC_VERSION.to_string(),
184            result: None,
185            error: Some(error),
186            id,
187        }
188    }
189
190    /// Check if response is successful
191    fn is_success(&self) -> bool {
192        self.error.is_none() && self.result.is_some()
193    }
194
195    /// Check if response is an error
196    fn is_error(&self) -> bool {
197        self.error.is_some()
198    }
199
200    /// Get result, returning error if response was an error
201    pub fn into_result(self) -> Result<Value, JsonRpcError> {
202        if let Some(error) = self.error {
203            Err(error)
204        } else {
205            Ok(self.result.unwrap_or(Value::Null))
206        }
207    }
208}
209
210impl JsonRpcError {
211    /// Create a new error
212    fn new(code: i32, message: impl Into<String>) -> Self {
213        Self { code, message: message.into(), data: None }
214    }
215
216    /// Create error with additional data
217    fn with_data(code: i32, message: impl Into<String>, data: Value) -> Self {
218        Self { code, message: message.into(), data: Some(data) }
219    }
220
221    /// Create a parse error
222    #[cold]
223    pub fn parse_error(details: impl Into<String>) -> Self {
224        Self::new(error_codes::PARSE_ERROR, details)
225    }
226
227    /// Create an invalid request error
228    #[cold]
229    pub fn invalid_request(details: impl Into<String>) -> Self {
230        Self::new(error_codes::INVALID_REQUEST, details)
231    }
232
233    /// Create a method not found error
234    #[cold]
235    fn method_not_found(method: impl Into<String>) -> Self {
236        Self::new(error_codes::METHOD_NOT_FOUND, format!("Method not found: {}", method.into()))
237    }
238
239    /// Create an invalid params error
240    #[cold]
241    pub fn invalid_params(details: impl Into<String>) -> Self {
242        Self::new(error_codes::INVALID_PARAMS, details)
243    }
244
245    /// Create an internal error
246    #[cold]
247    pub fn internal_error(details: impl Into<String>) -> Self {
248        Self::new(error_codes::INTERNAL_ERROR, details)
249    }
250
251    /// Create an authentication required error with list of available methods
252    ///
253    /// Per ACP spec, includes authMethods in error data to help clients
254    /// present appropriate UI for authentication options.
255    #[cold]
256    fn auth_required(auth_methods: Vec<super::AuthMethod>) -> Self {
257        let data = serde_json::json!({
258            "authMethods": auth_methods,
259        });
260        Self::with_data(error_codes::AUTH_REQUIRED, "Authentication required", data)
261    }
262
263    /// Create a permission denied error
264    fn permission_denied(details: impl Into<String>) -> Self {
265        Self::new(error_codes::PERMISSION_DENIED, details)
266    }
267
268    /// Create a session not found error
269    fn session_not_found(session_id: impl Into<String>) -> Self {
270        Self::new(error_codes::SESSION_NOT_FOUND, format!("Session not found: {}", session_id.into()))
271    }
272
273    /// Create a rate limited error
274    fn rate_limited(details: impl Into<String>) -> Self {
275        Self::new(error_codes::RATE_LIMITED, details)
276    }
277
278    /// Create a resource not found error
279    fn resource_not_found(resource: impl Into<String>) -> Self {
280        Self::new(error_codes::RESOURCE_NOT_FOUND, format!("Resource not found: {}", resource.into()))
281    }
282}
283
284#[cfg(test)]
285#[allow(
286    clippy::uninlined_format_args,
287    reason = "Intentional compatibility, platform, or test-only suppression."
288)]
289mod tests {
290    use super::*;
291    use serde_json::json;
292
293    #[test]
294    fn test_request_serialization() {
295        let req = JsonRpcRequest::new(
296            "initialize",
297            Some(json!({"protocolVersions": ["2025-01-01"]})),
298            Some(JsonRpcId::string("req-1")),
299        );
300
301        let json = serde_json::to_string(&req).unwrap();
302        assert!(json.contains("\"jsonrpc\":\"2.0\""));
303        assert!(json.contains("\"method\":\"initialize\""));
304        assert!(json.contains("\"id\":\"req-1\""));
305    }
306
307    #[test]
308    fn test_response_success() {
309        let resp = JsonRpcResponse::success(json!({"session_id": "sess-123"}), Some(JsonRpcId::string("req-1")));
310
311        assert!(resp.is_success());
312        assert!(!resp.is_error());
313    }
314
315    #[test]
316    fn test_response_error() {
317        let resp = JsonRpcResponse::error(JsonRpcError::method_not_found("unknown"), Some(JsonRpcId::string("req-1")));
318
319        assert!(resp.is_error());
320        assert!(!resp.is_success());
321    }
322
323    #[test]
324    fn test_notification() {
325        let notif = JsonRpcRequest::notification("session/update", Some(json!({"delta": "hello"})));
326
327        assert!(notif.is_notification());
328        assert!(notif.id.is_none());
329    }
330
331    #[test]
332    fn test_id_types() {
333        let string_id = JsonRpcId::string("abc");
334        let number_id = JsonRpcId::number(123);
335
336        assert_eq!(format!("{}", string_id), "abc");
337        assert_eq!(format!("{}", number_id), "123");
338    }
339
340    #[test]
341    fn test_auth_required_error() {
342        use super::super::AuthMethod;
343
344        let auth_methods = vec![
345            AuthMethod::Agent {
346                id: "agent_auth".to_string(),
347                name: "Agent Auth".to_string(),
348                description: None,
349            },
350            AuthMethod::EnvVar {
351                id: "openai_key".to_string(),
352                name: "OpenAI Key".to_string(),
353                description: None,
354                var_name: "OPENAI_API_KEY".to_string(),
355                link: None,
356            },
357        ];
358
359        let error = JsonRpcError::auth_required(auth_methods);
360
361        assert_eq!(error.code, error_codes::AUTH_REQUIRED);
362        assert_eq!(error.message, "Authentication required");
363        assert!(error.data.is_some());
364
365        let data = error.data.unwrap();
366        assert!(data["authMethods"].is_array());
367        assert_eq!(data["authMethods"].as_array().unwrap().len(), 2);
368    }
369
370    #[test]
371    fn test_auth_required_error_serialization() {
372        use super::super::AuthMethod;
373
374        let auth_methods = vec![AuthMethod::EnvVar {
375            id: "test".to_string(),
376            name: "Test".to_string(),
377            description: None,
378            var_name: "TEST_VAR".to_string(),
379            link: Some("https://example.com".to_string()),
380        }];
381
382        let error = JsonRpcError::auth_required(auth_methods);
383        let json = serde_json::to_value(&error).unwrap();
384
385        assert_eq!(json["code"], -32001);
386        assert_eq!(json["message"], "Authentication required");
387        assert_eq!(json["data"]["authMethods"][0]["type"], "env_var");
388        assert_eq!(json["data"]["authMethods"][0]["id"], "test");
389    }
390
391    #[test]
392    fn test_acp_error_helpers() {
393        let err_perm = JsonRpcError::permission_denied("Not allowed");
394        assert_eq!(err_perm.code, error_codes::PERMISSION_DENIED);
395
396        let err_session = JsonRpcError::session_not_found("sess-123");
397        assert_eq!(err_session.code, error_codes::SESSION_NOT_FOUND);
398        assert!(err_session.message.contains("sess-123"));
399
400        let err_rate = JsonRpcError::rate_limited("Too many requests");
401        assert_eq!(err_rate.code, error_codes::RATE_LIMITED);
402
403        let err_resource = JsonRpcError::resource_not_found("file.txt");
404        assert_eq!(err_resource.code, error_codes::RESOURCE_NOT_FOUND);
405        assert!(err_resource.message.contains("file.txt"));
406    }
407}