feat(rag): add attach-only Qdrant provider, attach wizard and sandbox wiring
Adds QdrantProvider as a read-only driver for pre-existing remote Qdrant collections, an interactive '.rag attach' wizard, and the sandbox credential and domain-whitelisting wiring that lets an attached RAG work inside a sandbox. Attach-only by design: rebuild_indexes bails for both the attached and the unattached case rather than silently succeeding. Coyote never writes to Qdrant in this change. Vectors are never hydrated back from Qdrant. Cosine collections L2-normalize stored vectors on write, so reading them back returns unit-length copies of the originals; the YAML vector copy is authoritative and the serialization guard stays scoped to the duckdb driver alone. Collections keyed by string or UUID point IDs are rejected at attach time. The read path parses point ids as u64 inside a filter_map, so such a collection would otherwise yield zero results with no error. The three response-shape parsers are pure functions over an already-parsed JSON body, unit-tested against captured fixtures, with the async wrappers delegating to them rather than duplicating the logic. '.rag' now splits its first argument, so '.rag attach <name>' no longer tries to load a RAG literally named 'attach <name>'.
This commit is contained in:
@@ -16,6 +16,7 @@ use crate::config::AppConfig;
|
||||
use crate::config::Config;
|
||||
use crate::config::VAULT_DATA_FILE_NAME;
|
||||
use crate::config::paths;
|
||||
use crate::rag::RagData;
|
||||
use crate::sandbox::mixins::DiscoveredMixin;
|
||||
use crate::utils::run_command_with_output;
|
||||
use crate::vault::SECRET_RE;
|
||||
@@ -51,6 +52,7 @@ pub fn launch(name: Option<String>, fresh: bool) -> Result<()> {
|
||||
inject_llm_secret(&config_content, &vault, ®istered)?;
|
||||
if !fresh {
|
||||
inject_mcp_secrets(&vault, ®istered)?;
|
||||
inject_rag_secrets(&vault, ®istered)?;
|
||||
}
|
||||
|
||||
let discovered = mixins::discover()?;
|
||||
@@ -301,6 +303,65 @@ fn inject_mcp_secrets(vault: &Vault, registered: &HashSet<String>) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Registers the API key of every attached RAG with the sbx proxy.
|
||||
///
|
||||
/// `launch()` has no notion of an active RAG — that is runtime state set by
|
||||
/// `--rag` / `.rag` and never persisted — so every attached RAG is scanned
|
||||
/// unconditionally, exactly as `inject_mcp_secrets` does for MCP servers.
|
||||
fn inject_rag_secrets(vault: &Vault, registered: &HashSet<String>) -> Result<()> {
|
||||
let rags_dir = paths::rags_dir();
|
||||
if !rags_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
for entry in fs::read_dir(&rags_dir)?.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|e| e.to_str()) != Some("yaml") {
|
||||
continue;
|
||||
}
|
||||
let stem = match path.file_stem().and_then(|s| s.to_str()) {
|
||||
// Skip sidecars ("myrag.sbx-mixin.yaml" has stem "myrag.sbx-mixin").
|
||||
Some(s) if !paths::is_rag_sidecar_name(s) => s.to_string(),
|
||||
_ => continue,
|
||||
};
|
||||
let Ok(raw) = fs::read_to_string(&path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(data) = serde_yaml::from_str::<RagData>(&raw) else {
|
||||
continue;
|
||||
};
|
||||
if !data.attached {
|
||||
continue;
|
||||
}
|
||||
let Some(placeholder) = data.driver_config.get("api_key") else {
|
||||
continue;
|
||||
};
|
||||
if registered.contains(&stem) {
|
||||
continue;
|
||||
}
|
||||
let secret_name = placeholder
|
||||
.trim_start_matches("{{")
|
||||
.trim_end_matches("}}")
|
||||
.trim();
|
||||
// Degrade rather than abort: one stale RAG key must not block the whole
|
||||
// sandbox launch. Queries to that RAG fail with a 401 at runtime, which
|
||||
// is recoverable without a restart.
|
||||
match vault.get_secret(secret_name, false) {
|
||||
Ok(secret_value) => {
|
||||
sbx_secret_set(&stem, &secret_value)
|
||||
.context("Failed to register RAG secret with sbx")?;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: could not load secret '{secret_name}' for RAG '{stem}': {e}. \
|
||||
Queries to this RAG will fail inside the sandbox. \
|
||||
Run `coyote --add-secret {secret_name}` to fix."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn provider_to_sbx_service(provider_type: &str, client_name: Option<&str>) -> String {
|
||||
match provider_type {
|
||||
"claude" => "anthropic".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user