Extension Discovery
ExtensionRegistry::discover() collects inventory registrations, instantiates factories, validates dependencies, and sorts topologically with cycle detection.
On this page
At startup, ExtensionRegistry::discover() collects all registered extensions, validates them, and sorts them for loading.
Discovery Process
let registry = ExtensionRegistry::discover()?;
This performs:
- Collection - Iterate
inventory::iter::<ExtensionRegistration> - Instantiation - Call each factory to create extension instances
- Merging - Include any runtime-injected extensions not already discovered
- Sorting - Topological order by dependencies, priority as tie-break (returns
LoaderError::DependencyCycleon a cycle)
Dependency validation is a separate step: registry.validate() (called by discover_and_merge) checks that every declared dependency is registered.
ExtensionRegistry
pub struct ExtensionRegistry {
extensions: HashMap<String, Arc<dyn Extension>>,
sorted_extensions: Vec<Arc<dyn Extension>>,
}
impl ExtensionRegistry {
pub fn discover() -> Result<Self, LoaderError>;
pub fn discover_and_merge(injected: Vec<Arc<dyn Extension>>) -> Result<Self, LoaderError>;
pub fn get(&self, id: &str) -> Option<&Arc<dyn Extension>>;
pub fn extensions(&self) -> &[Arc<dyn Extension>];
pub fn validate(&self) -> Result<(), LoaderError>;
}
Sorting
Extensions are ordered topologically by Extension::dependencies(), with Extension::priority() breaking ties (lower values first). A dependency declared but not loaded in the build is warned and ignored for ordering; a cycle fails discovery with LoaderError::DependencyCycle and a human-readable chain ("A -> B -> A").
Typical priorities:
1-10- Core infrastructure (database, users)10-50- Domain extensions50-100- Feature extensions100+- Optional/plugin extensions
Validation
Dependency Validation
pub fn validate_dependencies(&self) -> Result<(), LoaderError> {
for ext in self.extensions.values() {
for dep_id in ext.dependencies() {
if !self.extensions.contains_key(dep_id) {
return Err(LoaderError::MissingDependency {
extension: ext.id().to_owned(),
dependency: dep_id.to_owned(),
});
}
}
}
let ids: Vec<String> = self.extensions.keys().cloned().collect();
topo_sort(&ids, &self.extensions).map(|_| ())
}
Cycle Detection
Cycle detection is part of the topological sort: both the sorting done during discovery and validate_dependencies() run topo_sort, which returns LoaderError::DependencyCycle with the offending chain when a cycle exists.
Path Validation
API base paths must start with /api/ and must not collide with reserved path prefixes:
pub const RESERVED_PATHS: &[&str] = &[
"/api/v1/oauth",
"/api/v1/users",
"/api/v1/agents",
// ...
];
pub fn validate_api_paths(&self, ctx: &dyn ExtensionContext) -> Result<(), LoaderError> {
for ext in self.extensions.values() {
if let Some(router_config) = ext.router(ctx) {
let base_path = router_config.base_path;
if !base_path.starts_with("/api/") {
return Err(LoaderError::InvalidBasePath {
extension: ext.id().to_owned(),
path: base_path.to_owned(),
});
}
for reserved in RESERVED_PATHS {
if base_path.starts_with(reserved) {
return Err(LoaderError::ReservedPathCollision {
extension: ext.id().to_owned(),
path: base_path.to_owned(),
});
}
}
}
}
Ok(())
}
Filtering
Get extensions with specific capabilities:
// Extensions with schemas
let schema_exts = registry.schema_extensions();
// Extensions with jobs
let job_exts = registry.job_extensions();
// Extensions with routers (needs an ExtensionContext)
let api_exts = registry.api_extensions(&ctx);
Runtime Injection
Extensions can be injected programmatically:
use systemprompt::extension::runtime_config::{set_injected_extensions, InjectedExtensions};
let injected = InjectedExtensions {
extensions: vec![Arc::new(TestExtension)],
..InjectedExtensions::default()
};
set_injected_extensions(injected).expect("injected extensions already set");
set_injected_extensions uses a process-wide OnceLock, so it can only be called once. Injected extensions are merged during discovery; ids already discovered via inventory win. InjectedExtensions also carries a web_assets: WebAssetsStrategy field for hosts that cannot rely on linker-based asset registration.
Debugging
List Extensions
systemprompt plugins list
Show Extension Details
systemprompt plugins show <id>
Validate
systemprompt plugins validate --verbose
Common Errors
MissingDependency:
Extension 'my-ext' requires dependency 'users' which is not registered
Fix: Ensure dependency is linked and registered.
DependencyCycle:
Dependency cycle detected while ordering extensions: a -> b -> c -> a
Fix: Break the cycle by restructuring dependencies.
ReservedPathCollision:
Extension 'my-ext' uses reserved API path '/api/v1/users'
Fix: Use a different base path.