1use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20#[serde(tag = "command", rename_all = "snake_case")]
21pub enum DaemonRequest {
22 Health,
23 CreateTask {
24 title: String,
25 project_path: String,
26 model_id: String,
27 },
28 SessionMessages {
29 id: String,
30 },
31 #[serde(alias = "runtime_snapshot")]
32 Snapshot,
33 RuntimeDashboard,
34 RuntimeDiagnostics,
35 RuntimeHygienePreview,
36 RuntimeHygieneArchive,
37 RuntimeTaskDetail {
38 id: String,
39 },
40 RuntimeApprovalDetail {
41 id: String,
42 },
43 RuntimeCheckpointDetail {
44 id: String,
45 },
46 RuntimeTasks {
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 limit: Option<u64>,
49 },
50 RuntimeProcesses {
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 limit: Option<u64>,
53 },
54 RuntimeToolRuns {
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 limit: Option<u64>,
57 },
58 RuntimeCheckpoints {
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 limit: Option<u64>,
61 },
62 RuntimeApprovals,
63 RuntimePlugins,
64 Run {
65 prompt: String,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 project_path: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 model_id: Option<String>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 priority: Option<String>,
73 },
74 CancelTask {
75 id: String,
76 },
77 UpdateTask {
78 id: String,
79 status: String,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 final_report: Option<String>,
82 },
83 Logs {
84 id: String,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 tail_bytes: Option<u64>,
87 },
88 StopProcess {
89 id: String,
90 },
91 RestartProcess {
92 id: String,
93 },
94 OpenProcess {
95 id: String,
96 },
97 Ports,
98 RestoreCheckpoint {
99 id: String,
100 },
101 Approve {
102 id: String,
103 },
104 Deny {
105 id: String,
106 },
107 PluginPreview {
108 path: String,
109 },
110 PluginInstall {
111 path: String,
112 },
113 SetPluginEnabled {
114 id: String,
115 enabled: bool,
116 },
117 SetSafetyMode {
118 mode: String,
119 },
120 ModelInfo {
121 model: String,
122 },
123 Pair {
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 label: Option<String>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 ttl_days: Option<i64>,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
129 token_hash: Option<String>,
130 },
131 SubscribeTask {
135 task_id: String,
136 },
137}
138
139impl DaemonRequest {
140 pub fn requires_auth(&self) -> bool {
145 match self {
146 DaemonRequest::Health => false,
147 DaemonRequest::CreateTask { .. }
148 | DaemonRequest::Run { .. }
149 | DaemonRequest::CancelTask { .. }
150 | DaemonRequest::UpdateTask { .. }
151 | DaemonRequest::RestoreCheckpoint { .. }
152 | DaemonRequest::Approve { .. }
153 | DaemonRequest::Deny { .. }
154 | DaemonRequest::StopProcess { .. }
155 | DaemonRequest::RestartProcess { .. }
156 | DaemonRequest::OpenProcess { .. }
157 | DaemonRequest::PluginPreview { .. }
158 | DaemonRequest::PluginInstall { .. }
159 | DaemonRequest::SetPluginEnabled { .. }
160 | DaemonRequest::SetSafetyMode { .. }
161 | DaemonRequest::RuntimeHygieneArchive
162 | DaemonRequest::Pair { .. }
163 | DaemonRequest::Logs { .. }
164 | DaemonRequest::SessionMessages { .. }
165 | DaemonRequest::Snapshot
166 | DaemonRequest::RuntimeDashboard
167 | DaemonRequest::RuntimeDiagnostics
168 | DaemonRequest::RuntimeHygienePreview
169 | DaemonRequest::RuntimeTaskDetail { .. }
170 | DaemonRequest::RuntimeApprovalDetail { .. }
171 | DaemonRequest::RuntimeCheckpointDetail { .. }
172 | DaemonRequest::RuntimeTasks { .. }
173 | DaemonRequest::RuntimeProcesses { .. }
174 | DaemonRequest::RuntimeApprovals
175 | DaemonRequest::RuntimeToolRuns { .. }
176 | DaemonRequest::RuntimeCheckpoints { .. }
177 | DaemonRequest::RuntimePlugins
178 | DaemonRequest::ModelInfo { .. }
179 | DaemonRequest::SubscribeTask { .. } => true,
180 DaemonRequest::Ports => false,
183 }
184 }
185
186 pub fn to_wire(&self) -> serde_json::Value {
189 serde_json::to_value(self).expect("DaemonRequest serializes")
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
199 fn every_variant_round_trips_the_wire_shape() {
200 let cases: Vec<(DaemonRequest, &str)> = vec![
201 (DaemonRequest::Health, r#"{"command":"health"}"#),
202 (
203 DaemonRequest::CreateTask {
204 title: "t".into(),
205 project_path: "/p".into(),
206 model_id: "m".into(),
207 },
208 r#"{"command":"create_task","title":"t","project_path":"/p","model_id":"m"}"#,
209 ),
210 (
211 DaemonRequest::SessionMessages { id: "s1".into() },
212 r#"{"command":"session_messages","id":"s1"}"#,
213 ),
214 (DaemonRequest::Snapshot, r#"{"command":"snapshot"}"#),
215 (
216 DaemonRequest::RuntimeDashboard,
217 r#"{"command":"runtime_dashboard"}"#,
218 ),
219 (
220 DaemonRequest::RuntimeDiagnostics,
221 r#"{"command":"runtime_diagnostics"}"#,
222 ),
223 (
224 DaemonRequest::RuntimeHygienePreview,
225 r#"{"command":"runtime_hygiene_preview"}"#,
226 ),
227 (
228 DaemonRequest::RuntimeHygieneArchive,
229 r#"{"command":"runtime_hygiene_archive"}"#,
230 ),
231 (
232 DaemonRequest::RuntimeTaskDetail { id: "t1".into() },
233 r#"{"command":"runtime_task_detail","id":"t1"}"#,
234 ),
235 (
236 DaemonRequest::RuntimeApprovalDetail { id: "a1".into() },
237 r#"{"command":"runtime_approval_detail","id":"a1"}"#,
238 ),
239 (
240 DaemonRequest::RuntimeCheckpointDetail { id: "c1".into() },
241 r#"{"command":"runtime_checkpoint_detail","id":"c1"}"#,
242 ),
243 (
244 DaemonRequest::RuntimeTasks { limit: Some(50) },
245 r#"{"command":"runtime_tasks","limit":50}"#,
246 ),
247 (
248 DaemonRequest::RuntimeProcesses { limit: None },
249 r#"{"command":"runtime_processes"}"#,
250 ),
251 (
252 DaemonRequest::RuntimeToolRuns { limit: Some(100) },
253 r#"{"command":"runtime_tool_runs","limit":100}"#,
254 ),
255 (
256 DaemonRequest::RuntimeCheckpoints { limit: Some(50) },
257 r#"{"command":"runtime_checkpoints","limit":50}"#,
258 ),
259 (
260 DaemonRequest::RuntimeApprovals,
261 r#"{"command":"runtime_approvals"}"#,
262 ),
263 (
264 DaemonRequest::RuntimePlugins,
265 r#"{"command":"runtime_plugins"}"#,
266 ),
267 (
268 DaemonRequest::Run {
269 prompt: "p".into(),
270 project_path: Some(String::new()),
271 model_id: None,
272 priority: Some("high".into()),
273 },
274 r#"{"command":"run","prompt":"p","project_path":"","priority":"high"}"#,
275 ),
276 (
277 DaemonRequest::CancelTask { id: "t1".into() },
278 r#"{"command":"cancel_task","id":"t1"}"#,
279 ),
280 (
281 DaemonRequest::UpdateTask {
282 id: "t1".into(),
283 status: "completed".into(),
284 final_report: Some("done".into()),
285 },
286 r#"{"command":"update_task","id":"t1","status":"completed","final_report":"done"}"#,
287 ),
288 (
289 DaemonRequest::Logs {
290 id: "p1".into(),
291 tail_bytes: Some(4096),
292 },
293 r#"{"command":"logs","id":"p1","tail_bytes":4096}"#,
294 ),
295 (
296 DaemonRequest::StopProcess { id: "p1".into() },
297 r#"{"command":"stop_process","id":"p1"}"#,
298 ),
299 (
300 DaemonRequest::RestartProcess { id: "p1".into() },
301 r#"{"command":"restart_process","id":"p1"}"#,
302 ),
303 (
304 DaemonRequest::OpenProcess { id: "p1".into() },
305 r#"{"command":"open_process","id":"p1"}"#,
306 ),
307 (DaemonRequest::Ports, r#"{"command":"ports"}"#),
308 (
309 DaemonRequest::RestoreCheckpoint { id: "c1".into() },
310 r#"{"command":"restore_checkpoint","id":"c1"}"#,
311 ),
312 (
313 DaemonRequest::Approve { id: "a1".into() },
314 r#"{"command":"approve","id":"a1"}"#,
315 ),
316 (
317 DaemonRequest::Deny { id: "a1".into() },
318 r#"{"command":"deny","id":"a1"}"#,
319 ),
320 (
321 DaemonRequest::PluginPreview { path: "/pl".into() },
322 r#"{"command":"plugin_preview","path":"/pl"}"#,
323 ),
324 (
325 DaemonRequest::PluginInstall { path: "/pl".into() },
326 r#"{"command":"plugin_install","path":"/pl"}"#,
327 ),
328 (
329 DaemonRequest::SetPluginEnabled {
330 id: "pl1".into(),
331 enabled: true,
332 },
333 r#"{"command":"set_plugin_enabled","id":"pl1","enabled":true}"#,
334 ),
335 (
336 DaemonRequest::SetSafetyMode { mode: "ask".into() },
337 r#"{"command":"set_safety_mode","mode":"ask"}"#,
338 ),
339 (
340 DaemonRequest::ModelInfo { model: "m".into() },
341 r#"{"command":"model_info","model":"m"}"#,
342 ),
343 (
344 DaemonRequest::Pair {
345 label: Some("laptop".into()),
346 ttl_days: Some(30),
347 token_hash: None,
348 },
349 r#"{"command":"pair","label":"laptop","ttl_days":30}"#,
350 ),
351 (
352 DaemonRequest::SubscribeTask {
353 task_id: "t1".into(),
354 },
355 r#"{"command":"subscribe_task","task_id":"t1"}"#,
356 ),
357 ];
358 for (req, wire) in cases {
359 assert_eq!(serde_json::to_string(&req).unwrap(), wire, "{req:?}");
360 let back: DaemonRequest = serde_json::from_str(wire).unwrap();
361 assert_eq!(back, req, "{wire}");
362 }
363 }
364
365 #[test]
366 fn runtime_snapshot_alias_still_parses() {
367 let req: DaemonRequest = serde_json::from_str(r#"{"command":"runtime_snapshot"}"#).unwrap();
368 assert_eq!(req, DaemonRequest::Snapshot);
369 }
370
371 #[test]
372 fn requires_auth_matches_the_historical_matrix() {
373 assert!(!DaemonRequest::Health.requires_auth());
374 assert!(!DaemonRequest::Ports.requires_auth());
375 assert!(DaemonRequest::ModelInfo { model: "m".into() }.requires_auth());
376 for gated in [
377 DaemonRequest::Run {
378 prompt: "p".into(),
379 project_path: None,
380 model_id: None,
381 priority: None,
382 },
383 DaemonRequest::Snapshot,
384 DaemonRequest::SessionMessages { id: "s".into() },
385 DaemonRequest::Logs {
386 id: "p".into(),
387 tail_bytes: None,
388 },
389 DaemonRequest::Pair {
390 label: None,
391 ttl_days: None,
392 token_hash: None,
393 },
394 DaemonRequest::SubscribeTask {
395 task_id: "t".into(),
396 },
397 ] {
398 assert!(gated.requires_auth(), "{gated:?}");
399 }
400 }
401
402 #[test]
403 fn unknown_command_is_a_parse_error() {
404 assert!(serde_json::from_str::<DaemonRequest>(r#"{"command":"nope"}"#).is_err());
405 }
406}