soma_application/
capabilities.rs1use soma_provider_core::{
4 BrowserCapability, CapabilityGrant, EnvCapability, FilesystemCapability, GitHubCapability,
5 HostCapabilities, NetworkCapability, TerminalCapability,
6};
7
8use crate::provider_errors::ProviderError;
9
10#[derive(Debug, Clone, Default)]
13pub struct CapabilityBroker {
14 grants: Vec<CapabilityGrant>,
15}
16
17impl CapabilityBroker {
18 pub fn new(grants: Vec<CapabilityGrant>) -> Self {
20 Self { grants }
21 }
22
23 pub fn default_deny() -> Self {
25 Self::default()
26 }
27
28 pub fn authorize(
31 &self,
32 provider: &str,
33 action: &str,
34 requested: &HostCapabilities,
35 ) -> Result<(), ProviderError> {
36 if let Some(capability) = enabled_filesystem(requested) {
37 let granted = self.grants.iter().any(|grant| match grant {
38 CapabilityGrant::Filesystem {
39 read_roots,
40 write_roots,
41 } => {
42 all_paths_allowed(&capability.read_roots, read_roots)
43 && all_paths_allowed(&capability.write_roots, write_roots)
44 }
45 _ => false,
46 });
47 if !granted {
48 return Err(denied(provider, action, "filesystem"));
49 }
50 }
51 if let Some(capability) = enabled_network(requested) {
52 let granted = self.grants.iter().any(|grant| match grant {
53 CapabilityGrant::Network { allowed_hosts } => {
54 all_items_allowed(&capability.allowed_hosts, allowed_hosts)
55 }
56 _ => false,
57 });
58 if !granted {
59 return Err(denied(provider, action, "network"));
60 }
61 }
62 if let Some(capability) = enabled_env(requested) {
63 let granted = self.grants.iter().any(|grant| match grant {
64 CapabilityGrant::Env { allowed } => all_items_allowed(&capability.allowed, allowed),
65 _ => false,
66 });
67 if !granted {
68 return Err(denied(provider, action, "env"));
69 }
70 }
71 if let Some(capability) = enabled_terminal(requested) {
72 let granted = self.grants.iter().any(|grant| match grant {
73 CapabilityGrant::Terminal {
74 working_dir,
75 allowlist,
76 } => {
77 working_dir_allows(capability.working_dir.as_deref(), working_dir.as_deref())
78 && all_items_allowed(&capability.allowlist, allowlist)
79 }
80 _ => false,
81 });
82 if !granted {
83 return Err(denied(provider, action, "terminal"));
84 }
85 }
86 if let Some(capability) = enabled_browser(requested) {
87 let granted = self.grants.iter().any(|grant| match grant {
88 CapabilityGrant::Browser { allowed_origins } => {
89 all_items_allowed(&capability.allowed_origins, allowed_origins)
90 }
91 _ => false,
92 });
93 if !granted {
94 return Err(denied(provider, action, "browser"));
95 }
96 }
97 if let Some(capability) = enabled_github(requested) {
98 let granted = self.grants.iter().any(|grant| match grant {
99 CapabilityGrant::Github {
100 allowed_repos,
101 read_only,
102 } => {
103 all_items_allowed(&capability.allowed_repos, allowed_repos)
104 && (capability.read_only || !read_only)
105 }
106 _ => false,
107 });
108 if !granted {
109 return Err(denied(provider, action, "github"));
110 }
111 }
112 Ok(())
113 }
114}
115
116fn enabled_filesystem(requested: &HostCapabilities) -> Option<&FilesystemCapability> {
117 requested.filesystem.as_ref().filter(|cap| cap.enabled)
118}
119
120fn enabled_network(requested: &HostCapabilities) -> Option<&NetworkCapability> {
121 requested.network.as_ref().filter(|cap| cap.enabled)
122}
123
124fn enabled_env(requested: &HostCapabilities) -> Option<&EnvCapability> {
125 requested.env.as_ref().filter(|cap| cap.enabled)
126}
127
128fn enabled_terminal(requested: &HostCapabilities) -> Option<&TerminalCapability> {
129 requested.terminal.as_ref().filter(|cap| cap.enabled)
130}
131
132fn enabled_browser(requested: &HostCapabilities) -> Option<&BrowserCapability> {
133 requested.browser.as_ref().filter(|cap| cap.enabled)
134}
135
136fn enabled_github(requested: &HostCapabilities) -> Option<&GitHubCapability> {
137 requested.github.as_ref().filter(|cap| cap.enabled)
138}
139
140fn all_items_allowed(requested: &[String], allowed: &[String]) -> bool {
141 requested
142 .iter()
143 .all(|item| allowed.iter().any(|grant| grant == item))
144}
145
146fn all_paths_allowed(requested: &[String], allowed: &[String]) -> bool {
147 requested
148 .iter()
149 .all(|item| allowed.iter().any(|grant| path_allows(item, grant)))
150}
151
152fn path_allows(requested: &str, grant: &str) -> bool {
153 requested == grant
154 || requested
155 .strip_prefix(grant.trim_end_matches('/'))
156 .map(|suffix| suffix.starts_with('/'))
157 .unwrap_or(false)
158}
159
160fn working_dir_allows(requested: Option<&str>, granted: Option<&str>) -> bool {
161 match (requested, granted) {
162 (None, _) => true,
163 (Some(_), None) => false,
164 (Some(requested), Some(granted)) => path_allows(requested, granted),
165 }
166}
167
168fn denied(provider: &str, action: &str, capability: &str) -> ProviderError {
169 ProviderError::new(
170 "capability_denied",
171 provider,
172 Some(action.to_owned()),
173 format!("provider requested denied {capability} capability"),
174 "Grant the specific host capability in policy or disable the provider action.",
175 )
176}