Skip to main content

vex_runtime/
gate.rs

1use async_trait::async_trait;
2use uuid::Uuid;
3use vex_core::audit::EvidenceCapsule;
4use vex_llm::Capability;
5
6/// Exogenous Gate Decision Boundary
7///
8/// A Gate acts as a continuation authority, deciding whether an agent's
9/// output is "Safe", "Valid", or "Audit-Compliant".
10#[async_trait]
11pub trait Gate: Send + Sync + std::fmt::Debug {
12    /// Evaluate the current execution state and return a signed Evidence Capsule.
13    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/// A mock implementation of the Generic Gate for testing and local development.
24#[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        // Simple logic for the mock:
38        // 1. If confidence is very low (< 0.3), HALT.
39        // 2. If the output contains common failure patterns, HALT.
40        // 3. Otherwise, ALLOW.
41
42        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            // Example policy: If you have network capability but don't explain the URL, caution.
48            ("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            sensors: serde_json::json!({
62                "confidence_sensor": if confidence > 0.5 { "GREEN" } else { "YELLOW" },
63                "content_length": suggested_output.len(),
64            }),
65            reproducibility_context: serde_json::json!({
66                "gate_provider": "ChoraGateMock",
67                "version": "0.1.0",
68            }),
69        }
70    }
71}
72
73/// Networked Gate provider communicating over HTTP
74#[derive(Debug)]
75pub struct HttpGate {
76    pub client: reqwest::Client,
77    pub url: String,
78    pub api_key: String,
79}
80
81impl HttpGate {
82    pub fn new(url: String, api_key: String) -> Self {
83        Self {
84            client: reqwest::Client::new(),
85            url,
86            api_key,
87        }
88    }
89}
90
91#[async_trait]
92impl Gate for HttpGate {
93    async fn execute_gate(
94        &self,
95        agent_id: Uuid,
96        task_prompt: &str,
97        suggested_output: &str,
98        confidence: f64,
99        capabilities: Vec<Capability>,
100    ) -> EvidenceCapsule {
101        let payload = serde_json::json!({
102            "agent_id": agent_id,
103            "task_prompt": task_prompt,
104            "suggested_output": suggested_output,
105            "confidence": confidence,
106            "capabilities": capabilities,
107        });
108
109        let gate_url = if self.url.ends_with('/') {
110            format!("{}gate", self.url)
111        } else {
112            format!("{}/gate", self.url)
113        };
114
115        match self
116            .client
117            .post(&gate_url)
118            .header("x-api-key", &self.api_key)
119            .json(&payload)
120            .send()
121            .await
122        {
123            Ok(resp) => {
124                if resp.status().is_success() {
125                    resp.json::<EvidenceCapsule>()
126                        .await
127                        .unwrap_or_else(|e| EvidenceCapsule {
128                            capsule_id: "error".to_string(),
129                            outcome: "HALT".to_string(),
130                            reason_code: format!("API_PARSE_ERROR: {}", e),
131                            sensors: serde_json::Value::Null,
132                            reproducibility_context: serde_json::Value::Null,
133                        })
134                } else {
135                    EvidenceCapsule {
136                        capsule_id: "error".to_string(),
137                        outcome: "HALT".to_string(),
138                        reason_code: format!("API_STATUS_ERROR: {}", resp.status()),
139                        sensors: serde_json::Value::Null,
140                        reproducibility_context: serde_json::Value::Null,
141                    }
142                }
143            }
144            Err(e) => EvidenceCapsule {
145                capsule_id: "error".to_string(),
146                outcome: "HALT".to_string(),
147                reason_code: format!("API_CONNECTION_ERROR: {}", e),
148                sensors: serde_json::Value::Null,
149                reproducibility_context: serde_json::Value::Null,
150            },
151        }
152    }
153}