hessra_context_token/
inspect.rs1extern crate biscuit_auth as biscuit;
2
3use biscuit::macros::{authorizer, rule};
4use chrono::Utc;
5use hessra_token_core::{Biscuit, PublicKey, TokenError};
6
7#[derive(Debug, Clone)]
9pub struct ContextInspectResult {
10 pub subject: String,
12 pub exposure_labels: Vec<String>,
14 pub exposure_sources: Vec<String>,
16 pub expiry: Option<i64>,
18 pub is_expired: bool,
20 pub exposure_block_count: usize,
23}
24
25pub fn inspect_context_token(
34 token: String,
35 public_key: PublicKey,
36) -> Result<ContextInspectResult, TokenError> {
37 let biscuit = Biscuit::from_base64(&token, public_key)?;
38 let now = Utc::now().timestamp();
39
40 let authz = authorizer!(
41 r#"
42 time({now});
43 allow if true;
44 "#
45 );
46
47 let mut authorizer = authz
48 .build(&biscuit)
49 .map_err(|e| TokenError::internal(format!("failed to build authorizer: {e}")))?;
50
51 let subjects: Vec<(String,)> = authorizer
52 .query("data($name) <- context($name)")
53 .map_err(|e| TokenError::internal(format!("failed to query context subject: {e}")))?;
54 let subject = subjects.first().map(|(s,)| s.clone()).unwrap_or_default();
55
56 let pk = public_key;
57 let label_results: Vec<(String,)> = authorizer
58 .query_all(rule!(
59 r#"data($l) <- exposed_label($l) trusting authority, {pk}"#
60 ))
61 .map_err(|e| TokenError::internal(format!("failed to query exposure labels: {e}")))?;
62
63 let mut exposure_labels: Vec<String> = Vec::new();
64 for (label,) in label_results {
65 if !exposure_labels.contains(&label) {
66 exposure_labels.push(label);
67 }
68 }
69
70 let source_results: Vec<(String,)> = authorizer
71 .query_all(rule!(
72 r#"data($s) <- exposure_source($s) trusting authority, {pk}"#
73 ))
74 .map_err(|e| TokenError::internal(format!("failed to query exposure sources: {e}")))?;
75
76 let mut exposure_sources: Vec<String> = Vec::new();
77 for (source,) in source_results {
78 if !exposure_sources.contains(&source) {
79 exposure_sources.push(source);
80 }
81 }
82
83 let block_count = biscuit.block_count();
87 let exposure_block_count = block_count.saturating_sub(1);
88
89 let token_content = biscuit.print();
90 let expiry = extract_expiry_from_content(&token_content);
91 let is_expired = expiry.is_some_and(|exp| exp < now);
92
93 Ok(ContextInspectResult {
94 subject,
95 exposure_labels,
96 exposure_sources,
97 expiry,
98 is_expired,
99 exposure_block_count,
100 })
101}
102
103fn extract_expiry_from_content(content: &str) -> Option<i64> {
105 let mut earliest_expiry: Option<i64> = None;
106
107 for line in content.lines() {
108 if line.contains("check if")
109 && line.contains("time")
110 && line.contains("<")
111 && let Some(pos) = line.find("$time <")
112 {
113 let after_lt = &line[pos + 8..].trim();
114 let number_str = after_lt
115 .chars()
116 .take_while(|c| c.is_ascii_digit() || *c == '-')
117 .collect::<String>();
118
119 if let Ok(timestamp) = number_str.parse::<i64>() {
120 earliest_expiry = Some(earliest_expiry.map_or(timestamp, |e| e.min(timestamp)));
121 }
122 }
123 }
124
125 earliest_expiry
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::exposure::add_exposure;
132 use crate::mint::HessraContext;
133 use hessra_token_core::{KeyPair, TokenTimeConfig};
134
135 #[test]
136 fn test_inspect_fresh_context() {
137 let keypair = KeyPair::new();
138 let public_key = keypair.public();
139
140 let token = HessraContext::new("agent:openclaw".to_string(), TokenTimeConfig::default())
141 .issue(&keypair)
142 .expect("Failed to create context token");
143
144 let result =
145 inspect_context_token(token, public_key).expect("Failed to inspect context token");
146
147 assert_eq!(result.subject, "agent:openclaw");
148 assert!(result.exposure_labels.is_empty());
149 assert!(result.exposure_sources.is_empty());
150 assert!(!result.is_expired);
151 assert!(result.expiry.is_some());
152 assert_eq!(result.exposure_block_count, 0);
153 }
154
155 #[test]
156 fn test_inspect_exposed_context() {
157 let keypair = KeyPair::new();
158 let public_key = keypair.public();
159
160 let token = HessraContext::new("agent:openclaw".to_string(), TokenTimeConfig::default())
161 .issue(&keypair)
162 .expect("Failed to create context token");
163
164 let exposed = add_exposure(
165 &token,
166 &keypair,
167 &["PII:SSN".to_string()],
168 "data:user-ssn".to_string(),
169 )
170 .expect("Failed to add exposure");
171
172 let result =
173 inspect_context_token(exposed, public_key).expect("Failed to inspect exposed context");
174
175 assert_eq!(result.subject, "agent:openclaw");
176 assert_eq!(result.exposure_labels, vec!["PII:SSN".to_string()]);
177 assert_eq!(result.exposure_sources, vec!["data:user-ssn".to_string()]);
178 assert_eq!(result.exposure_block_count, 1);
179 }
180
181 #[test]
182 fn test_inspect_multi_exposed_context() {
183 let keypair = KeyPair::new();
184 let public_key = keypair.public();
185
186 let token = HessraContext::new("agent:openclaw".to_string(), TokenTimeConfig::default())
187 .issue(&keypair)
188 .expect("Failed to create context token");
189
190 let exposed = add_exposure(
191 &token,
192 &keypair,
193 &["PII:email".to_string(), "PII:address".to_string()],
194 "data:user-profile".to_string(),
195 )
196 .expect("Failed to add profile exposure");
197
198 let more_exposed = add_exposure(
199 &exposed,
200 &keypair,
201 &["PII:SSN".to_string()],
202 "data:user-ssn".to_string(),
203 )
204 .expect("Failed to add SSN exposure");
205
206 let result = inspect_context_token(more_exposed, public_key)
207 .expect("Failed to inspect multi-exposed context");
208
209 assert_eq!(result.subject, "agent:openclaw");
210 assert_eq!(result.exposure_labels.len(), 3);
211 assert!(result.exposure_labels.contains(&"PII:email".to_string()));
212 assert!(result.exposure_labels.contains(&"PII:address".to_string()));
213 assert!(result.exposure_labels.contains(&"PII:SSN".to_string()));
214 assert_eq!(result.exposure_sources.len(), 2);
215 assert_eq!(result.exposure_block_count, 2);
216 }
217
218 #[test]
219 fn test_inspect_initial_exposures_in_authority_block() {
220 let keypair = KeyPair::new();
221 let public_key = keypair.public();
222
223 let token = HessraContext::new("agent:openclaw".to_string(), TokenTimeConfig::default())
224 .with_initial_exposures(
225 &["PII:email".to_string(), "PII:address".to_string()],
226 "data:user-profile",
227 )
228 .issue(&keypair)
229 .expect("Failed to create context token");
230
231 let result = inspect_context_token(token, public_key)
232 .expect("Failed to inspect token with initial exposures");
233
234 assert_eq!(result.subject, "agent:openclaw");
235 assert_eq!(result.exposure_labels.len(), 2);
236 assert!(result.exposure_labels.contains(&"PII:email".to_string()));
237 assert!(result.exposure_labels.contains(&"PII:address".to_string()));
238 assert_eq!(
239 result.exposure_sources,
240 vec!["data:user-profile".to_string()]
241 );
242 assert_eq!(result.exposure_block_count, 0);
244 }
245
246 #[test]
247 fn test_inspect_expired_context() {
248 let keypair = KeyPair::new();
249 let public_key = keypair.public();
250
251 let expired_config = TokenTimeConfig {
252 start_time: Some(0),
253 duration: 1,
254 };
255
256 let token = HessraContext::new("agent:test".to_string(), expired_config)
257 .issue(&keypair)
258 .expect("Failed to create expired context token");
259
260 let result = inspect_context_token(token, public_key)
261 .expect("Should be able to inspect expired token");
262
263 assert_eq!(result.subject, "agent:test");
264 assert!(result.is_expired);
265 assert_eq!(result.expiry, Some(1));
266 }
267}