fix(providers): use Bearer auth for Gemini CLI OAuth tokens

* fix(providers): use Bearer auth for Gemini CLI OAuth tokens

When credentials come from ~/.gemini/oauth_creds.json (Gemini CLI),
send them as Authorization: Bearer header instead of ?key= query
parameter. API keys from env vars or config continue using ?key=.

Fixes #194

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(gemini): harden OAuth bearer auth flow and tests

* fix(gemini): granular auth source tracking and review fixes

Build on chumyin's auth model refactor with:
- Expand GeminiAuth to 4 variants (ExplicitKey/EnvGeminiKey/EnvGoogleKey/
  OAuthToken) so auth_source() uses stored discriminant without re-reading
  env vars at call time
- Add is_api_key()/credential() helpers on the enum
- Upgrade expired OAuth token log from debug to warn
- Add tests: provider_rejects_empty_key, auth_source_explicit_key,
  auth_source_none_without_credentials

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: apply rustfmt to fix CI lint failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: root <root@instance-20220913-1738.vcn09131738.oraclevcn.com>
Co-authored-by: argenis de la rosa <theonlyhennygod@gmail.com>
This commit is contained in:
Edvard Schøyen 2026-02-15 14:32:33 -05:00 committed by GitHub
parent e057bf4128
commit 49bb20f961
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 358 additions and 148 deletions

View file

@ -79,9 +79,7 @@ impl GitHubScout {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
"application/vnd.github+json"
.parse()
.expect("valid header"),
"application/vnd.github+json".parse().expect("valid header"),
);
headers.insert(
reqwest::header::USER_AGENT,
@ -101,10 +99,7 @@ impl GitHubScout {
Self {
client,
queries: vec![
"zeroclaw skill".into(),
"ai agent skill".into(),
],
queries: vec!["zeroclaw skill".into(), "ai agent skill".into()],
}
}
@ -143,10 +138,7 @@ impl GitHubScout {
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let has_license = item
.get("license")
.map(|v| !v.is_null())
.unwrap_or(false);
let has_license = item.get("license").map(|v| !v.is_null()).unwrap_or(false);
Some(ScoutResult {
name,
@ -225,9 +217,7 @@ impl Scout for GitHubScout {
/// Minimal percent-encoding for query strings (space → +).
fn urlencoding(s: &str) -> String {
s.replace(' ', "+")
.replace('&', "%26")
.replace('#', "%23")
s.replace(' ', "+").replace('&', "%26").replace('#', "%23")
}
/// Deduplicate scout results by URL (keeps first occurrence).
@ -246,13 +236,31 @@ mod tests {
#[test]
fn scout_source_from_str() {
assert_eq!("github".parse::<ScoutSource>().unwrap(), ScoutSource::GitHub);
assert_eq!("GitHub".parse::<ScoutSource>().unwrap(), ScoutSource::GitHub);
assert_eq!("clawhub".parse::<ScoutSource>().unwrap(), ScoutSource::ClawHub);
assert_eq!("huggingface".parse::<ScoutSource>().unwrap(), ScoutSource::HuggingFace);
assert_eq!("hf".parse::<ScoutSource>().unwrap(), ScoutSource::HuggingFace);
assert_eq!(
"github".parse::<ScoutSource>().unwrap(),
ScoutSource::GitHub
);
assert_eq!(
"GitHub".parse::<ScoutSource>().unwrap(),
ScoutSource::GitHub
);
assert_eq!(
"clawhub".parse::<ScoutSource>().unwrap(),
ScoutSource::ClawHub
);
assert_eq!(
"huggingface".parse::<ScoutSource>().unwrap(),
ScoutSource::HuggingFace
);
assert_eq!(
"hf".parse::<ScoutSource>().unwrap(),
ScoutSource::HuggingFace
);
// unknown falls back to GitHub
assert_eq!("unknown".parse::<ScoutSource>().unwrap(), ScoutSource::GitHub);
assert_eq!(
"unknown".parse::<ScoutSource>().unwrap(),
ScoutSource::GitHub
);
}
#[test]