Skip to main content

systemprompt_config/bootstrap/secrets/vault/
error.rs

1//! Failure modes of the Vault / `OpenBao` KV v2 secrets provider.
2//!
3//! No variant carries a response body, a token, or a secret value. The only
4//! upstream text that reaches an operator is Vault's own `errors[]` array,
5//! truncated to [`MAX_VAULT_ERROR_CHARS`], because that array is documented to
6//! hold policy diagnostics rather than secret material.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11pub(super) const MAX_VAULT_ERROR_CHARS: usize = 200;
12
13#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum VaultError {
16    #[error("vault address is not a permitted outbound URL: {message}")]
17    Address { message: String },
18
19    #[error("vault TLS CA certificate at {path} could not be loaded: {message}")]
20    CaCertificate { path: String, message: String },
21
22    #[error("vault HTTP client could not be built: {message}")]
23    ClientBuild { message: String },
24
25    #[error(
26        "vault credential is missing: {name}. Provide it in the process environment; never write \
27         a token into profile YAML."
28    )]
29    MissingCredential { name: String },
30
31    #[error("vault credential file {path} could not be read: {message}")]
32    CredentialFile { path: String, message: String },
33
34    #[error("vault {method} login was rejected with HTTP {status}{detail}")]
35    Auth {
36        method: &'static str,
37        status: u16,
38        detail: String,
39    },
40
41    #[error("vault denied access to {mount}/{path} (HTTP 403){detail}")]
42    Forbidden {
43        mount: String,
44        path: String,
45        detail: String,
46    },
47
48    #[error("vault has no secret at {mount}/{path}")]
49    NotFound { mount: String, path: String },
50
51    #[error("vault returned HTTP {status} for {mount}/{path}{detail}")]
52    Http {
53        status: u16,
54        mount: String,
55        path: String,
56        detail: String,
57    },
58
59    #[error("vault response was not the expected KV v2 shape: {message}")]
60    Malformed { message: String },
61
62    #[error("vault was unreachable after {attempts} attempt(s): {message}")]
63    Exhausted { attempts: u32, message: String },
64}
65
66#[must_use]
67pub(super) fn truncate_detail(raw: &str) -> String {
68    let trimmed = raw.trim();
69    if trimmed.is_empty() {
70        return String::new();
71    }
72    let clipped: String = trimmed.chars().take(MAX_VAULT_ERROR_CHARS).collect();
73    format!(": {clipped}")
74}