Add per-provider auth.json keys; make auth methods registry-generic (P0b)

Platform API keys now live in auth.json under the platform-id scope (the
per-provider auth.json key contract), resolved env > auth.json > legacy
[platforms.*] config.toml (read-only fallback). The TUI login picker,
paste box, auth-method advertising, and authenticate handler are all
registry-generic: a new PlatformSpec row appears in the login UI and
authenticates with zero UI changes. Spec rows gained vendor/console_host/
login_label display fields (moonshot strings byte-identical, pinned by
tests).

Adversarial review caught that auth.json keys were validated at login but
never stamped onto catalog entries (completions would 401; restart lost
eager auth). Fixed red-green: resolve_model_list/resolve_model_catalog now
take a resolved PlatformApiKeys snapshot consumed by the credential-
stamping layer (auth.json beats stale config.toml, matching the login
validator), with production callers resolving fresh per catalog build.
Also from review: the new auth.json writer takes the manager's cross-
process flock (bounded retry — an unlocked RMW racing a token refresh
could revert a rotated refresh token); the oauth-401 wiremock test is
hermetic (KIGI_SHARE_DIR tempdir; it could read a dev's real auth.json
and hit live moonshot); cli_models resolves real keys; auth.json is read
once per registry sweep; caller-less lock_config_writes deleted; catalog
resolvers tightened to pub(crate); stale config.toml doc comments and the
no-credentials error copy updated.
This commit is contained in:
2026-07-21 01:18:46 -04:00
parent 99d99fb47a
commit c5ddaec71e
19 changed files with 731 additions and 375 deletions
+24 -28
View File
@@ -264,41 +264,35 @@ pub enum AuthMode {
/// from the welcome login picker. Esc returns to the picker (no quit).
ApiKeyEntry(PlatformLogin),
}
/// Open-platform API-key login target, selected from the welcome picker.
/// Mirrors the shell's `moonshot-cn` / `moonshot-ai` interactive auth
/// methods ([`kigi_shell::agent::auth_method`]).
/// API-key platform login target, selected from the welcome picker. Wraps a
/// non-OAuth registry platform ([`kigi_shell::models::PlatformId`]);
/// [`Self::from_method_id`] — the production entry point — guarantees the
/// non-OAuth invariant.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlatformLogin {
MoonshotCn,
MoonshotAi,
}
pub struct PlatformLogin(pub kigi_shell::models::PlatformId);
impl PlatformLogin {
/// The picker target behind an advertised ACP method id; `None` for every
/// non-moonshot method.
/// method that isn't an API-key registry platform.
pub fn from_method_id(id: &acp::AuthMethodId) -> Option<Self> {
match id.0.as_ref() {
kigi_shell::agent::auth_method::MOONSHOT_CN_METHOD_ID => Some(Self::MoonshotCn),
kigi_shell::agent::auth_method::MOONSHOT_AI_METHOD_ID => Some(Self::MoonshotAi),
_ => None,
}
kigi_shell::agent::auth_method::platform_for_method_id(id).map(Self)
}
/// The registry platform whose `[platforms.<id>]` table stores the key.
/// The registry platform whose auth.json scope stores the key.
pub fn platform_id(self) -> kigi_shell::models::PlatformId {
match self {
Self::MoonshotCn => kigi_shell::models::PlatformId::MoonshotCn,
Self::MoonshotAi => kigi_shell::models::PlatformId::MoonshotAi,
}
self.0
}
/// The ACP auth method id to `authenticate` with after persisting the key.
pub fn method_id(self) -> acp::AuthMethodId {
acp::AuthMethodId::new(self.platform_id().as_str())
}
/// Console host shown in the paste-box copy.
/// Vendor word for the paste-box copy ("Paste your {vendor} API key").
pub fn vendor(self) -> &'static str {
self.0.vendor()
}
/// Console host shown in the paste-box copy. Every API-key registry row
/// carries one (pinned by a kigi-models registry test); the fallback is
/// unreachable copy, not control flow.
pub fn console_host(self) -> &'static str {
match self {
Self::MoonshotCn => "platform.moonshot.cn",
Self::MoonshotAi => "platform.moonshot.ai",
}
self.0.console_host().unwrap_or("the provider console")
}
}
/// One row of the unauthenticated welcome menu (the login picker).
@@ -6909,14 +6903,14 @@ pub(crate) mod tests {
assert_eq!(
items[1],
PendingMenuItem::ApiKey {
target: PlatformLogin::MoonshotCn,
target: PlatformLogin(kigi_shell::models::PlatformId::MoonshotCn),
label: "Moonshot Open Platform (API key \u{b7} moonshot.cn)".into(),
}
);
assert_eq!(
items[2],
PendingMenuItem::ApiKey {
target: PlatformLogin::MoonshotAi,
target: PlatformLogin(kigi_shell::models::PlatformId::MoonshotAi),
label: "Moonshot Open Platform (API key \u{b7} moonshot.ai)".into(),
}
);
@@ -6960,7 +6954,9 @@ pub(crate) mod tests {
assert!(
matches!(
outcome,
InputOutcome::Action(Action::BeginPlatformKeyEntry(PlatformLogin::MoonshotCn))
InputOutcome::Action(Action::BeginPlatformKeyEntry(PlatformLogin(
kigi_shell::models::PlatformId::MoonshotCn
)))
),
"Enter on row 1 must open moonshot-cn key entry, got {outcome:?}"
);
@@ -6976,7 +6972,7 @@ pub(crate) mod tests {
request_seq: 1,
handle: None,
auth_url: None,
mode: AuthMode::ApiKeyEntry(PlatformLogin::MoonshotCn),
mode: AuthMode::ApiKeyEntry(PlatformLogin(kigi_shell::models::PlatformId::MoonshotCn)),
};
let outcome = app.handle_input(&key_event(KeyCode::Esc, KeyModifiers::NONE));
assert!(
@@ -6994,7 +6990,7 @@ pub(crate) mod tests {
request_seq: 1,
handle: None,
auth_url: None,
mode: AuthMode::ApiKeyEntry(PlatformLogin::MoonshotAi),
mode: AuthMode::ApiKeyEntry(PlatformLogin(kigi_shell::models::PlatformId::MoonshotAi)),
};
// Empty input: Enter is a no-op.
let outcome = app.handle_input(&key_event(KeyCode::Enter, KeyModifiers::NONE));