commit 61baa10c9ad6abb858c0f83f48c525cc967eeec4
parent fae39d7c7bd860858ef9115a215f9458c54d32fc
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Apr 2026 17:16:47 +0000
secret-vault: add canonical backend taxonomy
Diffstat:
11 files changed, 647 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2599,6 +2599,10 @@ dependencies = [
]
[[package]]
+name = "radroots-secret-vault"
+version = "0.1.0-alpha.1"
+
+[[package]]
name = "radroots-simplex-agent-proto"
version = "0.1.0-alpha.1"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
@@ -17,6 +17,7 @@ members = [
"crates/nostr-ndb",
"crates/nostr-runtime",
"crates/runtime",
+ "crates/secret-vault",
"crates/simplex-agent-proto",
"crates/simplex-agent-runtime",
"crates/simplex-agent-store",
@@ -85,6 +86,7 @@ radroots-replica-db-wasm = { path = "crates/replica-db-wasm", version = "0.1.0-a
radroots-replica-sync-wasm = { path = "crates/replica-sync-wasm", version = "0.1.0-alpha.1" }
radroots-trade = { path = "crates/trade", version = "0.1.0-alpha.1", default-features = false }
radroots-types = { path = "crates/types", version = "0.1.0-alpha.1", default-features = false }
+radroots-secret-vault = { path = "crates/secret-vault", version = "0.1.0-alpha.1", default-features = false }
anyhow = { version = "1" }
base64 = { version = "0.22" }
diff --git a/contract/coverage/policy.toml b/contract/coverage/policy.toml
@@ -28,6 +28,7 @@ crates = [
"radroots-nostr-ndb",
"radroots-nostr-runtime",
"radroots-runtime",
+ "radroots-secret-vault",
"radroots-simplex-chat-proto",
"radroots-simplex-smp-proto",
"radroots-sql-core",
diff --git a/contract/release/publish-set.toml b/contract/release/publish-set.toml
@@ -8,6 +8,7 @@ crates = [
"radroots-events",
"radroots-log",
"radroots-runtime",
+ "radroots-secret-vault",
"radroots-simplex-chat-proto",
"radroots-simplex-smp-proto",
"radroots-identity",
@@ -44,6 +45,7 @@ crates = [
"radroots-log",
"radroots-events",
"radroots-runtime",
+ "radroots-secret-vault",
"radroots-simplex-chat-proto",
"radroots-simplex-smp-proto",
"radroots-events-codec",
diff --git a/crates/secret-vault/Cargo.toml b/crates/secret-vault/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "radroots-secret-vault"
+version = "0.1.0-alpha.1"
+edition.workspace = true
+authors = [
+ "Radroots Authors",
+]
+rust-version.workspace = true
+license.workspace = true
+description = "canonical secret backend taxonomy and fail-closed selection policy for radroots runtimes"
+repository.workspace = true
+homepage.workspace = true
+documentation = "https://docs.rs/radroots-secret-vault"
+readme = "README.md"
+
+[features]
+default = []
+std = []
+
+[dependencies]
diff --git a/crates/secret-vault/README.md b/crates/secret-vault/README.md
@@ -0,0 +1,3 @@
+# radroots-secret-vault
+
+Canonical secret backend taxonomy and fail-closed backend-selection policy for Rad Roots runtimes.
diff --git a/crates/secret-vault/src/backend.rs b/crates/secret-vault/src/backend.rs
@@ -0,0 +1,32 @@
+use crate::policy::RadrootsHostVaultPolicy;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum RadrootsSecretBackendKind {
+ HostVault,
+ EncryptedFile,
+ ExternalCommand,
+ Memory,
+ PlaintextFile,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum RadrootsSecretBackend {
+ HostVault(RadrootsHostVaultPolicy),
+ EncryptedFile,
+ ExternalCommand,
+ Memory,
+ PlaintextFile,
+}
+
+impl RadrootsSecretBackend {
+ #[must_use]
+ pub const fn kind(self) -> RadrootsSecretBackendKind {
+ match self {
+ Self::HostVault(_) => RadrootsSecretBackendKind::HostVault,
+ Self::EncryptedFile => RadrootsSecretBackendKind::EncryptedFile,
+ Self::ExternalCommand => RadrootsSecretBackendKind::ExternalCommand,
+ Self::Memory => RadrootsSecretBackendKind::Memory,
+ Self::PlaintextFile => RadrootsSecretBackendKind::PlaintextFile,
+ }
+ }
+}
diff --git a/crates/secret-vault/src/error.rs b/crates/secret-vault/src/error.rs
@@ -0,0 +1,76 @@
+use crate::backend::RadrootsSecretBackendKind;
+use core::fmt;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum RadrootsHostVaultRequirement {
+ DeviceLocalOnly,
+ UserPresence,
+ HardwareBacked,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum RadrootsSecretVaultError {
+ BackendUnavailable {
+ backend: RadrootsSecretBackendKind,
+ },
+ FallbackDisallowed {
+ primary: RadrootsSecretBackendKind,
+ fallback: RadrootsSecretBackendKind,
+ },
+ FallbackUnavailable {
+ primary: RadrootsSecretBackendKind,
+ fallback: RadrootsSecretBackendKind,
+ },
+ HostVaultPolicyUnsupported {
+ requirement: RadrootsHostVaultRequirement,
+ },
+}
+
+impl fmt::Display for RadrootsHostVaultRequirement {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let value = match self {
+ Self::DeviceLocalOnly => "device_local_only",
+ Self::UserPresence => "user_presence",
+ Self::HardwareBacked => "hardware_backed",
+ };
+ f.write_str(value)
+ }
+}
+
+impl fmt::Display for RadrootsSecretVaultError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::BackendUnavailable { backend } => {
+ write!(f, "secret backend {backend} is unavailable")
+ }
+ Self::FallbackDisallowed { primary, fallback } => write!(
+ f,
+ "secret backend {primary} may not silently downgrade to {fallback}"
+ ),
+ Self::FallbackUnavailable { primary, fallback } => write!(
+ f,
+ "secret backend {primary} fallback {fallback} is unavailable"
+ ),
+ Self::HostVaultPolicyUnsupported { requirement } => write!(
+ f,
+ "host vault does not satisfy the required {requirement} policy"
+ ),
+ }
+ }
+}
+
+impl fmt::Display for RadrootsSecretBackendKind {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let value = match self {
+ Self::HostVault => "host_vault",
+ Self::EncryptedFile => "encrypted_file",
+ Self::ExternalCommand => "external_command",
+ Self::Memory => "memory",
+ Self::PlaintextFile => "plaintext_file",
+ };
+ f.write_str(value)
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RadrootsSecretVaultError {}
diff --git a/crates/secret-vault/src/lib.rs b/crates/secret-vault/src/lib.rs
@@ -0,0 +1,34 @@
+#![forbid(unsafe_code)]
+#![no_std]
+
+#[cfg(any(feature = "std", test))]
+extern crate std;
+
+pub mod backend;
+pub mod error;
+pub mod policy;
+pub mod selection;
+
+pub mod prelude {
+ pub use crate::backend::{RadrootsSecretBackend, RadrootsSecretBackendKind};
+ pub use crate::error::{RadrootsHostVaultRequirement, RadrootsSecretVaultError};
+ pub use crate::policy::{
+ RadrootsHostVaultCapabilities, RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy,
+ RadrootsHostVaultResidency, RadrootsHostVaultUserPresencePolicy,
+ };
+ pub use crate::selection::{
+ RadrootsResolvedSecretBackend, RadrootsSecretBackendAvailability,
+ RadrootsSecretBackendSelection,
+ };
+}
+
+pub use backend::{RadrootsSecretBackend, RadrootsSecretBackendKind};
+pub use error::{RadrootsHostVaultRequirement, RadrootsSecretVaultError};
+pub use policy::{
+ RadrootsHostVaultCapabilities, RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy,
+ RadrootsHostVaultResidency, RadrootsHostVaultUserPresencePolicy,
+};
+pub use selection::{
+ RadrootsResolvedSecretBackend, RadrootsSecretBackendAvailability,
+ RadrootsSecretBackendSelection,
+};
diff --git a/crates/secret-vault/src/policy.rs b/crates/secret-vault/src/policy.rs
@@ -0,0 +1,130 @@
+use crate::error::{RadrootsHostVaultRequirement, RadrootsSecretVaultError};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum RadrootsHostVaultResidency {
+ UserProfile,
+ DeviceLocalOnly,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum RadrootsHostVaultUserPresencePolicy {
+ NotRequired,
+ Required,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum RadrootsHostVaultHardwarePolicy {
+ Any,
+ PreferHardwareBacked,
+ RequireHardwareBacked,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct RadrootsHostVaultPolicy {
+ pub residency: RadrootsHostVaultResidency,
+ pub user_presence: RadrootsHostVaultUserPresencePolicy,
+ pub hardware: RadrootsHostVaultHardwarePolicy,
+}
+
+impl RadrootsHostVaultPolicy {
+ #[must_use]
+ pub const fn desktop() -> Self {
+ Self {
+ residency: RadrootsHostVaultResidency::UserProfile,
+ user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired,
+ hardware: RadrootsHostVaultHardwarePolicy::Any,
+ }
+ }
+
+ #[must_use]
+ pub const fn device_local() -> Self {
+ Self {
+ residency: RadrootsHostVaultResidency::DeviceLocalOnly,
+ user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired,
+ hardware: RadrootsHostVaultHardwarePolicy::Any,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct RadrootsHostVaultCapabilities {
+ pub available: bool,
+ pub supports_device_local_only: bool,
+ pub supports_user_presence: bool,
+ pub supports_hardware_backed: bool,
+}
+
+impl RadrootsHostVaultCapabilities {
+ #[must_use]
+ pub const fn unavailable() -> Self {
+ Self {
+ available: false,
+ supports_device_local_only: false,
+ supports_user_presence: false,
+ supports_hardware_backed: false,
+ }
+ }
+
+ #[must_use]
+ pub const fn desktop_keyring() -> Self {
+ Self {
+ available: true,
+ supports_device_local_only: false,
+ supports_user_presence: false,
+ supports_hardware_backed: false,
+ }
+ }
+
+ #[must_use]
+ pub const fn secure_device() -> Self {
+ Self {
+ available: true,
+ supports_device_local_only: true,
+ supports_user_presence: true,
+ supports_hardware_backed: true,
+ }
+ }
+
+ pub const fn validate(
+ self,
+ policy: RadrootsHostVaultPolicy,
+ ) -> Result<(), RadrootsSecretVaultError> {
+ if !self.available {
+ return Err(RadrootsSecretVaultError::BackendUnavailable {
+ backend: crate::backend::RadrootsSecretBackendKind::HostVault,
+ });
+ }
+
+ if matches!(
+ policy.residency,
+ RadrootsHostVaultResidency::DeviceLocalOnly
+ ) && !self.supports_device_local_only
+ {
+ return Err(RadrootsSecretVaultError::HostVaultPolicyUnsupported {
+ requirement: RadrootsHostVaultRequirement::DeviceLocalOnly,
+ });
+ }
+
+ if matches!(
+ policy.user_presence,
+ RadrootsHostVaultUserPresencePolicy::Required
+ ) && !self.supports_user_presence
+ {
+ return Err(RadrootsSecretVaultError::HostVaultPolicyUnsupported {
+ requirement: RadrootsHostVaultRequirement::UserPresence,
+ });
+ }
+
+ if matches!(
+ policy.hardware,
+ RadrootsHostVaultHardwarePolicy::RequireHardwareBacked
+ ) && !self.supports_hardware_backed
+ {
+ return Err(RadrootsSecretVaultError::HostVaultPolicyUnsupported {
+ requirement: RadrootsHostVaultRequirement::HardwareBacked,
+ });
+ }
+
+ Ok(())
+ }
+}
diff --git a/crates/secret-vault/src/selection.rs b/crates/secret-vault/src/selection.rs
@@ -0,0 +1,343 @@
+use crate::backend::{RadrootsSecretBackend, RadrootsSecretBackendKind};
+use crate::error::RadrootsSecretVaultError;
+use crate::policy::RadrootsHostVaultCapabilities;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct RadrootsSecretBackendSelection {
+ pub primary: RadrootsSecretBackend,
+ pub fallback: Option<RadrootsSecretBackend>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct RadrootsSecretBackendAvailability {
+ pub host_vault: RadrootsHostVaultCapabilities,
+ pub encrypted_file: bool,
+ pub external_command: bool,
+ pub memory: bool,
+ pub plaintext_file: bool,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct RadrootsResolvedSecretBackend {
+ pub backend: RadrootsSecretBackend,
+ pub used_fallback: bool,
+}
+
+impl RadrootsSecretBackendSelection {
+ pub fn resolve(
+ self,
+ availability: RadrootsSecretBackendAvailability,
+ ) -> Result<RadrootsResolvedSecretBackend, RadrootsSecretVaultError> {
+ if availability.supports(self.primary).is_ok() {
+ return Ok(RadrootsResolvedSecretBackend {
+ backend: self.primary,
+ used_fallback: false,
+ });
+ }
+
+ if let RadrootsSecretBackend::HostVault(policy) = self.primary {
+ if availability.host_vault.available {
+ availability.host_vault.validate(policy)?;
+ }
+ }
+
+ match self.fallback {
+ Some(fallback) => {
+ if !self.primary.allows_fallback_to(fallback.kind()) {
+ return Err(RadrootsSecretVaultError::FallbackDisallowed {
+ primary: self.primary.kind(),
+ fallback: fallback.kind(),
+ });
+ }
+
+ availability.supports(fallback).map_err(|_| {
+ RadrootsSecretVaultError::FallbackUnavailable {
+ primary: self.primary.kind(),
+ fallback: fallback.kind(),
+ }
+ })?;
+
+ Ok(RadrootsResolvedSecretBackend {
+ backend: fallback,
+ used_fallback: true,
+ })
+ }
+ None => Err(RadrootsSecretVaultError::BackendUnavailable {
+ backend: self.primary.kind(),
+ }),
+ }
+ }
+}
+
+impl RadrootsSecretBackendAvailability {
+ fn supports(self, backend: RadrootsSecretBackend) -> Result<(), RadrootsSecretVaultError> {
+ match backend {
+ RadrootsSecretBackend::HostVault(policy) => self.host_vault.validate(policy),
+ RadrootsSecretBackend::EncryptedFile if self.encrypted_file => Ok(()),
+ RadrootsSecretBackend::ExternalCommand if self.external_command => Ok(()),
+ RadrootsSecretBackend::Memory if self.memory => Ok(()),
+ RadrootsSecretBackend::PlaintextFile if self.plaintext_file => Ok(()),
+ _ => Err(RadrootsSecretVaultError::BackendUnavailable {
+ backend: backend.kind(),
+ }),
+ }
+ }
+}
+
+impl RadrootsSecretBackend {
+ const fn allows_fallback_to(self, fallback: RadrootsSecretBackendKind) -> bool {
+ matches!(
+ (self.kind(), fallback),
+ (
+ RadrootsSecretBackendKind::HostVault,
+ RadrootsSecretBackendKind::EncryptedFile
+ )
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::error::RadrootsHostVaultRequirement;
+ use crate::policy::{
+ RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy, RadrootsHostVaultResidency,
+ RadrootsHostVaultUserPresencePolicy,
+ };
+
+ #[test]
+ fn host_vault_is_selected_when_available() {
+ let selection = RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()),
+ fallback: Some(RadrootsSecretBackend::EncryptedFile),
+ };
+
+ let resolved = selection
+ .resolve(RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::desktop_keyring(),
+ encrypted_file: true,
+ external_command: false,
+ memory: false,
+ plaintext_file: false,
+ })
+ .expect("host vault resolves");
+
+ assert_eq!(
+ resolved,
+ RadrootsResolvedSecretBackend {
+ backend: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()),
+ used_fallback: false,
+ }
+ );
+ }
+
+ #[test]
+ fn host_vault_may_explicitly_fallback_to_encrypted_file() {
+ let selection = RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()),
+ fallback: Some(RadrootsSecretBackend::EncryptedFile),
+ };
+
+ let resolved = selection
+ .resolve(RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::unavailable(),
+ encrypted_file: true,
+ external_command: false,
+ memory: false,
+ plaintext_file: false,
+ })
+ .expect("encrypted file fallback resolves");
+
+ assert_eq!(
+ resolved,
+ RadrootsResolvedSecretBackend {
+ backend: RadrootsSecretBackend::EncryptedFile,
+ used_fallback: true,
+ }
+ );
+ }
+
+ #[test]
+ fn host_vault_without_explicit_fallback_fails_closed() {
+ let selection = RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()),
+ fallback: None,
+ };
+
+ let err = selection
+ .resolve(RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::unavailable(),
+ encrypted_file: true,
+ external_command: false,
+ memory: false,
+ plaintext_file: false,
+ })
+ .expect_err("missing fallback must fail");
+
+ assert_eq!(
+ err,
+ RadrootsSecretVaultError::BackendUnavailable {
+ backend: RadrootsSecretBackendKind::HostVault,
+ }
+ );
+ }
+
+ #[test]
+ fn unsupported_host_vault_policy_fails_before_any_downgrade() {
+ let selection = RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy {
+ residency: RadrootsHostVaultResidency::DeviceLocalOnly,
+ user_presence: RadrootsHostVaultUserPresencePolicy::Required,
+ hardware: RadrootsHostVaultHardwarePolicy::RequireHardwareBacked,
+ }),
+ fallback: Some(RadrootsSecretBackend::EncryptedFile),
+ };
+
+ let err = selection
+ .resolve(RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::desktop_keyring(),
+ encrypted_file: true,
+ external_command: false,
+ memory: false,
+ plaintext_file: false,
+ })
+ .expect_err("unsupported host policy must fail");
+
+ assert_eq!(
+ err,
+ RadrootsSecretVaultError::HostVaultPolicyUnsupported {
+ requirement: RadrootsHostVaultRequirement::DeviceLocalOnly,
+ }
+ );
+ }
+
+ #[test]
+ fn encrypted_file_may_not_downgrade_to_plaintext_file() {
+ let selection = RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::EncryptedFile,
+ fallback: Some(RadrootsSecretBackend::PlaintextFile),
+ };
+
+ let err = selection
+ .resolve(RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::unavailable(),
+ encrypted_file: false,
+ external_command: false,
+ memory: false,
+ plaintext_file: true,
+ })
+ .expect_err("plaintext downgrade must fail");
+
+ assert_eq!(
+ err,
+ RadrootsSecretVaultError::FallbackDisallowed {
+ primary: RadrootsSecretBackendKind::EncryptedFile,
+ fallback: RadrootsSecretBackendKind::PlaintextFile,
+ }
+ );
+ }
+
+ #[test]
+ fn external_command_may_not_downgrade_to_encrypted_file() {
+ let selection = RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::ExternalCommand,
+ fallback: Some(RadrootsSecretBackend::EncryptedFile),
+ };
+
+ let err = selection
+ .resolve(RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::unavailable(),
+ encrypted_file: true,
+ external_command: false,
+ memory: false,
+ plaintext_file: false,
+ })
+ .expect_err("external command downgrade must fail");
+
+ assert_eq!(
+ err,
+ RadrootsSecretVaultError::FallbackDisallowed {
+ primary: RadrootsSecretBackendKind::ExternalCommand,
+ fallback: RadrootsSecretBackendKind::EncryptedFile,
+ }
+ );
+ }
+
+ #[test]
+ fn explicit_plaintext_file_selection_stays_explicit() {
+ let selection = RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::PlaintextFile,
+ fallback: None,
+ };
+
+ let resolved = selection
+ .resolve(RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::unavailable(),
+ encrypted_file: false,
+ external_command: false,
+ memory: false,
+ plaintext_file: true,
+ })
+ .expect("explicit plaintext file selection resolves");
+
+ assert_eq!(
+ resolved,
+ RadrootsResolvedSecretBackend {
+ backend: RadrootsSecretBackend::PlaintextFile,
+ used_fallback: false,
+ }
+ );
+ }
+
+ #[test]
+ fn memory_backend_must_be_selected_explicitly() {
+ let selection = RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::Memory,
+ fallback: None,
+ };
+
+ let resolved = selection
+ .resolve(RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::unavailable(),
+ encrypted_file: false,
+ external_command: false,
+ memory: true,
+ plaintext_file: false,
+ })
+ .expect("memory backend resolves");
+
+ assert_eq!(
+ resolved,
+ RadrootsResolvedSecretBackend {
+ backend: RadrootsSecretBackend::Memory,
+ used_fallback: false,
+ }
+ );
+ }
+
+ #[test]
+ fn unavailable_explicit_fallback_reports_fallback_unavailable() {
+ let selection = RadrootsSecretBackendSelection {
+ primary: RadrootsSecretBackend::HostVault(RadrootsHostVaultPolicy::desktop()),
+ fallback: Some(RadrootsSecretBackend::EncryptedFile),
+ };
+
+ let err = selection
+ .resolve(RadrootsSecretBackendAvailability {
+ host_vault: RadrootsHostVaultCapabilities::unavailable(),
+ encrypted_file: false,
+ external_command: false,
+ memory: false,
+ plaintext_file: false,
+ })
+ .expect_err("unavailable fallback must fail");
+
+ assert_eq!(
+ err,
+ RadrootsSecretVaultError::FallbackUnavailable {
+ primary: RadrootsSecretBackendKind::HostVault,
+ fallback: RadrootsSecretBackendKind::EncryptedFile,
+ }
+ );
+ }
+}