Skip to main content

lean_ctx/core/engine_interface/context_plan/
presenter.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Opt-in v2 materialization: the unchanged v1 exchange, presented under a
4//! presenter capability (`core::archive::presented`, "Presenters").
5//!
6//! The capability proves that a later request comes from a holder of the
7//! capability these sources were presented under. It names no agent, attests
8//! no identity and authorizes nothing: every later use re-checks project,
9//! role, policy identity, freshness and the admission of each source.
10//!
11//! This transport writes no decision receipt, so the presenter appears in no
12//! receipt here; the response carries its content-free reference, and the
13//! lineage of the delivered text records the same reference.
14
15use std::path::Path;
16
17use lean_ctx_protocol::{
18    EngineContextSourceMaterializationRequestV2, EngineContextSourceMaterializationResponseV2,
19    EnginePresenterGrantV1, EnginePresenterRequestV1, ProtocolReference,
20};
21
22use crate::core::archive::presented::{self, PresenterCapability};
23
24/// The one refusal for everything about a capability on this transport:
25/// shown where none belongs, missing where one is needed, unreadable,
26/// unknown, expired, revoked, or not issuable.
27pub const CAPABILITY_REFUSED: &str = "presenter_capability_refused";
28
29/// Materialize `request` as its v1 payload and record the presentations
30/// under a presenter capability.
31///
32/// - `issue`: a capability is minted, the materialization runs under it, and
33///   only a successful response carries it. A materialization that fails
34///   leaves a revoked presenter and returns no capability.
35/// - `renew`: `shown` is the capability from the host's capability file; it
36///   must be an issued one that is neither expired nor revoked. It is never
37///   returned again.
38pub fn materialize_v2(
39    root: &Path,
40    request: &EngineContextSourceMaterializationRequestV2,
41    shown: Option<PresenterCapability>,
42) -> Result<EngineContextSourceMaterializationResponseV2, &'static str> {
43    request
44        .validate_payload()
45        .map_err(|_| "invalid_source_materialization_request")?;
46    let (capability, expires_at, fresh) = match (request.presenter, shown) {
47        (EnginePresenterRequestV1::Issue { ttl_seconds }, None) => {
48            let issued = presented::issue(ttl_seconds).ok_or(CAPABILITY_REFUSED)?;
49            (issued.capability, issued.expires_at, true)
50        }
51        (EnginePresenterRequestV1::Renew, Some(capability)) => {
52            let expires_at =
53                presented::issued_lifetime_end(&capability).ok_or(CAPABILITY_REFUSED)?;
54            (capability, expires_at, false)
55        }
56        _ => return Err(CAPABILITY_REFUSED),
57    };
58    let response = presented::with_presenter(Some(capability.clone()), || {
59        super::materialize(root, &request.materialization)
60    })
61    .and_then(|materialization| {
62        let response = EngineContextSourceMaterializationResponseV2 {
63            schema_version: 2,
64            materialization,
65            presenter: EnginePresenterGrantV1 {
66                presenter_ref: ProtocolReference::new(capability.reference())
67                    .map_err(|_| "internal")?,
68                expires_at_unix: u64::try_from(expires_at).map_err(|_| "internal")?,
69                capability: fresh.then(|| String::clone(&capability.expose())),
70            },
71        };
72        response
73            .validate()
74            .map_err(|_| "source_materialization_invalid")?;
75        Ok(response)
76    });
77    if response.is_err() && fresh {
78        // Never returned to anyone: it must not stay usable either.
79        presented::revoke(&capability);
80    }
81    response
82}