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}