refactor(rag): discover driver_config secrets by grammar, not field name
Sandbox provisioning only ever looked at driver_config["api_key"], so a driver
whose credential is called anything else would have been silently unprovisioned
inside a sandbox. It now scans every driver_config value and treats any that is
a secret placeholder as a credential, which is the same rule resolve_driver_config
already used at point of use.
The first one binds to the RAG's own service id, which is what the generated
mixin declares; any others register under their own names, as MCP secrets do.
The mixin still carries a single credential entry, so a driver needing two bound
secrets remains a follow-up.
Also drops the placeholder parser added in 74bc613. crate::vault::SECRET_RE is
already the canonical definition and was already imported here, so that was a
third implementation of the same grammar. Requiring the whole value to match is
what keeps a literal key from being read as a secret name and printed.
The api_key check is gone from RagData::validate: a generic config validator
should not know a provider's field names.
This commit is contained in:
@@ -1582,18 +1582,6 @@ impl RagData {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(api_key) = self.driver_config.get("api_key")
|
||||
&& placeholder_secret_name(api_key).is_none()
|
||||
{
|
||||
bail!(
|
||||
"driver_config.api_key must be a secret placeholder of the form \
|
||||
'{{{{NAME}}}}', not a literal key. Store the credential with \
|
||||
`coyote --add-secret <NAME>` and reference it by name; a literal \
|
||||
key would be written to this RAG's YAML in plaintext and cannot \
|
||||
be provisioned into the sandbox."
|
||||
);
|
||||
}
|
||||
|
||||
match (self.driver.as_str(), self.attached) {
|
||||
("yaml", false) => Ok(()),
|
||||
("duckdb", false) => Ok(()),
|
||||
@@ -2203,21 +2191,6 @@ fn resolve_driver_config(
|
||||
})
|
||||
}
|
||||
|
||||
/// The secret NAME inside a `{{NAME}}` placeholder, or `None` for anything else.
|
||||
///
|
||||
/// Deliberately strict, and shared with sandbox provisioning so both agree on
|
||||
/// what a placeholder is. A RAG's `driver_config.api_key` is supposed to hold a
|
||||
/// placeholder, never a credential, but nothing stops a hand-edited or older
|
||||
/// config from holding the literal key. Consumers report failures *by name*, so
|
||||
/// treating a literal value as a name leaks the credential into stderr and logs.
|
||||
pub(crate) fn placeholder_secret_name(value: &str) -> Option<&str> {
|
||||
let inner = value.trim().strip_prefix("{{")?.strip_suffix("}}")?.trim();
|
||||
if inner.is_empty() || inner.contains(['{', '}']) {
|
||||
return None;
|
||||
}
|
||||
Some(inner)
|
||||
}
|
||||
|
||||
/// Interpolation core, taking the resolver as an argument so it can be exercised
|
||||
/// without a vault. Mirrors `interpolate_secrets` / `interpolate_secrets_with`.
|
||||
fn resolve_driver_config_with<F>(
|
||||
@@ -3280,64 +3253,6 @@ vectors: {}
|
||||
assert!(data.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ragdata_validate_rejects_a_literal_api_key() {
|
||||
let mut data = RagData::new(
|
||||
"m".into(),
|
||||
1024,
|
||||
50,
|
||||
None,
|
||||
5,
|
||||
None,
|
||||
GraphRagConfig::default(),
|
||||
);
|
||||
data.driver = "qdrant".to_string();
|
||||
data.attached = true;
|
||||
data.driver_config
|
||||
.insert("api_key".to_string(), "sk-a-real-looking-key".to_string());
|
||||
|
||||
let err = data.validate().unwrap_err().to_string();
|
||||
|
||||
assert!(err.contains("must be a secret placeholder"), "got: {err}");
|
||||
assert!(
|
||||
!err.contains("sk-a-real-looking-key"),
|
||||
"the error must never echo the credential back: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ragdata_validate_accepts_a_placeholder_api_key() {
|
||||
let mut data = RagData::new(
|
||||
"m".into(),
|
||||
1024,
|
||||
50,
|
||||
None,
|
||||
5,
|
||||
None,
|
||||
GraphRagConfig::default(),
|
||||
);
|
||||
data.driver = "qdrant".to_string();
|
||||
data.attached = true;
|
||||
data.driver_config
|
||||
.insert("api_key".to_string(), "{{QDRANT_KEY}}".to_string());
|
||||
|
||||
assert!(data.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_secret_name_accepts_only_well_formed_placeholders() {
|
||||
assert_eq!(placeholder_secret_name("{{NAME}}"), Some("NAME"));
|
||||
assert_eq!(placeholder_secret_name(" {{ NAME }} "), Some("NAME"));
|
||||
assert_eq!(placeholder_secret_name("sk-literal-key"), None);
|
||||
assert_eq!(placeholder_secret_name(""), None);
|
||||
assert_eq!(placeholder_secret_name("{{}}"), None);
|
||||
assert_eq!(placeholder_secret_name("{{ }}"), None);
|
||||
assert_eq!(placeholder_secret_name("{{A}}{{B}}"), None);
|
||||
assert_eq!(placeholder_secret_name("prefix{{NAME}}"), None);
|
||||
assert_eq!(placeholder_secret_name("{{NAME"), None);
|
||||
assert_eq!(placeholder_secret_name("NAME}}"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ragdata_validate_rejects_zero_top_k_from_a_truncated_yaml() {
|
||||
let yaml = "
|
||||
|
||||
Reference in New Issue
Block a user