1use std::collections::{BTreeMap, BTreeSet};
4use std::fs::{self, OpenOptions};
5use std::io::Write;
6use std::io::{BufRead, BufReader};
7use std::net::{TcpStream, ToSocketAddrs};
8use std::path::{Path, PathBuf};
9use std::process::{Child, Command, Stdio};
10use std::sync::mpsc;
11use std::time::Duration;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use chrono::{Datelike, Offset, Timelike, Utc};
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17
18use crate::auth::{
19 JwtClaimsValidationConfig, build_jwt_verification_key, inspect_jwt_header, parse_jwks_json,
20 parse_oidc_discovery_json, select_jwk_for_token, validate_jwt_with_verification_key,
21};
22use crate::baseline::{
23 BaselineVerificationError, BaselineVerificationReport, verify_meerkat_baseline_symbols,
24};
25use crate::decisions::{
26 AuthPolicy, AuthProvider, BigQueryNaming, ConsoleAccessRequest, ConsolePolicy,
27 DecisionPolicyError, ReleaseMetadata, RuntimeOpsPolicy, enforce_console_route_access,
28 load_trusted_mobkit_modules_from_toml, parse_release_metadata_json, validate_bigquery_naming,
29 validate_release_metadata, validate_runtime_ops_policy,
30};
31use crate::process::{ProcessBoundaryError, run_process_json_line};
32use crate::protocol::parse_unified_event_line;
33use crate::rpc::{RpcCapabilities, RpcCapabilitiesError, parse_rpc_capabilities};
34use crate::types::{
35 EventEnvelope, MobKitConfig, ModuleConfig, ModuleEvent, PreSpawnData, RestartPolicy,
36 UnifiedEvent,
37};
38
39mod bootstrap;
40mod console_ingress;
41pub mod cross_mob_control;
42pub mod cross_mob_remote;
43mod delivery;
44mod event_transport;
45mod gating;
46mod memory;
47pub mod metadata;
48mod module_boundary;
49mod routing;
50mod rpc;
51mod scheduling;
52mod session_store;
53mod supervisor;
54
55pub use bootstrap::{start_mobkit_runtime, start_mobkit_runtime_with_options};
56pub use console_ingress::{
57 ConsoleAgentLiveSnapshot, ConsoleLiveSnapshot, ConsoleMember, ConsoleModelCapabilities,
58 ConsoleRestJsonRequest, ConsoleRestJsonResponse, extract_bearer_token_from_header,
59 handle_console_rest_json_route, handle_console_rest_json_route_with_snapshot,
60 validate_console_token,
61};
62pub use event_transport::normalize_event_line;
63pub use metadata::{
64 InMemoryMetadataStore, LabelRpcResult, MetadataScope, MetadataStoreError,
65 PersistentMetadataStore, RuntimeMetadataTable, SqliteMetadataStore, dispatch_labels_delete,
66 dispatch_labels_get, dispatch_labels_set, labels_to_json_value, parse_labels_param,
67 parse_run_id_param,
68};
69pub use routing::WILDCARD_ROUTE;
70pub use routing::route_module_call;
71pub use rpc::{
72 route_module_call_rpc_json, route_module_call_rpc_subprocess,
73 run_rpc_capabilities_boundary_once,
74};
75pub use scheduling::evaluate_schedules_at_tick;
76pub use session_store::{
77 BigQueryGcConfig, BigQuerySessionStoreAdapter, BigQuerySessionStoreError, GcErrorCallback,
78 JsonFileSessionStore, JsonFileSessionStoreError, JsonStoreLockRecord, SessionPersistenceRow,
79 SessionStoreContract, SessionStoreKind, materialize_latest_session_rows,
80 materialize_live_session_rows, run_periodic_gc, run_periodic_gc_with_error_callback,
81 session_store_contracts,
82};
83pub use supervisor::{run_discovered_module_once, run_module_boundary_once};
84
85pub(crate) use scheduling::validate_schedules;
86
87use event_transport::{insert_event_sorted, merge_unified_events};
88use supervisor::supervise_module_start;
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum NormalizationError {
92 InvalidJson,
93 InvalidSchema,
94 MissingField(&'static str),
95 InvalidFieldType(&'static str),
96 SourceMismatch { expected: &'static str, got: String },
97}
98
99impl std::fmt::Display for NormalizationError {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 match self {
102 Self::InvalidJson => write!(f, "invalid JSON"),
103 Self::InvalidSchema => write!(f, "invalid schema"),
104 Self::MissingField(field) => write!(f, "missing field: {field}"),
105 Self::InvalidFieldType(field) => write!(f, "invalid field type: {field}"),
106 Self::SourceMismatch { expected, got } => {
107 write!(f, "source mismatch: expected {expected}, got {got}")
108 }
109 }
110 }
111}
112
113impl std::error::Error for NormalizationError {}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum RuntimeBoundaryError {
117 Process(ProcessBoundaryError),
118 Normalize(NormalizationError),
119 Mcp(McpBoundaryError),
120}
121
122impl std::fmt::Display for RuntimeBoundaryError {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 match self {
125 Self::Process(err) => write!(f, "process boundary: {err}"),
126 Self::Normalize(err) => write!(f, "normalization: {err}"),
127 Self::Mcp(err) => write!(f, "MCP boundary: {err}"),
128 }
129 }
130}
131
132impl std::error::Error for RuntimeBoundaryError {
133 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
134 match self {
135 Self::Process(err) => Some(err),
136 Self::Normalize(err) => Some(err),
137 Self::Mcp(err) => Some(err),
138 }
139 }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum McpBoundaryError {
144 RuntimeUnavailable(String),
145 McpRequired {
146 module_id: String,
147 flow: String,
148 },
149 Timeout {
150 module_id: String,
151 operation: String,
152 timeout_ms: u64,
153 },
154 ConnectionFailed {
155 module_id: String,
156 reason: String,
157 },
158 ToolListFailed {
159 module_id: String,
160 reason: String,
161 },
162 ToolNotFound {
163 module_id: String,
164 tool: String,
165 available_tools: Vec<String>,
166 },
167 ToolCallFailed {
168 module_id: String,
169 tool: String,
170 reason: String,
171 },
172 CloseFailed {
173 module_id: String,
174 reason: String,
175 },
176 OperationFailedWithCloseFailure {
177 primary: Box<McpBoundaryError>,
178 close: Box<McpBoundaryError>,
179 },
180 InvalidToolPayload {
181 module_id: String,
182 tool: String,
183 reason: String,
184 },
185 InvalidJsonResponse {
186 module_id: String,
187 tool: String,
188 response: String,
189 },
190}
191
192impl std::fmt::Display for McpBoundaryError {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 match self {
195 Self::RuntimeUnavailable(msg) => write!(f, "runtime unavailable: {msg}"),
196 Self::McpRequired { module_id, flow } => {
197 write!(f, "MCP required for module {module_id} flow {flow}")
198 }
199 Self::Timeout {
200 module_id,
201 operation,
202 timeout_ms,
203 } => {
204 write!(
205 f,
206 "timeout for module {module_id} operation {operation} after {timeout_ms}ms"
207 )
208 }
209 Self::ConnectionFailed { module_id, reason } => {
210 write!(f, "connection failed for module {module_id}: {reason}")
211 }
212 Self::ToolListFailed { module_id, reason } => {
213 write!(f, "tool list failed for module {module_id}: {reason}")
214 }
215 Self::ToolNotFound {
216 module_id,
217 tool,
218 available_tools,
219 } => {
220 write!(
221 f,
222 "tool {tool} not found for module {module_id} (available: {})",
223 available_tools.join(", ")
224 )
225 }
226 Self::ToolCallFailed {
227 module_id,
228 tool,
229 reason,
230 } => {
231 write!(
232 f,
233 "tool call {tool} failed for module {module_id}: {reason}"
234 )
235 }
236 Self::CloseFailed { module_id, reason } => {
237 write!(f, "close failed for module {module_id}: {reason}")
238 }
239 Self::OperationFailedWithCloseFailure { primary, close } => {
240 write!(f, "operation failed: {primary}; close also failed: {close}")
241 }
242 Self::InvalidToolPayload {
243 module_id,
244 tool,
245 reason,
246 } => {
247 write!(
248 f,
249 "invalid tool payload for module {module_id} tool {tool}: {reason}"
250 )
251 }
252 Self::InvalidJsonResponse {
253 module_id,
254 tool,
255 response,
256 } => {
257 write!(
258 f,
259 "invalid JSON response for module {module_id} tool {tool}: {response}"
260 )
261 }
262 }
263 }
264}
265
266impl std::error::Error for McpBoundaryError {}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
269pub enum ConfigResolutionError {
270 ModuleNotConfigured(String),
271 ModuleNotDiscovered(String),
272}
273
274impl std::fmt::Display for ConfigResolutionError {
275 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276 match self {
277 Self::ModuleNotConfigured(id) => write!(f, "module not configured: {id}"),
278 Self::ModuleNotDiscovered(id) => write!(f, "module not discovered: {id}"),
279 }
280 }
281}
282
283impl std::error::Error for ConfigResolutionError {}
284
285#[derive(Debug, Clone, PartialEq, Eq)]
286pub enum RuntimeFromConfigError {
287 Config(ConfigResolutionError),
288 Runtime(RuntimeBoundaryError),
289}
290
291impl std::fmt::Display for RuntimeFromConfigError {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 match self {
294 Self::Config(err) => write!(f, "config resolution: {err}"),
295 Self::Runtime(err) => write!(f, "runtime boundary: {err}"),
296 }
297 }
298}
299
300impl std::error::Error for RuntimeFromConfigError {
301 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
302 match self {
303 Self::Config(err) => Some(err),
304 Self::Runtime(err) => Some(err),
305 }
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub enum RpcRuntimeError {
311 Process(ProcessBoundaryError),
312 Capabilities(RpcCapabilitiesError),
313}
314
315impl std::fmt::Display for RpcRuntimeError {
316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317 match self {
318 Self::Process(err) => write!(f, "process boundary: {err}"),
319 Self::Capabilities(err) => write!(f, "capabilities: {err}"),
320 }
321 }
322}
323
324impl std::error::Error for RpcRuntimeError {
325 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
326 match self {
327 Self::Process(err) => Some(err),
328 Self::Capabilities(err) => Some(err),
329 }
330 }
331}
332
333#[derive(Debug, Clone, PartialEq, Eq)]
334pub enum BaselineRuntimeError {
335 Process(ProcessBoundaryError),
336 InvalidRepoPathJson,
337 MissingRepoRoot,
338 InvalidRepoRoot,
339 Baseline(BaselineVerificationError),
340}
341
342impl std::fmt::Display for BaselineRuntimeError {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 match self {
345 Self::Process(err) => write!(f, "process boundary: {err}"),
346 Self::InvalidRepoPathJson => write!(f, "invalid repo path JSON"),
347 Self::MissingRepoRoot => write!(f, "missing repo root"),
348 Self::InvalidRepoRoot => write!(f, "invalid repo root"),
349 Self::Baseline(err) => write!(f, "baseline verification: {err}"),
350 }
351 }
352}
353
354impl std::error::Error for BaselineRuntimeError {
355 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
356 match self {
357 Self::Process(err) => Some(err),
358 Self::Baseline(err) => Some(err),
359 _ => None,
360 }
361 }
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
365pub enum MobkitRuntimeError {
366 Config(ConfigResolutionError),
367 MemoryBackend(ElephantMemoryStoreError),
368}
369
370impl std::fmt::Display for MobkitRuntimeError {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372 match self {
373 Self::Config(err) => write!(f, "config resolution: {err}"),
374 Self::MemoryBackend(err) => write!(f, "memory backend: {err}"),
375 }
376 }
377}
378
379impl std::error::Error for MobkitRuntimeError {
380 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
381 match self {
382 Self::Config(err) => Some(err),
383 Self::MemoryBackend(err) => Some(err),
384 }
385 }
386}
387
388#[derive(Debug, Clone, PartialEq, Eq)]
389pub enum DecisionRuntimeError {
390 Policy(DecisionPolicyError),
391}
392
393impl std::fmt::Display for DecisionRuntimeError {
394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 match self {
396 Self::Policy(err) => write!(f, "decision policy: {err}"),
397 }
398 }
399}
400
401impl std::error::Error for DecisionRuntimeError {
402 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
403 match self {
404 Self::Policy(err) => Some(err),
405 }
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
410pub struct RuntimeDecisionInputs {
411 pub bigquery: BigQueryNaming,
412 pub trusted_mobkit_toml: String,
413 pub auth: AuthPolicy,
414 pub trusted_oidc: TrustedOidcRuntimeConfig,
415 pub console: ConsolePolicy,
416 pub ops: RuntimeOpsPolicy,
417 pub release_metadata_json: String,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
421pub struct RuntimeDecisionState {
422 pub bigquery: BigQueryNaming,
423 pub modules: Vec<ModuleConfig>,
424 pub auth: AuthPolicy,
425 pub trusted_oidc: TrustedOidcRuntimeConfig,
426 pub console: ConsolePolicy,
427 pub ops: RuntimeOpsPolicy,
428 pub release_metadata: ReleaseMetadata,
429}
430
431#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
432pub struct TrustedOidcRuntimeConfig {
433 pub discovery_json: String,
434 pub jwks_json: String,
435 pub audience: String,
436}
437
438#[derive(Debug, Clone, PartialEq, Eq)]
439pub enum ElephantMemoryStoreError {
440 InvalidConfig(String),
441 Io(String),
442 Serialize(String),
443 InvalidStoreData(String),
444 ExternalCallFailed(String),
445}
446
447impl std::fmt::Display for ElephantMemoryStoreError {
448 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
449 match self {
450 Self::InvalidConfig(msg) => write!(f, "invalid config: {msg}"),
451 Self::Io(msg) => write!(f, "I/O error: {msg}"),
452 Self::Serialize(msg) => write!(f, "serialization error: {msg}"),
453 Self::InvalidStoreData(msg) => write!(f, "invalid store data: {msg}"),
454 Self::ExternalCallFailed(msg) => write!(f, "external call failed: {msg}"),
455 }
456 }
457}
458
459impl std::error::Error for ElephantMemoryStoreError {}
460
461#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462pub struct ElephantMemoryBackendConfig {
463 pub endpoint: String,
464 pub state_path: String,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
468#[serde(tag = "kind", rename_all = "snake_case")]
469pub enum MemoryBackendConfig {
470 Elephant(ElephantMemoryBackendConfig),
471}
472
473#[derive(Debug, Clone, PartialEq, Eq)]
474struct ElephantMemoryStoreAdapter {
475 endpoint: String,
476 state_path: PathBuf,
477}
478
479#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
480pub struct RuntimeOptions {
481 pub on_failure_retry_budget: u32,
482 pub always_restart_budget: u32,
483 #[serde(default)]
484 pub supervisor_restart_backoff_ms: u64,
485 #[serde(default)]
486 pub supervisor_test_force_terminate_failure: bool,
487 #[serde(default)]
488 pub memory_backend: Option<MemoryBackendConfig>,
489 #[serde(default = "default_implicit_delegate_idle_retire_secs")]
490 pub implicit_delegate_idle_retire_secs: Option<u64>,
491 #[serde(default = "default_implicit_delegate_idle_sweep_interval_ms")]
492 pub implicit_delegate_idle_sweep_interval_ms: u64,
493}
494
495fn default_implicit_delegate_idle_retire_secs() -> Option<u64> {
496 Some(300)
497}
498
499fn default_implicit_delegate_idle_sweep_interval_ms() -> u64 {
500 10_000
501}
502
503impl Default for RuntimeOptions {
504 fn default() -> Self {
505 Self {
506 on_failure_retry_budget: 1,
507 always_restart_budget: 1,
508 supervisor_restart_backoff_ms: 0,
509 supervisor_test_force_terminate_failure: false,
510 memory_backend: None,
511 implicit_delegate_idle_retire_secs: default_implicit_delegate_idle_retire_secs(),
512 implicit_delegate_idle_sweep_interval_ms:
513 default_implicit_delegate_idle_sweep_interval_ms(),
514 }
515 }
516}
517
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
519pub enum LifecycleStage {
520 MobStarted,
521 ModulesStarted,
522 MergedStreamStarted,
523 ShutdownRequested,
524 ShutdownComplete,
525}
526
527#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
528pub struct LifecycleEvent {
529 pub seq: u64,
530 pub stage: LifecycleStage,
531}
532
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
534pub enum ModuleHealthState {
535 Starting,
536 Healthy,
537 Failed,
538 Restarting,
539 Stopped,
540}
541
542#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
543pub struct ModuleHealthTransition {
544 pub module_id: String,
545 pub from: Option<ModuleHealthState>,
546 pub to: ModuleHealthState,
547 pub attempt: u32,
548}
549
550#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
551pub struct SupervisorReport {
552 pub transitions: Vec<ModuleHealthTransition>,
553}
554
555#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
556pub struct RuntimeShutdownReport {
557 pub terminated_modules: Vec<String>,
558 pub orphan_processes: u32,
559}
560
561#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
562pub struct ScheduleDefinition {
563 pub schedule_id: String,
564 pub interval: String,
565 pub timezone: String,
566 pub enabled: bool,
567 #[serde(default)]
568 pub jitter_ms: u64,
569 #[serde(default)]
570 pub catch_up: bool,
571}
572
573#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
574pub struct ScheduleTrigger {
575 pub schedule_id: String,
576 pub interval: String,
577 pub timezone: String,
578 pub due_tick_ms: u64,
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582pub struct ScheduleEvaluation {
583 pub tick_ms: u64,
584 pub due_triggers: Vec<ScheduleTrigger>,
585}
586
587#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
588pub struct SchedulingSupervisorSignal {
589 pub module_id: String,
590 pub latest_state: ModuleHealthState,
591 pub latest_attempt: u32,
592 pub restart_observed: bool,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
596pub struct ScheduleDispatch {
597 pub claim_key: String,
598 pub schedule_id: String,
599 pub interval: String,
600 pub timezone: String,
601 pub due_tick_ms: u64,
602 pub tick_ms: u64,
603 pub event_id: String,
604 pub supervisor_signal: Option<SchedulingSupervisorSignal>,
605 #[serde(default)]
606 pub runtime_injection: Option<ScheduleRuntimeInjection>,
607 #[serde(default)]
608 pub runtime_injection_error: Option<String>,
609}
610
611#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
612pub struct ScheduleRuntimeInjection {
613 pub member_id: String,
614 pub message: String,
615 pub injection_event_id: String,
616}
617
618#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
619pub struct ScheduleDispatchReport {
620 pub tick_ms: u64,
621 pub due_count: usize,
622 pub dispatched: Vec<ScheduleDispatch>,
623 pub skipped_claims: Vec<String>,
624}
625
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
627pub struct RoutingResolveRequest {
628 pub recipient: String,
629 #[serde(default)]
630 pub channel: Option<String>,
631 #[serde(default)]
632 pub retry_max: Option<u32>,
633 #[serde(default)]
634 pub backoff_ms: Option<u64>,
635 #[serde(default)]
636 pub rate_limit_per_minute: Option<u32>,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
640pub struct RuntimeRoute {
641 pub route_key: String,
642 pub recipient: String,
643 #[serde(default)]
644 pub channel: Option<String>,
645 pub sink: String,
646 pub target_module: String,
647 #[serde(default)]
648 pub retry_max: Option<u32>,
649 #[serde(default)]
650 pub backoff_ms: Option<u64>,
651 #[serde(default)]
652 pub rate_limit_per_minute: Option<u32>,
653}
654
655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656pub struct RoutingResolution {
657 pub route_id: String,
658 pub recipient: String,
659 pub channel: String,
660 pub sink: String,
661 pub target_module: String,
662 pub retry_max: u32,
663 pub backoff_ms: u64,
664 pub rate_limit_per_minute: u32,
665}
666
667#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
668pub struct DeliverySendRequest {
669 pub resolution: RoutingResolution,
670 pub payload: Value,
671 #[serde(default)]
672 pub idempotency_key: Option<String>,
673}
674
675#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
676pub struct DeliveryAttempt {
677 pub attempt: u32,
678 pub status: String,
679 pub backoff_ms: u64,
680}
681
682#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
683pub struct DeliveryRecord {
684 pub delivery_id: String,
685 pub route_id: String,
686 pub recipient: String,
687 pub sink: String,
688 pub target_module: String,
689 pub payload: Value,
690 pub status: String,
691 pub attempts: Vec<DeliveryAttempt>,
692 pub first_attempt_ms: u64,
693 pub final_attempt_ms: u64,
694 pub idempotency_key: Option<String>,
695 pub sink_adapter: Option<String>,
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699pub struct DeliveryHistoryRequest {
700 #[serde(default)]
701 pub recipient: Option<String>,
702 #[serde(default)]
703 pub sink: Option<String>,
704 #[serde(default = "default_delivery_history_limit")]
705 pub limit: usize,
706}
707
708#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
709pub struct DeliveryHistoryResponse {
710 pub deliveries: Vec<DeliveryRecord>,
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
714pub struct MemoryStoreInfo {
715 pub store: String,
716 pub record_count: usize,
717}
718
719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
720pub struct MemoryIndexRequest {
721 pub entity: String,
722 pub topic: String,
723 #[serde(default)]
724 pub store: Option<String>,
725 #[serde(default)]
726 pub fact: Option<String>,
727 #[serde(default)]
728 pub metadata: Option<Value>,
729 #[serde(default)]
730 pub conflict: Option<bool>,
731 #[serde(default)]
732 pub conflict_reason: Option<String>,
733}
734
735#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
736pub struct MemoryAssertion {
737 pub assertion_id: String,
738 pub entity: String,
739 pub topic: String,
740 pub store: String,
741 pub fact: String,
742 #[serde(default)]
743 pub metadata: Option<Value>,
744 pub indexed_at_ms: u64,
745}
746
747#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
748pub struct MemoryConflictSignal {
749 pub entity: String,
750 pub topic: String,
751 pub store: String,
752 #[serde(default)]
753 pub reason: Option<String>,
754 pub updated_at_ms: u64,
755}
756
757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
758pub struct MemoryIndexResult {
759 pub entity: String,
760 pub topic: String,
761 pub store: String,
762 #[serde(default)]
763 pub assertion_id: Option<String>,
764 pub conflict_active: bool,
765}
766
767#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
768pub struct MemoryQueryRequest {
769 #[serde(default)]
770 pub entity: Option<String>,
771 #[serde(default)]
772 pub topic: Option<String>,
773 #[serde(default)]
774 pub store: Option<String>,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
778pub struct MemoryQueryResult {
779 pub assertions: Vec<MemoryAssertion>,
780 pub conflicts: Vec<MemoryConflictSignal>,
781}
782
783#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
784struct PersistedMemoryState {
785 #[serde(default)]
786 assertions: Vec<MemoryAssertion>,
787 #[serde(default)]
788 conflicts: Vec<MemoryConflictSignal>,
789}
790
791#[derive(Debug, Clone, PartialEq, Eq)]
792pub enum MemoryIndexError {
793 EntityRequired,
794 TopicRequired,
795 UnsupportedStore(String),
796 FactRequiredWhenConflictUnset,
797 BackendPersistFailed(ElephantMemoryStoreError),
798}
799
800impl std::fmt::Display for MemoryIndexError {
801 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
802 match self {
803 Self::EntityRequired => write!(f, "entity is required"),
804 Self::TopicRequired => write!(f, "topic is required"),
805 Self::UnsupportedStore(store) => write!(f, "unsupported store: {store}"),
806 Self::FactRequiredWhenConflictUnset => {
807 write!(f, "fact is required when conflict is unset")
808 }
809 Self::BackendPersistFailed(err) => write!(f, "backend persist failed: {err}"),
810 }
811 }
812}
813
814impl std::error::Error for MemoryIndexError {
815 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
816 match self {
817 Self::BackendPersistFailed(err) => Some(err),
818 _ => None,
819 }
820 }
821}
822
823#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
824#[serde(rename_all = "snake_case")]
825pub enum GatingRiskTier {
826 R0,
827 R1,
828 R2,
829 R3,
830}
831
832#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
833pub struct GatingEvaluateRequest {
834 pub action: String,
835 pub actor_id: String,
836 pub risk_tier: GatingRiskTier,
837 #[serde(default)]
838 pub rationale: Option<String>,
839 #[serde(default)]
840 pub requested_approver: Option<String>,
841 #[serde(default)]
842 pub approval_recipient: Option<String>,
843 #[serde(default)]
844 pub approval_channel: Option<String>,
845 #[serde(default)]
846 pub approval_timeout_ms: Option<u64>,
847 #[serde(default)]
848 pub entity: Option<String>,
849 #[serde(default)]
850 pub topic: Option<String>,
851}
852
853#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
854#[serde(rename_all = "snake_case")]
855pub enum GatingOutcome {
856 Allowed,
857 AllowedWithAudit,
858 PendingApproval,
859 SafeDraft,
860}
861
862#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
863pub struct GatingEvaluateResult {
864 pub action_id: String,
865 pub action: String,
866 pub actor_id: String,
867 pub risk_tier: GatingRiskTier,
868 pub outcome: GatingOutcome,
869 #[serde(default)]
870 pub pending_id: Option<String>,
871 #[serde(default)]
872 pub fallback_reason: Option<String>,
873}
874
875#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
876pub struct GatingPendingEntry {
877 pub pending_id: String,
878 pub action_id: String,
879 pub action: String,
880 pub actor_id: String,
881 pub risk_tier: GatingRiskTier,
882 #[serde(default)]
883 pub requested_approver: Option<String>,
884 #[serde(default)]
885 pub approval_recipient: Option<String>,
886 #[serde(default)]
887 pub approval_channel: Option<String>,
888 #[serde(default)]
889 pub approval_route_id: Option<String>,
890 #[serde(default)]
891 pub approval_delivery_id: Option<String>,
892 pub created_at_ms: u64,
893 pub deadline_at_ms: u64,
894}
895
896#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
897#[serde(rename_all = "snake_case")]
898pub enum GatingDecision {
899 Approve,
900 Reject,
901 Escalate,
902}
903
904#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
905pub struct GatingDecideRequest {
906 pub pending_id: String,
907 pub approver_id: String,
908 pub decision: GatingDecision,
909 #[serde(default)]
910 pub reason: Option<String>,
911}
912
913#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
914pub struct GatingDecisionResult {
915 pub pending_id: String,
916 pub action_id: String,
917 pub approver_id: String,
918 pub decision: GatingDecision,
919 pub outcome: GatingOutcome,
920 pub decided_at_ms: u64,
921 #[serde(default)]
922 pub reason: Option<String>,
923 #[serde(default)]
924 pub next_pending_id: Option<String>,
925}
926
927#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
928pub struct GatingAuditEntry {
929 pub audit_id: String,
930 pub timestamp_ms: u64,
931 pub event_type: String,
932 pub action_id: String,
933 #[serde(default)]
934 pub pending_id: Option<String>,
935 pub actor_id: String,
936 pub risk_tier: GatingRiskTier,
937 pub outcome: GatingOutcome,
938 pub detail: Value,
939}
940
941#[derive(Debug, Clone, PartialEq, Eq)]
942pub enum GatingDecideError {
943 UnknownPendingId(String),
944 SelfApprovalForbidden,
945 ApproverMismatch { expected: String, provided: String },
946}
947
948impl std::fmt::Display for GatingDecideError {
949 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
950 match self {
951 Self::UnknownPendingId(id) => write!(f, "unknown pending id: {id}"),
952 Self::SelfApprovalForbidden => write!(f, "self-approval is forbidden"),
953 Self::ApproverMismatch { expected, provided } => {
954 write!(f, "approver mismatch: expected {expected}, got {provided}")
955 }
956 }
957 }
958}
959
960impl std::error::Error for GatingDecideError {}
961
962#[derive(Debug, Clone, PartialEq, Eq)]
963struct DeliveryIdempotencyEntry {
964 delivery_id: String,
965 payload: Value,
966 canonical_resolution: RoutingResolution,
967}
968
969#[derive(Debug, Clone, PartialEq, Eq, Default)]
970struct RouterBoundaryOverrides {
971 channel: Option<String>,
972 sink: Option<String>,
973 target_module: Option<String>,
974 retry_max: Option<u32>,
975 backoff_ms: Option<u64>,
976 rate_limit_per_minute: Option<u32>,
977}
978
979#[derive(Debug, Clone, PartialEq, Eq, Default)]
980struct DeliveryBoundaryOutcome {
981 sink_adapter: Option<String>,
982 force_fail: bool,
983}
984
985#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
986struct DeliveryRateWindowKey {
987 route_id: String,
988 recipient: String,
989 sink: String,
990 window_start_ms: u64,
991}
992
993#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
994struct MemoryConflictKey {
995 entity: String,
996 topic: String,
997 store: String,
998}
999
1000#[derive(Debug)]
1001pub struct MobkitRuntimeHandle {
1002 config: MobKitConfig,
1003 runtime_options: RuntimeOptions,
1004 loaded_modules: BTreeSet<String>,
1005 live_children: BTreeMap<String, Child>,
1006 pub lifecycle_events: Vec<LifecycleEvent>,
1007 pub supervisor_report: SupervisorReport,
1008 pub merged_events: Vec<EventEnvelope<UnifiedEvent>>,
1009 scheduling_claims: BTreeSet<String>,
1010 scheduling_claim_ticks: BTreeMap<u64, Vec<String>>,
1011 scheduling_last_due_ticks: BTreeMap<String, u64>,
1012 scheduling_dispatch_sequence: u64,
1013 routing_sequence: u64,
1014 routing_resolutions: BTreeMap<String, RoutingResolution>,
1015 routing_resolution_order: Vec<String>,
1016 runtime_routes: BTreeMap<String, RuntimeRoute>,
1017 delivery_sequence: u64,
1018 delivery_runtime_epoch_ms: u64,
1019 delivery_now_floor_ms: u64,
1020 delivery_clock_ms: u64,
1021 delivery_history: Vec<DeliveryRecord>,
1022 delivery_idempotency: BTreeMap<String, DeliveryIdempotencyEntry>,
1023 delivery_idempotency_by_delivery: BTreeMap<String, Vec<String>>,
1024 delivery_rate_window_counts: BTreeMap<DeliveryRateWindowKey, u32>,
1025 gating_sequence: u64,
1026 gating_pending: BTreeMap<String, GatingPendingEntry>,
1027 gating_pending_order: Vec<String>,
1028 gating_audit: Vec<GatingAuditEntry>,
1029 memory_sequence: u64,
1030 memory_assertions: Vec<MemoryAssertion>,
1031 memory_conflicts: BTreeMap<MemoryConflictKey, MemoryConflictSignal>,
1032 memory_backend: Option<ElephantMemoryStoreAdapter>,
1033 running: bool,
1034}
1035
1036impl MobkitRuntimeHandle {
1037 pub fn lifecycle_events(&self) -> &[LifecycleEvent] {
1038 &self.lifecycle_events
1039 }
1040
1041 pub fn supervisor_report(&self) -> &SupervisorReport {
1042 &self.supervisor_report
1043 }
1044
1045 #[doc(hidden)]
1046 pub fn inject_test_events(&mut self, events: Vec<EventEnvelope<UnifiedEvent>>) {
1047 for event in events {
1048 insert_event_sorted(&mut self.merged_events, event);
1049 }
1050 }
1051
1052 fn next_sequence(counter: &mut u64) -> u64 {
1053 let seq = *counter;
1054 *counter = counter.saturating_add(1);
1055 seq
1056 }
1057}
1058
1059#[derive(Debug, Clone, PartialEq, Eq)]
1060pub enum ScheduleValidationError {
1061 EmptyScheduleId,
1062 DuplicateScheduleId(String),
1063 InvalidTickMs(u64),
1064 InvalidInterval {
1065 schedule_id: String,
1066 interval: String,
1067 },
1068 InvalidTimezone {
1069 schedule_id: String,
1070 timezone: String,
1071 },
1072}
1073
1074impl std::fmt::Display for ScheduleValidationError {
1075 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1076 match self {
1077 Self::EmptyScheduleId => write!(f, "empty schedule id"),
1078 Self::DuplicateScheduleId(id) => write!(f, "duplicate schedule id: {id}"),
1079 Self::InvalidTickMs(ms) => write!(f, "invalid tick ms: {ms}"),
1080 Self::InvalidInterval {
1081 schedule_id,
1082 interval,
1083 } => {
1084 write!(f, "invalid interval for schedule {schedule_id}: {interval}")
1085 }
1086 Self::InvalidTimezone {
1087 schedule_id,
1088 timezone,
1089 } => {
1090 write!(f, "invalid timezone for schedule {schedule_id}: {timezone}")
1091 }
1092 }
1093 }
1094}
1095
1096impl std::error::Error for ScheduleValidationError {}
1097
1098#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1099pub struct ModuleRouteRequest {
1100 pub module_id: String,
1101 pub method: String,
1102 pub params: Value,
1103}
1104
1105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1106pub struct ModuleRouteResponse {
1107 pub module_id: String,
1108 pub method: String,
1109 pub payload: Value,
1110}
1111
1112#[derive(Debug, Clone, PartialEq, Eq)]
1113pub enum ModuleRouteError {
1114 UnloadedModule(String),
1115 ModuleRuntime(RuntimeBoundaryError),
1116 UnexpectedRouteResponse,
1117}
1118
1119impl std::fmt::Display for ModuleRouteError {
1120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1121 match self {
1122 Self::UnloadedModule(id) => write!(f, "unloaded module: {id}"),
1123 Self::ModuleRuntime(err) => write!(f, "module runtime: {err}"),
1124 Self::UnexpectedRouteResponse => write!(f, "unexpected route response"),
1125 }
1126 }
1127}
1128
1129impl std::error::Error for ModuleRouteError {
1130 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1131 match self {
1132 Self::ModuleRuntime(err) => Some(err),
1133 _ => None,
1134 }
1135 }
1136}
1137
1138#[derive(Debug, Clone, PartialEq, Eq)]
1139pub enum RoutingResolveError {
1140 RouterModuleNotLoaded,
1141 DeliveryModuleNotLoaded,
1142 EmptyRecipient,
1143 InvalidChannel,
1144 InvalidRateLimitPerMinute,
1145 RetryMaxExceedsCap { provided: u32, cap: u32 },
1146 RouterBoundary(RuntimeBoundaryError),
1147}
1148
1149impl std::fmt::Display for RoutingResolveError {
1150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1151 match self {
1152 Self::RouterModuleNotLoaded => write!(f, "router module not loaded"),
1153 Self::DeliveryModuleNotLoaded => write!(f, "delivery module not loaded"),
1154 Self::EmptyRecipient => write!(f, "empty recipient"),
1155 Self::InvalidChannel => write!(f, "invalid channel"),
1156 Self::InvalidRateLimitPerMinute => write!(f, "invalid rate limit per minute"),
1157 Self::RetryMaxExceedsCap { provided, cap } => {
1158 write!(f, "retry max {provided} exceeds cap {cap}")
1159 }
1160 Self::RouterBoundary(err) => write!(f, "router boundary: {err}"),
1161 }
1162 }
1163}
1164
1165impl std::error::Error for RoutingResolveError {
1166 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1167 match self {
1168 Self::RouterBoundary(err) => Some(err),
1169 _ => None,
1170 }
1171 }
1172}
1173
1174#[derive(Debug, Clone, PartialEq, Eq)]
1175pub enum DeliverySendError {
1176 DeliveryModuleNotLoaded,
1177 InvalidRouteTarget(String),
1178 InvalidRouteId,
1179 UnknownRouteId(String),
1180 ForgedResolution,
1181 InvalidRecipient,
1182 InvalidSink,
1183 InvalidIdempotencyKey,
1184 IdempotencyPayloadMismatch,
1185 RateLimited {
1186 sink: String,
1187 window_start_ms: u64,
1188 limit: u32,
1189 },
1190 DeliveryBoundary(RuntimeBoundaryError),
1191}
1192
1193impl std::fmt::Display for DeliverySendError {
1194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1195 match self {
1196 Self::DeliveryModuleNotLoaded => write!(f, "delivery module not loaded"),
1197 Self::InvalidRouteTarget(target) => write!(f, "invalid route target: {target}"),
1198 Self::InvalidRouteId => write!(f, "invalid route id"),
1199 Self::UnknownRouteId(id) => write!(f, "unknown route id: {id}"),
1200 Self::ForgedResolution => write!(f, "forged resolution"),
1201 Self::InvalidRecipient => write!(f, "invalid recipient"),
1202 Self::InvalidSink => write!(f, "invalid sink"),
1203 Self::InvalidIdempotencyKey => write!(f, "invalid idempotency key"),
1204 Self::IdempotencyPayloadMismatch => write!(f, "idempotency payload mismatch"),
1205 Self::RateLimited {
1206 sink,
1207 window_start_ms,
1208 limit,
1209 } => {
1210 write!(
1211 f,
1212 "rate limited on sink {sink} (window {window_start_ms}ms, limit {limit})"
1213 )
1214 }
1215 Self::DeliveryBoundary(err) => write!(f, "delivery boundary: {err}"),
1216 }
1217 }
1218}
1219
1220impl std::error::Error for DeliverySendError {
1221 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1222 match self {
1223 Self::DeliveryBoundary(err) => Some(err),
1224 _ => None,
1225 }
1226 }
1227}
1228
1229#[derive(Debug, Clone, PartialEq, Eq)]
1230pub enum RuntimeRouteMutationError {
1231 EmptyRouteKey,
1232 EmptyRecipient,
1233 InvalidChannel,
1234 EmptySink,
1235 EmptyTargetModule,
1236 InvalidRateLimitPerMinute,
1237 RetryMaxExceedsCap { provided: u32, cap: u32 },
1238 RouteNotFound(String),
1239}
1240
1241impl std::fmt::Display for RuntimeRouteMutationError {
1242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1243 match self {
1244 Self::EmptyRouteKey => write!(f, "empty route key"),
1245 Self::EmptyRecipient => write!(f, "empty recipient"),
1246 Self::InvalidChannel => write!(f, "invalid channel"),
1247 Self::EmptySink => write!(f, "empty sink"),
1248 Self::EmptyTargetModule => write!(f, "empty target module"),
1249 Self::InvalidRateLimitPerMinute => write!(f, "invalid rate limit per minute"),
1250 Self::RetryMaxExceedsCap { provided, cap } => {
1251 write!(f, "retry max {provided} exceeds cap {cap}")
1252 }
1253 Self::RouteNotFound(key) => write!(f, "route not found: {key}"),
1254 }
1255 }
1256}
1257
1258impl std::error::Error for RuntimeRouteMutationError {}
1259
1260#[derive(Debug, Clone, PartialEq, Eq)]
1261pub enum RpcRouteError {
1262 InvalidRequest,
1263 BoundaryProcess(ProcessBoundaryError),
1264 Route(ModuleRouteError),
1265 InvalidResponse,
1266}
1267
1268impl std::fmt::Display for RpcRouteError {
1269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1270 match self {
1271 Self::InvalidRequest => write!(f, "invalid request"),
1272 Self::BoundaryProcess(err) => write!(f, "boundary process: {err}"),
1273 Self::Route(err) => write!(f, "route: {err}"),
1274 Self::InvalidResponse => write!(f, "invalid response"),
1275 }
1276 }
1277}
1278
1279impl std::error::Error for RpcRouteError {
1280 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1281 match self {
1282 Self::BoundaryProcess(err) => Some(err),
1283 Self::Route(err) => Some(err),
1284 _ => None,
1285 }
1286 }
1287}
1288
1289#[derive(Debug, Clone, PartialEq, Eq)]
1290pub enum RuntimeMutationError {
1291 Config(ConfigResolutionError),
1292 Runtime(RuntimeBoundaryError),
1293}
1294
1295impl std::fmt::Display for RuntimeMutationError {
1296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1297 match self {
1298 Self::Config(err) => write!(f, "config resolution: {err}"),
1299 Self::Runtime(err) => write!(f, "runtime boundary: {err}"),
1300 }
1301 }
1302}
1303
1304impl std::error::Error for RuntimeMutationError {
1305 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1306 match self {
1307 Self::Config(err) => Some(err),
1308 Self::Runtime(err) => Some(err),
1309 }
1310 }
1311}
1312
1313#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1314#[serde(rename_all = "snake_case")]
1315pub enum SubscribeScope {
1316 Mob,
1317 Agent,
1318 Interaction,
1319}
1320
1321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1322pub struct SubscribeRequest {
1323 pub scope: SubscribeScope,
1324 pub last_event_id: Option<String>,
1325 pub agent_id: Option<String>,
1326}
1327
1328impl Default for SubscribeRequest {
1329 fn default() -> Self {
1330 Self {
1331 scope: SubscribeScope::Mob,
1332 last_event_id: None,
1333 agent_id: None,
1334 }
1335 }
1336}
1337
1338#[derive(Debug, Clone, PartialEq, Eq)]
1339pub enum SubscribeError {
1340 EmptyCheckpoint,
1341 UnknownCheckpoint(String),
1342 MissingAgentId,
1343 InvalidAgentId,
1344}
1345
1346impl std::fmt::Display for SubscribeError {
1347 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1348 match self {
1349 Self::EmptyCheckpoint => write!(f, "empty checkpoint"),
1350 Self::UnknownCheckpoint(id) => write!(f, "unknown checkpoint: {id}"),
1351 Self::MissingAgentId => write!(f, "missing agent id"),
1352 Self::InvalidAgentId => write!(f, "invalid agent id"),
1353 }
1354 }
1355}
1356
1357impl std::error::Error for SubscribeError {}
1358
1359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1360pub struct SubscribeKeepAlive {
1361 pub interval_ms: u64,
1362 pub event: String,
1363}
1364
1365#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1366pub struct SubscribeResponse {
1367 pub scope: SubscribeScope,
1368 pub replay_from_event_id: Option<String>,
1369 pub keep_alive: SubscribeKeepAlive,
1370 pub keep_alive_comment: String,
1371 pub event_frames: Vec<String>,
1372 pub events: Vec<EventEnvelope<UnifiedEvent>>,
1373}
1374
1375const SSE_KEEP_ALIVE_INTERVAL_MS: u64 = 15_000;
1376const SSE_KEEP_ALIVE_EVENT_NAME: &str = "keep-alive";
1377const SSE_KEEP_ALIVE_COMMENT_FRAME: &str = ": keep-alive\n\n";
1378const SUBSCRIBE_REPLAY_EVENT_CAP: usize = 3;
1379const SCHEDULING_CLAIM_RETENTION_WINDOW_MS: u64 = 86_400_000;
1380const SCHEDULING_CLAIMS_MAX_RETAINED: usize = 4_096;
1381const SCHEDULING_LAST_DUE_MAX_RETAINED: usize = 4_096;
1382const DELIVERY_HISTORY_LIMIT_DEFAULT: usize = 20;
1383const DELIVERY_HISTORY_LIMIT_MAX: usize = 200;
1384const ROUTING_RESOLUTION_LIMIT_MAX: usize = 512;
1385pub const ROUTING_RETRY_MAX_CAP: u32 = 10;
1386const DELIVERY_RATE_WINDOW_MS: u64 = 60_000;
1387const DELIVERY_RATE_WINDOWS_RETAINED: u64 = 2;
1388const DELIVERY_CLOCK_STEP_MS: u64 = 1_000;
1389const GATING_APPROVAL_TIMEOUT_DEFAULT_MS: u64 = 60_000;
1390const GATING_AUDIT_MAX_RETAINED: usize = 512;
1391const GATING_PENDING_MAX_RETAINED: usize = 512;
1392const MEMORY_ASSERTIONS_MAX_RETAINED: usize = 4_096;
1393const MEMORY_SUPPORTED_STORES: [&str; 5] = [
1394 "knowledge_graph",
1395 "vector",
1396 "timeline",
1397 "todo",
1398 "top_of_mind",
1399];
1400const ELEPHANT_HEALTHCHECK_TIMEOUT: Duration = Duration::from_secs(2);
1401const CRON_LOOKBACK_MINUTES: u64 = 5_270_400;
1404const CONSOLE_EXPERIENCE_ROUTE: &str = "/console/experience";
1405const CONSOLE_MODULES_ROUTE: &str = "/console/modules";
1406const EVENTS_SUBSCRIBE_METHOD: &str = "mobkit/events/subscribe";
1407
1408fn default_delivery_history_limit() -> usize {
1409 DELIVERY_HISTORY_LIMIT_DEFAULT
1410}
1411
1412pub fn run_meerkat_baseline_verification_once(
1413 command: &str,
1414 args: &[String],
1415 env: &[(String, String)],
1416 timeout: Duration,
1417) -> Result<BaselineVerificationReport, BaselineRuntimeError> {
1418 let line = run_process_json_line(command, args, env, timeout)
1419 .map_err(BaselineRuntimeError::Process)?;
1420 let value: Value =
1421 serde_json::from_str(&line).map_err(|_| BaselineRuntimeError::InvalidRepoPathJson)?;
1422 let repo = value
1423 .as_object()
1424 .and_then(|obj| obj.get("repo_root"))
1425 .ok_or(BaselineRuntimeError::MissingRepoRoot)?
1426 .as_str()
1427 .ok_or(BaselineRuntimeError::InvalidRepoRoot)?;
1428 if repo.trim().is_empty() {
1429 return Err(BaselineRuntimeError::InvalidRepoRoot);
1430 }
1431 verify_meerkat_baseline_symbols(Some(std::path::Path::new(repo)))
1432 .map_err(BaselineRuntimeError::Baseline)
1433}
1434
1435pub fn build_runtime_decision_state(
1436 input: RuntimeDecisionInputs,
1437) -> Result<RuntimeDecisionState, DecisionRuntimeError> {
1438 validate_bigquery_naming(&input.bigquery).map_err(DecisionRuntimeError::Policy)?;
1439 let modules = load_trusted_mobkit_modules_from_toml(&input.trusted_mobkit_toml)
1440 .map_err(DecisionRuntimeError::Policy)?;
1441 validate_runtime_ops_policy(&input.ops).map_err(DecisionRuntimeError::Policy)?;
1442 let release_metadata = parse_release_metadata_json(&input.release_metadata_json)
1443 .map_err(DecisionRuntimeError::Policy)?;
1444 validate_release_metadata(&release_metadata).map_err(DecisionRuntimeError::Policy)?;
1445 if input.trusted_oidc.audience.trim().is_empty() {
1446 return Err(DecisionRuntimeError::Policy(
1447 DecisionPolicyError::InvalidTrustedAuthConfig(
1448 "trusted OIDC audience must not be empty".to_string(),
1449 ),
1450 ));
1451 }
1452 parse_oidc_discovery_json(&input.trusted_oidc.discovery_json).map_err(|err| {
1453 DecisionRuntimeError::Policy(DecisionPolicyError::InvalidTrustedAuthConfig(format!(
1454 "invalid trusted OIDC discovery: {err:?}"
1455 )))
1456 })?;
1457 parse_jwks_json(&input.trusted_oidc.jwks_json).map_err(|err| {
1458 DecisionRuntimeError::Policy(DecisionPolicyError::InvalidTrustedAuthConfig(format!(
1459 "invalid trusted JWKS: {err:?}"
1460 )))
1461 })?;
1462
1463 Ok(RuntimeDecisionState {
1464 bigquery: input.bigquery,
1465 modules,
1466 auth: input.auth,
1467 trusted_oidc: input.trusted_oidc,
1468 console: input.console,
1469 ops: input.ops,
1470 release_metadata,
1471 })
1472}
1473
1474fn current_time_ms() -> u64 {
1475 SystemTime::now()
1476 .duration_since(UNIX_EPOCH)
1477 .unwrap_or_default()
1478 .as_millis() as u64
1479}