synaps_cli/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.manifest.as_ref()
57 .and_then(|m| m.provides.as_ref())
58 .and_then(|p| p.sidecar.as_ref())
59 .and_then(|s| s.setup.as_ref())
60 .is_some();
61
62 let Some(extension) = &plugin.extension else {
63 return PluginPermissionSummary {
64 has_executable_extension: false,
65 has_setup_script,
66 permissions: Vec::new(),
67 hooks: Vec::new(),
68 config_keys: Vec::new(),
69 command: None,
70 };
71 };
72
73 let mut permissions = extension.permissions.clone();
74 permissions.sort();
75 permissions.dedup();
76
77 let hooks = extension
78 .hooks
79 .iter()
80 .map(|hook| match &hook.tool {
81 Some(tool) => format!("{}({})", hook.hook, tool),
82 None => hook.hook.clone(),
83 })
84 .collect();
85
86 let config_keys = extension
87 .config
88 .iter()
89 .map(|entry| {
90 if entry.required {
91 format!("{} [required]", entry.key)
92 } else {
93 entry.key.clone()
94 }
95 })
96 .collect();
97
98 PluginPermissionSummary {
99 has_executable_extension: true,
100 has_setup_script,
101 permissions,
102 hooks,
103 config_keys,
104 command: Some(format!("{} {}", extension.command, extension.args.join(" ")).trim().to_string()),
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::extensions::manifest::{ExtensionConfigEntry, ExtensionManifest, ExtensionRuntime, HookSubscription};
112 use std::path::PathBuf;
113
114 fn plugin(extension: Option<ExtensionManifest>) -> Plugin {
115 Plugin {
116 name: "policy".to_string(),
117 root: PathBuf::from("/tmp/policy"),
118 marketplace: None,
119 version: None,
120 description: None,
121 extension,
122 manifest: None,
123 }
124 }
125
126 #[test]
127 fn summary_is_empty_without_extension() {
128 let summary = summarize_plugin_permissions(&plugin(None));
129 assert!(summary.is_empty());
130 assert_eq!(summary.lines(), vec!["no executable extension or extension permissions declared"]);
131 }
132
133 #[test]
134 fn summary_lists_permissions_hooks_and_required_config() {
135 let summary = summarize_plugin_permissions(&plugin(Some(ExtensionManifest {
136 protocol_version: 1,
137 runtime: ExtensionRuntime::Process,
138 command: "python3".to_string(),
139 setup: None,
140 prebuilt: ::std::collections::HashMap::new(),
141 args: vec!["ext.py".to_string()],
142 permissions: vec!["tools.intercept".to_string(), "privacy.llm_content".to_string()],
143 hooks: vec![HookSubscription {
144 hook: "before_tool_call".to_string(),
145 tool: Some("bash".to_string()),
146 matcher: None,
147 }],
148 config: vec![ExtensionConfigEntry {
149 key: "api_key".to_string(),
150 value_type: None,
151 description: None,
152 required: true,
153 default: None,
154 secret_env: Some("API_KEY".to_string()),
155 }],
156 })));
157
158 assert!(summary.has_executable_extension);
159 assert_eq!(summary.permissions, vec!["privacy.llm_content", "tools.intercept"]);
160 assert_eq!(summary.hooks, vec!["before_tool_call(bash)"]);
161 assert_eq!(summary.config_keys, vec!["api_key [required]"]);
162 assert!(summary.lines().iter().any(|line| line.contains("permissions: privacy.llm_content, tools.intercept")));
163 }
164}