1use async_trait::async_trait;
2use uuid::Uuid;
3use vex_core::audit::EvidenceCapsule;
4use vex_llm::Capability;
5
6#[async_trait]
11pub trait Gate: Send + Sync + std::fmt::Debug {
12 async fn execute_gate(
14 &self,
15 agent_id: Uuid,
16 task_prompt: &str,
17 suggested_output: &str,
18 confidence: f64,
19 capabilities: Vec<Capability>,
20 ) -> EvidenceCapsule;
21}
22
23#[derive(Debug, Default)]
25pub struct GenericGateMock;
26
27#[async_trait]
28impl Gate for GenericGateMock {
29 async fn execute_gate(
30 &self,
31 _agent_id: Uuid,
32 _task_prompt: &str,
33 suggested_output: &str,
34 confidence: f64,
35 capabilities: Vec<Capability>,
36 ) -> EvidenceCapsule {
37 let (outcome, reason) = if confidence < 0.3 {
43 ("HALT", "LOW_CONFIDENCE")
44 } else if capabilities.contains(&Capability::Network)
45 && !suggested_output.to_lowercase().contains("http")
46 {
47 ("ALLOW", "SENSORS_ORANGE_NETWORK_IDLE")
49 } else if suggested_output.to_lowercase().contains("i'm sorry")
50 || suggested_output.to_lowercase().contains("cannot fulfill")
51 {
52 ("HALT", "REFUSAL_FILTER")
53 } else {
54 ("ALLOW", "SENSORS_GREEN")
55 };
56
57 EvidenceCapsule {
58 capsule_id: format!("mock-{}", &Uuid::new_v4().to_string()[..8]),
59 outcome: outcome.to_string(),
60 reason_code: reason.to_string(),
61 witness_receipt: "mock-receipt-0xdeadbeef".to_string(),
62 nonce: 0,
63 sensors: serde_json::json!({
64 "confidence_sensor": if confidence > 0.5 { "GREEN" } else { "YELLOW" },
65 "content_length": suggested_output.len(),
66 }),
67 reproducibility_context: serde_json::json!({
68 "gate_provider": "ChoraGateMock",
69 "version": "0.1.0",
70 }),
71 }
72 }
73}
74
75#[derive(Debug)]
77pub struct HttpGate {
78 pub client: reqwest::Client,
79 pub url: String,
80 pub api_key: String,
81}
82
83impl HttpGate {
84 pub fn new(url: String, api_key: String) -> Self {
85 Self {
86 client: reqwest::Client::new(),
87 url,
88 api_key,
89 }
90 }
91}
92
93#[async_trait]
94impl Gate for HttpGate {
95 async fn execute_gate(
96 &self,
97 agent_id: Uuid,
98 task_prompt: &str,
99 suggested_output: &str,
100 confidence: f64,
101 capabilities: Vec<Capability>,
102 ) -> EvidenceCapsule {
103 let payload = serde_json::json!({
106 "agent_id": agent_id,
107 "task_prompt": task_prompt,
108 "suggested_output": suggested_output,
109 "confidence": confidence,
110 "capabilities": capabilities.iter().map(|c| format!("{:?}", c)).collect::<Vec<String>>(),
111 });
112
113 let gate_url = if self.url.ends_with('/') {
114 format!("{}gate", self.url)
115 } else {
116 format!("{}/gate", self.url)
117 };
118
119 match self
120 .client
121 .post(&gate_url)
122 .header("x-api-key", &self.api_key)
123 .json(&payload)
124 .send()
125 .await
126 {
127 Ok(resp) => {
128 let status = resp.status();
129 if status.is_success() {
130 let text = resp.text().await.unwrap_or_else(|_| "".to_string());
131
132 #[derive(serde::Deserialize)]
141 struct VanguardSignedPayload {
142 capsule_id: String,
143 outcome: String,
144 reason_code: String,
145 #[serde(default)]
146 witness_receipt: Option<String>,
147 #[serde(default)]
148 nonce: Option<u64>,
149 }
150 #[derive(serde::Deserialize)]
151 struct VanguardResponse {
152 signed_payload: VanguardSignedPayload,
153 #[serde(default)]
154 witness_receipt: Option<String>,
155 #[serde(default)]
156 sensors: Option<serde_json::Value>,
157 #[serde(default)]
158 reproducibility_context: Option<serde_json::Value>,
159 }
160
161 match serde_json::from_str::<VanguardResponse>(&text) {
162 Ok(v_resp) => {
163 let witness = v_resp
165 .witness_receipt
166 .or(v_resp.signed_payload.witness_receipt)
167 .unwrap_or_else(|| {
168 format!("chora-{}", v_resp.signed_payload.capsule_id)
169 });
170 EvidenceCapsule {
171 capsule_id: v_resp.signed_payload.capsule_id,
172 outcome: v_resp.signed_payload.outcome,
173 reason_code: v_resp.signed_payload.reason_code,
174 witness_receipt: witness,
175 nonce: v_resp.signed_payload.nonce.unwrap_or(0),
176 sensors: v_resp.sensors.unwrap_or(serde_json::Value::Null),
177 reproducibility_context: v_resp
178 .reproducibility_context
179 .unwrap_or(serde_json::Value::Null),
180 }
181 }
182 Err(e) => EvidenceCapsule {
183 capsule_id: "error".to_string(),
184 outcome: "HALT".to_string(),
185 reason_code: format!("API_PARSE_ERROR: {} (Raw: {})", e, text),
186 witness_receipt: "error-none".to_string(),
187 nonce: 0,
188 sensors: serde_json::Value::Null,
189 reproducibility_context: serde_json::Value::Null,
190 },
191 }
192 } else {
193 let text = resp.text().await.unwrap_or_else(|_| "".to_string());
194 EvidenceCapsule {
195 capsule_id: "error".to_string(),
196 outcome: "HALT".to_string(),
197 reason_code: format!("API_STATUS_ERROR: {} (Raw: {})", status, text),
198 witness_receipt: "error-none".to_string(),
199 nonce: 0,
200 sensors: serde_json::Value::Null,
201 reproducibility_context: serde_json::Value::Null,
202 }
203 }
204 }
205 Err(e) => EvidenceCapsule {
206 capsule_id: "error".to_string(),
207 outcome: "HALT".to_string(),
208 reason_code: format!("API_CONNECTION_ERROR: {}", e),
209 witness_receipt: "error-none".to_string(),
210 nonce: 0,
211 sensors: serde_json::Value::Null,
212 reproducibility_context: serde_json::Value::Null,
213 },
214 }
215 }
216}