update
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user