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/// Returns the composed extension hook from every
50/// [`crate::register_authz_hook!`] submission in the binary, or `None` if no
51/// submissions exist.
52#[must_use]
53pub fn discover_authz_hook(ctx: &AuthzHookContext) -> Option<SharedAuthzHook> {
54 let hooks: Vec<SharedAuthzHook> = inventory::iter::<AuthzHookRegistration>()
55 .map(|reg| (reg.factory)(ctx))
56 .collect();
57 match hooks.len() {
58 0 => None,
59 1 => hooks.into_iter().next(),
60 _ => Some(Arc::new(CompositeAuthzHook::new(hooks))),
61 }
62}
63
64/// Register an extension authz hook factory at static-init time.
65///
66/// The factory receives a borrowed [`AuthzHookContext`] (pool + audit sink)
67/// and returns the constructed hook. Wire alongside `register_extension!`
68/// in the extension's `extension.rs`:
69///
70/// ```ignore
71/// systemprompt_security::register_authz_hook!(|ctx| {
72/// std::sync::Arc::new(MyHook::new(ctx.pool.clone(), ctx.sink.clone()))
73/// as systemprompt_security::authz::SharedAuthzHook
74/// });
75/// ```
76#[macro_export]
77macro_rules! register_authz_hook {
78 ($factory:expr) => {
79 ::inventory::submit! {
80 $crate::authz::AuthzHookRegistration {
81 factory: $factory,
82 }
83 }
84 };
85}