From ed8049cf771a84d71b655a16a576174eca36951f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 26 Jul 2026 23:41:45 -0400 Subject: [PATCH] fix(memory): scope embedding credentials to the endpoint that may receive them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MemoryBackendParams` carried the primary session `AuthManager` and the session api-key provider unconditionally, while `embed_base_url` is the CURRENT MODEL's endpoint and `AuthRetryMiddleware` stamps `Authorization` on every request it wraps. A user who enables `[memory.embedding] model` while running a BYOK or subscription-OAuth model therefore sent the Kimi session bearer to that third party. `EndpointScopedCredentials` binds the credential to the one endpoint it may reach: `for_endpoint` drops the handle unless the caller vouches for the URL, and `approved_for` re-checks at provider-build time in release too, because `MemoryBackendParams` is `Clone` and callers rewrite fields on the copy. The shell decides through `CredentialAuthority::manager_for` rather than a second URL predicate — it answers both whether a credential may ride and which manager governs it, so a subscription-OAuth platform gets its own pooled manager. The session's `SharedApiKeyProvider` is not forwarded at all: it is hard-wired to the primary manager, so at a pooled platform's host it would resolve the wrong bearer. A platform's own `embed_api_key` is untouched and keeps serving its own endpoint. The background reindex built a second provider straight from `ApiEmbeddingProvider::from_session`, outside the chokepoint and without 401 refresh; it now embeds through the session's own params. Test strength verified by mutation: with the guard reverted, exactly `session_credentials_are_withheld_from_a_foreign_endpoint` and `a_cloned_param_set_cannot_redirect_scoped_credentials` fail. --- crates/codegen/kigi-memory/src/backend.rs | 280 +++++++++++++----- crates/codegen/kigi-memory/src/lib.rs | 2 +- .../src/auth/credential_provider.rs | 61 ++++ .../src/session/acp_session_impl/spawn.rs | 43 +-- .../cancel_running_task_tests.rs | 3 +- .../inline_auto_compact_flow_tests.rs | 6 +- .../acp_session_tests/memory_config_tests.rs | 9 +- 7 files changed, 295 insertions(+), 109 deletions(-) diff --git a/crates/codegen/kigi-memory/src/backend.rs b/crates/codegen/kigi-memory/src/backend.rs index e2b464b..6b276be 100644 --- a/crates/codegen/kigi-memory/src/backend.rs +++ b/crates/codegen/kigi-memory/src/backend.rs @@ -18,6 +18,76 @@ use super::embedding::EmbeddingProvider as _; use super::storage::MemoryStorage; use super::watcher::MemoryFileWatcher; +/// The session's embedding credentials, bound to the one endpoint they may +/// reach. Only [`Self::for_endpoint`] retains a live handle; the default fails +/// closed. +/// +/// `embed_base_url` is the CURRENT MODEL's endpoint, so a session on a BYOK or +/// subscription-OAuth model aims memory embeddings at that provider's host — +/// and [`kigi_auth::AuthRetryMiddleware`] stamps `Authorization` on every +/// request it wraps, with no idea where the request is going. The caller that +/// owns the credential rule decides `trusted` once, here; a platform's own +/// `embed_api_key` is unaffected and keeps serving its own endpoint. +#[derive(Clone, Default)] +pub struct EndpointScopedCredentials { + endpoint: Option, + auth_credentials: Option>, +} + +// Redacts the credential handles; only their presence is printable. +impl std::fmt::Debug for EndpointScopedCredentials { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EndpointScopedCredentials") + .field("endpoint", &self.endpoint) + .field("has_auth_credentials", &self.auth_credentials.is_some()) + .finish() + } +} + +impl EndpointScopedCredentials { + /// No session credential may ride — the state every non-session caller wants. + pub fn none() -> Self { + Self::default() + } + + /// Retains the handles only for a `trusted`, parsable `endpoint`. + pub fn for_endpoint( + endpoint: &str, + trusted: bool, + auth_credentials: Option>, + ) -> Self { + if trusted && let Ok(url) = reqwest::Url::parse(endpoint) { + return Self { + endpoint: Some(url), + auth_credentials, + }; + } + if auth_credentials.is_some() { + tracing::info!( + target: kigi_log::memory_log::TARGET, + endpoint, + "memory embeddings: session credentials withheld from this endpoint; \ + its own key, if any, still applies" + ); + } + Self::none() + } + + pub fn is_empty(&self) -> bool { + self.auth_credentials.is_none() + } + + /// Enforced at request-build time, in release too: [`MemoryBackendParams`] + /// is `Clone` and callers rewrite fields on the copy, so construction-time + /// scoping alone would not survive a rewritten `embed_base_url`. + fn approved_for(&self, base_url: &str) -> bool { + match &self.endpoint { + None => self.is_empty(), + Some(endpoint) => reqwest::Url::parse(base_url).is_ok_and(|url| &url == endpoint), + } + } +} + /// All configuration needed to build a fully-wired [`MemoryBackendImpl`] for a live session. /// /// Grouping these in one struct ensures every call site — ToolBridge, first-turn @@ -30,7 +100,8 @@ pub struct MemoryBackendParams { pub session_id: String, /// Embedding provider config — `None` forces FTS-only fallback everywhere. pub embed_config: Option, - /// Base URL for embedding API calls (CLI proxy). + /// Base URL for embedding API calls (CLI proxy). Must match the endpoint + /// `embedding_credentials` was scoped to; a mismatch fails closed. pub embed_base_url: String, /// API key for embedding API calls. pub embed_api_key: Option, @@ -47,10 +118,8 @@ pub struct MemoryBackendParams { /// - `"injection"` — first-turn memory context injection /// - `"compaction_recovery"` — post-compaction context re-injection pub search_source: &'static str, - /// Dynamic API key provider — when set, `make_embedding_provider()` resolves - /// the key per-call instead of using the static `embed_api_key`. - pub api_key_provider: Option, - pub auth_credentials: Option>, + /// The session credentials, and the single endpoint they may reach. + pub embedding_credentials: EndpointScopedCredentials, } impl MemoryBackendParams { @@ -59,8 +128,7 @@ impl MemoryBackendParams { pub async fn make_embedding_provider(&self) -> Option { build_embedding_provider( self.embed_config.as_ref(), - self.auth_credentials.as_ref(), - self.api_key_provider.as_ref(), + &self.embedding_credentials, self.embed_api_key.as_deref(), &self.embed_base_url, ) @@ -70,8 +138,7 @@ impl MemoryBackendParams { async fn build_embedding_provider( config: Option<&kigi_config_types::MemoryEmbeddingConfig>, - auth_credentials: Option<&Arc>, - api_key_provider: Option<&kigi_tools::types::SharedApiKeyProvider>, + credentials: &EndpointScopedCredentials, static_api_key: Option<&str>, base_url: &str, ) -> Option { @@ -80,9 +147,19 @@ async fn build_embedding_provider( return None; } + let approved = credentials.approved_for(base_url); + if !approved { + tracing::error!( + target: kigi_log::memory_log::TARGET, + base_url, + approved_endpoint = ?credentials.endpoint, + "memory embeddings: scoped credentials do not match the request URL; dropping them" + ); + } + // Prefer the refresh-capable credential provider — the middleware gives // 401 retry for free without any per-call key resolution. - if let Some(creds) = auth_credentials { + if approved && let Some(creds) = credentials.auth_credentials.as_ref() { let client = super::embedding::build_middleware_client(creds.clone()); return super::embedding::ApiEmbeddingProvider::from_config( config, @@ -91,14 +168,13 @@ async fn build_embedding_provider( ); } - // Fallback: resolve API key per-call, wrap in a static middleware client - // (no 401 refresh, but auth header is still stamped by middleware). - let api_key = match api_key_provider { - Some(p) => p.current_api_key_async().await, - None => None, - } - .or_else(|| static_api_key.map(|s| s.to_owned()))?; - super::embedding::ApiEmbeddingProvider::from_session(config, base_url.to_owned(), api_key) + // The platform's own configured key, wrapped in a static middleware client + // (no 401 refresh, but the auth header is still stamped by middleware). + super::embedding::ApiEmbeddingProvider::from_session( + config, + base_url.to_owned(), + static_api_key?.to_owned(), + ) } /// `MemoryBackend` implementation backed by hybrid search (FTS5 + vector KNN). @@ -129,10 +205,8 @@ pub struct MemoryBackendImpl { /// Only the ToolBridge backend's counter is shared back to the session actor; /// injection and compaction-recovery backends use their own local counters. pub search_counter: std::sync::Arc, - /// Dynamic API key provider for embedding requests. - api_key_provider: Option, - /// Refresh-capable credential provider for embedding HTTP middleware. - auth_credentials: Option>, + /// The session credentials, and the single endpoint they may reach. + embedding_credentials: EndpointScopedCredentials, } impl MemoryBackendImpl { @@ -150,8 +224,7 @@ impl MemoryBackendImpl { stale_claim_secs: 60, session_id: String::new(), search_source: "tool", - api_key_provider: None, - auth_credentials: None, + embedding_credentials: EndpointScopedCredentials::none(), search_counter: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)), } } @@ -200,8 +273,7 @@ impl MemoryBackendImpl { async fn make_embedding_provider(&self) -> Option { build_embedding_provider( self.embed_config.as_ref(), - self.auth_credentials.as_ref(), - self.api_key_provider.as_ref(), + &self.embedding_credentials, self.embed_api_key.as_deref(), &self.embed_base_url, ) @@ -232,8 +304,7 @@ impl MemoryBackendImpl { if let Some(w) = ¶ms.watcher { backend = backend.with_watcher(w.clone(), params.stale_claim_secs); } - backend.api_key_provider = params.api_key_provider.clone(); - backend.auth_credentials = params.auth_credentials.clone(); + backend.embedding_credentials = params.embedding_credentials.clone(); backend } } @@ -479,8 +550,7 @@ mod factory_tests { watcher: None, stale_claim_secs: 60, search_source: "tool", - api_key_provider: None, - auth_credentials: None, + embedding_credentials: EndpointScopedCredentials::none(), } } @@ -1038,72 +1108,128 @@ mod factory_tests { ); } - /// Regression: provider build must use `current_api_key_async`, - /// never sync. Prevents memory_search 401s on rotated tokens. - #[tokio::test] - async fn make_embedding_provider_uses_async_api_key_resolution() { - use kigi_tools::types::ApiKeyProvider; - use std::sync::atomic::{AtomicU32, Ordering}; - - struct AsyncProbe { - sync_calls: Arc, - async_calls: Arc, + struct ProbeCredentials; + impl kigi_auth::HttpAuth for ProbeCredentials { + fn apply( + &self, + builder: reqwest::RequestBuilder, + _base_url: &str, + ) -> reqwest::RequestBuilder { + builder.bearer_auth("session-bearer") } - impl ApiKeyProvider for AsyncProbe { - fn current_api_key(&self) -> Option { - self.sync_calls.fetch_add(1, Ordering::SeqCst); - Some("sync-stale".into()) - } - fn current_api_key_async( - &self, - ) -> std::pin::Pin> + Send + '_>> - { - let counter = self.async_calls.clone(); - Box::pin(async move { - counter.fetch_add(1, Ordering::SeqCst); - Some("async-fresh".into()) - }) + } + #[async_trait::async_trait] + impl kigi_auth::AuthCredentialProvider for ProbeCredentials { + fn snapshot(&self) -> kigi_auth::CredentialSnapshot { + kigi_auth::CredentialSnapshot { + token: Some("session-bearer".into()), + ..Default::default() } } + async fn refresh_after_unauthorized(&self) -> bool { + false + } + } - let sync_calls = Arc::new(AtomicU32::new(0)); - let async_calls = Arc::new(AtomicU32::new(0)); - let probe: kigi_tools::types::SharedApiKeyProvider = Arc::new(AsyncProbe { - sync_calls: sync_calls.clone(), - async_calls: async_calls.clone(), - }); + const SESSION_ENDPOINT: &str = "https://api.kimi.com/coding/v1"; + const FOREIGN_ENDPOINT: &str = "https://api.anthropic.com/v1"; - let params = MemoryBackendParams { + fn params_at(base_url: &str, credentials: EndpointScopedCredentials) -> MemoryBackendParams { + MemoryBackendParams { session_id: "s1".into(), embed_config: Some(MemoryEmbeddingConfig { model: Some("test-embed-model".into()), ..Default::default() }), - embed_base_url: "http://example/v1".into(), - embed_api_key: Some("static-fallback".into()), + embed_base_url: base_url.into(), + embed_api_key: None, search_config: MemorySearchConfig::default(), watcher: None, stale_claim_secs: 60, search_source: "tool", - api_key_provider: Some(probe), - // No auth_credentials — forces the api_key_provider fallback path. - auth_credentials: None, - }; + embedding_credentials: credentials, + } + } - let provider = params.make_embedding_provider().await; + /// The session bearer must never ride to a third-party embedding host. + /// + /// `embed_base_url` is the CURRENT MODEL's endpoint, so a session on a BYOK + /// or subscription-OAuth model aims memory embeddings at that provider — + /// and the auth middleware stamps `Authorization` unconditionally. + #[tokio::test] + async fn session_credentials_are_withheld_from_a_foreign_endpoint() { + let scoped = EndpointScopedCredentials::for_endpoint( + FOREIGN_ENDPOINT, + false, + Some(Arc::new(ProbeCredentials)), + ); + assert!( + scoped.is_empty(), + "an untrusted endpoint must drop both handles" + ); + + let provider = params_at(FOREIGN_ENDPOINT, scoped) + .make_embedding_provider() + .await; + assert!( + provider.is_none(), + "no credential may ride to a foreign endpoint, and there is no static key to fall back to" + ); + } + + /// The trusted-endpoint path still builds a credentialed provider. + #[tokio::test] + async fn session_credentials_ride_their_own_endpoint() { + let scoped = EndpointScopedCredentials::for_endpoint( + SESSION_ENDPOINT, + true, + Some(Arc::new(ProbeCredentials)), + ); + assert!( + !scoped.is_empty(), + "a trusted endpoint must retain the handles" + ); + + let provider = params_at(SESSION_ENDPOINT, scoped) + .make_embedding_provider() + .await; assert!( provider.is_some(), - "provider must be built when model is set" + "the session endpoint keeps its refresh-capable credential" ); - assert_eq!( - async_calls.load(Ordering::SeqCst), - 1, - "must call current_api_key_async exactly once per provider build" + } + + /// The runtime re-check, not construction alone, is what guards the wire: + /// `MemoryBackendParams` is `Clone` and callers rewrite fields on the copy. + #[tokio::test] + async fn a_cloned_param_set_cannot_redirect_scoped_credentials() { + let scoped = EndpointScopedCredentials::for_endpoint( + SESSION_ENDPOINT, + true, + Some(Arc::new(ProbeCredentials)), ); - assert_eq!( - sync_calls.load(Ordering::SeqCst), - 0, - "sync current_api_key must NOT be called — the async path is the contract" + let redirected = MemoryBackendParams { + embed_base_url: FOREIGN_ENDPOINT.into(), + ..params_at(SESSION_ENDPOINT, scoped) + }; + + assert!( + redirected.make_embedding_provider().await.is_none(), + "credentials scoped to one endpoint must not follow a rewritten base_url" + ); + } + + /// A platform's own API key is not a session credential: it is resolved for + /// that platform and must keep serving that platform's endpoint. + #[tokio::test] + async fn a_platform_api_key_still_serves_its_own_endpoint() { + let params = MemoryBackendParams { + embed_api_key: Some("platform-key".into()), + ..params_at(FOREIGN_ENDPOINT, EndpointScopedCredentials::none()) + }; + assert!( + params.make_embedding_provider().await.is_some(), + "withholding the session credential must not disable BYOK embeddings" ); } } diff --git a/crates/codegen/kigi-memory/src/lib.rs b/crates/codegen/kigi-memory/src/lib.rs index e0f5644..cb10e99 100644 --- a/crates/codegen/kigi-memory/src/lib.rs +++ b/crates/codegen/kigi-memory/src/lib.rs @@ -31,7 +31,7 @@ pub mod storage; pub mod text_utils; pub mod watcher; -pub use backend::{MemoryBackendImpl, MemoryBackendParams}; +pub use backend::{EndpointScopedCredentials, MemoryBackendImpl, MemoryBackendParams}; pub use index::{MemoryIndex, init_sqlite_vec}; pub use storage::{MemoryScope, MemoryStorage}; diff --git a/crates/codegen/kigi-shell/src/auth/credential_provider.rs b/crates/codegen/kigi-shell/src/auth/credential_provider.rs index b8e5a52..7afb03e 100644 --- a/crates/codegen/kigi-shell/src/auth/credential_provider.rs +++ b/crates/codegen/kigi-shell/src/auth/credential_provider.rs @@ -83,6 +83,35 @@ impl AuthCredentialProvider for ShellAuthCredentialProvider { self.auth_manager.try_recover_unauthorized().await } } +/// The memory-embedding credentials for `embed_base_url`, decided by the ONE +/// authority rather than re-derived here (C1). +/// +/// `embed_base_url` is the session model's own endpoint, so on a BYOK or +/// subscription-OAuth model it points at that provider's host — and +/// [`kigi_auth::AuthRetryMiddleware`] stamps `Authorization` on every request +/// it wraps. Asking [`CredentialAuthority::manager_for`] answers both halves at +/// once: whether a session credential may ride there at all, and WHICH manager +/// governs it (a subscription-OAuth platform's pooled manager at its own host, +/// the primary at the session's coding endpoint). No manager means no session +/// credential at all; the platform's own `embed_api_key` is untouched and keeps +/// serving its own endpoint. +/// +/// The session's `SharedApiKeyProvider` is deliberately NOT forwarded: it is +/// hard-wired to the PRIMARY manager, so at a pooled platform's host — where a +/// credential may ride, but only that platform's own — it would resolve the +/// wrong bearer. +pub(crate) fn embedding_session_credentials( + embed_base_url: &str, + platform: Option, + authority: &crate::auth::credential_authority::CredentialAuthority, +) -> kigi_memory::EndpointScopedCredentials { + let auth_credentials = authority.manager_for(platform, embed_base_url).map(|am| { + Arc::new(ShellAuthCredentialProvider::new(am, None, None)) + as Arc + }); + let may_ride = auth_credentials.is_some(); + kigi_memory::EndpointScopedCredentials::for_endpoint(embed_base_url, may_ride, auth_credentials) +} #[cfg(test)] mod tests { use super::*; @@ -253,6 +282,38 @@ mod tests { "snapshot must reflect refreshed token for subsequent apply() calls" ); } + /// C1: memory embeddings follow the credential authority, not the session. + /// + /// A session whose model is a BYOK platform aims `embed_base_url` at that + /// provider's host; the authority answers "no manager governs a credential + /// there", so nothing rides. The session's own coding endpoint still does. + #[test] + fn embedding_credentials_follow_the_credential_authority() { + let _guard = EarlyInvalidationGuard::pin_to_default(); + let dir = tempfile::tempdir().unwrap(); + let mgr = make_manager( + &dir, + Some(make_auth("session-bearer", ChronoDuration::hours(1))), + ); + let endpoints = crate::agent::config::EndpointsConfig::default(); + let coding_endpoint = endpoints.proxy_url(); + let authority = + crate::auth::credential_authority::CredentialAuthority::new(endpoints, Some(mgr)); + + assert!( + !embedding_session_credentials(&coding_endpoint, None, &authority).is_empty(), + "the session's own coding endpoint keeps its credential" + ); + assert!( + embedding_session_credentials( + "https://api.anthropic.com/v1", + kigi_models::PlatformId::parse("anthropic"), + &authority, + ) + .is_empty(), + "an API-key platform's host must receive no session credential" + ); + } /// Deployment-key path has no recovery (operator owns the bearer). #[tokio::test] async fn refresh_after_unauthorized_is_noop_for_deployment_key() { diff --git a/crates/codegen/kigi-shell/src/session/acp_session_impl/spawn.rs b/crates/codegen/kigi-shell/src/session/acp_session_impl/spawn.rs index d3617a5..468760e 100644 --- a/crates/codegen/kigi-shell/src/session/acp_session_impl/spawn.rs +++ b/crates/codegen/kigi-shell/src/session/acp_session_impl/spawn.rs @@ -311,6 +311,18 @@ pub(crate) async fn spawn_session_actor( }; let embed_base_url = sampling_config.base_url.clone(); let embed_api_key = sampling_config.api_key.clone(); + // The platform behind the endpoint memory embeddings will call, resolved + // through the SAME session-key disambiguation the actor is seeded with, so + // a slug that collides across platforms cannot resolve to the twin (H-b). + let embed_platform = { + let models = models_manager.models(); + crate::agent::models::platform_for_slug( + &models, + crate::agent::models::selected_catalog_key_for_spawn(&models, &session_model_id) + .as_deref(), + &sampling_config.model, + ) + }; let session_pruning_config: crate::config::PruningConfig = memory_config.as_ref().map_or_else( || crate::config::PruningConfig { enabled: false, @@ -618,16 +630,11 @@ pub(crate) async fn spawn_session_actor( watcher, stale_claim_secs: watcher_config.stale_claim_secs, search_source: "tool", - api_key_provider: api_key_provider.clone(), - auth_credentials: auth_manager.as_ref().map(|am| { - std::sync::Arc::new( - crate::auth::credential_provider::ShellAuthCredentialProvider::new( - am.clone(), - None, - None, - ), - ) as std::sync::Arc - }), + embedding_credentials: crate::auth::credential_provider::embedding_session_credentials( + &embed_base_url, + embed_platform, + &models_manager.credential_authority(), + ), }; let backend = crate::session::memory::MemoryBackendImpl::from_session_params( storage.clone(), @@ -1287,8 +1294,11 @@ pub(crate) async fn spawn_session_actor( .map(|mc| mc.embedding.clone()) .unwrap_or_default(); let embed_dims = embed_config.dimensions; - let sampling_base_url = embed_base_url.clone(); - let sampling_api_key = embed_api_key.clone(); + // The session's own params, so the background reindex embeds through + // the same endpoint-scoped credential as every foreground path — a + // second locally-built provider would re-derive credentials outside + // the chokepoint and would carry a static key with no 401 refresh. + let reindex_params = session.memory.backend_params.clone(); let session_id_for_reindex = session_info.id.to_string(); let chunks_added_counter = session.memory.chunks_added.clone(); tokio::task::spawn_local(async move { @@ -1311,13 +1321,8 @@ pub(crate) async fn spawn_session_actor( target : kigi_log::memory_log::TARGET, files = files.len(), "MEMORY_REINDEX: background reindex complete" ); - if let Some(api_key) = sampling_api_key - && let Some(provider) = - crate::session::memory::embedding::ApiEmbeddingProvider::from_session( - &embed_config, - sampling_base_url, - api_key, - ) + if let Some(ref params) = reindex_params + && let Some(provider) = params.make_embedding_provider().await { crate::session::memory::embed_missing_chunks(&index, &provider).await; } diff --git a/crates/codegen/kigi-shell/src/session/acp_session_tests/cancel_running_task_tests.rs b/crates/codegen/kigi-shell/src/session/acp_session_tests/cancel_running_task_tests.rs index be4e93c..00a6a25 100644 --- a/crates/codegen/kigi-shell/src/session/acp_session_tests/cancel_running_task_tests.rs +++ b/crates/codegen/kigi-shell/src/session/acp_session_tests/cancel_running_task_tests.rs @@ -555,8 +555,7 @@ async fn first_turn_memory_injection_disabled_does_not_persist_to_chat_history() watcher: None, stale_claim_secs: 60, search_source: "tool", - api_key_provider: None, - auth_credentials: None, + embedding_credentials: kigi_memory::EndpointScopedCredentials::none(), }; let (event_tx, _event_rx) = tokio::sync::mpsc::unbounded_channel::(); let actor = Arc::new(SessionActor { diff --git a/crates/codegen/kigi-shell/src/session/acp_session_tests/inline_auto_compact_flow_tests.rs b/crates/codegen/kigi-shell/src/session/acp_session_tests/inline_auto_compact_flow_tests.rs index 000a923..5073d8b 100644 --- a/crates/codegen/kigi-shell/src/session/acp_session_tests/inline_auto_compact_flow_tests.rs +++ b/crates/codegen/kigi-shell/src/session/acp_session_tests/inline_auto_compact_flow_tests.rs @@ -393,8 +393,7 @@ fn initial_injection_backend_params_use_override_min_score() { watcher: None, stale_claim_secs: 60, search_source: "tool", - api_key_provider: None, - auth_credentials: None, + embedding_credentials: kigi_memory::EndpointScopedCredentials::none(), }; let initial_injection = crate::config::MemoryInitialInjectionConfig { enabled: true, @@ -422,8 +421,7 @@ fn initial_injection_backend_params_preserve_default_zero_min_score() { watcher: None, stale_claim_secs: 60, search_source: "tool", - api_key_provider: None, - auth_credentials: None, + embedding_credentials: kigi_memory::EndpointScopedCredentials::none(), }; let (adjusted, effective_min_score) = build_initial_injection_backend_params( ¶ms, diff --git a/crates/codegen/kigi-shell/src/session/acp_session_tests/memory_config_tests.rs b/crates/codegen/kigi-shell/src/session/acp_session_tests/memory_config_tests.rs index e31587c..1d79aaa 100644 --- a/crates/codegen/kigi-shell/src/session/acp_session_tests/memory_config_tests.rs +++ b/crates/codegen/kigi-shell/src/session/acp_session_tests/memory_config_tests.rs @@ -18,8 +18,7 @@ fn initial_injection_backend_params_use_override_min_score() { watcher: None, stale_claim_secs: 60, search_source: "tool", - api_key_provider: None, - auth_credentials: None, + embedding_credentials: kigi_memory::EndpointScopedCredentials::none(), }; let initial_injection = crate::config::MemoryInitialInjectionConfig { enabled: true, @@ -47,8 +46,7 @@ fn initial_injection_backend_params_preserve_default_zero_min_score() { watcher: None, stale_claim_secs: 60, search_source: "tool", - api_key_provider: None, - auth_credentials: None, + embedding_credentials: kigi_memory::EndpointScopedCredentials::none(), }; let (adjusted, effective_min_score) = build_initial_injection_backend_params( ¶ms, @@ -524,8 +522,7 @@ async fn create_injection_ready_actor( watcher: None, stale_claim_secs: 60, search_source: "tool", - api_key_provider: None, - auth_credentials: None, + embedding_credentials: kigi_memory::EndpointScopedCredentials::none(), }); actor .chat_state_handle