Skip to main content

objectiveai_cli/filesystem/install/
identifiers.rs

1use super::{InstallError, InstallKind};
2
3/// Reject reserved repository names before any install side-effect.
4/// `objectiveai` (case-insensitive) is reserved because the viewer
5/// uses it as the Tauri channel name for built-in events; a plugin
6/// with that repository name would shadow them. Only applied to
7/// plugin installs — tools have no such collision.
8fn check_repository_name(repository: &str) -> Result<(), InstallError> {
9    if repository.eq_ignore_ascii_case("objectiveai") {
10        return Err(InstallError::ReservedRepositoryName {
11            repository: repository.to_string(),
12        });
13    }
14    Ok(())
15}
16
17/// Identifier shape check shared by `owner`, `repository`, and
18/// `commit`: Anthropic's tool-name regex (`^[a-zA-Z0-9_-]{1,128}$`)
19/// plus `.` (so semver-shaped versions and dotted commit refs flow
20/// through cleanly; the `.` -> `-` substitution happens when the
21/// LLM-visible tool name is materialized downstream).
22fn validate_identifier(
23    kind: &'static str,
24    value: &str,
25) -> Result<(), InstallError> {
26    let valid_len = !value.is_empty() && value.len() <= 128;
27    let valid_chars = value
28        .chars()
29        .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'));
30    if !valid_len || !valid_chars {
31        return Err(InstallError::InvalidIdentifier {
32            kind,
33            value: value.to_string(),
34        });
35    }
36    Ok(())
37}
38
39/// Combined shape check for the three caller-supplied identifiers every
40/// install entry point takes. For [`InstallKind::Plugin`] the reserved
41/// repository-name check runs first (so a reserved-name failure takes
42/// precedence over a generic regex failure for the same input); tools
43/// skip it.
44pub(crate) fn validate_install_inputs(
45    kind: InstallKind,
46    owner: &str,
47    repository: &str,
48    commit_sha: Option<&str>,
49) -> Result<(), InstallError> {
50    if matches!(kind, InstallKind::Plugin) {
51        check_repository_name(repository)?;
52    }
53    validate_identifier("owner", owner)?;
54    validate_identifier("repository", repository)?;
55    if let Some(sha) = commit_sha {
56        validate_identifier("commit", sha)?;
57    }
58    Ok(())
59}
60
61/// Convention: the raw-GitHub URL we'd fetch `objectiveai.json` from
62/// for a given (owner, repository, optional commit sha). Defaults to
63/// `HEAD` when no commit is supplied. Lifted out so the cli and the
64/// SDK's own install wrappers share one source of truth.
65pub fn raw_manifest_url(
66    owner: &str,
67    repository: &str,
68    commit_sha: Option<&str>,
69) -> String {
70    let reference = commit_sha.unwrap_or("HEAD");
71    format!(
72        "https://raw.githubusercontent.com/{owner}/{repository}/{reference}/objectiveai.json"
73    )
74}
75
76pub(crate) fn build_headers(
77    headers: Option<&indexmap::IndexMap<String, String>>,
78) -> Result<reqwest::header::HeaderMap, InstallError> {
79    let mut out = reqwest::header::HeaderMap::new();
80    let Some(h) = headers else {
81        return Ok(out);
82    };
83    for (k, v) in h {
84        let name = reqwest::header::HeaderName::from_bytes(k.as_bytes())
85            .map_err(|e| InstallError::InvalidHeaderName {
86                name: k.clone(),
87                reason: e.to_string(),
88            })?;
89        let value = reqwest::header::HeaderValue::from_str(v).map_err(|e| {
90            InstallError::InvalidHeaderValue {
91                name: k.clone(),
92                reason: e.to_string(),
93            }
94        })?;
95        out.insert(name, value);
96    }
97    Ok(out)
98}