agent_engine/skills/
trust.rs1use crate::skills::Plugin;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct PluginPermissionSummary {
7 pub has_executable_extension: bool,
8 pub has_setup_script: bool,
9 pub permissions: Vec<String>,
10 pub hooks: Vec<String>,
11 pub config_keys: Vec<String>,
12 pub command: Option<String>,
13}
14
15impl PluginPermissionSummary {
16 pub fn is_empty(&self) -> bool {
17 !self.has_executable_extension
18 && !self.has_setup_script
19 && self.permissions.is_empty()
20 && self.hooks.is_empty()
21 && self.config_keys.is_empty()
22 && self.command.is_none()
23 }
24
25 pub fn lines(&self) -> Vec<String> {
26 if self.is_empty() {
27 return vec!["no executable extension or extension permissions declared".to_string()];
28 }
29
30 let mut lines = Vec::new();
31 if self.has_executable_extension {
32 lines.push("executable extension: yes".to_string());
33 }
34 if self.has_setup_script {
35 lines.push("⚠ runs setup script on install".to_string());
36 }
37 if let Some(command) = &self.command {
38 lines.push(format!("command: {}", command));
39 }
40 if self.permissions.is_empty() {
41 lines.push("permissions: <none>".to_string());
42 } else {
43 lines.push(format!("permissions: {}", self.permissions.join(", ")));
44 }
45 if !self.hooks.is_empty() {
46 lines.push(format!("hooks: {}", self.hooks.join(", ")));
47 }
48 if !self.config_keys.is_empty() {
49 lines.push(format!("config: {}", self.config_keys.join(", ")));
50 }
51 lines
52 }
53}
54
55pub fn summarize_plugin_permissions(plugin: &Plugin) -> PluginPermissionSummary {
56 let has_setup_script = plugin
57 .manifest
58 .as_ref()
59 .and_then(|m| m.provides.as_ref())
60 .and_then(|p| p.sidecar.as_ref())
61 .and_then(|s| s.setup.as_ref())
62 .is_some();
63
64 let Some(extension) = &plugin.extension else {
65 return PluginPermissionSummary {
66 has_executable_extension: false,
67 has_setup_script,
68 permissions: Vec::new(),
69 hooks: Vec::new(),
70 config_keys: Vec::new(),
71 command: None,
72 };
73 };
74
75 let mut permissions = extension.permissions.clone();
76 permissions.sort();
77 permissions.dedup();
78
79 let hooks = extension
80 .hooks
81 .iter()
82 .map(|hook| match &hook.tool {
83 Some(tool) => format!("{}({})", hook.hook, tool),
84 None => hook.hook.clone(),
85 })
86 .collect();
87
88 let config_keys = extension
89 .config
90 .iter()
91 .map(|entry| {
92 if entry.required {
93 format!("{} [required]", entry.key)
94 } else {
95 entry.key.clone()
96 }
97 })
98 .collect();
99
100 PluginPermissionSummary {
101 has_executable_extension: true,
102 has_setup_script,
103 permissions,
104 hooks,
105 config_keys,
106 command: Some(
107 format!("{} {}", extension.command, extension.args.join(" "))
108 .trim()
109 .to_string(),
110 ),
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use crate::extensions::manifest::{
118 ExtensionConfigEntry, ExtensionManifest, ExtensionRuntime, HookSubscription,
119 };
120 use std::path::PathBuf;
121
122 fn plugin(extension: Option<ExtensionManifest>) -> Plugin {
123 Plugin {
124 name: "policy".to_string(),
125 root: PathBuf::from("/tmp/policy"),
126 marketplace: None,
127 version: None,
128 description: None,
129 extension,
130 manifest: None,
131 }
132 }
133
134 #[test]
135 fn summary_is_empty_without_extension() {
136 let summary = summarize_plugin_permissions(&plugin(None));
137 assert!(summary.is_empty());
138 assert_eq!(
139 summary.lines(),
140 vec!["no executable extension or extension permissions declared"]
141 );
142 }
143
144 #[test]
145 fn summary_lists_permissions_hooks_and_required_config() {
146 let summary = summarize_plugin_permissions(&plugin(Some(ExtensionManifest {
147 theme_tokens: Default::default(),
148 deferred: None,
149 protocol_version: 1,
150 runtime: ExtensionRuntime::Process,
151 command: "python3".to_string(),
152 setup: None,
153 prebuilt: ::std::collections::HashMap::new(),
154 args: vec!["ext.py".to_string()],
155 permissions: vec![
156 "tools.intercept".to_string(),
157 "privacy.llm_content".to_string(),
158 ],
159 hooks: vec![HookSubscription {
160 hook: "before_tool_call".to_string(),
161 tool: Some("bash".to_string()),
162 matcher: None,
163 }],
164 config: vec![ExtensionConfigEntry {
165 key: "api_key".to_string(),
166 value_type: None,
167 description: None,
168 required: true,
169 default: None,
170 secret_env: Some("API_KEY".to_string()),
171 host_context: None,
172 }],
173 })));
174
175 assert!(summary.has_executable_extension);
176 assert_eq!(
177 summary.permissions,
178 vec!["privacy.llm_content", "tools.intercept"]
179 );
180 assert_eq!(summary.hooks, vec!["before_tool_call(bash)"]);
181 assert_eq!(summary.config_keys, vec!["api_key [required]"]);
182 assert!(summary
183 .lines()
184 .iter()
185 .any(|line| line.contains("permissions: privacy.llm_content, tools.intercept")));
186 }
187}