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