1pub mod metrics;
13mod trace;
14pub use trace::{NoopTraceBridge, TraceBridge, TraceContext};
15mod execution;
16pub use execution::{
17 ExecutionContext, ExecutionFailure, ExecutionReport, ExecutionRequest, ExecutionResult, Phase,
18 RuntimeExtension, RuntimeInvocation, RuntimeReply, RuntimeRequest,
19};
20mod invocation;
21mod json;
22pub use json::decode_json;
23mod wire;
24pub use invocation::InvocationIdentity;
25pub use wire::{
26 APPLICATION_INPUT_MAX_BYTES, DEFAULT_RUNTIME_FRAME_MAX_BYTES, MAX_RUNTIME_VALUE_DEPTH,
27 MAX_WIRE_VALUE_DEPTH, RUNTIME_EXTENSION_MAX_BYTES, RUNTIME_REQUEST_MAX_BYTES,
28 validate_runtime_payload, validate_wire_value,
29};
30
31use iri_string::types::{UriAbsoluteStr, UriReferenceStr};
32use serde::{Deserialize, Serialize};
33use serde_json::{Map, Value};
34use std::{
35 fmt,
36 future::Future,
37 path::{Path, PathBuf},
38 pin::Pin,
39 sync::{
40 Arc,
41 atomic::{AtomicBool, Ordering},
42 },
43 time::{Duration, Instant},
44};
45use time::{OffsetDateTime, format_description::well_known::Rfc3339};
46
47pub type PortFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
49pub type Result<T> = std::result::Result<T, Error>;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum ErrorKind {
54 InvalidInput,
55 NotFound,
56 Integrity,
57 Incompatible,
58 Unavailable,
59 Cancelled,
60 TimedOut,
61 Runtime,
62 Protocol,
63 Io,
64 Capacity,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct Error {
69 pub kind: ErrorKind,
70 pub message: String,
71}
72impl Error {
73 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
74 Self {
75 kind,
76 message: message.into(),
77 }
78 }
79}
80impl fmt::Display for Error {
81 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(f, "{:?}: {}", self.kind, self.message)
83 }
84}
85impl std::error::Error for Error {}
86impl From<std::io::Error> for Error {
87 fn from(value: std::io::Error) -> Self {
88 Self::new(ErrorKind::Io, value.to_string())
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
94#[serde(deny_unknown_fields)]
95pub struct ProgramRef {
96 pub id: String,
97 pub version: String,
98}
99impl ProgramRef {
100 pub fn validate(&self) -> Result<()> {
101 for (field, value) in [("program id", &self.id), ("program version", &self.version)] {
102 if value.is_empty()
103 || value.len() > 128
104 || value == "."
105 || value == ".."
106 || !value
107 .bytes()
108 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b"._-".contains(&b))
109 {
110 return Err(Error::new(
111 ErrorKind::InvalidInput,
112 format!("invalid {field}"),
113 ));
114 }
115 }
116 Ok(())
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
121#[serde(transparent)]
122pub struct Digest(pub String);
123impl Digest {
124 pub fn validate(&self) -> Result<()> {
125 if self.0.len() != 71
126 || !self.0.starts_with("sha256:")
127 || !self.0[7..]
128 .bytes()
129 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
130 {
131 return Err(Error::new(
132 ErrorKind::InvalidInput,
133 "expected sha256: followed by 64 lowercase hexadecimal digits",
134 ));
135 }
136 Ok(())
137 }
138 pub fn hex(&self) -> &str {
139 self.0.strip_prefix("sha256:").unwrap_or(&self.0)
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(deny_unknown_fields)]
145pub struct ProgramDescriptor {
146 pub program: ProgramRef,
147 pub digest: Digest,
148 pub size: u64,
149}
150impl ProgramDescriptor {
151 pub fn validate(&self) -> Result<()> {
152 self.program.validate()?;
153 self.digest.validate()?;
154 if self.size == 0 {
155 return Err(Error::new(ErrorKind::InvalidInput, "empty program archive"));
156 }
157 Ok(())
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(deny_unknown_fields)]
163pub struct PythonRuntime {
164 pub kind: String,
165 pub python: String,
166 pub protocol: u32,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(deny_unknown_fields)]
171pub struct Platform {
172 pub os: String,
173 pub arch: String,
174}
175
176#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177#[serde(deny_unknown_fields)]
178pub struct ProgramManifest {
179 pub schema_version: u32,
180 pub program: ProgramRef,
181 pub runtime: PythonRuntime,
182 pub handler: String,
183 pub platform: Platform,
184}
185impl ProgramManifest {
186 pub fn validate(&self) -> Result<()> {
187 self.program.validate()?;
188 if self.schema_version != 1
189 || self.runtime.kind != "python"
190 || !matches!(self.runtime.protocol, 1..=3)
191 {
192 return Err(Error::new(
193 ErrorKind::Incompatible,
194 "unsupported manifest, runtime, or protocol version",
195 ));
196 }
197 let Some(minor) = self
198 .runtime
199 .python
200 .strip_prefix("3.")
201 .and_then(|s| s.parse::<u32>().ok())
202 else {
203 return Err(Error::new(
204 ErrorKind::InvalidInput,
205 "python must be an exact 3.minor version",
206 ));
207 };
208 if minor < 11 || self.runtime.python != format!("3.{minor}") {
209 return Err(Error::new(
210 ErrorKind::Incompatible,
211 "CPython 3.11 or newer is required",
212 ));
213 }
214 let valid_identifier = |part: &str| {
215 !part.is_empty()
216 && part.bytes().enumerate().all(|(i, b)| {
217 b.is_ascii_alphabetic() || b == b'_' || (i > 0 && b.is_ascii_digit())
218 })
219 };
220 let Some((module, function)) = self.handler.split_once(':') else {
221 return Err(Error::new(
222 ErrorKind::InvalidInput,
223 "handler must be module:function",
224 ));
225 };
226 if !module.split('.').all(valid_identifier) || !valid_identifier(function) {
227 return Err(Error::new(
228 ErrorKind::InvalidInput,
229 "invalid Python handler",
230 ));
231 }
232 if !["linux", "macos"].contains(&self.platform.os.as_str())
233 || !["x86_64", "aarch64"].contains(&self.platform.arch.as_str())
234 {
235 return Err(Error::new(
236 ErrorKind::Incompatible,
237 "initial platforms are Linux/macOS on x86_64/aarch64",
238 ));
239 }
240 Ok(())
241 }
242 pub fn validate_host(&self) -> Result<()> {
243 self.validate()?;
244 if self.platform.os != std::env::consts::OS || self.platform.arch != std::env::consts::ARCH
245 {
246 return Err(Error::new(
247 ErrorKind::Incompatible,
248 "package platform differs from this worker",
249 ));
250 }
251 Ok(())
252 }
253}
254
255pub fn validate_json_cloudevent(value: &Value) -> Result<()> {
259 validate_cloudevent_context(value)?;
260 let object = value.as_object().expect("validated CloudEvent object");
261 if !object.contains_key("data") || object.contains_key("data_base64") {
262 return Err(Error::new(
263 ErrorKind::InvalidInput,
264 "this profile requires user-owned JSON data",
265 ));
266 }
267 if object.get("datacontenttype").and_then(Value::as_str) != Some("application/json") {
268 return Err(Error::new(
269 ErrorKind::InvalidInput,
270 "datacontenttype must be application/json",
271 ));
272 }
273 Ok(())
274}
275
276pub fn validate_cloudevent_context(value: &Value) -> Result<()> {
279 let object = value
280 .as_object()
281 .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "CloudEvent must be an object"))?;
282 for (name, field) in object {
283 if name == "data" {
284 continue;
285 }
286 if name.is_empty()
287 || !name
288 .bytes()
289 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
290 {
291 return Err(Error::new(
292 ErrorKind::InvalidInput,
293 format!("invalid CloudEvent context name {name}"),
294 ));
295 }
296 match field {
297 Value::String(text) if valid_context_string(text) => {}
298 Value::Bool(_) => {}
299 Value::Number(number)
300 if number
301 .as_i64()
302 .and_then(|n| i32::try_from(n).ok())
303 .is_some() => {}
304 _ => {
305 return Err(Error::new(
306 ErrorKind::InvalidInput,
307 format!("invalid CloudEvent context value for {name}"),
308 ));
309 }
310 }
311 }
312 if object.get("specversion").and_then(Value::as_str) != Some("1.0") {
313 return Err(Error::new(
314 ErrorKind::InvalidInput,
315 "CloudEvent specversion must be 1.0",
316 ));
317 }
318 for key in ["id", "source", "type"] {
319 if object
320 .get(key)
321 .and_then(Value::as_str)
322 .is_none_or(str::is_empty)
323 {
324 return Err(Error::new(
325 ErrorKind::InvalidInput,
326 format!("missing nonempty CloudEvent {key}"),
327 ));
328 }
329 }
330 let source = context_string(object, "source")?.expect("required source checked");
331 UriReferenceStr::new(source)
332 .map_err(|_| Error::new(ErrorKind::InvalidInput, "source must be a URI-reference"))?;
333 if let Some(schema) = context_string(object, "dataschema")? {
334 UriAbsoluteStr::new(schema).map_err(|_| {
335 Error::new(
336 ErrorKind::InvalidInput,
337 "dataschema must be an absolute URI without a fragment",
338 )
339 })?;
340 }
341 if context_string(object, "subject")?.is_some_and(str::is_empty) {
342 return Err(Error::new(
343 ErrorKind::InvalidInput,
344 "subject must be nonempty when present",
345 ));
346 }
347 if let Some(timestamp) = context_string(object, "time")? {
348 if !matches!(timestamp.as_bytes().get(10), Some(b'T' | b't')) {
351 return Err(Error::new(
352 ErrorKind::InvalidInput,
353 "time must use an RFC 3339 T separator",
354 ));
355 }
356 OffsetDateTime::parse(timestamp, &Rfc3339).map_err(|_| {
357 Error::new(
358 ErrorKind::InvalidInput,
359 "time must be an RFC 3339 timestamp",
360 )
361 })?;
362 }
363 if let Some(trace) = object.get("traceparent") {
364 let Some(trace) = trace.as_str() else {
365 return Err(Error::new(
366 ErrorKind::InvalidInput,
367 "traceparent must be a string",
368 ));
369 };
370 validate_traceparent(trace)?;
371 }
372 if let Some(state) = context_string(object, "tracestate")? {
373 if !object.contains_key("traceparent") {
374 return Err(Error::new(
375 ErrorKind::InvalidInput,
376 "tracestate requires traceparent in this invocation profile",
377 ));
378 }
379 validate_tracestate(state)?;
380 }
381 Ok(())
382}
383
384#[derive(Debug, Clone, PartialEq, Serialize)]
392#[serde(transparent)]
393pub struct CloudEvent(Value);
394impl CloudEvent {
395 pub fn new(value: Value) -> Result<Self> {
396 validate_json_cloudevent(&value)?;
397 let object = value.as_object().expect("validated CloudEvent object");
398 for key in [
399 "ldgtenantid",
400 "ldgnamespace",
401 "ldgrunid",
402 "ldgtaskid",
403 "ldgattemptid",
404 ] {
405 if object
406 .get(key)
407 .and_then(Value::as_str)
408 .is_none_or(str::is_empty)
409 {
410 return Err(Error::new(
411 ErrorKind::InvalidInput,
412 format!("missing nonempty CloudEvent {key}"),
413 ));
414 }
415 }
416 if object
417 .get("ldgattemptno")
418 .and_then(Value::as_i64)
419 .is_none_or(|n| !(1..=i64::from(i32::MAX)).contains(&n))
420 {
421 return Err(Error::new(
422 ErrorKind::InvalidInput,
423 "ldgattemptno must be a positive signed 32-bit integer",
424 ));
425 }
426 for key in [
427 "ldgworkflowid",
428 "ldgactivationid",
429 "ldgparentworkflowid",
430 "ldgrootworkflowid",
431 ] {
432 if let Some(id) = context_string(object, key)?
433 && (id.is_empty() || id.len() > 128)
434 {
435 return Err(Error::new(
436 ErrorKind::InvalidInput,
437 format!("{key} must contain 1..=128 bytes"),
438 ));
439 }
440 }
441 if let Some(activation) = context_string(object, "ldgactivationid")?
442 && (!object.contains_key("ldgworkflowid")
443 || Some(activation) != object.get("ldgtaskid").and_then(Value::as_str))
444 {
445 return Err(Error::new(
446 ErrorKind::InvalidInput,
447 "workflow activation requires its workflow ID and matching task ID",
448 ));
449 }
450 match (
451 context_string(object, "ldgworkflowid")?,
452 context_string(object, "ldgparentworkflowid")?,
453 context_string(object, "ldgrootworkflowid")?,
454 ) {
455 (_, None, None) => {}
456 (Some(workflow), Some(parent), Some(root))
457 if workflow != parent && workflow != root => {}
458 _ => {
459 return Err(Error::new(
460 ErrorKind::InvalidInput,
461 "nested workflow requires paired parent/root IDs distinct from itself",
462 ));
463 }
464 }
465 Ok(Self(value))
466 }
467 pub fn value(&self) -> &Value {
468 &self.0
469 }
470 pub fn into_value(self) -> Value {
471 self.0
472 }
473 pub fn id(&self) -> &str {
474 self.string("id")
475 }
476 pub fn attempt_id(&self) -> &str {
477 self.string("ldgattemptid")
478 }
479 pub fn task_id(&self) -> &str {
480 self.string("ldgtaskid")
481 }
482 pub fn tenant_id(&self) -> &str {
483 self.string("ldgtenantid")
484 }
485 pub fn namespace(&self) -> &str {
486 self.string("ldgnamespace")
487 }
488 pub fn traceparent(&self) -> Option<&str> {
489 self.0.get("traceparent").and_then(Value::as_str)
490 }
491 fn string(&self, key: &str) -> &str {
492 self.0[key].as_str().expect("validated CloudEvent string")
493 }
494}
495impl<'de> Deserialize<'de> for CloudEvent {
496 fn deserialize<D: serde::Deserializer<'de>>(
497 deserializer: D,
498 ) -> std::result::Result<Self, D::Error> {
499 struct EventVisitor;
500 impl<'de> serde::de::Visitor<'de> for EventVisitor {
501 type Value = CloudEvent;
502 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
503 formatter.write_str("a CloudEvent object with unique context attributes")
504 }
505 fn visit_map<A: serde::de::MapAccess<'de>>(
506 self,
507 mut access: A,
508 ) -> std::result::Result<Self::Value, A::Error> {
509 let mut object = Map::new();
510 while let Some((key, value)) = access.next_entry::<String, Value>()? {
511 if object.insert(key.clone(), value).is_some() {
512 return Err(serde::de::Error::custom(format!(
513 "duplicate CloudEvent field {key}"
514 )));
515 }
516 }
517 CloudEvent::new(Value::Object(object)).map_err(serde::de::Error::custom)
518 }
519 }
520 deserializer.deserialize_map(EventVisitor)
521 }
522}
523
524fn valid_context_string(value: &str) -> bool {
525 value.chars().all(|character| {
526 let code = u32::from(character);
527 !character.is_control() && !(0xfdd0..=0xfdef).contains(&code) && (code & 0xfffe) != 0xfffe
528 })
529}
530
531fn context_string<'a>(object: &'a Map<String, Value>, key: &str) -> Result<Option<&'a str>> {
532 object
533 .get(key)
534 .map(|value| {
535 value.as_str().ok_or_else(|| {
536 Error::new(ErrorKind::InvalidInput, format!("{key} must be a string"))
537 })
538 })
539 .transpose()
540}
541
542fn validate_tracestate(state: &str) -> Result<()> {
543 let invalid = || Error::new(ErrorKind::InvalidInput, "invalid or oversized tracestate");
544 if state.len() > 512 || !state.is_ascii() {
545 return Err(invalid());
546 }
547 let mut keys = std::collections::HashSet::new();
548 for (index, member) in state.split(',').enumerate() {
549 if index >= 32 {
550 return Err(invalid());
551 }
552 let member = member.trim_matches(' ');
555 if member.is_empty() {
556 continue;
557 }
558 let Some((key, value)) = member.split_once('=') else {
559 return Err(invalid());
560 };
561 if !valid_tracestate_key(key)
562 || !keys.insert(key)
563 || value.is_empty()
564 || value.len() > 256
565 || !value
566 .bytes()
567 .all(|b| (0x20..=0x7e).contains(&b) && b != b',' && b != b'=')
568 {
569 return Err(invalid());
570 }
571 }
572 Ok(())
573}
574
575fn valid_tracestate_key(key: &str) -> bool {
576 let allowed = |b: u8| b.is_ascii_lowercase() || b.is_ascii_digit() || b"_-*/".contains(&b);
577 let starts_lower = |part: &str| part.bytes().next().is_some_and(|b| b.is_ascii_lowercase());
578 if let Some((tenant, system)) = key.split_once('@') {
579 !tenant.is_empty()
580 && tenant.len() <= 241
581 && tenant
582 .bytes()
583 .next()
584 .is_some_and(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
585 && tenant.bytes().all(allowed)
586 && !system.is_empty()
587 && system.len() <= 14
588 && starts_lower(system)
589 && system.bytes().all(allowed)
590 } else {
591 !key.is_empty() && key.len() <= 256 && starts_lower(key) && key.bytes().all(allowed)
592 }
593}
594
595fn validate_traceparent(trace: &str) -> Result<()> {
596 let segments: Vec<_> = trace.split('-').collect();
598 if segments.len() != 4
599 || segments[0] != "00"
600 || segments[1].len() != 32
601 || segments[2].len() != 16
602 || segments[3].len() != 2
603 || !segments.iter().all(|s| {
604 s.bytes()
605 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
606 })
607 || segments[1].bytes().all(|b| b == b'0')
608 || segments[2].bytes().all(|b| b == b'0')
609 {
610 return Err(Error::new(
611 ErrorKind::InvalidInput,
612 "invalid or unsupported traceparent",
613 ));
614 }
615 Ok(())
616}
617
618#[derive(Debug, Clone)]
620pub struct RunControl {
621 cancelled: Arc<AtomicBool>,
622 deadline: Instant,
623}
624impl RunControl {
625 pub fn with_deadline(deadline: Instant) -> Self {
627 Self {
628 cancelled: Arc::new(AtomicBool::new(false)),
629 deadline,
630 }
631 }
632
633 pub fn new(timeout: Duration) -> Self {
636 let now = Instant::now();
637 Self {
638 cancelled: Arc::new(AtomicBool::new(false)),
639 deadline: now.checked_add(timeout).unwrap_or(now),
640 }
641 }
642 pub fn try_new(timeout: Duration) -> Result<Self> {
644 let deadline = Instant::now().checked_add(timeout).ok_or_else(|| {
645 Error::new(
646 ErrorKind::InvalidInput,
647 "timeout exceeds the host clock range",
648 )
649 })?;
650 Ok(Self {
651 cancelled: Arc::new(AtomicBool::new(false)),
652 deadline,
653 })
654 }
655 pub fn cancel(&self) {
656 self.cancelled.store(true, Ordering::Release);
657 }
658 pub fn deadline(&self) -> Instant {
659 self.deadline
660 }
661 pub fn is_cancelled(&self) -> bool {
662 self.cancelled.load(Ordering::Acquire)
663 }
664 pub fn check(&self) -> Result<()> {
665 if self.is_cancelled() {
666 Err(Error::new(ErrorKind::Cancelled, "invocation cancelled"))
667 } else if Instant::now() >= self.deadline {
668 Err(Error::new(
669 ErrorKind::TimedOut,
670 "invocation deadline expired",
671 ))
672 } else {
673 Ok(())
674 }
675 }
676}
677
678pub trait ArtifactLease: Send + Sync {}
679impl<T: Send + Sync> ArtifactLease for T {}
680
681#[derive(Clone)]
683pub struct PreparedArtifact {
684 root: PathBuf,
685 manifest: ProgramManifest,
686 digest: Digest,
687 lease: Arc<dyn ArtifactLease>,
688}
689impl PreparedArtifact {
690 pub fn new(
691 root: PathBuf,
692 manifest: ProgramManifest,
693 digest: Digest,
694 lease: Arc<dyn ArtifactLease>,
695 ) -> Self {
696 Self {
697 root,
698 manifest,
699 digest,
700 lease,
701 }
702 }
703 pub fn root(&self) -> &Path {
704 &self.root
705 }
706 pub fn manifest(&self) -> &ProgramManifest {
707 &self.manifest
708 }
709 pub fn digest(&self) -> &Digest {
710 &self.digest
711 }
712}
713impl fmt::Debug for PreparedArtifact {
714 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
715 f.debug_struct("PreparedArtifact")
716 .field("root", &self.root)
717 .field("manifest", &self.manifest)
718 .field("digest", &self.digest)
719 .field("pin_count", &Arc::strong_count(&self.lease))
720 .finish()
721 }
722}
723
724#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
725#[serde(tag = "status", rename_all = "snake_case")]
726pub enum ProgramOutcome {
727 Success { output: Value },
728 Failure { kind: String, message: String },
729}
730
731pub trait ProgramStore: Send + Sync {
736 fn resolve<'a>(&'a self, program: &'a ProgramRef) -> PortFuture<'a, ProgramDescriptor>;
737 fn fetch<'a>(&'a self, descriptor: &'a ProgramDescriptor) -> PortFuture<'a, Vec<u8>>;
738}
739pub trait ArtifactCache: Send + Sync {
740 fn lookup<'a>(
741 &'a self,
742 descriptor: &'a ProgramDescriptor,
743 ) -> PortFuture<'a, Option<PreparedArtifact>>;
744 fn publish<'a>(
745 &'a self,
746 descriptor: &'a ProgramDescriptor,
747 archive: Vec<u8>,
748 ) -> PortFuture<'a, PreparedArtifact>;
749}
750pub enum StartOutcome {
752 Ready(Box<dyn ExecutionSession>),
753 CleanupRequired {
754 error: Error,
755 session: Box<dyn ExecutionSession>,
756 },
757}
758
759pub trait ExecutionRuntime: Send + Sync {
760 fn start<'a>(
763 &'a self,
764 artifact: PreparedArtifact,
765 control: RunControl,
766 ) -> PortFuture<'a, StartOutcome>;
767}
768pub trait RuntimeRequestHandler: Send + Sync {
773 fn handle<'a>(
774 &'a self,
775 request: RuntimeRequest,
776 control: RunControl,
777 ) -> PortFuture<'a, RuntimeReply>;
778}
779
780pub trait ExecutionSession: Send {
781 fn pid(&self) -> u32;
782 fn execute<'a>(
786 &'a mut self,
787 invocation: RuntimeInvocation,
788 control: RunControl,
789 ) -> PortFuture<'a, ProgramOutcome>;
790 fn execute_with_requests<'a>(
795 &'a mut self,
796 _invocation: RuntimeInvocation,
797 _control: RunControl,
798 _handler: Arc<dyn RuntimeRequestHandler>,
799 ) -> PortFuture<'a, ProgramOutcome> {
800 Box::pin(async {
801 Err(Error::new(
802 ErrorKind::Incompatible,
803 "runtime does not support interactive execution",
804 ))
805 })
806 }
807 fn close(&mut self) -> PortFuture<'_, ()>;
811}
812
813pub fn event_extensions(value: &CloudEvent) -> Map<String, Value> {
815 value
816 .value()
817 .as_object()
818 .expect("validated object")
819 .iter()
820 .filter(|(k, _)| k.starts_with("ldg") || *k == "traceparent" || *k == "tracestate")
821 .map(|(k, v)| (k.clone(), v.clone()))
822 .collect()
823}