Skip to main content

systemprompt_extension/
error.rs

1//! Typed error enums raised by extension registration and configuration.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use systemprompt_identifiers::ExtensionId;
7use systemprompt_traits::BoxedSource;
8use thiserror::Error;
9
10#[derive(Debug, Error)]
11pub enum LoaderError {
12    #[error("Extension '{extension}' requires dependency '{dependency}' which is not registered")]
13    MissingDependency {
14        extension: ExtensionId,
15        dependency: ExtensionId,
16    },
17
18    #[error("Extension with ID '{0}' is already registered")]
19    DuplicateExtension(ExtensionId),
20
21    #[error("Extension '{0}' is required and cannot be disabled")]
22    RequiredExtensionDisabled(ExtensionId),
23
24    #[error(
25        "Extension '{dependency}' is disabled but extension '{extension}' depends on it; disable \
26         '{extension}' as well or re-enable '{dependency}'"
27    )]
28    DisabledDependency {
29        extension: ExtensionId,
30        dependency: ExtensionId,
31    },
32
33    #[error("Failed to initialize extension '{extension}': {message}")]
34    InitializationFailed {
35        extension: ExtensionId,
36        message: String,
37    },
38
39    #[error("Failed to install schema for extension '{extension}': {message}")]
40    SchemaInstallationFailed {
41        extension: ExtensionId,
42        message: String,
43    },
44
45    #[error("Failed to install schema for extension '{extension}': {context}: {source}")]
46    SchemaInstallationStepFailed {
47        extension: ExtensionId,
48        context: String,
49        #[source]
50        source: BoxedSource,
51    },
52
53    #[error("Migration failed for extension '{extension}': {message}")]
54    MigrationFailed {
55        extension: ExtensionId,
56        message: String,
57    },
58
59    #[error("Migration failed for extension '{extension}': {context}: {source}")]
60    MigrationStepFailed {
61        extension: ExtensionId,
62        context: String,
63        #[source]
64        source: BoxedSource,
65    },
66
67    #[error(
68        "Migration {version} for extension '{extension}' is not reversible (no down SQL provided)"
69    )]
70    MigrationNotReversible {
71        extension: ExtensionId,
72        version: u32,
73    },
74
75    #[error(
76        "Extension '{extension}' migration slot {version} was applied as '{stored_name}' but the \
77         tree now names it '{current_name}'. A migration file was deleted and its number reused; \
78         established databases have already spent that slot, so the new SQL would be skipped, \
79         never executed. Renumber it above every used slot and leave a \
80         `{version:03}_{stored_name}.tombstone` behind so the number cannot be claimed again."
81    )]
82    MigrationSlotReused {
83        extension: ExtensionId,
84        version: u32,
85        stored_name: String,
86        current_name: String,
87    },
88
89    #[error("Configuration validation failed for extension '{extension}': {message}")]
90    ConfigValidationFailed {
91        extension: ExtensionId,
92        message: String,
93    },
94
95    #[error("Extension '{extension}' uses reserved API path '{path}'")]
96    ReservedPathCollision {
97        extension: ExtensionId,
98        path: String,
99    },
100
101    #[error(
102        "Extension '{extension}' has invalid base path '{path}': must be / or start with /api/"
103    )]
104    InvalidBasePath {
105        extension: ExtensionId,
106        path: String,
107    },
108
109    #[error("Dependency cycle detected while ordering extensions: {chain}")]
110    DependencyCycle { chain: String },
111
112    #[error(
113        "Extension '{extension}' migration ALTERs table '{table}' but does not create it in its \
114         schemas() nor declare it in cross_extension_tables(); cross-extension table mutations \
115         must be declared explicitly"
116    )]
117    CrossExtensionAlterUndeclared {
118        extension: ExtensionId,
119        table: String,
120    },
121
122    #[error(
123        "Extension '{extension}' migration {migration} references {kind} '{object}' via {how}, \
124         which only a declarative schema file creates; the dependent phase runs after \
125         migrations, so a database that has not booted on that schema fails here. Create it in \
126         a migration, guard the reference in a DO $$ block that tests pg_trigger/pg_views, or \
127         leave it to the declarative schema"
128    )]
129    MigrationReferencesDeclarativeObject {
130        extension: ExtensionId,
131        migration: String,
132        kind: String,
133        object: String,
134        how: String,
135    },
136
137    #[error(
138        "Extension '{extension}' migration {migration} toggles trigger '{trigger}' on '{table}' \
139         by name; that statement fails on any database where the trigger has since been retired. \
140         The runner already suspends every row trigger on the tables a migration writes, so \
141         delete the toggle, or guard it in a DO $$ block that tests pg_trigger first"
142    )]
143    MigrationTogglesTriggerByName {
144        extension: ExtensionId,
145        migration: String,
146        table: String,
147        trigger: String,
148    },
149
150    #[error(
151        "Trigger '{trigger}' on '{table}' runs {function}, which uses '{relation}', and \
152         '{relation}' no longer exists: every write to '{table}' would fail. Retire the trigger in \
153         its extension's `retirements()` (it runs before any migration), or restore the relation"
154    )]
155    DanglingTriggerRoutine {
156        trigger: String,
157        table: String,
158        function: String,
159        relation: String,
160    },
161
162    #[error(
163        "Extension '{extension}' migration {version} ('{name}') has been edited since it was \
164         applied (stored checksum {stored_checksum}, current {current_checksum}). Refusing to \
165         proceed. If the database schema already matches the edited file, run `systemprompt \
166         infra db migrate-repair --reconcile-only --apply` to rewrite the stored checksum \
167         without executing any SQL. To re-execute the edited migration, run `systemprompt infra \
168         db migrate-repair --apply`. Passing --allow-checksum-drift bypasses the check without \
169         fixing it."
170    )]
171    MigrationChecksumDrift {
172        extension: ExtensionId,
173        version: u32,
174        name: String,
175        stored_checksum: String,
176        current_checksum: String,
177    },
178
179    #[error(
180        "Table '{table}' is created by both extension '{extension_a}' and '{extension_b}'; every \
181         table must be declared by exactly one extension"
182    )]
183    DuplicateTableOwner {
184        table: String,
185        extension_a: ExtensionId,
186        extension_b: ExtensionId,
187    },
188
189    #[error(
190        "Extension '{extension}' declares cross_extension_tables() entry '{table}', which is not \
191         a table created by any other loaded extension"
192    )]
193    CrossExtensionTableNotOwned {
194        extension: ExtensionId,
195        table: String,
196    },
197
198    #[error(
199        "Extension '{extension}' seed '{seed}' contains forbidden statement '{statement}'; seeds \
200         may only contain INSERT … ON CONFLICT, UPDATE, MERGE, or WITH … INSERT"
201    )]
202    InvalidSeedStatement {
203        extension: ExtensionId,
204        seed: String,
205        statement: String,
206    },
207
208    #[error(
209        "Extension '{extension}' seed '{seed}' contains a bare INSERT with no ON CONFLICT clause; \
210         seeds run on every boot and must be idempotent — add ON CONFLICT … DO NOTHING/UPDATE"
211    )]
212    SeedInsertNotIdempotent {
213        extension: ExtensionId,
214        seed: String,
215    },
216
217    #[error("Extension '{extension}' seed '{seed}' failed to parse or apply: {context}: {source}")]
218    SeedFailed {
219        extension: ExtensionId,
220        seed: String,
221        context: String,
222        #[source]
223        source: BoxedSource,
224    },
225}
226
227#[derive(Debug, Error)]
228pub enum ExtensionConfigError {
229    #[error("Configuration key '{0}' not found")]
230    NotFound(String),
231
232    #[error("Invalid configuration value for '{key}': {message}")]
233    InvalidValue { key: String, message: String },
234
235    #[error("Failed to parse configuration: {source}")]
236    ParseError {
237        #[source]
238        source: BoxedSource,
239    },
240
241    #[error("Schema validation failed: {0}")]
242    SchemaValidation(#[source] BoxedSource),
243}