1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::policy::{Capabilities, SandboxPolicy};
10
11#[non_exhaustive]
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct AgentConfig {
18 pub agent_id: String,
20 pub name: String,
22 #[serde(default)]
24 pub policy: SandboxPolicy,
25 #[serde(default)]
27 pub capabilities: Capabilities,
28 #[serde(default)]
30 pub workspace: Option<WorkspaceConfig>,
31 #[serde(default)]
33 pub lifecycle: LifecycleConfig,
34 #[serde(default)]
36 pub env: std::collections::HashMap<String, String>,
37 pub image: Option<String>,
39}
40
41impl AgentConfig {
42 pub fn new(agent_id: impl Into<String>, name: impl Into<String>) -> Self {
44 Self {
45 agent_id: agent_id.into(),
46 name: name.into(),
47 policy: SandboxPolicy::default(),
48 capabilities: Capabilities::default(),
49 workspace: None,
50 lifecycle: LifecycleConfig::default(),
51 env: std::collections::HashMap::new(),
52 image: None,
53 }
54 }
55
56 pub fn with_policy(mut self, policy: SandboxPolicy) -> Self {
58 self.policy = policy;
59 self
60 }
61
62 pub fn with_capabilities(mut self, capabilities: Capabilities) -> Self {
64 self.capabilities = capabilities;
65 self
66 }
67
68 pub fn with_workspace(mut self, workspace: WorkspaceConfig) -> Self {
70 self.workspace = Some(workspace);
71 self
72 }
73
74 pub fn with_lifecycle(mut self, lifecycle: LifecycleConfig) -> Self {
76 self.lifecycle = lifecycle;
77 self
78 }
79
80 pub fn with_image(mut self, image: impl Into<String>) -> Self {
82 self.image = Some(image.into());
83 self
84 }
85}
86
87#[non_exhaustive]
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct WorkspaceConfig {
93 pub host_path: Option<String>,
95 #[serde(default = "default_mount_point")]
97 pub mount_point: String,
98 #[serde(default)]
100 pub read_only: bool,
101}
102
103impl WorkspaceConfig {
104 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn with_host_path(mut self, path: impl Into<String>) -> Self {
111 self.host_path = Some(path.into());
112 self
113 }
114
115 pub fn with_mount_point(mut self, mount_point: impl Into<String>) -> Self {
117 self.mount_point = mount_point.into();
118 self
119 }
120
121 pub fn read_only(mut self) -> Self {
123 self.read_only = true;
124 self
125 }
126}
127
128impl Default for WorkspaceConfig {
129 fn default() -> Self {
130 Self {
131 host_path: None,
132 mount_point: default_mount_point(),
133 read_only: false,
134 }
135 }
136}
137
138fn default_mount_point() -> String {
139 "/workspace".into()
140}
141
142#[non_exhaustive]
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct LifecycleConfig {
148 #[serde(default = "default_max_idle_ms")]
150 pub max_idle_ms: u64,
151 pub max_lifetime_ms: Option<u64>,
153 #[serde(default)]
155 pub restart_on_crash: bool,
156 #[serde(default = "default_max_restarts")]
158 pub max_restarts: u32,
159}
160
161impl LifecycleConfig {
162 pub fn new(max_idle_ms: u64) -> Self {
164 Self {
165 max_idle_ms,
166 ..Self::default()
167 }
168 }
169
170 pub fn with_restart(mut self, max_restarts: u32) -> Self {
172 self.restart_on_crash = true;
173 self.max_restarts = max_restarts;
174 self
175 }
176
177 pub fn with_max_lifetime(mut self, ms: u64) -> Self {
179 self.max_lifetime_ms = Some(ms);
180 self
181 }
182}
183
184impl Default for LifecycleConfig {
185 fn default() -> Self {
186 Self {
187 max_idle_ms: default_max_idle_ms(),
188 max_lifetime_ms: None,
189 restart_on_crash: false,
190 max_restarts: default_max_restarts(),
191 }
192 }
193}
194
195fn default_max_idle_ms() -> u64 {
196 3_600_000
197}
198
199fn default_max_restarts() -> u32 {
200 3
201}
202
203#[non_exhaustive]
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct AgentInfo {
209 pub agent_id: String,
211 pub name: String,
213 pub status: AgentStatus,
215 pub container_id: Option<String>,
217 pub created_at: DateTime<Utc>,
219 pub last_activity: DateTime<Utc>,
221 pub execution_count: u64,
223 pub workspace_path: Option<String>,
225}
226
227impl AgentInfo {
228 pub fn new(agent_id: impl Into<String>, name: impl Into<String>, status: AgentStatus) -> Self {
230 let now = Utc::now();
231 Self {
232 agent_id: agent_id.into(),
233 name: name.into(),
234 status,
235 container_id: None,
236 created_at: now,
237 last_activity: now,
238 execution_count: 0,
239 workspace_path: None,
240 }
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246#[serde(rename_all = "snake_case")]
247#[non_exhaustive]
248pub enum AgentStatus {
249 Idle,
251 Running,
253 Starting,
255 Stopping,
257 Failed,
259 Terminated,
261}
262
263impl std::fmt::Display for AgentStatus {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 match self {
266 Self::Idle => write!(f, "idle"),
267 Self::Running => write!(f, "running"),
268 Self::Starting => write!(f, "starting"),
269 Self::Stopping => write!(f, "stopping"),
270 Self::Failed => write!(f, "failed"),
271 Self::Terminated => write!(f, "terminated"),
272 }
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn test_lifecycle_defaults() {
282 let lc = LifecycleConfig::default();
283 assert_eq!(lc.max_idle_ms, 3_600_000);
284 assert_eq!(lc.max_restarts, 3);
285 assert!(!lc.restart_on_crash);
286 assert!(lc.max_lifetime_ms.is_none());
287 }
288
289 #[test]
290 fn test_agent_status_display() {
291 assert_eq!(AgentStatus::Idle.to_string(), "idle");
292 assert_eq!(AgentStatus::Running.to_string(), "running");
293 }
294
295 #[test]
296 fn test_agent_config_serde() {
297 let json = r#"{
298 "agent_id": "test-agent",
299 "name": "Test Agent",
300 "policy": "container",
301 "capabilities": {},
302 "lifecycle": {},
303 "env": {},
304 "image": null
305 }"#;
306 let config: AgentConfig = serde_json::from_str(json).unwrap();
307 assert_eq!(config.agent_id, "test-agent");
308 assert_eq!(config.policy, SandboxPolicy::Container);
309 }
310
311 #[test]
312 fn test_agent_config_builder() {
313 let config = AgentConfig::new("my-agent", "My Agent").with_policy(SandboxPolicy::Container);
314 assert_eq!(config.agent_id, "my-agent");
315 assert_eq!(config.policy, SandboxPolicy::Container);
316 }
317
318 #[test]
319 fn test_agent_config_new_defaults() {
320 let config = AgentConfig::new("test", "Test");
321 assert_eq!(config.agent_id, "test");
322 assert_eq!(config.policy, SandboxPolicy::WasmOnly);
323 assert!(config.workspace.is_none());
324 assert!(config.image.is_none());
325 }
326
327 #[test]
328 fn test_agent_config_full_builder() {
329 let config = AgentConfig::new("a1", "Agent One")
330 .with_policy(SandboxPolicy::Container)
331 .with_capabilities(Capabilities::default())
332 .with_workspace(WorkspaceConfig::new().with_host_path("/data").read_only())
333 .with_lifecycle(LifecycleConfig::new(60000).with_restart(5))
334 .with_image("alpine:latest");
335 assert_eq!(config.policy, SandboxPolicy::Container);
336 assert!(config.workspace.as_ref().unwrap().read_only);
337 assert!(config.lifecycle.restart_on_crash);
338 assert_eq!(config.lifecycle.max_restarts, 5);
339 }
340
341 #[test]
342 fn test_workspace_config_defaults() {
343 let ws = WorkspaceConfig::default();
344 assert!(ws.host_path.is_none());
345 assert_eq!(ws.mount_point, "/workspace");
346 assert!(!ws.read_only);
347 }
348
349 #[test]
350 fn test_workspace_config_builder() {
351 let ws = WorkspaceConfig::new()
352 .with_mount_point("/custom")
353 .read_only();
354 assert_eq!(ws.mount_point, "/custom");
355 assert!(ws.read_only);
356 }
357
358 #[test]
359 fn test_agent_info_new() {
360 let info = AgentInfo::new("agent-1", "My Agent", AgentStatus::Idle);
361 assert_eq!(info.agent_id, "agent-1");
362 assert_eq!(info.status, AgentStatus::Idle);
363 assert!(info.container_id.is_none());
364 assert_eq!(info.execution_count, 0);
365 }
366
367 #[test]
368 fn test_lifecycle_config_builder() {
369 let lc = LifecycleConfig::new(30000)
370 .with_restart(10)
371 .with_max_lifetime(600000);
372 assert_eq!(lc.max_idle_ms, 30000);
373 assert!(lc.restart_on_crash);
374 assert_eq!(lc.max_lifetime_ms, Some(600000));
375 }
376
377 #[test]
378 fn test_agent_status_all_variants() {
379 for v in [
380 AgentStatus::Idle,
381 AgentStatus::Running,
382 AgentStatus::Starting,
383 AgentStatus::Stopping,
384 AgentStatus::Failed,
385 AgentStatus::Terminated,
386 ] {
387 assert!(!v.to_string().is_empty());
388 }
389 }
390}