systemprompt_security/authz/audit/
repository.rs1use sqlx::PgPool;
13use systemprompt_identifiers::Actor;
14
15use crate::authz::types::DecisionTag;
16use crate::policy::types::AccessScope;
17
18pub const AUDIT_WRITE_FAILED_TOTAL: &str = "governance_audit_write_failed_total";
19
20#[derive(Debug)]
21pub struct GovernanceDecisionRecord<'a> {
22 pub id: &'a str,
23 pub actor: &'a Actor,
24 pub session_id: &'a str,
25 pub tool_name: &'a str,
26 pub agent_id: Option<&'a str>,
27 pub agent_scope: Option<AccessScope>,
28 pub decision: DecisionTag,
29 pub policy: &'a str,
30 pub reason: &'a str,
31 pub evaluated_rules: &'a serde_json::Value,
34 pub plugin_id: Option<&'a str>,
35 pub act_chain: &'a [Actor],
36 pub context_id: &'a str,
37 pub task_id: Option<&'a str>,
38 pub trace_id: Option<&'a str>,
41}
42
43#[derive(Debug, Clone)]
44pub struct GovernanceDecisionRepository {
45 pool: std::sync::Arc<PgPool>,
46}
47
48impl GovernanceDecisionRepository {
49 pub const fn from_pool(pool: std::sync::Arc<PgPool>) -> Self {
50 Self { pool }
51 }
52
53 pub fn pool(&self) -> &PgPool {
54 &self.pool
55 }
56
57 pub async fn insert(&self, record: &GovernanceDecisionRecord<'_>) -> Result<(), sqlx::Error> {
58 insert_governance_decision(&self.pool, record).await
59 }
60}
61
62pub async fn insert_governance_decision(
63 pool: &PgPool,
64 record: &GovernanceDecisionRecord<'_>,
65) -> Result<(), sqlx::Error> {
66 let actor_kind = record.actor.kind.tag();
67 let actor_id = record.actor.kind.actor_id(&record.actor.user_id);
68 let act_chain =
69 serde_json::to_value(record.act_chain).unwrap_or_else(|_| serde_json::json!([]));
70 let result = sqlx::query!(
71 "INSERT INTO governance_decisions (id, user_id, session_id, tool_name, agent_id, \
72 agent_scope, decision, policy, reason, evaluated_rules, plugin_id, actor_kind, actor_id, \
73 act_chain, context_id, task_id, trace_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, \
74 $10, $11, $12, $13, $14, $15, $16, $17)",
75 record.id,
76 record.actor.user_id.as_str(),
77 record.session_id,
78 record.tool_name,
79 record.agent_id,
80 record.agent_scope.map(AccessScope::as_str),
81 record.decision.as_str(),
82 record.policy,
83 record.reason,
84 record.evaluated_rules,
85 record.plugin_id,
86 actor_kind.as_str(),
87 actor_id,
88 act_chain,
89 record.context_id,
90 record.task_id,
91 record.trace_id,
92 )
93 .execute(pool)
94 .await;
95 if let Err(error) = &result {
96 tracing::error!(
97 error = %error,
98 actor_kind = actor_kind.as_str(),
99 actor_id,
100 policy = record.policy,
101 decision = record.decision.as_str(),
102 session_id = record.session_id,
103 "governance_decisions insert failed; audit row dropped"
104 );
105 metrics::counter!(
106 AUDIT_WRITE_FAILED_TOTAL,
107 "actor_kind" => actor_kind.as_str(),
108 "decision" => record.decision.as_str(),
109 "policy" => record.policy.to_owned(),
110 )
111 .increment(1);
112 }
113 result.map(|_| ())
114}