feat: added round trip validation for vault providers to ensure permissions and authentication

This commit is contained in:
2026-06-03 08:30:47 -06:00
parent 77374354bd
commit 09e07cb67c
4 changed files with 73 additions and 2 deletions
+1 -1
View File
@@ -18,6 +18,6 @@
"type": "stdio",
"command": "uvx",
"args": ["duckduckgo-mcp-server"]
},
}
}
}
+3
View File
@@ -687,6 +687,9 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> {
},
};
create_vault_password_file(&mut vault)?;
if provider_choice.is_some() {
vault.validate_round_trip()?;
}
let client = Select::new("API Provider (required):", list_client_types()).prompt()?;
+58 -1
View File
@@ -8,7 +8,7 @@ pub use utils::prompt_provider_choice;
use crate::cli::Cli;
use crate::config::AppConfig;
use crate::vault::utils::ensure_password_file_initialized;
use anyhow::{Context, Result, anyhow};
use anyhow::{Context, Result, anyhow, bail};
use fancy_regex::Regex;
use gman::providers::SecretProvider;
use gman::providers::SupportedProvider;
@@ -157,6 +157,63 @@ impl Vault {
Ok(secrets)
}
pub fn auth_hint(&self) -> Option<&'static str> {
match &self.provider {
SupportedProvider::AwsSecretsManager { .. } => Some(
"Try `aws sso login` (for SSO setups) or `aws configure` (for static keys), then retry.",
),
SupportedProvider::GcpSecretManager { .. } => Some(
"Try `gcloud auth application-default login`, then retry.",
),
SupportedProvider::AzureKeyVault { .. } => Some(
"Try `az login`, then retry.",
),
SupportedProvider::Gopass { .. } => Some(
"Make sure `gopass init` has been run and `gopass` is on your PATH.",
),
SupportedProvider::OnePassword { .. } => Some(
"Try `op signin`, then retry.",
),
SupportedProvider::Local { .. } => None,
}
}
pub fn validate_round_trip(&self) -> Result<()> {
const PROBE_KEY: &str = "__coyote_setup_probe__";
const PROBE_VALUE: &str = "ok";
let h = Handle::current();
let result: Result<()> = tokio::task::block_in_place(|| {
h.block_on(async {
self.provider_ref()
.set_secret(PROBE_KEY, PROBE_VALUE)
.await
.with_context(|| "vault write probe failed")?;
let got = self
.provider_ref()
.get_secret(PROBE_KEY)
.await
.with_context(|| "vault read probe failed")?;
let _ = self.provider_ref().delete_secret(PROBE_KEY).await;
if got != PROBE_VALUE {
bail!("vault read probe returned an unexpected value");
}
Ok(())
})
});
result.with_context(|| {
let base = "Vault validation failed. Check that your credentials have permission to create, read, and delete secrets in the configured backend.";
match self.auth_hint() {
Some(hint) => format!("{base}\n\nHint: {hint}"),
None => base.to_string(),
}
})?;
println!("✓ Vault validation succeeded.");
Ok(())
}
pub fn handle_vault_flags(cli: Cli, vault: &Vault) -> Result<()> {
if let Some(secret_name) = cli.add_secret {
vault.add_secret(&secret_name)?;
+11
View File
@@ -374,6 +374,17 @@ pub fn interpolate_secrets(content: &str, vault: &Vault) -> Result<(String, Vec<
missing_secrets.push(name.to_string());
String::new()
}
Some(SecretError::AuthFailed { .. }) => {
let base = format!(
"Failed to fetch secret '{name}' from vault: {e}"
);
let msg = match vault.auth_hint() {
Some(hint) => format!("{base}\n\nHint: {hint}"),
None => base,
};
fatal_error = Some(anyhow!("{msg}"));
String::new()
}
_ => {
fatal_error = Some(anyhow!(
"Failed to fetch secret '{name}' from vault: {e}"