From 93a934439b2f174a9526f5984cba1e34f0b6aeb3 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 11 Aug 2026 13:44:00 -0600 Subject: [PATCH] fix(rag): fail loudly when a RAG's vault secret is missing `interpolate_secrets` does not error on a secret the vault cannot resolve: it substitutes the empty string and returns the name in its second tuple element. `load_async` discarded that vec, so a typo'd or deleted vault secret produced `api_key = ""` and an unexplained 401 from Qdrant. Bail instead, naming the RAG and the missing secrets, matching what global config loading already does. --- src/rag/mod.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/rag/mod.rs b/src/rag/mod.rs index dc20877..032830d 100644 --- a/src/rag/mod.rs +++ b/src/rag/mod.rs @@ -377,10 +377,22 @@ impl Rag { let api_key: Option = match data.driver_config.get("api_key") { Some(placeholder) => { - let (resolved, _) = - interpolate_secrets(placeholder, vault).with_context(|| { + let (resolved, missing) = interpolate_secrets(placeholder, vault) + .with_context(|| { format!("Failed to resolve api_key secret for RAG '{name}'") })?; + // A secret the vault does not hold is NOT an error inside + // `interpolate_secrets`: it substitutes an empty string and + // only reports the name. Accepting that silently attaches with + // `api_key = ""`, and the user sees an unexplained 401 from the + // server instead of the typo they made. + if !missing.is_empty() { + bail!( + "RAG '{name}' references secrets that are missing from the vault: {}. \ + Add them with `coyote --add-secret `, then try again.", + missing.join(", ") + ); + } Some(resolved) } None => None,