Skip to main content

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}