M1/F1: Kimi Code OAuth device-code flow

Replace the xAI OAuth stack with the Kimi device authorization grant:
- kimi_oauth.rs wire layer (device_authorization + token poll + refresh
  against kigi_env::oauth_host(); client_id per PRD; retryable statuses
  429/5xx with backoff; expired_token restarts authorization)
- X-Msh-Device-{Name,Model,Id} headers; device_id minted uuid4-hex at
  ~/.kigi/device_id (0600)
- Storage: system keyring service `kigi`, entry `oauth/kimi-code`
  (macOS/Windows native backends), atomic-file fallback under ~/.kigi;
  official client's keyring/~/.kimi never touched
- Refresh manager: 60s tick, threshold max(300, expires_in*0.5),
  401-tombstone keyed by rejected refresh token with 300s cooldown and
  rotation auto-clear, cross-process lock with sibling-adoption
  triple-check, sleep/wake forced refresh
- Deleted xAI machinery: enterprise OIDC (PKCE/JWKS/teams), devbox login,
  external auth provider, JWT tier gating + subscription paywall stack,
  X-XAI-Token-Auth marker headers, ZDR gates, /user enrichment
- kigi login / TUI /login both drive the device flow; login-host display
  now derives from kigi_env::oauth_host()
- 264 auth unit/wiremock tests; live contract probe of
  auth.kimi.com/api/oauth/device_authorization matches the wire shapes

Gates: check/clippy --all-targets clean, fmt, deny ok, kigi-shell lib
5131 tests green.
This commit is contained in:
2026-07-17 07:37:29 -04:00
parent d6c20fc13f
commit 021b82443d
117 changed files with 4052 additions and 19900 deletions
+16 -59
View File
@@ -22,28 +22,24 @@ impl AuthStatus {
/// Banner status: env key → session → BYOK → deployment → none.
///
/// Differs from sampling (`resolve_credentials`: BYOK → session → env) so a
/// logged-in user sees the login host. BYOK uses
/// [`crate::agent::auth_method::should_advertise_xai_api_key`] so
/// `disable_api_key_auth` is honored.
/// logged-in user sees the login host.
pub fn resolve(agent_config: &AgentConfig) -> Self {
if crate::agent::auth_method::has_xai_api_key_env() {
return Self::ApiKey;
}
if agent_config.create_auth_manager().current().is_some() {
let origin = &agent_config.grok_com_config.grok_ws_origin;
let origin = kigi_env::oauth_host();
let host = origin
.strip_prefix("https://")
.or_else(|| origin.strip_prefix("http://"))
.unwrap_or(origin);
.unwrap_or(&origin);
return Self::LoggedIn(host.to_owned());
}
let models = crate::agent::config::resolve_model_list(agent_config, None);
if crate::agent::auth_method::should_advertise_xai_api_key(
agent_config.grok_com_config.api_key_auth_disabled(),
models.values(),
) && let Some(name) = models
.iter()
.find_map(|(name, entry)| entry.has_own_credentials().then(|| name.clone()))
if crate::agent::auth_method::should_advertise_xai_api_key(models.values())
&& let Some(name) = models
.iter()
.find_map(|(name, entry)| entry.has_own_credentials().then(|| name.clone()))
{
return Self::ModelCredentials(name);
}
@@ -94,7 +90,7 @@ mod tests {
use super::*;
use crate::agent::auth_method::{LEGACY_XAI_API_KEY_ENV_VAR, XAI_API_KEY_ENV_VAR};
use crate::agent::config::Config;
use crate::auth::{AuthMode, GrokAuth};
use crate::auth::{AuthMode, KimiAuth};
use kigi_test_support::EnvGuard;
use serial_test::serial;
@@ -155,17 +151,17 @@ mod tests {
#[serial]
fn resolve_oauth_session() {
let (_dir, _g) = isolate_auth_sources();
let token = GrokAuth {
let token = KimiAuth {
key: "session-token".into(),
auth_mode: AuthMode::WebLogin,
..GrokAuth::test_default()
auth_mode: AuthMode::OAuth,
..KimiAuth::test_default()
};
let json = serde_json::to_string(&token).unwrap();
let _auth = EnvGuard::set("KIGI_AUTH", &json);
assert_eq!(
AuthStatus::resolve(&Config::default()),
AuthStatus::LoggedIn("grok.com".to_owned())
AuthStatus::LoggedIn("auth.kimi.com".to_owned())
);
}
@@ -247,10 +243,10 @@ mod tests {
#[serial]
fn resolve_priority_session_over_byok_and_deployment() {
let (_dir, _g) = isolate_auth_sources();
let token = GrokAuth {
let token = KimiAuth {
key: "session-token".into(),
auth_mode: AuthMode::WebLogin,
..GrokAuth::test_default()
auth_mode: AuthMode::OAuth,
..KimiAuth::test_default()
};
let json = serde_json::to_string(&token).unwrap();
let _auth = EnvGuard::set("KIGI_AUTH", &json);
@@ -259,7 +255,7 @@ mod tests {
let cfg = config_from_toml(&byok_and_deployment_toml(dm));
assert_eq!(
AuthStatus::resolve(&cfg),
AuthStatus::LoggedIn("grok.com".to_owned())
AuthStatus::LoggedIn("auth.kimi.com".to_owned())
);
}
@@ -275,45 +271,6 @@ mod tests {
);
}
#[test]
#[serial]
fn resolve_disable_api_key_auth_suppresses_byok_banner() {
let (_dir, _g) = isolate_auth_sources();
let dm = crate::models::default_model();
let cfg = config_from_toml(&format!(
r#"
[grok_com_config]
disable_api_key_auth = true
[model."{dm}"]
model = "{dm}"
api_key = "sk-byok"
"#
));
assert_eq!(AuthStatus::resolve(&cfg), AuthStatus::NotAuthenticated);
}
#[test]
#[serial]
fn resolve_disable_api_key_auth_falls_through_to_deployment() {
let (_dir, _g) = isolate_auth_sources();
let dm = crate::models::default_model();
let cfg = config_from_toml(&format!(
r#"
[grok_com_config]
disable_api_key_auth = true
[endpoints]
deployment_key = "deploy-key"
[model."{dm}"]
model = "{dm}"
api_key = "sk-byok"
"#
));
assert_eq!(AuthStatus::resolve(&cfg), AuthStatus::DeploymentKey);
}
#[test]
#[serial]
fn resolve_model_credentials_uses_first_catalog_key() {