Skip to main content

systemprompt_runtime/
error.rs

1//! Typed error boundary for the runtime crate.
2//!
3//! All public APIs of `systemprompt-runtime` return [`RuntimeResult<T>`]
4//! (i.e. `Result<T, RuntimeError>`). [`RuntimeError`] composes the typed
5//! errors of upstream layers (config, database, events, files, users,
6//! extensions) via `#[from]` so callers can pattern-match on the original
7//! cause without losing fidelity.
8//!
9//! Boot steps whose failure needs more context than the upstream error
10//! carries (a storage root, a bundle name) get a struct variant holding that
11//! context next to the `#[source]` cause.
12//!
13//! Copyright (c) systemprompt.io — Business Source License 1.1.
14//! See <https://systemprompt.io> for licensing details.
15
16use std::path::PathBuf;
17
18use systemprompt_agent::AgentError;
19use systemprompt_analytics::AnalyticsError;
20use systemprompt_config::paths::PathError;
21use systemprompt_config::{
22    ConfigError as ProfileConfigError, ProfileBootstrapError, SecretsBootstrapError,
23};
24use systemprompt_content::ContentError;
25use systemprompt_extension::LoaderError;
26use systemprompt_files::FilesError;
27use systemprompt_identifiers::SecretName;
28use systemprompt_loader::{BundleError, ConfigLoadError};
29use systemprompt_marketplace::managed::ManagedError;
30use systemprompt_mcp::McpDomainError;
31use systemprompt_models::errors::GlobalConfigError;
32use systemprompt_oauth::OauthError;
33use systemprompt_security::authz::AuthzError;
34use systemprompt_security::keys::TokenAuthorityError;
35use systemprompt_security::policy::GovernanceEngineError;
36use systemprompt_traits::{BoxedSource, FileStorageError, RepositoryError};
37use systemprompt_users::UserError;
38use thiserror::Error;
39
40pub type RuntimeResult<T> = Result<T, RuntimeError>;
41
42#[derive(Debug, Error)]
43pub enum RuntimeError {
44    #[error(transparent)]
45    Profile(#[from] ProfileConfigError),
46
47    #[error(transparent)]
48    ProfileBootstrap(#[from] ProfileBootstrapError),
49
50    #[error(transparent)]
51    Config(#[from] GlobalConfigError),
52
53    #[error(transparent)]
54    Paths(#[from] PathError),
55
56    #[error(transparent)]
57    Files(#[from] FilesError),
58
59    #[error(transparent)]
60    Users(#[from] UserError),
61
62    #[error(transparent)]
63    Repository(#[from] RepositoryError),
64
65    #[error(transparent)]
66    Analytics(#[from] AnalyticsError),
67
68    #[error(transparent)]
69    Mcp(#[from] McpDomainError),
70
71    #[error(transparent)]
72    Agent(#[from] AgentError),
73
74    #[error(transparent)]
75    Content(#[from] ContentError),
76
77    #[error(transparent)]
78    Oauth(#[from] OauthError),
79
80    #[error(transparent)]
81    Loader(#[from] LoaderError),
82
83    #[error(transparent)]
84    Governance(#[from] GovernanceEngineError),
85
86    #[error(transparent)]
87    Managed(#[from] ManagedError),
88
89    #[error(transparent)]
90    Secrets(#[from] SecretsBootstrapError),
91
92    #[error("services config: {0}")]
93    ServicesConfig(#[from] ConfigLoadError),
94
95    #[error("services bundle: {0}")]
96    ServicesBundle(#[from] BundleError),
97
98    #[error("services bundle {name} has no cached fetch state")]
99    ServicesBundleNotCached { name: String },
100
101    #[error("services bundle {name} manifest: {source}")]
102    ServicesBundleManifest {
103        name: String,
104        #[source]
105        source: BundleError,
106    },
107
108    #[error("services authz reconcile: {0}")]
109    ServicesReconcile(#[source] AuthzError),
110
111    #[error("services reconcile state: {0}")]
112    ServicesReconcileState(#[source] BundleError),
113
114    #[error("signing key init: {0}")]
115    Signing(#[from] TokenAuthorityError),
116
117    #[error("authz bootstrap: {0}")]
118    Authz(#[from] AuthzError),
119
120    #[error("storage root {} probe: {source}", .path.display())]
121    StorageProbe {
122        path: PathBuf,
123        #[source]
124        source: FileStorageError,
125    },
126
127    #[error("storage root {} did not read back what was written", .path.display())]
128    StorageReadBack { path: PathBuf },
129
130    #[error("storage.credentials names secret '{name}', which the secrets store does not hold")]
131    StorageCredentialMissing { name: SecretName },
132
133    #[error("storage.credentials secret '{name}' is not a service-account key: {source}")]
134    StorageCredential {
135        name: SecretName,
136        #[source]
137        source: serde_json::Error,
138    },
139
140    #[error("storage.backend 'gcs' requires storage.bucket")]
141    StorageBucketMissing,
142
143    #[error("storage endpoint: {0}")]
144    StorageEndpoint(#[source] url::ParseError),
145
146    #[error("storage HTTP client: {0}")]
147    StorageHttp(#[source] reqwest::Error),
148
149    #[error(
150        "Configured system admin '{username}' was not found in the users table. Run `systemprompt \
151         admin bootstrap` first."
152    )]
153    SystemAdminNotFound { username: String },
154
155    #[error(
156        "Configured system admin '{username}' exists but is not active. Re-activate the user \
157         before starting the platform."
158    )]
159    SystemAdminInactive { username: String },
160
161    #[error(
162        "Configured system admin '{username}' exists but does not carry the 'admin' role. Grant \
163         the role before starting the platform."
164    )]
165    SystemAdminMissingRole { username: String },
166
167    #[error(
168        "Configured GeoIP database at '{path}' could not be loaded: {source}. Fix or remove \
169         paths.geoip_database from the profile."
170    )]
171    GeoIpUnreadable {
172        path: String,
173        #[source]
174        source: BoxedSource,
175    },
176
177    #[error(
178        "database schema is behind this binary (migrations skipped at boot): {} extension(s) not \
179         installed [{}], {} pending migration(s) [{}], {} checksum drift(s) [{}]; run \
180         'systemprompt infra db migrate --profile {profile}' and restart",
181        .fresh.len(),
182        .fresh.join(", "),
183        .pending.len(),
184        .pending.join(", "),
185        .drift.len(),
186        .drift.join(", ")
187    )]
188    SchemaBehind {
189        profile: String,
190        fresh: Vec<String>,
191        pending: Vec<String>,
192        drift: Vec<String>,
193    },
194
195    #[error("DATABASE_URL is empty")]
196    EmptyDatabaseUrl,
197
198    #[error("DATABASE_URL must be a postgres:// or postgresql:// URL")]
199    UnsupportedDatabaseUrl,
200}