1#![allow(dead_code)] use crate::error::{AgenticResult, AgenticWardenError};
8use chrono::{DateTime, Duration, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11use std::path::PathBuf;
12use std::time::SystemTime;
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
16pub enum AiType {
17 #[serde(rename = "codex")]
18 Codex,
19 #[serde(rename = "claude")]
20 Claude,
21 #[serde(rename = "gemini")]
22 Gemini,
23 #[serde(rename = "all")]
24 All,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct TaskId(u64);
30
31impl TaskId {
32 #[allow(clippy::new_without_default)]
33 pub fn new() -> Self {
34 Self(
35 SystemTime::now()
36 .duration_since(SystemTime::UNIX_EPOCH)
37 .unwrap_or_else(|_| {
38 use std::time::Duration;
41 Duration::from_nanos(std::process::id() as u64)
42 })
43 .as_nanos() as u64,
44 )
45 }
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct ProcessTreeInfo {
51 #[serde(default)]
53 pub process_chain: Vec<u32>,
54 #[serde(default)]
56 pub root_parent_pid: Option<u32>,
57 #[serde(default, alias = "process_tree_depth")]
59 pub depth: usize,
60 #[serde(default)]
62 pub has_ai_cli_root: bool,
63 #[serde(default)]
65 pub ai_cli_type: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub ai_cli_process: Option<AiCliProcessInfo>,
69}
70
71impl ProcessTreeInfo {
72 pub fn new(process_chain: Vec<u32>) -> Self {
74 let depth = process_chain.len();
75 let root_parent_pid = process_chain.last().copied();
76 Self {
77 process_chain,
78 root_parent_pid,
79 depth,
80 has_ai_cli_root: false,
81 ai_cli_type: None,
82 ai_cli_process: None,
83 }
84 }
85
86 pub fn with_ai_cli_process(mut self, ai_cli_process: Option<AiCliProcessInfo>) -> Self {
88 if let Some(info) = ai_cli_process {
89 self.root_parent_pid = Some(info.pid);
90 self.ai_cli_type = Some(info.ai_type.clone());
91 self.has_ai_cli_root = true;
92 self.ai_cli_process = Some(info);
93 }
94 self
95 }
96
97 pub fn get_ai_cli_root(&self) -> Option<u32> {
99 if self.has_ai_cli_root {
100 self.ai_cli_process
101 .as_ref()
102 .map(|info| info.pid)
103 .or(self.root_parent_pid)
104 } else {
105 self.root_parent_pid
106 }
107 }
108
109 pub fn contains_process(&self, pid: u32) -> bool {
111 self.process_chain.contains(&pid)
112 }
113
114 pub fn get_chain_to_ai_cli_root(&self) -> Vec<u32> {
116 if let Some(root_pid) = self.get_ai_cli_root() {
117 if let Some(pos) = self.process_chain.iter().position(|pid| *pid == root_pid) {
118 return self.process_chain[..=pos].to_vec();
119 }
120 }
121 self.process_chain.clone()
122 }
123
124 pub fn validate(&self) -> AgenticResult<()> {
126 if self.process_chain.is_empty() {
127 return Err(validation_error(
128 "process_tree.process_chain",
129 "process chain cannot be empty",
130 ));
131 }
132
133 if self.depth != self.process_chain.len() {
134 return Err(validation_error(
135 "process_tree.depth",
136 format!(
137 "depth ({}) must equal process_chain length ({})",
138 self.depth,
139 self.process_chain.len()
140 ),
141 ));
142 }
143
144 let mut seen = HashSet::new();
145 for pid in &self.process_chain {
146 if !seen.insert(pid) {
147 return Err(validation_error(
148 "process_tree.process_chain",
149 format!("duplicate pid {} detected", pid),
150 ));
151 }
152 }
153
154 if self.has_ai_cli_root {
155 if self.ai_cli_type.is_none() {
156 return Err(validation_error(
157 "process_tree.ai_cli_type",
158 "ai_cli_type required when has_ai_cli_root=true",
159 ));
160 }
161 if self.ai_cli_process.is_none() {
162 return Err(validation_error(
163 "process_tree.ai_cli_process",
164 "ai_cli_process required when has_ai_cli_root=true",
165 ));
166 }
167 }
168
169 Ok(())
170 }
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
175pub struct AiCliProcessInfo {
176 pub pid: u32,
178 pub ai_type: String,
180 #[serde(default)]
182 pub process_name: String,
183 #[serde(default)]
185 pub command_line: String,
186 pub is_npm_package: bool,
188 pub detected_at: DateTime<Utc>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub executable_path: Option<PathBuf>,
193}
194
195impl AiCliProcessInfo {
196 pub fn new(pid: u32, ai_type: impl Into<String>) -> Self {
198 Self {
199 pid,
200 ai_type: ai_type.into(),
201 process_name: String::new(),
202 command_line: String::new(),
203 is_npm_package: false,
204 detected_at: Utc::now(),
205 executable_path: None,
206 }
207 }
208
209 pub fn with_process_name(mut self, name: impl Into<String>) -> Self {
210 self.process_name = name.into();
211 self
212 }
213
214 pub fn with_command_line(mut self, command_line: impl Into<String>) -> Self {
215 self.command_line = command_line.into();
216 self
217 }
218
219 pub fn with_is_npm_package(mut self, is_npm_package: bool) -> Self {
220 self.is_npm_package = is_npm_package;
221 self
222 }
223
224 pub fn with_executable_path(mut self, path: Option<PathBuf>) -> Self {
225 self.executable_path = path;
226 self
227 }
228
229 pub fn is_valid_ai_cli(&self) -> bool {
231 self.pid > 0 && !self.ai_type.is_empty() && !self.process_name.is_empty()
232 }
233
234 pub fn get_description(&self) -> String {
236 let mut description = format!("{} (pid {})", self.ai_type, self.pid);
237 if !self.process_name.is_empty() {
238 description.push_str(&format!(" via {}", self.process_name));
239 }
240 if self.is_npm_package {
241 description.push_str(" [npm]");
242 }
243 description
244 }
245
246 pub fn validate(&self) -> AgenticResult<()> {
248 if self.pid == 0 {
249 return Err(validation_error(
250 "ai_cli_process.pid",
251 "pid must be a non-zero value",
252 ));
253 }
254 if self.ai_type.trim().is_empty() {
255 return Err(validation_error(
256 "ai_cli_process.ai_type",
257 "ai_type cannot be empty",
258 ));
259 }
260 if self.process_name.trim().is_empty() {
261 return Err(validation_error(
262 "ai_cli_process.process_name",
263 "process_name cannot be empty",
264 ));
265 }
266 Ok(())
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ProcessInfo {
273 pub pid: u32,
275 pub ppid: u32,
277 pub name: String,
279 pub path: Option<PathBuf>,
281 pub command_line: String,
283 pub start_time: SystemTime,
285 pub user_id: Option<u32>,
287 pub is_root: bool,
289 pub depth: u32,
291}
292
293#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct Provider {
296 pub name: String,
298 pub description: String,
300 pub compatible_with: Vec<AiType>,
302 pub env: HashMap<String, String>,
304 #[serde(default)]
306 pub builtin: bool,
307 #[serde(default = "default_now")]
309 pub created_at: DateTime<Utc>,
310 #[serde(default = "default_now")]
312 pub updated_at: DateTime<Utc>,
313 #[serde(default)]
315 pub metadata: HashMap<String, serde_json::Value>,
316}
317
318fn default_now() -> DateTime<Utc> {
319 Utc::now()
320}
321
322#[derive(Debug, Serialize, Deserialize)]
324pub struct ProviderConfig {
325 #[serde(rename = "$schema")]
327 pub schema: String,
328 pub providers: HashMap<String, Provider>,
330 pub default_provider: String,
332 #[serde(default = "default_config_version")]
334 pub version: String,
335 #[serde(default = "default_format_version")]
337 pub format_version: u32,
338 #[serde(default)]
340 pub settings: ProviderSettings,
341}
342
343fn default_config_version() -> String {
344 "1.0.0".to_string()
345}
346
347fn default_format_version() -> u32 {
348 1
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ProviderSettings {
354 #[serde(default = "default_true")]
356 pub auto_refresh: bool,
357 #[serde(default = "default_health_check_interval")]
359 pub health_check_interval: u64,
360 #[serde(default = "default_connection_timeout")]
362 pub connection_timeout: u64,
363 #[serde(default = "default_max_retries")]
365 pub max_retries: u32,
366 #[serde(default = "default_true")]
368 pub validate_on_startup: bool,
369}
370
371fn default_true() -> bool {
372 true
373}
374
375fn default_health_check_interval() -> u64 {
376 300
377}
378
379fn default_connection_timeout() -> u64 {
380 30
381}
382
383fn default_max_retries() -> u32 {
384 3
385}
386
387impl Default for ProviderSettings {
388 fn default() -> Self {
389 Self {
390 auto_refresh: true,
391 health_check_interval: 300,
392 connection_timeout: 30,
393 max_retries: 3,
394 validate_on_startup: true,
395 }
396 }
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct TokenInfo {
402 pub access_token: String,
404 pub refresh_token: String,
406 pub token_type: String,
408 pub expires_in: u64,
410 #[serde(default = "calculate_expiry")]
412 pub expiry_time: DateTime<Utc>,
413 #[serde(default = "default_now")]
415 pub obtained_at: DateTime<Utc>,
416 pub scope: Option<String>,
418}
419
420fn calculate_expiry() -> DateTime<Utc> {
421 Utc::now() + Duration::seconds(3600) }
423
424#[derive(Debug, Serialize, Deserialize)]
426pub struct InstanceRegistry {
427 pub instance_id: usize,
429 pub start_time: SystemTime,
431 pub main_pid: u32,
433 pub username: String,
435 pub hostname: String,
437 pub working_directory: PathBuf,
439 pub version: String,
441 pub last_heartbeat: SystemTime,
443 pub task_count: usize,
445 pub active_task_count: usize,
447}
448
449fn validation_error(field: &str, message: impl Into<String>) -> AgenticWardenError {
450 AgenticWardenError::Validation {
451 message: message.into(),
452 field: Some(field.to_string()),
453 value: None,
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 #[test]
462 fn process_tree_info_roundtrip_includes_ai_cli_metadata() {
463 let ai_info = AiCliProcessInfo::new(42, "claude")
464 .with_process_name("claude-cli")
465 .with_command_line("claude ask --debug")
466 .with_is_npm_package(false);
467 let tree = ProcessTreeInfo::new(vec![4242, 1337, 42]).with_ai_cli_process(Some(ai_info));
468 tree.validate().expect("tree should be valid");
469
470 let serialized = serde_json::to_string(&tree).expect("serialize tree");
471 let restored: ProcessTreeInfo =
472 serde_json::from_str(&serialized).expect("deserialize tree");
473
474 assert_eq!(restored.depth, 3);
475 assert!(restored.has_ai_cli_root);
476 assert_eq!(restored.get_ai_cli_root(), Some(42));
477 assert!(restored.ai_cli_process.is_some());
478 }
479
480 #[test]
481 fn process_tree_info_accepts_legacy_depth_field() {
482 let json = r#"{
483 "process_chain": [100, 50],
484 "process_tree_depth": 2,
485 "root_parent_pid": 50
486 }"#;
487
488 let tree: ProcessTreeInfo =
489 serde_json::from_str(json).expect("legacy depth should deserialize");
490 assert_eq!(tree.depth, 2);
491 tree.validate().expect("tree should remain valid");
492 }
493
494 #[test]
495 fn ai_cli_process_requires_non_empty_name() {
496 let ai = AiCliProcessInfo::new(1, "codex").with_process_name("codex-cli");
497 assert!(ai.validate().is_ok());
498
499 let invalid = AiCliProcessInfo::new(0, "").with_process_name("");
500 assert!(invalid.validate().is_err());
501 }
502}