Skip to main content

systemprompt_models/profile/
governance.rs

1//! Governance configuration for the gateway + MCP authorization hook.
2//!
3//! Authz is **fail-closed** with an explicit-opt-in surface. Four modes:
4//!
5//! - `webhook` — production. Core POSTs every request to the configured URL;
6//!   any transport error, non-2xx, or decode failure denies the request.
7//! - `extension` — production. The hook is supplied at bootstrap by the binary
8//!   via `AppContextBuilder::with_authz_hook(...)`. Bootstrap errors if no hook
9//!   is supplied. See `internal/guides/authz.md`.
10//! - `disabled` — denies every request via `DenyAllHook`. Use when authz is
11//!   intentionally inactive but you want the surface installed.
12//! - `unrestricted` — TEST/DEV ONLY. Allows every request via `AllowAllHook`.
13//!   Requires `acknowledgement` to equal the literal sentence `"I understand
14//!   this disables all authorization"`. Bootstrap errors otherwise.
15//!
16//! Absent `governance` block, absent `authz`, or any unparseable config →
17//! bootstrap installs `DenyAllHook` (everything denied) so misconfiguration
18//! never silently grants access.
19//!
20//! `audit` bounds what the gateway retains per request. A body at or under
21//! `payload_cap_bytes` is stored whole in `ai_request_payloads`; over it, only
22//! the SHA-256 digest and a head+tail excerpt survive. The digest always covers
23//! the full bytes, so a capped capture still proves which body was sent.
24//!
25//! Example:
26//!
27//! ```yaml
28//! governance:
29//!   authz:
30//!     hook:
31//!       mode: webhook
32//!       url: http://localhost:8080/api/public/govern/authz
33//!       timeout_ms: 500
34//!   audit:
35//!     payload_cap_bytes: 4194304
36//! ```
37//!
38//! Copyright (c) systemprompt.io — Business Source License 1.1.
39//! See <https://systemprompt.io> for licensing details.
40
41use serde::{Deserialize, Serialize};
42
43pub const UNRESTRICTED_ACKNOWLEDGEMENT: &str = "I understand this disables all authorization";
44
45#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
46#[serde(deny_unknown_fields)]
47pub struct GovernanceConfig {
48    #[serde(default)]
49    pub authz: Option<AuthzConfig>,
50    #[serde(default)]
51    pub audit: AuditConfig,
52}
53
54/// Retention bounds for the gateway audit trail.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
56#[serde(deny_unknown_fields)]
57pub struct AuditConfig {
58    #[serde(default = "default_payload_cap_bytes")]
59    pub payload_cap_bytes: usize,
60}
61
62impl AuditConfig {
63    pub const DEFAULT_PAYLOAD_CAP_BYTES: usize = 1024 * 1024;
64    pub const MIN_PAYLOAD_CAP_BYTES: usize = 64 * 1024;
65}
66
67impl Default for AuditConfig {
68    fn default() -> Self {
69        Self {
70            payload_cap_bytes: Self::DEFAULT_PAYLOAD_CAP_BYTES,
71        }
72    }
73}
74
75const fn default_payload_cap_bytes() -> usize {
76    AuditConfig::DEFAULT_PAYLOAD_CAP_BYTES
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
80#[serde(deny_unknown_fields)]
81pub struct AuthzConfig {
82    pub hook: AuthzHookConfig,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
86#[serde(rename_all = "lowercase")]
87pub enum AuthzMode {
88    Webhook,
89    Extension,
90    Disabled,
91    Unrestricted,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
95#[serde(deny_unknown_fields)]
96pub struct AuthzHookConfig {
97    pub mode: AuthzMode,
98    #[serde(default)]
99    pub url: Option<String>,
100    #[serde(default = "default_timeout_ms")]
101    pub timeout_ms: u64,
102    #[serde(default)]
103    pub acknowledgement: Option<String>,
104}
105
106const fn default_timeout_ms() -> u64 {
107    500
108}