diff --git a/docs/architecture/platform-boundaries.md b/docs/architecture/platform-boundaries.md
index 930c1b6..8a01640 100644
--- a/docs/architecture/platform-boundaries.md
+++ b/docs/architecture/platform-boundaries.md
@@ -12,10 +12,11 @@ This repository is a modular multi-package platform library set.
| Core.Blueprint.Redis | Redis integration helpers and extension points | Core.Blueprint.Common |
| Core.Blueprint.SQLServer | SQL Server integration helpers and extension points | Core.Blueprint.Common |
| Core.Blueprint.Storage | Blob/file storage integration helpers | Core.Blueprint.Common |
-| Core.Blueprint.KeyVault | Key vault access integration helpers | Core.Blueprint.Common |
+| Core.Blueprint.KeyVault | Key vault and provider-agnostic secret contract helpers | Core.Blueprint.Common |
## Boundary Rules
- Blueprint remains library-only.
- Identity abstractions are not owned by this repository.
- Downstream repositories consume package contracts from this repo.
+- Secret provider abstractions are provider-agnostic; concrete provider adapters are bound at runtime.
diff --git a/docs/consumption/secret-provider-rollout.md b/docs/consumption/secret-provider-rollout.md
new file mode 100644
index 0000000..1918660
--- /dev/null
+++ b/docs/consumption/secret-provider-rollout.md
@@ -0,0 +1,34 @@
+# Provider-Agnostic Secret Provider Rollout
+
+This package defines a provider-agnostic contract for secret lookup without binding to Vault, cloud providers, or environment files in core layers.
+
+## Contract Surface
+
+- `IBlueprintSecretProvider`
+- `BlueprintSecretReference`
+- `BlueprintSecretResolutionResult`
+
+## Runtime Defaults
+
+- `AddBlueprintKeyVaultModule(...)` now registers:
+ - `BlueprintKeyVaultRuntimeSettings` with:
+ - `VaultName`
+ - `SecretProviderName`
+ - `NoOpBlueprintSecretProvider` as default fallback.
+
+The default fallback returns unresolved lookups and never introduces provider-specific behavior.
+
+## Binding Strategy
+
+1. Keep domain and application layers dependent only on `IBlueprintSecretProvider`.
+2. Bind provider implementation at runtime through DI:
+ - Vault adapter
+ - Cloud secret manager adapter
+ - Environment/test adapter
+3. Keep one active provider per deployment profile.
+
+## Rollout Notes
+
+- Stage 33 keeps this contract-only baseline.
+- Concrete Vault/OIDC provider integration should be implemented in infrastructure/runtime layers only.
+- Existing identity logic ownership remains in Thalos repositories.
diff --git a/src/Core.Blueprint.KeyVault/Contracts/BlueprintSecretReference.cs b/src/Core.Blueprint.KeyVault/Contracts/BlueprintSecretReference.cs
new file mode 100644
index 0000000..5c2cda2
--- /dev/null
+++ b/src/Core.Blueprint.KeyVault/Contracts/BlueprintSecretReference.cs
@@ -0,0 +1,12 @@
+namespace Core.Blueprint.KeyVault.Contracts;
+
+///
+/// Defines a provider-agnostic secret lookup reference.
+///
+/// Secret namespace, path, or scope name.
+/// Secret key name inside the scope.
+/// Optional secret version marker.
+public sealed record BlueprintSecretReference(
+ string Scope,
+ string Name,
+ string? Version = null);
diff --git a/src/Core.Blueprint.KeyVault/Contracts/BlueprintSecretResolutionResult.cs b/src/Core.Blueprint.KeyVault/Contracts/BlueprintSecretResolutionResult.cs
new file mode 100644
index 0000000..dcfbb23
--- /dev/null
+++ b/src/Core.Blueprint.KeyVault/Contracts/BlueprintSecretResolutionResult.cs
@@ -0,0 +1,36 @@
+namespace Core.Blueprint.KeyVault.Contracts;
+
+///
+/// Represents the outcome of a secret provider lookup.
+///
+/// True when a secret value was found.
+/// Resolved secret value, if found.
+/// Name of the active secret provider.
+/// Resolved version marker, if available.
+public sealed record BlueprintSecretResolutionResult(
+ bool IsResolved,
+ string? Value,
+ string ProviderName,
+ string? Version)
+{
+ ///
+ /// Creates a resolved secret result.
+ ///
+ public static BlueprintSecretResolutionResult Resolved(
+ string value,
+ string providerName,
+ string? version = null)
+ {
+ return new BlueprintSecretResolutionResult(true, value, providerName, version);
+ }
+
+ ///
+ /// Creates a missing secret result.
+ ///
+ public static BlueprintSecretResolutionResult Missing(
+ string providerName,
+ string? version = null)
+ {
+ return new BlueprintSecretResolutionResult(false, null, providerName, version);
+ }
+}
diff --git a/src/Core.Blueprint.KeyVault/Contracts/IBlueprintSecretProvider.cs b/src/Core.Blueprint.KeyVault/Contracts/IBlueprintSecretProvider.cs
new file mode 100644
index 0000000..0b9fe13
--- /dev/null
+++ b/src/Core.Blueprint.KeyVault/Contracts/IBlueprintSecretProvider.cs
@@ -0,0 +1,17 @@
+namespace Core.Blueprint.KeyVault.Contracts;
+
+///
+/// Defines provider-agnostic secret retrieval operations.
+///
+public interface IBlueprintSecretProvider
+{
+ ///
+ /// Resolves a secret from the configured provider.
+ ///
+ /// Provider-agnostic secret reference.
+ /// Cancellation token.
+ /// Resolution result describing value and provider metadata.
+ ValueTask GetSecretAsync(
+ BlueprintSecretReference reference,
+ CancellationToken cancellationToken = default);
+}
diff --git a/src/Core.Blueprint.KeyVault/DependencyInjection/BlueprintKeyVaultServiceCollectionExtensions.cs b/src/Core.Blueprint.KeyVault/DependencyInjection/BlueprintKeyVaultServiceCollectionExtensions.cs
index b5ea549..c2a6805 100644
--- a/src/Core.Blueprint.KeyVault/DependencyInjection/BlueprintKeyVaultServiceCollectionExtensions.cs
+++ b/src/Core.Blueprint.KeyVault/DependencyInjection/BlueprintKeyVaultServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
using Core.Blueprint.Common.DependencyInjection;
+using Core.Blueprint.KeyVault.Contracts;
using Core.Blueprint.KeyVault.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -15,13 +16,18 @@ public static class BlueprintKeyVaultServiceCollectionExtensions
///
/// Service collection.
/// Target key vault name.
+ /// Active provider name for secret resolution.
/// Service collection for fluent chaining.
public static IServiceCollection AddBlueprintKeyVaultModule(
this IServiceCollection services,
- string vaultName = "default-vault")
+ string vaultName = "default-vault",
+ string secretProviderName = "unconfigured")
{
services.AddBlueprintRuntimeCore();
- services.TryAddSingleton(new BlueprintKeyVaultRuntimeSettings(ResolveVaultName(vaultName)));
+ services.TryAddSingleton(new BlueprintKeyVaultRuntimeSettings(
+ ResolveVaultName(vaultName),
+ ResolveProviderName(secretProviderName)));
+ services.TryAddSingleton();
return services;
}
@@ -35,4 +41,14 @@ public static class BlueprintKeyVaultServiceCollectionExtensions
return "default-vault";
}
+
+ private static string ResolveProviderName(string secretProviderName)
+ {
+ if (!string.IsNullOrWhiteSpace(secretProviderName))
+ {
+ return secretProviderName;
+ }
+
+ return "unconfigured";
+ }
}
diff --git a/src/Core.Blueprint.KeyVault/Runtime/BlueprintKeyVaultRuntimeSettings.cs b/src/Core.Blueprint.KeyVault/Runtime/BlueprintKeyVaultRuntimeSettings.cs
index 4e7afff..c5e628c 100644
--- a/src/Core.Blueprint.KeyVault/Runtime/BlueprintKeyVaultRuntimeSettings.cs
+++ b/src/Core.Blueprint.KeyVault/Runtime/BlueprintKeyVaultRuntimeSettings.cs
@@ -4,4 +4,7 @@ namespace Core.Blueprint.KeyVault.Runtime;
/// Defines runtime settings for key vault integration helpers.
///
/// Target key vault name.
-public sealed record BlueprintKeyVaultRuntimeSettings(string VaultName);
+/// Active secret provider name bound through DI.
+public sealed record BlueprintKeyVaultRuntimeSettings(
+ string VaultName,
+ string SecretProviderName);
diff --git a/src/Core.Blueprint.KeyVault/Runtime/NoOpBlueprintSecretProvider.cs b/src/Core.Blueprint.KeyVault/Runtime/NoOpBlueprintSecretProvider.cs
new file mode 100644
index 0000000..8bcec56
--- /dev/null
+++ b/src/Core.Blueprint.KeyVault/Runtime/NoOpBlueprintSecretProvider.cs
@@ -0,0 +1,20 @@
+using Core.Blueprint.KeyVault.Contracts;
+
+namespace Core.Blueprint.KeyVault.Runtime;
+
+///
+/// Default provider used when no concrete secret manager adapter is configured.
+///
+public sealed class NoOpBlueprintSecretProvider(BlueprintKeyVaultRuntimeSettings settings)
+ : IBlueprintSecretProvider
+{
+ ///
+ public ValueTask GetSecretAsync(
+ BlueprintSecretReference reference,
+ CancellationToken cancellationToken = default)
+ {
+ var providerName = settings.SecretProviderName;
+ var result = BlueprintSecretResolutionResult.Missing(providerName, reference.Version);
+ return ValueTask.FromResult(result);
+ }
+}
diff --git a/tests/Core.Blueprint.KeyVault.UnitTests/UnitTest1.cs b/tests/Core.Blueprint.KeyVault.UnitTests/UnitTest1.cs
index 11d2e17..acf8651 100644
--- a/tests/Core.Blueprint.KeyVault.UnitTests/UnitTest1.cs
+++ b/tests/Core.Blueprint.KeyVault.UnitTests/UnitTest1.cs
@@ -1,3 +1,4 @@
+using Core.Blueprint.KeyVault.Contracts;
using Core.Blueprint.KeyVault.DependencyInjection;
using Core.Blueprint.KeyVault.Runtime;
using Microsoft.Extensions.DependencyInjection;
@@ -11,11 +12,62 @@ public class UnitTest1
{
var services = new ServiceCollection();
- services.AddBlueprintKeyVaultModule("agile-vault");
+ services.AddBlueprintKeyVaultModule("agile-vault", "vault");
using var provider = services.BuildServiceProvider();
var settings = provider.GetRequiredService();
Assert.Equal("agile-vault", settings.VaultName);
+ Assert.Equal("vault", settings.SecretProviderName);
+ }
+
+ [Fact]
+ public async Task AddBlueprintKeyVaultModule_WhenNoProviderBound_UsesNoOpProvider()
+ {
+ var services = new ServiceCollection();
+
+ services.AddBlueprintKeyVaultModule("agile-vault");
+
+ await using var provider = services.BuildServiceProvider();
+ var secretProvider = provider.GetRequiredService();
+ var result = await secretProvider.GetSecretAsync(
+ new BlueprintSecretReference("thalos/oidc", "google-client-secret"));
+
+ Assert.False(result.IsResolved);
+ Assert.Null(result.Value);
+ Assert.Equal("unconfigured", result.ProviderName);
+ }
+
+ [Fact]
+ public async Task AddBlueprintKeyVaultModule_WhenProviderAlreadyRegistered_PreservesCustomProvider()
+ {
+ var services = new ServiceCollection();
+ services.AddSingleton();
+
+ services.AddBlueprintKeyVaultModule();
+
+ await using var provider = services.BuildServiceProvider();
+ var secretProvider = provider.GetRequiredService();
+ var result = await secretProvider.GetSecretAsync(
+ new BlueprintSecretReference("scope", "name", "v1"));
+
+ Assert.True(result.IsResolved);
+ Assert.Equal("custom-provider", result.ProviderName);
+ Assert.Equal("secret-value", result.Value);
+ Assert.Equal("v1", result.Version);
+ }
+
+ private sealed class FakeSecretProvider : IBlueprintSecretProvider
+ {
+ public ValueTask GetSecretAsync(
+ BlueprintSecretReference reference,
+ CancellationToken cancellationToken = default)
+ {
+ var result = BlueprintSecretResolutionResult.Resolved(
+ "secret-value",
+ "custom-provider",
+ reference.Version);
+ return ValueTask.FromResult(result);
+ }
}
}