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")]
170 pub transfer_basis: Option<String>,
171}
172
173pub mod transfer_basis {
184 pub const ADEQUACY: &str = "adequacy";
185 pub const SAFEGUARDS: &str = "safeguards";
186 pub const PASAL_56B_DPA: &str = "pasal_56b_dpa";
187 pub const CONSENT: &str = "consent";
188}
189
190#[derive(Debug, Serialize, Deserialize, Clone, Default)]
191#[serde(default)]
192pub struct ConnectorMetadata {
193 pub id: String,
194 pub name: String,
195 #[serde(rename = "type")]
196 pub r#type: String,
197 pub version: String,
198 pub description: String,
199 pub category: String,
200 pub icon: String,
201 #[serde(deserialize_with = "null_to_default")]
202 pub tags: Vec<String>,
203 #[serde(deserialize_with = "null_to_default")]
204 pub capabilities: Vec<String>,
205 #[serde(deserialize_with = "null_to_default")]
206 pub config_schema: HashMap<String, serde_json::Value>,
207 pub installed: bool,
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub instance_name: Option<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub healthy: Option<bool>,
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub last_check: Option<String>,
214}
215
216#[derive(Debug, Serialize, Deserialize, Clone, Default)]
217#[serde(default)]
218pub struct ConnectorHealthStatus {
219 pub healthy: bool,
220 pub latency: i64,
221 #[serde(deserialize_with = "null_to_default")]
222 pub details: HashMap<String, String>,
223 pub timestamp: String,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub error: Option<String>,
226}
227
228#[derive(Debug, Serialize, Deserialize, Clone)]
229pub struct ConnectorInstallRequest {
230 pub connector_id: String,
231 pub name: String,
232 pub tenant_id: String,
233 pub options: HashMap<String, serde_json::Value>,
234 pub credentials: HashMap<String, String>,
235}
236
237#[must_use]
238#[derive(Debug, Serialize, Deserialize, Clone)]
239pub struct ConnectorResponse {
240 pub success: bool,
241 pub data: serde_json::Value,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub error: Option<String>,
244 #[serde(skip_serializing_if = "Option::is_none")]
245 pub meta: Option<HashMap<String, serde_json::Value>>,
246 #[serde(default)]
247 pub redacted: bool,
248 #[serde(default, deserialize_with = "null_to_default")]
249 pub redacted_fields: Vec<String>,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub policy_info: Option<PolicyInfo>,
252}
253
254#[derive(Debug, Serialize, Deserialize, Clone)]
255pub struct PolicyInfo {
256 pub policies_evaluated: usize,
257 pub blocked: bool,
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub block_reason: Option<String>,
260 pub redactions_applied: usize,
261 pub processing_time_ms: i64,
262 #[serde(default, deserialize_with = "null_to_default")]
263 pub matched_policies: Vec<PolicyMatchInfo>,
264}
265
266#[derive(Debug, Serialize, Deserialize, Clone)]
267pub struct PolicyMatchInfo {
268 pub policy_id: String,
269 pub policy_name: String,
270 pub category: String,
271 pub severity: String,
272 pub action: String,
273}
274
275#[derive(Debug, Serialize, Deserialize, Clone, Default)]
276pub struct PlanStep {
277 #[serde(default)]
278 pub id: String,
279 #[serde(default)]
280 pub name: String,
281 #[serde(default, rename = "type")]
282 pub r#type: String,
283 #[serde(default)]
284 pub description: String,
285 #[serde(default, deserialize_with = "null_to_default")]
286 pub dependencies: Vec<String>,
287 #[serde(default)]
288 pub agent: String,
289 #[serde(default, deserialize_with = "null_to_default")]
290 pub parameters: HashMap<String, serde_json::Value>,
291 #[serde(default)]
292 pub estimated_time: String,
293}
294
295#[must_use]
296#[derive(Debug, Serialize, Deserialize, Clone, Default)]
297#[serde(default)]
298pub struct PlanResponse {
299 pub plan_id: String,
300 pub status: String,
301 #[serde(deserialize_with = "null_to_default")]
302 pub steps: Vec<PlanStep>,
303 pub domain: String,
304 pub complexity: i32,
305 pub parallel: bool,
306 pub estimated_duration: String,
307 #[serde(deserialize_with = "null_to_default")]
308 pub metadata: HashMap<String, serde_json::Value>,
309 pub success: bool,
310 pub version: i32,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub result: Option<serde_json::Value>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub error: Option<String>,
315}
316
317#[derive(Debug, Serialize, Deserialize, Clone, Default)]
318#[serde(default)]
319pub struct StepResult {
320 pub step_id: String,
321 pub step_name: String,
322 pub status: String,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub result: Option<serde_json::Value>,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub error: Option<String>,
327 pub duration: String,
328}
329
330#[must_use]
331#[derive(Debug, Serialize, Deserialize, Clone, Default)]
332#[serde(default)]
333pub struct PlanExecutionResponse {
334 pub plan_id: String,
335 pub status: String,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub workflow_id: Option<String>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub result: Option<String>,
340 #[serde(deserialize_with = "null_to_default")]
341 pub step_results: Vec<StepResult>,
342 #[serde(skip_serializing_if = "Option::is_none")]
343 pub error: Option<String>,
344 pub duration: String,
345 pub completed_steps: i32,
346 pub total_steps: i32,
347}
348
349#[derive(Debug, Serialize, Deserialize, Clone, Default)]
350#[serde(default)]
351pub struct CancelPlanResponse {
352 pub plan_id: String,
353 pub status: String,
354 pub success: bool,
355}
356
357#[derive(Debug, Serialize, Deserialize, Clone)]
358pub struct CodeArtifact {
359 pub is_code_output: bool,
360 pub language: String,
361 pub code_type: String,
362 pub size_bytes: usize,
363 pub line_count: usize,
364 pub secrets_detected: usize,
365 pub unsafe_patterns: usize,
366 #[serde(default, deserialize_with = "null_to_default")]
367 pub policies_checked: Vec<String>,
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn audit_log_entry_with_residency_fields_deserializes() {
376 let json = r#"{
377 "id": "audit-001",
378 "request_id": "req-123",
379 "timestamp": "2026-05-26T00:00:00Z",
380 "user_email": "user@example.com",
381 "client_id": "client-1",
382 "tenant_id": "tenant-1",
383 "request_type": "llm_query",
384 "query_summary": "test query",
385 "success": true,
386 "blocked": false,
387 "risk_score": 0.15,
388 "provider": "openai",
389 "model": "gpt-4",
390 "tokens_used": 500,
391 "latency_ms": 120,
392 "policy_violations": [],
393 "data_residency": "id-jakarta",
394 "transfer_basis": "pdp-consent"
395 }"#;
396
397 let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
398 assert_eq!(entry.id, "audit-001");
399 assert_eq!(entry.data_residency.as_deref(), Some("id-jakarta"));
400 assert_eq!(entry.transfer_basis.as_deref(), Some("pdp-consent"));
401 assert!(entry.success);
402 assert!(!entry.blocked);
403 assert_eq!(entry.tokens_used, 500);
404 }
405
406 #[test]
407 fn audit_log_entry_without_new_fields_deserializes_backward_compat() {
408 let json = r#"{
409 "id": "audit-002",
410 "request_id": "req-456",
411 "timestamp": "2026-05-26T00:00:00Z",
412 "user_email": "user@example.com",
413 "client_id": "client-1",
414 "tenant_id": "tenant-1",
415 "request_type": "llm_query",
416 "query_summary": "test query",
417 "success": true,
418 "blocked": false,
419 "risk_score": 0.0,
420 "provider": "anthropic",
421 "model": "claude-3",
422 "tokens_used": 100,
423 "latency_ms": 50
424 }"#;
425
426 let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
427 assert_eq!(entry.id, "audit-002");
428 assert!(entry.data_residency.is_none());
429 assert!(entry.transfer_basis.is_none());
430 assert!(entry.policy_violations.is_empty());
431 assert!(entry.metadata.is_none());
432 }
433
434 #[test]
435 fn audit_log_entry_empty_optional_fields_omitted_in_serialization() {
436 let entry = AuditLogEntry {
437 id: "audit-003".to_string(),
438 request_id: "req-789".to_string(),
439 success: true,
440 ..Default::default()
441 };
442
443 let json = serde_json::to_string(&entry).unwrap();
444 assert!(!json.contains("data_residency"));
446 assert!(!json.contains("transfer_basis"));
447 assert!(!json.contains("metadata"));
448 assert!(!json.contains("policy_violations"));
450 }
451
452 #[test]
453 fn audit_log_entry_with_metadata_round_trips() {
454 let mut meta = HashMap::new();
455 meta.insert(
456 "region".to_string(),
457 serde_json::Value::String("ap-southeast-3".to_string()),
458 );
459
460 let entry = AuditLogEntry {
461 id: "audit-004".to_string(),
462 data_residency: Some("id-jakarta".to_string()),
463 transfer_basis: Some("pdp-consent".to_string()),
464 metadata: Some(meta),
465 ..Default::default()
466 };
467
468 let json = serde_json::to_string(&entry).unwrap();
469 let back: AuditLogEntry = serde_json::from_str(&json).unwrap();
470 assert_eq!(back.data_residency.as_deref(), Some("id-jakarta"));
471 assert_eq!(back.transfer_basis.as_deref(), Some("pdp-consent"));
472 assert!(back.metadata.is_some());
473 assert_eq!(
474 back.metadata.unwrap().get("region").unwrap(),
475 &serde_json::Value::String("ap-southeast-3".to_string())
476 );
477 }
478
479 #[test]
482 fn transfer_basis_constants_wire_values() {
483 assert_eq!(transfer_basis::ADEQUACY, "adequacy");
484 assert_eq!(transfer_basis::SAFEGUARDS, "safeguards");
485 assert_eq!(transfer_basis::PASAL_56B_DPA, "pasal_56b_dpa");
486 assert_eq!(transfer_basis::CONSENT, "consent");
487 }
488
489 #[test]
490 fn audit_log_entry_pasal_56b_dpa_round_trips_verbatim() {
491 let json = r#"{
492 "id": "aud-56b",
493 "timestamp": "2026-05-30T10:00:00Z",
494 "data_residency": "ID",
495 "transfer_basis": "pasal_56b_dpa"
496 }"#;
497 let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
498 assert_eq!(
499 entry.transfer_basis.as_deref(),
500 Some(transfer_basis::PASAL_56B_DPA)
501 );
502
503 let back: AuditLogEntry =
505 serde_json::from_str(&serde_json::to_string(&entry).unwrap()).unwrap();
506 assert_eq!(back.transfer_basis.as_deref(), Some("pasal_56b_dpa"));
507 }
508
509 #[test]
510 fn audit_log_entry_safeguards_backward_compat() {
511 let json =
513 r#"{"id":"aud-sg","timestamp":"2026-05-26T10:00:00Z","transfer_basis":"safeguards"}"#;
514 let entry: AuditLogEntry = serde_json::from_str(json).unwrap();
515 assert_eq!(
516 entry.transfer_basis.as_deref(),
517 Some(transfer_basis::SAFEGUARDS)
518 );
519 }
520}