Skip to main content

systemprompt_security/authz/
registry.rs

1//! Inventory-based registration for extension-built authz hooks.
2//!
3//! Companion to [`AppContextBuilder::with_authz_hook`][with]: binaries that
4//! delegate to `systemprompt::cli::run()` have no builder site to call, so
5//! they register a hook factory at static-init time via
6//! [`crate::register_authz_hook!`]. [`build_authz_hook`][bah] consults this
7//! registry when no builder-supplied hook is present and the profile selects
8//! `mode: extension`.
9//!
10//! Multiple registrations are auto-composed into a [`CompositeAuthzHook`] in
11//! collection order. For deterministic ordering across many extensions,
12//! register a single factory that returns a pre-composed hook.
13//!
14//! [with]: ../../../runtime/struct.AppContextBuilder.html#method.with_authz_hook
15//! [bah]: super::runtime::build_authz_hook
16
17use std::sync::Arc;
18
19use super::audit::AuthzAuditSink;
20use super::composite::CompositeAuthzHook;
21use super::hook::SharedAuthzHook;
22
23/// Inputs passed to every registered factory at bootstrap.
24///
25/// `pool` is the write-side Postgres pool already used by the audit sink;
26/// `sink` is the same [`DbAuditSink`][super::audit::DbAuditSink] core uses
27/// internally so extension hooks record through one consistent audit path.
28#[derive(Clone)]
29pub struct AuthzHookContext {
30    pub pool: Arc<sqlx::PgPool>,
31    pub sink: Arc<dyn AuthzAuditSink>,
32}
33
34impl std::fmt::Debug for AuthzHookContext {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        f.debug_struct("AuthzHookContext").finish_non_exhaustive()
37    }
38}
39
40/// One inventory submission per [`crate::register_authz_hook!`] call. The
41/// factory runs once at `AppContext` build time and must not block.
42#[derive(Debug, Clone, Copy)]
43pub struct AuthzHookRegistration {
44    pub factory: fn(&AuthzHookContext) -> SharedAuthzHook,
45}
46
47inventory::collect!(AuthzHookRegistration);
48
49#[must_use]
50pub fn discover_authz_hook(ctx: &AuthzHookContext) -> Option<SharedAuthzHook> {
51    let hooks: Vec<SharedAuthzHook> = inventory::iter::<AuthzHookRegistration>()
52        .map(|reg| (reg.factory)(ctx))
53        .collect();
54    match hooks.len() {
55        0 => None,
56        1 => hooks.into_iter().next(),
57        _ => Some(Arc::new(CompositeAuthzHook::new(hooks))),
58    }
59}
60
61/// Register an extension authz hook factory at static-init time.
62///
63/// The factory receives a borrowed [`AuthzHookContext`] (pool + audit sink)
64/// and returns the constructed hook. Wire alongside `register_extension!`
65/// in the extension's `extension.rs`:
66///
67/// ```ignore
68/// systemprompt_security::register_authz_hook!(|ctx| {
69///     std::sync::Arc::new(MyHook::new(ctx.pool.clone(), ctx.sink.clone()))
70///         as systemprompt_security::authz::SharedAuthzHook
71/// });
72/// ```
73#[macro_export]
74macro_rules! register_authz_hook {
75    ($factory:expr) => {
76        ::inventory::submit! {
77            $crate::authz::AuthzHookRegistration {
78                factory: $factory,
79            }
80        }
81    };
82}