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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user