Skip to main content

tap_node/event/
decision_log_handler.rs

1//! Decision log handler
2//!
3//! Implements the `DecisionHandler` trait by writing decisions to the
4//! `decision_log` table in the agent's SQLite database. This enables
5//! poll-based decision making where an external process (e.g., a separate
6//! tap-mcp instance) can query pending decisions and act on them.
7
8use crate::state_machine::fsm::{Decision, DecisionHandler, TransactionContext};
9use crate::storage::{DecisionType, Storage};
10use async_trait::async_trait;
11use serde_json::json;
12use std::sync::Arc;
13use tracing::{debug, error};
14
15/// Writes decisions to the decision_log table for external polling
16#[derive(Debug)]
17pub struct DecisionLogHandler {
18    storage: Arc<Storage>,
19    agent_dids: Vec<String>,
20}
21
22impl DecisionLogHandler {
23    /// Create a new decision log handler
24    pub fn new(storage: Arc<Storage>, agent_dids: Vec<String>) -> Self {
25        Self {
26            storage,
27            agent_dids,
28        }
29    }
30}
31
32#[async_trait]
33impl DecisionHandler for DecisionLogHandler {
34    async fn handle_decision(&self, ctx: &TransactionContext, decision: &Decision) {
35        let (decision_type, context_json) = match decision {
36            Decision::AuthorizationRequired {
37                transaction_id,
38                pending_agents,
39            } => (
40                DecisionType::AuthorizationRequired,
41                json!({
42                    "transaction_state": ctx.state.to_string(),
43                    "pending_agents": pending_agents,
44                    "transaction_id": transaction_id,
45                }),
46            ),
47            Decision::PolicySatisfactionRequired {
48                transaction_id,
49                requested_by,
50            } => (
51                DecisionType::PolicySatisfactionRequired,
52                json!({
53                    "transaction_state": ctx.state.to_string(),
54                    "requested_by": requested_by,
55                    "transaction_id": transaction_id,
56                }),
57            ),
58            Decision::SettlementRequired { transaction_id } => (
59                DecisionType::SettlementRequired,
60                json!({
61                    "transaction_state": ctx.state.to_string(),
62                    "transaction_id": transaction_id,
63                }),
64            ),
65        };
66
67        let agent_did = self.agent_dids.first().cloned().unwrap_or_default();
68
69        match self
70            .storage
71            .insert_decision(
72                &ctx.transaction_id,
73                &agent_did,
74                decision_type,
75                &context_json,
76            )
77            .await
78        {
79            Ok(decision_id) => {
80                debug!(
81                    "Logged decision {} for transaction {} (poll mode)",
82                    decision_id, ctx.transaction_id
83                );
84            }
85            Err(e) => {
86                error!(
87                    "Failed to log decision for transaction {}: {}",
88                    ctx.transaction_id, e
89                );
90            }
91        }
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::state_machine::fsm::TransactionState;
99    use crate::storage::DecisionStatus;
100
101    #[tokio::test]
102    async fn test_decision_log_handler_writes_to_db() {
103        let storage = Arc::new(Storage::new_in_memory().await.unwrap());
104        let handler =
105            DecisionLogHandler::new(storage.clone(), vec!["did:key:z6MkAgent1".to_string()]);
106
107        let ctx = TransactionContext {
108            transaction_id: "txn-dlh-1".to_string(),
109            state: TransactionState::Received,
110            agents: Default::default(),
111            has_pending_policies: false,
112        };
113
114        let decision = Decision::AuthorizationRequired {
115            transaction_id: "txn-dlh-1".to_string(),
116            pending_agents: vec!["did:key:z6MkAgent1".to_string()],
117        };
118
119        handler.handle_decision(&ctx, &decision).await;
120
121        let entries = storage
122            .list_decisions(
123                Some("did:key:z6MkAgent1"),
124                Some(DecisionStatus::Pending),
125                None,
126                100,
127            )
128            .await
129            .unwrap();
130        assert_eq!(entries.len(), 1);
131        assert_eq!(entries[0].transaction_id, "txn-dlh-1");
132        assert_eq!(
133            entries[0].decision_type,
134            DecisionType::AuthorizationRequired
135        );
136    }
137
138    #[tokio::test]
139    async fn test_decision_log_handler_settlement() {
140        let storage = Arc::new(Storage::new_in_memory().await.unwrap());
141        let handler =
142            DecisionLogHandler::new(storage.clone(), vec!["did:key:z6MkAgent1".to_string()]);
143
144        let ctx = TransactionContext {
145            transaction_id: "txn-dlh-2".to_string(),
146            state: TransactionState::ReadyToSettle,
147            agents: Default::default(),
148            has_pending_policies: false,
149        };
150
151        let decision = Decision::SettlementRequired {
152            transaction_id: "txn-dlh-2".to_string(),
153        };
154
155        handler.handle_decision(&ctx, &decision).await;
156
157        let entries = storage
158            .list_decisions(
159                Some("did:key:z6MkAgent1"),
160                Some(DecisionStatus::Pending),
161                None,
162                100,
163            )
164            .await
165            .unwrap();
166        assert_eq!(entries.len(), 1);
167        assert_eq!(entries[0].decision_type, DecisionType::SettlementRequired);
168    }
169}