1use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::KernelError;
12use crate::process::{Pid, ProcessTable};
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub struct ResourceLimits {
20 #[serde(default = "default_max_memory", alias = "maxMemoryBytes")]
22 pub max_memory_bytes: u64,
23
24 #[serde(default = "default_max_cpu", alias = "maxCpuTimeMs")]
26 pub max_cpu_time_ms: u64,
27
28 #[serde(default = "default_max_tool_calls", alias = "maxToolCalls")]
30 pub max_tool_calls: u64,
31
32 #[serde(default = "default_max_messages", alias = "maxMessages")]
34 pub max_messages: u64,
35
36 #[cfg(feature = "os-patterns")]
41 #[serde(default = "default_max_disk", alias = "maxDiskBytes")]
42 pub max_disk_bytes: u64,
43}
44
45fn default_max_memory() -> u64 {
46 256 * 1024 * 1024 }
48
49fn default_max_cpu() -> u64 {
50 300_000 }
52
53fn default_max_tool_calls() -> u64 {
54 1000
55}
56
57fn default_max_messages() -> u64 {
58 5000
59}
60
61#[cfg(feature = "os-patterns")]
62fn default_max_disk() -> u64 {
63 100 * 1024 * 1024 }
65
66impl Default for ResourceLimits {
67 fn default() -> Self {
68 Self {
69 max_memory_bytes: default_max_memory(),
70 max_cpu_time_ms: default_max_cpu(),
71 max_tool_calls: default_max_tool_calls(),
72 max_messages: default_max_messages(),
73 #[cfg(feature = "os-patterns")]
74 max_disk_bytes: default_max_disk(),
75 }
76 }
77}
78
79#[non_exhaustive]
81#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
82pub enum IpcScope {
83 #[default]
85 All,
86 ParentOnly,
88 Restricted(Vec<u64>),
90 Topic(Vec<String>),
92 None,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
100pub struct AgentCapabilities {
101 #[serde(default = "default_true", alias = "canSpawn")]
103 pub can_spawn: bool,
104
105 #[serde(default = "default_true", alias = "canIpc")]
107 pub can_ipc: bool,
108
109 #[serde(default = "default_true", alias = "canExecTools")]
111 pub can_exec_tools: bool,
112
113 #[serde(default, alias = "canNetwork")]
115 pub can_network: bool,
116
117 #[serde(default, alias = "ipcScope")]
119 pub ipc_scope: IpcScope,
120
121 #[serde(default, alias = "resourceLimits")]
123 pub resource_limits: ResourceLimits,
124}
125
126fn default_true() -> bool {
127 true
128}
129
130impl Default for AgentCapabilities {
131 fn default() -> Self {
132 Self {
133 can_spawn: true,
134 can_ipc: true,
135 can_exec_tools: true,
136 can_network: false,
137 ipc_scope: IpcScope::default(),
138 resource_limits: ResourceLimits::default(),
139 }
140 }
141}
142
143impl AgentCapabilities {
144 pub fn browser_default() -> Self {
150 Self {
151 can_spawn: false,
152 can_ipc: true,
153 can_exec_tools: true,
154 can_network: false,
155 ipc_scope: IpcScope::Restricted(vec![]),
156 resource_limits: ResourceLimits {
157 max_memory_bytes: 64 * 1024 * 1024, max_cpu_time_ms: 60_000, max_tool_calls: 200,
160 max_messages: 500,
161 #[cfg(feature = "os-patterns")]
162 max_disk_bytes: 10 * 1024 * 1024, },
164 }
165 }
166
167 pub fn can_message(&self, target_pid: u64) -> bool {
169 if !self.can_ipc {
170 return false;
171 }
172 match &self.ipc_scope {
173 IpcScope::All => true,
174 IpcScope::ParentOnly => false, IpcScope::Restricted(pids) => pids.contains(&target_pid),
176 IpcScope::Topic(_) => false, IpcScope::None => false,
178 }
179 }
180
181 pub fn can_topic(&self, topic: &str) -> bool {
183 if !self.can_ipc {
184 return false;
185 }
186 match &self.ipc_scope {
187 IpcScope::All => true,
188 IpcScope::Topic(topics) => topics.iter().any(|t| t == topic),
189 IpcScope::ParentOnly | IpcScope::Restricted(_) => true, IpcScope::None => false,
191 }
192 }
193
194 pub fn within_limits(&self, memory: u64, cpu: u64, tools: u64, msgs: u64) -> bool {
196 memory <= self.resource_limits.max_memory_bytes
197 && cpu <= self.resource_limits.max_cpu_time_ms
198 && tools <= self.resource_limits.max_tool_calls
199 && msgs <= self.resource_limits.max_messages
200 }
201}
202
203#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
208pub struct SandboxPolicy {
209 #[serde(default, alias = "allowShell")]
211 pub allow_shell: bool,
212
213 #[serde(default, alias = "allowNetwork")]
215 pub allow_network: bool,
216
217 #[serde(default, alias = "allowedPaths")]
219 pub allowed_paths: Vec<String>,
220
221 #[serde(default, alias = "deniedPaths")]
223 pub denied_paths: Vec<String>,
224}
225
226#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
231pub struct ToolPermissions {
232 #[serde(default, alias = "tools")]
234 pub allow: Vec<String>,
235
236 #[serde(default, alias = "denyTools")]
238 pub deny: Vec<String>,
239
240 #[serde(default, alias = "serviceAccess")]
242 pub service_access: Vec<String>,
243}
244
245#[non_exhaustive]
249#[derive(Debug, Clone)]
250pub enum ResourceType {
251 Memory(u64),
253 CpuTime(u64),
255 ConcurrentTools(u32),
257 Messages(u64),
259}
260
261pub struct CapabilityChecker {
269 process_table: Arc<ProcessTable>,
270}
271
272impl CapabilityChecker {
273 pub fn new(process_table: Arc<ProcessTable>) -> Self {
275 Self { process_table }
276 }
277
278 pub fn check_tool_access(
293 &self,
294 pid: Pid,
295 tool_name: &str,
296 tool_permissions: Option<&ToolPermissions>,
297 sandbox: Option<&SandboxPolicy>,
298 ) -> Result<(), KernelError> {
299 let entry = self
300 .process_table
301 .get(pid)
302 .ok_or(KernelError::ProcessNotFound { pid })?;
303
304 if !entry.capabilities.can_exec_tools {
306 return Err(KernelError::CapabilityDenied {
307 pid,
308 action: format!("execute tool '{tool_name}'"),
309 reason: "agent does not have can_exec_tools capability".into(),
310 });
311 }
312
313 if let Some(perms) = tool_permissions {
315 if perms.deny.iter().any(|d| d == tool_name) {
316 return Err(KernelError::CapabilityDenied {
317 pid,
318 action: format!("execute tool '{tool_name}'"),
319 reason: "tool is in the deny list".into(),
320 });
321 }
322
323 if !perms.allow.is_empty() && !perms.allow.iter().any(|a| a == tool_name) {
325 return Err(KernelError::CapabilityDenied {
326 pid,
327 action: format!("execute tool '{tool_name}'"),
328 reason: "tool is not in the allow list".into(),
329 });
330 }
331 }
332
333 if let Some(sb) = sandbox
335 && is_shell_tool(tool_name)
336 && !sb.allow_shell
337 {
338 return Err(KernelError::CapabilityDenied {
339 pid,
340 action: format!("execute shell tool '{tool_name}'"),
341 reason: "sandbox policy does not allow shell execution".into(),
342 });
343 }
344
345 Ok(())
346 }
347
348 pub fn check_ipc_target(&self, from_pid: Pid, to_pid: Pid) -> Result<(), KernelError> {
358 let entry = self
359 .process_table
360 .get(from_pid)
361 .ok_or(KernelError::ProcessNotFound { pid: from_pid })?;
362
363 if !entry.capabilities.can_ipc {
364 return Err(KernelError::CapabilityDenied {
365 pid: from_pid,
366 action: format!("send IPC message to PID {to_pid}"),
367 reason: "agent does not have IPC capability".into(),
368 });
369 }
370
371 if !entry.capabilities.can_message(to_pid) {
372 return Err(KernelError::CapabilityDenied {
373 pid: from_pid,
374 action: format!("send IPC message to PID {to_pid}"),
375 reason: format!(
376 "target PID {to_pid} is outside IPC scope {:?}",
377 entry.capabilities.ipc_scope
378 ),
379 });
380 }
381
382 Ok(())
383 }
384
385 pub fn check_ipc_topic(&self, pid: Pid, topic: &str) -> Result<(), KernelError> {
392 let entry = self
393 .process_table
394 .get(pid)
395 .ok_or(KernelError::ProcessNotFound { pid })?;
396
397 if !entry.capabilities.can_topic(topic) {
398 return Err(KernelError::CapabilityDenied {
399 pid,
400 action: format!("access topic '{topic}'"),
401 reason: format!(
402 "topic '{topic}' is outside IPC scope {:?}",
403 entry.capabilities.ipc_scope
404 ),
405 });
406 }
407
408 Ok(())
409 }
410
411 pub fn check_service_access(
421 &self,
422 pid: Pid,
423 service_name: &str,
424 tool_permissions: Option<&ToolPermissions>,
425 ) -> Result<(), KernelError> {
426 let _entry = self
428 .process_table
429 .get(pid)
430 .ok_or(KernelError::ProcessNotFound { pid })?;
431
432 if let Some(perms) = tool_permissions
433 && !perms.service_access.is_empty()
434 && !perms.service_access.iter().any(|s| s == service_name)
435 {
436 return Err(KernelError::CapabilityDenied {
437 pid,
438 action: format!("access service '{service_name}'"),
439 reason: "service is not in the agent's service access list".into(),
440 });
441 }
442
443 Ok(())
444 }
445
446 pub fn check_resource_limit(
453 &self,
454 pid: Pid,
455 resource: &ResourceType,
456 ) -> Result<(), KernelError> {
457 let entry = self
458 .process_table
459 .get(pid)
460 .ok_or(KernelError::ProcessNotFound { pid })?;
461
462 let limits = &entry.capabilities.resource_limits;
463
464 match resource {
465 ResourceType::Memory(bytes) => {
466 if *bytes > limits.max_memory_bytes {
467 return Err(KernelError::ResourceLimitExceeded {
468 pid,
469 resource: "memory".into(),
470 current: *bytes,
471 limit: limits.max_memory_bytes,
472 });
473 }
474 }
475 ResourceType::CpuTime(ms) => {
476 if *ms > limits.max_cpu_time_ms {
477 return Err(KernelError::ResourceLimitExceeded {
478 pid,
479 resource: "cpu_time".into(),
480 current: *ms,
481 limit: limits.max_cpu_time_ms,
482 });
483 }
484 }
485 ResourceType::ConcurrentTools(count) => {
486 if u64::from(*count) > limits.max_tool_calls {
487 return Err(KernelError::ResourceLimitExceeded {
488 pid,
489 resource: "concurrent_tools".into(),
490 current: u64::from(*count),
491 limit: limits.max_tool_calls,
492 });
493 }
494 }
495 ResourceType::Messages(count) => {
496 if *count > limits.max_messages {
497 return Err(KernelError::ResourceLimitExceeded {
498 pid,
499 resource: "messages".into(),
500 current: *count,
501 limit: limits.max_messages,
502 });
503 }
504 }
505 }
506
507 Ok(())
508 }
509
510 pub fn process_table(&self) -> &Arc<ProcessTable> {
512 &self.process_table
513 }
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
523pub struct CapabilityElevationRequest {
524 pub pid: u64,
526 pub current: AgentCapabilities,
528 pub requested: AgentCapabilities,
530 pub reason: String,
532}
533
534#[non_exhaustive]
536#[derive(Debug, Clone)]
537pub enum ElevationResult {
538 Granted {
540 new_capabilities: AgentCapabilities,
541 },
542 Denied {
544 reason: String,
545 },
546}
547
548impl AgentCapabilities {
549 pub fn request_elevation(
552 current: &AgentCapabilities,
553 requested: &AgentCapabilities,
554 platform: &str,
555 ) -> CapabilityElevationRequest {
556 CapabilityElevationRequest {
557 pid: 0, current: current.clone(),
559 requested: requested.clone(),
560 reason: format!("capability elevation for {platform} agent"),
561 }
562 }
563
564 pub fn needs_elevation(platform: &str, requested: &AgentCapabilities) -> bool {
570 platform == "browser"
571 && (requested.can_spawn
572 || requested.can_network
573 || !matches!(requested.ipc_scope, IpcScope::Restricted(_)))
574 }
575}
576
577fn is_shell_tool(tool_name: &str) -> bool {
579 matches!(
580 tool_name,
581 "shell_exec" | "exec_shell" | "bash" | "command" | "run_command"
582 )
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
590 fn default_capabilities() {
591 let caps = AgentCapabilities::default();
592 assert!(caps.can_spawn);
593 assert!(caps.can_ipc);
594 assert!(caps.can_exec_tools);
595 assert!(!caps.can_network);
596 assert_eq!(caps.ipc_scope, IpcScope::All);
597 }
598
599 #[test]
600 fn default_resource_limits() {
601 let limits = ResourceLimits::default();
602 assert_eq!(limits.max_memory_bytes, 256 * 1024 * 1024);
603 assert_eq!(limits.max_cpu_time_ms, 300_000);
604 assert_eq!(limits.max_tool_calls, 1000);
605 assert_eq!(limits.max_messages, 5000);
606 }
607
608 #[test]
609 fn can_message_all_scope() {
610 let caps = AgentCapabilities::default();
611 assert!(caps.can_message(1));
612 assert!(caps.can_message(999));
613 }
614
615 #[test]
616 fn can_message_restricted_scope() {
617 let caps = AgentCapabilities {
618 ipc_scope: IpcScope::Restricted(vec![1, 2, 3]),
619 ..Default::default()
620 };
621 assert!(caps.can_message(1));
622 assert!(caps.can_message(2));
623 assert!(!caps.can_message(4));
624 }
625
626 #[test]
627 fn can_message_none_scope() {
628 let caps = AgentCapabilities {
629 ipc_scope: IpcScope::None,
630 ..Default::default()
631 };
632 assert!(!caps.can_message(1));
633 }
634
635 #[test]
636 fn can_message_topic_scope_blocks_direct() {
637 let caps = AgentCapabilities {
638 ipc_scope: IpcScope::Topic(vec!["build".into(), "deploy".into()]),
639 ..Default::default()
640 };
641 assert!(!caps.can_message(1));
643 assert!(!caps.can_message(999));
644 }
645
646 #[test]
647 fn can_topic_with_topic_scope() {
648 let caps = AgentCapabilities {
649 ipc_scope: IpcScope::Topic(vec!["build".into(), "deploy".into()]),
650 ..Default::default()
651 };
652 assert!(caps.can_topic("build"));
653 assert!(caps.can_topic("deploy"));
654 assert!(!caps.can_topic("admin"));
655 }
656
657 #[test]
658 fn can_topic_with_all_scope() {
659 let caps = AgentCapabilities::default(); assert!(caps.can_topic("anything"));
661 }
662
663 #[test]
664 fn can_topic_with_none_scope() {
665 let caps = AgentCapabilities {
666 ipc_scope: IpcScope::None,
667 ..Default::default()
668 };
669 assert!(!caps.can_topic("build"));
670 }
671
672 #[test]
673 fn can_message_ipc_disabled() {
674 let caps = AgentCapabilities {
675 can_ipc: false,
676 ..Default::default()
677 };
678 assert!(!caps.can_message(1));
679 }
680
681 #[test]
682 fn within_limits_ok() {
683 let caps = AgentCapabilities::default();
684 assert!(caps.within_limits(1000, 1000, 10, 10));
685 }
686
687 #[test]
688 fn within_limits_exceeded() {
689 let caps = AgentCapabilities {
690 resource_limits: ResourceLimits {
691 max_memory_bytes: 100,
692 max_cpu_time_ms: 100,
693 max_tool_calls: 5,
694 max_messages: 5,
695 ..Default::default()
696 },
697 ..Default::default()
698 };
699 assert!(!caps.within_limits(200, 50, 3, 3)); assert!(!caps.within_limits(50, 200, 3, 3)); assert!(!caps.within_limits(50, 50, 10, 3)); assert!(!caps.within_limits(50, 50, 3, 10)); }
704
705 #[test]
706 fn serde_roundtrip_capabilities() {
707 let caps = AgentCapabilities {
708 can_spawn: false,
709 can_ipc: true,
710 can_exec_tools: false,
711 can_network: true,
712 ipc_scope: IpcScope::Restricted(vec![1, 2]),
713 resource_limits: ResourceLimits {
714 max_memory_bytes: 1024,
715 max_cpu_time_ms: 500,
716 max_tool_calls: 10,
717 max_messages: 20,
718 ..Default::default()
719 },
720 };
721 let json = serde_json::to_string(&caps).unwrap();
722 let restored: AgentCapabilities = serde_json::from_str(&json).unwrap();
723 assert_eq!(restored, caps);
724 }
725
726 #[test]
727 fn deserialize_empty_capabilities() {
728 let caps: AgentCapabilities = serde_json::from_str("{}").unwrap();
729 assert!(caps.can_spawn);
730 assert!(caps.can_ipc);
731 assert!(caps.can_exec_tools);
732 assert!(!caps.can_network);
733 }
734
735 #[test]
738 fn sandbox_policy_default() {
739 let sb = SandboxPolicy::default();
740 assert!(!sb.allow_shell);
741 assert!(!sb.allow_network);
742 assert!(sb.allowed_paths.is_empty());
743 assert!(sb.denied_paths.is_empty());
744 }
745
746 #[test]
747 fn sandbox_policy_serde_roundtrip() {
748 let sb = SandboxPolicy {
749 allow_shell: true,
750 allow_network: false,
751 allowed_paths: vec!["/workspace".into()],
752 denied_paths: vec!["/etc".into(), "/root".into()],
753 };
754 let json = serde_json::to_string(&sb).unwrap();
755 let restored: SandboxPolicy = serde_json::from_str(&json).unwrap();
756 assert_eq!(restored, sb);
757 }
758
759 #[test]
762 fn tool_permissions_default() {
763 let perms = ToolPermissions::default();
764 assert!(perms.allow.is_empty());
765 assert!(perms.deny.is_empty());
766 assert!(perms.service_access.is_empty());
767 }
768
769 #[test]
770 fn tool_permissions_serde_roundtrip() {
771 let perms = ToolPermissions {
772 allow: vec!["read_file".into(), "write_file".into()],
773 deny: vec!["shell_exec".into()],
774 service_access: vec!["memory".into()],
775 };
776 let json = serde_json::to_string(&perms).unwrap();
777 let restored: ToolPermissions = serde_json::from_str(&json).unwrap();
778 assert_eq!(restored, perms);
779 }
780
781 use crate::process::{ProcessEntry, ProcessState, ResourceUsage};
784 use tokio_util::sync::CancellationToken;
785
786 fn make_checker_with_entry(caps: AgentCapabilities) -> (CapabilityChecker, Pid) {
787 let table = Arc::new(ProcessTable::new(16));
788 let entry = ProcessEntry {
789 pid: 0,
790 agent_id: "test-agent".to_owned(),
791 state: ProcessState::Running,
792 capabilities: caps,
793 resource_usage: ResourceUsage::default(),
794 cancel_token: CancellationToken::new(),
795 parent_pid: None,
796 };
797 let pid = table.insert(entry).unwrap();
798 (CapabilityChecker::new(table), pid)
799 }
800
801 #[test]
802 fn checker_tool_access_allowed_by_default() {
803 let (checker, pid) = make_checker_with_entry(AgentCapabilities::default());
804 assert!(
805 checker
806 .check_tool_access(pid, "read_file", None, None)
807 .is_ok()
808 );
809 }
810
811 #[test]
812 fn checker_tool_access_denied_no_exec_tools() {
813 let caps = AgentCapabilities {
814 can_exec_tools: false,
815 ..Default::default()
816 };
817 let (checker, pid) = make_checker_with_entry(caps);
818 let result = checker.check_tool_access(pid, "read_file", None, None);
819 assert!(result.is_err());
820 }
821
822 #[test]
823 fn checker_tool_deny_list_blocks() {
824 let (checker, pid) = make_checker_with_entry(AgentCapabilities::default());
825 let perms = ToolPermissions {
826 deny: vec!["shell_exec".into()],
827 ..Default::default()
828 };
829 let result = checker.check_tool_access(pid, "shell_exec", Some(&perms), None);
830 assert!(result.is_err());
831 }
832
833 #[test]
834 fn checker_tool_deny_overrides_allow() {
835 let (checker, pid) = make_checker_with_entry(AgentCapabilities::default());
836 let perms = ToolPermissions {
837 allow: vec!["shell_exec".into()],
838 deny: vec!["shell_exec".into()],
839 ..Default::default()
840 };
841 let result = checker.check_tool_access(pid, "shell_exec", Some(&perms), None);
842 assert!(result.is_err());
843 }
844
845 #[test]
846 fn checker_tool_allow_list_restricts() {
847 let (checker, pid) = make_checker_with_entry(AgentCapabilities::default());
848 let perms = ToolPermissions {
849 allow: vec!["read_file".into(), "write_file".into()],
850 ..Default::default()
851 };
852 assert!(
853 checker
854 .check_tool_access(pid, "read_file", Some(&perms), None)
855 .is_ok()
856 );
857 assert!(
858 checker
859 .check_tool_access(pid, "web_search", Some(&perms), None)
860 .is_err()
861 );
862 }
863
864 #[test]
865 fn checker_sandbox_blocks_shell() {
866 let (checker, pid) = make_checker_with_entry(AgentCapabilities::default());
867 let sb = SandboxPolicy {
868 allow_shell: false,
869 ..Default::default()
870 };
871 let result = checker.check_tool_access(pid, "shell_exec", None, Some(&sb));
872 assert!(result.is_err());
873 }
874
875 #[test]
876 fn checker_sandbox_allows_shell() {
877 let (checker, pid) = make_checker_with_entry(AgentCapabilities::default());
878 let sb = SandboxPolicy {
879 allow_shell: true,
880 ..Default::default()
881 };
882 assert!(
883 checker
884 .check_tool_access(pid, "shell_exec", None, Some(&sb))
885 .is_ok()
886 );
887 }
888
889 #[test]
890 fn checker_ipc_allowed() {
891 let table = Arc::new(ProcessTable::new(16));
892 let entry1 = ProcessEntry {
893 pid: 0,
894 agent_id: "sender".to_owned(),
895 state: ProcessState::Running,
896 capabilities: AgentCapabilities::default(), resource_usage: ResourceUsage::default(),
898 cancel_token: CancellationToken::new(),
899 parent_pid: None,
900 };
901 let entry2 = ProcessEntry {
902 pid: 0,
903 agent_id: "receiver".to_owned(),
904 state: ProcessState::Running,
905 capabilities: AgentCapabilities::default(),
906 resource_usage: ResourceUsage::default(),
907 cancel_token: CancellationToken::new(),
908 parent_pid: None,
909 };
910 let pid1 = table.insert(entry1).unwrap();
911 let pid2 = table.insert(entry2).unwrap();
912
913 let checker = CapabilityChecker::new(table);
914 assert!(checker.check_ipc_target(pid1, pid2).is_ok());
915 }
916
917 #[test]
918 fn checker_ipc_denied_no_ipc() {
919 let caps = AgentCapabilities {
920 can_ipc: false,
921 ..Default::default()
922 };
923 let (checker, pid) = make_checker_with_entry(caps);
924 let result = checker.check_ipc_target(pid, 999);
925 assert!(result.is_err());
926 }
927
928 #[test]
929 fn checker_ipc_denied_restricted_scope() {
930 let caps = AgentCapabilities {
931 ipc_scope: IpcScope::Restricted(vec![5, 10]),
932 ..Default::default()
933 };
934 let (checker, pid) = make_checker_with_entry(caps);
935 assert!(checker.check_ipc_target(pid, 5).is_ok());
936 assert!(checker.check_ipc_target(pid, 10).is_ok());
937 assert!(checker.check_ipc_target(pid, 15).is_err());
938 }
939
940 #[test]
941 fn checker_service_access_allowed_empty_list() {
942 let (checker, pid) = make_checker_with_entry(AgentCapabilities::default());
943 let perms = ToolPermissions::default();
945 assert!(
946 checker
947 .check_service_access(pid, "memory", Some(&perms))
948 .is_ok()
949 );
950 }
951
952 #[test]
953 fn checker_service_access_restricted() {
954 let (checker, pid) = make_checker_with_entry(AgentCapabilities::default());
955 let perms = ToolPermissions {
956 service_access: vec!["memory".into(), "cron".into()],
957 ..Default::default()
958 };
959 assert!(
960 checker
961 .check_service_access(pid, "memory", Some(&perms))
962 .is_ok()
963 );
964 assert!(
965 checker
966 .check_service_access(pid, "cron", Some(&perms))
967 .is_ok()
968 );
969 assert!(
970 checker
971 .check_service_access(pid, "network", Some(&perms))
972 .is_err()
973 );
974 }
975
976 #[test]
977 fn checker_resource_limit_memory_ok() {
978 let (checker, pid) = make_checker_with_entry(AgentCapabilities::default());
979 assert!(
980 checker
981 .check_resource_limit(pid, &ResourceType::Memory(1024))
982 .is_ok()
983 );
984 }
985
986 #[test]
987 fn checker_resource_limit_memory_exceeded() {
988 let caps = AgentCapabilities {
989 resource_limits: ResourceLimits {
990 max_memory_bytes: 100,
991 ..Default::default()
992 },
993 ..Default::default()
994 };
995 let (checker, pid) = make_checker_with_entry(caps);
996 let result = checker.check_resource_limit(pid, &ResourceType::Memory(200));
997 assert!(result.is_err());
998 }
999
1000 #[test]
1001 fn checker_resource_limit_cpu_exceeded() {
1002 let caps = AgentCapabilities {
1003 resource_limits: ResourceLimits {
1004 max_cpu_time_ms: 100,
1005 ..Default::default()
1006 },
1007 ..Default::default()
1008 };
1009 let (checker, pid) = make_checker_with_entry(caps);
1010 let result = checker.check_resource_limit(pid, &ResourceType::CpuTime(200));
1011 assert!(result.is_err());
1012 }
1013
1014 #[test]
1015 fn checker_resource_limit_messages_exceeded() {
1016 let caps = AgentCapabilities {
1017 resource_limits: ResourceLimits {
1018 max_messages: 10,
1019 ..Default::default()
1020 },
1021 ..Default::default()
1022 };
1023 let (checker, pid) = make_checker_with_entry(caps);
1024 let result = checker.check_resource_limit(pid, &ResourceType::Messages(20));
1025 assert!(result.is_err());
1026 }
1027
1028 #[test]
1029 fn checker_ipc_topic_allowed() {
1030 let caps = AgentCapabilities {
1031 ipc_scope: IpcScope::Topic(vec!["build".into(), "deploy".into()]),
1032 ..Default::default()
1033 };
1034 let (checker, pid) = make_checker_with_entry(caps);
1035 assert!(checker.check_ipc_topic(pid, "build").is_ok());
1036 assert!(checker.check_ipc_topic(pid, "deploy").is_ok());
1037 assert!(checker.check_ipc_topic(pid, "admin").is_err());
1038 }
1039
1040 #[test]
1041 fn checker_ipc_topic_denied_for_direct_messaging() {
1042 let caps = AgentCapabilities {
1043 ipc_scope: IpcScope::Topic(vec!["build".into()]),
1044 ..Default::default()
1045 };
1046 let (checker, pid) = make_checker_with_entry(caps);
1047 assert!(checker.check_ipc_target(pid, 999).is_err());
1049 }
1050
1051 #[test]
1052 fn checker_nonexistent_pid() {
1053 let table = Arc::new(ProcessTable::new(16));
1054 let checker = CapabilityChecker::new(table);
1055 assert!(
1056 checker
1057 .check_tool_access(999, "read_file", None, None)
1058 .is_err()
1059 );
1060 assert!(checker.check_ipc_target(999, 1).is_err());
1061 assert!(
1062 checker
1063 .check_resource_limit(999, &ResourceType::Memory(0))
1064 .is_err()
1065 );
1066 }
1067
1068 #[test]
1069 fn browser_default_uses_restricted_ipc() {
1070 let caps = AgentCapabilities::browser_default();
1071 assert!(
1072 matches!(caps.ipc_scope, IpcScope::Restricted(ref pids) if pids.is_empty()),
1073 "browser agents must default to IpcScope::Restricted([])"
1074 );
1075 assert!(!caps.can_spawn, "browser agents must not spawn");
1076 assert!(!caps.can_network, "browser agents must not access network");
1077 assert!(caps.can_ipc, "browser agents need IPC for kernel comms");
1078 assert!(caps.can_exec_tools, "browser agents need tool execution");
1079 assert!(caps.resource_limits.max_memory_bytes < ResourceLimits::default().max_memory_bytes);
1081 assert!(caps.resource_limits.max_cpu_time_ms < ResourceLimits::default().max_cpu_time_ms);
1082 }
1083
1084 #[test]
1085 fn browser_default_blocks_direct_messages() {
1086 let caps = AgentCapabilities::browser_default();
1087 assert!(!caps.can_message(1));
1089 assert!(!caps.can_message(999));
1090 }
1091
1092 #[test]
1093 fn is_shell_tool_recognizes_variants() {
1094 assert!(is_shell_tool("shell_exec"));
1095 assert!(is_shell_tool("exec_shell"));
1096 assert!(is_shell_tool("bash"));
1097 assert!(is_shell_tool("command"));
1098 assert!(is_shell_tool("run_command"));
1099 assert!(!is_shell_tool("read_file"));
1100 assert!(!is_shell_tool("web_search"));
1101 }
1102
1103 #[test]
1106 fn browser_elevation_needed_for_spawn() {
1107 let requested = AgentCapabilities {
1108 can_spawn: true,
1109 ..AgentCapabilities::browser_default()
1110 };
1111 assert!(AgentCapabilities::needs_elevation("browser", &requested));
1112 }
1113
1114 #[test]
1115 fn browser_elevation_needed_for_network() {
1116 let requested = AgentCapabilities {
1117 can_network: true,
1118 ..AgentCapabilities::browser_default()
1119 };
1120 assert!(AgentCapabilities::needs_elevation("browser", &requested));
1121 }
1122
1123 #[test]
1124 fn browser_elevation_not_needed_for_restricted() {
1125 let requested = AgentCapabilities::browser_default();
1127 assert!(!AgentCapabilities::needs_elevation("browser", &requested));
1128 }
1129
1130 #[test]
1131 fn non_browser_elevation_not_needed() {
1132 let requested = AgentCapabilities {
1134 can_spawn: true,
1135 can_network: true,
1136 ipc_scope: IpcScope::All,
1137 ..Default::default()
1138 };
1139 assert!(!AgentCapabilities::needs_elevation("native", &requested));
1140 assert!(!AgentCapabilities::needs_elevation("wasi", &requested));
1141 }
1142
1143 #[test]
1144 fn browser_elevation_needed_for_ipc_all() {
1145 let requested = AgentCapabilities {
1146 ipc_scope: IpcScope::All,
1147 ..AgentCapabilities::browser_default()
1148 };
1149 assert!(AgentCapabilities::needs_elevation("browser", &requested));
1150 }
1151
1152 #[test]
1153 fn request_elevation_builds_request() {
1154 let current = AgentCapabilities::browser_default();
1155 let requested = AgentCapabilities {
1156 can_network: true,
1157 ..AgentCapabilities::browser_default()
1158 };
1159 let req = AgentCapabilities::request_elevation(¤t, &requested, "browser");
1160 assert_eq!(req.pid, 0);
1161 assert!(!req.current.can_network);
1162 assert!(req.requested.can_network);
1163 assert!(req.reason.contains("browser"));
1164 }
1165
1166 #[cfg(feature = "os-patterns")]
1169 mod disk_quota_tests {
1170 use super::*;
1171
1172 #[test]
1173 fn default_disk_quota_is_100_mib() {
1174 let limits = ResourceLimits::default();
1175 assert_eq!(limits.max_disk_bytes, 100 * 1024 * 1024);
1176 }
1177
1178 #[test]
1179 fn browser_disk_quota_is_10_mib() {
1180 let caps = AgentCapabilities::browser_default();
1181 assert_eq!(caps.resource_limits.max_disk_bytes, 10 * 1024 * 1024);
1182 }
1183
1184 #[test]
1185 fn disk_quota_serde_roundtrip() {
1186 let limits = ResourceLimits {
1187 max_disk_bytes: 50 * 1024 * 1024,
1188 ..Default::default()
1189 };
1190 let json = serde_json::to_string(&limits).unwrap();
1191 let restored: ResourceLimits = serde_json::from_str(&json).unwrap();
1192 assert_eq!(restored.max_disk_bytes, 50 * 1024 * 1024);
1193 }
1194
1195 #[test]
1196 fn disk_quota_zero_means_unlimited() {
1197 let limits = ResourceLimits {
1198 max_disk_bytes: 0,
1199 ..Default::default()
1200 };
1201 assert_eq!(limits.max_disk_bytes, 0);
1202 }
1203 }
1204}