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
@@ -11,9 +11,8 @@ mod common;
#[cfg(unix)]
use common::skip_as_root;
use common::{
MANAGED, REQUIREMENTS_FAIL_CLOSED, TEST_EXPIRES_AT, TEST_KEY_ID, forged_team_body,
install_test_key, reset, sign_envelope, signed_team_body, spawn_mock, team_identity, test_home,
write_config, write_dk_config, write_team_auth,
MANAGED, REQUIREMENTS_FAIL_CLOSED, TEST_EXPIRES_AT, TEST_KEY_ID, dk_identity, forged_dk_body,
install_test_key, reset, sign_envelope, signed_dk_body, spawn_mock, test_home, write_dk_config,
};
use kigi_config::signed_policy::{self, SignedPayload};
use serial_test::serial;
@@ -21,14 +20,13 @@ use serial_test::serial;
/// The healthy fail-closed starting state the tamper/heal scenarios mutate;
/// the mock keeps serving the same body, so a healing sync can refetch it.
async fn sync_fail_closed_policy(home: &std::path::Path, kp: &ring::signature::Ed25519KeyPair) {
let url = spawn_mock(signed_team_body(
let url = spawn_mock(signed_dk_body(
kp,
"team-007",
"dep-42",
Some(MANAGED),
Some(REQUIREMENTS_FAIL_CLOSED),
));
write_config(home, &url);
write_team_auth(home, "team-007");
write_dk_config(home, &url, "dep-key-1");
kigi_shell::managed_config::sync()
.await
.expect("initial sync should succeed");
@@ -94,9 +92,8 @@ async fn rejected_signature_surfaces_as_setup_and_login_failure() {
reset(&home);
let (kp, _pubkey) = install_test_key();
let url = spawn_mock(forged_team_body(&kp, "team-007"));
write_config(&home, &url);
write_team_auth(&home, "team-007");
let url = spawn_mock(forged_dk_body(&kp, "dep-42"));
write_dk_config(&home, &url, "dep-key-1");
let outcome = kigi_shell::managed_config::run_setup().await;
assert!(
@@ -130,8 +127,8 @@ async fn withdrawn_requirements_is_deleted_and_covered_by_the_new_sidecar() {
sync_fail_closed_policy(&home, &kp).await;
assert!(home.join("requirements.toml").exists());
let url_partial = spawn_mock(signed_team_body(&kp, "team-007", Some(MANAGED), None));
write_config(&home, &url_partial);
let url_partial = spawn_mock(signed_dk_body(&kp, "dep-42", Some(MANAGED), None));
write_dk_config(&home, &url_partial, "dep-key-1");
let wrote = kigi_shell::managed_config::sync()
.await
.expect("withdrawing sync should succeed");
@@ -156,7 +153,7 @@ async fn withdrawn_requirements_is_deleted_and_covered_by_the_new_sidecar() {
"the new sidecar covers the absence"
);
assert!(
!kigi_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
!kigi_shell::config::is_managed_config_hard_stale_for(&dk_identity()),
"the converged, covered cache is not hard-stale"
);
assert!(kigi_shell::managed_config::managed_policy_gate().is_ok());
@@ -192,7 +189,7 @@ async fn directory_squat_reads_compromised_and_online_sync_heals() {
"the refusal is the managed-policy gate message"
);
assert!(
kigi_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
kigi_shell::config::is_managed_config_hard_stale_for(&dk_identity()),
"the squat must trigger the refetch"
);
@@ -239,7 +236,7 @@ async fn sidecar_read_blip_allows_session_and_triggers_refetch() {
"a transient sidecar read blip must not refuse the session"
);
assert!(
kigi_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
kigi_shell::config::is_managed_config_hard_stale_for(&dk_identity()),
"the blip must trigger the refetch so the self-heal runs"
);
// Restore so the tempdir (and later tests) stay clean.
@@ -276,7 +273,7 @@ async fn sidecar_directory_squat_refuses_then_online_sync_heals() {
"the refusal is the managed-policy gate message"
);
assert!(
kigi_shell::config::is_managed_config_hard_stale_for(&team_identity("team-007")),
kigi_shell::config::is_managed_config_hard_stale_for(&dk_identity()),
"the squat must trigger the refetch"
);