1use serde::{Deserialize, Deserializer, Serialize};
2use std::collections::HashMap;
3
4fn null_to_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
12where
13 D: Deserializer<'de>,
14 T: Default + Deserialize<'de>,
15{
16 let opt = Option::<T>::deserialize(deserializer)?;
17 Ok(opt.unwrap_or_default())
18}
19
20#[derive(Debug, Serialize, Deserialize, Clone)]
21pub struct MediaContent {
22 pub id: String,
23 pub r#type: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub url: Option<String>,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub base64: Option<String>,
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone)]
31pub struct ClientRequest {
32 pub query: String,
33 pub user_token: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub client_id: Option<String>,
36 pub request_type: String,
37 #[serde(default)]
38 pub context: HashMap<String, serde_json::Value>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub media: Option<Vec<MediaContent>>,
41}
42
43#[must_use]
44#[derive(Debug, Serialize, Deserialize, Clone)]
45pub struct ClientResponse {
46 pub success: bool,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub data: Option<serde_json::Value>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub result: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub plan_id: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub request_id: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub metadata: Option<HashMap<String, serde_json::Value>>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub error: Option<String>,
59 #[serde(default)]
60 pub blocked: bool,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub block_reason: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub policy_info: Option<PolicyEvaluationInfo>,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub budget_info: Option<BudgetInfo>,
67}
68
69impl ClientResponse {
70 pub fn fail_open(error: crate::error::AxonFlowError) -> Self {
71 Self {
72 success: true,
73 data: None,
74 result: None,
75 plan_id: None,
76 request_id: None,
77 metadata: None,
78 error: Some(format!("AxonFlow unavailable (fail-open): {}", error)),
79 blocked: false,
80 block_reason: None,
81 policy_info: None,
82 budget_info: None,
83 }
84 }
85}
86
87#[derive(Debug, Serialize, Deserialize, Clone)]
88pub struct BudgetInfo {
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub budget_id: Option<String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub budget_name: Option<String>,
93 pub used_usd: f64,
94 pub limit_usd: f64,
95 pub percentage: f64,
96 pub exceeded: bool,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub action: Option<String>,
99}
100
101#[derive(Debug, Serialize, Deserialize, Clone, Default)]
102#[serde(default)]
103pub struct PolicyEvaluationInfo {
104 #[serde(deserialize_with = "null_to_default")]
105 pub policies_evaluated: Vec<String>,
106 #[serde(deserialize_with = "null_to_default")]
107 pub static_checks: Vec<String>,
108 pub processing_time: String,
109 pub tenant_id: String,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub code_artifact: Option<CodeArtifact>,
112}
113
114#[derive(Debug, Serialize, Deserialize, Clone, Default)]
115pub struct TokenUsage {
116 pub prompt_tokens: usize,
117 pub completion_tokens: usize,
118 pub total_tokens: usize,
119}
120
121#[derive(Debug, Clone)]
122pub struct AuditRequest {
123 pub context_id: String,
124 pub response_summary: String,
125 pub provider: String,
126 pub model: String,
127 pub token_usage: TokenUsage,
128 pub latency_ms: i64,
129 pub metadata: Option<HashMap<String, serde_json::Value>>,
130}
131
132#[derive(Debug, Serialize, Deserialize, Clone, Default)]
133#[serde(default)]
134pub struct AuditResult {
135 pub success: bool,
136 pub audit_id: String,
137}
138
139#[derive(Debug, Serialize, Deserialize, Clone, Default)]
141#[serde(default)]
142pub struct AuditLogEntry {
143 pub id: String,
144 pub request_id: String,
145 pub timestamp: String,
146 pub user_email: String,
147 pub client_id: String,
148 pub tenant_id: String,
149 pub request_type: String,
150 pub query_summary: String,
151 pub success: bool,
152 pub blocked: bool,
153 pub risk_score: f64,
154 pub provider: String,
155 pub model: String,
156 pub tokens_used: i64,
157 pub latency_ms: i64,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
159 pub policy_violations: Vec<String>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub metadata: Option<HashMap<String, serde_json::Value>>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub data_residency: Option<String>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub transfer_basis: Option<String>,
166}
167
168#[derive(Debug, Serialize, Deserialize, Clone, Default)]
169#[serde(default)]
170pub struct ConnectorMetadata {
171 pub id: String,
172 pub name: String,
173 #[serde(rename = "type")]
174 pub r#type: String,
175 pub version: String,
176 pub description: String,
177 pub category: String,
178 pub icon: String,
179 #[serde(deserialize_with = "null_to_default")]
180 pub tags: Vec<String>,
181 #[serde(deserialize_with = "null_to_default")]
182 pub capabilities: Vec<String>,
183 #[serde(deserialize_with = "null_to_default")]
184 pub config_schema: HashMap<String, serde_json::Value>,
185 pub installed: bool,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub instance_name: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub healthy: Option<bool>,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub last_check: Option<String>,
192}
193
194#[derive(Debug, Serialize, Deserialize, Clone, Default)]
195#[serde(default)]
196pub struct ConnectorHealthStatus {
197 pub healthy: bool,
198 pub latency: i64,
199 #[serde(deserialize_with = "null_to_default")]
200 pub details: HashMap<String, String>,
201 pub timestamp: String,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub error: Option<String>,
204}
205
206#[derive(Debug, Serialize, Deserialize, Clone)]
207pub struct ConnectorInstallRequest {
208 pub connector_id: String,
209 pub name: String,
210 pub tenant_id: String,
211 pub options: HashMap<String, serde_json::Value>,
212 pub credentials: HashMap<String, String>,
213}
214
215#[must_use]
216#[derive(Debug, Serialize, Deserialize, Clone)]
217pub struct ConnectorResponse {
218 pub success: bool,
219 pub data: serde_json::Value,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub error: Option<String>,
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub meta: Option<HashMap<String, serde_json::Value>>,
224 #[serde(default)]
225 pub redacted: bool,
226 #[serde(default, deserialize_with = "null_to_default")]
227 pub redacted_fields: Vec<String>,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub policy_info: Option<PolicyInfo>,
230}
231
232#[derive(Debug, Serialize, Deserialize, Clone)]
233pub struct PolicyInfo {
234 pub policies_evaluated: usize,
235 pub blocked: bool,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub block_reason: Option<String>,
238 pub redactions_applied: usize,
239 pub processing_time_ms: i64,
240 #[serde(default, deserialize_with = "null_to_default")]
241 pub matched_policies: Vec<PolicyMatchInfo>,
242}
243
244#[derive(Debug, Serialize, Deserialize, Clone)]
245pub struct PolicyMatchInfo {
246 pub policy_id: String,
247 pub policy_name: String,
248 pub category: String,
249 pub severity: String,
250 pub action: String,
251}
252
253#[derive(Debug, Serialize, Deserialize, Clone, Default)]
254pub struct PlanStep {
255 #[serde(default)]
256 pub id: String,
257 #[serde(default)]
258 pub name: String,
259 #[serde(default, rename = "type")]
260 pub r#type: String,
261 #[serde(default)]
262 pub description: String,
263 #[serde(default, deserialize_with = "null_to_default")]
264 pub dependencies: Vec<String>,
265 #[serde(default)]
266 pub agent: String,
267 #[serde(default, deserialize_with = "null_to_default")]
268 pub parameters: HashMap<String, serde_json::Value>,
269 #[serde(default)]
270 pub estimated_time: String,
271}
272
273#[must_use]
274#[derive(Debug, Serialize, Deserialize, Clone, Default)]
275#[serde(default)]
276pub struct PlanResponse {
277 pub plan_id: String,
278 pub status: String,
279 #[serde(deserialize_with = "null_to_default")]
280 pub steps: Vec<PlanStep>,
281 pub domain: String,
282 pub complexity: i32,
283 pub parallel: bool,
284 pub estimated_duration: String,
285 #[serde(deserialize_with = "null_to_default")]
286 pub metadata: HashMap<String, serde_json::Value>,
287 pub success: bool,
288 pub version: i32,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 pub result: Option<serde_json::Value>,
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub error: Option<String>,
293}
294
295#[derive(Debug, Serialize, Deserialize, Clone, Default)]
296#[serde(default)]
297pub struct StepResult {
298 pub step_id: String,
299 pub step_name: String,
300 pub status: String,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub result: Option<serde_json::Value>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub error: Option<String>,
305 pub duration: String,
306}
307
308#[must_use]
309#[derive(Debug, Serialize, Deserialize, Clone, Default)]
310#[serde(default)]
311pub struct PlanExecutionResponse {
312 pub plan_id: String,
313 pub status: String,
314 #[serde(skip_serializing_if = "Option::is_none")]
315 pub workflow_id: Option<String>,
316 #[serde(skip_serializing_if = "Option::is_none")]
317 pub result: Option<String>,
318 #[serde(deserialize_with = "null_to_default")]
319 pub step_results: Vec<StepResult>,
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub error: Option<String>,
322 pub duration: String,
323 pub completed_steps: i32,
324 pub total_steps: i32,
325}
326
327#[derive(Debug, Serialize, Deserialize, Clone, Default)]
328#[serde(default)]
329pub struct CancelPlanResponse {
330 pub plan_id: String,
331 pub status: String,
332 pub success: bool,
333}
334
335#[derive(Debug, Serialize, Deserialize, Clone)]
336pub struct CodeArtifact {
337 pub is_code_output: bool,
338 pub language: String,
339 pub code_type: String,
340 pub size_bytes: usize,
341 pub line_count: usize,
342 pub secrets_detected: usize,
343 pub unsafe_patterns: usize,
344 #[serde(default, deserialize_with = "null_to_default")]
345 pub policies_checked: Vec<String>,
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn audit_log_entry_with_residency_fields_deserializes() {
354 let json = r#"{
355 "id": "audit-001",
356 "request_id": "req-123",
357 "timestamp": "2026-05-26T00:00:00Z",
358 "user_email": "user@example.com",
359 "client_id": "client-1",
360 "tenant_id": "tenant-1",
361 "request_type": "llm_query",
362 "query_summary": "test query",
363 "success": true,
364 "blocked": false,
365 "risk_score": 0.15,
366 "provider": "openai",
367 "model": "gpt-4",
368 "tokens_used": 500,
369 "latency_ms": 120,
370 "policy_violations": [],
371 "data_residency": "id-jakarta",
372 "transfer_basis": "pdp-consent"
373 }"#;
374
375 let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
376 assert_eq!(entry.id, "audit-001");
377 assert_eq!(entry.data_residency.as_deref(), Some("id-jakarta"));
378 assert_eq!(entry.transfer_basis.as_deref(), Some("pdp-consent"));
379 assert!(entry.success);
380 assert!(!entry.blocked);
381 assert_eq!(entry.tokens_used, 500);
382 }
383
384 #[test]
385 fn audit_log_entry_without_new_fields_deserializes_backward_compat() {
386 let json = r#"{
387 "id": "audit-002",
388 "request_id": "req-456",
389 "timestamp": "2026-05-26T00:00:00Z",
390 "user_email": "user@example.com",
391 "client_id": "client-1",
392 "tenant_id": "tenant-1",
393 "request_type": "llm_query",
394 "query_summary": "test query",
395 "success": true,
396 "blocked": false,
397 "risk_score": 0.0,
398 "provider": "anthropic",
399 "model": "claude-3",
400 "tokens_used": 100,
401 "latency_ms": 50
402 }"#;
403
404 let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
405 assert_eq!(entry.id, "audit-002");
406 assert!(entry.data_residency.is_none());
407 assert!(entry.transfer_basis.is_none());
408 assert!(entry.policy_violations.is_empty());
409 assert!(entry.metadata.is_none());
410 }
411
412 #[test]
413 fn audit_log_entry_empty_optional_fields_omitted_in_serialization() {
414 let entry = AuditLogEntry {
415 id: "audit-003".to_string(),
416 request_id: "req-789".to_string(),
417 success: true,
418 ..Default::default()
419 };
420
421 let json = serde_json::to_string(&entry).unwrap();
422 assert!(!json.contains("data_residency"));
424 assert!(!json.contains("transfer_basis"));
425 assert!(!json.contains("metadata"));
426 assert!(!json.contains("policy_violations"));
428 }
429
430 #[test]
431 fn audit_log_entry_with_metadata_round_trips() {
432 let mut meta = HashMap::new();
433 meta.insert(
434 "region".to_string(),
435 serde_json::Value::String("ap-southeast-3".to_string()),
436 );
437
438 let entry = AuditLogEntry {
439 id: "audit-004".to_string(),
440 data_residency: Some("id-jakarta".to_string()),
441 transfer_basis: Some("pdp-consent".to_string()),
442 metadata: Some(meta),
443 ..Default::default()
444 };
445
446 let json = serde_json::to_string(&entry).unwrap();
447 let back: AuditLogEntry = serde_json::from_str(&json).unwrap();
448 assert_eq!(back.data_residency.as_deref(), Some("id-jakarta"));
449 assert_eq!(back.transfer_basis.as_deref(), Some("pdp-consent"));
450 assert!(back.metadata.is_some());
451 assert_eq!(
452 back.metadata.unwrap().get("region").unwrap(),
453 &serde_json::Value::String("ap-southeast-3".to_string())
454 );
455 }
456}