collective

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

diff --git a/crates/ai-comfy-env/src/source/hosts.rs b/crates/ai-comfy-env/src/source/hosts.rs--- a/crates/ai-comfy-env/src/source/hosts.rs+++ b/crates/ai-comfy-env/src/source/hosts.rs@@ -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 {diff --git a/crates/ai-comfy-env/src/source/resolve/api.rs b/crates/ai-comfy-env/src/source/resolve/api.rs--- a/crates/ai-comfy-env/src/source/resolve/api.rs+++ b/crates/ai-comfy-env/src/source/resolve/api.rs@@ -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}",+            );+        }+    }+}diff --git a/tools/mutation/mutation_check.py b/tools/mutation/mutation_check.py--- a/tools/mutation/mutation_check.py+++ b/tools/mutation/mutation_check.py@@ -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'),