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,
input_origin: Option<&super::origin::InputOrigin>,
) -> Option<String> {
let class = class_of_path(path, input_origin);
self.admit_object(text, Some(path), &class)
}
#[must_use]
pub fn admit_text(&self, text: &str) -> Option<String> {
self.admit_object(text, None, &STORE_UNCLASSIFIED)
}
#[must_use]
pub fn admit_text_as(&self, text: &str, class: &super::origin::SourceClass) -> Option<String> {
self.admit_object(text, None, class)
}
#[must_use]
pub fn admit_instance_text(&self, text: &str) -> Option<String> {
self.admit_object(text, None, &super::origin::SourceClass::Project)
}
#[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, &super::origin::SourceClass::Project, &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_as(text, &super::origin::SourceClass::Project)?;
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>,
class: &super::origin::SourceClass,
) -> Option<String> {
super::capture::note_origin(class);
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, class, &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, None)
}
}
const STORE_UNCLASSIFIED: super::origin::SourceClass =
super::origin::SourceClass::Unclassified { tool: "store" };
pub(crate) fn class_of_path(
path: &Path,
input_origin: Option<&super::origin::InputOrigin>,
) -> super::origin::SourceClass {
use super::origin::SourceClass;
if let Some(origin) = input_origin {
return SourceClass::External(origin.clone());
}
let label = path.to_string_lossy();
if label.contains("://") {
return STORE_UNCLASSIFIED;
}
let escapes = path
.components()
.any(|part| matches!(part, std::path::Component::ParentDir));
if escapes {
return STORE_UNCLASSIFIED;
}
if path.is_relative() {
return SourceClass::Project;
}
let Some(root) = crate::core::policy::diagnostics::project() else {
return STORE_UNCLASSIFIED;
};
if path.starts_with(&root) && !escapes_by_symlink(path, &root) {
return SourceClass::Project;
}
let canonical_root = crate::core::pathutil::canonicalize_secure(&root).unwrap_or(root);
let resolved =
crate::core::pathutil::canonicalize_secure(path).unwrap_or_else(|_| path.to_path_buf());
if resolved.starts_with(&canonical_root) {
SourceClass::Project
} else {
STORE_UNCLASSIFIED
}
}
fn escapes_by_symlink(path: &Path, root: &Path) -> bool {
let Ok(resolved) = crate::core::pathutil::canonicalize_secure(path) else {
return false;
};
let root = crate::core::pathutil::canonicalize_secure(root).unwrap_or_else(|_| root.into());
!resolved.starts_with(root)
}
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)
}