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
+40 -361
View File
@@ -1,5 +1,5 @@
use crate::agent::auth_method::ModelByok;
use crate::auth::{AuthManager, GrokComConfig, OidcAuthConfig};
use crate::auth::{AuthManager, KimiCodeConfig};
use crate::remote::DEFAULT_CONTEXT_WINDOW;
use crate::{config::StorageMode, sampling::ApiBackend, tools::config::ShellToolsetConfig};
use agent_client_protocol as acp;
@@ -1108,7 +1108,7 @@ pub struct Config {
/// Warnings from `[model.*]` parsing; surfaced by `grok inspect`.
#[serde(skip)]
pub model_override_warnings: Vec<super::config_model_override_parse::ModelOverrideWarning>,
pub grok_com_config: GrokComConfig,
pub kimi_code_config: KimiCodeConfig,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shortcuts: Option<toml::Value>,
/// Written by the client via `config_toml_edit`; absorbed so it isn't
@@ -1178,9 +1178,9 @@ pub struct Config {
#[serde(default, skip_serializing)]
pub managed_mcps: crate::config::ManagedMcpsConfig,
/// `[auth]` alias — consumed by `expand_auth_alias` before serde.
/// Typed as `GrokComConfig` (same schema) so sub-field typos are caught.
/// Typed as `KimiCodeConfig` (same schema) so sub-field typos are caught.
#[serde(default, skip_serializing)]
pub auth: Option<GrokComConfig>,
pub auth: Option<KimiCodeConfig>,
/// `[desktop]` section — owned by grok-desktop (Electron app), opaque to the CLI agent.
#[serde(default, skip_serializing)]
pub desktop: Option<toml::Value>,
@@ -1525,7 +1525,7 @@ impl Default for Config {
auto_mode: AutoModeConfig::default(),
config_models: IndexMap::new(),
model_override_warnings: Vec::new(),
grok_com_config: GrokComConfig::default(),
kimi_code_config: KimiCodeConfig::default(),
shortcuts: None,
hints: None,
ui: UiConfig::default(),
@@ -1620,13 +1620,12 @@ impl Config {
}
Ok(())
}
/// Build an `AuthManager` with the configured proxy URL applied.
/// Build an `AuthManager` for this configuration.
pub fn create_auth_manager(&self) -> AuthManager {
AuthManager::new(
&crate::util::kigi_home::kigi_home(),
self.grok_com_config.clone(),
self.kimi_code_config.clone(),
)
.with_proxy_base_url(&self.endpoints.proxy_url())
}
/// Deserialize the merged `base` document, also returning the ignored key
/// paths whose top-level key appears in `user_config`. Paths outside it
@@ -1679,12 +1678,6 @@ impl Config {
}
config.config_models = config_models;
config.model_override_warnings = model_override_warnings;
if config.grok_com_config.oidc.is_none() {
config.grok_com_config.oidc = OidcAuthConfig::from_env();
}
if config.grok_com_config.oidc.is_none() && config.grok_com_config.oauth2.is_none() {
config.grok_com_config.oauth2 = crate::auth::OAuth2ProviderConfig::from_env();
}
if config.client_version.is_none() {
config.client_version = Self::default().client_version;
}
@@ -1818,16 +1811,16 @@ impl Config {
self.resolve_runtime_fields(&ctx);
crate::util::config::set_remote_campaigns_from_settings(self.remote_settings.as_ref());
}
/// If the TOML contains `[auth]`, copy its contents under `[grok_com_config]`.
/// `[grok_com_config]` takes precedence if both are present (explicit wins).
/// If the TOML contains `[auth]`, copy its contents under `[kimi_code_config]`.
/// `[kimi_code_config]` takes precedence if both are present (explicit wins).
///
/// This lets customers write the shorter `[auth.oidc]` instead of `[grok_com_config.oidc]`.
/// This lets customers write the shorter `[auth.oidc]` instead of `[kimi_code_config.oidc]`.
fn expand_auth_alias(raw_config: &toml::Value) -> toml::Value {
let mut config = raw_config.clone();
if let toml::Value::Table(ref mut table) = config
&& let Some(auth) = table.remove("auth")
{
if let Some(gcc) = table.get_mut("grok_com_config") {
if let Some(gcc) = table.get_mut("kimi_code_config") {
if let (toml::Value::Table(gcc_table), toml::Value::Table(auth_table)) =
(gcc, &auth)
{
@@ -1836,7 +1829,7 @@ impl Config {
}
}
} else {
table.insert("grok_com_config".to_owned(), auth);
table.insert("kimi_code_config".to_owned(), auth);
}
}
config
@@ -2320,18 +2313,6 @@ impl Config {
.default(true)
.resolve()
}
/// Resolve whether to use grok's default OAuth2 (xAI auth.x.ai).
///
/// Enterprise OIDC (`oidc` in config.toml) always wins — this only gates
/// the default xAI OAuth2 fallback when no enterprise OIDC is configured.
///
/// Priority: `--oauth` > KIGI_OAUTH_ENABLED env > default (true = OAuth).
pub fn resolve_grok_oauth(&self, cli_oidc: Option<bool>) -> Resolved<bool> {
BoolFlag::env("KIGI_OAUTH_ENABLED")
.cli(cli_oidc)
.default(true)
.resolve()
}
/// Resolve whether to spawn the per-`Ready`-client transport
/// liveness pollers and the session-actor `StatusDispatcher`.
///
@@ -3918,42 +3899,6 @@ pub fn resolve_credentials(model: &ModelEntry, session_key: Option<&str>) -> Res
auth_scheme,
}
}
/// `disable_api_key_auth` at the credential seam: swap a first-party xAI API
/// key for the IdP session (absent => request fails => forces login). BYOK
/// (non-xAI `base_url`) is untouched; no-op when the switch is off.
pub fn enforce_disable_api_key_auth(
creds: &mut ResolvedCredentials,
disable_api_key_auth: bool,
session_key: Option<&str>,
) {
if disable_api_key_auth
&& creds.auth_type == kigi_chat_state::AuthType::ApiKey
&& crate::util::is_first_party_xai_url(&creds.base_url)
{
creds.auth_type = kigi_chat_state::AuthType::SessionToken;
creds.api_key = session_key.map(str::to_owned);
kigi_log::unified_log::debug(
"auth: kill switch blocked a first-party API key at the credential seam",
None,
Some(serde_json::json!(
{ "replaced_with_session" : session_key.is_some(), "base_url" : creds
.base_url, }
)),
);
}
}
/// Resolve credentials for an auxiliary sampling path (web search, image
/// description) with the first-party API-key kill switch applied, so these
/// paths honor `disable_api_key_auth` exactly like the main chat path.
fn resolve_credentials_enforced(
entry: &ModelEntry,
session_key: Option<&str>,
disable_api_key_auth: bool,
) -> ResolvedCredentials {
let mut credentials = resolve_credentials(entry, session_key);
enforce_disable_api_key_auth(&mut credentials, disable_api_key_auth, session_key);
credentials
}
/// Try to resolve credentials for a model by loading the effective config.
/// Returns `None` (with a warning) if config loading, parsing, or model
@@ -3971,12 +3916,7 @@ pub fn try_resolve_model_credentials(
.ok()?;
let models = resolve_model_list(&cfg, None);
let entry = find_model_by_id(&models, model_id)?;
let mut credentials = resolve_credentials(entry, session_key);
enforce_disable_api_key_auth(
&mut credentials,
cfg.grok_com_config.api_key_auth_disabled(),
session_key,
);
let credentials = resolve_credentials(entry, session_key);
Some(credentials)
}
/// Per-model auth facts (BYOK status + auth scheme) from one effective-config
@@ -4045,13 +3985,12 @@ pub fn resolve_aux_model_sampling_config(
models: &IndexMap<String, ModelEntry>,
endpoints: &EndpointsConfig,
session_key: Option<&str>,
disable_api_key_auth: bool,
alpha_test_key: Option<String>,
client_version: Option<String>,
) -> Option<SamplerConfig> {
let catalog_entry = find_model_by_id(models, model_id).cloned();
if let Some(entry) = &catalog_entry {
let credentials = resolve_credentials_enforced(entry, session_key, disable_api_key_auth);
let credentials = resolve_credentials(entry, session_key);
let sampler = sampling_config_for_model(
entry,
credentials,
@@ -4108,7 +4047,7 @@ pub fn resolve_aux_model_sampling_config(
env_key: None,
api_base_url: None,
};
let credentials = resolve_credentials_enforced(&entry, session_key, disable_api_key_auth);
let credentials = resolve_credentials(&entry, session_key);
let sampler = sampling_config_for_model(
&entry,
credentials,
@@ -4240,11 +4179,7 @@ pub fn sampling_config_for_model(
/// URL-derived header logic at the shell boundary so callers downstream see a
/// single homogenous header bag.
///
/// * cli-chat-proxy bases get `X-XAI-Token-Auth` and
/// `x-authenticateresponse` headers (mirrors the inline match in the legacy
/// `sampling::Client::new` on `is_cli_chat_proxy_url`).
/// * With the optional non-production feature, matching first-party hosts may
/// get an extra access header from the corresponding key argument.
/// * First-party bases get the client-mode header.
///
/// Existing entries are never overwritten so callers can pre-set a value.
pub fn inject_url_derived_headers(
@@ -4253,12 +4188,6 @@ pub fn inject_url_derived_headers(
base_url: &str,
) {
if crate::util::is_cli_chat_proxy_url(base_url) {
headers
.entry("X-XAI-Token-Auth".to_string())
.or_insert_with(|| "xai-grok-cli".to_string());
headers
.entry("x-authenticateresponse".to_string())
.or_insert_with(|| "authenticate-response".to_string());
headers
.entry(crate::http::CLIENT_MODE_HEADER.to_string())
.or_insert_with(|| crate::http::process_client_mode().to_string());
@@ -4289,7 +4218,6 @@ pub fn resolve_model_to_sampling_config(
fn resolve_hidden_default_web_search_sampling_config(
model_id: &str,
session_key: Option<&str>,
disable_api_key_auth: bool,
alpha_test_key: Option<String>,
client_version: Option<String>,
endpoints: &EndpointsConfig,
@@ -4331,7 +4259,7 @@ fn resolve_hidden_default_web_search_sampling_config(
env_key: None,
api_base_url: None,
};
let credentials = resolve_credentials_enforced(&entry, session_key, disable_api_key_auth);
let credentials = resolve_credentials(&entry, session_key);
sampling_config_for_model(
&entry,
credentials,
@@ -4345,13 +4273,12 @@ pub fn resolve_web_search_sampling_config(
model_id: &str,
models: &IndexMap<String, ModelEntry>,
session_key: Option<&str>,
disable_api_key_auth: bool,
alpha_test_key: Option<String>,
client_version: Option<String>,
endpoints: &EndpointsConfig,
) -> Option<SamplerConfig> {
let resolved = if let Some(entry) = find_model_by_id(models, model_id).cloned() {
let credentials = resolve_credentials_enforced(&entry, session_key, disable_api_key_auth);
let credentials = resolve_credentials(&entry, session_key);
Some(sampling_config_for_model(
&entry,
credentials,
@@ -4364,7 +4291,6 @@ pub fn resolve_web_search_sampling_config(
Some(resolve_hidden_default_web_search_sampling_config(
model_id,
session_key,
disable_api_key_auth,
alpha_test_key,
client_version,
endpoints,
@@ -4643,28 +4569,21 @@ reasoning_effort = "low"
}
}
#[test]
fn inject_url_derived_headers_adds_proxy_headers_for_cli_chat_proxy_url() {
fn inject_url_derived_headers_adds_client_mode_for_first_party_url() {
let mut headers = IndexMap::new();
inject_url_derived_headers(
&mut headers,
None,
kigi_env::PRODUCTION_ENDPOINTS.coding_api_base_url,
);
assert_eq!(
headers.get("X-XAI-Token-Auth").map(String::as_str),
Some("xai-grok-cli")
);
assert_eq!(
headers.get("x-authenticateresponse").map(String::as_str),
Some("authenticate-response")
);
assert!(headers.get(crate::http::CLIENT_MODE_HEADER).is_some());
assert!(headers.get("X-XAI-Token-Auth").is_none());
}
#[test]
fn inject_url_derived_headers_skips_proxy_headers_for_external_url() {
fn inject_url_derived_headers_skips_headers_for_external_url() {
let mut headers = IndexMap::new();
inject_url_derived_headers(&mut headers, None, "https://api.x.ai/v1");
assert!(headers.get("X-XAI-Token-Auth").is_none());
assert!(headers.get("x-authenticateresponse").is_none());
inject_url_derived_headers(&mut headers, None, "https://api.example.com/v1");
assert!(headers.get(crate::http::CLIENT_MODE_HEADER).is_none());
}
#[test]
fn inject_url_derived_headers_preserves_caller_extra_headers() {
@@ -4679,24 +4598,6 @@ reasoning_effort = "low"
headers.get("x-custom-byok").map(String::as_str),
Some("value")
);
assert_eq!(
headers.get("X-XAI-Token-Auth").map(String::as_str),
Some("xai-grok-cli")
);
}
#[test]
fn inject_url_derived_headers_does_not_overwrite_existing_entries() {
let mut headers = IndexMap::new();
headers.insert("X-XAI-Token-Auth".to_string(), "caller-set".to_string());
inject_url_derived_headers(
&mut headers,
None,
kigi_env::PRODUCTION_ENDPOINTS.coding_api_base_url,
);
assert_eq!(
headers.get("X-XAI-Token-Auth").map(String::as_str),
Some("caller-set"),
);
}
#[test]
fn parses_toolset_overrides() {
@@ -4834,7 +4735,6 @@ reasoning_effort = "low"
crate::models::default_web_search_model(),
&IndexMap::new(),
Some("session-token"),
false,
None,
None,
&endpoints,
@@ -4891,51 +4791,14 @@ reasoning_effort = "low"
None,
),
);
let resolved = resolve_aux_model_sampling_config(
"grok-build",
&catalog,
&endpoints,
None,
false,
None,
None,
)
.expect("override entry has an API key, so resolution succeeds");
let resolved =
resolve_aux_model_sampling_config("grok-build", &catalog, &endpoints, None, None, None)
.expect("override entry has an API key, so resolution succeeds");
assert_eq!(resolved.model, "v9m-rl-learnability-tp8");
assert_eq!(resolved.base_url, "https://vendor.example/v1");
assert_eq!(resolved.api_key.as_deref(), Some("vendor-key"));
}
#[test]
fn web_search_disable_api_key_auth_swaps_first_party_key_for_session() {
let endpoints = EndpointsConfig::default();
let mut models = IndexMap::new();
models.insert(
"ws-model".to_string(),
test_model_entry(
"ws-model",
"https://api.x.ai/v1",
Some("first-party-key"),
None,
None,
),
);
let resolved = resolve_web_search_sampling_config(
"ws-model",
&models,
Some("session-token"),
true,
None,
None,
&endpoints,
)
.expect("web search model should resolve");
assert_eq!(
resolved.api_key.as_deref(),
Some("session-token"),
"first-party API key must be swapped for the session token when disabled"
);
}
#[test]
fn parses_model_api_key() {
let raw_config: toml::Value = toml::from_str(
r#"
@@ -5351,12 +5214,9 @@ reasoning_effort = "low"
config.base_url,
kigi_env::PRODUCTION_ENDPOINTS.coding_api_base_url
);
assert_eq!(
config
.extra_headers
.get("X-XAI-Token-Auth")
.map(String::as_str),
Some("xai-grok-cli")
assert!(
config.extra_headers.get("X-XAI-Token-Auth").is_none(),
"the xAI token-auth marker header must be gone"
);
}
/// Regression: without a session key, `resolve_credentials` falls through
@@ -5376,75 +5236,6 @@ reasoning_effort = "low"
auth_scheme: Default::default(),
}
}
/// `disable_api_key_auth` kill switch (Claude `forceLoginMethod` parity).
#[test]
fn enforce_disable_api_key_auth_blocks_first_party_only() {
use kigi_chat_state::AuthType;
let mut creds = api_key_creds("https://api.x.ai/v1");
enforce_disable_api_key_auth(&mut creds, false, Some("session-jwt"));
assert_eq!(creds.auth_type, AuthType::ApiKey);
assert_eq!(creds.api_key.as_deref(), Some("xai-secret"));
let mut creds = api_key_creds("https://api.x.ai/v1");
enforce_disable_api_key_auth(&mut creds, true, Some("session-jwt"));
assert_eq!(creds.auth_type, AuthType::SessionToken);
assert_eq!(creds.api_key.as_deref(), Some("session-jwt"));
let mut creds = api_key_creds("https://api.x.ai/v1");
enforce_disable_api_key_auth(&mut creds, true, None);
assert_eq!(creds.auth_type, AuthType::SessionToken);
assert_eq!(creds.api_key, None);
let mut creds = api_key_creds("https://api.example.com/v1");
enforce_disable_api_key_auth(&mut creds, true, Some("session-jwt"));
assert_eq!(creds.auth_type, AuthType::ApiKey);
assert_eq!(creds.api_key.as_deref(), Some("xai-secret"));
let mut creds = ResolvedCredentials {
auth_type: AuthType::SessionToken,
..api_key_creds("https://api.x.ai/v1")
};
enforce_disable_api_key_auth(&mut creds, true, Some("session-jwt"));
assert_eq!(creds.auth_type, AuthType::SessionToken);
}
/// Regression for the OVERRIDE_MODEL kill-switch bypass: a first-party model
/// with its own api_key resolves to `ApiKey` (priority 1, beating the
/// session), and the kill switch — now applied inside
/// `try_resolve_model_credentials` — swaps it for the session token. BYOK
/// (non-x.ai) own keys are preserved. (`try_resolve_model_credentials`
/// loads global config, so this exercises its resolve + enforce core.)
#[test]
fn try_resolve_model_credentials_swaps_first_party_own_key_under_kill_switch() {
use kigi_chat_state::AuthType;
let entry = test_model_entry(
"m",
"https://api.x.ai/v1",
Some("xai-model-key"),
None,
None,
);
let mut creds = resolve_credentials(&entry, Some("session-jwt"));
assert_eq!(
creds.auth_type,
AuthType::ApiKey,
"own key wins over session"
);
assert_eq!(creds.api_key.as_deref(), Some("xai-model-key"));
enforce_disable_api_key_auth(&mut creds, true, Some("session-jwt"));
assert_eq!(
creds.auth_type,
AuthType::SessionToken,
"swapped under switch"
);
assert_eq!(creds.api_key.as_deref(), Some("session-jwt"));
let byok = test_model_entry(
"b",
"https://api.example.com/v1",
Some("sk-byok"),
None,
None,
);
let mut byok_creds = resolve_credentials(&byok, Some("session-jwt"));
enforce_disable_api_key_auth(&mut byok_creds, true, Some("session-jwt"));
assert_eq!(byok_creds.auth_type, AuthType::ApiKey);
assert_eq!(byok_creds.api_key.as_deref(), Some("sk-byok"));
}
#[test]
fn x_api_key_auth_scheme_flows_from_config_to_sampler() {
let mut model = test_model_entry(
@@ -6631,124 +6422,16 @@ reasoning_effort = "low"
let info = ModelInfo::from_config(&entry);
assert_eq!(info.inference_idle_timeout_secs, Some(120));
}
/// The `[auth]` alias and the explicit `[kimi_code_config]` table both
/// deserialize (the auth block currently carries no per-deployment
/// options; the alias machinery is retained for future knobs).
#[test]
fn auth_alias_maps_to_grok_com_config() {
let raw: toml::Value = toml::from_str(
r#"
[auth.oidc]
issuer = "https://example.okta.com"
client_id = "test-id"
"#,
)
.unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
let oidc = cfg.grok_com_config.oidc.expect("oidc should be set");
assert_eq!(oidc.issuer, "https://example.okta.com");
assert_eq!(oidc.client_id, "test-id");
}
#[test]
fn grok_com_config_still_works() {
let raw: toml::Value = toml::from_str(
r#"
[grok_com_config.oidc]
issuer = "https://example.okta.com"
client_id = "test-id"
"#,
)
.unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
let oidc = cfg.grok_com_config.oidc.expect("oidc should be set");
assert_eq!(oidc.issuer, "https://example.okta.com");
}
/// `disable_api_key_auth` plumbs through the `[auth]` alias, and absent
/// means None (opt-in knob, zero impact by default).
#[test]
fn disable_api_key_auth_parses_from_auth_alias() {
let absent = Config::new_from_toml_cfg(&toml::from_str("").unwrap()).unwrap();
assert_eq!(absent.grok_com_config.disable_api_key_auth, None);
let raw: toml::Value = toml::from_str(
r#"
[auth]
disable_api_key_auth = true
"#,
)
.unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
assert_eq!(cfg.grok_com_config.disable_api_key_auth, Some(true));
}
/// `force_login_team_uuid` parses a string (pin), array (any-of), or `[]`
/// (fail closed); absent => None.
#[test]
fn force_login_team_uuid_parses_string_and_array() {
use crate::auth::ForceLoginTeam;
let absent = Config::new_from_toml_cfg(&toml::from_str("").unwrap()).unwrap();
assert_eq!(absent.grok_com_config.force_login_team_uuid, None);
let raw: toml::Value = toml::from_str(
r#"
[auth]
force_login_team_uuid = "team-abc"
"#,
)
.unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
assert_eq!(
cfg.grok_com_config.force_login_team_uuid,
Some(ForceLoginTeam::Single("team-abc".into())),
);
let raw: toml::Value = toml::from_str(
r#"
[grok_com_config]
force_login_team_uuid = ["team-a", "team-b"]
"#,
)
.unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
assert_eq!(
cfg.grok_com_config.force_login_team_uuid,
Some(ForceLoginTeam::AnyOf(vec![
"team-a".into(),
"team-b".into()
])),
);
let raw: toml::Value = toml::from_str(
r#"
[auth]
force_login_team_uuid = []
"#,
)
.unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
assert_eq!(
cfg.grok_com_config.force_login_team_uuid,
Some(ForceLoginTeam::AnyOf(vec![])),
);
}
/// Pinning a team via `force_login_team_uuid` implies API-key auth is
/// disabled even without an explicit `disable_api_key_auth` (team
/// membership can't be verified from a bare API key, so it needs IdP login).
#[test]
fn force_login_team_uuid_implies_api_key_auth_disabled() {
use crate::auth::{ForceLoginTeam, GrokComConfig};
let base = GrokComConfig {
disable_api_key_auth: None,
force_login_team_uuid: None,
..GrokComConfig::default()
};
assert!(!base.api_key_auth_disabled());
assert!(
GrokComConfig {
disable_api_key_auth: Some(true),
..base.clone()
}
.api_key_auth_disabled()
);
assert!(
GrokComConfig {
force_login_team_uuid: Some(ForceLoginTeam::Single("team-x".into())),
..base
}
.api_key_auth_disabled()
);
fn auth_alias_and_kimi_code_config_tables_parse() {
for body in ["[auth]\n", "[kimi_code_config]\n"] {
let raw: toml::Value = toml::from_str(body).unwrap();
let cfg = Config::new_from_toml_cfg(&raw).expect("config should parse");
assert_eq!(cfg.kimi_code_config.auth_scope(), "oauth/kimi-code");
}
}
fn resolve_models_from_toml(
toml_str: &str,
@@ -8662,11 +8345,7 @@ agent_type = "cursor"
persistent_shell = true
[shortcuts]
ctrl_k = "search"
[grok_com_config]
token_header = "test"
[auth.oidc]
issuer = "https://sso.corp.com"
client_id = "abc123"
[kimi_code_config]
[storage]
cleanup_ttl_days = 7
[permission]