1use std::collections::{HashMap, HashSet};
13
14use chrono::{DateTime, Duration, Utc};
15use serde::{Deserialize, Serialize};
16
17use super::types::{AgentContext, AgentContextError, AgentContextResult, ContextUpdate};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
21#[serde(rename_all = "camelCase")]
22pub enum SandboxState {
23 #[default]
25 Active,
26 Suspended,
28 Terminated,
30}
31
32impl SandboxState {
33 pub fn can_transition_to(&self, target: SandboxState) -> bool {
35 match (self, target) {
36 (SandboxState::Active, SandboxState::Suspended) => true,
38 (SandboxState::Active, SandboxState::Terminated) => true,
39 (SandboxState::Suspended, SandboxState::Active) => true,
41 (SandboxState::Suspended, SandboxState::Terminated) => true,
42 (SandboxState::Terminated, _) => false,
44 (s1, s2) if *s1 == s2 => true,
46 _ => false,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Default, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct ResourceUsage {
55 pub tokens_used: usize,
57 pub files_accessed: usize,
59 pub tool_results_count: usize,
61 pub tool_calls_made: usize,
63}
64
65impl ResourceUsage {
66 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn add_tokens(&mut self, count: usize) {
73 self.tokens_used += count;
74 }
75
76 pub fn add_file_access(&mut self) {
78 self.files_accessed += 1;
79 }
80
81 pub fn add_tool_result(&mut self) {
83 self.tool_results_count += 1;
84 }
85
86 pub fn add_tool_call(&mut self) {
88 self.tool_calls_made += 1;
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct SandboxRestrictions {
96 pub max_tokens: usize,
98 pub max_files: usize,
100 pub max_tool_results: usize,
102 pub allowed_tools: Option<HashSet<String>>,
104 pub denied_tools: Option<HashSet<String>>,
106}
107
108impl Default for SandboxRestrictions {
109 fn default() -> Self {
110 Self {
111 max_tokens: 100_000,
112 max_files: 50,
113 max_tool_results: 100,
114 allowed_tools: None,
115 denied_tools: None,
116 }
117 }
118}
119
120impl SandboxRestrictions {
121 pub fn new(max_tokens: usize, max_files: usize, max_tool_results: usize) -> Self {
123 Self {
124 max_tokens,
125 max_files,
126 max_tool_results,
127 allowed_tools: None,
128 denied_tools: None,
129 }
130 }
131
132 pub fn with_allowed_tools(
134 mut self,
135 tools: impl IntoIterator<Item = impl Into<String>>,
136 ) -> Self {
137 self.allowed_tools = Some(tools.into_iter().map(|t| t.into()).collect());
138 self
139 }
140
141 pub fn with_denied_tools(mut self, tools: impl IntoIterator<Item = impl Into<String>>) -> Self {
143 self.denied_tools = Some(tools.into_iter().map(|t| t.into()).collect());
144 self
145 }
146
147 pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
149 if let Some(allowed) = &self.allowed_tools {
151 if !allowed.contains(tool_name) {
152 return false;
153 }
154 }
155
156 if let Some(denied) = &self.denied_tools {
158 if denied.contains(tool_name) {
159 return false;
160 }
161 }
162
163 true
164 }
165
166 pub fn check_limits(&self, usage: &ResourceUsage) -> Option<ResourceLimitViolation> {
168 if usage.tokens_used > self.max_tokens {
169 return Some(ResourceLimitViolation::TokensExceeded {
170 used: usage.tokens_used,
171 limit: self.max_tokens,
172 });
173 }
174 if usage.files_accessed > self.max_files {
175 return Some(ResourceLimitViolation::FilesExceeded {
176 used: usage.files_accessed,
177 limit: self.max_files,
178 });
179 }
180 if usage.tool_results_count > self.max_tool_results {
181 return Some(ResourceLimitViolation::ToolResultsExceeded {
182 used: usage.tool_results_count,
183 limit: self.max_tool_results,
184 });
185 }
186 None
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub enum ResourceLimitViolation {
193 TokensExceeded { used: usize, limit: usize },
195 FilesExceeded { used: usize, limit: usize },
197 ToolResultsExceeded { used: usize, limit: usize },
199}
200
201impl std::fmt::Display for ResourceLimitViolation {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 match self {
204 ResourceLimitViolation::TokensExceeded { used, limit } => {
205 write!(f, "Token limit exceeded: {} used, {} allowed", used, limit)
206 }
207 ResourceLimitViolation::FilesExceeded { used, limit } => {
208 write!(
209 f,
210 "File limit exceeded: {} accessed, {} allowed",
211 used, limit
212 )
213 }
214 ResourceLimitViolation::ToolResultsExceeded { used, limit } => {
215 write!(
216 f,
217 "Tool results limit exceeded: {} stored, {} allowed",
218 used, limit
219 )
220 }
221 }
222 }
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "camelCase")]
228pub struct SandboxedContext {
229 pub sandbox_id: String,
231 pub agent_id: String,
233 pub context: AgentContext,
235 pub restrictions: SandboxRestrictions,
237 pub state: SandboxState,
239 pub created_at: DateTime<Utc>,
241 pub expires_at: Option<DateTime<Utc>>,
243 pub resources: ResourceUsage,
245 pub suspension_reason: Option<String>,
247}
248
249impl SandboxedContext {
250 pub fn new(
252 context: AgentContext,
253 agent_id: impl Into<String>,
254 restrictions: Option<SandboxRestrictions>,
255 ) -> Self {
256 Self {
257 sandbox_id: uuid::Uuid::new_v4().to_string(),
258 agent_id: agent_id.into(),
259 context,
260 restrictions: restrictions.unwrap_or_default(),
261 state: SandboxState::Active,
262 created_at: Utc::now(),
263 expires_at: None,
264 resources: ResourceUsage::new(),
265 suspension_reason: None,
266 }
267 }
268
269 pub fn with_expiration(mut self, expires_at: DateTime<Utc>) -> Self {
271 self.expires_at = Some(expires_at);
272 self
273 }
274
275 pub fn with_ttl(mut self, ttl: Duration) -> Self {
277 self.expires_at = Some(Utc::now() + ttl);
278 self
279 }
280
281 pub fn is_expired(&self) -> bool {
283 if let Some(expires_at) = self.expires_at {
284 Utc::now() > expires_at
285 } else {
286 false
287 }
288 }
289
290 pub fn is_active(&self) -> bool {
292 self.state == SandboxState::Active && !self.is_expired()
293 }
294
295 pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
297 self.restrictions.is_tool_allowed(tool_name)
298 }
299
300 pub fn check_limits(&self) -> Option<ResourceLimitViolation> {
302 self.restrictions.check_limits(&self.resources)
303 }
304
305 pub fn record_tokens(&mut self, count: usize) -> AgentContextResult<()> {
307 self.resources.add_tokens(count);
308 self.check_and_suspend_if_exceeded()
309 }
310
311 pub fn record_file_access(&mut self) -> AgentContextResult<()> {
313 self.resources.add_file_access();
314 self.check_and_suspend_if_exceeded()
315 }
316
317 pub fn record_tool_result(&mut self) -> AgentContextResult<()> {
319 self.resources.add_tool_result();
320 self.check_and_suspend_if_exceeded()
321 }
322
323 fn check_and_suspend_if_exceeded(&mut self) -> AgentContextResult<()> {
325 if let Some(violation) = self.check_limits() {
326 self.state = SandboxState::Suspended;
327 self.suspension_reason = Some(violation.to_string());
328 return Err(AgentContextError::ResourceLimitExceeded(
329 violation.to_string(),
330 ));
331 }
332 Ok(())
333 }
334}
335
336#[derive(Debug, Default)]
344pub struct ContextIsolation {
345 sandboxes: HashMap<String, SandboxedContext>,
347 agent_sandboxes: HashMap<String, String>,
349}
350
351impl ContextIsolation {
352 pub fn new() -> Self {
354 Self {
355 sandboxes: HashMap::new(),
356 agent_sandboxes: HashMap::new(),
357 }
358 }
359
360 pub fn create_sandbox(
362 &mut self,
363 context: AgentContext,
364 agent_id: Option<String>,
365 restrictions: Option<SandboxRestrictions>,
366 ) -> SandboxedContext {
367 let agent_id = agent_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
368 let sandbox = SandboxedContext::new(context, agent_id.clone(), restrictions);
369
370 let sandbox_id = sandbox.sandbox_id.clone();
371 self.sandboxes.insert(sandbox_id.clone(), sandbox.clone());
372 self.agent_sandboxes.insert(agent_id, sandbox_id);
373
374 sandbox
375 }
376
377 pub fn create_sandbox_with_ttl(
379 &mut self,
380 context: AgentContext,
381 agent_id: Option<String>,
382 restrictions: Option<SandboxRestrictions>,
383 ttl: Duration,
384 ) -> SandboxedContext {
385 let agent_id = agent_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
386 let sandbox = SandboxedContext::new(context, agent_id.clone(), restrictions).with_ttl(ttl);
387
388 let sandbox_id = sandbox.sandbox_id.clone();
389 self.sandboxes.insert(sandbox_id.clone(), sandbox.clone());
390 self.agent_sandboxes.insert(agent_id, sandbox_id);
391
392 sandbox
393 }
394
395 pub fn get_sandbox(&self, sandbox_id: &str) -> Option<&SandboxedContext> {
397 self.sandboxes.get(sandbox_id)
398 }
399
400 pub fn get_sandbox_mut(&mut self, sandbox_id: &str) -> Option<&mut SandboxedContext> {
402 self.sandboxes.get_mut(sandbox_id)
403 }
404
405 pub fn get_isolated_context(&self, agent_id: &str) -> Option<&AgentContext> {
407 self.agent_sandboxes
408 .get(agent_id)
409 .and_then(|sandbox_id| self.sandboxes.get(sandbox_id))
410 .map(|sandbox| &sandbox.context)
411 }
412
413 pub fn get_sandbox_by_agent(&self, agent_id: &str) -> Option<&SandboxedContext> {
415 self.agent_sandboxes
416 .get(agent_id)
417 .and_then(|sandbox_id| self.sandboxes.get(sandbox_id))
418 }
419
420 pub fn get_sandbox_by_agent_mut(&mut self, agent_id: &str) -> Option<&mut SandboxedContext> {
422 if let Some(sandbox_id) = self.agent_sandboxes.get(agent_id).cloned() {
423 self.sandboxes.get_mut(&sandbox_id)
424 } else {
425 None
426 }
427 }
428
429 pub fn update_sandbox(
431 &mut self,
432 sandbox_id: &str,
433 updates: ContextUpdate,
434 ) -> AgentContextResult<()> {
435 let sandbox = self
436 .sandboxes
437 .get_mut(sandbox_id)
438 .ok_or_else(|| AgentContextError::NotFound(sandbox_id.to_string()))?;
439
440 if sandbox.state != SandboxState::Active {
442 return Err(AgentContextError::InvalidStateTransition(format!(
443 "Cannot update sandbox in {:?} state",
444 sandbox.state
445 )));
446 }
447
448 if sandbox.is_expired() {
450 sandbox.state = SandboxState::Terminated;
451 return Err(AgentContextError::InvalidStateTransition(
452 "Sandbox has expired".to_string(),
453 ));
454 }
455
456 let context = &mut sandbox.context;
458
459 if let Some(messages) = updates.add_messages {
460 context.conversation_history.extend(messages);
461 }
462
463 if let Some(files) = updates.add_files {
464 for file in files {
465 sandbox.resources.add_file_access();
466 context.file_context.push(file);
467 }
468 }
469
470 if let Some(results) = updates.add_tool_results {
471 for result in results {
472 sandbox.resources.add_tool_result();
473 context.tool_results.push(result);
474 }
475 }
476
477 if let Some(env) = updates.set_environment {
478 context.environment.extend(env);
479 }
480
481 if let Some(prompt) = updates.set_system_prompt {
482 context.system_prompt = Some(prompt);
483 }
484
485 if let Some(dir) = updates.set_working_directory {
486 context.working_directory = dir;
487 }
488
489 if let Some(tags) = updates.add_tags {
490 for tag in tags {
491 context.metadata.add_tag(tag);
492 }
493 }
494
495 if let Some(custom) = updates.set_custom_metadata {
496 for (key, value) in custom {
497 context.metadata.set_custom(key, value);
498 }
499 }
500
501 context.metadata.touch();
502
503 sandbox.check_and_suspend_if_exceeded()?;
505
506 Ok(())
507 }
508
509 pub fn is_tool_allowed(&self, sandbox_id: &str, tool_name: &str) -> bool {
511 self.sandboxes
512 .get(sandbox_id)
513 .map(|s| s.is_tool_allowed(tool_name))
514 .unwrap_or(false)
515 }
516
517 pub fn suspend(&mut self, sandbox_id: &str) -> AgentContextResult<()> {
519 let sandbox = self
520 .sandboxes
521 .get_mut(sandbox_id)
522 .ok_or_else(|| AgentContextError::NotFound(sandbox_id.to_string()))?;
523
524 if !sandbox.state.can_transition_to(SandboxState::Suspended) {
525 return Err(AgentContextError::InvalidStateTransition(format!(
526 "Cannot suspend sandbox in {:?} state",
527 sandbox.state
528 )));
529 }
530
531 sandbox.state = SandboxState::Suspended;
532 sandbox.suspension_reason = Some("Manually suspended".to_string());
533 Ok(())
534 }
535
536 pub fn resume(&mut self, sandbox_id: &str) -> AgentContextResult<()> {
538 let sandbox = self
539 .sandboxes
540 .get_mut(sandbox_id)
541 .ok_or_else(|| AgentContextError::NotFound(sandbox_id.to_string()))?;
542
543 if !sandbox.state.can_transition_to(SandboxState::Active) {
544 return Err(AgentContextError::InvalidStateTransition(format!(
545 "Cannot resume sandbox in {:?} state",
546 sandbox.state
547 )));
548 }
549
550 if sandbox.is_expired() {
552 sandbox.state = SandboxState::Terminated;
553 return Err(AgentContextError::InvalidStateTransition(
554 "Cannot resume expired sandbox".to_string(),
555 ));
556 }
557
558 sandbox.state = SandboxState::Active;
559 sandbox.suspension_reason = None;
560 Ok(())
561 }
562
563 pub fn terminate(&mut self, sandbox_id: &str) -> AgentContextResult<()> {
565 let sandbox = self
566 .sandboxes
567 .get_mut(sandbox_id)
568 .ok_or_else(|| AgentContextError::NotFound(sandbox_id.to_string()))?;
569
570 if !sandbox.state.can_transition_to(SandboxState::Terminated) {
571 return Err(AgentContextError::InvalidStateTransition(format!(
572 "Cannot terminate sandbox in {:?} state",
573 sandbox.state
574 )));
575 }
576
577 sandbox.state = SandboxState::Terminated;
578 Ok(())
579 }
580
581 pub fn cleanup(&mut self, sandbox_id: &str) {
583 if let Some(sandbox) = self.sandboxes.remove(sandbox_id) {
584 self.agent_sandboxes.remove(&sandbox.agent_id);
585 }
586 }
587
588 pub fn cleanup_expired(&mut self) -> usize {
591 let expired_ids: Vec<String> = self
592 .sandboxes
593 .iter()
594 .filter(|(_, sandbox)| sandbox.is_expired())
595 .map(|(id, _)| id.clone())
596 .collect();
597
598 let count = expired_ids.len();
599
600 for sandbox_id in expired_ids {
601 self.cleanup(&sandbox_id);
602 }
603
604 count
605 }
606
607 pub fn list_sandbox_ids(&self) -> Vec<String> {
609 self.sandboxes.keys().cloned().collect()
610 }
611
612 pub fn list_sandboxes_by_state(&self, state: SandboxState) -> Vec<&SandboxedContext> {
614 self.sandboxes
615 .values()
616 .filter(|s| s.state == state)
617 .collect()
618 }
619
620 pub fn sandbox_count(&self) -> usize {
622 self.sandboxes.len()
623 }
624
625 pub fn active_sandbox_count(&self) -> usize {
627 self.sandboxes.values().filter(|s| s.is_active()).count()
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634 use crate::agents::context::types::FileContext;
635
636 #[test]
637 fn test_sandbox_state_transitions() {
638 assert!(SandboxState::Active.can_transition_to(SandboxState::Suspended));
640 assert!(SandboxState::Active.can_transition_to(SandboxState::Terminated));
642 assert!(SandboxState::Suspended.can_transition_to(SandboxState::Active));
644 assert!(SandboxState::Suspended.can_transition_to(SandboxState::Terminated));
646 assert!(!SandboxState::Terminated.can_transition_to(SandboxState::Active));
648 assert!(!SandboxState::Terminated.can_transition_to(SandboxState::Suspended));
649 assert!(SandboxState::Active.can_transition_to(SandboxState::Active));
651 }
652
653 #[test]
654 fn test_resource_usage_tracking() {
655 let mut usage = ResourceUsage::new();
656 assert_eq!(usage.tokens_used, 0);
657 assert_eq!(usage.files_accessed, 0);
658
659 usage.add_tokens(100);
660 assert_eq!(usage.tokens_used, 100);
661
662 usage.add_file_access();
663 assert_eq!(usage.files_accessed, 1);
664
665 usage.add_tool_result();
666 assert_eq!(usage.tool_results_count, 1);
667
668 usage.add_tool_call();
669 assert_eq!(usage.tool_calls_made, 1);
670 }
671
672 #[test]
673 fn test_sandbox_restrictions_default() {
674 let restrictions = SandboxRestrictions::default();
675 assert_eq!(restrictions.max_tokens, 100_000);
676 assert_eq!(restrictions.max_files, 50);
677 assert_eq!(restrictions.max_tool_results, 100);
678 assert!(restrictions.allowed_tools.is_none());
679 assert!(restrictions.denied_tools.is_none());
680 }
681
682 #[test]
683 fn test_sandbox_restrictions_tool_allowed() {
684 let restrictions = SandboxRestrictions::default();
686 assert!(restrictions.is_tool_allowed("bash"));
687 assert!(restrictions.is_tool_allowed("read_file"));
688
689 let restrictions =
691 SandboxRestrictions::default().with_allowed_tools(vec!["bash", "read_file"]);
692 assert!(restrictions.is_tool_allowed("bash"));
693 assert!(restrictions.is_tool_allowed("read_file"));
694 assert!(!restrictions.is_tool_allowed("write_file"));
695
696 let restrictions = SandboxRestrictions::default().with_denied_tools(vec!["bash"]);
698 assert!(!restrictions.is_tool_allowed("bash"));
699 assert!(restrictions.is_tool_allowed("read_file"));
700
701 let restrictions = SandboxRestrictions::default()
703 .with_allowed_tools(vec!["bash", "read_file", "write_file"])
704 .with_denied_tools(vec!["write_file"]);
705 assert!(restrictions.is_tool_allowed("bash"));
706 assert!(restrictions.is_tool_allowed("read_file"));
707 assert!(!restrictions.is_tool_allowed("write_file")); assert!(!restrictions.is_tool_allowed("other")); }
710
711 #[test]
712 fn test_sandbox_restrictions_check_limits() {
713 let restrictions = SandboxRestrictions::new(100, 5, 10);
714
715 let usage = ResourceUsage {
717 tokens_used: 50,
718 files_accessed: 3,
719 tool_results_count: 5,
720 tool_calls_made: 0,
721 };
722 assert!(restrictions.check_limits(&usage).is_none());
723
724 let usage = ResourceUsage {
726 tokens_used: 150,
727 files_accessed: 3,
728 tool_results_count: 5,
729 tool_calls_made: 0,
730 };
731 assert!(matches!(
732 restrictions.check_limits(&usage),
733 Some(ResourceLimitViolation::TokensExceeded { .. })
734 ));
735
736 let usage = ResourceUsage {
738 tokens_used: 50,
739 files_accessed: 10,
740 tool_results_count: 5,
741 tool_calls_made: 0,
742 };
743 assert!(matches!(
744 restrictions.check_limits(&usage),
745 Some(ResourceLimitViolation::FilesExceeded { .. })
746 ));
747
748 let usage = ResourceUsage {
750 tokens_used: 50,
751 files_accessed: 3,
752 tool_results_count: 15,
753 tool_calls_made: 0,
754 };
755 assert!(matches!(
756 restrictions.check_limits(&usage),
757 Some(ResourceLimitViolation::ToolResultsExceeded { .. })
758 ));
759 }
760
761 #[test]
762 fn test_sandboxed_context_creation() {
763 let context = AgentContext::new();
764 let sandbox = SandboxedContext::new(context, "agent-1", None);
765
766 assert!(!sandbox.sandbox_id.is_empty());
767 assert_eq!(sandbox.agent_id, "agent-1");
768 assert_eq!(sandbox.state, SandboxState::Active);
769 assert!(sandbox.is_active());
770 assert!(!sandbox.is_expired());
771 }
772
773 #[test]
774 fn test_sandboxed_context_with_ttl() {
775 let context = AgentContext::new();
776 let sandbox = SandboxedContext::new(context, "agent-1", None).with_ttl(Duration::hours(1));
777
778 assert!(sandbox.expires_at.is_some());
779 assert!(!sandbox.is_expired());
780
781 let context = AgentContext::new();
783 let sandbox =
784 SandboxedContext::new(context, "agent-2", None).with_ttl(Duration::seconds(-1));
785
786 assert!(sandbox.is_expired());
787 assert!(!sandbox.is_active());
788 }
789
790 #[test]
791 fn test_sandboxed_context_record_resources() {
792 let context = AgentContext::new();
793 let restrictions = SandboxRestrictions::new(100, 5, 10);
794 let mut sandbox = SandboxedContext::new(context, "agent-1", Some(restrictions));
795
796 assert!(sandbox.record_tokens(50).is_ok());
798 assert_eq!(sandbox.resources.tokens_used, 50);
799
800 let result = sandbox.record_tokens(100);
802 assert!(result.is_err());
803 assert_eq!(sandbox.state, SandboxState::Suspended);
804 }
805
806 #[test]
807 fn test_context_isolation_create_sandbox() {
808 let mut isolation = ContextIsolation::new();
809 let context = AgentContext::new();
810
811 let sandbox = isolation.create_sandbox(context, Some("agent-1".to_string()), None);
812
813 assert!(!sandbox.sandbox_id.is_empty());
814 assert_eq!(sandbox.agent_id, "agent-1");
815 assert_eq!(isolation.sandbox_count(), 1);
816
817 assert!(isolation.get_sandbox(&sandbox.sandbox_id).is_some());
819
820 assert!(isolation.get_isolated_context("agent-1").is_some());
822 }
823
824 #[test]
825 fn test_context_isolation_suspend_resume() {
826 let mut isolation = ContextIsolation::new();
827 let context = AgentContext::new();
828 let sandbox = isolation.create_sandbox(context, Some("agent-1".to_string()), None);
829 let sandbox_id = sandbox.sandbox_id.clone();
830
831 assert!(isolation.suspend(&sandbox_id).is_ok());
833 assert_eq!(
834 isolation.get_sandbox(&sandbox_id).unwrap().state,
835 SandboxState::Suspended
836 );
837
838 assert!(isolation.resume(&sandbox_id).is_ok());
840 assert_eq!(
841 isolation.get_sandbox(&sandbox_id).unwrap().state,
842 SandboxState::Active
843 );
844
845 assert!(isolation.terminate(&sandbox_id).is_ok());
847 assert_eq!(
848 isolation.get_sandbox(&sandbox_id).unwrap().state,
849 SandboxState::Terminated
850 );
851
852 assert!(isolation.resume(&sandbox_id).is_err());
854 }
855
856 #[test]
857 fn test_context_isolation_cleanup() {
858 let mut isolation = ContextIsolation::new();
859
860 let context1 = AgentContext::new();
862 let sandbox1 = isolation.create_sandbox(context1, Some("agent-1".to_string()), None);
863 let sandbox1_id = sandbox1.sandbox_id.clone();
864
865 let context2 = AgentContext::new();
866 let _sandbox2 = isolation.create_sandbox(context2, Some("agent-2".to_string()), None);
867
868 assert_eq!(isolation.sandbox_count(), 2);
869
870 isolation.cleanup(&sandbox1_id);
872 assert_eq!(isolation.sandbox_count(), 1);
873 assert!(isolation.get_sandbox(&sandbox1_id).is_none());
874 assert!(isolation.get_isolated_context("agent-1").is_none());
875 }
876
877 #[test]
878 fn test_context_isolation_cleanup_expired() {
879 let mut isolation = ContextIsolation::new();
880
881 let context1 = AgentContext::new();
883 let _sandbox1 = isolation.create_sandbox_with_ttl(
884 context1,
885 Some("agent-1".to_string()),
886 None,
887 Duration::seconds(-1), );
889
890 let context2 = AgentContext::new();
892 let _sandbox2 = isolation.create_sandbox_with_ttl(
893 context2,
894 Some("agent-2".to_string()),
895 None,
896 Duration::hours(1),
897 );
898
899 assert_eq!(isolation.sandbox_count(), 2);
900
901 let cleaned = isolation.cleanup_expired();
903 assert_eq!(cleaned, 1);
904 assert_eq!(isolation.sandbox_count(), 1);
905 assert!(isolation.get_isolated_context("agent-1").is_none());
906 assert!(isolation.get_isolated_context("agent-2").is_some());
907 }
908
909 #[test]
910 fn test_context_isolation_is_tool_allowed() {
911 let mut isolation = ContextIsolation::new();
912 let context = AgentContext::new();
913 let restrictions =
914 SandboxRestrictions::default().with_allowed_tools(vec!["bash", "read_file"]);
915
916 let sandbox =
917 isolation.create_sandbox(context, Some("agent-1".to_string()), Some(restrictions));
918
919 assert!(isolation.is_tool_allowed(&sandbox.sandbox_id, "bash"));
920 assert!(isolation.is_tool_allowed(&sandbox.sandbox_id, "read_file"));
921 assert!(!isolation.is_tool_allowed(&sandbox.sandbox_id, "write_file"));
922 assert!(!isolation.is_tool_allowed("nonexistent", "bash"));
923 }
924
925 #[test]
926 fn test_context_isolation_update_sandbox() {
927 let mut isolation = ContextIsolation::new();
928 let context = AgentContext::new();
929 let sandbox = isolation.create_sandbox(context, Some("agent-1".to_string()), None);
930 let sandbox_id = sandbox.sandbox_id.clone();
931
932 let updates = ContextUpdate {
933 add_files: Some(vec![FileContext::new("/test.rs", "fn main() {}")]),
934 ..Default::default()
935 };
936
937 assert!(isolation.update_sandbox(&sandbox_id, updates).is_ok());
938
939 let sandbox = isolation.get_sandbox(&sandbox_id).unwrap();
940 assert_eq!(sandbox.context.file_context.len(), 1);
941 assert_eq!(sandbox.resources.files_accessed, 1);
942 }
943
944 #[test]
945 fn test_context_isolation_update_suspended_sandbox_fails() {
946 let mut isolation = ContextIsolation::new();
947 let context = AgentContext::new();
948 let sandbox = isolation.create_sandbox(context, Some("agent-1".to_string()), None);
949 let sandbox_id = sandbox.sandbox_id.clone();
950
951 isolation.suspend(&sandbox_id).unwrap();
953
954 let updates = ContextUpdate {
956 add_files: Some(vec![FileContext::new("/test.rs", "fn main() {}")]),
957 ..Default::default()
958 };
959
960 assert!(isolation.update_sandbox(&sandbox_id, updates).is_err());
961 }
962
963 #[test]
964 fn test_context_isolation_list_by_state() {
965 let mut isolation = ContextIsolation::new();
966
967 let context1 = AgentContext::new();
969 let sandbox1 = isolation.create_sandbox(context1, Some("agent-1".to_string()), None);
970
971 let context2 = AgentContext::new();
972 let sandbox2 = isolation.create_sandbox(context2, Some("agent-2".to_string()), None);
973 isolation.suspend(&sandbox2.sandbox_id).unwrap();
974
975 let context3 = AgentContext::new();
976 let sandbox3 = isolation.create_sandbox(context3, Some("agent-3".to_string()), None);
977 isolation.terminate(&sandbox3.sandbox_id).unwrap();
978
979 let active = isolation.list_sandboxes_by_state(SandboxState::Active);
981 assert_eq!(active.len(), 1);
982 assert_eq!(active[0].sandbox_id, sandbox1.sandbox_id);
983
984 let suspended = isolation.list_sandboxes_by_state(SandboxState::Suspended);
985 assert_eq!(suspended.len(), 1);
986
987 let terminated = isolation.list_sandboxes_by_state(SandboxState::Terminated);
988 assert_eq!(terminated.len(), 1);
989 }
990}