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