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
+20 -18
View File
@@ -10,7 +10,7 @@ use chrono::{DateTime, Duration as ChronoDuration, Utc};
use indexmap::IndexMap;
use crate::agent::config::{self, ModelEntry, resolve_credentials, sampling_config_for_model};
use crate::auth::{AuthManager, GrokAuth, GrokComConfig};
use crate::auth::{AuthManager, KimiAuth, KimiCodeConfig};
use crate::remote::{FetchModelsResult, fetch_models_blocking};
use crate::sampling::SamplerConfig as SamplingConfig;
use globset::{Glob, GlobSet, GlobSetBuilder};
@@ -151,7 +151,7 @@ struct Inner {
impl Default for ModelsManager {
fn default() -> Self {
let kigi_home = crate::util::kigi_home::kigi_home();
let auth_manager = Arc::new(AuthManager::new(&kigi_home, GrokComConfig::default()));
let auth_manager = Arc::new(AuthManager::new(&kigi_home, KimiCodeConfig::default()));
Self::new(
None,
IndexMap::new(),
@@ -1397,7 +1397,7 @@ fn build_prefetched_map(
/// Fetch remote models. Checks disk cache first; persists after fetch.
pub(crate) fn prefetch_models_blocking(
endpoints: &config::EndpointsConfig,
auth: Option<&GrokAuth>,
auth: Option<&KimiAuth>,
fetch_auth: ModelFetchAuth,
) -> Option<IndexMap<String, ModelEntry>> {
prefetch_models_blocking_gated(
@@ -1414,7 +1414,7 @@ pub(crate) fn prefetch_models_blocking(
/// decisions cannot disagree mid-startup.
pub(crate) fn prefetch_models_and_settings_blocking(
endpoints: &config::EndpointsConfig,
auth: Option<&GrokAuth>,
auth: Option<&KimiAuth>,
fetch_auth: ModelFetchAuth,
) -> (
Option<IndexMap<String, ModelEntry>>,
@@ -1441,7 +1441,7 @@ pub(crate) fn prefetch_models_and_settings_blocking(
/// knob once for both halves.
fn prefetch_models_blocking_gated(
endpoints: &config::EndpointsConfig,
auth: Option<&GrokAuth>,
auth: Option<&KimiAuth>,
fetch_auth: ModelFetchAuth,
remote_fetch_enabled: bool,
) -> Option<IndexMap<String, ModelEntry>> {
@@ -1499,12 +1499,12 @@ pub struct EarlyPrefetchResult {
pub type EarlyPrefetchHandle = std::thread::JoinHandle<EarlyPrefetchResult>;
struct PrefetchEnv {
auth: Option<GrokAuth>,
auth: Option<KimiAuth>,
endpoints: config::EndpointsConfig,
model_fetch_auth: ModelFetchAuth,
}
fn resolve_prefetch_env_with_auth(auth: Option<GrokAuth>) -> Option<PrefetchEnv> {
fn resolve_prefetch_env_with_auth(auth: Option<KimiAuth>) -> Option<PrefetchEnv> {
let _timer = crate::instrumentation_timer!("startup.early_prefetch_launch");
// Config-aware (not env-only) so the prefetch can't leak the bearer to api.x.ai.
let mut endpoints = config::EndpointsConfig::from_effective_config();
@@ -1529,7 +1529,7 @@ fn resolve_prefetch_env_with_auth(auth: Option<GrokAuth>) -> Option<PrefetchEnv>
/// `deployment_key` would re-arm the prefetch — and with it the `/v1/settings`
/// fetch and the deployment-config sync on the prefetch thread.
fn resolve_prefetch_env_from_parts(
auth: Option<GrokAuth>,
auth: Option<KimiAuth>,
endpoints: config::EndpointsConfig,
remote_fetch_enabled: bool,
) -> Option<PrefetchEnv> {
@@ -1554,9 +1554,9 @@ fn resolve_prefetch_env_from_parts(
})
}
fn resolve_prefetch_env(grok_com_config: Option<GrokComConfig>) -> Option<PrefetchEnv> {
fn resolve_prefetch_env(kimi_code_config: Option<KimiCodeConfig>) -> Option<PrefetchEnv> {
let kigi_home = crate::util::kigi_home::kigi_home();
let auth_manager = AuthManager::new(&kigi_home, grok_com_config.unwrap_or_default());
let auth_manager = AuthManager::new(&kigi_home, kimi_code_config.unwrap_or_default());
let auth = auth_manager.current();
resolve_prefetch_env_with_auth(auth)
}
@@ -1566,7 +1566,7 @@ fn resolve_prefetch_env(grok_com_config: Option<GrokComConfig>) -> Option<Prefet
/// When the caller has already obtained valid credentials (e.g. via
/// `try_ensure_fresh_auth`), pass them here to avoid re-reading stale cached
/// credentials from disk.
pub fn start_early_prefetch_with_auth(auth: Option<GrokAuth>) -> Option<EarlyPrefetchHandle> {
pub fn start_early_prefetch_with_auth(auth: Option<KimiAuth>) -> Option<EarlyPrefetchHandle> {
let env = resolve_prefetch_env_with_auth(auth)?;
Some(spawn_prefetch_thread(env))
}
@@ -1575,8 +1575,10 @@ pub fn start_early_prefetch_with_auth(auth: Option<GrokAuth>) -> Option<EarlyPre
///
/// Convenience wrapper that reads cached auth from disk. Prefer
/// `start_early_prefetch_with_auth` when you have pre-resolved credentials.
pub fn start_early_prefetch(grok_com_config: Option<GrokComConfig>) -> Option<EarlyPrefetchHandle> {
let env = resolve_prefetch_env(grok_com_config)?;
pub fn start_early_prefetch(
kimi_code_config: Option<KimiCodeConfig>,
) -> Option<EarlyPrefetchHandle> {
let env = resolve_prefetch_env(kimi_code_config)?;
Some(spawn_prefetch_thread(env))
}
@@ -1969,7 +1971,7 @@ pub(crate) fn validate_selectable(
/// Async wrapper around `prefetch_models_blocking`.
pub(crate) async fn fetch_models_async(
endpoints: config::EndpointsConfig,
auth: Option<GrokAuth>,
auth: Option<KimiAuth>,
fetch_auth: ModelFetchAuth,
) -> Option<IndexMap<String, ModelEntry>> {
tokio::task::spawn_blocking(move || {
@@ -1991,7 +1993,7 @@ mod tests {
// Use a temp dir so AuthManager finds no credentials — ensures
// refresh_async bails at the auth check without needing a tokio runtime.
let tmp = std::env::temp_dir().join("grok-test-models-manager");
let auth_manager = Arc::new(AuthManager::new(&tmp, GrokComConfig::default()));
let auth_manager = Arc::new(AuthManager::new(&tmp, KimiCodeConfig::default()));
ModelsManager::new(
None,
IndexMap::new(),
@@ -2233,7 +2235,7 @@ mod tests {
#[test]
fn current_reasoning_effort_seeded_from_config() {
let tmp = std::env::temp_dir().join("grok-test-models-manager-seed");
let auth_manager = Arc::new(AuthManager::new(&tmp, GrokComConfig::default()));
let auth_manager = Arc::new(AuthManager::new(&tmp, KimiCodeConfig::default()));
let mut cfg = config::Config::default();
cfg.models.default_reasoning_effort = Some(ReasoningEffort::Xhigh);
let mgr = ModelsManager::new(
@@ -2401,7 +2403,7 @@ mod tests {
// The internal getters read those derived fields.
let tmp = std::env::temp_dir().join("grok-test-models-manager-menu-only");
let auth_manager = Arc::new(AuthManager::new(&tmp, GrokComConfig::default()));
let auth_manager = Arc::new(AuthManager::new(&tmp, KimiCodeConfig::default()));
let mgr = ModelsManager::new(
None,
catalog,
@@ -3222,7 +3224,7 @@ mod tests {
};
assert!(
resolve_prefetch_env_from_parts(
Some(GrokAuth::test_default()),
Some(KimiAuth::test_default()),
endpoints.clone(),
false,
)