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:
@@ -210,24 +210,14 @@ impl AuthProvider for LeaderAuthProvider {
|
||||
AuthCredential::bearer(token)
|
||||
}
|
||||
/// Owner identity from the leader's `AuthManager`, surfaced on the auth
|
||||
/// provider instead of a separate auth.json
|
||||
/// read. Mirrors the in-process path (`mvp_agent`): prefer `GrokAuth.team_id`
|
||||
/// (what shell telemetry/snapshot use) mapped onto a `"Team"` principal so
|
||||
/// team attribution is derived; otherwise pass principal fields through.
|
||||
/// `None` when no credential is available (identity resolution never blocks).
|
||||
/// provider instead of a separate auth.json read. The Kimi credential
|
||||
/// carries no principal metadata; only the (possibly empty) user id.
|
||||
fn identity(&self) -> Option<AuthIdentity> {
|
||||
let a = self.auth_manager.current_or_expired()?;
|
||||
Some(match a.team_id.filter(|t| !t.is_empty()) {
|
||||
Some(team) => AuthIdentity {
|
||||
user_id: a.user_id,
|
||||
principal_type: Some("Team".to_string()),
|
||||
principal_id: Some(team),
|
||||
},
|
||||
None => AuthIdentity {
|
||||
user_id: a.user_id,
|
||||
principal_type: a.principal_type,
|
||||
principal_id: a.principal_id,
|
||||
},
|
||||
Some(AuthIdentity {
|
||||
user_id: a.user_id,
|
||||
principal_type: None,
|
||||
principal_id: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user