use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock, PoisonError};
use crate::core::input_filters::FilterAction;
use crate::core::policy::runtime::PublicationAuthority;
use super::{
AdmissionPolicy, OutputCompleteness, admit, admit_output_for_poll, admit_output_for_storage,
clean, fingerprint_digest,
};
#[derive(Debug, Clone)]
pub struct StoreAdmission {
policy: AdmissionPolicy,
fingerprint: Vec<u8>,
digest: String,
epoch: u64,
}
static EPOCH: AtomicU64 = AtomicU64::new(0);
#[must_use]
pub fn current_epoch() -> u64 {
EPOCH.load(Ordering::SeqCst)
}
impl StoreAdmission {
#[must_use]
pub fn current() -> Self {
let policy = AdmissionPolicy::from_config(&crate::core::config::Config::load_arc());
let mut admission = Self::with_policy(policy);
admission.epoch = observe_policy(&admission.digest);
admission
}
#[must_use]
pub fn with_policy(policy: AdmissionPolicy) -> Self {
let fingerprint = policy.fingerprint();
let digest = fingerprint_digest(&fingerprint).hex().to_owned();
Self {
policy,
fingerprint,
digest,
epoch: u64::MAX,
}
}
#[must_use]
pub fn digest(&self) -> &str {
&self.digest
}
pub fn admission_policy(&self) -> &AdmissionPolicy {
&self.policy
}
#[must_use]
pub fn is_current(&self) -> bool {
self.epoch == current_epoch()
}
#[must_use]
pub fn epoch(&self) -> u64 {
self.epoch
}
pub fn authorize_publication(&self, authority: &PublicationAuthority) -> Result<(), String> {
if self.epoch == u64::MAX || !self.is_current() || !self.matches_current_config() {
return Err("session admission policy changed; retry".into());
}
authority.verify()
}
fn matches_current_config(&self) -> bool {
let config = crate::core::config::Config::load_arc();
let policy = AdmissionPolicy::from_config(&config);
fingerprint_digest(&policy.fingerprint()).hex() == self.digest
}
#[must_use]
pub fn admit(&self, text: &str, path: &Path) -> Option<String> {
self.admit_object(text, Some(path))
}
#[must_use]
pub fn admit_text(&self, text: &str) -> Option<String> {
self.admit_object(text, None)
}
#[must_use]
pub(crate) fn built_in_floor(&self) -> Self {
let mut floor = self.clone();
floor.policy.semantic = None;
floor.fingerprint = floor.policy.fingerprint();
floor
}
#[must_use]
pub(crate) fn admit_engine_session_id(
&self,
id: crate::core::session::EngineSessionId<'_>,
) -> Option<String> {
let text = id.as_str();
let mut policy = self.policy.clone();
policy.gateway.pii = FilterAction::Off;
let admission = admit(text, None, &policy);
(admission.storable() && admission.text.as_deref() == Some(text)).then(|| text.to_owned())
}
#[must_use]
pub fn admit_shell_command(&self, text: &str) -> Option<String> {
crate::core::policy::content::protect_active(text).ok()?;
let admitted = self.admit_text(text)?;
crate::core::policy::content::protect_active(&admitted)
.ok()
.map(std::borrow::Cow::into_owned)
}
#[must_use]
pub fn admit_shell_output(
&self,
text: &str,
completeness: OutputCompleteness,
) -> Option<String> {
super::semantic_deferred::for_stored_context(|| {
admit_output_for_storage(text, completeness, &self.policy)
})
}
#[must_use]
pub fn admit_background_output(
&self,
text: &str,
completeness: OutputCompleteness,
) -> Option<String> {
super::semantic_deferred::for_stored_context(|| {
admit_output_for_poll(text, completeness, &self.policy)
})
}
fn admit_object(&self, text: &str, path: Option<&Path>) -> Option<String> {
if !self.policy.enabled_effective() {
return Some(text.to_owned());
}
let label = path.map(|path| path.to_string_lossy());
let key = self
.policy
.semantic
.is_none()
.then(|| clean::key(&self.fingerprint, label.as_deref().unwrap_or(""), text));
if let Some(key) = &key
&& clean::get(key).is_some()
&& path.is_none_or(|path| crate::core::io_boundary::is_secret_like(path).is_none())
{
return Some(text.to_owned());
}
let admission =
super::semantic_deferred::for_stored_context(|| admit(text, path, &self.policy));
if !admission.storable() {
return None;
}
if let Some(key) = key
&& admission.decision.reason_codes.is_empty()
&& admission.text.as_deref() == Some(text)
{
clean::insert(key, admission.decision.clone());
}
admission.text
}
#[must_use]
pub fn read(&self, path: &Path) -> Option<String> {
let decoded = crate::core::text_decode::read_text_checked(path).ok()?;
if !decoded.complete() {
tracing::debug!(
"store admission: incompletely decoded source skipped ({})",
decoded.encoding.label()
);
return None;
}
self.admit(&decoded.text, path)
}
}
fn observe_policy(digest: &str) -> u64 {
static LAST: OnceLock<Mutex<Option<String>>> = OnceLock::new();
let mut last = LAST
.get_or_init(|| Mutex::new(None))
.lock()
.unwrap_or_else(PoisonError::into_inner);
if last.as_deref() != Some(digest) {
if last.is_some() {
EPOCH.fetch_add(1, Ordering::SeqCst);
crate::core::content_cache::clear();
crate::core::search_index::clear_resident();
}
*last = Some(digest.to_owned());
}
current_epoch()
}
#[must_use]
pub fn admitted_store_text(path: &Path) -> Option<String> {
StoreAdmission::current().read(path)
}