objectiveai_cli/filesystem/plugins/whitelist.rs
1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4/// One whitelist row. Each field is a regex matched **anchored**
5/// against the corresponding install parameter (compiled as
6/// `^(?:pattern)$`). All four fields must match for the row to
7/// allow the install.
8///
9/// The host loads a list of these and accepts a plugin install only
10/// if the (owner, repository, commit_sha_or_HEAD, manifest.version)
11/// quadruple matches at least one row. Use `.*` for catch-all fields.
12#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
13#[schemars(rename = "filesystem.plugins.WhitelistEntry")]
14pub struct WhitelistEntry {
15 /// Regex matching the GitHub owner (org or user). Case-sensitive.
16 pub owner: String,
17 /// Regex matching the repository name.
18 pub repository: String,
19 /// Regex matching the commit sha. When the user omits
20 /// `--commit-sha`, this is matched against the literal string
21 /// `"HEAD"`.
22 pub commit_sha: String,
23 /// Regex matching the manifest's `version` field.
24 pub version: String,
25}
26
27/// The hard-coded default whitelist. Single entry: any repo under
28/// the ObjectiveAI GitHub org passes; everything else requires
29/// `--insecure`.
30pub fn default_whitelist() -> Vec<WhitelistEntry> {
31 vec![WhitelistEntry {
32 // (?i) is the regex inline flag for case-insensitive matching;
33 // GitHub usernames are case-insensitive at the URL level, so
34 // accept any capitalization of the canonical org name.
35 owner: "(?i)ObjectiveAI".to_string(),
36 repository: ".*".to_string(),
37 commit_sha: ".*".to_string(),
38 version: ".*".to_string(),
39 }]
40}
41
42/// Returns `Ok(true)` iff the quadruple matches at least one entry
43/// in `whitelist`. Each field is compiled as `^(?:pattern)$` so
44/// patterns like `Object` won't match `ObjectAttacker`. Returns
45/// `Err(regex::Error)` if any pattern fails to compile.
46pub fn check_plugin_whitelist(
47 owner: &str,
48 repository: &str,
49 commit_sha: &str,
50 version: &str,
51 whitelist: &[WhitelistEntry],
52) -> Result<bool, regex::Error> {
53 for entry in whitelist {
54 if anchored_match(&entry.owner, owner)?
55 && anchored_match(&entry.repository, repository)?
56 && anchored_match(&entry.commit_sha, commit_sha)?
57 && anchored_match(&entry.version, version)?
58 {
59 return Ok(true);
60 }
61 }
62 Ok(false)
63}
64
65fn anchored_match(pattern: &str, value: &str) -> Result<bool, regex::Error> {
66 let anchored = format!("^(?:{pattern})$");
67 let re = regex::Regex::new(&anchored)?;
68 Ok(re.is_match(value))
69}