Skip to main content

lean_ctx/core/archive/
presented.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Presentation register: the authority of caller-supplied context sources.
3//!
4//! A source the caller hands to the context plan is not a project file. The
5//! Engine cannot re-read it and does not learn when the caller's or a
6//! provider's copy changes. What it can assert, and all a [`PresentedBinding`]
7//! asserts, is: *this exact content (full digest) under this object reference
8//! and source id was presented for this project and admitted under this
9//! policy.*
10//!
11//! Every time the plan admits such a source for delivery (the revalidation
12//! that closes a materialization, after the delivery policy has passed) it
13//! records the presentation here. A binding holds only while a presentation
14//! of exactly that binding exists that was admitted under the CURRENT policy
15//! identity and is at most `VALIDITY_SECONDS` old — a caller that
16//! materializes its plan each turn always passes; one that stopped
17//! presenting the source does not.
18//!
19//! # Presenters
20//!
21//! A presentation belongs to a PRESENTER, and holds only for a request that
22//! shows that presenter's capability ([`PresenterCapability`]): 32 bytes from
23//! the system CSPRNG. Two kinds exist.
24//!
25//! - An ISSUED capability (`context-materialize-sources-v2`): the Engine
26//!   mints it, returns it once to the caller and stores nothing it could be
27//!   rebuilt from. The caller shows it again on later requests — to this
28//!   Engine, the proxy, the MCP server, the CLI — and so reuses what it
29//!   presented, across processes, until the capability expires or is revoked.
30//! - The PROCESS capability: drawn once per process, never written, logged or
31//!   returned. It is the presenter of every request that shows no issued
32//!   capability, so such a presentation holds inside the presenting process
33//!   only. This is the default for a caller that does not negotiate.
34//!
35//! What a capability proves is exactly this: *the request comes from a holder
36//! of the capability under which the source was presented.* It is not an
37//! agent name, not an attested identity and not an authorization. Project,
38//! OS user, role, policy identity and freshness are checked as before, after
39//! it; file sources of the same lineage are re-admitted as before.
40//!
41//! Per source the register keeps a bounded SET of presenter entries. Each has
42//! its own verifier, presentations and expiry; one presenter's presentation,
43//! lapse or revocation never touches another's entry.
44//!
45//! The register holds hashes, policy identities and times. No content, no
46//! object reference, no source id, and nothing a capability can be derived
47//! from: a reader of the store learns verifiers and presenter references,
48//! never a capability.
49
50use std::collections::BTreeMap;
51use std::path::Path;
52use std::sync::OnceLock;
53
54use serde::{Deserialize, Serialize};
55use zeroize::Zeroizing;
56
57use super::register::{self, Register};
58
59/// How long one admitted presentation keeps a binding valid: ten minutes.
60pub(crate) const VALIDITY_SECONDS: i64 = 10 * 60;
61
62/// Longest lifetime of an issued capability: one day.
63pub(crate) const MAX_CAPABILITY_TTL_SECONDS: i64 = 24 * 60 * 60;
64
65/// Wire prefix of a capability; 64 lowercase hex characters follow.
66const CAPABILITY_PREFIX: &str = "lcp1_";
67/// Wire prefix of a presenter reference; 32 lowercase hex characters follow.
68const REFERENCE_PREFIX: &str = "presenter:";
69/// A capability file is this long at most (the capability and a line end).
70const MAX_CAPABILITY_FILE_BYTES: u64 = 128;
71
72/// Issued presenters one source can have at a time. A further one is not
73/// recorded; no live entry is ever removed to make room.
74const MAX_PRESENTERS_PER_SOURCE: usize = 16;
75/// Process presenters one source can have at a time. Their entries are of
76/// use to their own process only, so the oldest of THEM makes room.
77const MAX_PROCESS_PRESENTERS_PER_SOURCE: usize = 16;
78/// Policy identities kept per presenter entry (a policy change adds one).
79const MAX_IDENTITIES_PER_BINDING: usize = 8;
80
81/// Principal where the platform offers no verified per-caller fact (non-Unix
82/// builds): one local operator.
83#[cfg(not(unix))]
84const LOCAL_OPERATOR: &str = "local-operator";
85
86/// The caller principal of a plan request, as far as the Engine can VERIFY
87/// it without anything the caller holds: the OS user its own process runs as,
88/// `os-user:<euid>`, stable and not secret. A name the client supplies is
89/// never a principal. Callers of one OS user are told apart by the presenter
90/// capability (module documentation, "Presenters"), not by this value.
91#[must_use]
92pub(crate) fn verified_principal() -> String {
93    #[cfg(unix)]
94    {
95        // SAFETY: `geteuid` has no preconditions and cannot fail.
96        let uid = unsafe { libc::geteuid() };
97        format!("os-user:{uid}")
98    }
99    #[cfg(not(unix))]
100    {
101        LOCAL_OPERATOR.to_owned()
102    }
103}
104
105/// A presenter capability: a bearer secret. Whoever shows it is treated as
106/// the holder of the capability its presentations were recorded under — no
107/// more than that.
108///
109/// The secret is overwritten when the value is dropped, has no `Display`, is
110/// not serializable and formats as `<redacted>`. It leaves this type in one
111/// place only: [`Self::expose`], for the response that hands a freshly issued
112/// capability to its caller.
113#[derive(Clone)]
114pub struct PresenterCapability(Zeroizing<[u8; 32]>);
115
116impl std::fmt::Debug for PresenterCapability {
117    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        formatter.write_str("PresenterCapability(<redacted>)")
119    }
120}
121
122fn hex_nibble(digit: u8) -> Option<u8> {
123    match digit {
124        b'0'..=b'9' => Some(digit - b'0'),
125        b'a'..=b'f' => Some(digit - b'a' + 10),
126        _ => None,
127    }
128}
129
130impl PresenterCapability {
131    fn mint() -> Option<Self> {
132        let mut secret = Zeroizing::new([0u8; 32]);
133        getrandom::fill(&mut *secret).ok()?;
134        Some(Self(secret))
135    }
136
137    /// The capability a host presented, from its wire form. Form only: an
138    /// unknown capability parses and then matches no presenter entry.
139    #[must_use]
140    pub fn parse(text: &str) -> Option<Self> {
141        let digits = text.strip_prefix(CAPABILITY_PREFIX)?.as_bytes();
142        if digits.len() != 64 {
143            return None;
144        }
145        let mut secret = Zeroizing::new([0u8; 32]);
146        for (index, pair) in digits.as_chunks::<2>().0.iter().enumerate() {
147            secret[index] = (hex_nibble(pair[0])? << 4) | hex_nibble(pair[1])?;
148        }
149        Some(Self(secret))
150    }
151
152    /// The wire form. Only for the response that issues the capability.
153    #[must_use]
154    pub fn expose(&self) -> Zeroizing<String> {
155        const DIGITS: &[u8; 16] = b"0123456789abcdef";
156        let mut text = Zeroizing::new(String::with_capacity(CAPABILITY_PREFIX.len() + 64));
157        text.push_str(CAPABILITY_PREFIX);
158        for byte in self.0.iter() {
159            text.push(char::from(DIGITS[usize::from(byte >> 4)]));
160            text.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
161        }
162        text
163    }
164
165    /// One-way root verifier of the capability. Everything public about a
166    /// presenter is derived from this, never from the secret directly.
167    fn root_verifier(&self) -> Zeroizing<[u8; 32]> {
168        Zeroizing::new(blake3::derive_key(
169            "leanctx 2026-10 presenter root verifier v1",
170            &*self.0,
171        ))
172    }
173
174    /// Register key of the presenter: a digest of the root verifier.
175    fn id(&self) -> String {
176        let mut hash = blake3::Hasher::new();
177        hash.update(b"leanctx-presenter-id-v1\0");
178        hash.update(&*self.root_verifier());
179        hash.finalize().to_hex().to_string()
180    }
181
182    /// Content-free reference of the presenter entry, for records: a digest
183    /// of a verifier, not of the capability. Naming it reveals no secret and
184    /// proves nothing by itself.
185    #[must_use]
186    pub fn reference(&self) -> String {
187        format!("{REFERENCE_PREFIX}{}", &self.id()[..32])
188    }
189
190    /// What the register stores for this presenter on one source: a keyed
191    /// hash, under the capability, of the source fields and the presenter
192    /// id. It verifies for no other source, project, caller or role.
193    fn source_verifier(&self, binding: &PresentedBinding, id: &str) -> blake3::Hash {
194        let mut hash = blake3::Hasher::new_keyed(&self.0);
195        hash.update(b"leanctx-presenter-source-v1\0");
196        binding.absorb_source(&mut hash);
197        hash.update(id.as_bytes());
198        hash.finalize()
199    }
200}
201
202/// The capability of this process: drawn once, held here only. `None`: the
203/// CSPRNG failed — the process then presents nothing and re-authorizes
204/// nothing without an issued capability.
205static PROCESS_SESSION: OnceLock<Option<PresenterCapability>> = OnceLock::new();
206
207fn process_session() -> Option<&'static PresenterCapability> {
208    PROCESS_SESSION
209        .get_or_init(PresenterCapability::mint)
210        .as_ref()
211}
212
213fn is_process_session(id: &str) -> bool {
214    PROCESS_SESSION
215        .get()
216        .and_then(Option::as_ref)
217        .is_some_and(|own| own.id() == id)
218}
219
220tokio::task_local! {
221    /// The capability the request in scope showed, if any. Request state,
222    /// like `policy::runtime::REQUEST_PROJECT`; never a global.
223    static REQUEST_PRESENTER: Option<PresenterCapability>;
224}
225
226/// Run `operation` as the request of a host that showed `capability`
227/// (`None`: it showed none — the process capability presents).
228pub fn with_presenter<T>(
229    capability: Option<PresenterCapability>,
230    operation: impl FnOnce() -> T,
231) -> T {
232    REQUEST_PRESENTER.sync_scope(capability, operation)
233}
234
235/// [`with_presenter`] around a future: the whole request task.
236pub async fn presenter_scope<F: std::future::Future>(
237    capability: Option<PresenterCapability>,
238    request: F,
239) -> F::Output {
240    REQUEST_PRESENTER.scope(capability, request).await
241}
242
243/// The capability the request in scope showed, to carry it onto a thread the
244/// request task hands work to: task-locals do not follow `spawn_blocking`,
245/// and a handler that ran without it would be served as this process's own
246/// presenter. The value goes back into [`with_presenter`] and nowhere else.
247#[must_use]
248pub fn request_presenter() -> Option<PresenterCapability> {
249    REQUEST_PRESENTER.try_with(Clone::clone).ok().flatten()
250}
251
252/// Run `use_capability` with the presenter of the request in scope: the
253/// capability it showed, else this process's own.
254fn with_current<T>(use_capability: impl FnOnce(&PresenterCapability) -> T) -> Option<T> {
255    let shown = REQUEST_PRESENTER.try_with(Clone::clone).ok().flatten();
256    match shown {
257        Some(capability) => Some(use_capability(&capability)),
258        None => process_session().map(use_capability),
259    }
260}
261
262/// Content-free reference of the presenter of the request in scope, for
263/// lineage, recovery and decision records.
264#[must_use]
265pub(crate) fn current_presenter_reference() -> Option<String> {
266    with_current(PresenterCapability::reference)
267}
268
269/// The reference of the presenter entry of an ISSUED capability the request
270/// in scope showed, while that capability may still present (known, not
271/// expired, not revoked). `None` for a request that showed nothing or
272/// something that is not such a capability: decision receipts name a
273/// presenter only then, and never the process capability.
274#[must_use]
275pub fn shown_presenter_reference() -> Option<String> {
276    let shown = REQUEST_PRESENTER.try_with(Clone::clone).ok().flatten()?;
277    let id = shown.id();
278    (!is_process_session(&id) && lifetime_end(&id, now()).is_some()).then(|| shown.reference())
279}
280
281/// What the store knows about an issued capability.
282#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
283#[serde(deny_unknown_fields)]
284struct PresenterRecord {
285    /// End of the lifetime fixed at issue (register clock, seconds).
286    expires_at: i64,
287    revoked: bool,
288}
289
290const PRESENTERS: Register = Register {
291    name: "presenters",
292    shard_entries: 128,
293    max_age_seconds: MAX_CAPABILITY_TTL_SECONDS,
294};
295
296/// Directory of the presenter sets, under the archive store. NOT the
297/// `presented` directory of binaries before presenters existed: those keep
298/// their own register (other value type), and two binaries writing one
299/// directory would each start the other's shards afresh. This binary neither
300/// reads nor writes nor removes `archives/presented`; what an older binary
301/// left there lapses by that binary's own ten-minute rule and is dead data
302/// afterwards.
303pub const REGISTER_DIRECTORY: &str = "presented-v2";
304
305const REGISTER: Register = Register {
306    name: REGISTER_DIRECTORY,
307    shard_entries: 128,
308    max_age_seconds: MAX_CAPABILITY_TTL_SECONDS,
309};
310
311/// End of the lifetime of the presenter `id` at `now`, or `None` when it may
312/// not present or reuse: unknown, expired, revoked. The process capability
313/// has no record and lives as long as its process.
314fn lifetime_end(id: &str, now: i64) -> Option<i64> {
315    match PRESENTERS.read::<PresenterRecord>(id, now) {
316        Some(record) if record.revoked || record.expires_at < now => None,
317        Some(record) => Some(record.expires_at),
318        None => is_process_session(id).then(|| now.saturating_add(MAX_CAPABILITY_TTL_SECONDS)),
319    }
320}
321
322/// A freshly issued capability with the end of its lifetime.
323pub struct IssuedCapability {
324    pub capability: PresenterCapability,
325    /// Register clock, seconds.
326    pub(crate) expires_at: i64,
327}
328
329/// Mint a capability that lives `ttl_seconds` (1 to
330/// `MAX_CAPABILITY_TTL_SECONDS`) and put its presenter on record. `None`:
331/// no randomness, a lifetime out of range, or the record could not be
332/// written — then no capability exists.
333#[must_use]
334pub fn issue(ttl_seconds: u32) -> Option<IssuedCapability> {
335    let ttl = i64::from(ttl_seconds);
336    if !(1..=MAX_CAPABILITY_TTL_SECONDS).contains(&ttl) {
337        return None;
338    }
339    let capability = PresenterCapability::mint()?;
340    let now = now();
341    let expires_at = now.checked_add(ttl)?;
342    PRESENTERS
343        .update::<PresenterRecord>(&capability.id(), now, |known| {
344            known.is_none().then_some(PresenterRecord {
345                expires_at,
346                revoked: false,
347            })
348        })
349        .then_some(IssuedCapability {
350            capability,
351            expires_at,
352        })
353}
354
355/// End of the lifetime of an ISSUED capability that may still present, or
356/// `None` (unknown, expired, revoked, or not an issued capability).
357#[must_use]
358pub fn issued_lifetime_end(capability: &PresenterCapability) -> Option<i64> {
359    let id = capability.id();
360    if is_process_session(&id) {
361        return None;
362    }
363    lifetime_end(&id, now())
364}
365
366/// Revoke an issued capability: from now on it presents nothing and reuses
367/// nothing. `false`: no such presenter on record (nothing to revoke), or the
368/// record could not be written.
369pub fn revoke(capability: &PresenterCapability) -> bool {
370    let now = now();
371    PRESENTERS.update::<PresenterRecord>(&capability.id(), now, |known| {
372        known.map(|record| PresenterRecord {
373            revoked: true,
374            ..record
375        })
376    })
377}
378
379/// The capability in a host's capability file, or `None`. On every platform
380/// the path must name a regular file and not a symlink (checked on the path
381/// itself, and again on the opened file), at most
382/// `MAX_CAPABILITY_FILE_BYTES` long. On Unix the open itself refuses a
383/// symlink (`O_NOFOLLOW`) and the file must be owned by this user and closed
384/// to group and others. On Windows ownership and access rights are NOT
385/// checked, and the two checks of the path are not atomic. Nothing about a
386/// refused file is reported.
387#[must_use]
388pub fn read_capability_file(path: &Path) -> Option<PresenterCapability> {
389    use std::io::Read as _;
390
391    if !std::fs::symlink_metadata(path).ok()?.file_type().is_file() {
392        return None;
393    }
394    let mut options = std::fs::OpenOptions::new();
395    options.read(true);
396    #[cfg(unix)]
397    {
398        use std::os::unix::fs::OpenOptionsExt;
399        options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
400    }
401    let file = options.open(path).ok()?;
402    let metadata = file.metadata().ok()?;
403    if !metadata.is_file() || metadata.len() > MAX_CAPABILITY_FILE_BYTES {
404        return None;
405    }
406    #[cfg(unix)]
407    {
408        use std::os::unix::fs::MetadataExt;
409        // SAFETY: `geteuid` has no preconditions and cannot fail.
410        let own = unsafe { libc::geteuid() };
411        if metadata.uid() != own || metadata.mode() & 0o077 != 0 {
412            return None;
413        }
414    }
415    let mut text = Zeroizing::new(String::new());
416    let mut bounded = file.take(MAX_CAPABILITY_FILE_BYTES);
417    bounded.read_to_string(&mut text).ok()?;
418    PresenterCapability::parse(text.trim_end_matches(['\r', '\n']))
419}
420
421/// One caller-supplied source as the plan admission resolved it. Content-free.
422/// It says WHICH source under WHICH policy; who may reuse it is the presenter
423/// set of the register, not a field here.
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425#[serde(deny_unknown_fields)]
426pub struct PresentedBinding {
427    /// Canonical project root the plan ran for.
428    project: String,
429    /// Caller principal ([`verified_principal`]).
430    caller: String,
431    /// Role active at admission.
432    role: String,
433    object_ref: String,
434    source_id: String,
435    /// The full content digest the plan binds (`sha256:…` of the caller's
436    /// original bytes).
437    content_digest: String,
438    /// Gateway policy, project policy pack and kernel context policy at
439    /// admission ([`policy_identity`]).
440    policy_identity: String,
441}
442
443/// What the register knows about a binding now, for the presenter of the
444/// request in scope.
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
446pub(super) enum Presentation {
447    /// Presented and admitted under the current policy identity, in time.
448    Current,
449    /// Presented in time, but only under another policy identity: under the
450    /// current one it has not been admitted.
451    OtherPolicy,
452    /// No presentation within the validity by the presenter that asks: none
453    /// at all, only by another presenter, or its capability is unknown,
454    /// expired or revoked. One answer for all of these.
455    None,
456}
457
458/// All presenter entries of one source.
459#[derive(Debug, Default, Serialize, Deserialize)]
460#[serde(deny_unknown_fields)]
461struct SourcePresenters {
462    /// Presenter id → entry.
463    presenters: BTreeMap<String, PresenterEntry>,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
467#[serde(deny_unknown_fields)]
468struct PresenterEntry {
469    /// `PresenterCapability::source_verifier`, hex.
470    verifier: String,
471    /// Policy identity → last admitted presentation.
472    admitted: BTreeMap<String, i64>,
473    /// End of the presenter's lifetime as known when it last presented.
474    expires_at: i64,
475    /// Recorded under a process capability.
476    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
477    process_only: bool,
478}
479
480fn in_time(presented_at: i64, now: i64) -> bool {
481    now.checked_sub(presented_at)
482        .is_some_and(|age| (0..=VALIDITY_SECONDS).contains(&age))
483}
484
485impl PresenterEntry {
486    /// Still able to authorize something: not expired, and presented within
487    /// the validity. Only an entry that is NOT live is ever removed.
488    fn live(&self, now: i64) -> bool {
489        self.expires_at >= now && self.admitted.values().any(|at| in_time(*at, now))
490    }
491
492    fn last_presented(&self) -> i64 {
493        self.admitted.values().copied().max().unwrap_or(i64::MIN)
494    }
495}
496
497/// Identity of everything that decides a plan admission for `project`: the
498/// gateway admission policy, the project policy pack and the kernel context
499/// policy (sensitivity ceiling, budget cap, retention). `None`: one of them
500/// cannot be loaded — nothing is then bound or re-authorized.
501pub(crate) fn policy_identity(project: &Path) -> Option<String> {
502    let gateway = crate::core::context_admission::stores::StoreAdmission::current();
503    let pack = crate::core::policy::runtime::for_project(project).ok()?;
504    let pack = serde_json::to_vec(&pack.as_ref().map(|active| &active.resolved)).ok()?;
505    let kernel =
506        crate::core::engine_interface::context_plan::sources::kernel_policy_identity(project)?;
507    let mut hash = blake3::Hasher::new();
508    hash.update(b"leanctx-presented-policy-v1\0");
509    hash.update(gateway.digest().as_bytes());
510    hash.update(b"\0");
511    hash.update(&pack);
512    hash.update(b"\0");
513    hash.update(kernel.as_bytes());
514    Some(hash.finalize().to_hex().to_string())
515}
516
517fn canonical_project(project: &Path) -> Option<String> {
518    crate::core::pathutil::canonicalize_secure(project)
519        .ok()?
520        .to_str()
521        .map(str::to_owned)
522}
523
524impl PresentedBinding {
525    /// The binding of a source the plan has admitted in this very step, with
526    /// the caller, role and policy identity that hold now.
527    pub fn admitted_now(
528        project: &Path,
529        object_ref: &str,
530        source_id: &str,
531        content_digest: &str,
532    ) -> Option<Self> {
533        Self::admitted_by(
534            &verified_principal(),
535            project,
536            object_ref,
537            source_id,
538            content_digest,
539        )
540    }
541
542    /// [`Self::admitted_now`] for an explicit, already verified principal.
543    fn admitted_by(
544        principal: &str,
545        project: &Path,
546        object_ref: &str,
547        source_id: &str,
548        content_digest: &str,
549    ) -> Option<Self> {
550        Some(Self {
551            project: canonical_project(project)?,
552            caller: principal.to_owned(),
553            role: crate::core::roles::active_role_name(),
554            object_ref: object_ref.to_owned(),
555            source_id: source_id.to_owned(),
556            content_digest: content_digest.to_owned(),
557            policy_identity: policy_identity(project)?,
558        })
559    }
560
561    pub(super) fn project(&self) -> &str {
562        &self.project
563    }
564
565    /// The caller's id of the source. Content-free; it authorizes nothing.
566    pub(super) fn source_id(&self) -> &str {
567        &self.source_id
568    }
569
570    /// Same project, caller, role, object reference, source id and content
571    /// digest. The policy identity is not part of what a binding IS, and
572    /// neither is a presenter: two presenters of one source share the
573    /// binding and differ in the register only.
574    pub(super) fn same_source(&self, other: &Self) -> bool {
575        self.key() == other.key()
576    }
577
578    /// A stable, content-free identity of this binding INCLUDING the policy
579    /// it was admitted under: what a stored copy bound to it is addressed by.
580    pub(super) fn identity(&self) -> String {
581        format!("{}:{}", self.key(), self.policy_identity)
582    }
583
584    /// Feed `hash` the six fields that say WHICH source this is,
585    /// length-prefixed.
586    fn absorb_source(&self, hash: &mut blake3::Hasher) {
587        for field in [
588            &self.project,
589            &self.caller,
590            &self.role,
591            &self.object_ref,
592            &self.source_id,
593            &self.content_digest,
594        ] {
595            hash.update(&(field.len() as u64).to_be_bytes());
596            hash.update(field.as_bytes());
597        }
598    }
599
600    /// Register key: a hash over every identifying field, length-prefixed.
601    fn key(&self) -> String {
602        let mut hash = blake3::Hasher::new();
603        hash.update(b"leanctx-presented-binding-v2\0");
604        self.absorb_source(&mut hash);
605        hash.finalize().to_hex().to_string()
606    }
607
608    /// Whether project, caller and role of this binding are the ones of the
609    /// request in scope.
610    pub(super) fn matches_request(&self) -> bool {
611        let project = crate::core::policy::runtime::REQUEST_PROJECT
612            .try_with(|slot| slot.borrow().clone())
613            .ok()
614            .flatten()
615            .and_then(|path| canonical_project(&path));
616        project.as_deref() == Some(self.project.as_str())
617            && self.caller == verified_principal()
618            && self.role == crate::core::roles::active_role_name()
619    }
620
621    /// The entry of the presenter of the request in scope, if that presenter
622    /// may reuse this source at `now`: its capability matches the stored
623    /// verifier (compared in constant time, `blake3::Hash`), the entry has
624    /// not expired, and the presenter is neither expired nor revoked.
625    fn own_entry(&self, now: i64) -> Option<PresenterEntry> {
626        with_current(|capability| {
627            let id = capability.id();
628            let mut set = REGISTER.read::<SourcePresenters>(&self.key(), now)?;
629            let entry = set.presenters.remove(&id)?;
630            let stored = blake3::Hash::from_hex(entry.verifier.as_bytes()).ok()?;
631            (stored == capability.source_verifier(self, &id)
632                && entry.expires_at >= now
633                && lifetime_end(&id, now).is_some())
634            .then_some(entry)
635        })
636        .flatten()
637    }
638
639    /// The state of this binding under the policy that is current NOW, for
640    /// the presenter of the request in scope.
641    ///
642    /// Asked first: does the request hold a capability this source was
643    /// presented under? Without one nothing is presented, whatever the
644    /// register holds — the request learns neither the policy state nor the
645    /// age of another presenter's entry.
646    ///
647    /// Then, as before: a text delivered under one policy is not what another
648    /// policy would deliver from the same source (other redaction, other
649    /// ceiling). So a binding admitted under a policy identity that is no
650    /// longer the current one never holds, whatever is presented since: the
651    /// text has to be delivered — and recorded — again under the current
652    /// policy.
653    pub(super) fn presentation(&self) -> Presentation {
654        let now = now();
655        if self.own_entry(now).is_none() {
656            return Presentation::None;
657        }
658        let Some(current) = policy_identity(Path::new(&self.project)) else {
659            return Presentation::OtherPolicy;
660        };
661        if self.policy_identity != current {
662            return Presentation::OtherPolicy;
663        }
664        self.presentation_at(&current, now)
665    }
666
667    fn presentation_at(&self, current_identity: &str, now: i64) -> Presentation {
668        let Some(entry) = self.own_entry(now) else {
669            return Presentation::None;
670        };
671        if entry
672            .admitted
673            .get(current_identity)
674            .is_some_and(|at| in_time(*at, now))
675        {
676            Presentation::Current
677        } else if entry.admitted.values().any(|at| in_time(*at, now)) {
678            Presentation::OtherPolicy
679        } else {
680            Presentation::None
681        }
682    }
683}
684
685/// Record that `binding` was presented and admitted now under its policy
686/// identity, by the presenter of the request in scope. Call only from the
687/// plan admission, after it admitted the source. A restricted source is
688/// never passed here (E3). `false`: not recorded — no usable presenter, the
689/// source already has its full set of live issued presenters, a bound, the
690/// lock, the file system.
691pub fn record_presentation(binding: &PresentedBinding) -> bool {
692    record_presentation_at(binding, now())
693}
694
695/// "Now" of the validity check and of a recorded presentation: the
696/// register's clock. Tests can move it ahead with [`TestClock`].
697#[cfg(not(any(test, feature = "test-support")))]
698fn now() -> i64 {
699    register::now()
700}
701
702#[cfg(any(test, feature = "test-support"))]
703fn now() -> i64 {
704    register::now().saturating_add(TEST_CLOCK_AHEAD.load(std::sync::atomic::Ordering::SeqCst))
705}
706
707/// Seconds the presentation clock runs ahead of the system clock in tests.
708#[cfg(any(test, feature = "test-support"))]
709static TEST_CLOCK_AHEAD: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
710
711/// Moves the presentation clock ahead until the guard drops. The offset is
712/// process-wide: hold `core::data_dir::isolated_data_dir()` (which serializes
713/// every test that has a register) for as long as a guard lives.
714#[cfg(any(test, feature = "test-support"))]
715pub struct TestClock;
716
717#[cfg(any(test, feature = "test-support"))]
718impl TestClock {
719    /// One second past the validity of every presentation recorded so far.
720    #[must_use]
721    pub fn past_validity() -> Self {
722        TEST_CLOCK_AHEAD.fetch_add(VALIDITY_SECONDS + 1, std::sync::atomic::Ordering::SeqCst);
723        Self
724    }
725
726    /// One second past the longest lifetime of every capability issued so
727    /// far.
728    #[must_use]
729    pub fn past_capability_lifetime() -> Self {
730        TEST_CLOCK_AHEAD.fetch_add(
731            MAX_CAPABILITY_TTL_SECONDS + 1,
732            std::sync::atomic::Ordering::SeqCst,
733        );
734        Self
735    }
736}
737
738#[cfg(any(test, feature = "test-support"))]
739impl Drop for TestClock {
740    fn drop(&mut self) {
741        TEST_CLOCK_AHEAD.store(0, std::sync::atomic::Ordering::SeqCst);
742    }
743}
744
745fn record_presentation_at(binding: &PresentedBinding, now: i64) -> bool {
746    let Some((id, verifier, expires_at, process_only)) = with_current(|capability| {
747        let id = capability.id();
748        let expires_at = lifetime_end(&id, now)?;
749        let verifier = capability
750            .source_verifier(binding, &id)
751            .to_hex()
752            .to_string();
753        let process_only = is_process_session(&id);
754        Some((id, verifier, expires_at, process_only))
755    })
756    .flatten() else {
757        return false;
758    };
759    REGISTER.update::<SourcePresenters>(&binding.key(), now, |known| {
760        let mut set = known.unwrap_or_default();
761        // Only entries that authorize nothing any more go.
762        set.presenters.retain(|_, entry| entry.live(now));
763        let mut entry = set
764            .presenters
765            .remove(&id)
766            .filter(|known| known.verifier == verifier)
767            .unwrap_or_else(|| PresenterEntry {
768                verifier: verifier.clone(),
769                admitted: BTreeMap::new(),
770                expires_at,
771                process_only,
772            });
773        entry.expires_at = expires_at;
774        entry.admitted.retain(|_, at| in_time(*at, now));
775        entry.admitted.insert(binding.policy_identity.clone(), now);
776        while entry.admitted.len() > MAX_IDENTITIES_PER_BINDING {
777            let oldest = entry
778                .admitted
779                .iter()
780                .min_by_key(|(identity, presented_at)| (**presented_at, (*identity).clone()))
781                .map(|(identity, _)| identity.clone())?;
782            entry.admitted.remove(&oldest);
783        }
784        let of_kind = |set: &SourcePresenters| {
785            set.presenters
786                .values()
787                .filter(|known| known.process_only == process_only)
788                .count()
789        };
790        if process_only {
791            // A process entry serves its own process only: the oldest of
792            // them makes room, an issued presenter's entry never does.
793            while of_kind(&set) >= MAX_PROCESS_PRESENTERS_PER_SOURCE {
794                let oldest = set
795                    .presenters
796                    .iter()
797                    .filter(|(_, known)| known.process_only)
798                    .min_by_key(|(known_id, known)| (known.last_presented(), (*known_id).clone()))
799                    .map(|(known_id, _)| known_id.clone())?;
800                set.presenters.remove(&oldest);
801            }
802        } else if of_kind(&set) >= MAX_PRESENTERS_PER_SOURCE {
803            // Full of live issued presenters: this one is not recorded, and
804            // none of them is displaced.
805            return None;
806        }
807        set.presenters.insert(id.clone(), entry);
808        Some(set)
809    })
810}
811
812#[cfg(test)]
813mod tests {
814    use super::*;
815
816    fn binding(project: &Path, object_ref: &str, digest: &str, identity: &str) -> PresentedBinding {
817        PresentedBinding {
818            project: canonical_project(project).expect("project"),
819            caller: verified_principal(),
820            role: crate::core::roles::active_role_name(),
821            object_ref: object_ref.to_owned(),
822            source_id: "source-one".to_owned(),
823            content_digest: digest.to_owned(),
824            policy_identity: identity.to_owned(),
825        }
826    }
827
828    /// Two verified principals are two bindings: a presentation by one does
829    /// not satisfy the other's, and a binding of another principal is not the
830    /// request's own. Before, every binding carried the constant
831    /// `local-operator`, so the caller separated nothing.
832    #[test]
833    fn a_presentation_by_one_principal_does_not_satisfy_another() {
834        let _data = crate::core::data_dir::isolated_data_dir();
835        let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
836        let directory = tempfile::tempdir().expect("project");
837        let root = directory.path().canonicalize().expect("canonical root");
838        let by = |principal: &str| {
839            PresentedBinding::admitted_by(principal, &root, "ref-quartz", "source-one", "sha256:aa")
840                .expect("policy identity available")
841        };
842        let mine = PresentedBinding::admitted_now(&root, "ref-quartz", "source-one", "sha256:aa")
843            .expect("policy identity available");
844        assert_eq!(mine, by(&verified_principal()));
845        assert_ne!(verified_principal(), "another-verified-principal");
846        let theirs = by("another-verified-principal");
847        assert!(!mine.same_source(&theirs));
848
849        assert!(record_presentation(&theirs));
850        assert_eq!(theirs.presentation(), Presentation::Current);
851        assert_eq!(
852            mine.presentation(),
853            Presentation::None,
854            "their presentation is not mine"
855        );
856        crate::core::policy::runtime::REQUEST_PROJECT.sync_scope(
857            std::cell::RefCell::new(Some(root.clone())),
858            || {
859                assert!(mine.matches_request());
860                assert!(
861                    !theirs.matches_request(),
862                    "a binding of another principal is refused for this request"
863                );
864            },
865        );
866
867        // On disk the principal is part of the keyed hash only, and a
868        // capability is in no form: not the secret, not its wire text.
869        let issued = issue(60).expect("issued");
870        with_presenter(Some(issued.capability.clone()), || {
871            assert!(record_presentation(&theirs));
872        });
873        let raw = std::fs::read_to_string(REGISTER.shard_path(&theirs.key())).expect("shard");
874        assert!(!raw.contains("another-verified-principal"), "{raw}");
875        let wire = issued.capability.expose();
876        let records = std::fs::read_to_string(PRESENTERS.shard_path(&issued.capability.id()))
877            .expect("presenter record");
878        for stored in [&raw, &records] {
879            assert!(!stored.contains(&wire[CAPABILITY_PREFIX.len()..]));
880        }
881        assert_eq!(
882            format!("{:?}", issued.capability),
883            "PresenterCapability(<redacted>)"
884        );
885        assert!(PresenterCapability::parse(&wire).is_some());
886        let upper = wire.to_uppercase();
887        for malformed in ["", "lcp1_", &wire[..wire.len() - 1], upper.as_str()] {
888            assert!(PresenterCapability::parse(malformed).is_none());
889        }
890    }
891
892    /// The register by itself, as a second process reads it: validity,
893    /// policy identity, exact binding, bounds, and no identifying text.
894    #[test]
895    fn a_presentation_holds_for_its_exact_binding_policy_and_validity_only() {
896        let _data = crate::core::data_dir::isolated_data_dir();
897        let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
898        let directory = tempfile::tempdir().expect("project");
899        let root = directory.path().canonicalize().expect("canonical root");
900        let start = register::now();
901        let one = binding(&root, "ref-quartz", "sha256:aa", "policy-one");
902
903        assert_eq!(one.presentation_at("policy-one", start), Presentation::None);
904        assert!(record_presentation_at(&one, start));
905        assert_eq!(
906            one.presentation_at("policy-one", start),
907            Presentation::Current
908        );
909        assert_eq!(
910            one.presentation_at("policy-one", start + VALIDITY_SECONDS),
911            Presentation::Current
912        );
913        assert_eq!(
914            one.presentation_at("policy-one", start + VALIDITY_SECONDS + 1),
915            Presentation::None,
916            "not presented again within the validity"
917        );
918        assert_eq!(
919            one.presentation_at("policy-two", start),
920            Presentation::OtherPolicy,
921            "presented, but not admitted under the current policy"
922        );
923
924        // Not the same binding: other content, other reference, other
925        // caller, other role, other project.
926        let other_content = binding(&root, "ref-quartz", "sha256:bb", "policy-one");
927        let other_ref = binding(&root, "ref-basalt", "sha256:aa", "policy-one");
928        let mut other_caller = one.clone();
929        other_caller.caller = "another-principal".to_owned();
930        let mut other_role = one.clone();
931        other_role.role = "another-role".to_owned();
932        let elsewhere = tempfile::tempdir().expect("project");
933        let other_project = binding(elsewhere.path(), "ref-quartz", "sha256:aa", "policy-one");
934        for different in [
935            &other_content,
936            &other_ref,
937            &other_caller,
938            &other_role,
939            &other_project,
940        ] {
941            assert!(!one.same_source(different));
942            assert_eq!(
943                different.presentation_at("policy-one", start),
944                Presentation::None
945            );
946        }
947
948        // Presented again under the new policy: current again.
949        let again = binding(&root, "ref-quartz", "sha256:aa", "policy-two");
950        assert!(one.same_source(&again));
951        assert!(record_presentation_at(&again, start + 5));
952        assert_eq!(
953            one.presentation_at("policy-two", start + 5),
954            Presentation::Current
955        );
956
957        // Persisted, owner-only, and free of identifying text.
958        let shard = REGISTER.shard_path(&one.key());
959        let raw = std::fs::read_to_string(&shard).expect("persisted shard");
960        let principal = verified_principal();
961        for identifying in ["ref-quartz", "source-one", "sha256:aa", principal.as_str()] {
962            assert!(!raw.contains(identifying), "{identifying}");
963        }
964        #[cfg(unix)]
965        {
966            use std::os::unix::fs::PermissionsExt;
967            let mode = std::fs::metadata(&shard)
968                .expect("shard")
969                .permissions()
970                .mode();
971            assert_eq!(mode & 0o777, 0o600, "owner-only");
972        }
973
974        // Bound: policy identities per presenter entry.
975        for index in 0..MAX_IDENTITIES_PER_BINDING + 2 {
976            let identity = format!("policy-bound-{index}");
977            let presented = binding(&root, "ref-quartz", "sha256:aa", &identity);
978            assert!(record_presentation_at(&presented, start + 10));
979        }
980        let kept = REGISTER
981            .read::<SourcePresenters>(&one.key(), start + 10)
982            .expect("entry");
983        assert_eq!(kept.presenters.len(), 1, "one presenter: this process");
984        let own = kept.presenters.values().next().expect("own entry");
985        assert_eq!(own.admitted.len(), MAX_IDENTITIES_PER_BINDING);
986
987        // Bound: issued presenters per source. The set fills; a further one
988        // is not recorded and displaces nobody.
989        let issued: Vec<IssuedCapability> = (0..=MAX_PRESENTERS_PER_SOURCE)
990            .map(|_| issue(60).expect("issued"))
991            .collect();
992        let recorded: Vec<bool> = issued
993            .iter()
994            .map(|presenter| {
995                with_presenter(Some(presenter.capability.clone()), || {
996                    record_presentation_at(&one, start + 10)
997                })
998            })
999            .collect();
1000        assert_eq!(
1001            recorded.iter().filter(|recorded| **recorded).count(),
1002            MAX_PRESENTERS_PER_SOURCE
1003        );
1004        assert_eq!(recorded.last(), Some(&false), "the one too many");
1005        with_presenter(Some(issued[0].capability.clone()), || {
1006            assert_eq!(
1007                one.presentation_at("policy-one", start + 10),
1008                Presentation::Current,
1009                "the first presenter kept its entry"
1010            );
1011        });
1012    }
1013}