Skip to main content

spn_client/
protocol.rs

1//! Protocol types for daemon communication.
2//!
3//! The protocol uses length-prefixed JSON over Unix sockets.
4//!
5//! ## Wire Format
6//!
7//! ```text
8//! [4 bytes: message length (big-endian u32)][JSON payload]
9//! ```
10//!
11//! ## Example
12//!
13//! Request:
14//! ```json
15//! { "cmd": "GET_SECRET", "provider": "anthropic" }
16//! ```
17//!
18//! Response:
19//! ```json
20//! { "ok": true, "secret": "sk-ant-..." }
21//! ```
22
23use serde::{Deserialize, Serialize};
24use spn_core::{LoadConfig, ModelInfo, RunningModel};
25
26/// Request sent to the daemon.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28#[serde(tag = "cmd")]
29pub enum Request {
30    /// Ping the daemon to check it's alive.
31    #[serde(rename = "PING")]
32    Ping,
33
34    /// Get a secret for a provider.
35    #[serde(rename = "GET_SECRET")]
36    GetSecret { provider: String },
37
38    /// Check if a secret exists.
39    #[serde(rename = "HAS_SECRET")]
40    HasSecret { provider: String },
41
42    /// List all available providers.
43    #[serde(rename = "LIST_PROVIDERS")]
44    ListProviders,
45
46    // ==================== Model Commands ====================
47    /// List all installed models.
48    #[serde(rename = "MODEL_LIST")]
49    ModelList,
50
51    /// Pull/download a model.
52    #[serde(rename = "MODEL_PULL")]
53    ModelPull { name: String },
54
55    /// Load a model into memory.
56    #[serde(rename = "MODEL_LOAD")]
57    ModelLoad {
58        name: String,
59        #[serde(default)]
60        config: Option<LoadConfig>,
61    },
62
63    /// Unload a model from memory.
64    #[serde(rename = "MODEL_UNLOAD")]
65    ModelUnload { name: String },
66
67    /// Get status of running models.
68    #[serde(rename = "MODEL_STATUS")]
69    ModelStatus,
70
71    /// Delete a model.
72    #[serde(rename = "MODEL_DELETE")]
73    ModelDelete { name: String },
74}
75
76/// Response from the daemon.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78#[serde(untagged)]
79pub enum Response {
80    /// Successful ping response.
81    Pong { version: String },
82
83    /// Secret value response.
84    ///
85    /// # Security Note
86    ///
87    /// The secret is transmitted as plain JSON over the Unix socket. This is secure because:
88    /// - Unix socket requires peer credential verification (same UID only)
89    /// - Socket permissions are 0600 (owner-only)
90    /// - Connection is local-only (no network exposure)
91    Secret { value: String },
92
93    /// Secret existence check response.
94    Exists { exists: bool },
95
96    /// Provider list response.
97    Providers { providers: Vec<String> },
98
99    // ==================== Model Responses ====================
100    /// List of installed models.
101    Models { models: Vec<ModelInfo> },
102
103    /// List of currently running/loaded models.
104    RunningModels { running: Vec<RunningModel> },
105
106    /// Generic success response.
107    Success { success: bool },
108
109    /// Error response.
110    Error { message: String },
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_request_serialization() {
119        let ping = Request::Ping;
120        let json = serde_json::to_string(&ping).unwrap();
121        assert_eq!(json, r#"{"cmd":"PING"}"#);
122
123        let get_secret = Request::GetSecret {
124            provider: "anthropic".to_string(),
125        };
126        let json = serde_json::to_string(&get_secret).unwrap();
127        assert_eq!(json, r#"{"cmd":"GET_SECRET","provider":"anthropic"}"#);
128
129        let has_secret = Request::HasSecret {
130            provider: "openai".to_string(),
131        };
132        let json = serde_json::to_string(&has_secret).unwrap();
133        assert_eq!(json, r#"{"cmd":"HAS_SECRET","provider":"openai"}"#);
134
135        let list = Request::ListProviders;
136        let json = serde_json::to_string(&list).unwrap();
137        assert_eq!(json, r#"{"cmd":"LIST_PROVIDERS"}"#);
138    }
139
140    #[test]
141    fn test_response_deserialization() {
142        // Pong
143        let json = r#"{"version":"0.9.0"}"#;
144        let response: Response = serde_json::from_str(json).unwrap();
145        assert!(matches!(response, Response::Pong { version } if version == "0.9.0"));
146
147        // Secret
148        let json = r#"{"value":"sk-test-123"}"#;
149        let response: Response = serde_json::from_str(json).unwrap();
150        assert!(matches!(response, Response::Secret { value } if value == "sk-test-123"));
151
152        // Exists
153        let json = r#"{"exists":true}"#;
154        let response: Response = serde_json::from_str(json).unwrap();
155        assert!(matches!(response, Response::Exists { exists } if exists));
156
157        // Providers
158        let json = r#"{"providers":["anthropic","openai"]}"#;
159        let response: Response = serde_json::from_str(json).unwrap();
160        assert!(
161            matches!(response, Response::Providers { providers } if providers == vec!["anthropic", "openai"])
162        );
163
164        // Error
165        let json = r#"{"message":"Not found"}"#;
166        let response: Response = serde_json::from_str(json).unwrap();
167        assert!(matches!(response, Response::Error { message } if message == "Not found"));
168    }
169}