use std::collections::BTreeMap;
use std::path::Path;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
use super::register::{self, Register};
pub(crate) const VALIDITY_SECONDS: i64 = 10 * 60;
pub(crate) const MAX_CAPABILITY_TTL_SECONDS: i64 = 24 * 60 * 60;
const CAPABILITY_PREFIX: &str = "lcp1_";
const REFERENCE_PREFIX: &str = "presenter:";
const MAX_CAPABILITY_FILE_BYTES: u64 = 128;
const MAX_PRESENTERS_PER_SOURCE: usize = 16;
const MAX_PROCESS_PRESENTERS_PER_SOURCE: usize = 16;
const MAX_IDENTITIES_PER_BINDING: usize = 8;
#[cfg(not(unix))]
const LOCAL_OPERATOR: &str = "local-operator";
#[must_use]
pub(crate) fn verified_principal() -> String {
#[cfg(unix)]
{
let uid = unsafe { libc::geteuid() };
format!("os-user:{uid}")
}
#[cfg(not(unix))]
{
LOCAL_OPERATOR.to_owned()
}
}
#[derive(Clone)]
pub struct PresenterCapability(Zeroizing<[u8; 32]>);
impl std::fmt::Debug for PresenterCapability {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("PresenterCapability(<redacted>)")
}
}
fn hex_nibble(digit: u8) -> Option<u8> {
match digit {
b'0'..=b'9' => Some(digit - b'0'),
b'a'..=b'f' => Some(digit - b'a' + 10),
_ => None,
}
}
impl PresenterCapability {
fn mint() -> Option<Self> {
let mut secret = Zeroizing::new([0u8; 32]);
getrandom::fill(&mut *secret).ok()?;
Some(Self(secret))
}
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
let digits = text.strip_prefix(CAPABILITY_PREFIX)?.as_bytes();
if digits.len() != 64 {
return None;
}
let mut secret = Zeroizing::new([0u8; 32]);
for (index, pair) in digits.as_chunks::<2>().0.iter().enumerate() {
secret[index] = (hex_nibble(pair[0])? << 4) | hex_nibble(pair[1])?;
}
Some(Self(secret))
}
#[must_use]
pub fn expose(&self) -> Zeroizing<String> {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut text = Zeroizing::new(String::with_capacity(CAPABILITY_PREFIX.len() + 64));
text.push_str(CAPABILITY_PREFIX);
for byte in self.0.iter() {
text.push(char::from(DIGITS[usize::from(byte >> 4)]));
text.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
}
text
}
fn root_verifier(&self) -> Zeroizing<[u8; 32]> {
Zeroizing::new(blake3::derive_key(
"leanctx 2026-10 presenter root verifier v1",
&*self.0,
))
}
fn id(&self) -> String {
let mut hash = blake3::Hasher::new();
hash.update(b"leanctx-presenter-id-v1\0");
hash.update(&*self.root_verifier());
hash.finalize().to_hex().to_string()
}
#[must_use]
pub fn reference(&self) -> String {
format!("{REFERENCE_PREFIX}{}", &self.id()[..32])
}
fn source_verifier(&self, binding: &PresentedBinding, id: &str) -> blake3::Hash {
let mut hash = blake3::Hasher::new_keyed(&self.0);
hash.update(b"leanctx-presenter-source-v1\0");
binding.absorb_source(&mut hash);
hash.update(id.as_bytes());
hash.finalize()
}
}
static PROCESS_SESSION: OnceLock<Option<PresenterCapability>> = OnceLock::new();
fn process_session() -> Option<&'static PresenterCapability> {
PROCESS_SESSION
.get_or_init(PresenterCapability::mint)
.as_ref()
}
fn is_process_session(id: &str) -> bool {
PROCESS_SESSION
.get()
.and_then(Option::as_ref)
.is_some_and(|own| own.id() == id)
}
tokio::task_local! {
static REQUEST_PRESENTER: Option<PresenterCapability>;
}
pub fn with_presenter<T>(
capability: Option<PresenterCapability>,
operation: impl FnOnce() -> T,
) -> T {
REQUEST_PRESENTER.sync_scope(capability, operation)
}
pub async fn presenter_scope<F: std::future::Future>(
capability: Option<PresenterCapability>,
request: F,
) -> F::Output {
REQUEST_PRESENTER.scope(capability, request).await
}
#[must_use]
pub fn request_presenter() -> Option<PresenterCapability> {
REQUEST_PRESENTER.try_with(Clone::clone).ok().flatten()
}
fn with_current<T>(use_capability: impl FnOnce(&PresenterCapability) -> T) -> Option<T> {
let shown = REQUEST_PRESENTER.try_with(Clone::clone).ok().flatten();
match shown {
Some(capability) => Some(use_capability(&capability)),
None => process_session().map(use_capability),
}
}
#[must_use]
pub(crate) fn current_presenter_reference() -> Option<String> {
with_current(PresenterCapability::reference)
}
#[must_use]
pub fn shown_presenter_reference() -> Option<String> {
let shown = REQUEST_PRESENTER.try_with(Clone::clone).ok().flatten()?;
let id = shown.id();
(!is_process_session(&id) && lifetime_end(&id, now()).is_some()).then(|| shown.reference())
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct PresenterRecord {
expires_at: i64,
revoked: bool,
}
const PRESENTERS: Register = Register {
name: "presenters",
shard_entries: 128,
max_age_seconds: MAX_CAPABILITY_TTL_SECONDS,
};
pub const REGISTER_DIRECTORY: &str = "presented-v2";
const REGISTER: Register = Register {
name: REGISTER_DIRECTORY,
shard_entries: 128,
max_age_seconds: MAX_CAPABILITY_TTL_SECONDS,
};
fn lifetime_end(id: &str, now: i64) -> Option<i64> {
match PRESENTERS.read::<PresenterRecord>(id, now) {
Some(record) if record.revoked || record.expires_at < now => None,
Some(record) => Some(record.expires_at),
None => is_process_session(id).then(|| now.saturating_add(MAX_CAPABILITY_TTL_SECONDS)),
}
}
pub struct IssuedCapability {
pub capability: PresenterCapability,
pub(crate) expires_at: i64,
}
#[must_use]
pub fn issue(ttl_seconds: u32) -> Option<IssuedCapability> {
issue_at(ttl_seconds, now())
}
fn issue_at(ttl_seconds: u32, now: i64) -> Option<IssuedCapability> {
let ttl = i64::from(ttl_seconds);
if !(1..=MAX_CAPABILITY_TTL_SECONDS).contains(&ttl) {
return None;
}
let capability = PresenterCapability::mint()?;
let expires_at = now.checked_add(ttl)?;
PRESENTERS
.update::<PresenterRecord>(&capability.id(), now, |known| {
known.is_none().then_some(PresenterRecord {
expires_at,
revoked: false,
})
})
.then_some(IssuedCapability {
capability,
expires_at,
})
}
#[must_use]
pub fn issued_lifetime_end(capability: &PresenterCapability) -> Option<i64> {
let id = capability.id();
if is_process_session(&id) {
return None;
}
lifetime_end(&id, now())
}
pub fn revoke(capability: &PresenterCapability) -> bool {
let now = now();
PRESENTERS.update::<PresenterRecord>(&capability.id(), now, |known| {
known.map(|record| PresenterRecord {
revoked: true,
..record
})
})
}
#[must_use]
pub fn read_capability_file(path: &Path) -> Option<PresenterCapability> {
use std::io::Read as _;
if !std::fs::symlink_metadata(path).ok()?.file_type().is_file() {
return None;
}
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
}
let file = options.open(path).ok()?;
let metadata = file.metadata().ok()?;
if !metadata.is_file() || metadata.len() > MAX_CAPABILITY_FILE_BYTES {
return None;
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let own = unsafe { libc::geteuid() };
if metadata.uid() != own || metadata.mode() & 0o077 != 0 {
return None;
}
}
let mut text = Zeroizing::new(String::new());
let mut bounded = file.take(MAX_CAPABILITY_FILE_BYTES);
bounded.read_to_string(&mut text).ok()?;
PresenterCapability::parse(text.trim_end_matches(['\r', '\n']))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PresentedBinding {
project: String,
caller: String,
role: String,
object_ref: String,
source_id: String,
content_digest: String,
policy_identity: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Presentation {
Current,
OtherPolicy,
None,
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct SourcePresenters {
presenters: BTreeMap<String, PresenterEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct PresenterEntry {
verifier: String,
admitted: BTreeMap<String, i64>,
expires_at: i64,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
process_only: bool,
}
fn in_time(presented_at: i64, now: i64) -> bool {
now.checked_sub(presented_at)
.is_some_and(|age| (0..=VALIDITY_SECONDS).contains(&age))
}
impl PresenterEntry {
fn live(&self, now: i64) -> bool {
self.expires_at >= now && self.admitted.values().any(|at| in_time(*at, now))
}
fn last_presented(&self) -> i64 {
self.admitted.values().copied().max().unwrap_or(i64::MIN)
}
}
pub(crate) fn policy_identity(project: &Path) -> Option<String> {
let gateway = crate::core::context_admission::stores::StoreAdmission::current();
let pack = crate::core::policy::runtime::for_project(project).ok()?;
let pack = serde_json::to_vec(&pack.as_ref().map(|active| &active.resolved)).ok()?;
let kernel =
crate::core::engine_interface::context_plan::sources::kernel_policy_identity(project)?;
let mut hash = blake3::Hasher::new();
hash.update(b"leanctx-presented-policy-v1\0");
hash.update(gateway.digest().as_bytes());
hash.update(b"\0");
hash.update(&pack);
hash.update(b"\0");
hash.update(kernel.as_bytes());
Some(hash.finalize().to_hex().to_string())
}
fn canonical_project(project: &Path) -> Option<String> {
crate::core::pathutil::canonicalize_secure(project)
.ok()?
.to_str()
.map(str::to_owned)
}
impl PresentedBinding {
pub fn admitted_now(
project: &Path,
object_ref: &str,
source_id: &str,
content_digest: &str,
) -> Option<Self> {
Self::admitted_by(
&verified_principal(),
project,
object_ref,
source_id,
content_digest,
)
}
fn admitted_by(
principal: &str,
project: &Path,
object_ref: &str,
source_id: &str,
content_digest: &str,
) -> Option<Self> {
Some(Self {
project: canonical_project(project)?,
caller: principal.to_owned(),
role: crate::core::roles::active_role_name(),
object_ref: object_ref.to_owned(),
source_id: source_id.to_owned(),
content_digest: content_digest.to_owned(),
policy_identity: policy_identity(project)?,
})
}
pub(super) fn project(&self) -> &str {
&self.project
}
pub(super) fn source_id(&self) -> &str {
&self.source_id
}
pub(super) fn same_source(&self, other: &Self) -> bool {
self.key() == other.key()
}
pub(super) fn identity(&self) -> String {
format!("{}:{}", self.key(), self.policy_identity)
}
fn absorb_source(&self, hash: &mut blake3::Hasher) {
for field in [
&self.project,
&self.caller,
&self.role,
&self.object_ref,
&self.source_id,
&self.content_digest,
] {
hash.update(&(field.len() as u64).to_be_bytes());
hash.update(field.as_bytes());
}
}
fn key(&self) -> String {
let mut hash = blake3::Hasher::new();
hash.update(b"leanctx-presented-binding-v2\0");
self.absorb_source(&mut hash);
hash.finalize().to_hex().to_string()
}
pub(super) fn matches_request(&self) -> bool {
let project = crate::core::policy::runtime::REQUEST_PROJECT
.try_with(|slot| slot.borrow().clone())
.ok()
.flatten()
.and_then(|path| canonical_project(&path));
project.as_deref() == Some(self.project.as_str())
&& self.caller == verified_principal()
&& self.role == crate::core::roles::active_role_name()
}
fn own_entry(&self, now: i64) -> Option<PresenterEntry> {
with_current(|capability| {
let id = capability.id();
let mut set = REGISTER.read::<SourcePresenters>(&self.key(), now)?;
let entry = set.presenters.remove(&id)?;
let stored = blake3::Hash::from_hex(entry.verifier.as_bytes()).ok()?;
(stored == capability.source_verifier(self, &id)
&& entry.expires_at >= now
&& lifetime_end(&id, now).is_some())
.then_some(entry)
})
.flatten()
}
pub(super) fn presentation(&self) -> Presentation {
let now = now();
if self.own_entry(now).is_none() {
return Presentation::None;
}
let Some(current) = policy_identity(Path::new(&self.project)) else {
return Presentation::OtherPolicy;
};
if self.policy_identity != current {
return Presentation::OtherPolicy;
}
self.presentation_at(¤t, now)
}
fn presentation_at(&self, current_identity: &str, now: i64) -> Presentation {
let Some(entry) = self.own_entry(now) else {
return Presentation::None;
};
if entry
.admitted
.get(current_identity)
.is_some_and(|at| in_time(*at, now))
{
Presentation::Current
} else if entry.admitted.values().any(|at| in_time(*at, now)) {
Presentation::OtherPolicy
} else {
Presentation::None
}
}
}
pub fn record_presentation(binding: &PresentedBinding) -> bool {
record_presentation_at(binding, now())
}
#[cfg(not(any(test, feature = "test-support")))]
fn now() -> i64 {
register::now()
}
#[cfg(any(test, feature = "test-support"))]
fn now() -> i64 {
register::now().saturating_add(TEST_CLOCK_AHEAD.load(std::sync::atomic::Ordering::SeqCst))
}
#[cfg(any(test, feature = "test-support"))]
static TEST_CLOCK_AHEAD: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
#[cfg(any(test, feature = "test-support"))]
pub struct TestClock;
#[cfg(any(test, feature = "test-support"))]
impl TestClock {
#[must_use]
pub fn past_validity() -> Self {
TEST_CLOCK_AHEAD.fetch_add(VALIDITY_SECONDS + 1, std::sync::atomic::Ordering::SeqCst);
Self
}
#[must_use]
pub fn past_capability_lifetime() -> Self {
TEST_CLOCK_AHEAD.fetch_add(
MAX_CAPABILITY_TTL_SECONDS + 1,
std::sync::atomic::Ordering::SeqCst,
);
Self
}
}
#[cfg(any(test, feature = "test-support"))]
impl Drop for TestClock {
fn drop(&mut self) {
TEST_CLOCK_AHEAD.store(0, std::sync::atomic::Ordering::SeqCst);
}
}
fn record_presentation_at(binding: &PresentedBinding, now: i64) -> bool {
let Some((id, verifier, expires_at, process_only)) = with_current(|capability| {
let id = capability.id();
let expires_at = lifetime_end(&id, now)?;
let verifier = capability
.source_verifier(binding, &id)
.to_hex()
.to_string();
let process_only = is_process_session(&id);
Some((id, verifier, expires_at, process_only))
})
.flatten() else {
return false;
};
REGISTER.update::<SourcePresenters>(&binding.key(), now, |known| {
let mut set = known.unwrap_or_default();
set.presenters.retain(|_, entry| entry.live(now));
let mut entry = set
.presenters
.remove(&id)
.filter(|known| known.verifier == verifier)
.unwrap_or_else(|| PresenterEntry {
verifier: verifier.clone(),
admitted: BTreeMap::new(),
expires_at,
process_only,
});
entry.expires_at = expires_at;
entry.admitted.retain(|_, at| in_time(*at, now));
entry.admitted.insert(binding.policy_identity.clone(), now);
while entry.admitted.len() > MAX_IDENTITIES_PER_BINDING {
let oldest = entry
.admitted
.iter()
.min_by_key(|(identity, presented_at)| (**presented_at, (*identity).clone()))
.map(|(identity, _)| identity.clone())?;
entry.admitted.remove(&oldest);
}
let of_kind = |set: &SourcePresenters| {
set.presenters
.values()
.filter(|known| known.process_only == process_only)
.count()
};
if process_only {
while of_kind(&set) >= MAX_PROCESS_PRESENTERS_PER_SOURCE {
let oldest = set
.presenters
.iter()
.filter(|(_, known)| known.process_only)
.min_by_key(|(known_id, known)| (known.last_presented(), (*known_id).clone()))
.map(|(known_id, _)| known_id.clone())?;
set.presenters.remove(&oldest);
}
} else if of_kind(&set) >= MAX_PRESENTERS_PER_SOURCE {
return None;
}
set.presenters.insert(id.clone(), entry);
Some(set)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn binding(project: &Path, object_ref: &str, digest: &str, identity: &str) -> PresentedBinding {
PresentedBinding {
project: canonical_project(project).expect("project"),
caller: verified_principal(),
role: crate::core::roles::active_role_name(),
object_ref: object_ref.to_owned(),
source_id: "source-one".to_owned(),
content_digest: digest.to_owned(),
policy_identity: identity.to_owned(),
}
}
#[test]
fn a_presentation_by_one_principal_does_not_satisfy_another() {
let _data = crate::core::data_dir::isolated_data_dir();
let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
let directory = tempfile::tempdir().expect("project");
let root = directory.path().canonicalize().expect("canonical root");
let by = |principal: &str| {
PresentedBinding::admitted_by(principal, &root, "ref-quartz", "source-one", "sha256:aa")
.expect("policy identity available")
};
let mine = PresentedBinding::admitted_now(&root, "ref-quartz", "source-one", "sha256:aa")
.expect("policy identity available");
assert_eq!(mine, by(&verified_principal()));
assert_ne!(verified_principal(), "another-verified-principal");
let theirs = by("another-verified-principal");
assert!(!mine.same_source(&theirs));
assert!(record_presentation(&theirs));
assert_eq!(theirs.presentation(), Presentation::Current);
assert_eq!(
mine.presentation(),
Presentation::None,
"their presentation is not mine"
);
crate::core::policy::runtime::REQUEST_PROJECT.sync_scope(
std::cell::RefCell::new(Some(root.clone())),
|| {
assert!(mine.matches_request());
assert!(
!theirs.matches_request(),
"a binding of another principal is refused for this request"
);
},
);
let issued = issue(60).expect("issued");
with_presenter(Some(issued.capability.clone()), || {
assert!(record_presentation(&theirs));
});
let raw = std::fs::read_to_string(REGISTER.shard_path(&theirs.key())).expect("shard");
assert!(!raw.contains("another-verified-principal"), "{raw}");
let wire = issued.capability.expose();
let records = std::fs::read_to_string(PRESENTERS.shard_path(&issued.capability.id()))
.expect("presenter record");
for stored in [&raw, &records] {
assert!(!stored.contains(&wire[CAPABILITY_PREFIX.len()..]));
}
assert_eq!(
format!("{:?}", issued.capability),
"PresenterCapability(<redacted>)"
);
assert!(PresenterCapability::parse(&wire).is_some());
let upper = wire.to_uppercase();
for malformed in ["", "lcp1_", &wire[..wire.len() - 1], upper.as_str()] {
assert!(PresenterCapability::parse(malformed).is_none());
}
}
#[test]
fn a_presentation_holds_for_its_exact_binding_policy_and_validity_only() {
let _data = crate::core::data_dir::isolated_data_dir();
let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
let directory = tempfile::tempdir().expect("project");
let root = directory.path().canonicalize().expect("canonical root");
let start = register::now();
let one = binding(&root, "ref-quartz", "sha256:aa", "policy-one");
assert_eq!(one.presentation_at("policy-one", start), Presentation::None);
assert!(record_presentation_at(&one, start));
assert_eq!(
one.presentation_at("policy-one", start),
Presentation::Current
);
assert_eq!(
one.presentation_at("policy-one", start + VALIDITY_SECONDS),
Presentation::Current
);
assert_eq!(
one.presentation_at("policy-one", start + VALIDITY_SECONDS + 1),
Presentation::None,
"not presented again within the validity"
);
assert_eq!(
one.presentation_at("policy-two", start),
Presentation::OtherPolicy,
"presented, but not admitted under the current policy"
);
let other_content = binding(&root, "ref-quartz", "sha256:bb", "policy-one");
let other_ref = binding(&root, "ref-basalt", "sha256:aa", "policy-one");
let mut other_caller = one.clone();
other_caller.caller = "another-principal".to_owned();
let mut other_role = one.clone();
other_role.role = "another-role".to_owned();
let elsewhere = tempfile::tempdir().expect("project");
let other_project = binding(elsewhere.path(), "ref-quartz", "sha256:aa", "policy-one");
for different in [
&other_content,
&other_ref,
&other_caller,
&other_role,
&other_project,
] {
assert!(!one.same_source(different));
assert_eq!(
different.presentation_at("policy-one", start),
Presentation::None
);
}
let again = binding(&root, "ref-quartz", "sha256:aa", "policy-two");
assert!(one.same_source(&again));
assert!(record_presentation_at(&again, start + 5));
assert_eq!(
one.presentation_at("policy-two", start + 5),
Presentation::Current
);
let shard = REGISTER.shard_path(&one.key());
let raw = std::fs::read_to_string(&shard).expect("persisted shard");
let principal = verified_principal();
for identifying in ["ref-quartz", "source-one", "sha256:aa", principal.as_str()] {
assert!(!raw.contains(identifying), "{identifying}");
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&shard)
.expect("shard")
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o600, "owner-only");
}
for index in 0..MAX_IDENTITIES_PER_BINDING + 2 {
let identity = format!("policy-bound-{index}");
let presented = binding(&root, "ref-quartz", "sha256:aa", &identity);
assert!(record_presentation_at(&presented, start + 10));
}
let kept = REGISTER
.read::<SourcePresenters>(&one.key(), start + 10)
.expect("entry");
assert_eq!(kept.presenters.len(), 1, "one presenter: this process");
let own = kept.presenters.values().next().expect("own entry");
assert_eq!(own.admitted.len(), MAX_IDENTITIES_PER_BINDING);
let issued: Vec<IssuedCapability> = (0..=MAX_PRESENTERS_PER_SOURCE)
.map(|_| issue_at(60, start + 10).expect("issued"))
.collect();
let recorded: Vec<bool> = issued
.iter()
.map(|presenter| {
with_presenter(Some(presenter.capability.clone()), || {
record_presentation_at(&one, start + 10)
})
})
.collect();
assert_eq!(
recorded.iter().filter(|recorded| **recorded).count(),
MAX_PRESENTERS_PER_SOURCE
);
assert_eq!(recorded.last(), Some(&false), "the one too many");
with_presenter(Some(issued[0].capability.clone()), || {
assert_eq!(
one.presentation_at("policy-one", start + 10),
Presentation::Current,
"the first presenter kept its entry"
);
});
}
}