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