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
@@ -7,7 +7,7 @@ use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info};
use crate::auth::{GrokAuth, read_auth_json};
use crate::auth::{KimiAuth, read_auth_json};
use super::watcher::ConfigChangeEvent;
@@ -15,7 +15,7 @@ use super::watcher::ConfigChangeEvent;
#[derive(Debug)]
pub enum ConfigUpdate {
/// New auth credentials from disk.
Auth(Box<GrokAuth>),
Auth(Box<KimiAuth>),
/// Auth scope was removed (user logged out).
AuthCleared,
/// A **broadcast** MCP reload — applies to every active session
@@ -535,14 +535,14 @@ fn extract_ui_fields(config: &toml::Value) -> (Option<String>, bool, Option<Stri
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::GrokAuth;
use crate::auth::KimiAuth;
use std::collections::BTreeMap;
fn make_auth(key: &str) -> GrokAuth {
GrokAuth {
fn make_auth(key: &str) -> KimiAuth {
KimiAuth {
key: key.to_string(),
email: Some("test@test.com".to_string()),
..GrokAuth::test_default()
..KimiAuth::test_default()
}
}
@@ -604,7 +604,7 @@ mod tests {
reloader.reload_auth().unwrap();
let update = rx.try_recv().expect("should send Auth update");
assert!(
matches!(update, ConfigUpdate::Auth(a) if a.key == "new-key"), // a is Box<GrokAuth>, Deref coercion
matches!(update, ConfigUpdate::Auth(a) if a.key == "new-key"), // a is Box<KimiAuth>, Deref coercion
"should contain new key"
);
}