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}