Skip to main content

lenso_kernel/
invocation.rs

1use std::time::Duration;
2use std::{collections::BTreeMap, fmt, rc::Rc};
3
4use super::{RequestId, lifecycle::CancellationToken};
5
6/// An opaque extension supplied by a caller Plugin.
7#[derive(Clone, Eq, PartialEq)]
8pub struct InvocationExtension {
9    key: String,
10    value: Vec<u8>,
11}
12
13impl fmt::Debug for InvocationExtension {
14    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
15        formatter
16            .debug_struct("InvocationExtension")
17            .field("key", &self.key)
18            .field("value", &"<redacted>")
19            .finish()
20    }
21}
22
23impl InvocationExtension {
24    /// Creates one ordinary, caller-supplied extension value.
25    pub fn new(key: impl Into<String>, value: Vec<u8>) -> Self {
26        Self {
27            key: key.into(),
28            value,
29        }
30    }
31
32    /// Returns the stable extension key.
33    pub fn key(&self) -> &str {
34        &self.key
35    }
36
37    /// Returns the opaque extension bytes.
38    pub fn value(&self) -> &[u8] {
39        &self.value
40    }
41}
42
43/// An opaque extension whose issuer and audience must survive Adapter hops.
44#[derive(Clone, Eq, PartialEq)]
45pub struct SealedInvocationExtension {
46    key: String,
47    issuer: String,
48    audience: Vec<String>,
49    value: Vec<u8>,
50    proof: String,
51}
52
53impl SealedInvocationExtension {
54    /// Carries one domain-signed extension without granting it validity.
55    ///
56    /// Domain provider bindings must validate `proof` before projecting the
57    /// payload. The Kernel preserves the signed fields and prevents replacement.
58    pub fn signed(
59        key: impl Into<String>,
60        issuer: impl Into<String>,
61        audience: impl IntoIterator<Item = impl Into<String>>,
62        value: Vec<u8>,
63        proof: impl Into<String>,
64    ) -> Self {
65        Self {
66            key: key.into(),
67            issuer: issuer.into(),
68            audience: audience.into_iter().map(Into::into).collect(),
69            value,
70            proof: proof.into(),
71        }
72    }
73
74    /// Returns the stable extension key.
75    pub fn key(&self) -> &str {
76        &self.key
77    }
78
79    /// Returns the issuer provenance without interpreting its domain.
80    pub fn issuer(&self) -> &str {
81        &self.issuer
82    }
83
84    /// Returns the intended Capability/Operation audience.
85    pub fn audience(&self) -> &[String] {
86        &self.audience
87    }
88
89    /// Returns the opaque extension bytes.
90    pub fn value(&self) -> &[u8] {
91        &self.value
92    }
93
94    /// Returns the domain proof covering issuer, audience, and payload.
95    pub fn proof(&self) -> &str {
96        &self.proof
97    }
98
99    /// Returns whether the signed audience covers one exact target Operation.
100    pub fn covers(&self, capability_id: &str, operation: &str) -> bool {
101        let target = format!("{capability_id}:{operation}");
102        self.audience.iter().any(|audience| audience == &target)
103    }
104}
105
106impl fmt::Debug for SealedInvocationExtension {
107    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108        formatter
109            .debug_struct("SealedInvocationExtension")
110            .field("key", &self.key)
111            .field("issuer", &self.issuer)
112            .field("audience", &self.audience)
113            .field("value", &"<redacted>")
114            .field("proof", &"<redacted>")
115            .finish()
116    }
117}
118
119/// Failure returned when an Invocation Context extension cannot be attached.
120#[derive(Clone, Debug, Eq, PartialEq)]
121pub enum InvocationContextError {
122    /// An extension key cannot be empty.
123    EmptyExtensionKey,
124    /// An ordinary extension already occupies the requested key.
125    ExtensionAlreadySet { key: String },
126    /// A sealed extension cannot be replaced by another extension value.
127    SealedExtensionAlreadySet { key: String },
128    /// Sealed provenance must name an issuer and at least one audience entry.
129    InvalidSealedExtension { key: String },
130}
131
132impl std::fmt::Display for InvocationContextError {
133    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        match self {
135            Self::EmptyExtensionKey => {
136                formatter.write_str("Invocation Context extension key is empty")
137            }
138            Self::ExtensionAlreadySet { key } => {
139                write!(
140                    formatter,
141                    "Invocation Context extension `{key}` is already set"
142                )
143            }
144            Self::SealedExtensionAlreadySet { key } => {
145                write!(
146                    formatter,
147                    "sealed Invocation Context extension `{key}` is already set"
148                )
149            }
150            Self::InvalidSealedExtension { key } => {
151                write!(
152                    formatter,
153                    "sealed Invocation Context extension `{key}` has invalid provenance"
154                )
155            }
156        }
157    }
158}
159
160/// Kernel-owned context propagated across one native request invocation.
161#[derive(Clone, Debug)]
162pub struct InvocationContext {
163    pub(crate) execution: Option<super::settlement::ExecutionScope>,
164    pub(super) caller_instance: Option<Rc<str>>,
165    pub(super) request_id: RequestId,
166    pub(super) deadline: Option<Duration>,
167    pub(super) remaining_budget: Option<Duration>,
168    pub(super) shutdown_dependency_call: bool,
169    pub(super) cancellation: CancellationToken,
170    pub(super) extensions: BTreeMap<String, InvocationExtension>,
171    pub(super) sealed_extensions: BTreeMap<String, SealedInvocationExtension>,
172}
173
174impl InvocationContext {
175    /// Retains execution capacity for Adapter-managed work beyond its reply.
176    /// The returned lease must be settled on observed termination, not cancellation acknowledgement.
177    pub fn retain_execution(&self) -> Result<super::ExecutionLease, super::RuntimeFailure> {
178        self.execution
179            .as_ref()
180            .ok_or(super::RuntimeFailure::AdmissionClosed)?
181            .retain()
182    }
183    /// Creates an invocation context with an absolute Driver-monotonic deadline.
184    pub fn new(
185        request_id: RequestId,
186        deadline: Option<Duration>,
187        cancellation: CancellationToken,
188    ) -> Self {
189        Self {
190            execution: None,
191            caller_instance: None,
192            request_id,
193            deadline,
194            remaining_budget: None,
195            shutdown_dependency_call: false,
196            cancellation,
197            extensions: BTreeMap::new(),
198            sealed_extensions: BTreeMap::new(),
199        }
200    }
201
202    /// Attaches the resolved Caller Plugin Instance to this context.
203    #[must_use]
204    pub fn with_caller_instance(mut self, caller_instance: impl Into<String>) -> Self {
205        self.caller_instance = Some(Rc::from(caller_instance.into()));
206        self
207    }
208
209    pub(crate) fn with_shared_caller_instance(mut self, caller_instance: Rc<str>) -> Self {
210        self.caller_instance = Some(caller_instance);
211        self
212    }
213
214    pub(super) fn for_caller(mut self, caller_instance: &str) -> Self {
215        if self.caller_instance.as_deref() != Some(caller_instance) {
216            self.caller_instance = Some(Rc::from(caller_instance));
217        }
218        self
219    }
220
221    pub(super) fn for_shutdown_dependency_call(mut self) -> Self {
222        self.shutdown_dependency_call = true;
223        self
224    }
225
226    pub(super) const fn is_shutdown_dependency_call(&self) -> bool {
227        self.shutdown_dependency_call
228    }
229
230    /// Returns the Caller Plugin Instance, when the App attached one.
231    pub fn caller_instance(&self) -> Option<&str> {
232        self.caller_instance.as_deref()
233    }
234
235    /// Returns the Kernel Request ID used for correlation and cancellation.
236    pub const fn request_id(&self) -> RequestId {
237        self.request_id
238    }
239
240    /// Returns the absolute Driver-monotonic deadline, when one was supplied.
241    pub const fn deadline(&self) -> Option<Duration> {
242        self.deadline
243    }
244
245    /// Returns the deadline budget captured immediately before provider dispatch.
246    ///
247    /// A missing value means the invocation has no deadline. Adapter code can
248    /// forward this relative duration across an execution boundary without
249    /// learning or reproducing the Driver's monotonic clock.
250    pub const fn remaining_budget(&self) -> Option<Duration> {
251        self.remaining_budget
252    }
253
254    /// Returns the caller-owned cooperative cancellation signal.
255    pub fn cancellation(&self) -> CancellationToken {
256        self.cancellation.clone()
257    }
258
259    pub(crate) fn for_child_request(mut self, request_id: RequestId) -> Self {
260        self.request_id = request_id;
261        self.cancellation = self.cancellation.child();
262        self
263    }
264
265    /// Adds one ordinary opaque extension without replacing an existing value.
266    pub fn with_extension(
267        mut self,
268        key: impl Into<String>,
269        value: Vec<u8>,
270    ) -> Result<Self, InvocationContextError> {
271        let extension = InvocationExtension::new(key, value);
272        if extension.key().is_empty() {
273            return Err(InvocationContextError::EmptyExtensionKey);
274        }
275        if self.sealed_extensions.contains_key(extension.key()) {
276            return Err(InvocationContextError::SealedExtensionAlreadySet {
277                key: extension.key().to_owned(),
278            });
279        }
280        if self.extensions.contains_key(extension.key()) {
281            return Err(InvocationContextError::ExtensionAlreadySet {
282                key: extension.key().to_owned(),
283            });
284        }
285        self.extensions
286            .insert(extension.key().to_owned(), extension);
287        Ok(self)
288    }
289
290    /// Adds one sealed extension while preserving issuer, audience, and key ownership.
291    pub fn with_sealed_extension(
292        mut self,
293        extension: SealedInvocationExtension,
294    ) -> Result<Self, InvocationContextError> {
295        if extension.key().is_empty() {
296            return Err(InvocationContextError::EmptyExtensionKey);
297        }
298        if extension.issuer().is_empty()
299            || extension.audience().is_empty()
300            || extension.proof().is_empty()
301            || extension.audience().iter().any(String::is_empty)
302        {
303            return Err(InvocationContextError::InvalidSealedExtension {
304                key: extension.key().to_owned(),
305            });
306        }
307        if self.sealed_extensions.contains_key(extension.key())
308            || self.extensions.contains_key(extension.key())
309        {
310            return Err(InvocationContextError::SealedExtensionAlreadySet {
311                key: extension.key().to_owned(),
312            });
313        }
314        self.sealed_extensions
315            .insert(extension.key().to_owned(), extension);
316        Ok(self)
317    }
318
319    /// Returns one ordinary extension's opaque bytes.
320    pub fn extension(&self, key: &str) -> Option<&[u8]> {
321        self.extensions.get(key).map(InvocationExtension::value)
322    }
323
324    /// Returns ordinary extensions in deterministic key order.
325    pub fn extensions(&self) -> impl Iterator<Item = &InvocationExtension> {
326        self.extensions.values()
327    }
328
329    /// Returns one sealed extension by key.
330    pub fn sealed_extension(&self, key: &str) -> Option<&SealedInvocationExtension> {
331        self.sealed_extensions.get(key)
332    }
333
334    /// Returns sealed extensions in deterministic key order.
335    pub fn sealed_extensions(&self) -> impl Iterator<Item = &SealedInvocationExtension> {
336        self.sealed_extensions.values()
337    }
338
339    /// Restricts sealed extensions to one exact Capability/Operation target.
340    ///
341    /// Ordinary baggage is preserved. A sealed extension whose audience does
342    /// not cover the target is not disclosed to that provider.
343    #[must_use]
344    pub fn for_target(mut self, capability_id: &str, operation: &str) -> Self {
345        self.sealed_extensions
346            .retain(|_, extension| extension.covers(capability_id, operation));
347        self
348    }
349
350    /// Returns whether the caller has already cancelled this invocation.
351    pub fn is_cancelled(&self) -> bool {
352        self.cancellation.is_cancelled()
353    }
354
355    /// Returns whether the context deadline has passed at a Driver instant.
356    pub fn is_expired(&self, now: Duration) -> bool {
357        self.deadline.is_some_and(|deadline| deadline <= now)
358    }
359}
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn resolved_caller_reuses_matching_storage_and_overrides_spoofed_identity() {
367        let context = InvocationContext::new(1, None, CancellationToken::new())
368            .with_caller_instance("consumer".to_owned());
369        let original = context.caller_instance().unwrap().as_ptr();
370        let context = context.for_caller("consumer");
371        assert_eq!(context.caller_instance().unwrap().as_ptr(), original);
372
373        let context = context.for_caller("resolved-consumer");
374        assert_eq!(context.caller_instance(), Some("resolved-consumer"));
375    }
376
377    #[test]
378    fn cloning_context_reuses_caller_storage() {
379        let context = InvocationContext::new(1, None, CancellationToken::new())
380            .with_caller_instance("consumer".to_owned());
381        let cloned = context.clone();
382
383        assert_eq!(
384            context.caller_instance().unwrap().as_ptr(),
385            cloned.caller_instance().unwrap().as_ptr()
386        );
387    }
388
389    #[test]
390    fn child_request_has_fresh_identity_and_one_way_cancellation() {
391        let parent =
392            InvocationContext::new(7, Some(Duration::from_secs(2)), CancellationToken::new())
393                .with_extension("trace", b"kept".to_vec())
394                .unwrap();
395        let child = parent.clone().for_child_request(8);
396        assert_eq!(child.request_id(), 8);
397        assert_eq!(child.deadline(), parent.deadline());
398        assert_eq!(child.extension("trace"), Some(b"kept".as_slice()));
399
400        child.cancellation().cancel();
401        assert!(child.is_cancelled());
402        assert!(!parent.is_cancelled());
403
404        let second_child = parent.clone().for_child_request(9);
405        parent.cancellation().cancel();
406        assert!(second_child.is_cancelled());
407    }
408}