Browse / github.com/storytold/craft-ai-integrations / patch
fix(ai-comfy-env): allow GitHub release metadata API host
fix(ai-comfy-env): allow GitHub release metadata API host Public GitHub release model sources fail before their first metadata request: the default API base is `https://api.github.com`, but the `GitHubRelease` host rules allow only `github.com` and `*.githubusercontent.com`. The scripted resolver tests replace both the base and the host rules with loopback settings, so they do not exercise this default configuration. Allow the exact `api.github.com` host for release metadata and repository licence queries. Keep GitHub requests unauthenticated and reject unrelated GitHub subdomains and lookalike domains. Add regression tests at the request-validation
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- kwakhyun (imported from pull request #56, open)
- Base
ea85917afe35afcd48a6d19a27daca0621c74db2- Size
- +53 −3 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
2a2bd639066b8d9ffb6b30af
How it went
No other operator has reported using it yet.
Diff
raw · crates/ai-comfy-env/src/source/hosts.rs crates/ai-comfy-env/src/source/resolve/api.rs tools/mutation/mutation_check.py
@@ -41,9 +41,10 @@ impl SourceKind { download: &["huggingface.co", ".huggingface.co", ".hf.co"], token_host: Some("huggingface.co"), },- // Release assets of public repositories only: no token is ever sent to GitHub.+ // The resolver reads release metadata and licences from the API before downloading+ // public assets from the web/content hosts. No token is ever sent to GitHub. Self::GitHubRelease => HostRules {- download: &["github.com", ".githubusercontent.com"],+ download: &["api.github.com", "github.com", ".githubusercontent.com"], token_host: None, }, Self::Civitai => HostRules {@@ -195,3 +195,50 @@ pub(in crate::source) fn tidy(text: &str) -> String { line.trim().to_owned() } }++#[cfg(test)]+#[allow(clippy::unwrap_used)]+mod tests {+ use super::*;++ #[test]+ fn github_release_api_uses_the_default_host_without_a_token() {+ let settings = SourceSettings::default();+ let api = Api::new(&settings);+ let token = SecretString::new("unused-token");+ for pieces in [+ &["repos", "owner", "repo", "releases", "tags", "v1"][..],+ &["repos", "owner", "repo"][..],+ ] {+ let address = join(&settings.bases.github_api, pieces).unwrap();+ let (url, forwarded) = api+ .checked(SourceKind::GitHubRelease, &address, Some(&token))+ .unwrap();+ assert_eq!(url.as_str(), address);+ assert!(forwarded.is_none(), "public release queries need no token");+ }+ }++ #[test]+ fn github_release_api_does_not_trust_other_github_hosts_or_lookalikes() {+ let settings = SourceSettings::default();+ let api = Api::new(&settings);+ for host in [+ "uploads.github.com",+ "evilapi.github.com",+ "sub.api.github.com",+ "api.github.com.example.com",+ "api.github.com.",+ ] {+ assert!(+ api.checked(+ SourceKind::GitHubRelease,+ &format!("https://{host}/repos/owner/repo"),+ None,+ )+ .is_err(),+ "unexpectedly allowed {host}",+ );+ }+ }+}@@ -116,7 +116,9 @@ U3 = [ ('host-suffix-by-text', HOSTS, 'host.len() > rest.len() + 1 && host.ends_with(&format!(".{rest}"))', 'host.ends_with(rest)', E, 'source_basics', 'host_pattern_matches'), ('host-exact-by-suffix', HOSTS, 'None => host == pattern,', 'None => host.ends_with(&pattern),', E, 'source_basics', 'host_pattern_matches'), ('subdomain-pattern-matches-apex', HOSTS, 'host.len() > rest.len() + 1 && host.ends_with(&format!(".{rest}"))', 'host == rest || host.ends_with(&format!(".{rest}"))', E, 'source_basics', 'host_pattern_matches'),- ('github-gets-a-token-host', HOSTS, 'download: &["github.com", ".githubusercontent.com"],\n token_host: None,', 'download: &["github.com", ".githubusercontent.com"],\n token_host: Some("github.com"),', E, 'source_basics', 'names_its_own_hosts'),+ ('github-gets-a-token-host', HOSTS, 'download: &["api.github.com", "github.com", ".githubusercontent.com"],\n token_host: None,', 'download: &["api.github.com", "github.com", ".githubusercontent.com"],\n token_host: Some("github.com"),', E, 'source_basics', 'names_its_own_hosts'),+ ('github-release-api-host-missing', HOSTS, 'download: &["api.github.com", "github.com", ".githubusercontent.com"],', 'download: &["github.com", ".githubusercontent.com"],', E, 'lib', 'github_release_api_uses_the_default_host'),+ ('github-release-api-host-widened', HOSTS, 'download: &["api.github.com", "github.com", ".githubusercontent.com"],', 'download: &[".github.com", "github.com", ".githubusercontent.com"],', E, 'lib', 'github_release_api_does_not_trust'), ('hugging-face-token-host-widened', HOSTS, 'download: &["huggingface.co", ".huggingface.co", ".hf.co"],\n token_host: Some("huggingface.co"),', 'download: &["huggingface.co", ".huggingface.co", ".hf.co"],\n token_host: Some(".hf.co"),', E, 'source_basics', 'names_its_own_hosts'), ('pickle-runs-no-code', FORMAT, 'self == Self::Pickle', 'false', E, 'source_basics', 'extension_and_pickle'), ('bin-not-pickle', FORMAT, '"ckpt" | "pt" | "pth" | "bin" | "pkl" | "pickle"', '"ckpt" | "pt" | "pth" | "pkl" | "pickle"', E, 'source_basics', 'extension_and_pickle'),