1use std::time::Duration;
2use std::{collections::BTreeMap, fmt, rc::Rc};
3
4use super::{RequestId, lifecycle::CancellationToken};
5
6#[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 pub fn new(key: impl Into<String>, value: Vec<u8>) -> Self {
26 Self {
27 key: key.into(),
28 value,
29 }
30 }
31
32 pub fn key(&self) -> &str {
34 &self.key
35 }
36
37 pub fn value(&self) -> &[u8] {
39 &self.value
40 }
41}
42
43#[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 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 pub fn key(&self) -> &str {
76 &self.key
77 }
78
79 pub fn issuer(&self) -> &str {
81 &self.issuer
82 }
83
84 pub fn audience(&self) -> &[String] {
86 &self.audience
87 }
88
89 pub fn value(&self) -> &[u8] {
91 &self.value
92 }
93
94 pub fn proof(&self) -> &str {
96 &self.proof
97 }
98
99 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#[derive(Clone, Debug, Eq, PartialEq)]
121pub enum InvocationContextError {
122 EmptyExtensionKey,
124 ExtensionAlreadySet { key: String },
126 SealedExtensionAlreadySet { key: String },
128 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#[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 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 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 #[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 pub fn caller_instance(&self) -> Option<&str> {
232 self.caller_instance.as_deref()
233 }
234
235 pub const fn request_id(&self) -> RequestId {
237 self.request_id
238 }
239
240 pub const fn deadline(&self) -> Option<Duration> {
242 self.deadline
243 }
244
245 pub const fn remaining_budget(&self) -> Option<Duration> {
251 self.remaining_budget
252 }
253
254 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 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 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 pub fn extension(&self, key: &str) -> Option<&[u8]> {
321 self.extensions.get(key).map(InvocationExtension::value)
322 }
323
324 pub fn extensions(&self) -> impl Iterator<Item = &InvocationExtension> {
326 self.extensions.values()
327 }
328
329 pub fn sealed_extension(&self, key: &str) -> Option<&SealedInvocationExtension> {
331 self.sealed_extensions.get(key)
332 }
333
334 pub fn sealed_extensions(&self) -> impl Iterator<Item = &SealedInvocationExtension> {
336 self.sealed_extensions.values()
337 }
338
339 #[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 pub fn is_cancelled(&self) -> bool {
352 self.cancellation.is_cancelled()
353 }
354
355 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}