1use std::path::{Path, PathBuf};
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, Ordering};
19#[cfg(any(test, feature = "test-support"))]
20use std::sync::{OnceLock, RwLock};
21
22use regex::Regex;
23
24use super::ResolvedPolicy;
25#[cfg(test)]
26use super::{parse_file, resolve};
27use crate::core::input_filters::{FilterAction, FilterConfig};
28
29mod refresh;
30pub const REQUIRED_POLICY_ROOT_ENV: &str = "LEAN_CTX_REQUIRED_POLICY_ROOT";
32pub const REQUIRED_POLICY_DIGEST_ENV: &str = "LEAN_CTX_REQUIRED_POLICY_DIGEST";
33pub use refresh::{protected_authority_paths, protected_policy_digest};
34#[cfg(any(test, feature = "test-support"))]
35pub mod held_horizon_support;
36#[cfg(test)]
37pub(crate) mod held_horizon_tests;
38#[cfg(test)]
39mod refresh_tests;
40
41tokio::task_local! {
42 pub static REQUEST_PROJECT: std::cell::RefCell<Option<PathBuf>>;
44 pub(crate) static SOURCE_VIEW: std::cell::RefCell<Option<SourceView>>;
46}
47
48#[derive(Clone)]
49pub struct SourceView {
50 project: PathBuf,
51 policy: Option<Arc<ActivePolicy>>,
52 fingerprint: blake3::Hash,
53 role: crate::core::roles::Role,
54 role_fingerprint: blake3::Hash,
55 lifetime: Option<super::org::Lifetime>,
66 observation: Option<super::org::Observation>,
69 valid: Arc<AtomicBool>,
70 live: Arc<AtomicBool>,
71}
72
73impl SourceView {
74 fn within_horizon(&self) -> bool {
77 let holds = self
78 .lifetime
79 .is_none_or(|lifetime| lifetime.check_horizon().is_ok());
80 if !holds {
81 self.valid.store(false, Ordering::Release);
82 }
83 holds
84 }
85
86 fn invalidate_if_live(&self) {
94 if self.live.load(Ordering::Acquire) {
95 self.valid.store(false, Ordering::Release);
96 }
97 }
98}
99
100struct SourceViewLifetime(Arc<AtomicBool>);
101
102#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
106#[serde(deny_unknown_fields)]
107pub struct StoredPublicationAuthority {
108 project: PathBuf,
109 fingerprint: [u8; 32],
110 role_fingerprint: [u8; 32],
111 lifetime: Option<StoredLifetime>,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
117#[serde(deny_unknown_fields)]
118pub struct StoredLifetime {
119 horizon: Option<chrono::DateTime<chrono::Utc>>,
120}
121
122#[derive(Debug)]
125pub struct PublicationAuthority {
126 project: PathBuf,
127 fingerprint: blake3::Hash,
128 role_fingerprint: blake3::Hash,
129 lifetime: Option<super::org::Lifetime>,
131 valid: Arc<AtomicBool>,
132}
133
134impl PublicationAuthority {
135 pub fn capture() -> Result<Self, String> {
136 let view = current_source_view().ok_or("source authority unavailable")?;
137 if !view.within_horizon() {
138 return Err("source authority unavailable".into());
139 }
140 Ok(Self {
141 project: view.project,
142 fingerprint: view.fingerprint,
143 role_fingerprint: view.role_fingerprint,
144 lifetime: view.lifetime,
145 valid: view.valid,
146 })
147 }
148
149 pub fn stored(&self) -> Option<StoredPublicationAuthority> {
156 self.valid
157 .load(Ordering::Acquire)
158 .then(|| StoredPublicationAuthority {
159 project: self.project.clone(),
160 fingerprint: *self.fingerprint.as_bytes(),
161 role_fingerprint: *self.role_fingerprint.as_bytes(),
162 lifetime: self.lifetime.map(|lifetime| StoredLifetime {
163 horizon: lifetime.horizon(),
164 }),
165 })
166 }
167
168 #[must_use]
174 pub fn restore(stored: StoredPublicationAuthority) -> Self {
175 Self {
176 project: stored.project,
177 fingerprint: blake3::Hash::from_bytes(stored.fingerprint),
178 role_fingerprint: blake3::Hash::from_bytes(stored.role_fingerprint),
179 lifetime: stored
180 .lifetime
181 .map(|lifetime| super::org::Lifetime::restored(lifetime.horizon)),
182 valid: Arc::new(AtomicBool::new(true)),
183 }
184 }
185
186 pub fn verify(&self) -> Result<(), String> {
187 if !self.valid.load(Ordering::Acquire)
188 || REQUEST_PROJECT
189 .try_with(|slot| {
190 slot.borrow()
191 .as_ref()
192 .is_some_and(|root| root != &self.project)
193 })
194 .unwrap_or(false)
195 {
196 return Err("source authority changed before publication".into());
197 }
198 REQUEST_PROJECT.sync_scope(std::cell::RefCell::new(Some(self.project.clone())), || {
199 SOURCE_VIEW.sync_scope(std::cell::RefCell::new(None), || {
200 let latest = refresh::current_verified(self.project.clone())?;
201 let role = role_fingerprint(&crate::core::roles::active_role())?;
202 if latest.fingerprint != self.fingerprint || role != self.role_fingerprint {
203 return Err("source authority changed before publication".into());
204 }
205 if let Some(lifetime) = &self.lifetime {
209 lifetime.check_horizon()?;
210 }
211 Ok(())
212 })
213 })
214 }
215
216 pub fn fingerprint_bytes(&self) -> [u8; 32] {
219 *self.fingerprint.as_bytes()
220 }
221
222 #[cfg(any(test, feature = "http-server"))]
225 pub fn role_fingerprint_bytes(&self) -> [u8; 32] {
226 *self.role_fingerprint.as_bytes()
227 }
228}
229
230impl Drop for SourceViewLifetime {
231 fn drop(&mut self) {
232 self.0.store(false, Ordering::Release);
233 }
234}
235
236fn current_source_view() -> Option<SourceView> {
237 SOURCE_VIEW
238 .try_with(|slot| slot.borrow().clone())
239 .ok()
240 .flatten()
241}
242
243pub fn inherited_source_view() -> Option<SourceView> {
245 current_source_view()
246}
247
248pub fn detached_project_root(fallback: &Path) -> Result<PathBuf, String> {
252 let request_project = REQUEST_PROJECT
253 .try_with(|slot| slot.borrow().clone())
254 .ok()
255 .flatten();
256 if let Some(view) = current_source_view() {
257 if !view.live.load(Ordering::Acquire)
258 || !view.valid.load(Ordering::Acquire)
259 || request_project.as_ref() != Some(&view.project)
260 {
261 view.invalidate_if_live();
262 return Err("source policy view is no longer active".into());
263 }
264 return Ok(view.project);
265 }
266 Ok(request_project.unwrap_or_else(|| fallback.to_path_buf()))
267}
268
269pub(crate) fn with_inherited_source_view<T>(
271 view: Option<SourceView>,
272 operation: impl FnOnce() -> T,
273) -> T {
274 match view {
275 Some(view) => SOURCE_VIEW.sync_scope(std::cell::RefCell::new(Some(view)), operation),
276 None => operation(),
277 }
278}
279
280pub(crate) fn pinned_role() -> Option<crate::core::roles::Role> {
283 current_source_view().map(|view| {
284 let project = REQUEST_PROJECT
285 .try_with(|slot| slot.borrow().clone())
286 .ok()
287 .flatten();
288 if !view.live.load(Ordering::Acquire)
289 || !view.valid.load(Ordering::Acquire)
290 || project.as_ref() != Some(&view.project)
291 {
292 view.invalidate_if_live();
293 crate::core::roles::closed_source_view_role()
294 } else {
295 view.role
296 }
297 })
298}
299
300pub fn in_project<T>(project: impl Into<PathBuf>, operation: impl FnOnce() -> T) -> T {
302 REQUEST_PROJECT.sync_scope(std::cell::RefCell::new(Some(project.into())), operation)
303}
304
305pub fn spawn_for_project<T: Send + 'static>(
309 project: impl Into<PathBuf>,
310 operation: impl FnOnce() -> T + Send + 'static,
311) -> std::thread::JoinHandle<T> {
312 let project = project.into();
313 std::thread::spawn(move || in_project(project, operation))
314}
315
316pub fn bind_request_project(project: Option<&str>) {
317 if current_source_view().is_some() {
318 return;
319 }
320 let _ = REQUEST_PROJECT.try_with(|slot| {
321 *slot.borrow_mut() = project.map(PathBuf::from);
322 });
323}
324
325fn canonical_project_root(project: &Path) -> Result<PathBuf, String> {
331 let project = crate::core::pathutil::canonicalize_secure(project)
332 .map_err(|_| "policy project root unavailable")?;
333 if !project.is_dir() {
334 return Err("policy project root unavailable".into());
335 }
336 Ok(project)
337}
338
339pub(crate) fn request_project_is(root: &Path) -> bool {
345 match REQUEST_PROJECT.try_with(|slot| slot.borrow().clone()) {
346 Ok(Some(bound)) => canonical_project_root(&bound).is_ok_and(|bound| bound == root),
347 _ => true,
348 }
349}
350
351pub fn with_project_source_view<T>(
355 project_root: &str,
356 operation: impl FnOnce() -> T,
357) -> Result<T, String> {
358 let project = REQUEST_PROJECT
359 .try_with(|slot| slot.borrow().clone())
360 .ok()
361 .flatten()
362 .or_else(|| current_source_view().map(|view| view.project))
363 .unwrap_or_else(|| PathBuf::from(project_root));
364 let project = canonical_project_root(&project)?;
365 REQUEST_PROJECT.sync_scope(std::cell::RefCell::new(Some(project)), || {
366 with_source_view(operation)
367 })
368}
369
370pub fn with_explicit_project_source_view<T>(
374 project_root: &Path,
375 operation: impl FnOnce(&PublicationAuthority) -> T,
376) -> Result<T, String> {
377 let project = canonical_project_root(project_root)?;
378
379 let bound = REQUEST_PROJECT
380 .try_with(|slot| slot.borrow().clone())
381 .ok()
382 .flatten();
383 if let Some(bound) = bound {
384 let bound = crate::core::pathutil::canonicalize_secure(&bound)
385 .map_err(|_| "request project authority unavailable")?;
386 if bound != project {
387 return Err("request project authority does not match egress root".into());
388 }
389 }
390 if let Some(view) = current_source_view()
391 && (!view.live.load(Ordering::Acquire)
392 || !view.valid.load(Ordering::Acquire)
393 || view.project != project)
394 {
395 view.invalidate_if_live();
396 return Err("source policy view does not match egress root".into());
397 }
398
399 REQUEST_PROJECT.sync_scope(std::cell::RefCell::new(Some(project)), || {
400 with_source_view(|| {
401 let authority = PublicationAuthority::capture()?;
402 let result = operation(&authority);
403 authority.verify()?;
404 Ok(result)
405 })?
406 })
407}
408
409pub fn with_source_view<T>(operation: impl FnOnce() -> T) -> Result<T, String> {
413 with_source_view_when(|_, _| true, operation)?
414 .ok_or_else(|| "source policy view is no longer active".into())
415}
416
417#[cfg(feature = "http-server")]
429pub fn with_governed_source_view<T>(operation: impl FnOnce() -> T) -> Result<Option<T>, String> {
430 with_source_view_when(|policy, observed| policy || observed, operation)
431}
432
433fn with_source_view_when<T>(
436 enter: impl FnOnce(bool, bool) -> bool,
437 operation: impl FnOnce() -> T,
438) -> Result<Option<T>, String> {
439 if let Some(view) = current_source_view() {
440 let project = REQUEST_PROJECT
441 .try_with(|slot| slot.borrow().clone())
442 .ok()
443 .flatten()
444 .ok_or("source policy view is no longer active")?;
445 let project = canonical_project_root(&project)
446 .map_err(|_| "source policy view is no longer active")?;
447 if !view.live.load(Ordering::Acquire)
448 || !view.valid.load(Ordering::Acquire)
449 || project != view.project
450 || !view.within_horizon()
451 {
452 view.invalidate_if_live();
453 return Err("source policy view is no longer active".into());
454 }
455 if !enter(view.policy.is_some(), view.observation.is_some()) {
456 return Ok(None);
457 }
458 return REQUEST_PROJECT
459 .sync_scope(std::cell::RefCell::new(Some(view.project.clone())), || {
460 Ok(Some(operation()))
461 });
462 }
463
464 let project = match REQUEST_PROJECT.try_with(|slot| slot.borrow().clone()) {
468 Ok(project) => project.ok_or("project_unresolved")?,
469 Err(_) => std::env::current_dir().map_err(|_| "policy project root unavailable")?,
470 };
471 let project = canonical_project_root(&project)?;
472 REQUEST_PROJECT.sync_scope(std::cell::RefCell::new(Some(project.clone())), || {
473 let verified = refresh::current_verified_settled(&project)?;
474 let lifetime = verified.lifetime;
475 let observation = verified.observation;
476 let role = crate::core::roles::active_role();
477 let pinned_role_fingerprint = role_fingerprint(&role)?;
478 let view = SourceView {
479 project: project.clone(),
480 policy: source_view_policy(verified.policy),
481 fingerprint: verified.fingerprint,
482 role,
483 role_fingerprint: pinned_role_fingerprint,
484 lifetime,
485 observation,
486 valid: Arc::new(AtomicBool::new(true)),
487 live: Arc::new(AtomicBool::new(true)),
488 };
489
490 if !view.within_horizon() {
493 return Err("source policy view is no longer active".into());
494 }
495 if !enter(view.policy.is_some(), view.observation.is_some()) {
497 return Ok(None);
498 }
499 let view_lifetime = SourceViewLifetime(view.live.clone());
500 let value = SOURCE_VIEW.sync_scope(std::cell::RefCell::new(Some(view.clone())), operation);
501 drop(view_lifetime);
502
503 let project_unchanged = REQUEST_PROJECT
506 .try_with(|slot| slot.borrow().as_ref() == Some(&view.project))
507 .unwrap_or(false);
508 let latest_role = crate::core::roles::active_role();
509 let latest_role_fingerprint = role_fingerprint(&latest_role)?;
510 let latest = refresh::current_verified_settled(&project)?;
511 if !project_unchanged
512 || latest.fingerprint != view.fingerprint
513 || latest_role_fingerprint != view.role_fingerprint
514 || !view.valid.load(Ordering::Acquire)
515 {
516 return Err("source policy view changed during operation".into());
517 }
518 if !view.within_horizon() {
522 return Err("source policy view changed during operation".into());
523 }
524 Ok(Some(value))
525 })
526}
527
528pub(crate) fn role_fingerprint(role: &crate::core::roles::Role) -> Result<blake3::Hash, String> {
529 serde_json::to_vec(role)
530 .map(|bytes| blake3::hash(&bytes))
531 .map_err(|_| "source role view unavailable".into())
532}
533
534#[cfg(any(test, feature = "test-support"))]
535fn source_view_policy(fresh: Option<Arc<ActivePolicy>>) -> Option<Arc<ActivePolicy>> {
536 let state = test_override()
537 .read()
538 .unwrap_or_else(std::sync::PoisonError::into_inner);
539 match &*state {
540 OverrideState::Live => fresh,
541 OverrideState::Fixed(policy) => policy.clone(),
542 }
543}
544
545#[cfg(not(any(test, feature = "test-support")))]
546fn source_view_policy(fresh: Option<Arc<ActivePolicy>>) -> Option<Arc<ActivePolicy>> {
547 fresh
548}
549
550const PROJECT_PACK_PATH: &str = ".lean-ctx/policy.toml";
553
554pub struct ActivePolicy {
557 pub resolved: ResolvedPolicy,
558 pub redaction: Vec<(String, Regex)>,
561 pub(crate) blocked_patterns: Vec<(String, Regex)>,
562 pub(crate) hive_rules: usize,
565 pub content_valid: bool,
566 pub filters: FilterConfig,
569 pub egress: crate::core::egress::EgressConfig,
572 horizon: Option<chrono::DateTime<chrono::Utc>>,
578 expired: AtomicBool,
582}
583
584impl ActivePolicy {
585 #[must_use]
591 pub fn within_horizon(&self) -> bool {
592 if self.expired.load(Ordering::Acquire) {
593 return false;
594 }
595 let holds = self
596 .horizon
597 .is_none_or(|horizon| chrono::Utc::now() < horizon);
598 if !holds {
599 self.expired.store(true, Ordering::Release);
600 }
601 holds
602 }
603
604 fn until(mut self, horizon: Option<chrono::DateTime<chrono::Utc>>) -> Self {
606 self.horizon = horizon;
607 self
608 }
609
610 pub fn from_resolved(resolved: ResolvedPolicy) -> Self {
611 Self::from_resolved_with_hive(
612 resolved,
613 &crate::core::policy::hive_filters::FilterRules::default(),
614 )
615 }
616
617 pub(crate) fn from_resolved_with_hive(
622 mut resolved: ResolvedPolicy,
623 hive: &crate::core::policy::hive_filters::FilterRules,
624 ) -> Self {
625 use super::content::{MAX_REDACTION_RULES, MAX_RULE_LABEL_BYTES, MAX_RULE_PATTERN_BYTES};
626 let hive_rules = hive.redaction.len() + hive.blocked.len();
627 let hive_bounded = hive_rules <= crate::core::policy::hive_filters::HIVE_MAX_RULES
628 && hive
629 .redaction
630 .iter()
631 .chain(&hive.blocked)
632 .all(|(label, pattern)| {
633 label.starts_with(super::HIVE_LABEL_PREFIX)
634 && label.len() <= MAX_RULE_LABEL_BYTES
635 && !label.chars().any(char::is_control)
636 && pattern.len() <= MAX_RULE_PATTERN_BYTES
637 });
638 let bounded = hive_bounded
639 && resolved.redaction.len() + resolved.filters.blocked_patterns.len()
640 <= MAX_REDACTION_RULES
641 && resolved.egress.forbidden_patterns.len() <= MAX_REDACTION_RULES
642 && resolved
643 .egress
644 .forbidden_patterns
645 .iter()
646 .all(|pattern| pattern.len() <= MAX_RULE_PATTERN_BYTES)
647 && resolved
648 .redaction
649 .iter()
650 .chain(&resolved.filters.blocked_patterns)
651 .all(|(label, pattern)| {
652 !label.is_empty()
653 && label.len() <= MAX_RULE_LABEL_BYTES
654 && !label.chars().any(char::is_control)
655 && pattern.len() <= MAX_RULE_PATTERN_BYTES
656 });
657 resolved.redaction.extend(hive.redaction.iter().cloned());
660 resolved
661 .filters
662 .blocked_patterns
663 .extend(hive.blocked.iter().cloned());
664 let compiled = bounded.then(|| {
665 resolved
666 .redaction
667 .iter()
668 .map(|(label, pattern)| {
669 regex::RegexBuilder::new(pattern)
670 .size_limit(1024 * 1024)
671 .dfa_size_limit(1024 * 1024)
672 .build()
673 .map(|re| (label.clone(), re))
674 })
675 .collect::<Result<Vec<_>, _>>()
676 });
677 let content_valid = matches!(&compiled, Some(Ok(_)));
678 let redaction = compiled.and_then(Result::ok).unwrap_or_default();
679 let compiled_blocks = bounded.then(|| {
680 resolved
681 .filters
682 .blocked_patterns
683 .iter()
684 .map(|(label, pattern)| {
685 regex::RegexBuilder::new(pattern)
686 .size_limit(1024 * 1024)
687 .dfa_size_limit(1024 * 1024)
688 .build()
689 .map(|re| (label.clone(), re))
690 })
691 .collect::<Result<Vec<_>, _>>()
692 });
693 let content_valid = content_valid && matches!(&compiled_blocks, Some(Ok(_)));
694 let blocked_patterns = compiled_blocks.and_then(Result::ok).unwrap_or_default();
695 let filters = FilterConfig::new(
696 filter_action(resolved.filters.pii.as_ref()),
697 filter_action(resolved.filters.classification.as_ref()),
698 filter_action(resolved.filters.injection.as_ref()),
699 &resolved.filters.blocked_labels,
700 );
701 let egress = crate::core::egress::EgressConfig::new(
702 &resolved.egress.forbidden_patterns,
703 resolved.egress.block_secrets.unwrap_or(false),
704 resolved.egress.max_writes_per_min,
705 );
706 Self {
707 resolved,
708 redaction,
709 blocked_patterns,
710 hive_rules,
711 content_valid,
712 filters,
713 egress,
714 horizon: None,
715 expired: AtomicBool::new(false),
716 }
717 }
718
719 pub(crate) fn deny_all() -> Self {
721 let mut active = Self::from_resolved(ResolvedPolicy {
722 name: "invalid-local-policy".into(),
723 version: "0.0.0".into(),
724 description: "deny all because the local policy pack is invalid".into(),
725 chain: Vec::new(),
726 default_read_mode: None,
727 allow_tools: Some(Vec::new()),
728 deny_tools: Vec::new(),
729 max_context_tokens: None,
730 audit_retention_days: None,
731 redaction: std::collections::BTreeMap::new(),
732 filters: crate::core::policy::FilterRules::default(),
733 egress: crate::core::policy::EgressRules::default(),
734 routing: crate::core::policy::RoutingPolicyRules::default(),
735 budgets: crate::core::policy::BudgetRules::default(),
736 });
737 active.content_valid = false;
738 active
739 }
740
741 #[must_use]
745 pub fn tool_allowed(&self, tool: &str) -> bool {
746 if !self.content_valid || !self.within_horizon() {
747 return false;
748 }
749 if self.resolved.deny_tools.iter().any(|t| t == tool) {
750 return false;
751 }
752 match &self.resolved.allow_tools {
753 Some(allow) => allow.iter().any(|t| t == tool),
754 None => true,
755 }
756 }
757}
758
759fn filter_action(opt: Option<&String>) -> FilterAction {
762 opt.map(String::as_str)
763 .and_then(FilterAction::parse)
764 .unwrap_or(FilterAction::Off)
765}
766
767#[cfg(test)]
768fn load_from_disk() -> Option<Arc<ActivePolicy>> {
769 let local = match load_local_pack() {
770 Ok(local) => local,
771 Err(msg) => {
772 tracing::error!(
773 "policy: failed to load invalid {PROJECT_PACK_PATH} ({msg}); enforcing deny-all"
774 );
775 return Some(Arc::new(ActivePolicy::deny_all()));
776 }
777 };
778 apply_org_floor(
779 local,
780 crate::core::policy::org::active_resolved_checked(),
781 None,
782 &crate::core::policy::hive_filters::FilterRules::default(),
783 None,
784 )
785}
786
787fn apply_org_floor(
792 local: Option<ResolvedPolicy>,
793 org: Result<Option<ResolvedPolicy>, String>,
794 horizon: Option<chrono::DateTime<chrono::Utc>>,
795 hive: &crate::core::policy::hive_filters::FilterRules,
796 hive_floor: Option<&ResolvedPolicy>,
797) -> Option<Arc<ActivePolicy>> {
798 let effective = match org {
800 Ok(Some(org)) => Some(crate::core::policy::floor::merge_floor(
801 &org,
802 local.as_ref(),
803 )),
804 Ok(None) => local,
805 Err(error) => {
806 tracing::error!("org policy rejected; enforcing deny-all: {error}");
807 return Some(Arc::new(ActivePolicy::deny_all()));
808 }
809 };
810 let effective = match hive_floor {
811 Some(floor) => Some(crate::core::policy::hive_floor::apply(
812 floor,
813 effective.as_ref(),
814 )),
815 None => effective,
816 };
817 let effective = match effective {
818 Some(effective) => effective,
819 None if hive.redaction.is_empty() && hive.blocked.is_empty() => return None,
820 None => hive_filters_only(),
822 };
823 Some(Arc::new(
824 ActivePolicy::from_resolved_with_hive(effective, hive).until(horizon),
825 ))
826}
827
828fn hive_filters_only() -> ResolvedPolicy {
830 ResolvedPolicy {
831 name: "hive-filters".into(),
832 version: "1.0.0".into(),
833 description: "content rules of the installed Hive filter packs".into(),
834 chain: Vec::new(),
835 default_read_mode: None,
836 allow_tools: None,
837 deny_tools: Vec::new(),
838 max_context_tokens: None,
839 audit_retention_days: None,
840 redaction: std::collections::BTreeMap::new(),
841 filters: crate::core::policy::FilterRules::default(),
842 egress: crate::core::policy::EgressRules::default(),
843 routing: crate::core::policy::RoutingPolicyRules::default(),
844 budgets: crate::core::policy::BudgetRules::default(),
845 }
846}
847
848#[cfg(test)]
851fn load_local_pack() -> Result<Option<ResolvedPolicy>, String> {
852 let path = PathBuf::from(PROJECT_PACK_PATH);
853 load_local_pack_at(&path)
854}
855
856#[cfg(test)]
857fn load_local_pack_at(path: &Path) -> Result<Option<ResolvedPolicy>, String> {
858 match std::fs::symlink_metadata(path) {
859 Ok(_) => {}
860 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
861 Err(error) => return Err(format!("{}: {error}", path.display())),
862 }
863 match parse_file(path).and_then(|p| resolve(&p)) {
864 Ok(resolved) => Ok(Some(resolved)),
865 Err(e) => Err(format!("{}: {e}", path.display())),
866 }
867}
868
869pub fn for_project(project_root: &Path) -> Result<Option<Arc<ActivePolicy>>, String> {
872 refresh::current(project_root.to_path_buf())
873}
874
875#[must_use]
878pub fn active() -> Option<Arc<ActivePolicy>> {
879 active_observed().0
880}
881
882#[must_use]
894pub(crate) fn active_observed() -> (Option<Arc<ActivePolicy>>, Option<super::org::Observation>) {
895 let deny_all = || (Some(Arc::new(ActivePolicy::deny_all())), None);
896 if let Some(view) = current_source_view() {
897 if !view.live.load(Ordering::Acquire) || !view.valid.load(Ordering::Acquire) {
898 return deny_all();
899 }
900 let project = REQUEST_PROJECT
901 .try_with(|slot| slot.borrow().clone())
902 .ok()
903 .flatten();
904 if project.as_ref() != Some(&view.project) || !view.within_horizon() {
905 view.invalidate_if_live();
906 return deny_all();
907 }
908 return (view.policy, view.observation);
909 }
910 #[cfg(any(test, feature = "test-support"))]
911 {
912 let state = test_override()
913 .read()
914 .unwrap_or_else(std::sync::PoisonError::into_inner);
915 if let OverrideState::Fixed(policy) = &*state {
916 return (policy.clone(), None);
917 }
918 }
919 let scoped = REQUEST_PROJECT
920 .try_with(|slot| slot.borrow().clone())
921 .ok()
922 .flatten();
923 match scoped
924 .map_or_else(std::env::current_dir, Ok)
925 .map_err(|_| "policy project root unavailable".into())
926 .and_then(refresh::current_verified)
927 {
928 Ok(verified) => (verified.policy, verified.observation),
929 Err(_) => deny_all(),
930 }
931}
932
933#[cfg(any(test, feature = "test-support"))]
938pub fn with_observed_view_for_test<T>(
939 project: &Path,
940 observation: Option<super::org::Observation>,
941 operation: impl FnOnce() -> T,
942) -> T {
943 let project = canonical_project_root(project).expect("test project root");
944 let role = crate::core::roles::active_role();
945 let verified = refresh::current_verified(project.clone()).expect("test authority");
949 let view = SourceView {
950 project: project.clone(),
951 policy: verified.policy,
952 fingerprint: verified.fingerprint,
953 role_fingerprint: role_fingerprint(&role).expect("test role"),
954 role,
955 lifetime: None,
956 observation,
957 valid: Arc::new(AtomicBool::new(true)),
958 live: Arc::new(AtomicBool::new(true)),
959 };
960 REQUEST_PROJECT.sync_scope(std::cell::RefCell::new(Some(project)), || {
961 SOURCE_VIEW.sync_scope(std::cell::RefCell::new(Some(view)), operation)
962 })
963}
964
965pub(crate) enum HeldObservation {
967 NotHeld,
969 Held(Option<super::org::Observation>),
972}
973
974pub(crate) fn held_observation() -> HeldObservation {
981 let Some(view) = current_source_view() else {
982 return HeldObservation::NotHeld;
983 };
984 let project = REQUEST_PROJECT
985 .try_with(|slot| slot.borrow().clone())
986 .ok()
987 .flatten();
988 if !view.live.load(Ordering::Acquire)
989 || !view.valid.load(Ordering::Acquire)
990 || project.as_ref() != Some(&view.project)
991 || !view.within_horizon()
992 {
993 return HeldObservation::NotHeld;
994 }
995 HeldObservation::Held(view.observation)
996}
997
998#[must_use]
1000pub fn is_active() -> bool {
1001 active().is_some()
1002}
1003
1004pub fn reload() {
1006 let _ = active();
1007}
1008
1009#[cfg(any(test, feature = "test-support"))]
1010enum OverrideState {
1011 Live,
1012 Fixed(Option<Arc<ActivePolicy>>),
1013}
1014
1015#[cfg(any(test, feature = "test-support"))]
1016fn test_override() -> &'static RwLock<OverrideState> {
1017 static OVERRIDE: OnceLock<RwLock<OverrideState>> = OnceLock::new();
1018 OVERRIDE.get_or_init(|| RwLock::new(OverrideState::Live))
1019}
1020
1021#[cfg(any(test, feature = "test-support"))]
1022fn test_override_lock() -> &'static std::sync::Mutex<()> {
1023 static LOCK: OnceLock<std::sync::Mutex<()>> = OnceLock::new();
1024 LOCK.get_or_init(|| std::sync::Mutex::new(()))
1025}
1026
1027#[cfg(any(test, feature = "test-support"))]
1032pub struct TestOverrideGuard {
1033 _policy: std::sync::MutexGuard<'static, ()>,
1034 _environment: crate::core::data_dir::TestEnvGuard,
1035}
1036
1037#[cfg(any(test, feature = "test-support"))]
1038pub fn lock_test_override() -> TestOverrideGuard {
1039 let environment = crate::core::data_dir::test_env_lock();
1040 let policy = test_override_lock()
1041 .lock()
1042 .unwrap_or_else(std::sync::PoisonError::into_inner);
1043 TestOverrideGuard {
1044 _policy: policy,
1045 _environment: environment,
1046 }
1047}
1048
1049#[cfg(any(test, feature = "test-support"))]
1051pub struct TestPolicyOverride {
1052 previous: OverrideState,
1053 _lock: TestOverrideGuard,
1054}
1055
1056#[cfg(any(test, feature = "test-support"))]
1057impl TestPolicyOverride {
1058 pub fn set(resolved: Option<ResolvedPolicy>) -> Self {
1059 let lock = lock_test_override();
1060 let previous = std::mem::replace(
1061 &mut *test_override()
1062 .write()
1063 .unwrap_or_else(std::sync::PoisonError::into_inner),
1064 OverrideState::Fixed(
1065 resolved.map(|policy| Arc::new(ActivePolicy::from_resolved(policy))),
1066 ),
1067 );
1068 Self {
1069 previous,
1070 _lock: lock,
1071 }
1072 }
1073}
1074
1075#[cfg(any(test, feature = "test-support"))]
1076impl Drop for TestPolicyOverride {
1077 fn drop(&mut self) {
1078 *test_override()
1079 .write()
1080 .unwrap_or_else(std::sync::PoisonError::into_inner) =
1081 std::mem::replace(&mut self.previous, OverrideState::Live);
1082 }
1083}
1084
1085#[cfg(test)]
1086#[path = "runtime/tests.rs"]
1087mod tests;