Skip to main content

meerkat_mobkit/
rpc.rs

1//! JSON-RPC request handling for both module-only and unified runtime modes.
2
3use std::collections::BTreeMap;
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9use crate::runtime::{
10    BigQuerySessionStoreAdapter, BigQuerySessionStoreError, ConsoleRestJsonRequest,
11    ConsoleRestJsonResponse, DeliveryHistoryRequest, DeliverySendError, DeliverySendRequest,
12    ElephantMemoryStoreError, GatingDecideError, GatingDecideRequest, GatingDecision,
13    GatingEvaluateRequest, GatingRiskTier, MemoryIndexError, MemoryIndexRequest,
14    MemoryQueryRequest, MobkitRuntimeHandle, ModuleRouteError, ModuleRouteRequest,
15    ROUTING_RETRY_MAX_CAP, RoutingResolveError, RoutingResolveRequest, RuntimeDecisionState,
16    RuntimeRoute, RuntimeRouteMutationError, ScheduleDefinition, ScheduleValidationError,
17    SessionPersistenceRow, SubscribeError, SubscribeRequest, SubscribeScope,
18    handle_console_rest_json_route, route_module_call, validate_schedules,
19};
20use crate::unified_runtime::{EventQuery, UnifiedRuntime};
21
22mod console_ingress;
23mod gating_methods;
24mod memory_methods;
25pub(crate) mod mob_methods;
26pub(crate) mod params;
27mod routing_delivery_methods;
28mod scheduling_methods;
29mod session_store_methods;
30mod subscribe_methods;
31
32pub use console_ingress::handle_console_ingress_json;
33
34use gating_methods::{
35    GatingParamsError, parse_gating_audit_params, parse_gating_decide_params,
36    parse_gating_evaluate_params, parse_gating_pending_params,
37};
38use memory_methods::{
39    MemoryParamsError, parse_memory_index_params, parse_memory_query_params,
40    parse_memory_stores_params,
41};
42use routing_delivery_methods::{
43    RoutingDeliveryParamsError, parse_delivery_history_params, parse_delivery_send_params,
44    parse_routing_resolve_params, parse_routing_route_add_params,
45    parse_routing_route_delete_params, parse_routing_routes_list_params,
46};
47use scheduling_methods::{format_schedule_validation_error, parse_scheduling_params};
48use session_store_methods::{
49    BigQuerySessionStoreRpcError, format_bigquery_store_error, parse_bigquery_session_store_params,
50    run_bigquery_session_store_request,
51};
52use subscribe_methods::{SubscribeParamsError, parse_subscribe_request};
53
54pub const JSONRPC_VERSION: &str = "2.0";
55pub const MOBKIT_CONTRACT_VERSION: &str = "0.3.0";
56pub const MAX_SCHEDULES_PER_REQUEST: usize = 256;
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum RpcCapabilitiesError {
60    InvalidJson,
61    InvalidSchema,
62    MissingContractVersion,
63    InvalidContractVersion,
64}
65
66impl std::fmt::Display for RpcCapabilitiesError {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::InvalidJson => write!(f, "invalid JSON"),
70            Self::InvalidSchema => write!(f, "invalid schema"),
71            Self::MissingContractVersion => write!(f, "missing contract version"),
72            Self::InvalidContractVersion => write!(f, "invalid contract version"),
73        }
74    }
75}
76
77impl std::error::Error for RpcCapabilitiesError {}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct RpcCapabilities {
81    pub contract_version: String,
82    #[serde(flatten)]
83    pub extra: BTreeMap<String, Value>,
84}
85
86pub fn parse_rpc_capabilities(line: &str) -> Result<RpcCapabilities, RpcCapabilitiesError> {
87    let raw: Value = serde_json::from_str(line).map_err(|_| RpcCapabilitiesError::InvalidJson)?;
88    let object = raw.as_object().ok_or(RpcCapabilitiesError::InvalidSchema)?;
89    let contract = object
90        .get("contract_version")
91        .ok_or(RpcCapabilitiesError::MissingContractVersion)?;
92    let contract_str = contract
93        .as_str()
94        .ok_or(RpcCapabilitiesError::InvalidContractVersion)?;
95    if contract_str.trim().is_empty() {
96        return Err(RpcCapabilitiesError::InvalidContractVersion);
97    }
98    serde_json::from_value(raw).map_err(|_| RpcCapabilitiesError::InvalidSchema)
99}
100
101/// JSON-RPC error code returned by `mobkit/mob_events/{query,subscribe}`
102/// when the caller's `after_seq` is past the current ledger frontier.
103/// The error `data` field carries `{ after_cursor, latest_cursor }` so
104/// SDKs can surface a typed exception. Single source of truth — keep
105/// this in sync with `MobEventsStaleError` in the Python and TypeScript
106/// SDKs.
107pub const MOB_EVENTS_STALE_CURSOR_CODE: i64 = -32010;
108
109/// JSON-RPC error code returned by `mobkit/memory/index` and
110/// `mobkit/memory/query` when the configured memory backend cannot
111/// persist or retrieve the row. Distinct from
112/// [`MOB_EVENTS_STALE_CURSOR_CODE`] so SDKs can branch on `-32010`
113/// without misclassifying a memory backend failure as a stale-cursor
114/// event.
115pub const MEMORY_BACKEND_UNAVAILABLE_CODE: i64 = -32012;
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct JsonRpcRequest {
119    pub jsonrpc: String,
120    #[serde(default)]
121    pub id: Option<Value>,
122    pub method: String,
123    #[serde(default)]
124    pub params: Value,
125}
126
127#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
128pub struct JsonRpcError {
129    pub code: i64,
130    pub message: String,
131    /// Optional structured payload as defined by JSON-RPC 2.0. Used by
132    /// typed errors (e.g. `event_query_stale` with `after_cursor` /
133    /// `latest_cursor`) so SDKs can surface a typed exception. Existing
134    /// construction sites can omit it via `..Default::default()`.
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub data: Option<Value>,
137}
138
139impl JsonRpcError {
140    pub fn new(code: i64, message: impl Into<String>) -> Self {
141        Self {
142            code,
143            message: message.into(),
144            data: None,
145        }
146    }
147
148    #[must_use]
149    pub fn with_data(mut self, data: Value) -> Self {
150        self.data = Some(data);
151        self
152    }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct JsonRpcResponse {
157    pub jsonrpc: String,
158    pub id: Value,
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub result: Option<Value>,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub error: Option<JsonRpcError>,
163}
164
165pub fn handle_mobkit_rpc_json(
166    runtime: &mut MobkitRuntimeHandle,
167    request_json: &str,
168    timeout: Duration,
169) -> String {
170    let raw_request: Value = match serde_json::from_str(request_json) {
171        Ok(raw_request) => raw_request,
172        Err(_) => {
173            return serialize_response(&JsonRpcResponse {
174                jsonrpc: JSONRPC_VERSION.to_string(),
175                id: Value::Null,
176                result: None,
177                error: Some(JsonRpcError {
178                    code: -32700,
179                    message: "Parse error".to_string(),
180                    data: None,
181                }),
182            });
183        }
184    };
185    let response_id = raw_request
186        .as_object()
187        .and_then(|object| object.get("id"))
188        .cloned()
189        .unwrap_or(Value::Null);
190    let request: JsonRpcRequest = match serde_json::from_value(raw_request) {
191        Ok(request) => request,
192        Err(_) => {
193            return serialize_response(&JsonRpcResponse {
194                jsonrpc: JSONRPC_VERSION.to_string(),
195                id: response_id,
196                result: None,
197                error: Some(JsonRpcError {
198                    code: -32600,
199                    message: "Invalid Request".to_string(),
200                    data: None,
201                }),
202            });
203        }
204    };
205    let is_notification = request.id.is_none();
206    let response_id = request.id.clone().unwrap_or(Value::Null);
207
208    if request.jsonrpc != "2.0" {
209        let response = JsonRpcResponse {
210            jsonrpc: JSONRPC_VERSION.to_string(),
211            id: response_id,
212            result: None,
213            error: Some(JsonRpcError {
214                code: -32600,
215                message: "Invalid Request".to_string(),
216                data: None,
217            }),
218        };
219        return if is_notification {
220            String::new()
221        } else {
222            serialize_response(&response)
223        };
224    }
225
226    let response = match request.method.as_str() {
227        "mobkit/status" => JsonRpcResponse {
228            jsonrpc: JSONRPC_VERSION.to_string(),
229            id: response_id,
230            result: Some(serde_json::json!({
231                "contract_version": MOBKIT_CONTRACT_VERSION,
232                "running": runtime.is_running(),
233                "loaded_modules": runtime.loaded_modules(),
234            })),
235            error: None,
236        },
237        "mobkit/capabilities" => JsonRpcResponse {
238            jsonrpc: JSONRPC_VERSION.to_string(),
239            id: response_id,
240            result: Some(serde_json::json!({
241                "contract_version": MOBKIT_CONTRACT_VERSION,
242                "methods": [
243                    "mobkit/status",
244                    "mobkit/capabilities",
245                    "mobkit/reconcile",
246                    "mobkit/spawn_member",
247                    "mobkit/scheduling/evaluate",
248                    "mobkit/scheduling/dispatch",
249                    "mobkit/routing/resolve",
250                    "mobkit/routing/routes/list",
251                    "mobkit/routing/routes/add",
252                    "mobkit/routing/routes/delete",
253                    "mobkit/delivery/send",
254                    "mobkit/delivery/history",
255                    "mobkit/events/subscribe",
256                    "mobkit/memory/stores",
257                    "mobkit/memory/index",
258                    "mobkit/memory/query",
259                    "mobkit/session_store/bigquery",
260                    "mobkit/gating/evaluate",
261                    "mobkit/gating/pending",
262                    "mobkit/gating/decide",
263                    "mobkit/gating/audit",
264                    "mobkit/call_tool",
265                    "mobkit/models/catalog"
266                ],
267                "loaded_modules": runtime.loaded_modules(),
268                "runtime_capabilities": {
269                    "can_spawn_members": false,
270                    "can_send_messages": false,
271                    "can_wire_members": false,
272                    "can_retire_members": false,
273                    "available_spawn_modes": ["module"],
274                }
275            })),
276            error: None,
277        },
278        "mobkit/models/catalog" => JsonRpcResponse {
279            jsonrpc: JSONRPC_VERSION.to_string(),
280            id: response_id,
281            result: Some(build_models_catalog_result()),
282            error: None,
283        },
284        "mobkit/reconcile" => {
285            let modules = match params::required_string_array(&request.params, "modules") {
286                Ok(m) => m,
287                Err(reason) => {
288                    return serialize_response(&JsonRpcResponse {
289                        jsonrpc: JSONRPC_VERSION.to_string(),
290                        id: response_id,
291                        result: None,
292                        error: Some(JsonRpcError {
293                            code: -32602,
294                            message: format!("Invalid params: {reason}"),
295                            data: None,
296                        }),
297                    });
298                }
299            };
300
301            match runtime.reconcile_modules(modules.clone(), timeout) {
302                Ok(added) => JsonRpcResponse {
303                    jsonrpc: JSONRPC_VERSION.to_string(),
304                    id: response_id,
305                    result: Some(serde_json::json!({
306                        "accepted": true,
307                        "reconciled_modules": modules,
308                        "added": added
309                    })),
310                    error: None,
311                },
312                Err(err) => JsonRpcResponse {
313                    jsonrpc: JSONRPC_VERSION.to_string(),
314                    id: response_id,
315                    result: None,
316                    error: Some(JsonRpcError {
317                        code: -32602,
318                        message: format!("Invalid params: {err:?}"),
319                        data: None,
320                    }),
321                },
322            }
323        }
324        "mobkit/spawn_member" => {
325            let module_id = request
326                .params
327                .get("module_id")
328                .and_then(Value::as_str)
329                .unwrap_or_default()
330                .to_string();
331            if module_id.is_empty() {
332                JsonRpcResponse {
333                    jsonrpc: JSONRPC_VERSION.to_string(),
334                    id: response_id,
335                    result: None,
336                    error: Some(JsonRpcError {
337                        code: -32602,
338                        message: "Invalid params: module_id required".to_string(),
339                        data: None,
340                    }),
341                }
342            } else {
343                match runtime.spawn_member(&module_id, timeout) {
344                    Ok(()) => JsonRpcResponse {
345                        jsonrpc: JSONRPC_VERSION.to_string(),
346                        id: response_id,
347                        result: Some(serde_json::json!({
348                            "accepted": true,
349                            "module_id": module_id
350                        })),
351                        error: None,
352                    },
353                    Err(err) => JsonRpcResponse {
354                        jsonrpc: JSONRPC_VERSION.to_string(),
355                        id: response_id,
356                        result: None,
357                        error: Some(JsonRpcError {
358                            code: -32602,
359                            message: format!("Invalid params: {err:?}"),
360                            data: None,
361                        }),
362                    },
363                }
364            }
365        }
366        "mobkit/scheduling/evaluate" => match parse_scheduling_params(&request.params) {
367            Ok((schedules, tick_ms)) => match runtime.evaluate_schedule_tick(&schedules, tick_ms) {
368                Ok(evaluation) => JsonRpcResponse {
369                    jsonrpc: JSONRPC_VERSION.to_string(),
370                    id: response_id,
371                    result: Some(serde_json::to_value(evaluation).unwrap_or(Value::Null)),
372                    error: None,
373                },
374                Err(err) => JsonRpcResponse {
375                    jsonrpc: JSONRPC_VERSION.to_string(),
376                    id: response_id,
377                    result: None,
378                    error: Some(JsonRpcError {
379                        code: -32602,
380                        message: format!(
381                            "Invalid params: {}",
382                            format_schedule_validation_error(err)
383                        ),
384                        data: None,
385                    }),
386                },
387            },
388            Err(message) => JsonRpcResponse {
389                jsonrpc: JSONRPC_VERSION.to_string(),
390                id: response_id,
391                result: None,
392                error: Some(JsonRpcError {
393                    code: -32602,
394                    message: format!("Invalid params: {message}"),
395                    data: None,
396                }),
397            },
398        },
399        "mobkit/scheduling/dispatch" => match parse_scheduling_params(&request.params) {
400            Ok((schedules, tick_ms)) => match runtime.dispatch_schedule_tick(&schedules, tick_ms) {
401                Ok(dispatch) => JsonRpcResponse {
402                    jsonrpc: JSONRPC_VERSION.to_string(),
403                    id: response_id,
404                    result: Some(serde_json::to_value(dispatch).unwrap_or(Value::Null)),
405                    error: None,
406                },
407                Err(err) => JsonRpcResponse {
408                    jsonrpc: JSONRPC_VERSION.to_string(),
409                    id: response_id,
410                    result: None,
411                    error: Some(JsonRpcError {
412                        code: -32602,
413                        message: format!(
414                            "Invalid params: {}",
415                            format_schedule_validation_error(err)
416                        ),
417                        data: None,
418                    }),
419                },
420            },
421            Err(message) => JsonRpcResponse {
422                jsonrpc: JSONRPC_VERSION.to_string(),
423                id: response_id,
424                result: None,
425                error: Some(JsonRpcError {
426                    code: -32602,
427                    message: format!("Invalid params: {message}"),
428                    data: None,
429                }),
430            },
431        },
432        "mobkit/routing/resolve" => {
433            match parse_routing_resolve_params(&request.params).and_then(|resolve_request| {
434                runtime
435                    .resolve_routing(resolve_request)
436                    .map_err(RoutingDeliveryParamsError::Routing)
437            }) {
438                Ok(resolution) => JsonRpcResponse {
439                    jsonrpc: JSONRPC_VERSION.to_string(),
440                    id: response_id,
441                    result: Some(serde_json::to_value(resolution).unwrap_or(Value::Null)),
442                    error: None,
443                },
444                Err(err) => JsonRpcResponse {
445                    jsonrpc: JSONRPC_VERSION.to_string(),
446                    id: response_id,
447                    result: None,
448                    error: Some(JsonRpcError {
449                        code: -32602,
450                        message: format!("Invalid params: {}", err.message()),
451                        data: None,
452                    }),
453                },
454            }
455        }
456        "mobkit/routing/routes/list" => match parse_routing_routes_list_params(&request.params) {
457            Ok(()) => JsonRpcResponse {
458                jsonrpc: JSONRPC_VERSION.to_string(),
459                id: response_id,
460                result: Some(serde_json::json!({
461                    "routes": runtime.list_runtime_routes()
462                })),
463                error: None,
464            },
465            Err(err) => JsonRpcResponse {
466                jsonrpc: JSONRPC_VERSION.to_string(),
467                id: response_id,
468                result: None,
469                error: Some(JsonRpcError {
470                    code: -32602,
471                    message: format!("Invalid params: {}", err.message()),
472                    data: None,
473                }),
474            },
475        },
476        "mobkit/routing/routes/add" => match parse_routing_route_add_params(&request.params)
477            .and_then(|route| {
478                runtime
479                    .add_runtime_route(route)
480                    .map_err(RoutingDeliveryParamsError::RouteMutation)
481            }) {
482            Ok(route) => JsonRpcResponse {
483                jsonrpc: JSONRPC_VERSION.to_string(),
484                id: response_id,
485                result: Some(serde_json::json!({ "route": route })),
486                error: None,
487            },
488            Err(err) => JsonRpcResponse {
489                jsonrpc: JSONRPC_VERSION.to_string(),
490                id: response_id,
491                result: None,
492                error: Some(JsonRpcError {
493                    code: -32602,
494                    message: format!("Invalid params: {}", err.message()),
495                    data: None,
496                }),
497            },
498        },
499        "mobkit/routing/routes/delete" => match parse_routing_route_delete_params(&request.params)
500            .and_then(|route_key| {
501                runtime
502                    .delete_runtime_route(&route_key)
503                    .map_err(RoutingDeliveryParamsError::RouteMutation)
504            }) {
505            Ok(route) => JsonRpcResponse {
506                jsonrpc: JSONRPC_VERSION.to_string(),
507                id: response_id,
508                result: Some(serde_json::json!({ "deleted": route })),
509                error: None,
510            },
511            Err(err) => JsonRpcResponse {
512                jsonrpc: JSONRPC_VERSION.to_string(),
513                id: response_id,
514                result: None,
515                error: Some(JsonRpcError {
516                    code: -32602,
517                    message: format!("Invalid params: {}", err.message()),
518                    data: None,
519                }),
520            },
521        },
522        "mobkit/delivery/send" => {
523            match parse_delivery_send_params(&request.params).and_then(|send_request| {
524                runtime
525                    .send_delivery(send_request)
526                    .map_err(RoutingDeliveryParamsError::Delivery)
527            }) {
528                Ok(record) => JsonRpcResponse {
529                    jsonrpc: JSONRPC_VERSION.to_string(),
530                    id: response_id,
531                    result: Some(serde_json::to_value(record).unwrap_or(Value::Null)),
532                    error: None,
533                },
534                Err(err) => JsonRpcResponse {
535                    jsonrpc: JSONRPC_VERSION.to_string(),
536                    id: response_id,
537                    result: None,
538                    error: Some(JsonRpcError {
539                        code: -32602,
540                        message: format!("Invalid params: {}", err.message()),
541                        data: None,
542                    }),
543                },
544            }
545        }
546        "mobkit/delivery/history" => match parse_delivery_history_params(&request.params) {
547            Ok(history_request) => JsonRpcResponse {
548                jsonrpc: JSONRPC_VERSION.to_string(),
549                id: response_id,
550                result: Some(
551                    serde_json::to_value(runtime.delivery_history(history_request))
552                        .unwrap_or(Value::Null),
553                ),
554                error: None,
555            },
556            Err(err) => JsonRpcResponse {
557                jsonrpc: JSONRPC_VERSION.to_string(),
558                id: response_id,
559                result: None,
560                error: Some(JsonRpcError {
561                    code: -32602,
562                    message: format!("Invalid params: {}", err.message()),
563                    data: None,
564                }),
565            },
566        },
567        "mobkit/events/subscribe" => {
568            match parse_subscribe_request(&request.params).and_then(|subscribe_request| {
569                runtime
570                    .subscribe_events(subscribe_request)
571                    .map_err(SubscribeParamsError::Runtime)
572            }) {
573                Ok(subscribe_result) => JsonRpcResponse {
574                    jsonrpc: JSONRPC_VERSION.to_string(),
575                    id: response_id,
576                    result: Some(serde_json::to_value(subscribe_result).unwrap_or(Value::Null)),
577                    error: None,
578                },
579                Err(err) => JsonRpcResponse {
580                    jsonrpc: JSONRPC_VERSION.to_string(),
581                    id: response_id,
582                    result: None,
583                    error: Some(JsonRpcError {
584                        code: -32602,
585                        message: format!("Invalid params: {}", err.message()),
586                        data: None,
587                    }),
588                },
589            }
590        }
591        "mobkit/memory/stores" => match parse_memory_stores_params(&request.params) {
592            Ok(()) => JsonRpcResponse {
593                jsonrpc: JSONRPC_VERSION.to_string(),
594                id: response_id,
595                result: Some(serde_json::json!({
596                    "stores": runtime.memory_stores(),
597                })),
598                error: None,
599            },
600            Err(err) => JsonRpcResponse {
601                jsonrpc: JSONRPC_VERSION.to_string(),
602                id: response_id,
603                result: None,
604                error: Some(JsonRpcError {
605                    code: -32602,
606                    message: format!("Invalid params: {}", err.message()),
607                    data: None,
608                }),
609            },
610        },
611        "mobkit/memory/index" => match parse_memory_index_params(&request.params) {
612            Ok(index_request) => match runtime.memory_index(index_request) {
613                Ok(indexed) => JsonRpcResponse {
614                    jsonrpc: JSONRPC_VERSION.to_string(),
615                    id: response_id,
616                    result: Some(serde_json::to_value(indexed).unwrap_or(Value::Null)),
617                    error: None,
618                },
619                Err(MemoryIndexError::BackendPersistFailed(error)) => JsonRpcResponse {
620                    jsonrpc: JSONRPC_VERSION.to_string(),
621                    id: response_id,
622                    result: None,
623                    error: Some(JsonRpcError {
624                        code: MEMORY_BACKEND_UNAVAILABLE_CODE,
625                        message: format!(
626                            "Memory backend unavailable: {}",
627                            MemoryParamsError::backend_message(&error)
628                        ),
629                        data: None,
630                    }),
631                },
632                Err(err) => JsonRpcResponse {
633                    jsonrpc: JSONRPC_VERSION.to_string(),
634                    id: response_id,
635                    result: None,
636                    error: Some(JsonRpcError {
637                        code: -32602,
638                        message: format!(
639                            "Invalid params: {}",
640                            MemoryParamsError::Index(err).message()
641                        ),
642                        data: None,
643                    }),
644                },
645            },
646            Err(err) => JsonRpcResponse {
647                jsonrpc: JSONRPC_VERSION.to_string(),
648                id: response_id,
649                result: None,
650                error: Some(JsonRpcError {
651                    code: -32602,
652                    message: format!("Invalid params: {}", err.message()),
653                    data: None,
654                }),
655            },
656        },
657        "mobkit/memory/query" => match parse_memory_query_params(&request.params) {
658            Ok(query_request) => JsonRpcResponse {
659                jsonrpc: JSONRPC_VERSION.to_string(),
660                id: response_id,
661                result: Some(
662                    serde_json::to_value(runtime.memory_query(query_request))
663                        .unwrap_or(Value::Null),
664                ),
665                error: None,
666            },
667            Err(err) => JsonRpcResponse {
668                jsonrpc: JSONRPC_VERSION.to_string(),
669                id: response_id,
670                result: None,
671                error: Some(JsonRpcError {
672                    code: -32602,
673                    message: format!("Invalid params: {}", err.message()),
674                    data: None,
675                }),
676            },
677        },
678        "mobkit/session_store/bigquery" => {
679            match parse_bigquery_session_store_params(&request.params)
680                .and_then(run_bigquery_session_store_request)
681            {
682                Ok(result) => JsonRpcResponse {
683                    jsonrpc: JSONRPC_VERSION.to_string(),
684                    id: response_id,
685                    result: Some(result),
686                    error: None,
687                },
688                Err(BigQuerySessionStoreRpcError::Params(message)) => JsonRpcResponse {
689                    jsonrpc: JSONRPC_VERSION.to_string(),
690                    id: response_id,
691                    result: None,
692                    error: Some(JsonRpcError {
693                        code: -32602,
694                        message: format!("Invalid params: {message}"),
695                        data: None,
696                    }),
697                },
698                Err(BigQuerySessionStoreRpcError::Store(error)) => JsonRpcResponse {
699                    jsonrpc: JSONRPC_VERSION.to_string(),
700                    id: response_id,
701                    result: None,
702                    error: Some(JsonRpcError {
703                        code: -32011,
704                        message: format!(
705                            "BigQuery session store request failed: {}",
706                            format_bigquery_store_error(&error)
707                        ),
708                        data: None,
709                    }),
710                },
711            }
712        }
713        "mobkit/gating/evaluate" => match parse_gating_evaluate_params(&request.params) {
714            Ok(gating_request) => JsonRpcResponse {
715                jsonrpc: JSONRPC_VERSION.to_string(),
716                id: response_id,
717                result: Some(
718                    serde_json::to_value(runtime.evaluate_gating_action(gating_request))
719                        .unwrap_or(Value::Null),
720                ),
721                error: None,
722            },
723            Err(err) => JsonRpcResponse {
724                jsonrpc: JSONRPC_VERSION.to_string(),
725                id: response_id,
726                result: None,
727                error: Some(JsonRpcError {
728                    code: -32602,
729                    message: format!("Invalid params: {}", err.message()),
730                    data: None,
731                }),
732            },
733        },
734        "mobkit/gating/pending" => match parse_gating_pending_params(&request.params) {
735            Ok(()) => JsonRpcResponse {
736                jsonrpc: JSONRPC_VERSION.to_string(),
737                id: response_id,
738                result: Some(serde_json::json!({
739                    "pending": runtime.list_gating_pending(),
740                })),
741                error: None,
742            },
743            Err(err) => JsonRpcResponse {
744                jsonrpc: JSONRPC_VERSION.to_string(),
745                id: response_id,
746                result: None,
747                error: Some(JsonRpcError {
748                    code: -32602,
749                    message: format!("Invalid params: {}", err.message()),
750                    data: None,
751                }),
752            },
753        },
754        "mobkit/gating/decide" => {
755            match parse_gating_decide_params(&request.params).and_then(|decide_request| {
756                runtime
757                    .decide_gating_action(decide_request)
758                    .map_err(GatingParamsError::Decision)
759            }) {
760                Ok(result) => JsonRpcResponse {
761                    jsonrpc: JSONRPC_VERSION.to_string(),
762                    id: response_id,
763                    result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
764                    error: None,
765                },
766                Err(err) => JsonRpcResponse {
767                    jsonrpc: JSONRPC_VERSION.to_string(),
768                    id: response_id,
769                    result: None,
770                    error: Some(JsonRpcError {
771                        code: -32602,
772                        message: format!("Invalid params: {}", err.message()),
773                        data: None,
774                    }),
775                },
776            }
777        }
778        "mobkit/gating/audit" => match parse_gating_audit_params(&request.params) {
779            Ok(limit) => JsonRpcResponse {
780                jsonrpc: JSONRPC_VERSION.to_string(),
781                id: response_id,
782                result: Some(serde_json::json!({
783                    "entries": runtime.gating_audit_entries(limit),
784                })),
785                error: None,
786            },
787            Err(err) => JsonRpcResponse {
788                jsonrpc: JSONRPC_VERSION.to_string(),
789                id: response_id,
790                result: None,
791                error: Some(JsonRpcError {
792                    code: -32602,
793                    message: format!("Invalid params: {}", err.message()),
794                    data: None,
795                }),
796            },
797        },
798        "mobkit/call_tool" => {
799            let module_id = request.params.get("module_id").and_then(Value::as_str);
800            let tool = request.params.get("tool").and_then(Value::as_str);
801            let arguments = request
802                .params
803                .get("arguments")
804                .cloned()
805                .unwrap_or(serde_json::json!({}));
806
807            match (module_id, tool) {
808                (Some(module_id), Some(tool)) if !module_id.is_empty() && !tool.is_empty() => {
809                    let route = route_module_call(
810                        runtime,
811                        &ModuleRouteRequest {
812                            module_id: module_id.to_string(),
813                            method: tool.to_string(),
814                            params: arguments,
815                        },
816                        timeout,
817                    );
818                    match route {
819                        Ok(response) => JsonRpcResponse {
820                            jsonrpc: JSONRPC_VERSION.to_string(),
821                            id: response_id,
822                            result: Some(serde_json::json!({
823                                "module_id": response.module_id,
824                                "tool": response.method,
825                                "result": response.payload
826                            })),
827                            error: None,
828                        },
829                        Err(ModuleRouteError::UnloadedModule(mid)) => JsonRpcResponse {
830                            jsonrpc: JSONRPC_VERSION.to_string(),
831                            id: response_id,
832                            result: None,
833                            error: Some(JsonRpcError {
834                                code: -32601,
835                                message: format!("Module '{mid}' not loaded"),
836                                data: None,
837                            }),
838                        },
839                        Err(err) => JsonRpcResponse {
840                            jsonrpc: JSONRPC_VERSION.to_string(),
841                            id: response_id,
842                            result: None,
843                            error: Some(JsonRpcError {
844                                code: -32000,
845                                message: format!("Tool call failed: {err:?}"),
846                                data: None,
847                            }),
848                        },
849                    }
850                }
851                _ => JsonRpcResponse {
852                    jsonrpc: JSONRPC_VERSION.to_string(),
853                    id: response_id,
854                    result: None,
855                    error: Some(JsonRpcError {
856                        code: -32602,
857                        message: "Invalid params: module_id and tool required".to_string(),
858                        data: None,
859                    }),
860                },
861            }
862        }
863        method if method.contains('/') && !method.starts_with("mobkit/") => {
864            let module_id = method
865                .split('/')
866                .next()
867                .map(ToString::to_string)
868                .unwrap_or_default();
869            let route = route_module_call(
870                runtime,
871                &ModuleRouteRequest {
872                    module_id,
873                    method: method.to_string(),
874                    params: request.params,
875                },
876                timeout,
877            );
878            match route {
879                Ok(response) => JsonRpcResponse {
880                    jsonrpc: JSONRPC_VERSION.to_string(),
881                    id: response_id,
882                    result: Some(serde_json::json!({
883                        "module_id": response.module_id,
884                        "method": response.method,
885                        "payload": response.payload
886                    })),
887                    error: None,
888                },
889                Err(ModuleRouteError::UnloadedModule(module_id)) => JsonRpcResponse {
890                    jsonrpc: JSONRPC_VERSION.to_string(),
891                    id: response_id,
892                    result: None,
893                    error: Some(JsonRpcError {
894                        code: -32601,
895                        message: format!("Module '{module_id}' not loaded"),
896                        data: None,
897                    }),
898                },
899                Err(err) => JsonRpcResponse {
900                    jsonrpc: JSONRPC_VERSION.to_string(),
901                    id: response_id,
902                    result: None,
903                    error: Some(JsonRpcError {
904                        code: -32000,
905                        message: format!("Module route failed: {err:?}"),
906                        data: None,
907                    }),
908                },
909            }
910        }
911        _ => JsonRpcResponse {
912            jsonrpc: JSONRPC_VERSION.to_string(),
913            id: response_id,
914            result: None,
915            error: Some(JsonRpcError {
916                code: -32601,
917                message: "Method not found".to_string(),
918                data: None,
919            }),
920        },
921    };
922    if is_notification {
923        String::new()
924    } else {
925        serialize_response(&response)
926    }
927}
928
929/// Identity-first runtime context passed to the RPC handler.
930pub struct IdentityFirstContext {
931    pub runtime: std::sync::Arc<crate::identity_first::IdentityRuntime>,
932    pub roster_provider: std::sync::Arc<dyn crate::identity_first::contracts::RosterProvider>,
933    pub topology_provider:
934        Option<std::sync::Arc<dyn crate::identity_first::contracts::TopologyProvider>>,
935    pub customizer: Option<std::sync::Arc<dyn crate::identity_first::contracts::AgentCustomizer>>,
936}
937
938pub async fn handle_unified_rpc_json(
939    runtime: &UnifiedRuntime,
940    request_json: &str,
941    timeout: Duration,
942    http_base_url: Option<&str>,
943    identity_ctx: Option<&IdentityFirstContext>,
944) -> String {
945    let raw_request: Value = match serde_json::from_str(request_json) {
946        Ok(raw_request) => raw_request,
947        Err(_) => {
948            return serialize_response(&JsonRpcResponse {
949                jsonrpc: JSONRPC_VERSION.to_string(),
950                id: Value::Null,
951                result: None,
952                error: Some(JsonRpcError {
953                    code: -32700,
954                    message: "Parse error".to_string(),
955                    data: None,
956                }),
957            });
958        }
959    };
960    let response_id = raw_request
961        .as_object()
962        .and_then(|object| object.get("id"))
963        .cloned()
964        .unwrap_or(Value::Null);
965    let request: JsonRpcRequest = match serde_json::from_value(raw_request) {
966        Ok(request) => request,
967        Err(_) => {
968            return serialize_response(&JsonRpcResponse {
969                jsonrpc: JSONRPC_VERSION.to_string(),
970                id: response_id,
971                result: None,
972                error: Some(JsonRpcError {
973                    code: -32600,
974                    message: "Invalid Request".to_string(),
975                    data: None,
976                }),
977            });
978        }
979    };
980    let is_notification = request.id.is_none();
981    let response_id = request.id.clone().unwrap_or(Value::Null);
982
983    if request.jsonrpc != "2.0" {
984        let response = JsonRpcResponse {
985            jsonrpc: JSONRPC_VERSION.to_string(),
986            id: response_id,
987            result: None,
988            error: Some(JsonRpcError {
989                code: -32600,
990                message: "Invalid Request".to_string(),
991                data: None,
992            }),
993        };
994        return if is_notification {
995            String::new()
996        } else {
997            serialize_response(&response)
998        };
999    }
1000
1001    let response = match request.method.as_str() {
1002        "mobkit/status" => {
1003            let mob_state = runtime.mob_handle().status().await.ok();
1004            let is_running = runtime.module_is_running().await;
1005            let loaded = runtime.loaded_modules().await;
1006            let mut result = serde_json::json!({
1007                "contract_version": MOBKIT_CONTRACT_VERSION,
1008                "running": is_running,
1009                "loaded_modules": loaded,
1010                "mob_state": format!("{mob_state:?}"),
1011            });
1012            if let Some(url) = http_base_url {
1013                result["http_base_url"] = Value::String(url.to_string());
1014            }
1015            JsonRpcResponse {
1016                jsonrpc: JSONRPC_VERSION.to_string(),
1017                id: response_id,
1018                result: Some(result),
1019                error: None,
1020            }
1021        }
1022        "mobkit/capabilities" => {
1023            let loaded = runtime.loaded_modules().await;
1024            let mut methods = vec![
1025                "mobkit/init",
1026                "mobkit/status",
1027                "mobkit/capabilities",
1028                "mobkit/reconcile",
1029                "mobkit/spawn_member",
1030                "mobkit/scheduling/evaluate",
1031                "mobkit/scheduling/dispatch",
1032                "mobkit/routing/resolve",
1033                "mobkit/routing/routes/list",
1034                "mobkit/routing/routes/add",
1035                "mobkit/routing/routes/delete",
1036                "mobkit/delivery/send",
1037                "mobkit/delivery/history",
1038                "mobkit/events/subscribe",
1039                "mobkit/query_events",
1040                "mobkit/memory/stores",
1041                "mobkit/memory/index",
1042                "mobkit/memory/query",
1043                "mobkit/session_store/bigquery",
1044                "mobkit/gating/evaluate",
1045                "mobkit/gating/pending",
1046                "mobkit/gating/decide",
1047                "mobkit/gating/audit",
1048                "mobkit/call_tool",
1049                "mobkit/models/catalog",
1050                "mobkit/blob/get",
1051                "mobkit/send_message",
1052                "mobkit/find_members",
1053                "mobkit/ensure_member",
1054                "mobkit/list_members",
1055                "mobkit/get_member",
1056                "mobkit/retire_member",
1057                "mobkit/respawn_member",
1058                "mobkit/reconcile_edges",
1059                "mobkit/rediscover",
1060                "mobkit/mob_events/query",
1061                "mobkit/mob_events/subscribe",
1062                // Always available: local-only member introspection
1063                "mobkit/cross_mob/peer_info",
1064                "mobkit/cross_mob/wire_local",
1065                "mobkit/cross_mob/unwire_local",
1066                "mobkit/peer_pubkey",
1067                "mobkit/member_status",
1068                "mobkit/force_cancel_member",
1069                "mobkit/spawn_helper",
1070                "mobkit/fork_helper",
1071                "mobkit/attach_existing_session",
1072                "mobkit/cancel_flow",
1073                "mobkit/flow_status",
1074                "mobkit/list_flows",
1075                "mobkit/list_runs",
1076                "mobkit/run_flow",
1077                "mobkit/collect_completed",
1078                "mobkit/wait_ready",
1079                "mobkit/mob_labels/set",
1080                "mobkit/mob_labels/get",
1081                "mobkit/mob_labels/delete",
1082                "mobkit/run_labels/set",
1083                "mobkit/run_labels/get",
1084                "mobkit/run_labels/delete",
1085            ];
1086            if identity_ctx.is_some() {
1087                methods.extend_from_slice(&[
1088                    "mobkit/send",
1089                    "mobkit/interact",
1090                    "mobkit/dispatch",
1091                    "mobkit/subscribe",
1092                    "mobkit/status_identity",
1093                    "mobkit/respawn",
1094                    "mobkit/retire",
1095                    "mobkit/reset",
1096                    "mobkit/delete_identity",
1097                    "mobkit/inspect_identity",
1098                    "mobkit/reconcile_identity",
1099                ]);
1100            }
1101            // Cross-mob directory always advertised when configured
1102            if runtime.has_contact_directory() {
1103                methods.push("mobkit/cross_mob/directory");
1104            }
1105            // High-level wire/unwire/send require peer mob handles AND inproc contacts.
1106            // resolve_contact() rejects non-Inproc transports at execution time, so
1107            // advertising these methods for TCP/UDS-only deployments guarantees failures.
1108            if runtime.has_peer_mob_handles().await && runtime.has_inproc_contacts() {
1109                methods.extend_from_slice(&[
1110                    "mobkit/cross_mob/wire",
1111                    "mobkit/cross_mob/unwire",
1112                    "mobkit/cross_mob/send",
1113                ]);
1114            }
1115            JsonRpcResponse {
1116                jsonrpc: JSONRPC_VERSION.to_string(),
1117                id: response_id,
1118                result: Some(serde_json::json!({
1119                    "contract_version": MOBKIT_CONTRACT_VERSION,
1120                    "runtime_type": "unified",
1121                    "methods": methods,
1122                    "loaded_modules": loaded,
1123                    "runtime_capabilities": {
1124                        "can_spawn_members": true,
1125                        "can_send_messages": true,
1126                        "can_wire_members": true,
1127                        "can_retire_members": true,
1128                        "available_spawn_modes": ["module", "profile"],
1129                    }
1130                })),
1131                error: None,
1132            }
1133        }
1134        "mobkit/reconcile" => {
1135            let modules = match params::required_string_array(&request.params, "modules") {
1136                Ok(m) => m,
1137                Err(reason) => {
1138                    return serialize_response(&JsonRpcResponse {
1139                        jsonrpc: JSONRPC_VERSION.to_string(),
1140                        id: response_id,
1141                        result: None,
1142                        error: Some(JsonRpcError {
1143                            code: -32602,
1144                            message: format!("Invalid params: {reason}"),
1145                            data: None,
1146                        }),
1147                    });
1148                }
1149            };
1150
1151            match runtime.reconcile_modules(modules.clone(), timeout).await {
1152                Ok(added) => JsonRpcResponse {
1153                    jsonrpc: JSONRPC_VERSION.to_string(),
1154                    id: response_id,
1155                    result: Some(serde_json::json!({
1156                        "accepted": true,
1157                        "reconciled_modules": modules,
1158                        "added": added
1159                    })),
1160                    error: None,
1161                },
1162                Err(err) => JsonRpcResponse {
1163                    jsonrpc: JSONRPC_VERSION.to_string(),
1164                    id: response_id,
1165                    result: None,
1166                    error: Some(JsonRpcError {
1167                        code: -32602,
1168                        message: format!("Invalid params: {err:?}"),
1169                        data: None,
1170                    }),
1171                },
1172            }
1173        }
1174        "mobkit/spawn_member" => {
1175            // Support both legacy module_id pattern and mob profile+meerkat_id pattern
1176            let module_id = request.params.get("module_id").and_then(Value::as_str);
1177            let profile = request.params.get("profile").and_then(Value::as_str);
1178            let meerkat_id = request.params.get("meerkat_id").and_then(Value::as_str);
1179
1180            if let Some(module_id) = module_id {
1181                // Legacy module spawn: {"module_id": "routing"}
1182                if module_id.is_empty() {
1183                    JsonRpcResponse {
1184                        jsonrpc: JSONRPC_VERSION.to_string(),
1185                        id: response_id,
1186                        result: None,
1187                        error: Some(JsonRpcError {
1188                            code: -32602,
1189                            message: "Invalid params: module_id required".to_string(),
1190                            data: None,
1191                        }),
1192                    }
1193                } else {
1194                    match runtime.spawn_member(module_id, timeout).await {
1195                        Ok(()) => JsonRpcResponse {
1196                            jsonrpc: JSONRPC_VERSION.to_string(),
1197                            id: response_id,
1198                            result: Some(serde_json::json!({
1199                                "accepted": true,
1200                                "module_id": module_id
1201                            })),
1202                            error: None,
1203                        },
1204                        Err(err) => JsonRpcResponse {
1205                            jsonrpc: JSONRPC_VERSION.to_string(),
1206                            id: response_id,
1207                            result: None,
1208                            error: Some(JsonRpcError {
1209                                code: -32602,
1210                                message: format!("Invalid params: {err:?}"),
1211                                data: None,
1212                            }),
1213                        },
1214                    }
1215                }
1216            } else if let (Some(profile), Some(meerkat_id)) = (profile, meerkat_id) {
1217                // Mob agent spawn: {"profile": "default", "meerkat_id": "agent-1"}
1218                let spec = meerkat_mob::SpawnMemberSpec::from_wire(
1219                    profile.to_string(),
1220                    meerkat_id.to_string(),
1221                    request
1222                        .params
1223                        .get("initial_message")
1224                        .and_then(Value::as_str)
1225                        .map(|s| meerkat_core::ContentInput::from(s.to_string())),
1226                    None,
1227                    None,
1228                );
1229                match runtime.spawn(spec).await {
1230                    Ok(_member_ref) => JsonRpcResponse {
1231                        jsonrpc: JSONRPC_VERSION.to_string(),
1232                        id: response_id,
1233                        result: Some(serde_json::json!({
1234                            "accepted": true,
1235                            "meerkat_id": meerkat_id
1236                        })),
1237                        error: None,
1238                    },
1239                    Err(err) => JsonRpcResponse {
1240                        jsonrpc: JSONRPC_VERSION.to_string(),
1241                        id: response_id,
1242                        result: None,
1243                        error: Some(JsonRpcError {
1244                            code: -32602,
1245                            message: format!("Invalid params: {err}"),
1246                            data: None,
1247                        }),
1248                    },
1249                }
1250            } else {
1251                JsonRpcResponse {
1252                    jsonrpc: JSONRPC_VERSION.to_string(),
1253                    id: response_id,
1254                    result: None,
1255                    error: Some(JsonRpcError {
1256                        code: -32602,
1257                        message: "Invalid params: module_id or (profile + meerkat_id) required"
1258                            .to_string(),
1259                        data: None,
1260                    }),
1261                }
1262            }
1263        }
1264        "mobkit/scheduling/evaluate" => match parse_scheduling_params(&request.params) {
1265            Ok((schedules, tick_ms)) => {
1266                match runtime.evaluate_schedule_tick(&schedules, tick_ms).await {
1267                    Ok(evaluation) => JsonRpcResponse {
1268                        jsonrpc: JSONRPC_VERSION.to_string(),
1269                        id: response_id,
1270                        result: Some(serde_json::to_value(evaluation).unwrap_or(Value::Null)),
1271                        error: None,
1272                    },
1273                    Err(err) => JsonRpcResponse {
1274                        jsonrpc: JSONRPC_VERSION.to_string(),
1275                        id: response_id,
1276                        result: None,
1277                        error: Some(JsonRpcError {
1278                            code: -32602,
1279                            message: format!(
1280                                "Invalid params: {}",
1281                                format_schedule_validation_error(err)
1282                            ),
1283                            data: None,
1284                        }),
1285                    },
1286                }
1287            }
1288            Err(message) => JsonRpcResponse {
1289                jsonrpc: JSONRPC_VERSION.to_string(),
1290                id: response_id,
1291                result: None,
1292                error: Some(JsonRpcError {
1293                    code: -32602,
1294                    message: format!("Invalid params: {message}"),
1295                    data: None,
1296                }),
1297            },
1298        },
1299        "mobkit/scheduling/dispatch" => match parse_scheduling_params(&request.params) {
1300            Ok((schedules, tick_ms)) => {
1301                match runtime.dispatch_schedule_tick(&schedules, tick_ms).await {
1302                    Ok(dispatch) => JsonRpcResponse {
1303                        jsonrpc: JSONRPC_VERSION.to_string(),
1304                        id: response_id,
1305                        result: Some(serde_json::to_value(dispatch).unwrap_or(Value::Null)),
1306                        error: None,
1307                    },
1308                    Err(err) => JsonRpcResponse {
1309                        jsonrpc: JSONRPC_VERSION.to_string(),
1310                        id: response_id,
1311                        result: None,
1312                        error: Some(JsonRpcError {
1313                            code: -32602,
1314                            message: format!("Invalid params: {err}"),
1315                            data: None,
1316                        }),
1317                    },
1318                }
1319            }
1320            Err(message) => JsonRpcResponse {
1321                jsonrpc: JSONRPC_VERSION.to_string(),
1322                id: response_id,
1323                result: None,
1324                error: Some(JsonRpcError {
1325                    code: -32602,
1326                    message: format!("Invalid params: {message}"),
1327                    data: None,
1328                }),
1329            },
1330        },
1331        "mobkit/routing/resolve" => {
1332            let resolve_result = match parse_routing_resolve_params(&request.params) {
1333                Ok(resolve_request) => runtime
1334                    .resolve_routing(resolve_request)
1335                    .await
1336                    .map_err(RoutingDeliveryParamsError::Routing),
1337                Err(e) => Err(e),
1338            };
1339            match resolve_result {
1340                Ok(resolution) => JsonRpcResponse {
1341                    jsonrpc: JSONRPC_VERSION.to_string(),
1342                    id: response_id,
1343                    result: Some(serde_json::to_value(resolution).unwrap_or(Value::Null)),
1344                    error: None,
1345                },
1346                Err(err) => JsonRpcResponse {
1347                    jsonrpc: JSONRPC_VERSION.to_string(),
1348                    id: response_id,
1349                    result: None,
1350                    error: Some(JsonRpcError {
1351                        code: -32602,
1352                        message: format!("Invalid params: {}", err.message()),
1353                        data: None,
1354                    }),
1355                },
1356            }
1357        }
1358        "mobkit/routing/routes/list" => match parse_routing_routes_list_params(&request.params) {
1359            Ok(()) => {
1360                let routes = runtime.list_runtime_routes().await;
1361                JsonRpcResponse {
1362                    jsonrpc: JSONRPC_VERSION.to_string(),
1363                    id: response_id,
1364                    result: Some(serde_json::json!({
1365                        "routes": routes
1366                    })),
1367                    error: None,
1368                }
1369            }
1370            Err(err) => JsonRpcResponse {
1371                jsonrpc: JSONRPC_VERSION.to_string(),
1372                id: response_id,
1373                result: None,
1374                error: Some(JsonRpcError {
1375                    code: -32602,
1376                    message: format!("Invalid params: {}", err.message()),
1377                    data: None,
1378                }),
1379            },
1380        },
1381        "mobkit/routing/routes/add" => {
1382            let add_result = match parse_routing_route_add_params(&request.params) {
1383                Ok(route) => runtime
1384                    .add_runtime_route(route)
1385                    .await
1386                    .map_err(RoutingDeliveryParamsError::RouteMutation),
1387                Err(e) => Err(e),
1388            };
1389            match add_result {
1390                Ok(route) => JsonRpcResponse {
1391                    jsonrpc: JSONRPC_VERSION.to_string(),
1392                    id: response_id,
1393                    result: Some(serde_json::json!({ "route": route })),
1394                    error: None,
1395                },
1396                Err(err) => JsonRpcResponse {
1397                    jsonrpc: JSONRPC_VERSION.to_string(),
1398                    id: response_id,
1399                    result: None,
1400                    error: Some(JsonRpcError {
1401                        code: -32602,
1402                        message: format!("Invalid params: {}", err.message()),
1403                        data: None,
1404                    }),
1405                },
1406            }
1407        }
1408        "mobkit/routing/routes/delete" => {
1409            let delete_result = match parse_routing_route_delete_params(&request.params) {
1410                Ok(route_key) => runtime
1411                    .delete_runtime_route(&route_key)
1412                    .await
1413                    .map_err(RoutingDeliveryParamsError::RouteMutation),
1414                Err(e) => Err(e),
1415            };
1416            match delete_result {
1417                Ok(route) => JsonRpcResponse {
1418                    jsonrpc: JSONRPC_VERSION.to_string(),
1419                    id: response_id,
1420                    result: Some(serde_json::json!({ "deleted": route })),
1421                    error: None,
1422                },
1423                Err(err) => JsonRpcResponse {
1424                    jsonrpc: JSONRPC_VERSION.to_string(),
1425                    id: response_id,
1426                    result: None,
1427                    error: Some(JsonRpcError {
1428                        code: -32602,
1429                        message: format!("Invalid params: {}", err.message()),
1430                        data: None,
1431                    }),
1432                },
1433            }
1434        }
1435        "mobkit/delivery/send" => {
1436            let send_result = match parse_delivery_send_params(&request.params) {
1437                Ok(send_request) => runtime
1438                    .send_delivery(send_request)
1439                    .await
1440                    .map_err(RoutingDeliveryParamsError::Delivery),
1441                Err(e) => Err(e),
1442            };
1443            match send_result {
1444                Ok(record) => JsonRpcResponse {
1445                    jsonrpc: JSONRPC_VERSION.to_string(),
1446                    id: response_id,
1447                    result: Some(serde_json::to_value(record).unwrap_or(Value::Null)),
1448                    error: None,
1449                },
1450                Err(err) => JsonRpcResponse {
1451                    jsonrpc: JSONRPC_VERSION.to_string(),
1452                    id: response_id,
1453                    result: None,
1454                    error: Some(JsonRpcError {
1455                        code: -32602,
1456                        message: format!("Invalid params: {}", err.message()),
1457                        data: None,
1458                    }),
1459                },
1460            }
1461        }
1462        "mobkit/delivery/history" => match parse_delivery_history_params(&request.params) {
1463            Ok(history_request) => {
1464                let history = runtime.delivery_history(history_request).await;
1465                JsonRpcResponse {
1466                    jsonrpc: JSONRPC_VERSION.to_string(),
1467                    id: response_id,
1468                    result: Some(serde_json::to_value(history).unwrap_or(Value::Null)),
1469                    error: None,
1470                }
1471            }
1472            Err(err) => JsonRpcResponse {
1473                jsonrpc: JSONRPC_VERSION.to_string(),
1474                id: response_id,
1475                result: None,
1476                error: Some(JsonRpcError {
1477                    code: -32602,
1478                    message: format!("Invalid params: {}", err.message()),
1479                    data: None,
1480                }),
1481            },
1482        },
1483        "mobkit/events/subscribe" => match parse_subscribe_request(&request.params) {
1484            Ok(subscribe_request) => match runtime.subscribe_events(subscribe_request).await {
1485                Ok(subscribe_result) => JsonRpcResponse {
1486                    jsonrpc: JSONRPC_VERSION.to_string(),
1487                    id: response_id,
1488                    result: Some(serde_json::to_value(subscribe_result).unwrap_or(Value::Null)),
1489                    error: None,
1490                },
1491                Err(err) => JsonRpcResponse {
1492                    jsonrpc: JSONRPC_VERSION.to_string(),
1493                    id: response_id,
1494                    result: None,
1495                    error: Some(JsonRpcError {
1496                        code: -32602,
1497                        message: format!("Invalid params: {err}"),
1498                        data: None,
1499                    }),
1500                },
1501            },
1502            Err(err) => JsonRpcResponse {
1503                jsonrpc: JSONRPC_VERSION.to_string(),
1504                id: response_id,
1505                result: None,
1506                error: Some(JsonRpcError {
1507                    code: -32602,
1508                    message: format!("Invalid params: {}", err.message()),
1509                    data: None,
1510                }),
1511            },
1512        },
1513        "mobkit/query_events" => {
1514            let query: EventQuery = if request.params.is_null() {
1515                EventQuery::default()
1516            } else {
1517                match serde_json::from_value(request.params.clone()) {
1518                    Ok(query) => query,
1519                    Err(err) => {
1520                        return serde_json::to_string(&JsonRpcResponse {
1521                            jsonrpc: JSONRPC_VERSION.to_string(),
1522                            id: response_id,
1523                            result: None,
1524                            error: Some(JsonRpcError {
1525                                code: -32602,
1526                                message: format!("Invalid params: invalid query params: {err}"),
1527                                data: None,
1528                            }),
1529                        })
1530                        .unwrap_or_else(|_| r#"{"error":"serialization failed"}"#.to_string());
1531                    }
1532                }
1533            };
1534            match runtime.event_log_store() {
1535                Some(store) => match store.query(query).await {
1536                    Ok(events) => JsonRpcResponse {
1537                        jsonrpc: JSONRPC_VERSION.to_string(),
1538                        id: response_id,
1539                        result: Some(serde_json::to_value(events).unwrap_or(Value::Null)),
1540                        error: None,
1541                    },
1542                    Err(err) => JsonRpcResponse {
1543                        jsonrpc: JSONRPC_VERSION.to_string(),
1544                        id: response_id,
1545                        result: None,
1546                        error: Some(JsonRpcError {
1547                            code: -32603,
1548                            message: format!("query_events failed: {err}"),
1549                            data: None,
1550                        }),
1551                    },
1552                },
1553                None => JsonRpcResponse {
1554                    jsonrpc: JSONRPC_VERSION.to_string(),
1555                    id: response_id,
1556                    result: Some(serde_json::json!({
1557                        "status": "no_event_log_configured",
1558                        "events": [],
1559                    })),
1560                    error: None,
1561                },
1562            }
1563        }
1564        "mobkit/memory/stores" => match parse_memory_stores_params(&request.params) {
1565            Ok(()) => {
1566                let stores = runtime.memory_stores().await;
1567                JsonRpcResponse {
1568                    jsonrpc: JSONRPC_VERSION.to_string(),
1569                    id: response_id,
1570                    result: Some(serde_json::json!({
1571                        "stores": stores,
1572                    })),
1573                    error: None,
1574                }
1575            }
1576            Err(err) => JsonRpcResponse {
1577                jsonrpc: JSONRPC_VERSION.to_string(),
1578                id: response_id,
1579                result: None,
1580                error: Some(JsonRpcError {
1581                    code: -32602,
1582                    message: format!("Invalid params: {}", err.message()),
1583                    data: None,
1584                }),
1585            },
1586        },
1587        "mobkit/memory/index" => match parse_memory_index_params(&request.params) {
1588            Ok(index_request) => match runtime.memory_index(index_request).await {
1589                Ok(indexed) => JsonRpcResponse {
1590                    jsonrpc: JSONRPC_VERSION.to_string(),
1591                    id: response_id,
1592                    result: Some(serde_json::to_value(indexed).unwrap_or(Value::Null)),
1593                    error: None,
1594                },
1595                Err(MemoryIndexError::BackendPersistFailed(error)) => JsonRpcResponse {
1596                    jsonrpc: JSONRPC_VERSION.to_string(),
1597                    id: response_id,
1598                    result: None,
1599                    error: Some(JsonRpcError {
1600                        code: MEMORY_BACKEND_UNAVAILABLE_CODE,
1601                        message: format!(
1602                            "Memory backend unavailable: {}",
1603                            MemoryParamsError::backend_message(&error)
1604                        ),
1605                        data: None,
1606                    }),
1607                },
1608                Err(err) => JsonRpcResponse {
1609                    jsonrpc: JSONRPC_VERSION.to_string(),
1610                    id: response_id,
1611                    result: None,
1612                    error: Some(JsonRpcError {
1613                        code: -32602,
1614                        message: format!(
1615                            "Invalid params: {}",
1616                            MemoryParamsError::Index(err).message()
1617                        ),
1618                        data: None,
1619                    }),
1620                },
1621            },
1622            Err(err) => JsonRpcResponse {
1623                jsonrpc: JSONRPC_VERSION.to_string(),
1624                id: response_id,
1625                result: None,
1626                error: Some(JsonRpcError {
1627                    code: -32602,
1628                    message: format!("Invalid params: {}", err.message()),
1629                    data: None,
1630                }),
1631            },
1632        },
1633        "mobkit/memory/query" => match parse_memory_query_params(&request.params) {
1634            Ok(query_request) => {
1635                let query_result = runtime.memory_query(query_request).await;
1636                JsonRpcResponse {
1637                    jsonrpc: JSONRPC_VERSION.to_string(),
1638                    id: response_id,
1639                    result: Some(serde_json::to_value(query_result).unwrap_or(Value::Null)),
1640                    error: None,
1641                }
1642            }
1643            Err(err) => JsonRpcResponse {
1644                jsonrpc: JSONRPC_VERSION.to_string(),
1645                id: response_id,
1646                result: None,
1647                error: Some(JsonRpcError {
1648                    code: -32602,
1649                    message: format!("Invalid params: {}", err.message()),
1650                    data: None,
1651                }),
1652            },
1653        },
1654        "mobkit/session_store/bigquery" => {
1655            match parse_bigquery_session_store_params(&request.params)
1656                .and_then(run_bigquery_session_store_request)
1657            {
1658                Ok(result) => JsonRpcResponse {
1659                    jsonrpc: JSONRPC_VERSION.to_string(),
1660                    id: response_id,
1661                    result: Some(result),
1662                    error: None,
1663                },
1664                Err(BigQuerySessionStoreRpcError::Params(message)) => JsonRpcResponse {
1665                    jsonrpc: JSONRPC_VERSION.to_string(),
1666                    id: response_id,
1667                    result: None,
1668                    error: Some(JsonRpcError {
1669                        code: -32602,
1670                        message: format!("Invalid params: {message}"),
1671                        data: None,
1672                    }),
1673                },
1674                Err(BigQuerySessionStoreRpcError::Store(error)) => JsonRpcResponse {
1675                    jsonrpc: JSONRPC_VERSION.to_string(),
1676                    id: response_id,
1677                    result: None,
1678                    error: Some(JsonRpcError {
1679                        code: -32011,
1680                        message: format!(
1681                            "BigQuery session store request failed: {}",
1682                            format_bigquery_store_error(&error)
1683                        ),
1684                        data: None,
1685                    }),
1686                },
1687            }
1688        }
1689        "mobkit/gating/evaluate" => match parse_gating_evaluate_params(&request.params) {
1690            Ok(gating_request) => {
1691                let gating_result = runtime.evaluate_gating_action(gating_request).await;
1692                JsonRpcResponse {
1693                    jsonrpc: JSONRPC_VERSION.to_string(),
1694                    id: response_id,
1695                    result: Some(serde_json::to_value(gating_result).unwrap_or(Value::Null)),
1696                    error: None,
1697                }
1698            }
1699            Err(err) => JsonRpcResponse {
1700                jsonrpc: JSONRPC_VERSION.to_string(),
1701                id: response_id,
1702                result: None,
1703                error: Some(JsonRpcError {
1704                    code: -32602,
1705                    message: format!("Invalid params: {}", err.message()),
1706                    data: None,
1707                }),
1708            },
1709        },
1710        "mobkit/gating/pending" => match parse_gating_pending_params(&request.params) {
1711            Ok(()) => {
1712                let pending = runtime.list_gating_pending().await;
1713                JsonRpcResponse {
1714                    jsonrpc: JSONRPC_VERSION.to_string(),
1715                    id: response_id,
1716                    result: Some(serde_json::json!({
1717                        "pending": pending,
1718                    })),
1719                    error: None,
1720                }
1721            }
1722            Err(err) => JsonRpcResponse {
1723                jsonrpc: JSONRPC_VERSION.to_string(),
1724                id: response_id,
1725                result: None,
1726                error: Some(JsonRpcError {
1727                    code: -32602,
1728                    message: format!("Invalid params: {}", err.message()),
1729                    data: None,
1730                }),
1731            },
1732        },
1733        "mobkit/gating/decide" => {
1734            let decide_result = match parse_gating_decide_params(&request.params) {
1735                Ok(decide_request) => runtime
1736                    .decide_gating_action(decide_request)
1737                    .await
1738                    .map_err(GatingParamsError::Decision),
1739                Err(e) => Err(e),
1740            };
1741            match decide_result {
1742                Ok(result) => JsonRpcResponse {
1743                    jsonrpc: JSONRPC_VERSION.to_string(),
1744                    id: response_id,
1745                    result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
1746                    error: None,
1747                },
1748                Err(err) => JsonRpcResponse {
1749                    jsonrpc: JSONRPC_VERSION.to_string(),
1750                    id: response_id,
1751                    result: None,
1752                    error: Some(JsonRpcError {
1753                        code: -32602,
1754                        message: format!("Invalid params: {}", err.message()),
1755                        data: None,
1756                    }),
1757                },
1758            }
1759        }
1760        "mobkit/gating/audit" => match parse_gating_audit_params(&request.params) {
1761            Ok(limit) => {
1762                let entries = runtime.gating_audit_entries(limit).await;
1763                JsonRpcResponse {
1764                    jsonrpc: JSONRPC_VERSION.to_string(),
1765                    id: response_id,
1766                    result: Some(serde_json::json!({
1767                        "entries": entries,
1768                    })),
1769                    error: None,
1770                }
1771            }
1772            Err(err) => JsonRpcResponse {
1773                jsonrpc: JSONRPC_VERSION.to_string(),
1774                id: response_id,
1775                result: None,
1776                error: Some(JsonRpcError {
1777                    code: -32602,
1778                    message: format!("Invalid params: {}", err.message()),
1779                    data: None,
1780                }),
1781            },
1782        },
1783        "mobkit/call_tool" => {
1784            let module_id = request.params.get("module_id").and_then(Value::as_str);
1785            let tool = request.params.get("tool").and_then(Value::as_str);
1786            let arguments = request
1787                .params
1788                .get("arguments")
1789                .cloned()
1790                .unwrap_or(serde_json::json!({}));
1791
1792            match (module_id, tool) {
1793                (Some(module_id), Some(tool)) if !module_id.is_empty() && !tool.is_empty() => {
1794                    let route = runtime
1795                        .route_module_call(
1796                            &ModuleRouteRequest {
1797                                module_id: module_id.to_string(),
1798                                method: tool.to_string(),
1799                                params: arguments,
1800                            },
1801                            timeout,
1802                        )
1803                        .await;
1804                    match route {
1805                        Ok(response) => JsonRpcResponse {
1806                            jsonrpc: JSONRPC_VERSION.to_string(),
1807                            id: response_id,
1808                            result: Some(serde_json::json!({
1809                                "module_id": response.module_id,
1810                                "tool": response.method,
1811                                "result": response.payload
1812                            })),
1813                            error: None,
1814                        },
1815                        Err(ModuleRouteError::UnloadedModule(mid)) => JsonRpcResponse {
1816                            jsonrpc: JSONRPC_VERSION.to_string(),
1817                            id: response_id,
1818                            result: None,
1819                            error: Some(JsonRpcError {
1820                                code: -32601,
1821                                message: format!("Module '{mid}' not loaded"),
1822                                data: None,
1823                            }),
1824                        },
1825                        Err(err) => JsonRpcResponse {
1826                            jsonrpc: JSONRPC_VERSION.to_string(),
1827                            id: response_id,
1828                            result: None,
1829                            error: Some(JsonRpcError {
1830                                code: -32000,
1831                                message: format!("Tool call failed: {err:?}"),
1832                                data: None,
1833                            }),
1834                        },
1835                    }
1836                }
1837                _ => JsonRpcResponse {
1838                    jsonrpc: JSONRPC_VERSION.to_string(),
1839                    id: response_id,
1840                    result: None,
1841                    error: Some(JsonRpcError {
1842                        code: -32602,
1843                        message: "Invalid params: module_id and tool required".to_string(),
1844                        data: None,
1845                    }),
1846                },
1847            }
1848        }
1849        "mobkit/models/catalog" => JsonRpcResponse {
1850            jsonrpc: JSONRPC_VERSION.to_string(),
1851            id: response_id,
1852            result: Some(build_models_catalog_result()),
1853            error: None,
1854        },
1855        "mobkit/blob/get" => {
1856            mob_methods::handle_blob_get(runtime, response_id, &request.params).await
1857        }
1858        "mobkit/send_message" => {
1859            mob_methods::handle_send_message(runtime, response_id, &request.params).await
1860        }
1861        "mobkit/find_members" => {
1862            mob_methods::handle_find_members(runtime, response_id, &request.params).await
1863        }
1864        "mobkit/ensure_member" => {
1865            mob_methods::handle_ensure_member(runtime, response_id, &request.params).await
1866        }
1867        "mobkit/list_members" => mob_methods::handle_list_members(runtime, response_id).await,
1868        "mobkit/get_member" => {
1869            mob_methods::handle_get_member(runtime, response_id, &request.params).await
1870        }
1871        "mobkit/retire_member" => {
1872            mob_methods::handle_retire_member(runtime, response_id, &request.params).await
1873        }
1874        "mobkit/respawn_member" => {
1875            mob_methods::handle_respawn_member(runtime, response_id, &request.params).await
1876        }
1877        "mobkit/reconcile_edges" => mob_methods::handle_reconcile_edges(runtime, response_id).await,
1878        "mobkit/rediscover" => mob_methods::handle_rediscover(runtime, response_id).await,
1879        "mobkit/mob_events/query" => {
1880            mob_methods::handle_mob_events_query(runtime, response_id, request.params).await
1881        }
1882        "mobkit/mob_events/subscribe" => {
1883            mob_methods::handle_mob_events_subscribe(runtime, response_id, request.params).await
1884        }
1885        "mobkit/cross_mob/wire" => {
1886            mob_methods::handle_cross_mob_wire(runtime, response_id, &request.params).await
1887        }
1888        "mobkit/cross_mob/unwire" => {
1889            mob_methods::handle_cross_mob_unwire(runtime, response_id, &request.params).await
1890        }
1891        "mobkit/cross_mob/send" => {
1892            mob_methods::handle_cross_mob_send(runtime, response_id, &request.params).await
1893        }
1894        "mobkit/cross_mob/directory" => {
1895            mob_methods::handle_cross_mob_directory(runtime, response_id).await
1896        }
1897        "mobkit/cross_mob/peer_info" => {
1898            mob_methods::handle_cross_mob_peer_info(runtime, response_id, &request.params).await
1899        }
1900        "mobkit/cross_mob/wire_local" => {
1901            mob_methods::handle_cross_mob_wire_local(runtime, response_id, &request.params).await
1902        }
1903        "mobkit/cross_mob/unwire_local" => {
1904            mob_methods::handle_cross_mob_unwire_local(runtime, response_id, &request.params).await
1905        }
1906        "mobkit/peer_pubkey" => mob_methods::handle_peer_pubkey(runtime, response_id).await,
1907        "mobkit/member_status" => {
1908            mob_methods::handle_member_status(runtime, response_id, &request.params).await
1909        }
1910        "mobkit/force_cancel_member" => {
1911            mob_methods::handle_force_cancel_member(runtime, response_id, &request.params).await
1912        }
1913        "mobkit/spawn_helper" => {
1914            mob_methods::handle_spawn_helper(runtime, response_id, &request.params).await
1915        }
1916        "mobkit/fork_helper" => {
1917            mob_methods::handle_fork_helper(runtime, response_id, &request.params).await
1918        }
1919        "mobkit/attach_existing_session" => {
1920            mob_methods::handle_attach_existing_session(runtime, response_id, &request.params).await
1921        }
1922        "mobkit/cancel_flow" => {
1923            mob_methods::handle_cancel_flow(runtime, response_id, &request.params).await
1924        }
1925        "mobkit/flow_status" => {
1926            mob_methods::handle_flow_status(runtime, response_id, &request.params).await
1927        }
1928        "mobkit/list_flows" => mob_methods::handle_list_flows(runtime, response_id).await,
1929        "mobkit/list_runs" => {
1930            mob_methods::handle_list_runs(runtime, response_id, &request.params).await
1931        }
1932        "mobkit/run_flow" => {
1933            mob_methods::handle_run_flow(runtime, response_id, &request.params).await
1934        }
1935        "mobkit/collect_completed" => {
1936            mob_methods::handle_collect_completed(runtime, response_id).await
1937        }
1938        "mobkit/wait_ready" => {
1939            mob_methods::handle_wait_ready(runtime, response_id, &request.params).await
1940        }
1941        "mobkit/mob_labels/set" => {
1942            mob_methods::handle_mob_labels_set(runtime, response_id, &request.params).await
1943        }
1944        "mobkit/mob_labels/get" => mob_methods::handle_mob_labels_get(runtime, response_id).await,
1945        "mobkit/mob_labels/delete" => {
1946            mob_methods::handle_mob_labels_delete(runtime, response_id).await
1947        }
1948        "mobkit/run_labels/set" => {
1949            mob_methods::handle_run_labels_set(runtime, response_id, &request.params).await
1950        }
1951        "mobkit/run_labels/get" => {
1952            mob_methods::handle_run_labels_get(runtime, response_id, &request.params).await
1953        }
1954        "mobkit/run_labels/delete" => {
1955            mob_methods::handle_run_labels_delete(runtime, response_id, &request.params).await
1956        }
1957        // ----- identity-first methods -----
1958        "mobkit/send" => {
1959            let identity_rt = match identity_ctx {
1960                Some(ctx) => &*ctx.runtime,
1961                None => return identity_not_configured(response_id),
1962            };
1963            let identity_str = request
1964                .params
1965                .get("identity")
1966                .and_then(|v| v.as_str())
1967                .unwrap_or("");
1968            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
1969                Ok(id) => id,
1970                Err(e) => {
1971                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
1972                }
1973            };
1974            let content_val = request
1975                .params
1976                .get("content")
1977                .cloned()
1978                .unwrap_or(Value::Null);
1979            let content = match serde_json::from_value::<meerkat_core::ContentInput>(content_val) {
1980                Ok(content) => content,
1981                Err(err) => {
1982                    return error_response(response_id, -32602, format!("invalid content: {err}"));
1983                }
1984            };
1985            match identity_rt.send(&identity, &content).await {
1986                Ok(token) => JsonRpcResponse {
1987                    jsonrpc: JSONRPC_VERSION.to_string(),
1988                    id: response_id,
1989                    result: Some(serde_json::json!({ "fencing_token": token.get() })),
1990                    error: None,
1991                },
1992                Err(e) => identity_error_response(response_id, &e),
1993            }
1994        }
1995        "mobkit/interact" => {
1996            let identity_rt = match identity_ctx {
1997                Some(ctx) => &*ctx.runtime,
1998                None => return identity_not_configured(response_id),
1999            };
2000            let identity_str = request
2001                .params
2002                .get("identity")
2003                .and_then(|v| v.as_str())
2004                .unwrap_or("");
2005            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
2006                Ok(id) => id,
2007                Err(e) => {
2008                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
2009                }
2010            };
2011            let content_val = request
2012                .params
2013                .get("content")
2014                .cloned()
2015                .unwrap_or(Value::Null);
2016            let content =
2017                match serde_json::from_value::<meerkat_core::ContentInput>(content_val.clone()) {
2018                    Ok(content) => content,
2019                    Err(err) => {
2020                        return error_response(
2021                            response_id,
2022                            -32602,
2023                            format!("invalid content: {err}"),
2024                        );
2025                    }
2026                };
2027            let origin = request
2028                .params
2029                .get("origin")
2030                .and_then(|v| v.as_str())
2031                .unwrap_or("console");
2032            let interaction_id = request
2033                .params
2034                .get("interaction_id")
2035                .and_then(|v| v.as_str())
2036                .map(ToString::to_string)
2037                .unwrap_or_else(|| meerkat_core::types::SessionId::new().to_string());
2038            let runtime_member_id = identity_rt
2039                .status(&identity)
2040                .await
2041                .ok()
2042                .and_then(|status| status.agent_runtime_id.map(|id| id.as_str().to_string()));
2043
2044            if let Err(err) = runtime
2045                .reserve_identity_interaction(
2046                    identity.as_str(),
2047                    runtime_member_id.as_deref(),
2048                    &interaction_id,
2049                    origin,
2050                    content_val,
2051                )
2052                .await
2053            {
2054                return error_response(
2055                    response_id,
2056                    -32003,
2057                    format!("failed to reserve interaction: {err}"),
2058                );
2059            }
2060
2061            match identity_rt.send(&identity, &content).await {
2062                Ok(token) => JsonRpcResponse {
2063                    jsonrpc: JSONRPC_VERSION.to_string(),
2064                    id: response_id,
2065                    result: Some(serde_json::json!({
2066                        "interaction_id": interaction_id,
2067                        "fencing_token": token.get(),
2068                        "stream": {
2069                            "route": format!("/console/identity/{}/stream", identity.as_str()),
2070                            "identity": identity.as_str(),
2071                        }
2072                    })),
2073                    error: None,
2074                },
2075                Err(e) => {
2076                    runtime
2077                        .record_console_lifecycle(
2078                            identity.as_str(),
2079                            "interaction_failed",
2080                            serde_json::json!({
2081                                "interaction_id": interaction_id,
2082                                "origin": origin,
2083                                "error": e.to_string(),
2084                            }),
2085                        )
2086                        .await;
2087                    identity_error_response(response_id, &e)
2088                }
2089            }
2090        }
2091        "mobkit/dispatch" => {
2092            let identity_rt = match identity_ctx {
2093                Some(ctx) => &*ctx.runtime,
2094                None => return identity_not_configured(response_id),
2095            };
2096            let identity_str = request
2097                .params
2098                .get("identity")
2099                .and_then(|v| v.as_str())
2100                .unwrap_or("");
2101            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
2102                Ok(id) => id,
2103                Err(e) => {
2104                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
2105                }
2106            };
2107            let di_val = request
2108                .params
2109                .get("dispatch_input")
2110                .cloned()
2111                .unwrap_or(Value::Null);
2112            let content_val = di_val
2113                .get("content")
2114                .cloned()
2115                .unwrap_or_else(|| Value::String(String::new()));
2116            let content = match serde_json::from_value::<meerkat_core::ContentInput>(content_val) {
2117                Ok(content) => content,
2118                Err(err) => {
2119                    return error_response(
2120                        response_id,
2121                        -32602,
2122                        format!("invalid dispatch_input.content: {err}"),
2123                    );
2124                }
2125            };
2126            let origin_str = di_val
2127                .get("origin")
2128                .and_then(|v| v.as_str())
2129                .unwrap_or("system");
2130            let origin = match origin_str {
2131                "connector" => crate::identity_first::DispatchOrigin::Connector,
2132                "scheduler" => crate::identity_first::DispatchOrigin::Scheduler,
2133                "policy" => crate::identity_first::DispatchOrigin::Policy,
2134                "flow" => crate::identity_first::DispatchOrigin::Flow,
2135                _ => crate::identity_first::DispatchOrigin::System,
2136            };
2137            let correlation_id = di_val
2138                .get("correlation_id")
2139                .and_then(|v| v.as_str())
2140                .map(crate::identity_first::CorrelationId::new);
2141            let idempotency_key = di_val
2142                .get("idempotency_key")
2143                .and_then(|v| v.as_str())
2144                .map(crate::identity_first::DispatchIdempotencyKey::new);
2145            let dispatch_input = crate::identity_first::DispatchInput {
2146                content,
2147                origin,
2148                correlation_id,
2149                idempotency_key,
2150            };
2151            match identity_rt.dispatch(&identity, &dispatch_input).await {
2152                Ok((token, durable)) => JsonRpcResponse {
2153                    jsonrpc: JSONRPC_VERSION.to_string(),
2154                    id: response_id,
2155                    result: Some(
2156                        serde_json::json!({ "fencing_token": token.get(), "durable": durable }),
2157                    ),
2158                    error: None,
2159                },
2160                Err(e) => identity_error_response(response_id, &e),
2161            }
2162        }
2163        "mobkit/subscribe" => {
2164            let identity_rt = match identity_ctx {
2165                Some(ctx) => &*ctx.runtime,
2166                None => return identity_not_configured(response_id),
2167            };
2168            let identity_str = request
2169                .params
2170                .get("identity")
2171                .and_then(|v| v.as_str())
2172                .unwrap_or("");
2173            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
2174                Ok(id) => id,
2175                Err(e) => {
2176                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
2177                }
2178            };
2179            match identity_rt.subscribe(&identity).await {
2180                Ok(_receiver) => JsonRpcResponse {
2181                    jsonrpc: JSONRPC_VERSION.to_string(),
2182                    id: response_id,
2183                    result: Some(serde_json::json!({
2184                        "identity": identity.as_str(),
2185                        "stream_id": identity.as_str(),
2186                        "subscribed": true,
2187                    })),
2188                    error: None,
2189                },
2190                Err(e) => identity_error_response(response_id, &e),
2191            }
2192        }
2193        "mobkit/status_identity" => {
2194            let identity_rt = match identity_ctx {
2195                Some(ctx) => &*ctx.runtime,
2196                None => return identity_not_configured(response_id),
2197            };
2198            let identity_str = request
2199                .params
2200                .get("identity")
2201                .and_then(|v| v.as_str())
2202                .unwrap_or("");
2203            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
2204                Ok(id) => id,
2205                Err(e) => {
2206                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
2207                }
2208            };
2209            match identity_rt.status(&identity).await {
2210                Ok(status) => {
2211                    let result = serde_json::json!({
2212                        "state": format!("{:?}", status.state),
2213                        "identity": identity_str,
2214                        "agent_runtime_id": status.agent_runtime_id.as_ref().map(super::identity_first::AgentRuntimeId::as_str),
2215                        "session_id": status.session_id.as_ref().map(ToString::to_string),
2216                        "profile": status.profile.as_ref().map(meerkat_mob::ProfileName::as_str),
2217                        "addressability": addressability_json(status.addressability),
2218                        "display_name": status.display_name.as_ref().map(super::identity_first::DisplayName::as_str),
2219                        "labels": status.labels,
2220                        "generation": status.generation.map(super::identity_first::ContinuityGeneration::get),
2221                        "checkpoint_version": status.checkpoint_version.map(super::identity_first::CheckpointVersion::get),
2222                        "lease_healthy": status.lease.as_ref().map(|lease| lease.healthy),
2223                        "lease": status.lease.as_ref().map(|lease| serde_json::json!({
2224                            "fencing_token": lease.fencing_token.get(),
2225                            "ttl_remaining_ms": lease.ttl_remaining.as_millis() as u64,
2226                            "healthy": lease.healthy,
2227                        })),
2228                    });
2229                    JsonRpcResponse {
2230                        jsonrpc: JSONRPC_VERSION.to_string(),
2231                        id: response_id,
2232                        result: Some(result),
2233                        error: None,
2234                    }
2235                }
2236                Err(e) => identity_error_response(response_id, &e),
2237            }
2238        }
2239        "mobkit/respawn" => {
2240            let identity_rt = match identity_ctx {
2241                Some(ctx) => &*ctx.runtime,
2242                None => return identity_not_configured(response_id),
2243            };
2244            let identity_str = request
2245                .params
2246                .get("identity")
2247                .and_then(|v| v.as_str())
2248                .unwrap_or("");
2249            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
2250                Ok(id) => id,
2251                Err(e) => {
2252                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
2253                }
2254            };
2255            match identity_rt.respawn(&identity).await {
2256                Ok(record) => {
2257                    runtime
2258                        .record_console_lifecycle(
2259                            identity.as_str(),
2260                            "identity_respawned",
2261                            serde_json::json!({
2262                                "generation": record.generation.get(),
2263                                "checkpoint_version": record.checkpoint_version.get(),
2264                            }),
2265                        )
2266                        .await;
2267                    JsonRpcResponse {
2268                        jsonrpc: JSONRPC_VERSION.to_string(),
2269                        id: response_id,
2270                        result: Some(serde_json::json!({
2271                            "identity": record.identity.as_str(),
2272                            "agent_runtime_id": record.agent_runtime_id.as_str(),
2273                            "session_id": record.session_id.to_string(),
2274                            "generation": record.generation.get(),
2275                            "checkpoint_version": record.checkpoint_version.get(),
2276                        })),
2277                        error: None,
2278                    }
2279                }
2280                Err(e) => identity_error_response(response_id, &e),
2281            }
2282        }
2283        "mobkit/retire" => {
2284            let identity_rt = match identity_ctx {
2285                Some(ctx) => &*ctx.runtime,
2286                None => return identity_not_configured(response_id),
2287            };
2288            let identity_str = request
2289                .params
2290                .get("identity")
2291                .and_then(|v| v.as_str())
2292                .unwrap_or("");
2293            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
2294                Ok(id) => id,
2295                Err(e) => {
2296                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
2297                }
2298            };
2299            match identity_rt.retire(&identity).await {
2300                Ok(token) => {
2301                    runtime
2302                        .record_console_lifecycle(
2303                            identity.as_str(),
2304                            "identity_retired",
2305                            serde_json::json!({ "fencing_token": token.get() }),
2306                        )
2307                        .await;
2308                    JsonRpcResponse {
2309                        jsonrpc: JSONRPC_VERSION.to_string(),
2310                        id: response_id,
2311                        result: Some(serde_json::json!({ "fencing_token": token.get() })),
2312                        error: None,
2313                    }
2314                }
2315                Err(e) => identity_error_response(response_id, &e),
2316            }
2317        }
2318        "mobkit/reset" => {
2319            let identity_rt = match identity_ctx {
2320                Some(ctx) => &*ctx.runtime,
2321                None => return identity_not_configured(response_id),
2322            };
2323            let identity_str = request
2324                .params
2325                .get("identity")
2326                .and_then(|v| v.as_str())
2327                .unwrap_or("");
2328            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
2329                Ok(id) => id,
2330                Err(e) => {
2331                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
2332                }
2333            };
2334            match identity_rt.reset(&identity).await {
2335                Ok(record) => {
2336                    runtime
2337                        .record_console_lifecycle(
2338                            identity.as_str(),
2339                            "identity_reset",
2340                            serde_json::json!({
2341                                "generation": record.generation.get(),
2342                                "checkpoint_version": record.checkpoint_version.get(),
2343                            }),
2344                        )
2345                        .await;
2346                    JsonRpcResponse {
2347                        jsonrpc: JSONRPC_VERSION.to_string(),
2348                        id: response_id,
2349                        result: Some(serde_json::json!({
2350                            "identity": record.identity.as_str(),
2351                            "agent_runtime_id": record.agent_runtime_id.as_str(),
2352                            "session_id": record.session_id.to_string(),
2353                            "generation": record.generation.get(),
2354                            "checkpoint_version": record.checkpoint_version.get(),
2355                        })),
2356                        error: None,
2357                    }
2358                }
2359                Err(e) => identity_error_response(response_id, &e),
2360            }
2361        }
2362        "mobkit/delete_identity" => {
2363            let identity_rt = match identity_ctx {
2364                Some(ctx) => &*ctx.runtime,
2365                None => return identity_not_configured(response_id),
2366            };
2367            let identity_str = request
2368                .params
2369                .get("identity")
2370                .and_then(|v| v.as_str())
2371                .unwrap_or("");
2372            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
2373                Ok(id) => id,
2374                Err(e) => {
2375                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
2376                }
2377            };
2378            match identity_rt.delete_identity(&identity).await {
2379                Ok(()) => JsonRpcResponse {
2380                    jsonrpc: JSONRPC_VERSION.to_string(),
2381                    id: response_id,
2382                    result: Some(serde_json::json!({})),
2383                    error: None,
2384                },
2385                Err(e) => identity_error_response(response_id, &e),
2386            }
2387        }
2388        "mobkit/inspect_identity" => {
2389            let identity_rt = match identity_ctx {
2390                Some(ctx) => &*ctx.runtime,
2391                None => return identity_not_configured(response_id),
2392            };
2393            let identity_str = request
2394                .params
2395                .get("identity")
2396                .and_then(|v| v.as_str())
2397                .unwrap_or("");
2398            let identity = match crate::identity_first::AgentIdentity::parse(identity_str) {
2399                Ok(id) => id,
2400                Err(e) => {
2401                    return error_response(response_id, -32602, format!("invalid identity: {e}"));
2402                }
2403            };
2404            let status = identity_rt.status(&identity).await;
2405            match identity_rt.inspect(&identity).await {
2406                Ok(inspection) => {
2407                    let status = status.ok();
2408                    JsonRpcResponse {
2409                        jsonrpc: JSONRPC_VERSION.to_string(),
2410                        id: response_id,
2411                        result: Some(serde_json::json!({
2412                            "identity": identity_str,
2413                            "state": status.as_ref().map(|status| format!("{:?}", status.state)),
2414                            "profile": status.as_ref().and_then(|status| status.profile.as_ref().map(meerkat_mob::ProfileName::as_str)),
2415                            "addressability": status.as_ref().map(|status| addressability_json(status.addressability)),
2416                            "display_name": status.as_ref().and_then(|status| status.display_name.as_ref().map(super::identity_first::DisplayName::as_str)),
2417                            "labels": status.as_ref().map(|status| status.labels.clone()).unwrap_or_default(),
2418                            "generation": status.as_ref().and_then(|status| status.generation.map(super::identity_first::ContinuityGeneration::get)),
2419                            "checkpoint_version": status.as_ref().and_then(|status| status.checkpoint_version.map(super::identity_first::CheckpointVersion::get)),
2420                            "lease_healthy": status.as_ref().and_then(|status| status.lease.as_ref().map(|lease| lease.healthy)),
2421                            "continuity": status.as_ref().map(|status| serde_json::json!({
2422                                "generation": status.generation.map(super::identity_first::ContinuityGeneration::get),
2423                                "checkpoint_version": status.checkpoint_version.map(super::identity_first::CheckpointVersion::get),
2424                                "session_id": status.session_id.as_ref().map(ToString::to_string),
2425                                "agent_runtime_id": status.agent_runtime_id.as_ref().map(super::identity_first::AgentRuntimeId::as_str),
2426                            })).unwrap_or_else(|| serde_json::json!({})),
2427                            "lease": status.as_ref().and_then(|status| status.lease.as_ref().map(|lease| serde_json::json!({
2428                                "fencing_token": lease.fencing_token.get(),
2429                                "ttl_remaining_ms": lease.ttl_remaining.as_millis() as u64,
2430                                "healthy": lease.healthy,
2431                            }))),
2432                            "output_preview": inspection.output_preview,
2433                            "is_final": inspection.is_final,
2434                            "peer_reachable_count": inspection.peer_reachable_count,
2435                        })),
2436                        error: None,
2437                    }
2438                }
2439                Err(e) => identity_error_response(response_id, &e),
2440            }
2441        }
2442        "mobkit/reconcile_identity" => {
2443            let ctx = match identity_ctx {
2444                Some(ctx) => ctx,
2445                None => return identity_not_configured(response_id),
2446            };
2447            // Re-fetch roster from provider and re-run restore_flow
2448            let roster_specs = match ctx
2449                .roster_provider
2450                .roster(&crate::identity_first::RosterContext {
2451                    mob_definition: None,
2452                    previous_identities: Vec::new(),
2453                })
2454                .await
2455            {
2456                Ok(specs) => specs,
2457                Err(e) => {
2458                    return error_response(
2459                        response_id,
2460                        -32603,
2461                        format!("roster provider failed: {e}"),
2462                    );
2463                }
2464            };
2465            match crate::identity_first::restore_flow(
2466                &ctx.runtime,
2467                &roster_specs,
2468                ctx.topology_provider.as_deref(),
2469                ctx.customizer.as_deref(),
2470            )
2471            .await
2472            {
2473                Ok(result) => {
2474                    let outcomes: serde_json::Map<String, Value> = result
2475                        .outcomes
2476                        .iter()
2477                        .map(|(id, outcome)| {
2478                            let val = match outcome {
2479                                crate::identity_first::RestoreOutcome::Created {
2480                                    record, ..
2481                                } => {
2482                                    serde_json::json!({
2483                                        "outcome": "created",
2484                                        "identity": record.identity.as_str(),
2485                                        "agent_runtime_id": record.agent_runtime_id.as_str(),
2486                                        "session_id": record.session_id.to_string(),
2487                                        "generation": record.generation.get(),
2488                                    })
2489                                }
2490                                crate::identity_first::RestoreOutcome::Resumed {
2491                                    record, ..
2492                                } => {
2493                                    serde_json::json!({
2494                                        "outcome": "resumed",
2495                                        "identity": record.identity.as_str(),
2496                                        "agent_runtime_id": record.agent_runtime_id.as_str(),
2497                                        "session_id": record.session_id.to_string(),
2498                                        "generation": record.generation.get(),
2499                                    })
2500                                }
2501                                crate::identity_first::RestoreOutcome::Broken(failure) => {
2502                                    serde_json::json!({
2503                                        "outcome": "broken",
2504                                        "identity": failure.identity.as_str(),
2505                                        "detail": failure.detail,
2506                                    })
2507                                }
2508                            };
2509                            (id.to_string(), val)
2510                        })
2511                        .collect();
2512                    JsonRpcResponse {
2513                        jsonrpc: JSONRPC_VERSION.to_string(),
2514                        id: response_id,
2515                        result: Some(serde_json::json!({
2516                            "outcomes": outcomes,
2517                            "managed_edges": result.managed_edges.len(),
2518                        })),
2519                        error: None,
2520                    }
2521                }
2522                Err(e) => identity_error_response(response_id, &e),
2523            }
2524        }
2525        method if method.contains('/') && !method.starts_with("mobkit/") => {
2526            let module_id = method
2527                .split('/')
2528                .next()
2529                .map(ToString::to_string)
2530                .unwrap_or_default();
2531            let route = runtime
2532                .route_module_call(
2533                    &ModuleRouteRequest {
2534                        module_id: module_id.clone(),
2535                        method: method.to_string(),
2536                        params: request.params,
2537                    },
2538                    timeout,
2539                )
2540                .await;
2541            match route {
2542                Ok(response) => JsonRpcResponse {
2543                    jsonrpc: JSONRPC_VERSION.to_string(),
2544                    id: response_id,
2545                    result: Some(serde_json::json!({
2546                        "module_id": response.module_id,
2547                        "method": response.method,
2548                        "payload": response.payload
2549                    })),
2550                    error: None,
2551                },
2552                Err(ModuleRouteError::UnloadedModule(module_id)) => JsonRpcResponse {
2553                    jsonrpc: JSONRPC_VERSION.to_string(),
2554                    id: response_id,
2555                    result: None,
2556                    error: Some(JsonRpcError {
2557                        code: -32601,
2558                        message: format!("Module '{module_id}' not loaded"),
2559                        data: None,
2560                    }),
2561                },
2562                Err(err) => JsonRpcResponse {
2563                    jsonrpc: JSONRPC_VERSION.to_string(),
2564                    id: response_id,
2565                    result: None,
2566                    error: Some(JsonRpcError {
2567                        code: -32000,
2568                        message: format!("Module route failed: {err:?}"),
2569                        data: None,
2570                    }),
2571                },
2572            }
2573        }
2574        _ => JsonRpcResponse {
2575            jsonrpc: JSONRPC_VERSION.to_string(),
2576            id: response_id,
2577            result: None,
2578            error: Some(JsonRpcError {
2579                code: -32601,
2580                message: "Method not found".to_string(),
2581                data: None,
2582            }),
2583        },
2584    };
2585    if is_notification {
2586        String::new()
2587    } else {
2588        serialize_response(&response)
2589    }
2590}
2591
2592fn build_models_catalog_result() -> Value {
2593    let entries: Vec<Value> = meerkat_models::catalog()
2594        .iter()
2595        .filter_map(|e| {
2596            let mut val = serde_json::to_value(e).ok()?;
2597            if let Some(provider) = meerkat_core::Provider::parse_strict(e.provider)
2598                && let Some(profile) = meerkat_models::profile_for(provider, e.id)
2599                && let Ok(p) = serde_json::to_value(&profile)
2600            {
2601                val["profile"] = p;
2602            }
2603            Some(val)
2604        })
2605        .collect();
2606    let defaults: Vec<Value> = meerkat_models::provider_defaults()
2607        .iter()
2608        .filter_map(|d| serde_json::to_value(d).ok())
2609        .collect();
2610    serde_json::json!({
2611        "models": entries,
2612        "provider_defaults": defaults,
2613    })
2614}
2615
2616fn identity_not_configured(response_id: Value) -> String {
2617    error_response(response_id, -32601, "identity-first runtime not configured")
2618}
2619
2620fn addressability_json(addressability: crate::identity_first::AgentAddressability) -> &'static str {
2621    match addressability {
2622        crate::identity_first::AgentAddressability::Addressable => "addressable",
2623        crate::identity_first::AgentAddressability::InternalOnly => "internal_only",
2624    }
2625}
2626
2627fn identity_error_response(
2628    response_id: Value,
2629    err: &crate::identity_first::IdentityRuntimeError,
2630) -> JsonRpcResponse {
2631    use crate::identity_first::IdentityRuntimeError;
2632    let (code, message) = match err {
2633        IdentityRuntimeError::UnknownIdentity(id) => (-32001, format!("unknown identity: {id}")),
2634        IdentityRuntimeError::NotAddressable(na) => {
2635            (-32002, format!("not addressable: {}", na.identity))
2636        }
2637        IdentityRuntimeError::NoActiveLease(id) => (-32003, format!("no active lease: {id}")),
2638        IdentityRuntimeError::LeaseLost(id) => (-32004, format!("lease lost: {id}")),
2639        _ => (-32603, format!("{err}")),
2640    };
2641    JsonRpcResponse {
2642        jsonrpc: JSONRPC_VERSION.to_string(),
2643        id: response_id,
2644        result: None,
2645        error: Some(JsonRpcError {
2646            code,
2647            message,
2648            data: None,
2649        }),
2650    }
2651}
2652
2653fn error_response(response_id: Value, code: i64, message: impl Into<String>) -> String {
2654    serialize_response(&JsonRpcResponse {
2655        jsonrpc: JSONRPC_VERSION.to_string(),
2656        id: response_id,
2657        result: None,
2658        error: Some(JsonRpcError {
2659            code,
2660            message: message.into(),
2661            data: None,
2662        }),
2663    })
2664}
2665
2666fn serialize_response(response: &JsonRpcResponse) -> String {
2667    serde_json::to_string(response).unwrap_or_else(|_| {
2668        r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Internal error"}}"#
2669            .to_string()
2670    })
2671}