1use serde::{Deserialize, Serialize};
51use serde_json::Value;
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(untagged)]
56pub enum EventPattern {
57 Specific(String),
59 Wildcard,
61}
62
63impl EventPattern {
64 pub fn matches(&self, event_type_id: &str) -> bool {
66 match self {
67 EventPattern::Specific(pattern) => pattern == event_type_id,
68 EventPattern::Wildcard => true,
69 }
70 }
71
72 pub fn wildcard() -> Self {
74 EventPattern::Wildcard
75 }
76
77 pub fn specific(event_type_id: impl Into<String>) -> Self {
79 EventPattern::Specific(event_type_id.into())
80 }
81}
82
83impl From<String> for EventPattern {
84 fn from(s: String) -> Self {
85 if s == "*" || s == "all" {
86 EventPattern::Wildcard
87 } else {
88 EventPattern::Specific(s)
89 }
90 }
91}
92
93impl From<&str> for EventPattern {
94 fn from(s: &str) -> Self {
95 if s == "*" || s == "all" {
96 EventPattern::Wildcard
97 } else {
98 EventPattern::Specific(s.to_string())
99 }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
106pub enum EventHandlerType {
107 Llm {
109 instruction: String,
111 },
112
113 Script {
115 language: String,
117 code: String,
119 #[serde(default)]
125 resident: bool,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
130 scope: Option<String>,
131 },
132
133 Static {
138 actions: Vec<serde_json::Value>,
140 },
141
142 Manual {
151 #[serde(default = "default_manual_timeout_secs")]
153 timeout_secs: u64,
154 },
155}
156
157pub const DEFAULT_MANUAL_TIMEOUT_SECS: u64 = 300;
163
164fn default_manual_timeout_secs() -> u64 {
165 DEFAULT_MANUAL_TIMEOUT_SECS
166}
167
168impl EventHandlerType {
169 pub fn script(language: impl Into<String>, code: impl Into<String>) -> Self {
171 EventHandlerType::Script {
172 language: language.into(),
173 code: code.into(),
174 resident: false,
175 scope: None,
176 }
177 }
178
179 pub fn script_resident(
182 language: impl Into<String>,
183 code: impl Into<String>,
184 scope: Option<String>,
185 ) -> Self {
186 EventHandlerType::Script {
187 language: language.into(),
188 code: code.into(),
189 resident: true,
190 scope,
191 }
192 }
193
194 pub fn static_response(actions: Vec<serde_json::Value>) -> Self {
196 EventHandlerType::Static { actions }
197 }
198
199 pub fn llm(instruction: impl Into<String>) -> Self {
201 EventHandlerType::Llm {
202 instruction: instruction.into(),
203 }
204 }
205
206 pub fn manual(timeout_secs: u64) -> Self {
208 EventHandlerType::Manual { timeout_secs }
209 }
210
211 pub fn validate(&self) -> Result<(), InterpolationError> {
223 if let EventHandlerType::Script { code, .. } = self {
224 if code.len() > super::types::ScriptSource::MAX_CODE_BYTES {
225 return Err(budget_error());
226 }
227 }
228 match self {
229 EventHandlerType::Static { actions } => {
230 for action in actions {
231 validate_event_references(action)?;
232 }
233 Ok(())
234 }
235 EventHandlerType::Script {
242 language,
243 code,
244 resident,
245 ..
246 } if !*resident && defines_handle(language, code) => {
247 Err(InterpolationError::script_contract(language.clone()))
248 }
249 _ => Ok(()),
250 }
251 }
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(deny_unknown_fields)]
264pub struct EventHandler {
265 pub event_pattern: EventPattern,
267
268 pub handler: EventHandlerType,
270}
271
272impl EventHandler {
273 pub fn new(event_pattern: EventPattern, handler: EventHandlerType) -> Self {
275 Self {
276 event_pattern,
277 handler,
278 }
279 }
280
281 pub fn matches(&self, event_type_id: &str) -> bool {
283 self.event_pattern.matches(event_type_id)
284 }
285}
286
287#[derive(Debug, Clone, Default, Serialize, Deserialize)]
289pub struct EventHandlerConfig {
290 pub handlers: Vec<EventHandler>,
292}
293
294impl EventHandlerConfig {
295 pub fn new() -> Self {
297 Self {
298 handlers: Vec::new(),
299 }
300 }
301
302 pub fn add_handler(&mut self, handler: EventHandler) {
304 self.handlers.push(handler);
305 }
306
307 pub fn find_handler(&self, event_type_id: &str) -> Option<&EventHandlerType> {
309 self.handlers
310 .iter()
311 .find(|h| h.matches(event_type_id))
312 .map(|h| &h.handler)
313 }
314
315 pub fn is_empty(&self) -> bool {
317 self.handlers.is_empty()
318 }
319
320 pub fn len(&self) -> usize {
322 self.handlers.len()
323 }
324}
325
326const REF_OPEN: &str = "{{";
332const REF_CLOSE: &str = "}}";
334const REF_ROOT: &str = "event";
336const REF_ROOT_DOT: &str = "event.";
338
339#[derive(Debug, Clone, PartialEq, Eq)]
344pub struct InterpolationError {
345 pub reference: String,
348 pub detail: String,
350 pub kind: ErrorKind,
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub enum ErrorKind {
357 Interpolation,
359 ScriptContract,
361}
362
363impl InterpolationError {
364 fn new(reference: impl Into<String>, detail: impl Into<String>) -> Self {
365 Self {
366 reference: reference.into(),
367 detail: detail.into(),
368 kind: ErrorKind::Interpolation,
369 }
370 }
371
372 fn script_contract(language: impl Into<String>) -> Self {
374 Self {
375 reference: language.into(),
376 detail: String::new(),
377 kind: ErrorKind::ScriptContract,
378 }
379 }
380}
381
382impl std::fmt::Display for InterpolationError {
383 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
384 match self.kind {
385 ErrorKind::Interpolation => write!(
386 f,
387 "static handler reference `{}` could not be resolved: {}",
388 self.reference, self.detail
389 ),
390 ErrorKind::ScriptContract => write!(
391 f,
392 "this {} script defines `handle(...)`, which is the RESIDENT entry point, \
393 but the handler is not marked `resident: true`. As written the script \
394 reads nothing from stdin and prints nothing, so the handler produces no \
395 actions and the server fails closed -- with no hint of why. Add \
396 `\"resident\": true`, or rewrite the script to read the event from stdin \
397 and print its actions.",
398 self.reference
399 ),
400 }
401 }
402}
403
404impl std::error::Error for InterpolationError {}
405
406struct FoundRef<'a> {
408 start: usize,
410 end: usize,
412 raw: &'a str,
414 inner: &'a str,
416}
417
418fn find_reference(s: &str, from: usize) -> Option<FoundRef<'_>> {
424 let mut cursor = from;
425 while cursor < s.len() {
426 let open = cursor + s[cursor..].find(REF_OPEN)?;
427 let after_open = open + REF_OPEN.len();
428 let Some(rel_close) = s[after_open..].find(REF_CLOSE) else {
429 return None;
431 };
432 let close = after_open + rel_close;
433 let inner = s[after_open..close].trim();
434 if inner == REF_ROOT || inner.starts_with(REF_ROOT_DOT) {
435 return Some(FoundRef {
436 start: open,
437 end: close + REF_CLOSE.len(),
438 raw: &s[open..close + REF_CLOSE.len()],
439 inner,
440 });
441 }
442 cursor = open + 1;
443 }
444 None
445}
446
447fn parse_path<'a>(found: &FoundRef<'a>) -> Result<Vec<&'a str>, InterpolationError> {
449 if found.inner == REF_ROOT {
450 return Ok(Vec::new());
451 }
452 let rest = &found.inner[REF_ROOT_DOT.len()..];
453 if rest.is_empty() {
454 return Err(InterpolationError::new(
455 found.raw,
456 format!(
457 "the path after `{}.` is empty; write `{{{{event.field}}}}` or `{{{{event}}}}` for the whole payload",
458 REF_ROOT
459 ),
460 ));
461 }
462 let mut segments = Vec::new();
463 for segment in rest.split('.') {
464 let segment = segment.trim();
465 if segment.is_empty() {
466 return Err(InterpolationError::new(
467 found.raw,
468 "it contains an empty path segment (a doubled or trailing `.`)",
469 ));
470 }
471 segments.push(segment);
472 }
473 Ok(segments)
474}
475
476fn describe_available(value: &Value) -> String {
478 match value {
479 Value::Object(map) if map.is_empty() => "this object is empty".to_string(),
480 Value::Object(map) => format!(
481 "available fields: {}",
482 map.keys()
483 .take(16)
484 .map(|key| key.chars().take(64).collect::<String>())
485 .collect::<Vec<_>>()
486 .join(", ")
487 ),
488 Value::Array(items) if items.is_empty() => "this array is empty".to_string(),
489 Value::Array(items) => format!("available indices: 0..{}", items.len() - 1),
490 other => format!("it is a {} and has no fields", json_type_name(other)),
491 }
492}
493
494fn json_type_name(value: &Value) -> &'static str {
495 match value {
496 Value::Null => "null",
497 Value::Bool(_) => "boolean",
498 Value::Number(_) => "number",
499 Value::String(_) => "string",
500 Value::Array(_) => "array",
501 Value::Object(_) => "object",
502 }
503}
504
505fn resolve<'a>(
507 event: &'a Value,
508 segments: &[&str],
509 raw: &str,
510) -> Result<&'a Value, InterpolationError> {
511 let mut current = event;
512 let mut walked = REF_ROOT.to_string();
513 for segment in segments {
514 let next = match current {
515 Value::Object(map) => map.get(*segment),
516 Value::Array(items) => segment.parse::<usize>().ok().and_then(|i| items.get(i)),
517 _ => None,
518 };
519 current = next.ok_or_else(|| {
520 InterpolationError::new(
521 raw,
522 format!(
523 "`{}` has no `{}` ({})",
524 walked,
525 segment,
526 describe_available(current)
527 ),
528 )
529 })?;
530 walked.push('.');
531 walked.push_str(segment);
532 }
533 Ok(current)
534}
535
536pub const MAX_INTERPOLATION_BYTES: usize = 8 * 1024 * 1024;
538pub const MAX_INTERPOLATION_NODES: usize = 65_536;
539pub const MAX_INTERPOLATION_DEPTH: usize = 64;
540
541fn budget_error() -> InterpolationError {
542 InterpolationError::new(
543 "event",
544 "static handler exceeds the 8 MiB, 65536-node or 64-level interpolation budget",
545 )
546}
547
548fn check_tree(value: &Value) -> Result<(), InterpolationError> {
549 if crate::utils::json_budget::within_budget(
550 value,
551 MAX_INTERPOLATION_BYTES,
552 MAX_INTERPOLATION_NODES,
553 MAX_INTERPOLATION_DEPTH,
554 ) {
555 Ok(())
556 } else {
557 Err(budget_error())
558 }
559}
560
561struct ExpansionBudget {
564 bytes: usize,
565 nodes: usize,
566}
567impl ExpansionBudget {
568 fn append(&mut self, out: &mut String, text: &str) -> Result<(), InterpolationError> {
569 if text.len() > MAX_INTERPOLATION_BYTES.saturating_sub(self.bytes) {
570 return Err(budget_error());
571 }
572 self.bytes += text.len();
573 out.push_str(text);
574 Ok(())
575 }
576 fn visit(&mut self) -> Result<(), InterpolationError> {
577 self.nodes += 1;
578 if self.nodes > MAX_INTERPOLATION_NODES {
579 return Err(budget_error());
580 }
581 Ok(())
582 }
583}
584
585fn bounded_display(
586 value: &Value,
587 budget: &mut ExpansionBudget,
588) -> Result<String, InterpolationError> {
589 check_tree(value)?;
590 if let Value::String(text) = value {
591 let mut result = String::new();
592 budget.append(&mut result, text)?;
593 return Ok(result);
594 }
595 struct Writer<'a> {
596 data: Vec<u8>,
597 budget: &'a mut ExpansionBudget,
598 }
599 impl std::io::Write for Writer<'_> {
600 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
601 if bytes.len() > MAX_INTERPOLATION_BYTES.saturating_sub(self.budget.bytes) {
602 return Err(std::io::Error::other("interpolation byte cap"));
603 }
604 self.budget.bytes += bytes.len();
605 self.data.extend_from_slice(bytes);
606 Ok(bytes.len())
607 }
608 fn flush(&mut self) -> std::io::Result<()> {
609 Ok(())
610 }
611 }
612 let mut writer = Writer {
613 data: Vec::new(),
614 budget,
615 };
616 serde_json::to_writer(&mut writer, value).map_err(|_| budget_error())?;
617 String::from_utf8(writer.data).map_err(|_| budget_error())
618}
619
620fn clone_bounded(value: &Value, budget: &mut ExpansionBudget) -> Result<Value, InterpolationError> {
621 check_tree(value)?;
622 fn copy(value: &Value, budget: &mut ExpansionBudget) -> Result<Value, InterpolationError> {
623 budget.visit()?;
624 match value {
625 Value::String(text) => {
626 let mut out = String::new();
627 budget.append(&mut out, text)?;
628 Ok(Value::String(out))
629 }
630 Value::Array(items) => items
631 .iter()
632 .map(|v| copy(v, budget))
633 .collect::<Result<Vec<_>, _>>()
634 .map(Value::Array),
635 Value::Object(map) => {
636 let mut out = serde_json::Map::new();
637 for (key, value) in map {
638 let mut owned = String::new();
639 budget.append(&mut owned, key)?;
640 out.insert(owned, copy(value, budget)?);
641 }
642 Ok(Value::Object(out))
643 }
644 other => Ok(other.clone()),
645 }
646 }
647 copy(value, budget)
648}
649
650fn missing_event_error(raw: &str) -> InterpolationError {
652 InterpolationError::new(
653 raw,
654 "this event carries no structured data, so nothing can be substituted",
655 )
656}
657
658fn interpolate_string(
660 s: &str,
661 event: Option<&Value>,
662 budget: &mut ExpansionBudget,
663) -> Result<Value, InterpolationError> {
664 let Some(first) = find_reference(s, 0) else {
665 let mut out = String::new();
667 budget.append(&mut out, s)?;
668 return Ok(Value::String(out));
669 };
670
671 if first.start == 0 && first.end == s.len() {
673 let segments = parse_path(&first)?;
674 let event = event.ok_or_else(|| missing_event_error(first.raw))?;
675 return clone_bounded(resolve(event, &segments, first.raw)?, budget);
676 }
677
678 let mut out = String::new();
680 let mut cursor = 0usize;
681 let mut found = Some(first);
682 while let Some(f) = found {
683 budget.append(&mut out, &s[cursor..f.start])?;
684 let segments = parse_path(&f)?;
685 let event = event.ok_or_else(|| missing_event_error(f.raw))?;
686 out.push_str(&bounded_display(resolve(event, &segments, f.raw)?, budget)?);
687 cursor = f.end;
688 found = find_reference(s, cursor);
689 }
690 budget.append(&mut out, &s[cursor..])?;
691 Ok(Value::String(out))
692}
693
694pub fn interpolate_value(
699 value: &Value,
700 event_data: Option<&Value>,
701) -> Result<Value, InterpolationError> {
702 check_tree(value)?;
703 let result = interpolate_inner(
704 value,
705 event_data,
706 &mut ExpansionBudget { bytes: 0, nodes: 0 },
707 )?;
708 if check_tree(&result).is_err() {
709 crate::utils::json_budget::drop_iteratively(result);
710 return Err(budget_error());
711 }
712 Ok(result)
713}
714
715fn interpolate_inner(
716 value: &Value,
717 event_data: Option<&Value>,
718 budget: &mut ExpansionBudget,
719) -> Result<Value, InterpolationError> {
720 budget.visit()?;
721 match value {
722 Value::String(s) => interpolate_string(s, event_data, budget),
723 Value::Array(items) => items
724 .iter()
725 .map(|item| interpolate_inner(item, event_data, budget))
726 .collect::<Result<Vec<_>, _>>()
727 .map(Value::Array),
728 Value::Object(map) => {
729 let mut out = serde_json::Map::new();
730 for (key, val) in map {
731 let key = match interpolate_string(key, event_data, budget)? {
732 Value::String(s) => s,
733 other => bounded_display(&other, budget)?,
734 };
735 if out.contains_key(&key) {
736 return Err(InterpolationError::new(key, "interpolated keys collide"));
737 }
738 out.insert(key, interpolate_inner(val, event_data, budget)?);
739 }
740 Ok(Value::Object(out))
741 }
742 other => Ok(other.clone()),
743 }
744}
745
746pub fn interpolate_actions(
748 actions: &[Value],
749 event_data: Option<&Value>,
750) -> Result<Vec<Value>, InterpolationError> {
751 if actions.len() > MAX_INTERPOLATION_NODES {
752 return Err(budget_error());
753 }
754 let mut budget = ExpansionBudget { bytes: 0, nodes: 0 };
755 let expanded = actions
756 .iter()
757 .map(|action| {
758 check_tree(action)?;
759 interpolate_inner(action, event_data, &mut budget)
760 })
761 .collect::<Result<Vec<_>, _>>()?;
762 let combined = Value::Array(expanded);
763 if check_tree(&combined).is_err() {
764 crate::utils::json_budget::drop_iteratively(combined);
765 return Err(budget_error());
766 }
767 let Value::Array(expanded) = combined else {
768 unreachable!()
769 };
770 Ok(expanded)
771}
772
773pub fn contains_event_reference(value: &Value) -> bool {
775 if check_tree(value).is_err() {
776 return false;
777 }
778 let mut pending = vec![value];
779 while let Some(value) = pending.pop() {
780 match value {
781 Value::String(s) if find_reference(s, 0).is_some() => return true,
782 Value::Array(items) => pending.extend(items),
783 Value::Object(map) => {
784 if map.keys().any(|key| find_reference(key, 0).is_some()) {
785 return true;
786 }
787 pending.extend(map.values());
788 }
789 _ => {}
790 }
791 }
792 false
793}
794
795fn defines_handle(language: &str, code: &str) -> bool {
800 let language = language.to_ascii_lowercase();
801 let python = matches!(language.as_str(), "python" | "python3");
802 let perl = language == "perl";
803 let javascript = matches!(language.as_str(), "javascript" | "js" | "node");
804 if !python && !perl && !javascript && language != "go" {
805 return false;
806 }
807 let bytes = code.as_bytes();
810 let mut tokens: Vec<&str> = Vec::new();
811 let mut i = 0;
812 while i < bytes.len() {
813 if bytes[i].is_ascii_whitespace() {
814 i += 1;
815 continue;
816 }
817 if ((python || perl) && bytes[i] == b'#')
818 || (!python && !perl && bytes[i..].starts_with(b"//"))
819 {
820 while i < bytes.len() && bytes[i] != b'\n' {
821 i += 1;
822 }
823 continue;
824 }
825 if !python && !perl && bytes[i..].starts_with(b"/*") {
826 i += 2;
827 while i < bytes.len() && !bytes[i..].starts_with(b"*/") {
828 i += 1;
829 }
830 i = (i + 2).min(bytes.len());
831 continue;
832 }
833 if perl && bytes[i] == b'q' {
835 let mut delimiter = i + 1;
836 if bytes
837 .get(delimiter)
838 .is_some_and(|b| matches!(*b, b'q' | b'w' | b'x' | b'r'))
839 {
840 delimiter += 1;
841 }
842 while bytes.get(delimiter).is_some_and(u8::is_ascii_whitespace) {
843 delimiter += 1;
844 }
845 if let Some(&open) = bytes
846 .get(delimiter)
847 .filter(|b| !b.is_ascii_alphanumeric() && **b != b'_')
848 {
849 let close = match open {
850 b'{' => b'}',
851 b'[' => b']',
852 b'(' => b')',
853 b'<' => b'>',
854 other => other,
855 };
856 let mut depth = 1;
857 i = delimiter + 1;
858 while i < bytes.len() && depth > 0 {
859 if bytes[i] == b'\\' {
860 i = (i + 2).min(bytes.len());
861 continue;
862 }
863 if bytes[i] == close {
864 depth -= 1;
865 } else if open != close && bytes[i] == open {
866 depth += 1;
867 }
868 i += 1;
869 }
870 tokens.push("<literal>");
871 continue;
872 }
873 }
874 if matches!(bytes[i], b'\'' | b'"' | b'`') {
875 let quote = bytes[i];
876 let triple = python && bytes[i..].starts_with(&[quote; 3]);
877 let size = if triple { 3 } else { 1 };
878 i += size;
879 while i < bytes.len() {
880 if bytes[i] == b'\\' {
881 i = (i + 2).min(bytes.len());
882 continue;
883 }
884 if bytes[i] == quote && (!triple || bytes[i..].starts_with(&[quote; 3])) {
885 i += size;
886 break;
887 }
888 i += 1;
889 }
890 tokens.push("<literal>");
892 continue;
893 }
894 if javascript
896 && bytes[i] == b'/'
897 && tokens
898 .last()
899 .is_none_or(|t| matches!(*t, "=" | "(" | "," | ":" | "return"))
900 {
901 i += 1;
902 let mut class = false;
903 while i < bytes.len() {
904 match bytes[i] {
905 b'\\' => {
906 i = (i + 2).min(bytes.len());
907 continue;
908 }
909 b'[' => class = true,
910 b']' => class = false,
911 b'/' if !class => {
912 i += 1;
913 break;
914 }
915 b'\n' => break,
916 _ => {}
917 }
918 i += 1;
919 }
920 tokens.push("<literal>");
921 continue;
922 }
923 let start = i;
924 if bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_' || bytes[i] == b'$' {
925 i += 1;
926 while i < bytes.len()
927 && (bytes[i].is_ascii_alphanumeric() || matches!(bytes[i], b'_' | b'$'))
928 {
929 i += 1;
930 }
931 } else {
932 i += code[i..].chars().next().unwrap().len_utf8();
934 }
935 tokens.push(&code[start..i]);
936 }
937 for (index, token) in tokens.iter().enumerate() {
938 if *token != "handle" {
939 continue;
940 }
941 let before = index.checked_sub(1).and_then(|n| tokens.get(n)).copied();
942 let after = tokens.get(index + 1).copied();
943 if python && before == Some("def") && after == Some("(") {
944 return true;
945 }
946 if perl && before == Some("sub") && matches!(after, Some("{" | "(" | ":")) {
947 return true;
948 }
949 if language == "go" && before == Some("func") && after == Some("(") {
950 return true;
951 }
952 if javascript {
953 if before == Some("function") && after == Some("(") {
954 return true;
955 }
956 if after != Some("=") {
957 continue;
958 }
959 let mut next = index + 2;
960 if tokens.get(next) == Some(&"async") {
961 next += 1;
962 }
963 if tokens.get(next) == Some(&"function") {
964 return true;
965 }
966 if tokens.get(next) == Some(&"(") {
967 let mut depth = 0;
968 while let Some(token) = tokens.get(next) {
969 match *token {
970 "(" => depth += 1,
971 ")" => depth -= 1,
972 _ => {}
973 }
974 next += 1;
975 if depth == 0 {
976 break;
977 }
978 }
979 } else {
980 next += 1;
981 } if tokens.get(next..next.saturating_add(2)) == Some(&["=", ">"][..]) {
983 return true;
984 }
985 }
986 }
987 false
988}
989
990pub fn validate_event_references(value: &Value) -> Result<(), InterpolationError> {
995 check_tree(value)?;
996 fn check_string(s: &str) -> Result<(), InterpolationError> {
997 let mut cursor = 0;
998 while let Some(found) = find_reference(s, cursor) {
999 parse_path(&found)?;
1000 cursor = found.end;
1001 }
1002 Ok(())
1003 }
1004 let mut pending = vec![value];
1005 while let Some(value) = pending.pop() {
1006 match value {
1007 Value::String(s) => check_string(s)?,
1008 Value::Array(items) => pending.extend(items),
1009 Value::Object(map) => {
1010 for key in map.keys() {
1011 check_string(key)?;
1012 }
1013 pending.extend(map.values());
1014 }
1015 _ => {}
1016 }
1017 }
1018 Ok(())
1019}