Skip to main content

tap_http/external_decision/
protocol.rs

1//! JSON-RPC 2.0 protocol types for external decision communication.
2//!
3//! Messages flow over stdin (tap-http → child) and stdout (child → tap-http)
4//! using newline-delimited JSON.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// JSON-RPC 2.0 request (has an `id` — expects a response)
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct JsonRpcRequest {
12    pub jsonrpc: String,
13    pub id: Value,
14    pub method: String,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub params: Option<Value>,
17}
18
19/// JSON-RPC 2.0 notification (no `id` — fire-and-forget)
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct JsonRpcNotification {
22    pub jsonrpc: String,
23    pub method: String,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub params: Option<Value>,
26}
27
28/// JSON-RPC 2.0 success response
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct JsonRpcResponse {
31    pub jsonrpc: String,
32    pub id: Value,
33    pub result: Value,
34}
35
36/// JSON-RPC 2.0 error response
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct JsonRpcErrorResponse {
39    pub jsonrpc: String,
40    pub id: Value,
41    pub error: JsonRpcError,
42}
43
44/// JSON-RPC 2.0 error object
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct JsonRpcError {
47    pub code: i64,
48    pub message: String,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub data: Option<Value>,
51}
52
53/// A message read from stdout can be a request (tool call) or a notification
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(untagged)]
56pub enum IncomingMessage {
57    Request(JsonRpcRequest),
58    Notification(JsonRpcNotification),
59}
60
61// -----------------------------------------------------------------------
62// tap-http → External Process (stdin)
63// -----------------------------------------------------------------------
64
65/// Initialize message sent when the external process starts
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct InitializeParams {
68    pub version: String,
69    pub agent_dids: Vec<String>,
70    pub subscribe_mode: String,
71    pub capabilities: InitializeCapabilities,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct InitializeCapabilities {
76    pub tools: bool,
77    pub decisions: bool,
78}
79
80/// Decision request sent to external process
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct DecisionRequestParams {
83    pub decision_id: i64,
84    pub transaction_id: String,
85    pub agent_did: String,
86    pub decision_type: String,
87    pub context: Value,
88    pub created_at: String,
89}
90
91/// Event notification sent to external process
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct EventNotificationParams {
94    pub event_type: String,
95    pub agent_did: Option<String>,
96    pub data: Value,
97    pub timestamp: String,
98}
99
100// -----------------------------------------------------------------------
101// External Process → tap-http (stdout)
102// -----------------------------------------------------------------------
103
104/// Decision response from external process
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct DecisionResponse {
107    pub action: String,
108    #[serde(default, skip_serializing_if = "Option::is_none")]
109    pub detail: Option<Value>,
110}
111
112/// Ready notification from external process
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct ReadyParams {
115    #[serde(default)]
116    pub version: Option<String>,
117    #[serde(default)]
118    pub name: Option<String>,
119}
120
121// -----------------------------------------------------------------------
122// Helpers
123// -----------------------------------------------------------------------
124
125impl JsonRpcRequest {
126    pub fn new(id: impl Into<Value>, method: impl Into<String>, params: Option<Value>) -> Self {
127        Self {
128            jsonrpc: "2.0".to_string(),
129            id: id.into(),
130            method: method.into(),
131            params,
132        }
133    }
134}
135
136impl JsonRpcNotification {
137    pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
138        Self {
139            jsonrpc: "2.0".to_string(),
140            method: method.into(),
141            params,
142        }
143    }
144}
145
146impl JsonRpcResponse {
147    pub fn new(id: Value, result: Value) -> Self {
148        Self {
149            jsonrpc: "2.0".to_string(),
150            id,
151            result,
152        }
153    }
154}
155
156impl JsonRpcErrorResponse {
157    pub fn new(id: Value, code: i64, message: impl Into<String>) -> Self {
158        Self {
159            jsonrpc: "2.0".to_string(),
160            id,
161            error: JsonRpcError {
162                code,
163                message: message.into(),
164                data: None,
165            },
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use serde_json::json;
174
175    #[test]
176    fn test_decision_request_serialization() {
177        let params = DecisionRequestParams {
178            decision_id: 42,
179            transaction_id: "txn-123".to_string(),
180            agent_did: "did:key:z6Mk...".to_string(),
181            decision_type: "authorization_required".to_string(),
182            context: json!({
183                "transaction_state": "Received",
184                "pending_agents": ["did:key:z6Mk..."],
185            }),
186            created_at: "2026-02-21T12:00:00Z".to_string(),
187        };
188
189        let req = JsonRpcRequest::new(
190            1,
191            "tap/decision",
192            Some(serde_json::to_value(&params).unwrap()),
193        );
194        let json = serde_json::to_string(&req).unwrap();
195        let parsed: JsonRpcRequest = serde_json::from_str(&json).unwrap();
196
197        assert_eq!(parsed.method, "tap/decision");
198        assert_eq!(parsed.id, json!(1));
199
200        let parsed_params: DecisionRequestParams =
201            serde_json::from_value(parsed.params.unwrap()).unwrap();
202        assert_eq!(parsed_params.decision_id, 42);
203        assert_eq!(parsed_params.transaction_id, "txn-123");
204    }
205
206    #[test]
207    fn test_decision_response_deserialization() {
208        let json = r#"{"action":"authorize","detail":{"settlement_address":"eip155:1:0xABC"}}"#;
209        let resp: DecisionResponse = serde_json::from_str(json).unwrap();
210        assert_eq!(resp.action, "authorize");
211        assert_eq!(resp.detail.unwrap()["settlement_address"], "eip155:1:0xABC");
212    }
213
214    #[test]
215    fn test_event_notification_serialization() {
216        let params = EventNotificationParams {
217            event_type: "message_received".to_string(),
218            agent_did: Some("did:key:z6Mk...".to_string()),
219            data: json!({"message_id": "msg-1", "message_type": "Transfer"}),
220            timestamp: "2026-02-21T12:00:00Z".to_string(),
221        };
222
223        let notif =
224            JsonRpcNotification::new("tap/event", Some(serde_json::to_value(&params).unwrap()));
225        let json = serde_json::to_string(&notif).unwrap();
226
227        // Should not have an id field
228        let parsed: Value = serde_json::from_str(&json).unwrap();
229        assert!(parsed.get("id").is_none());
230        assert_eq!(parsed["method"], "tap/event");
231    }
232
233    #[test]
234    fn test_initialize_params_serialization() {
235        let params = InitializeParams {
236            version: "0.1.0".to_string(),
237            agent_dids: vec!["did:key:z6Mk1".to_string(), "did:key:z6Mk2".to_string()],
238            subscribe_mode: "decisions".to_string(),
239            capabilities: InitializeCapabilities {
240                tools: true,
241                decisions: true,
242            },
243        };
244
245        let json_str = serde_json::to_string(&params).unwrap();
246        let parsed: InitializeParams = serde_json::from_str(&json_str).unwrap();
247
248        assert_eq!(parsed.version, "0.1.0");
249        assert_eq!(parsed.agent_dids.len(), 2);
250        assert!(parsed.capabilities.tools);
251    }
252
253    #[test]
254    fn test_incoming_message_request() {
255        let json = r#"{"jsonrpc":"2.0","id":100,"method":"tools/call","params":{"name":"tap_authorize","arguments":{}}}"#;
256        let msg: IncomingMessage = serde_json::from_str(json).unwrap();
257        match msg {
258            IncomingMessage::Request(req) => {
259                assert_eq!(req.method, "tools/call");
260                assert_eq!(req.id, json!(100));
261            }
262            _ => panic!("Expected request"),
263        }
264    }
265
266    #[test]
267    fn test_incoming_message_notification() {
268        let json = r#"{"jsonrpc":"2.0","method":"tap/ready","params":{"name":"my-engine"}}"#;
269        let msg: IncomingMessage = serde_json::from_str(json).unwrap();
270        match msg {
271            IncomingMessage::Notification(notif) => {
272                assert_eq!(notif.method, "tap/ready");
273            }
274            _ => panic!("Expected notification"),
275        }
276    }
277
278    #[test]
279    fn test_json_rpc_error_response() {
280        let err = JsonRpcErrorResponse::new(json!(1), -32600, "Invalid request");
281        let json_str = serde_json::to_string(&err).unwrap();
282        let parsed: Value = serde_json::from_str(&json_str).unwrap();
283
284        assert_eq!(parsed["error"]["code"], -32600);
285        assert_eq!(parsed["error"]["message"], "Invalid request");
286    }
287
288    #[test]
289    fn test_ready_params_deserialization() {
290        let json = r#"{"version":"1.0.0","name":"my-compliance-engine"}"#;
291        let params: ReadyParams = serde_json::from_str(json).unwrap();
292        assert_eq!(params.version, Some("1.0.0".to_string()));
293        assert_eq!(params.name, Some("my-compliance-engine".to_string()));
294    }
295
296    #[test]
297    fn test_ready_params_minimal() {
298        let json = r#"{}"#;
299        let params: ReadyParams = serde_json::from_str(json).unwrap();
300        assert!(params.version.is_none());
301        assert!(params.name.is_none());
302    }
303}