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