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