agentstategraph_policy_rego/
lib.rs1use std::process::{Command, Stdio};
45use std::time::Duration;
46
47use agentstategraph_policy::external::{
48 DEFAULT_TIMEOUT, ExternalError, ExternalEvaluator, wait_with_timeout,
49};
50use agentstategraph_policy::selector::Situation;
51use agentstategraph_policy::types::{Decision, EvaluatorSource};
52
53pub struct RegoEvaluator {
55 opa_path: String,
58 timeout: Duration,
60}
61
62impl RegoEvaluator {
63 pub fn new() -> Self {
65 Self::with_opa_path("opa")
66 }
67
68 pub fn with_opa_path(opa_path: impl Into<String>) -> Self {
71 Self {
72 opa_path: opa_path.into(),
73 timeout: DEFAULT_TIMEOUT,
74 }
75 }
76
77 pub fn with_timeout(mut self, timeout: Duration) -> Self {
81 self.timeout = timeout;
82 self
83 }
84}
85
86impl Default for RegoEvaluator {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl ExternalEvaluator for RegoEvaluator {
93 fn kind(&self) -> &'static str {
94 "rego"
95 }
96
97 fn evaluate(
98 &self,
99 source: &EvaluatorSource,
100 situation: &Situation,
101 action: &str,
102 agent_id: &str,
103 ) -> Result<Decision, ExternalError> {
104 let rego = match source {
106 EvaluatorSource::Inline { body } => body.clone(),
107 EvaluatorSource::FilePath { path } => std::fs::read_to_string(path)
108 .map_err(|e| ExternalError::SourceResolution(e.to_string()))?,
109 EvaluatorSource::CommitRef { .. } => {
110 return Err(ExternalError::SourceResolution(
111 "commit_ref not supported by RegoEvaluator".into(),
112 ));
113 }
114 };
115
116 let tmp = tempfile::Builder::new()
118 .suffix(".rego")
119 .tempfile()
120 .map_err(|e| ExternalError::Execution(format!("tempfile: {e}")))?;
121 std::fs::write(tmp.path(), rego.as_bytes())
122 .map_err(|e| ExternalError::Execution(format!("write rego: {e}")))?;
123
124 let input = serde_json::json!({
126 "situation": &situation.0,
127 "action": action,
128 "agent_id": agent_id,
129 });
130 let input_bytes = serde_json::to_vec(&input)
131 .map_err(|e| ExternalError::Execution(format!("input serialize: {e}")))?;
132
133 let child = Command::new(&self.opa_path)
135 .arg("eval")
136 .arg("--data")
137 .arg(tmp.path())
138 .arg("--stdin-input")
139 .arg("--format=json")
140 .arg("data.policy.decision")
141 .stdin(Stdio::piped())
142 .stdout(Stdio::piped())
143 .stderr(Stdio::piped())
144 .spawn()
145 .map_err(|e| {
146 ExternalError::Execution(format!(
147 "failed to invoke {}: {e} (is OPA on $PATH?)",
148 self.opa_path
149 ))
150 })?;
151 let output = wait_with_timeout(child, Some(input_bytes), self.timeout)?;
152
153 if !output.status.success() {
154 return Err(ExternalError::Execution(format!(
155 "opa eval exited {}: {}",
156 output.status,
157 String::from_utf8_lossy(&output.stderr)
158 )));
159 }
160
161 let envelope: serde_json::Value = serde_json::from_slice(&output.stdout)
164 .map_err(|e| ExternalError::Execution(format!("parse opa output: {e}")))?;
165 let decision_value = envelope
166 .pointer("/result/0/expressions/0/value")
167 .cloned()
168 .ok_or_else(|| {
169 ExternalError::Execution("opa output missing /result/0/expressions/0/value".into())
170 })?;
171 let decision: Decision = serde_json::from_value(decision_value)
172 .map_err(|e| ExternalError::Execution(format!("parse decision: {e}")))?;
173 Ok(decision)
174 }
175}