Login picker: offer all three platforms like the official CLI

The unauthenticated welcome screen previously offered only 'Login with
Kimi Code'. It now lists every interactive platform the shell
advertises, matching the official kimi-cli picker:

  Kimi Code (OAuth)
  Moonshot Open Platform (API key · moonshot.cn)
  Moonshot Open Platform (API key · moonshot.ai)
  Quit

- Shell: new ACP auth methods moonshot-cn / moonshot-ai (advertised
  after kimi-code; the BYOK first-position invariant holds).
  authenticate(moonshot-*) reloads keys from env>config, fails with an
  actionable message when none is configured, validates the key against
  GET {base}/models (401 → 'invalid API key' naming the console), then
  swaps the fresh config in, triggers the model sync, and reports
  auth_mode api_key so the pager treats it like other API-key logins.
  Never session-based; keys never logged.
- Config: save_platform_api_key persists [platforms.<id>].api_key via an
  atomic mode-preserving write under the config lock; refuses OAuth
  platforms, blank keys, and unparseable files.
- TUI: the Pending welcome renders the picker rows from the advertised
  methods (arrows/Enter/mouse; 'l' keeps selecting the OAuth row).
  Choosing a Moonshot row opens a masked paste box ('Paste your Moonshot
  API key (from platform.moonshot.cn)'); Esc returns to the picker,
  Enter persists the key and authenticates in one sequential effect;
  failures return to the picker with the error line, success lands on
  the normal welcome. Startup eager-auth is unchanged: a key already in
  the environment authenticates exactly as before, and single-method
  shells keep the historical auto-device-flow.

Gates: workspace check/clippy 0/0; shell 4870 + tui 6620 lib tests
green; headless probe advertises [kimi-code, moonshot-cn, moonshot-ai]
for a fresh user and xai.api_key-first with a key configured.
This commit is contained in:
ZacharyZhang-NY committed 2026-07-18 04:02:39 -04:00
1 parent f6253fbf56
commit 3952c28f16
14 files changed
+1253 -31

No files matched your search

@@ -447,6 +447,66 @@ impl MvpAgent {
)
.await
}
/// `authenticate(moonshot-cn / moonshot-ai)`: interactive open-platform
/// API-key login from the welcome picker.
///
/// Reloads the platform keys from disk+env (the TUI persists the pasted
/// key to `[platforms.<id>]` in config.toml immediately before this call),
/// fails with an actionable error when none is configured, validates the
/// key against `GET {platform_base}/models`, then marks the session
/// authenticated exactly like an external API key: publish the method id
/// (NOT session-based — no token refresh), swap the freshly-stamped config
/// into the models manager, and trigger the model sync so the catalog
/// gains the platform's entries. The key itself is never logged.
pub(super) async fn authenticate_moonshot(
&self,
platform: kigi_models::PlatformId,
method_id: acp::AuthMethodId,
) -> Result<AuthenticateResponse, acp::Error> {
let keys =
crate::agent::models::PlatformApiKeys::resolve_from_effective_config();
auth_method::authenticate_platform_api_key(platform, keys.key_for(platform))
.await
.inspect_err(|_| {
emit_login_span(
false,
method_id.0.as_ref(),
None,
Some("platform_key_invalid_or_missing"),
);
})?;
// Swap the on-disk config (now carrying the key) into the models
// manager so `apply_platform_credentials` stamps the platform's
// catalog entries; a parse failure keeps the last-known-good config
// (`on_auth_changed` below still re-resolves keys from disk itself).
match crate::config::load_effective_config()
.map_err(|e| e.to_string())
.and_then(|raw| crate::agent::config::Config::new_from_toml_cfg(&raw))
{
Ok(new_cfg) => self.models_manager.apply_config(new_cfg),
Err(e) => {
tracing::warn!(
error = % e,
"moonshot auth: config reload failed; keeping last-known-good"
);
}
}
self.set_auth_method(method_id.clone());
self.models_manager.on_auth_changed().await;
emit_login_span(true, method_id.0.as_ref(), None, None);
// Report api-key auth mode so the pager's `apply_auth_meta` treats
// the session like every other external-API-key login (badge shown,
// `/usage` hidden).
let auth_meta = crate::auth::AuthMeta {
email: None,
auth_mode: Some("api_key".to_string()),
show_resolved_model: None,
};
let meta = serde_json::to_value(auth_meta)
.ok()
.and_then(|v| v.as_object().cloned());
Ok(AuthenticateResponse::new().meta(meta))
}
pub(crate) fn deployment_key(&self) -> Option<String> {
self.cfg.borrow().endpoints.deployment_key.clone()
}