This commit is contained in:
2026-07-22 11:08:54 -04:00
parent 422e241e13
commit 2d00a4e6e6
19 changed files with 1955 additions and 296 deletions
@@ -1003,16 +1003,22 @@ fn resolve_model_override_to_config(
} else {
acp::ModelId::new(entry.info().model.clone())
};
// Resolve the child's session token by the OVERRIDE model's OWN platform,
// not the parent's primary auth: a grok (oauth-platform) override draws its
// pooled grok token or `None` — NEVER the primary Kimi session token (which
// `resolve_credentials` would otherwise stamp onto the child's api.x.ai
// credentials, leaking it in the logout-mid-session edge). A first-party /
// non-oauth override still resolves to the primary (byte-identical).
let managed_key = entry.info().id.as_deref().unwrap_or(model_id);
let session_key = crate::auth::oauth_registry::session_key_for_model(
&crate::util::kigi_home::kigi_home(),
managed_key,
// Resolve the child's session token by the OVERRIDE model's OWN platform
// AND endpoint, not the parent's primary auth: a grok (oauth-platform)
// override draws its pooled grok token or `None`, and an API-key registry
// platform / a third-party `[model.*]` host draws NOTHING — NEVER the
// primary Kimi session token, which `resolve_credentials` would otherwise
// stamp onto the child's api.x.ai / api.moonshot.cn credentials. The
// first-party subscription channel still resolves to the primary
// (byte-identical).
let session_key = crate::auth::oauth_registry::session_key_for_endpoint(
entry
.info()
.id
.as_deref()
.and_then(kigi_models::parse_managed_model_key)
.map(|(platform, _)| platform),
&entry.info().base_url,
Some(&ctx.auth_manager),
);
let has_session_key = session_key.is_some();
@@ -3131,19 +3131,25 @@ fn fresh_tool_model_rejects_unavailable_exact_key_over_visible_slug_collision()
"validation must inspect the unavailable exact-key entry selected by execution"
);
}
/// Validation must inspect the SAME slug-collision entry execution selects.
/// Both go through `find_model_by_id`, whose slug scan takes the LAST match —
/// aligned with the picker's `resolve_catalog_key` so the auth layer and the
/// picker can never resolve different platforms for one slug (the H5 collision).
/// So a blocked LAST entry must be rejected even though an available earlier one
/// shares the slug.
#[test]
fn fresh_tool_model_rejects_unavailable_first_slug_collision() {
fn fresh_tool_model_rejects_unavailable_last_slug_collision() {
let mut models = indexmap::IndexMap::new();
let mut unavailable_first = test_model_entry("shared-routing-slug");
unavailable_first.info.user_selectable = false;
models.insert("blocked-first".to_string(), unavailable_first);
models.insert("visible-second".to_string(), test_model_entry("shared-routing-slug"));
models.insert("visible-first".to_string(), test_model_entry("shared-routing-slug"));
let mut unavailable_last = test_model_entry("shared-routing-slug");
unavailable_last.info.user_selectable = false;
models.insert("blocked-last".to_string(), unavailable_last);
assert_eq!(
super::handle_request::task_model_override_error(Some("shared-routing-slug"),
ModelOverrideProvenance::Tool, false, & models, false,).as_deref(),
Some("Unknown Task.model slug 'shared-routing-slug'. Valid model slugs: \
visible-second. Omit `model` to inherit the parent model."),
"validation must inspect the first routing-slug entry selected by execution"
visible-first. Omit `model` to inherit the parent model."),
"validation must inspect the last routing-slug entry selected by execution"
);
}
#[test]
@@ -2511,14 +2511,48 @@ async fn subagent_override_grok_model_never_leaks_kimi_session_token() {
"a grok override must never receive the primary Kimi session token",
);
}
/// Byte-identical guard: a non-oauth override with a Kimi primary still resolves
/// to the primary session token — passes both before and after the fix (the
/// non-oauth path is unchanged).
/// Byte-identical guard: an override on the SESSION's own first-party endpoint
/// (the kimi-code subscription channel) still resolves to the primary session
/// token — the primary path is unchanged.
#[tokio::test]
async fn subagent_override_non_oauth_model_still_gets_primary_token() {
async fn subagent_override_first_party_model_still_gets_primary_token() {
let (_kd, manager) = kimi_primary_with_token("kimi-secret");
let mut entry = test_model_entry("kimi-for-coding");
entry.info.id = Some("kimi-code/kimi-for-coding".to_string());
entry.info.base_url = kigi_env::PRODUCTION_ENDPOINTS.coding_api_base_url.to_string();
let mut models = indexmap::IndexMap::new();
models.insert("kfc".to_string(), entry);
let mut ctx = ctx_with_toggle(HashMap::new());
ctx.available_models = models;
ctx.auth = Some(crate::auth::KimiAuth {
key: "kimi-secret".to_string(),
auth_mode: crate::auth::AuthMode::OAuth,
..crate::auth::KimiAuth::test_default()
});
ctx.auth_manager = manager;
let (config, _model_id) = resolve_model_override_to_config("kfc", &ctx)
.expect("first-party override resolves to a config");
assert_eq!(
config.api_key.as_deref(),
Some("kimi-secret"),
"a first-party override must still receive the primary session token",
);
}
/// LEAK guard (C1, subagent-override `api_key` channel): an API-key registry
/// platform override must NOT receive the parent's primary Kimi session token —
/// `resolve_credentials` would stamp it as the child's `api_key` on
/// `api.moonshot.cn`. This test previously asserted the opposite
/// (`subagent_override_non_oauth_model_still_gets_primary_token`), which encoded
/// the defect.
///
/// Revert-to-red: dropping the `platform_takes_session_credential` term from
/// `oauth_registry::session_key_for_endpoint` makes `api_key` `Some("kimi-secret")`.
#[tokio::test]
async fn subagent_override_api_key_platform_never_gets_the_primary_token() {
let (_kd, manager) = kimi_primary_with_token("kimi-secret");
let mut entry = test_model_entry("kimi-k2-0905-preview");
entry.info.id = Some("moonshot-cn/kimi-k2".to_string());
entry.info.base_url = "https://api.moonshot.cn/v1".to_string();
let mut models = indexmap::IndexMap::new();
models.insert("k2".to_string(), entry);
let mut ctx = ctx_with_toggle(HashMap::new());
@@ -2529,12 +2563,12 @@ async fn subagent_override_non_oauth_model_still_gets_primary_token() {
..crate::auth::KimiAuth::test_default()
});
ctx.auth_manager = manager;
let (config, _model_id) =
resolve_model_override_to_config("k2", &ctx).expect("non-oauth override resolves to a config");
assert_eq!(
let (config, _model_id) = resolve_model_override_to_config("k2", &ctx)
.expect("an API-key-platform override still resolves to a config");
assert_ne!(
config.api_key.as_deref(),
Some("kimi-secret"),
"a non-oauth override must still receive the primary session token",
"LEAK: an API-key-platform override must never receive the primary Kimi session token",
);
}
/// An unresolvable `AgentDefinition.model` pin (model absent from