1use std::collections::{BTreeMap, BTreeSet};
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::console_aggregator::is_implicit_delegate_member;
13use crate::mob_handle_runtime::{topology_restore_failed_peer_ids, topology_restore_warning_json};
14use crate::runtime::{
15 BigQuerySessionStoreAdapter, BigQuerySessionStoreError, ConsoleRestJsonRequest,
16 ConsoleRestJsonResponse, DeliveryHistoryRequest, DeliverySendError, DeliverySendRequest,
17 GatingDecideError, GatingDecideRequest, GatingDecision, GatingEvaluateRequest, GatingRiskTier,
18 LocalJsonMemoryStoreError, MemoryIndexError, MemoryIndexRequest, MemoryQueryRequest,
19 MobkitRuntimeHandle, ModuleRouteError, ModuleRouteRequest, ROUTING_RETRY_MAX_CAP,
20 RoutingResolveError, RoutingResolveRequest, RuntimeDecisionState, RuntimeRoute,
21 RuntimeRouteMutationError, ScheduleDefinition, ScheduleValidationError, SessionPersistenceRow,
22 SubscribeError, SubscribeRequest, SubscribeScope, handle_console_rest_json_route,
23 route_module_call, validate_schedules,
24};
25use crate::unified_runtime::{EventQuery, UnifiedRuntime};
26
27mod console_ingress;
28mod gating_methods;
29pub(crate) mod memory_methods;
30pub(crate) mod mob_methods;
31pub(crate) mod params;
32mod routing_delivery_methods;
33mod scheduling_methods;
34mod session_store_methods;
35pub(crate) mod storage_methods;
36mod subscribe_methods;
37pub(crate) mod topology_methods;
38pub(crate) mod workgraph_methods;
39
40pub use console_ingress::handle_console_ingress_json;
41
42use gating_methods::{
43 GatingParamsError, parse_gating_audit_params, parse_gating_decide_params,
44 parse_gating_evaluate_params, parse_gating_pending_params,
45};
46use memory_methods::{
47 MemoryParamsError, parse_agent_memory_forget_params, parse_agent_memory_manifest_params,
48 parse_agent_memory_recall_params, parse_agent_memory_remember_params,
49 parse_agent_memory_update_params, parse_memory_index_params, parse_memory_query_params,
50 parse_memory_stores_params,
51};
52use routing_delivery_methods::{
53 RoutingDeliveryParamsError, parse_delivery_history_params, parse_delivery_send_params,
54 parse_routing_resolve_params, parse_routing_route_add_params,
55 parse_routing_route_delete_params, parse_routing_routes_list_params,
56};
57use scheduling_methods::{format_schedule_validation_error, parse_scheduling_params};
58use session_store_methods::{
59 BigQuerySessionStoreRpcError, format_bigquery_store_error, parse_bigquery_session_store_params,
60 run_bigquery_session_store_request,
61};
62use subscribe_methods::{SubscribeParamsError, parse_subscribe_request};
63
64pub const JSONRPC_VERSION: &str = "2.0";
65pub const MOBKIT_CONTRACT_VERSION: &str = "0.4.0";
66pub const MAX_SCHEDULES_PER_REQUEST: usize = 256;
67pub(crate) const MOBPACK_AUTHORING_METHODS: &[&str] = &[
68 "mobkit/mobpacks/schema",
69 "mobkit/mobpacks/catalogs",
70 "mobkit/tools/catalog",
71 "mobkit/skills/catalog",
72 "mobkit/agent_definitions/list",
73 "mobkit/mobpacks/templates",
74 "mobkit/mobpacks/validate",
75 "mobkit/mobpacks/source",
76 "mobkit/mobpacks/export",
77 "mobkit/mobpacks/import",
78 "mobkit/mobpacks/list",
79 "mobkit/mobpacks/get",
80 "mobkit/mobpacks/create",
81 "mobkit/mobpacks/save",
82 "mobkit/mobpacks/delete",
83 "mobkit/mobpacks/undo",
84 "mobkit/mobpacks/redo",
85 "mobkit/mobpacks/apply_operation",
86 "mobkit/mobpacks/graph_projection",
87 "mobkit/mobpacks/graph_to_flow",
88 "mobkit/mobpacks/deploy_command",
89 "mobkit/mobpacks/deploy",
90];
91
92pub(crate) fn mobpack_authoring_capabilities() -> Value {
93 serde_json::json!({
94 "domain": "mobpack_authoring",
95 "runtime_mutation": false,
96 "host_mutation_methods": {
97 "mobkit/mobpacks/deploy": "when execute=true, writes a mobpack archive and runs rkat mob run on the host",
98 "mobkit/mobpacks/validate": "when rkat_validate=true, writes a mobpack archive and runs rkat mob validate on the host"
99 },
100 "deploy_command": "rkat mob run",
101 "methods": MOBPACK_AUTHORING_METHODS,
102 "operations": crate::mobpack::mobpack_authoring_operations(),
103 })
104}
105
106async fn mobpack_runtime_catalog_state(
107 runtime: &UnifiedRuntime,
108) -> crate::mobpack::MobpackRuntimeCatalogState {
109 let loaded_modules = runtime.loaded_modules().await;
110 let runtime_flow_rows = crate::mobpack::runtime_flow_registry_rows_from_definition(
111 runtime.mob_handle().definition(),
112 );
113 let runtime_agent_definition_sources =
114 crate::mobpack::runtime_agent_definition_sources_from_definition(
115 runtime.mob_handle().definition(),
116 );
117 let runtime_skill_realms =
118 crate::mobpack::runtime_skill_realms_from_definition(runtime.mob_handle().definition());
119 let mut runtime_methods = vec![
120 "mobkit/capabilities".to_string(),
121 "mobkit/models/catalog".to_string(),
122 "mobkit/spawn_member".to_string(),
123 "mobkit/list_members".to_string(),
124 "mobkit/get_member".to_string(),
125 "mobkit/run_flow".to_string(),
126 "mobkit/list_flows".to_string(),
127 "mobkit/list_runs".to_string(),
128 ];
129 runtime_methods.extend(
130 MOBPACK_AUTHORING_METHODS
131 .iter()
132 .map(std::string::ToString::to_string),
133 );
134 if runtime.has_contact_directory() {
135 runtime_methods.push("mobkit/cross_mob/directory".to_string());
136 }
137 if runtime.has_peer_mob_handles().await && runtime.has_inproc_contacts() {
138 runtime_methods.extend([
139 "mobkit/cross_mob/wire".to_string(),
140 "mobkit/cross_mob/unwire".to_string(),
141 "mobkit/cross_mob/send".to_string(),
142 ]);
143 }
144 crate::mobpack::MobpackRuntimeCatalogState {
145 loaded_modules,
146 runtime_methods,
147 has_contact_directory: runtime.has_contact_directory(),
148 has_peer_mob_handles: runtime.has_peer_mob_handles().await,
149 has_inproc_contacts: runtime.has_inproc_contacts(),
150 runtime_flow_rows,
151 runtime_agent_definition_sources,
152 runtime_skill_realms,
153 }
154}
155
156async fn handle_unified_mobpack_authoring_rpc(
157 runtime: &UnifiedRuntime,
158 method: &str,
159 params: &Value,
160 response_id: Value,
161) -> JsonRpcResponse {
162 let runtime_catalog_state = match method {
163 "mobkit/mobpacks/schema"
164 | "mobkit/mobpacks/catalogs"
165 | "mobkit/tools/catalog"
166 | "mobkit/skills/catalog"
167 | "mobkit/agent_definitions/list"
168 | "mobkit/mobpacks/templates"
169 | "mobkit/mobpacks/list"
170 | "mobkit/mobpacks/get"
171 | "mobkit/mobpacks/apply_operation" => Some(mobpack_runtime_catalog_state(runtime).await),
172 _ => None,
173 };
174 let result = match method {
175 "mobkit/mobpacks/catalogs" => Ok(crate::mobpack::mobpack_catalogs_response_with_runtime(
176 runtime_catalog_state.as_ref(),
177 )),
178 "mobkit/tools/catalog" => Ok(crate::mobpack::mobpack_tools_catalog_response_with_runtime(
179 runtime_catalog_state.as_ref(),
180 )),
181 "mobkit/skills/catalog" => Ok(
182 crate::mobpack::mobpack_skills_catalog_response_with_runtime(
183 runtime_catalog_state.as_ref(),
184 ),
185 ),
186 "mobkit/agent_definitions/list" => Ok(
187 crate::mobpack::mobpack_agent_definitions_response_with_runtime(
188 runtime_catalog_state.as_ref(),
189 ),
190 ),
191 "mobkit/mobpacks/templates" => Ok(crate::mobpack::mobpack_templates_response_with_runtime(
192 runtime_catalog_state.as_ref(),
193 )),
194 _ => {
195 return handle_mobpack_authoring_rpc_with_runtime(
196 method,
197 params,
198 response_id.clone(),
199 runtime_catalog_state.as_ref(),
200 )
201 .unwrap_or_else(|| JsonRpcResponse {
202 jsonrpc: JSONRPC_VERSION.to_string(),
203 id: response_id,
204 result: None,
205 error: Some(JsonRpcError {
206 code: -32601,
207 message: "Method not found".to_string(),
208 data: None,
209 }),
210 });
211 }
212 };
213 match result {
214 Ok(result) => JsonRpcResponse {
215 jsonrpc: JSONRPC_VERSION.to_string(),
216 id: response_id,
217 result: Some(result),
218 error: None,
219 },
220 Err(message) => JsonRpcResponse {
221 jsonrpc: JSONRPC_VERSION.to_string(),
222 id: response_id,
223 result: None,
224 error: Some(JsonRpcError {
225 code: -32602,
226 message,
227 data: None,
228 }),
229 },
230 }
231}
232
233pub(crate) fn handle_mobpack_authoring_rpc(
234 method: &str,
235 params: &Value,
236 response_id: Value,
237) -> Option<JsonRpcResponse> {
238 handle_mobpack_authoring_rpc_with_runtime(method, params, response_id, None)
239}
240
241pub(crate) fn handle_mobpack_authoring_rpc_with_runtime(
242 method: &str,
243 params: &Value,
244 response_id: Value,
245 runtime: Option<&crate::mobpack::MobpackRuntimeCatalogState>,
246) -> Option<JsonRpcResponse> {
247 let result = match method {
248 "mobkit/mobpacks/schema" => Ok(crate::mobpack::mobpack_schema_response_with_runtime(
249 runtime,
250 )),
251 "mobkit/mobpacks/catalogs" => Ok(crate::mobpack::mobpack_catalogs_response_with_runtime(
252 runtime,
253 )),
254 "mobkit/tools/catalog" => Ok(crate::mobpack::mobpack_tools_catalog_response_with_runtime(
255 runtime,
256 )),
257 "mobkit/skills/catalog" => {
258 Ok(crate::mobpack::mobpack_skills_catalog_response_with_runtime(runtime))
259 }
260 "mobkit/agent_definitions/list" => {
261 Ok(crate::mobpack::mobpack_agent_definitions_response_with_runtime(runtime))
262 }
263 "mobkit/mobpacks/templates" => Ok(crate::mobpack::mobpack_templates_response_with_runtime(
264 runtime,
265 )),
266 "mobkit/mobpacks/validate" => crate::mobpack::validate_mobpack(params)
267 .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
268 "mobkit/mobpacks/source" => crate::mobpack::source_mobpack(params)
269 .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
270 "mobkit/mobpacks/export" => crate::mobpack::export_mobpack(params)
271 .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
272 "mobkit/mobpacks/import" => crate::mobpack::import_mobpack(params),
273 "mobkit/mobpacks/list" => crate::mobpack::list_mobpack_drafts_with_runtime(params, runtime),
274 "mobkit/mobpacks/get" => crate::mobpack::get_mobpack_draft_with_runtime(params, runtime),
275 "mobkit/mobpacks/create" => crate::mobpack::create_mobpack_draft(params),
276 "mobkit/mobpacks/save" => crate::mobpack::save_mobpack_draft(params),
277 "mobkit/mobpacks/delete" => crate::mobpack::delete_mobpack_draft(params),
278 "mobkit/mobpacks/undo" => crate::mobpack::undo_mobpack_draft(params),
279 "mobkit/mobpacks/redo" => crate::mobpack::redo_mobpack_draft(params),
280 "mobkit/mobpacks/apply_operation" => {
281 crate::mobpack::apply_mobpack_authoring_operation_with_runtime(params, runtime)
282 }
283 "mobkit/mobpacks/graph_projection" => crate::mobpack::graph_projection_mobpack(params),
284 "mobkit/mobpacks/graph_to_flow" => crate::mobpack::graph_to_flow_mobpack(params),
285 "mobkit/mobpacks/deploy_command" => crate::mobpack::deploy_command_preview(params)
286 .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
287 "mobkit/mobpacks/deploy" => crate::mobpack::deploy_mobpack(params)
288 .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
289 _ => return None,
290 };
291 Some(match result {
292 Ok(result) => JsonRpcResponse {
293 jsonrpc: JSONRPC_VERSION.to_string(),
294 id: response_id,
295 result: Some(result),
296 error: None,
297 },
298 Err(message) => JsonRpcResponse {
299 jsonrpc: JSONRPC_VERSION.to_string(),
300 id: response_id,
301 result: None,
302 error: Some(JsonRpcError {
303 code: -32602,
304 message,
305 data: None,
306 }),
307 },
308 })
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
312pub enum RpcCapabilitiesError {
313 InvalidJson,
314 InvalidSchema,
315 MissingContractVersion,
316 InvalidContractVersion,
317}
318
319impl std::fmt::Display for RpcCapabilitiesError {
320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321 match self {
322 Self::InvalidJson => write!(f, "invalid JSON"),
323 Self::InvalidSchema => write!(f, "invalid schema"),
324 Self::MissingContractVersion => write!(f, "missing contract version"),
325 Self::InvalidContractVersion => write!(f, "invalid contract version"),
326 }
327 }
328}
329
330impl std::error::Error for RpcCapabilitiesError {}
331
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
333pub struct RpcCapabilities {
334 pub contract_version: String,
335 #[serde(flatten)]
336 pub extra: BTreeMap<String, Value>,
337}
338
339pub fn parse_rpc_capabilities(line: &str) -> Result<RpcCapabilities, RpcCapabilitiesError> {
340 let raw: Value = serde_json::from_str(line).map_err(|_| RpcCapabilitiesError::InvalidJson)?;
341 let object = raw.as_object().ok_or(RpcCapabilitiesError::InvalidSchema)?;
342 let contract = object
343 .get("contract_version")
344 .ok_or(RpcCapabilitiesError::MissingContractVersion)?;
345 let contract_str = contract
346 .as_str()
347 .ok_or(RpcCapabilitiesError::InvalidContractVersion)?;
348 if contract_str.trim().is_empty() {
349 return Err(RpcCapabilitiesError::InvalidContractVersion);
350 }
351 serde_json::from_value(raw).map_err(|_| RpcCapabilitiesError::InvalidSchema)
352}
353
354pub const MOB_EVENTS_STALE_CURSOR_CODE: i64 = -32010;
361
362pub const MEMORY_BACKEND_UNAVAILABLE_CODE: i64 = -32012;
369
370pub const CONSOLE_TIMELINE_REPLAY_UNAVAILABLE_CODE: i64 = -32013;
375
376pub const STORAGE_RESOLUTION_CODE: i64 = -32014;
386
387pub const WORKGRAPH_UNAVAILABLE_CODE: i64 = -32041;
392
393pub const WORKGRAPH_CONFLICT_CODE: i64 = -32042;
398
399pub const WORKGRAPH_ERROR_CODE: i64 = -32000;
402
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct JsonRpcRequest {
405 pub jsonrpc: String,
406 #[serde(default)]
407 pub id: Option<Value>,
408 pub method: String,
409 #[serde(default)]
410 pub params: Value,
411}
412
413#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
414pub struct JsonRpcError {
415 pub code: i64,
416 pub message: String,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub data: Option<Value>,
423}
424
425impl JsonRpcError {
426 pub fn new(code: i64, message: impl Into<String>) -> Self {
427 Self {
428 code,
429 message: message.into(),
430 data: None,
431 }
432 }
433
434 #[must_use]
435 pub fn with_data(mut self, data: Value) -> Self {
436 self.data = Some(data);
437 self
438 }
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
442pub struct JsonRpcResponse {
443 pub jsonrpc: String,
444 pub id: Value,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub result: Option<Value>,
447 #[serde(skip_serializing_if = "Option::is_none")]
448 pub error: Option<JsonRpcError>,
449}
450
451pub fn handle_mobkit_rpc_json(
452 runtime: &mut MobkitRuntimeHandle,
453 request_json: &str,
454 timeout: Duration,
455) -> String {
456 let raw_request: Value = match serde_json::from_str(request_json) {
457 Ok(raw_request) => raw_request,
458 Err(_) => {
459 return serialize_response(&JsonRpcResponse {
460 jsonrpc: JSONRPC_VERSION.to_string(),
461 id: Value::Null,
462 result: None,
463 error: Some(JsonRpcError {
464 code: -32700,
465 message: "Parse error".to_string(),
466 data: None,
467 }),
468 });
469 }
470 };
471 let response_id = raw_request
472 .as_object()
473 .and_then(|object| object.get("id"))
474 .cloned()
475 .unwrap_or(Value::Null);
476 let request: JsonRpcRequest = match serde_json::from_value(raw_request) {
477 Ok(request) => request,
478 Err(_) => {
479 return serialize_response(&JsonRpcResponse {
480 jsonrpc: JSONRPC_VERSION.to_string(),
481 id: response_id,
482 result: None,
483 error: Some(JsonRpcError {
484 code: -32600,
485 message: "Invalid Request".to_string(),
486 data: None,
487 }),
488 });
489 }
490 };
491 let is_notification = request.id.is_none();
492 let response_id = request.id.clone().unwrap_or(Value::Null);
493
494 if request.jsonrpc != "2.0" {
495 let response = JsonRpcResponse {
496 jsonrpc: JSONRPC_VERSION.to_string(),
497 id: response_id,
498 result: None,
499 error: Some(JsonRpcError {
500 code: -32600,
501 message: "Invalid Request".to_string(),
502 data: None,
503 }),
504 };
505 return if is_notification {
506 String::new()
507 } else {
508 serialize_response(&response)
509 };
510 }
511
512 if let Err(message) =
513 crate::member_comms_id::validate_public_rpc_member_aliases(&request.params)
514 {
515 let response = JsonRpcResponse {
516 jsonrpc: JSONRPC_VERSION.to_string(),
517 id: response_id,
518 result: None,
519 error: Some(JsonRpcError {
520 code: -32602,
521 message: format!("Invalid params: {message}"),
522 data: None,
523 }),
524 };
525 return if is_notification {
526 String::new()
527 } else {
528 serialize_response(&response)
529 };
530 }
531
532 let response = match request.method.as_str() {
533 "mobkit/status" => JsonRpcResponse {
538 jsonrpc: JSONRPC_VERSION.to_string(),
539 id: response_id,
540 result: Some(serde_json::json!({
541 "contract_version": MOBKIT_CONTRACT_VERSION,
542 "running": runtime.is_running(),
543 "loaded_modules": runtime.loaded_modules(),
544 })),
545 error: None,
546 },
547 "mobkit/capabilities" => {
548 let mut methods = vec![
549 "mobkit/status",
550 "mobkit/capabilities",
551 "mobkit/reconcile",
552 "mobkit/spawn_member",
553 "mobkit/scheduling/evaluate",
554 "mobkit/scheduling/dispatch",
555 "mobkit/routing/resolve",
556 "mobkit/routing/routes/list",
557 "mobkit/routing/routes/add",
558 "mobkit/routing/routes/delete",
559 "mobkit/delivery/send",
560 "mobkit/delivery/history",
561 "mobkit/events/subscribe",
562 "mobkit/memory/stores",
563 "mobkit/memory/index",
564 "mobkit/memory/query",
565 "mobkit/session_store/bigquery",
566 "mobkit/gating/evaluate",
567 "mobkit/gating/pending",
568 "mobkit/gating/decide",
569 "mobkit/gating/audit",
570 "mobkit/call_tool",
571 "mobkit/models/catalog",
572 storage_methods::STORAGE_DOCTOR_METHOD,
573 ];
574 methods.extend_from_slice(MOBPACK_AUTHORING_METHODS);
575 JsonRpcResponse {
576 jsonrpc: JSONRPC_VERSION.to_string(),
577 id: response_id,
578 result: Some(serde_json::json!({
579 "contract_version": MOBKIT_CONTRACT_VERSION,
580 "methods": methods,
581 "loaded_modules": runtime.loaded_modules(),
582 "runtime_capabilities": {
583 "can_spawn_members": false,
584 "can_send_messages": false,
585 "can_wire_members": false,
586 "can_retire_members": false,
587 "available_spawn_modes": ["module"],
588 },
589 "authoring_capabilities": mobpack_authoring_capabilities(),
590 })),
591 error: None,
592 }
593 }
594 "mobkit/models/catalog" => JsonRpcResponse {
595 jsonrpc: JSONRPC_VERSION.to_string(),
596 id: response_id,
597 result: Some(build_models_catalog_result()),
598 error: None,
599 },
600 storage_methods::STORAGE_DOCTOR_METHOD => {
604 match storage_methods::parse_storage_doctor_params(&request.params) {
605 Ok(Some(params)) => {
606 let diagnosis = crate::storage_doctor::diagnose_state_dir_blocking_with_options(
607 ¶ms.scope(),
608 None,
609 params.doctor_options(),
610 );
611 JsonRpcResponse {
612 jsonrpc: JSONRPC_VERSION.to_string(),
613 id: response_id,
614 result: Some(storage_methods::storage_doctor_result_json(
615 ¶ms, &diagnosis, None,
616 )),
617 error: None,
618 }
619 }
620 Ok(None) => JsonRpcResponse {
621 jsonrpc: JSONRPC_VERSION.to_string(),
622 id: response_id,
623 result: None,
624 error: Some(JsonRpcError {
625 code: -32602,
626 message: "Invalid params: state_dir required (the module-only runtime \
627 has no state directory)"
628 .to_string(),
629 data: None,
630 }),
631 },
632 Err(reason) => JsonRpcResponse {
633 jsonrpc: JSONRPC_VERSION.to_string(),
634 id: response_id,
635 result: None,
636 error: Some(JsonRpcError {
637 code: -32602,
638 message: format!("Invalid params: {reason}"),
639 data: None,
640 }),
641 },
642 }
643 }
644 method if MOBPACK_AUTHORING_METHODS.contains(&method) => {
645 handle_mobpack_authoring_rpc(method, &request.params, response_id.clone())
646 .unwrap_or_else(|| JsonRpcResponse {
647 jsonrpc: JSONRPC_VERSION.to_string(),
648 id: response_id,
649 result: None,
650 error: Some(JsonRpcError {
651 code: -32601,
652 message: "Method not found".to_string(),
653 data: None,
654 }),
655 })
656 }
657 "mobkit/reconcile" => {
658 let modules = match params::required_string_array(&request.params, "modules") {
659 Ok(m) => m,
660 Err(reason) => {
661 return serialize_response(&JsonRpcResponse {
662 jsonrpc: JSONRPC_VERSION.to_string(),
663 id: response_id,
664 result: None,
665 error: Some(JsonRpcError {
666 code: -32602,
667 message: format!("Invalid params: {reason}"),
668 data: None,
669 }),
670 });
671 }
672 };
673
674 match runtime.reconcile_modules(modules.clone(), timeout) {
675 Ok(added) => JsonRpcResponse {
676 jsonrpc: JSONRPC_VERSION.to_string(),
677 id: response_id,
678 result: Some(serde_json::json!({
679 "accepted": true,
680 "reconciled_modules": modules,
681 "added": added
682 })),
683 error: None,
684 },
685 Err(err) => JsonRpcResponse {
686 jsonrpc: JSONRPC_VERSION.to_string(),
687 id: response_id,
688 result: None,
689 error: Some(JsonRpcError {
690 code: -32602,
691 message: format!("Invalid params: {err:?}"),
692 data: None,
693 }),
694 },
695 }
696 }
697 "mobkit/spawn_member" => {
698 let module_id = request
699 .params
700 .get("module_id")
701 .and_then(Value::as_str)
702 .unwrap_or_default()
703 .to_string();
704 if module_id.is_empty() {
705 JsonRpcResponse {
706 jsonrpc: JSONRPC_VERSION.to_string(),
707 id: response_id,
708 result: None,
709 error: Some(JsonRpcError {
710 code: -32602,
711 message: "Invalid params: module_id required".to_string(),
712 data: None,
713 }),
714 }
715 } else {
716 match runtime.spawn_member(&module_id, timeout) {
717 Ok(()) => JsonRpcResponse {
718 jsonrpc: JSONRPC_VERSION.to_string(),
719 id: response_id,
720 result: Some(serde_json::json!({
721 "accepted": true,
722 "module_id": module_id
723 })),
724 error: None,
725 },
726 Err(err) => JsonRpcResponse {
727 jsonrpc: JSONRPC_VERSION.to_string(),
728 id: response_id,
729 result: None,
730 error: Some(JsonRpcError {
731 code: -32602,
732 message: format!("Invalid params: {err:?}"),
733 data: None,
734 }),
735 },
736 }
737 }
738 }
739 "mobkit/scheduling/evaluate" => match parse_scheduling_params(&request.params) {
740 Ok((schedules, tick_ms)) => match runtime.evaluate_schedule_tick(&schedules, tick_ms) {
741 Ok(evaluation) => JsonRpcResponse {
742 jsonrpc: JSONRPC_VERSION.to_string(),
743 id: response_id,
744 result: Some(serde_json::to_value(evaluation).unwrap_or(Value::Null)),
745 error: None,
746 },
747 Err(err) => JsonRpcResponse {
748 jsonrpc: JSONRPC_VERSION.to_string(),
749 id: response_id,
750 result: None,
751 error: Some(JsonRpcError {
752 code: -32602,
753 message: format!(
754 "Invalid params: {}",
755 format_schedule_validation_error(err)
756 ),
757 data: None,
758 }),
759 },
760 },
761 Err(message) => JsonRpcResponse {
762 jsonrpc: JSONRPC_VERSION.to_string(),
763 id: response_id,
764 result: None,
765 error: Some(JsonRpcError {
766 code: -32602,
767 message: format!("Invalid params: {message}"),
768 data: None,
769 }),
770 },
771 },
772 "mobkit/scheduling/dispatch" => match parse_scheduling_params(&request.params) {
773 Ok((schedules, tick_ms)) => match runtime.dispatch_schedule_tick(&schedules, tick_ms) {
774 Ok(dispatch) => JsonRpcResponse {
775 jsonrpc: JSONRPC_VERSION.to_string(),
776 id: response_id,
777 result: Some(serde_json::to_value(dispatch).unwrap_or(Value::Null)),
778 error: None,
779 },
780 Err(err) => JsonRpcResponse {
781 jsonrpc: JSONRPC_VERSION.to_string(),
782 id: response_id,
783 result: None,
784 error: Some(JsonRpcError {
785 code: -32602,
786 message: format!(
787 "Invalid params: {}",
788 format_schedule_validation_error(err)
789 ),
790 data: None,
791 }),
792 },
793 },
794 Err(message) => JsonRpcResponse {
795 jsonrpc: JSONRPC_VERSION.to_string(),
796 id: response_id,
797 result: None,
798 error: Some(JsonRpcError {
799 code: -32602,
800 message: format!("Invalid params: {message}"),
801 data: None,
802 }),
803 },
804 },
805 "mobkit/routing/resolve" => {
806 match parse_routing_resolve_params(&request.params).and_then(|resolve_request| {
807 runtime
808 .resolve_routing(resolve_request)
809 .map_err(RoutingDeliveryParamsError::Routing)
810 }) {
811 Ok(resolution) => JsonRpcResponse {
812 jsonrpc: JSONRPC_VERSION.to_string(),
813 id: response_id,
814 result: Some(serde_json::to_value(resolution).unwrap_or(Value::Null)),
815 error: None,
816 },
817 Err(err) => JsonRpcResponse {
818 jsonrpc: JSONRPC_VERSION.to_string(),
819 id: response_id,
820 result: None,
821 error: Some(JsonRpcError {
822 code: -32602,
823 message: format!("Invalid params: {}", err.message()),
824 data: None,
825 }),
826 },
827 }
828 }
829 "mobkit/routing/routes/list" => match parse_routing_routes_list_params(&request.params) {
830 Ok(()) => JsonRpcResponse {
831 jsonrpc: JSONRPC_VERSION.to_string(),
832 id: response_id,
833 result: Some(serde_json::json!({
834 "routes": runtime.list_runtime_routes()
835 })),
836 error: None,
837 },
838 Err(err) => JsonRpcResponse {
839 jsonrpc: JSONRPC_VERSION.to_string(),
840 id: response_id,
841 result: None,
842 error: Some(JsonRpcError {
843 code: -32602,
844 message: format!("Invalid params: {}", err.message()),
845 data: None,
846 }),
847 },
848 },
849 "mobkit/routing/routes/add" => match parse_routing_route_add_params(&request.params)
850 .and_then(|route| {
851 runtime
852 .add_runtime_route(route)
853 .map_err(RoutingDeliveryParamsError::RouteMutation)
854 }) {
855 Ok(route) => JsonRpcResponse {
856 jsonrpc: JSONRPC_VERSION.to_string(),
857 id: response_id,
858 result: Some(serde_json::json!({ "route": route })),
859 error: None,
860 },
861 Err(err) => JsonRpcResponse {
862 jsonrpc: JSONRPC_VERSION.to_string(),
863 id: response_id,
864 result: None,
865 error: Some(JsonRpcError {
866 code: -32602,
867 message: format!("Invalid params: {}", err.message()),
868 data: None,
869 }),
870 },
871 },
872 "mobkit/routing/routes/delete" => match parse_routing_route_delete_params(&request.params)
873 .and_then(|route_key| {
874 runtime
875 .delete_runtime_route(&route_key)
876 .map_err(RoutingDeliveryParamsError::RouteMutation)
877 }) {
878 Ok(route) => JsonRpcResponse {
879 jsonrpc: JSONRPC_VERSION.to_string(),
880 id: response_id,
881 result: Some(serde_json::json!({ "deleted": route })),
882 error: None,
883 },
884 Err(err) => JsonRpcResponse {
885 jsonrpc: JSONRPC_VERSION.to_string(),
886 id: response_id,
887 result: None,
888 error: Some(JsonRpcError {
889 code: -32602,
890 message: format!("Invalid params: {}", err.message()),
891 data: None,
892 }),
893 },
894 },
895 "mobkit/delivery/send" => {
896 match parse_delivery_send_params(&request.params).and_then(|send_request| {
897 runtime
898 .send_delivery(send_request)
899 .map_err(RoutingDeliveryParamsError::Delivery)
900 }) {
901 Ok(record) => JsonRpcResponse {
902 jsonrpc: JSONRPC_VERSION.to_string(),
903 id: response_id,
904 result: Some(serde_json::to_value(record).unwrap_or(Value::Null)),
905 error: None,
906 },
907 Err(err) => JsonRpcResponse {
908 jsonrpc: JSONRPC_VERSION.to_string(),
909 id: response_id,
910 result: None,
911 error: Some(JsonRpcError {
912 code: -32602,
913 message: format!("Invalid params: {}", err.message()),
914 data: None,
915 }),
916 },
917 }
918 }
919 "mobkit/delivery/history" => match parse_delivery_history_params(&request.params) {
920 Ok(history_request) => JsonRpcResponse {
921 jsonrpc: JSONRPC_VERSION.to_string(),
922 id: response_id,
923 result: Some(
924 serde_json::to_value(runtime.delivery_history(history_request))
925 .unwrap_or(Value::Null),
926 ),
927 error: None,
928 },
929 Err(err) => JsonRpcResponse {
930 jsonrpc: JSONRPC_VERSION.to_string(),
931 id: response_id,
932 result: None,
933 error: Some(JsonRpcError {
934 code: -32602,
935 message: format!("Invalid params: {}", err.message()),
936 data: None,
937 }),
938 },
939 },
940 "mobkit/events/subscribe" => {
941 match parse_subscribe_request(&request.params).and_then(|subscribe_request| {
942 runtime
943 .subscribe_events(subscribe_request)
944 .map_err(SubscribeParamsError::Runtime)
945 }) {
946 Ok(subscribe_result) => JsonRpcResponse {
947 jsonrpc: JSONRPC_VERSION.to_string(),
948 id: response_id,
949 result: Some(serde_json::to_value(subscribe_result).unwrap_or(Value::Null)),
950 error: None,
951 },
952 Err(err) => JsonRpcResponse {
953 jsonrpc: JSONRPC_VERSION.to_string(),
954 id: response_id,
955 result: None,
956 error: Some(JsonRpcError {
957 code: -32602,
958 message: format!("Invalid params: {}", err.message()),
959 data: None,
960 }),
961 },
962 }
963 }
964 "mobkit/memory/stores" => match parse_memory_stores_params(&request.params) {
965 Ok(()) => JsonRpcResponse {
966 jsonrpc: JSONRPC_VERSION.to_string(),
967 id: response_id,
968 result: Some(serde_json::json!({
969 "stores": runtime.memory_stores(),
970 })),
971 error: None,
972 },
973 Err(err) => JsonRpcResponse {
974 jsonrpc: JSONRPC_VERSION.to_string(),
975 id: response_id,
976 result: None,
977 error: Some(JsonRpcError {
978 code: -32602,
979 message: format!("Invalid params: {}", err.message()),
980 data: None,
981 }),
982 },
983 },
984 "mobkit/memory/index" => match parse_memory_index_params(&request.params) {
985 Ok(index_request) => match runtime.memory_index(index_request) {
986 Ok(indexed) => JsonRpcResponse {
987 jsonrpc: JSONRPC_VERSION.to_string(),
988 id: response_id,
989 result: Some(serde_json::to_value(indexed).unwrap_or(Value::Null)),
990 error: None,
991 },
992 Err(MemoryIndexError::BackendPersistFailed(error)) => JsonRpcResponse {
993 jsonrpc: JSONRPC_VERSION.to_string(),
994 id: response_id,
995 result: None,
996 error: Some(JsonRpcError {
997 code: MEMORY_BACKEND_UNAVAILABLE_CODE,
998 message: format!(
999 "Memory backend unavailable: {}",
1000 MemoryParamsError::backend_message(&error)
1001 ),
1002 data: None,
1003 }),
1004 },
1005 Err(err) => JsonRpcResponse {
1006 jsonrpc: JSONRPC_VERSION.to_string(),
1007 id: response_id,
1008 result: None,
1009 error: Some(JsonRpcError {
1010 code: -32602,
1011 message: format!(
1012 "Invalid params: {}",
1013 MemoryParamsError::Index(err).message()
1014 ),
1015 data: None,
1016 }),
1017 },
1018 },
1019 Err(err) => JsonRpcResponse {
1020 jsonrpc: JSONRPC_VERSION.to_string(),
1021 id: response_id,
1022 result: None,
1023 error: Some(JsonRpcError {
1024 code: -32602,
1025 message: format!("Invalid params: {}", err.message()),
1026 data: None,
1027 }),
1028 },
1029 },
1030 "mobkit/memory/query" => match parse_memory_query_params(&request.params) {
1031 Ok(query_request) => JsonRpcResponse {
1032 jsonrpc: JSONRPC_VERSION.to_string(),
1033 id: response_id,
1034 result: Some(
1035 serde_json::to_value(runtime.memory_query(query_request))
1036 .unwrap_or(Value::Null),
1037 ),
1038 error: None,
1039 },
1040 Err(err) => JsonRpcResponse {
1041 jsonrpc: JSONRPC_VERSION.to_string(),
1042 id: response_id,
1043 result: None,
1044 error: Some(JsonRpcError {
1045 code: -32602,
1046 message: format!("Invalid params: {}", err.message()),
1047 data: None,
1048 }),
1049 },
1050 },
1051 "mobkit/session_store/bigquery" => {
1052 match parse_bigquery_session_store_params(&request.params)
1053 .and_then(run_bigquery_session_store_request)
1054 {
1055 Ok(result) => JsonRpcResponse {
1056 jsonrpc: JSONRPC_VERSION.to_string(),
1057 id: response_id,
1058 result: Some(result),
1059 error: None,
1060 },
1061 Err(BigQuerySessionStoreRpcError::Params(message)) => JsonRpcResponse {
1062 jsonrpc: JSONRPC_VERSION.to_string(),
1063 id: response_id,
1064 result: None,
1065 error: Some(JsonRpcError {
1066 code: -32602,
1067 message: format!("Invalid params: {message}"),
1068 data: None,
1069 }),
1070 },
1071 Err(BigQuerySessionStoreRpcError::Store(error)) => JsonRpcResponse {
1072 jsonrpc: JSONRPC_VERSION.to_string(),
1073 id: response_id,
1074 result: None,
1075 error: Some(JsonRpcError {
1076 code: -32011,
1077 message: format!(
1078 "BigQuery session store request failed: {}",
1079 format_bigquery_store_error(&error)
1080 ),
1081 data: None,
1082 }),
1083 },
1084 }
1085 }
1086 "mobkit/gating/evaluate" => match parse_gating_evaluate_params(&request.params) {
1087 Ok(gating_request) => JsonRpcResponse {
1088 jsonrpc: JSONRPC_VERSION.to_string(),
1089 id: response_id,
1090 result: Some(
1091 serde_json::to_value(runtime.evaluate_gating_action(gating_request))
1092 .unwrap_or(Value::Null),
1093 ),
1094 error: None,
1095 },
1096 Err(err) => JsonRpcResponse {
1097 jsonrpc: JSONRPC_VERSION.to_string(),
1098 id: response_id,
1099 result: None,
1100 error: Some(JsonRpcError {
1101 code: -32602,
1102 message: format!("Invalid params: {}", err.message()),
1103 data: None,
1104 }),
1105 },
1106 },
1107 "mobkit/gating/pending" => match parse_gating_pending_params(&request.params) {
1108 Ok(()) => JsonRpcResponse {
1109 jsonrpc: JSONRPC_VERSION.to_string(),
1110 id: response_id,
1111 result: Some(serde_json::json!({
1112 "pending": runtime.list_gating_pending(),
1113 })),
1114 error: None,
1115 },
1116 Err(err) => JsonRpcResponse {
1117 jsonrpc: JSONRPC_VERSION.to_string(),
1118 id: response_id,
1119 result: None,
1120 error: Some(JsonRpcError {
1121 code: -32602,
1122 message: format!("Invalid params: {}", err.message()),
1123 data: None,
1124 }),
1125 },
1126 },
1127 "mobkit/gating/decide" => {
1128 match parse_gating_decide_params(&request.params).and_then(|decide_request| {
1129 runtime
1130 .decide_gating_action(decide_request)
1131 .map_err(GatingParamsError::Decision)
1132 }) {
1133 Ok(result) => JsonRpcResponse {
1134 jsonrpc: JSONRPC_VERSION.to_string(),
1135 id: response_id,
1136 result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
1137 error: None,
1138 },
1139 Err(err) => JsonRpcResponse {
1140 jsonrpc: JSONRPC_VERSION.to_string(),
1141 id: response_id,
1142 result: None,
1143 error: Some(JsonRpcError {
1144 code: -32602,
1145 message: format!("Invalid params: {}", err.message()),
1146 data: None,
1147 }),
1148 },
1149 }
1150 }
1151 "mobkit/gating/audit" => match parse_gating_audit_params(&request.params) {
1152 Ok(limit) => JsonRpcResponse {
1153 jsonrpc: JSONRPC_VERSION.to_string(),
1154 id: response_id,
1155 result: Some(serde_json::json!({
1156 "entries": runtime.gating_audit_entries(limit),
1157 })),
1158 error: None,
1159 },
1160 Err(err) => JsonRpcResponse {
1161 jsonrpc: JSONRPC_VERSION.to_string(),
1162 id: response_id,
1163 result: None,
1164 error: Some(JsonRpcError {
1165 code: -32602,
1166 message: format!("Invalid params: {}", err.message()),
1167 data: None,
1168 }),
1169 },
1170 },
1171 "mobkit/call_tool" => {
1172 let module_id = request.params.get("module_id").and_then(Value::as_str);
1173 let tool = request.params.get("tool").and_then(Value::as_str);
1174 let arguments = request
1175 .params
1176 .get("arguments")
1177 .cloned()
1178 .unwrap_or(serde_json::json!({}));
1179
1180 match (module_id, tool) {
1181 (Some(module_id), Some(tool)) if !module_id.is_empty() && !tool.is_empty() => {
1182 let route = route_module_call(
1183 runtime,
1184 &ModuleRouteRequest {
1185 module_id: module_id.to_string(),
1186 method: tool.to_string(),
1187 params: arguments,
1188 },
1189 timeout,
1190 );
1191 match route {
1192 Ok(response) => JsonRpcResponse {
1193 jsonrpc: JSONRPC_VERSION.to_string(),
1194 id: response_id,
1195 result: Some(serde_json::json!({
1196 "module_id": response.module_id,
1197 "tool": response.method,
1198 "result": response.payload
1199 })),
1200 error: None,
1201 },
1202 Err(ModuleRouteError::UnloadedModule(mid)) => JsonRpcResponse {
1203 jsonrpc: JSONRPC_VERSION.to_string(),
1204 id: response_id,
1205 result: None,
1206 error: Some(JsonRpcError {
1207 code: -32601,
1208 message: format!("Module '{mid}' not loaded"),
1209 data: None,
1210 }),
1211 },
1212 Err(err) => JsonRpcResponse {
1213 jsonrpc: JSONRPC_VERSION.to_string(),
1214 id: response_id,
1215 result: None,
1216 error: Some(JsonRpcError {
1217 code: -32000,
1218 message: format!("Tool call failed: {err:?}"),
1219 data: None,
1220 }),
1221 },
1222 }
1223 }
1224 _ => JsonRpcResponse {
1225 jsonrpc: JSONRPC_VERSION.to_string(),
1226 id: response_id,
1227 result: None,
1228 error: Some(JsonRpcError {
1229 code: -32602,
1230 message: "Invalid params: module_id and tool required".to_string(),
1231 data: None,
1232 }),
1233 },
1234 }
1235 }
1236 method if method.contains('/') && !method.starts_with("mobkit/") => {
1237 let module_id = method
1238 .split('/')
1239 .next()
1240 .map(ToString::to_string)
1241 .unwrap_or_default();
1242 let route = route_module_call(
1243 runtime,
1244 &ModuleRouteRequest {
1245 module_id,
1246 method: method.to_string(),
1247 params: request.params,
1248 },
1249 timeout,
1250 );
1251 match route {
1252 Ok(response) => JsonRpcResponse {
1253 jsonrpc: JSONRPC_VERSION.to_string(),
1254 id: response_id,
1255 result: Some(serde_json::json!({
1256 "module_id": response.module_id,
1257 "method": response.method,
1258 "payload": response.payload
1259 })),
1260 error: None,
1261 },
1262 Err(ModuleRouteError::UnloadedModule(module_id)) => JsonRpcResponse {
1263 jsonrpc: JSONRPC_VERSION.to_string(),
1264 id: response_id,
1265 result: None,
1266 error: Some(JsonRpcError {
1267 code: -32601,
1268 message: format!("Module '{module_id}' not loaded"),
1269 data: None,
1270 }),
1271 },
1272 Err(err) => JsonRpcResponse {
1273 jsonrpc: JSONRPC_VERSION.to_string(),
1274 id: response_id,
1275 result: None,
1276 error: Some(JsonRpcError {
1277 code: -32000,
1278 message: format!("Module route failed: {err:?}"),
1279 data: None,
1280 }),
1281 },
1282 }
1283 }
1284 _ => JsonRpcResponse {
1285 jsonrpc: JSONRPC_VERSION.to_string(),
1286 id: response_id,
1287 result: None,
1288 error: Some(JsonRpcError {
1289 code: -32601,
1290 message: "Method not found".to_string(),
1291 data: None,
1292 }),
1293 },
1294 };
1295 if is_notification {
1296 String::new()
1297 } else {
1298 serialize_response(&response)
1299 }
1300}
1301
1302pub struct IdentityFirstContext {
1304 pub runtime: std::sync::Arc<crate::identity_first::IdentityRuntime>,
1305 pub roster_provider: std::sync::Arc<dyn crate::identity_first::contracts::RosterProvider>,
1306 pub topology_provider:
1307 Option<std::sync::Arc<dyn crate::identity_first::contracts::TopologyProvider>>,
1308 pub customizer: Option<std::sync::Arc<dyn crate::identity_first::contracts::AgentCustomizer>>,
1309 pub agent_memory_provider:
1310 Option<std::sync::Arc<dyn crate::identity_first::AgentMemoryProvider>>,
1311 pub mob_definition: Option<meerkat_mob::MobDefinition>,
1312}
1313
1314#[derive(Debug, Clone, PartialEq, Eq)]
1315enum IdentityMemberReadiness {
1316 Ready,
1317 TimedOut,
1318 Failed(String),
1319}
1320
1321struct IdentityStartupReadyWait {
1322 status: crate::identity_first::IdentityBootstrapStatus,
1323 timed_out: bool,
1324 startup_ready: bool,
1325}
1326
1327async fn wait_identity_startup_ready<F, Fut>(
1332 identity_rt: &crate::identity_first::IdentityRuntime,
1333 wait_timeout: Duration,
1334 mut wait_members: F,
1335) -> Result<IdentityStartupReadyWait, String>
1336where
1337 F: FnMut(Vec<meerkat_mob::ids::AgentIdentity>, Duration) -> Fut + Send,
1338 Fut: Future<Output = IdentityMemberReadiness> + Send,
1339{
1340 let started = std::time::Instant::now();
1341 let (mut status, mut timed_out, mut generation) = identity_rt
1342 .wait_identity_bootstrap_terminal_with_generation(wait_timeout)
1343 .await;
1344 loop {
1345 if timed_out || !status.ready {
1346 return Ok(IdentityStartupReadyWait {
1347 status,
1348 timed_out,
1349 startup_ready: false,
1350 });
1351 }
1352
1353 let mut bootstrap_changes = identity_rt.subscribe_identity_bootstrap_status();
1357 let (current_generation, current_status) =
1358 identity_rt.identity_bootstrap_status_with_generation();
1359 if current_generation != generation || current_status != status {
1360 let remaining = wait_timeout.saturating_sub(started.elapsed());
1361 if remaining.is_zero() {
1362 return Ok(IdentityStartupReadyWait {
1363 status: current_status,
1364 timed_out: true,
1365 startup_ready: false,
1366 });
1367 }
1368 (status, timed_out, generation) = identity_rt
1369 .wait_identity_bootstrap_terminal_with_generation(remaining)
1370 .await;
1371 continue;
1372 }
1373
1374 let member_ids = identity_rt
1375 .identity_bootstrap_member_ids_for_status(&status)
1376 .await;
1377 let (mapped_generation, mapped_status) =
1378 identity_rt.identity_bootstrap_status_with_generation();
1379 if mapped_generation != generation || mapped_status != status {
1380 let remaining = wait_timeout.saturating_sub(started.elapsed());
1381 if remaining.is_zero() {
1382 return Ok(IdentityStartupReadyWait {
1383 status: mapped_status,
1384 timed_out: true,
1385 startup_ready: false,
1386 });
1387 }
1388 (status, timed_out, generation) = identity_rt
1389 .wait_identity_bootstrap_terminal_with_generation(remaining)
1390 .await;
1391 continue;
1392 }
1393 if member_ids.len() != status.identities.len() {
1394 return Err("identity bootstrap readiness mapping is incomplete".to_string());
1395 }
1396
1397 let remaining = wait_timeout.saturating_sub(started.elapsed());
1398 let readiness = wait_members(member_ids, remaining);
1399 tokio::pin!(readiness);
1400 let readiness_result = tokio::select! {
1401 result = &mut readiness => Some(result),
1402 changed = bootstrap_changes.changed() => {
1403 if changed.is_err() {
1404 return Err(
1405 "identity bootstrap readiness status channel closed".to_string(),
1406 );
1407 }
1408 None
1409 }
1410 };
1411 let Some(readiness_result) = readiness_result else {
1412 let remaining = wait_timeout.saturating_sub(started.elapsed());
1413 if remaining.is_zero() {
1414 let (_, latest) = identity_rt.identity_bootstrap_status_with_generation();
1415 return Ok(IdentityStartupReadyWait {
1416 status: latest,
1417 timed_out: true,
1418 startup_ready: false,
1419 });
1420 }
1421 (status, timed_out, generation) = identity_rt
1422 .wait_identity_bootstrap_terminal_with_generation(remaining)
1423 .await;
1424 continue;
1425 };
1426
1427 let (result_generation, result_status) =
1431 identity_rt.identity_bootstrap_status_with_generation();
1432 if result_generation != generation || result_status != status {
1433 let remaining = wait_timeout.saturating_sub(started.elapsed());
1434 if remaining.is_zero() {
1435 return Ok(IdentityStartupReadyWait {
1436 status: result_status,
1437 timed_out: true,
1438 startup_ready: false,
1439 });
1440 }
1441 (status, timed_out, generation) = identity_rt
1442 .wait_identity_bootstrap_terminal_with_generation(remaining)
1443 .await;
1444 continue;
1445 }
1446 match readiness_result {
1447 IdentityMemberReadiness::Ready => {
1448 let (ready_generation, ready_status) =
1449 identity_rt.identity_bootstrap_status_with_generation();
1450 if ready_generation == generation && ready_status == status {
1451 return Ok(IdentityStartupReadyWait {
1452 status,
1453 timed_out: false,
1454 startup_ready: true,
1455 });
1456 }
1457 let remaining = wait_timeout.saturating_sub(started.elapsed());
1458 if remaining.is_zero() {
1459 return Ok(IdentityStartupReadyWait {
1460 status: ready_status,
1461 timed_out: true,
1462 startup_ready: false,
1463 });
1464 }
1465 (status, timed_out, generation) = identity_rt
1466 .wait_identity_bootstrap_terminal_with_generation(remaining)
1467 .await;
1468 }
1469 IdentityMemberReadiness::TimedOut => {
1470 return Ok(IdentityStartupReadyWait {
1471 status,
1472 timed_out: true,
1473 startup_ready: false,
1474 });
1475 }
1476 IdentityMemberReadiness::Failed(error) => {
1477 return Err(format!("identity bootstrap readiness failed: {error}"));
1478 }
1479 }
1480 }
1481}
1482
1483pub fn handle_unified_rpc_json<'a>(
1484 runtime: &'a UnifiedRuntime,
1485 request_json: &'a str,
1486 timeout: Duration,
1487 http_base_url: Option<&'a str>,
1488 identity_ctx: Option<&'a IdentityFirstContext>,
1489) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
1490 handle_unified_rpc_json_with_live(
1491 runtime,
1492 request_json,
1493 timeout,
1494 http_base_url,
1495 identity_ctx,
1496 None,
1497 )
1498}
1499
1500pub fn handle_unified_rpc_json_arc<'a>(
1504 runtime: &'a Arc<UnifiedRuntime>,
1505 request_json: &'a str,
1506 timeout: Duration,
1507 http_base_url: Option<&'a str>,
1508 identity_ctx: Option<&'a IdentityFirstContext>,
1509) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
1510 handle_unified_rpc_json_with_live_arc(
1511 runtime,
1512 request_json,
1513 timeout,
1514 http_base_url,
1515 identity_ctx,
1516 None,
1517 )
1518}
1519
1520pub fn handle_unified_rpc_json_with_live<'a>(
1525 runtime: &'a UnifiedRuntime,
1526 request_json: &'a str,
1527 timeout: Duration,
1528 http_base_url: Option<&'a str>,
1529 identity_ctx: Option<&'a IdentityFirstContext>,
1530 live: Option<&'a crate::live_wiring::LiveRpcHandler>,
1531) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
1532 Box::pin(handle_unified_rpc_json_inner(
1533 runtime,
1534 None,
1535 request_json,
1536 timeout,
1537 http_base_url,
1538 identity_ctx,
1539 live,
1540 ))
1541}
1542
1543pub fn handle_unified_rpc_json_with_live_arc<'a>(
1546 runtime: &'a Arc<UnifiedRuntime>,
1547 request_json: &'a str,
1548 timeout: Duration,
1549 http_base_url: Option<&'a str>,
1550 identity_ctx: Option<&'a IdentityFirstContext>,
1551 live: Option<&'a crate::live_wiring::LiveRpcHandler>,
1552) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
1553 Box::pin(handle_unified_rpc_json_inner(
1554 runtime.as_ref(),
1555 Some(runtime),
1556 request_json,
1557 timeout,
1558 http_base_url,
1559 identity_ctx,
1560 live,
1561 ))
1562}
1563
1564async fn handle_unified_rpc_json_inner(
1565 runtime: &UnifiedRuntime,
1566 runtime_owner: Option<&Arc<UnifiedRuntime>>,
1567 request_json: &str,
1568 timeout: Duration,
1569 http_base_url: Option<&str>,
1570 identity_ctx: Option<&IdentityFirstContext>,
1571 live: Option<&crate::live_wiring::LiveRpcHandler>,
1572) -> String {
1573 let raw_request: Value = match serde_json::from_str(request_json) {
1574 Ok(raw_request) => raw_request,
1575 Err(_) => {
1576 return serialize_response(&JsonRpcResponse {
1577 jsonrpc: JSONRPC_VERSION.to_string(),
1578 id: Value::Null,
1579 result: None,
1580 error: Some(JsonRpcError {
1581 code: -32700,
1582 message: "Parse error".to_string(),
1583 data: None,
1584 }),
1585 });
1586 }
1587 };
1588 let response_id = raw_request
1589 .as_object()
1590 .and_then(|object| object.get("id"))
1591 .cloned()
1592 .unwrap_or(Value::Null);
1593 let request: JsonRpcRequest = match serde_json::from_value(raw_request) {
1594 Ok(request) => request,
1595 Err(_) => {
1596 return serialize_response(&JsonRpcResponse {
1597 jsonrpc: JSONRPC_VERSION.to_string(),
1598 id: response_id,
1599 result: None,
1600 error: Some(JsonRpcError {
1601 code: -32600,
1602 message: "Invalid Request".to_string(),
1603 data: None,
1604 }),
1605 });
1606 }
1607 };
1608 let is_notification = request.id.is_none();
1609 let response_id = request.id.clone().unwrap_or(Value::Null);
1610
1611 if request.jsonrpc != "2.0" {
1612 let response = JsonRpcResponse {
1613 jsonrpc: JSONRPC_VERSION.to_string(),
1614 id: response_id,
1615 result: None,
1616 error: Some(JsonRpcError {
1617 code: -32600,
1618 message: "Invalid Request".to_string(),
1619 data: None,
1620 }),
1621 };
1622 return if is_notification {
1623 String::new()
1624 } else {
1625 serialize_response(&response)
1626 };
1627 }
1628
1629 if let Err(message) =
1630 crate::member_comms_id::validate_public_rpc_member_aliases(&request.params)
1631 {
1632 let response = JsonRpcResponse {
1633 jsonrpc: JSONRPC_VERSION.to_string(),
1634 id: response_id,
1635 result: None,
1636 error: Some(JsonRpcError {
1637 code: -32602,
1638 message: format!("Invalid params: {message}"),
1639 data: None,
1640 }),
1641 };
1642 return if is_notification {
1643 String::new()
1644 } else {
1645 serialize_response(&response)
1646 };
1647 }
1648
1649 let response = match request.method.as_str() {
1650 "mobkit/status" => {
1651 let mob_state = Some(runtime.mob_handle().status_observation_snapshot());
1652 let is_running = runtime.module_is_running().await;
1653 let loaded = runtime.loaded_modules().await;
1654 let mut result = serde_json::json!({
1655 "contract_version": MOBKIT_CONTRACT_VERSION,
1656 "running": is_running,
1657 "loaded_modules": loaded,
1658 "mob_state": format!("{mob_state:?}"),
1659 });
1660 if let Some(url) = http_base_url {
1661 result["http_base_url"] = Value::String(url.to_string());
1662 }
1663 if let Some(ctx) = identity_ctx {
1664 result["identity_bootstrap"] =
1665 serde_json::to_value(ctx.runtime.identity_bootstrap_status())
1666 .unwrap_or(Value::Null);
1667 }
1668 if let Some(storage) = runtime.resolved_storage() {
1669 result["storage"] = storage.status_json();
1670 }
1671 if let Some(job_health) = runtime.job_health_projection() {
1672 result["detached_jobs"] = job_health
1673 .get("detached_jobs")
1674 .cloned()
1675 .unwrap_or(Value::Null);
1676 }
1677 JsonRpcResponse {
1678 jsonrpc: JSONRPC_VERSION.to_string(),
1679 id: response_id,
1680 result: Some(result),
1681 error: None,
1682 }
1683 }
1684 "mobkit/capabilities" => {
1685 let loaded = runtime.loaded_modules().await;
1686 let mut methods = vec![
1687 "mobkit/init",
1688 "mobkit/status",
1689 "mobkit/capabilities",
1690 "mobkit/reconcile",
1691 "mobkit/spawn_member",
1692 "mobkit/scheduling/evaluate",
1693 "mobkit/scheduling/dispatch",
1694 "mobkit/routing/resolve",
1695 "mobkit/routing/routes/list",
1696 "mobkit/routing/routes/add",
1697 "mobkit/routing/routes/delete",
1698 "mobkit/delivery/send",
1699 "mobkit/delivery/history",
1700 "mobkit/events/subscribe",
1701 "mobkit/query_events",
1702 "mobkit/memory/stores",
1703 "mobkit/memory/index",
1704 "mobkit/memory/query",
1705 "mobkit/session_store/bigquery",
1706 "mobkit/gating/evaluate",
1707 "mobkit/gating/pending",
1708 "mobkit/gating/decide",
1709 "mobkit/gating/audit",
1710 "mobkit/call_tool",
1711 "mobkit/models/catalog",
1712 "mobkit/blob/get",
1713 "mobkit/send_message",
1714 "mobkit/find_members",
1715 "mobkit/ensure_member",
1716 "mobkit/list_members",
1717 "mobkit/get_member",
1718 "mobkit/retire_member",
1719 "mobkit/respawn_member",
1720 "mobkit/reconcile_edges",
1721 "mobkit/rediscover",
1722 "mobkit/mob_events/query",
1723 "mobkit/mob_events/subscribe",
1724 "mobkit/cross_mob/peer_info",
1726 "mobkit/cross_mob/wire_local",
1727 "mobkit/cross_mob/unwire_local",
1728 "mobkit/peer_pubkey",
1729 "mobkit/member_status",
1730 "mobkit/identity/resolved_tools",
1731 "mobkit/force_cancel_member",
1732 "mobkit/spawn_helper",
1733 "mobkit/fork_helper",
1734 "mobkit/attach_existing_session",
1735 "mobkit/cancel_flow",
1736 "mobkit/flow_status",
1737 "mobkit/list_flows",
1738 "mobkit/list_runs",
1739 "mobkit/run_flow",
1740 "mobkit/collect_completed",
1741 "mobkit/wait_ready",
1742 "mobkit/mob_labels/set",
1743 "mobkit/mob_labels/get",
1744 "mobkit/mob_labels/delete",
1745 "mobkit/run_labels/set",
1746 "mobkit/run_labels/get",
1747 "mobkit/run_labels/delete",
1748 storage_methods::STORAGE_DOCTOR_METHOD,
1749 ];
1750 methods.extend_from_slice(MOBPACK_AUTHORING_METHODS);
1751 let workgraph_configured = runtime.workgraph_service().is_some();
1755 if workgraph_configured {
1756 methods.extend_from_slice(workgraph_methods::WORKGRAPH_READ_METHODS);
1757 methods.extend_from_slice(workgraph_methods::WORKGRAPH_MUTATE_METHODS);
1758 }
1759 if live.is_some() {
1762 methods.extend_from_slice(&[
1763 "mobkit/live/open",
1764 "mobkit/live/status",
1765 "mobkit/live/close",
1766 "mobkit/live/refresh",
1767 "mobkit/live/send_input",
1768 "mobkit/live/commit_input",
1769 "mobkit/live/interrupt",
1770 "mobkit/live/truncate",
1771 ]);
1772 }
1773 if identity_ctx.is_some() {
1774 methods.extend_from_slice(&[
1775 "mobkit/send",
1776 "mobkit/interact",
1777 "mobkit/dispatch",
1778 "mobkit/subscribe",
1779 "mobkit/status_identity",
1780 "mobkit/respawn",
1781 "mobkit/retire",
1782 "mobkit/reset",
1783 "mobkit/delete_identity",
1784 "mobkit/inspect_identity",
1785 "mobkit/reconcile_identity",
1786 "mobkit/status_identity_bootstrap",
1787 "mobkit/wait_identity_bootstrap",
1788 ]);
1789 }
1790 if identity_ctx
1791 .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1792 .is_some()
1793 {
1794 methods.push("mobkit/agent_memory/recall");
1795 if identity_ctx
1796 .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1797 .is_some_and(|provider| provider.supports_remember())
1798 {
1799 methods.push("mobkit/agent_memory/remember");
1800 }
1801 if identity_ctx
1802 .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1803 .is_some_and(|provider| provider.supports_forget())
1804 {
1805 methods.push("mobkit/agent_memory/forget");
1806 }
1807 if identity_ctx
1808 .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1809 .is_some_and(|provider| provider.supports_supersede())
1810 {
1811 methods.push("mobkit/agent_memory/update");
1812 }
1813 if identity_ctx
1814 .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1815 .is_some_and(|provider| provider.supports_manifest())
1816 {
1817 methods.push("mobkit/agent_memory/manifest");
1818 }
1819 }
1820 if runtime.has_contact_directory() {
1822 methods.push("mobkit/cross_mob/directory");
1823 }
1824 if runtime.has_peer_mob_handles().await && runtime.has_inproc_contacts() {
1828 methods.extend_from_slice(&[
1829 "mobkit/cross_mob/wire",
1830 "mobkit/cross_mob/unwire",
1831 "mobkit/cross_mob/send",
1832 ]);
1833 }
1834 let job_health = runtime.job_health_projection();
1835 if job_health.is_some() {
1836 methods.extend_from_slice(&[
1837 "jobs/get",
1838 "jobs/list",
1839 "jobs/cancel",
1840 "jobs/progress",
1841 "jobs/result",
1842 "jobs/artifacts",
1843 "jobs/retry",
1844 "jobs/health",
1845 "jobs/subscribe",
1846 "jobs/unsubscribe",
1847 ]);
1848 if job_health
1849 .as_ref()
1850 .and_then(|projection| projection.get("monitors_available"))
1851 .and_then(Value::as_bool)
1852 .unwrap_or(false)
1853 {
1854 methods.push("monitors/start");
1855 }
1856 }
1857 let topology = runtime.topology_runtime_handle();
1858 let (topology_methods, topology_capabilities) =
1859 topology_methods::capability_projection(&topology, None, false);
1860 methods.extend(topology_methods);
1861 let storage = runtime
1865 .resolved_storage()
1866 .map(|summary| summary.status_json())
1867 .unwrap_or(Value::Null);
1868 JsonRpcResponse {
1869 jsonrpc: JSONRPC_VERSION.to_string(),
1870 id: response_id,
1871 result: Some(serde_json::json!({
1872 "contract_version": MOBKIT_CONTRACT_VERSION,
1873 "runtime_type": "unified",
1874 "methods": methods,
1875 "storage": storage,
1876 "identity_first": identity_ctx.is_some(),
1880 "workgraph": workgraph_configured,
1883 "detached_jobs": job_health
1884 .as_ref()
1885 .and_then(|projection| projection.get("detached_jobs"))
1886 .cloned()
1887 .unwrap_or(Value::Null),
1888 "loaded_modules": loaded,
1889 "runtime_capabilities": {
1890 "can_spawn_members": true,
1891 "can_send_messages": true,
1892 "can_wire_members": true,
1893 "can_retire_members": true,
1894 "available_spawn_modes": ["module", "profile"],
1895 },
1896 "authoring_capabilities": mobpack_authoring_capabilities(),
1897 "topology_control": topology_capabilities,
1898 })),
1899 error: None,
1900 }
1901 }
1902 topology_methods::TOPOLOGY_QUERY_METHOD => {
1903 let topology = runtime.topology_runtime_handle();
1904 topology_methods::handle_query(&topology, response_id, None, false).await
1905 }
1906 topology_methods::TOPOLOGY_PLAN_METHOD => {
1907 let topology = runtime.topology_runtime_handle();
1908 topology_methods::handle_plan(&topology, response_id, &request.params, None).await
1909 }
1910 topology_methods::TOPOLOGY_APPLY_METHOD => {
1911 let topology = runtime.topology_runtime_handle();
1912 topology_methods::handle_apply(
1913 &topology,
1914 response_id,
1915 &request.params,
1916 None,
1917 Some("local-host"),
1918 )
1919 .await
1920 }
1921 topology_methods::TOPOLOGY_OPERATION_METHOD => {
1922 let topology = runtime.topology_runtime_handle();
1923 topology_methods::handle_operation(&topology, response_id, &request.params, None).await
1924 }
1925 topology_methods::TOPOLOGY_AUDIT_METHOD => {
1926 let topology = runtime.topology_runtime_handle();
1927 topology_methods::handle_audit(&topology, response_id, &request.params, None).await
1928 }
1929 "mobkit/reconcile" => {
1930 let modules = match params::required_string_array(&request.params, "modules") {
1931 Ok(m) => m,
1932 Err(reason) => {
1933 return serialize_response(&JsonRpcResponse {
1934 jsonrpc: JSONRPC_VERSION.to_string(),
1935 id: response_id,
1936 result: None,
1937 error: Some(JsonRpcError {
1938 code: -32602,
1939 message: format!("Invalid params: {reason}"),
1940 data: None,
1941 }),
1942 });
1943 }
1944 };
1945
1946 match runtime.reconcile_modules(modules.clone(), timeout).await {
1947 Ok(added) => JsonRpcResponse {
1948 jsonrpc: JSONRPC_VERSION.to_string(),
1949 id: response_id,
1950 result: Some(serde_json::json!({
1951 "accepted": true,
1952 "reconciled_modules": modules,
1953 "added": added
1954 })),
1955 error: None,
1956 },
1957 Err(err) => JsonRpcResponse {
1958 jsonrpc: JSONRPC_VERSION.to_string(),
1959 id: response_id,
1960 result: None,
1961 error: Some(JsonRpcError {
1962 code: -32602,
1963 message: format!("Invalid params: {err:?}"),
1964 data: None,
1965 }),
1966 },
1967 }
1968 }
1969 "mobkit/spawn_member" => {
1970 let module_id = request.params.get("module_id").and_then(Value::as_str);
1972 let profile = request.params.get("profile").and_then(Value::as_str);
1973 let meerkat_id = request.params.get("meerkat_id").and_then(Value::as_str);
1974
1975 if let Some(module_id) = module_id {
1976 if module_id.is_empty() {
1978 JsonRpcResponse {
1979 jsonrpc: JSONRPC_VERSION.to_string(),
1980 id: response_id,
1981 result: None,
1982 error: Some(JsonRpcError {
1983 code: -32602,
1984 message: "Invalid params: module_id required".to_string(),
1985 data: None,
1986 }),
1987 }
1988 } else {
1989 match runtime.spawn_member(module_id, timeout).await {
1990 Ok(()) => JsonRpcResponse {
1991 jsonrpc: JSONRPC_VERSION.to_string(),
1992 id: response_id,
1993 result: Some(serde_json::json!({
1994 "accepted": true,
1995 "module_id": module_id
1996 })),
1997 error: None,
1998 },
1999 Err(err) => JsonRpcResponse {
2000 jsonrpc: JSONRPC_VERSION.to_string(),
2001 id: response_id,
2002 result: None,
2003 error: Some(JsonRpcError {
2004 code: -32602,
2005 message: format!("Invalid params: {err:?}"),
2006 data: None,
2007 }),
2008 },
2009 }
2010 }
2011 } else if let (Some(profile), Some(meerkat_id)) = (profile, meerkat_id) {
2012 let target_identity_runtime = identity_ctx
2017 .map(|ctx| &ctx.runtime)
2018 .or_else(|| runtime.identity_runtime());
2019 let raw_target_validation = crate::member_comms_id::validate_raw_member_target(
2020 target_identity_runtime,
2021 meerkat_id,
2022 )
2023 .await;
2024 if meerkat_id == crate::console_contracts::SYSTEM_EVENT_IDENTITY {
2025 JsonRpcResponse {
2026 jsonrpc: JSONRPC_VERSION.to_string(),
2027 id: response_id,
2028 result: None,
2029 error: Some(JsonRpcError {
2030 code: -32602,
2031 message: format!("Invalid params: '{meerkat_id}' is reserved"),
2032 data: None,
2033 }),
2034 }
2035 } else if let Err(message) = raw_target_validation.as_ref() {
2036 JsonRpcResponse {
2037 jsonrpc: JSONRPC_VERSION.to_string(),
2038 id: response_id,
2039 result: None,
2040 error: Some(JsonRpcError {
2041 code: -32602,
2042 message: format!("Invalid params: {message}"),
2043 data: None,
2044 }),
2045 }
2046 } else {
2047 let compatibility_reservation = if runtime.identity_runtime().is_none() {
2053 crate::member_comms_id::reserve_raw_member_target(
2054 target_identity_runtime,
2055 meerkat_id,
2056 )
2057 .await
2058 .map(Some)
2059 } else {
2060 Ok(None)
2061 };
2062 match compatibility_reservation {
2063 Err(message) => JsonRpcResponse {
2064 jsonrpc: JSONRPC_VERSION.to_string(),
2065 id: response_id,
2066 result: None,
2067 error: Some(JsonRpcError {
2068 code: -32602,
2069 message: format!("Invalid params: {message}"),
2070 data: None,
2071 }),
2072 },
2073 Ok(compatibility_reservation) => {
2074 let member_id = compatibility_reservation
2075 .as_ref()
2076 .map(|reservation| reservation.alias().to_string())
2077 .or_else(|| raw_target_validation.as_ref().ok().cloned())
2078 .unwrap_or_else(|| meerkat_id.trim().to_string());
2079 let spec = meerkat_mob::SpawnMemberSpec::from_wire(
2081 profile.to_string(),
2082 member_id.clone(),
2083 request
2084 .params
2085 .get("initial_message")
2086 .and_then(Value::as_str)
2087 .map(|s| meerkat_core::ContentInput::from(s.to_string())),
2088 None,
2089 None,
2090 );
2091 let spawn_result = Box::pin(runtime.spawn(spec)).await;
2092 drop(compatibility_reservation);
2093 match spawn_result {
2094 Ok(_member_ref) => JsonRpcResponse {
2095 jsonrpc: JSONRPC_VERSION.to_string(),
2096 id: response_id,
2097 result: Some(serde_json::json!({
2098 "accepted": true,
2099 "meerkat_id": member_id
2100 })),
2101 error: None,
2102 },
2103 Err(err) => JsonRpcResponse {
2104 jsonrpc: JSONRPC_VERSION.to_string(),
2105 id: response_id,
2106 result: None,
2107 error: Some(JsonRpcError {
2108 code: -32602,
2109 message: format!("Invalid params: {err}"),
2110 data: None,
2111 }),
2112 },
2113 }
2114 }
2115 }
2116 }
2117 } else {
2118 JsonRpcResponse {
2119 jsonrpc: JSONRPC_VERSION.to_string(),
2120 id: response_id,
2121 result: None,
2122 error: Some(JsonRpcError {
2123 code: -32602,
2124 message: "Invalid params: module_id or (profile + meerkat_id) required"
2125 .to_string(),
2126 data: None,
2127 }),
2128 }
2129 }
2130 }
2131 "mobkit/scheduling/evaluate" => match parse_scheduling_params(&request.params) {
2132 Ok((schedules, tick_ms)) => {
2133 match runtime.evaluate_schedule_tick(&schedules, tick_ms).await {
2134 Ok(evaluation) => JsonRpcResponse {
2135 jsonrpc: JSONRPC_VERSION.to_string(),
2136 id: response_id,
2137 result: Some(serde_json::to_value(evaluation).unwrap_or(Value::Null)),
2138 error: None,
2139 },
2140 Err(err) => JsonRpcResponse {
2141 jsonrpc: JSONRPC_VERSION.to_string(),
2142 id: response_id,
2143 result: None,
2144 error: Some(JsonRpcError {
2145 code: -32602,
2146 message: format!(
2147 "Invalid params: {}",
2148 format_schedule_validation_error(err)
2149 ),
2150 data: None,
2151 }),
2152 },
2153 }
2154 }
2155 Err(message) => JsonRpcResponse {
2156 jsonrpc: JSONRPC_VERSION.to_string(),
2157 id: response_id,
2158 result: None,
2159 error: Some(JsonRpcError {
2160 code: -32602,
2161 message: format!("Invalid params: {message}"),
2162 data: None,
2163 }),
2164 },
2165 },
2166 "mobkit/scheduling/dispatch" => match parse_scheduling_params(&request.params) {
2167 Ok((schedules, tick_ms)) => {
2168 match runtime.dispatch_schedule_tick(&schedules, tick_ms).await {
2169 Ok(dispatch) => JsonRpcResponse {
2170 jsonrpc: JSONRPC_VERSION.to_string(),
2171 id: response_id,
2172 result: Some(serde_json::to_value(dispatch).unwrap_or(Value::Null)),
2173 error: None,
2174 },
2175 Err(err) => JsonRpcResponse {
2176 jsonrpc: JSONRPC_VERSION.to_string(),
2177 id: response_id,
2178 result: None,
2179 error: Some(JsonRpcError {
2180 code: -32602,
2181 message: format!("Invalid params: {err}"),
2182 data: None,
2183 }),
2184 },
2185 }
2186 }
2187 Err(message) => JsonRpcResponse {
2188 jsonrpc: JSONRPC_VERSION.to_string(),
2189 id: response_id,
2190 result: None,
2191 error: Some(JsonRpcError {
2192 code: -32602,
2193 message: format!("Invalid params: {message}"),
2194 data: None,
2195 }),
2196 },
2197 },
2198 "mobkit/routing/resolve" => {
2199 let resolve_result = match parse_routing_resolve_params(&request.params) {
2200 Ok(resolve_request) => runtime
2201 .resolve_routing(resolve_request)
2202 .await
2203 .map_err(RoutingDeliveryParamsError::Routing),
2204 Err(e) => Err(e),
2205 };
2206 match resolve_result {
2207 Ok(resolution) => JsonRpcResponse {
2208 jsonrpc: JSONRPC_VERSION.to_string(),
2209 id: response_id,
2210 result: Some(serde_json::to_value(resolution).unwrap_or(Value::Null)),
2211 error: None,
2212 },
2213 Err(err) => JsonRpcResponse {
2214 jsonrpc: JSONRPC_VERSION.to_string(),
2215 id: response_id,
2216 result: None,
2217 error: Some(JsonRpcError {
2218 code: -32602,
2219 message: format!("Invalid params: {}", err.message()),
2220 data: None,
2221 }),
2222 },
2223 }
2224 }
2225 "mobkit/routing/routes/list" => match parse_routing_routes_list_params(&request.params) {
2226 Ok(()) => {
2227 let routes = runtime.list_runtime_routes().await;
2228 JsonRpcResponse {
2229 jsonrpc: JSONRPC_VERSION.to_string(),
2230 id: response_id,
2231 result: Some(serde_json::json!({
2232 "routes": routes
2233 })),
2234 error: None,
2235 }
2236 }
2237 Err(err) => JsonRpcResponse {
2238 jsonrpc: JSONRPC_VERSION.to_string(),
2239 id: response_id,
2240 result: None,
2241 error: Some(JsonRpcError {
2242 code: -32602,
2243 message: format!("Invalid params: {}", err.message()),
2244 data: None,
2245 }),
2246 },
2247 },
2248 "mobkit/routing/routes/add" => {
2249 let add_result = match parse_routing_route_add_params(&request.params) {
2250 Ok(route) => runtime
2251 .add_runtime_route(route)
2252 .await
2253 .map_err(RoutingDeliveryParamsError::RouteMutation),
2254 Err(e) => Err(e),
2255 };
2256 match add_result {
2257 Ok(route) => JsonRpcResponse {
2258 jsonrpc: JSONRPC_VERSION.to_string(),
2259 id: response_id,
2260 result: Some(serde_json::json!({ "route": route })),
2261 error: None,
2262 },
2263 Err(err) => JsonRpcResponse {
2264 jsonrpc: JSONRPC_VERSION.to_string(),
2265 id: response_id,
2266 result: None,
2267 error: Some(JsonRpcError {
2268 code: -32602,
2269 message: format!("Invalid params: {}", err.message()),
2270 data: None,
2271 }),
2272 },
2273 }
2274 }
2275 "mobkit/routing/routes/delete" => {
2276 let delete_result = match parse_routing_route_delete_params(&request.params) {
2277 Ok(route_key) => runtime
2278 .delete_runtime_route(&route_key)
2279 .await
2280 .map_err(RoutingDeliveryParamsError::RouteMutation),
2281 Err(e) => Err(e),
2282 };
2283 match delete_result {
2284 Ok(route) => JsonRpcResponse {
2285 jsonrpc: JSONRPC_VERSION.to_string(),
2286 id: response_id,
2287 result: Some(serde_json::json!({ "deleted": route })),
2288 error: None,
2289 },
2290 Err(err) => JsonRpcResponse {
2291 jsonrpc: JSONRPC_VERSION.to_string(),
2292 id: response_id,
2293 result: None,
2294 error: Some(JsonRpcError {
2295 code: -32602,
2296 message: format!("Invalid params: {}", err.message()),
2297 data: None,
2298 }),
2299 },
2300 }
2301 }
2302 "mobkit/delivery/send" => {
2303 let send_result = match parse_delivery_send_params(&request.params) {
2304 Ok(send_request) => runtime
2305 .send_delivery(send_request)
2306 .await
2307 .map_err(RoutingDeliveryParamsError::Delivery),
2308 Err(e) => Err(e),
2309 };
2310 match send_result {
2311 Ok(record) => JsonRpcResponse {
2312 jsonrpc: JSONRPC_VERSION.to_string(),
2313 id: response_id,
2314 result: Some(serde_json::to_value(record).unwrap_or(Value::Null)),
2315 error: None,
2316 },
2317 Err(err) => JsonRpcResponse {
2318 jsonrpc: JSONRPC_VERSION.to_string(),
2319 id: response_id,
2320 result: None,
2321 error: Some(JsonRpcError {
2322 code: -32602,
2323 message: format!("Invalid params: {}", err.message()),
2324 data: None,
2325 }),
2326 },
2327 }
2328 }
2329 "mobkit/delivery/history" => match parse_delivery_history_params(&request.params) {
2330 Ok(history_request) => {
2331 let history = runtime.delivery_history(history_request).await;
2332 JsonRpcResponse {
2333 jsonrpc: JSONRPC_VERSION.to_string(),
2334 id: response_id,
2335 result: Some(serde_json::to_value(history).unwrap_or(Value::Null)),
2336 error: None,
2337 }
2338 }
2339 Err(err) => JsonRpcResponse {
2340 jsonrpc: JSONRPC_VERSION.to_string(),
2341 id: response_id,
2342 result: None,
2343 error: Some(JsonRpcError {
2344 code: -32602,
2345 message: format!("Invalid params: {}", err.message()),
2346 data: None,
2347 }),
2348 },
2349 },
2350 "mobkit/events/subscribe" => match parse_subscribe_request(&request.params) {
2351 Ok(subscribe_request) => match runtime.subscribe_events(subscribe_request).await {
2352 Ok(subscribe_result) => JsonRpcResponse {
2353 jsonrpc: JSONRPC_VERSION.to_string(),
2354 id: response_id,
2355 result: Some(serde_json::to_value(subscribe_result).unwrap_or(Value::Null)),
2356 error: None,
2357 },
2358 Err(err) => JsonRpcResponse {
2359 jsonrpc: JSONRPC_VERSION.to_string(),
2360 id: response_id,
2361 result: None,
2362 error: Some(JsonRpcError {
2363 code: -32602,
2364 message: format!("Invalid params: {err}"),
2365 data: None,
2366 }),
2367 },
2368 },
2369 Err(err) => JsonRpcResponse {
2370 jsonrpc: JSONRPC_VERSION.to_string(),
2371 id: response_id,
2372 result: None,
2373 error: Some(JsonRpcError {
2374 code: -32602,
2375 message: format!("Invalid params: {}", err.message()),
2376 data: None,
2377 }),
2378 },
2379 },
2380 "mobkit/query_events" => {
2381 let query: EventQuery = if request.params.is_null() {
2382 EventQuery::default()
2383 } else {
2384 match serde_json::from_value(request.params.clone()) {
2385 Ok(query) => query,
2386 Err(err) => {
2387 return serde_json::to_string(&JsonRpcResponse {
2388 jsonrpc: JSONRPC_VERSION.to_string(),
2389 id: response_id,
2390 result: None,
2391 error: Some(JsonRpcError {
2392 code: -32602,
2393 message: format!("Invalid params: invalid query params: {err}"),
2394 data: None,
2395 }),
2396 })
2397 .unwrap_or_else(|_| r#"{"error":"serialization failed"}"#.to_string());
2398 }
2399 }
2400 };
2401 match runtime.event_log_store() {
2402 Some(store) => match store.query(query).await {
2403 Ok(events) => JsonRpcResponse {
2404 jsonrpc: JSONRPC_VERSION.to_string(),
2405 id: response_id,
2406 result: Some(serde_json::to_value(events).unwrap_or(Value::Null)),
2407 error: None,
2408 },
2409 Err(err) => JsonRpcResponse {
2410 jsonrpc: JSONRPC_VERSION.to_string(),
2411 id: response_id,
2412 result: None,
2413 error: Some(JsonRpcError {
2414 code: -32603,
2415 message: format!("query_events failed: {err}"),
2416 data: None,
2417 }),
2418 },
2419 },
2420 None => JsonRpcResponse {
2421 jsonrpc: JSONRPC_VERSION.to_string(),
2422 id: response_id,
2423 result: Some(serde_json::json!({
2424 "status": "no_event_log_configured",
2425 "events": [],
2426 })),
2427 error: None,
2428 },
2429 }
2430 }
2431 "mobkit/memory/stores" => match parse_memory_stores_params(&request.params) {
2432 Ok(()) => {
2433 let stores = runtime.memory_stores().await;
2434 JsonRpcResponse {
2435 jsonrpc: JSONRPC_VERSION.to_string(),
2436 id: response_id,
2437 result: Some(serde_json::json!({
2438 "stores": stores,
2439 })),
2440 error: None,
2441 }
2442 }
2443 Err(err) => JsonRpcResponse {
2444 jsonrpc: JSONRPC_VERSION.to_string(),
2445 id: response_id,
2446 result: None,
2447 error: Some(JsonRpcError {
2448 code: -32602,
2449 message: format!("Invalid params: {}", err.message()),
2450 data: None,
2451 }),
2452 },
2453 },
2454 "mobkit/memory/index" => match parse_memory_index_params(&request.params) {
2455 Ok(index_request) => match runtime.memory_index(index_request).await {
2456 Ok(indexed) => JsonRpcResponse {
2457 jsonrpc: JSONRPC_VERSION.to_string(),
2458 id: response_id,
2459 result: Some(serde_json::to_value(indexed).unwrap_or(Value::Null)),
2460 error: None,
2461 },
2462 Err(MemoryIndexError::BackendPersistFailed(error)) => JsonRpcResponse {
2463 jsonrpc: JSONRPC_VERSION.to_string(),
2464 id: response_id,
2465 result: None,
2466 error: Some(JsonRpcError {
2467 code: MEMORY_BACKEND_UNAVAILABLE_CODE,
2468 message: format!(
2469 "Memory backend unavailable: {}",
2470 MemoryParamsError::backend_message(&error)
2471 ),
2472 data: None,
2473 }),
2474 },
2475 Err(err) => JsonRpcResponse {
2476 jsonrpc: JSONRPC_VERSION.to_string(),
2477 id: response_id,
2478 result: None,
2479 error: Some(JsonRpcError {
2480 code: -32602,
2481 message: format!(
2482 "Invalid params: {}",
2483 MemoryParamsError::Index(err).message()
2484 ),
2485 data: None,
2486 }),
2487 },
2488 },
2489 Err(err) => JsonRpcResponse {
2490 jsonrpc: JSONRPC_VERSION.to_string(),
2491 id: response_id,
2492 result: None,
2493 error: Some(JsonRpcError {
2494 code: -32602,
2495 message: format!("Invalid params: {}", err.message()),
2496 data: None,
2497 }),
2498 },
2499 },
2500 "mobkit/memory/query" => match parse_memory_query_params(&request.params) {
2501 Ok(query_request) => {
2502 let query_result = runtime.memory_query(query_request).await;
2503 JsonRpcResponse {
2504 jsonrpc: JSONRPC_VERSION.to_string(),
2505 id: response_id,
2506 result: Some(serde_json::to_value(query_result).unwrap_or(Value::Null)),
2507 error: None,
2508 }
2509 }
2510 Err(err) => JsonRpcResponse {
2511 jsonrpc: JSONRPC_VERSION.to_string(),
2512 id: response_id,
2513 result: None,
2514 error: Some(JsonRpcError {
2515 code: -32602,
2516 message: format!("Invalid params: {}", err.message()),
2517 data: None,
2518 }),
2519 },
2520 },
2521 "mobkit/agent_memory/remember" => {
2522 let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2523 Some(runtime) => runtime,
2524 None => {
2525 return maybe_error_response(
2526 is_notification,
2527 response_id,
2528 -32601,
2529 "agent memory is not configured".to_string(),
2530 );
2531 }
2532 };
2533 match parse_agent_memory_remember_params(&request.params) {
2534 Ok(remember_request) => match runtime
2535 .remember_agent_memory(
2536 &remember_request.realm,
2537 &remember_request.identity,
2538 remember_request.memory,
2539 )
2540 .await
2541 {
2542 Ok(record) => JsonRpcResponse {
2543 jsonrpc: JSONRPC_VERSION.to_string(),
2544 id: response_id,
2545 result: Some(serde_json::to_value(record).unwrap_or(Value::Null)),
2546 error: None,
2547 },
2548 Err(err) => JsonRpcResponse {
2549 jsonrpc: JSONRPC_VERSION.to_string(),
2550 id: response_id,
2551 result: None,
2552 error: Some(agent_memory_rpc_error("write", err)),
2553 },
2554 },
2555 Err(err) => JsonRpcResponse {
2556 jsonrpc: JSONRPC_VERSION.to_string(),
2557 id: response_id,
2558 result: None,
2559 error: Some(JsonRpcError {
2560 code: -32602,
2561 message: format!("Invalid params: {}", err.message()),
2562 data: None,
2563 }),
2564 },
2565 }
2566 }
2567 "mobkit/agent_memory/forget" => {
2568 let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2569 Some(runtime) => runtime,
2570 None => {
2571 return maybe_error_response(
2572 is_notification,
2573 response_id,
2574 -32601,
2575 "agent memory is not configured".to_string(),
2576 );
2577 }
2578 };
2579 match parse_agent_memory_forget_params(&request.params) {
2580 Ok(forget_request) => match runtime
2581 .forget_agent_memory(
2582 &forget_request.realm,
2583 &forget_request.identity,
2584 &forget_request.memory_id,
2585 )
2586 .await
2587 {
2588 Ok(result) => JsonRpcResponse {
2589 jsonrpc: JSONRPC_VERSION.to_string(),
2590 id: response_id,
2591 result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
2592 error: None,
2593 },
2594 Err(err) => JsonRpcResponse {
2595 jsonrpc: JSONRPC_VERSION.to_string(),
2596 id: response_id,
2597 result: None,
2598 error: Some(agent_memory_rpc_error("forget", err)),
2599 },
2600 },
2601 Err(err) => JsonRpcResponse {
2602 jsonrpc: JSONRPC_VERSION.to_string(),
2603 id: response_id,
2604 result: None,
2605 error: Some(JsonRpcError {
2606 code: -32602,
2607 message: format!("Invalid params: {}", err.message()),
2608 data: None,
2609 }),
2610 },
2611 }
2612 }
2613 "mobkit/agent_memory/recall" => {
2614 let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2615 Some(runtime) => runtime,
2616 None => {
2617 return maybe_error_response(
2618 is_notification,
2619 response_id,
2620 -32601,
2621 "agent memory is not configured".to_string(),
2622 );
2623 }
2624 };
2625 match parse_agent_memory_recall_params(&request.params) {
2626 Ok(recall_request) => {
2627 match runtime.recall_agent_memory(recall_request.request).await {
2628 Ok(records) => JsonRpcResponse {
2629 jsonrpc: JSONRPC_VERSION.to_string(),
2630 id: response_id,
2631 result: Some(serde_json::json!({ "records": records })),
2632 error: None,
2633 },
2634 Err(err) => JsonRpcResponse {
2635 jsonrpc: JSONRPC_VERSION.to_string(),
2636 id: response_id,
2637 result: None,
2638 error: Some(agent_memory_rpc_error("recall", err)),
2639 },
2640 }
2641 }
2642 Err(err) => JsonRpcResponse {
2643 jsonrpc: JSONRPC_VERSION.to_string(),
2644 id: response_id,
2645 result: None,
2646 error: Some(JsonRpcError {
2647 code: -32602,
2648 message: format!("Invalid params: {}", err.message()),
2649 data: None,
2650 }),
2651 },
2652 }
2653 }
2654 "mobkit/agent_memory/update" => {
2655 let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2656 Some(runtime) => runtime,
2657 None => {
2658 return maybe_error_response(
2659 is_notification,
2660 response_id,
2661 -32601,
2662 "agent memory is not configured".to_string(),
2663 );
2664 }
2665 };
2666 match parse_agent_memory_update_params(&request.params) {
2667 Ok(update_request) => match runtime
2668 .update_agent_memory(
2669 &update_request.realm,
2670 &update_request.identity,
2671 &update_request.memory_id,
2672 update_request.memory,
2673 )
2674 .await
2675 {
2676 Ok(new_id) => JsonRpcResponse {
2677 jsonrpc: JSONRPC_VERSION.to_string(),
2678 id: response_id,
2679 result: Some(serde_json::json!({
2680 "memory_id": new_id,
2681 "supersedes": update_request.memory_id,
2682 })),
2683 error: None,
2684 },
2685 Err(err) => JsonRpcResponse {
2686 jsonrpc: JSONRPC_VERSION.to_string(),
2687 id: response_id,
2688 result: None,
2689 error: Some(agent_memory_rpc_error("update", err)),
2690 },
2691 },
2692 Err(err) => JsonRpcResponse {
2693 jsonrpc: JSONRPC_VERSION.to_string(),
2694 id: response_id,
2695 result: None,
2696 error: Some(JsonRpcError {
2697 code: -32602,
2698 message: format!("Invalid params: {}", err.message()),
2699 data: None,
2700 }),
2701 },
2702 }
2703 }
2704 "mobkit/agent_memory/manifest" => {
2705 let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2706 Some(runtime) => runtime,
2707 None => {
2708 return maybe_error_response(
2709 is_notification,
2710 response_id,
2711 -32601,
2712 "agent memory is not configured".to_string(),
2713 );
2714 }
2715 };
2716 match parse_agent_memory_manifest_params(&request.params) {
2717 Ok(manifest_request) => match runtime
2718 .manifest_agent_memory(
2719 &manifest_request.realm,
2720 &manifest_request.identity,
2721 manifest_request.tier,
2722 )
2723 .await
2724 {
2725 Ok(records) => JsonRpcResponse {
2726 jsonrpc: JSONRPC_VERSION.to_string(),
2727 id: response_id,
2728 result: Some(serde_json::json!({ "records": records })),
2729 error: None,
2730 },
2731 Err(err) => JsonRpcResponse {
2732 jsonrpc: JSONRPC_VERSION.to_string(),
2733 id: response_id,
2734 result: None,
2735 error: Some(agent_memory_rpc_error("manifest", err)),
2736 },
2737 },
2738 Err(err) => JsonRpcResponse {
2739 jsonrpc: JSONRPC_VERSION.to_string(),
2740 id: response_id,
2741 result: None,
2742 error: Some(JsonRpcError {
2743 code: -32602,
2744 message: format!("Invalid params: {}", err.message()),
2745 data: None,
2746 }),
2747 },
2748 }
2749 }
2750 "mobkit/session_store/bigquery" => {
2751 match parse_bigquery_session_store_params(&request.params)
2752 .and_then(run_bigquery_session_store_request)
2753 {
2754 Ok(result) => JsonRpcResponse {
2755 jsonrpc: JSONRPC_VERSION.to_string(),
2756 id: response_id,
2757 result: Some(result),
2758 error: None,
2759 },
2760 Err(BigQuerySessionStoreRpcError::Params(message)) => JsonRpcResponse {
2761 jsonrpc: JSONRPC_VERSION.to_string(),
2762 id: response_id,
2763 result: None,
2764 error: Some(JsonRpcError {
2765 code: -32602,
2766 message: format!("Invalid params: {message}"),
2767 data: None,
2768 }),
2769 },
2770 Err(BigQuerySessionStoreRpcError::Store(error)) => JsonRpcResponse {
2771 jsonrpc: JSONRPC_VERSION.to_string(),
2772 id: response_id,
2773 result: None,
2774 error: Some(JsonRpcError {
2775 code: -32011,
2776 message: format!(
2777 "BigQuery session store request failed: {}",
2778 format_bigquery_store_error(&error)
2779 ),
2780 data: None,
2781 }),
2782 },
2783 }
2784 }
2785 "mobkit/gating/evaluate" => match parse_gating_evaluate_params(&request.params) {
2786 Ok(gating_request) => {
2787 let gating_result = runtime.evaluate_gating_action(gating_request).await;
2788 JsonRpcResponse {
2789 jsonrpc: JSONRPC_VERSION.to_string(),
2790 id: response_id,
2791 result: Some(serde_json::to_value(gating_result).unwrap_or(Value::Null)),
2792 error: None,
2793 }
2794 }
2795 Err(err) => JsonRpcResponse {
2796 jsonrpc: JSONRPC_VERSION.to_string(),
2797 id: response_id,
2798 result: None,
2799 error: Some(JsonRpcError {
2800 code: -32602,
2801 message: format!("Invalid params: {}", err.message()),
2802 data: None,
2803 }),
2804 },
2805 },
2806 "mobkit/gating/pending" => match parse_gating_pending_params(&request.params) {
2807 Ok(()) => {
2808 let pending = runtime.list_gating_pending().await;
2809 JsonRpcResponse {
2810 jsonrpc: JSONRPC_VERSION.to_string(),
2811 id: response_id,
2812 result: Some(serde_json::json!({
2813 "pending": pending,
2814 })),
2815 error: None,
2816 }
2817 }
2818 Err(err) => JsonRpcResponse {
2819 jsonrpc: JSONRPC_VERSION.to_string(),
2820 id: response_id,
2821 result: None,
2822 error: Some(JsonRpcError {
2823 code: -32602,
2824 message: format!("Invalid params: {}", err.message()),
2825 data: None,
2826 }),
2827 },
2828 },
2829 "mobkit/gating/decide" => {
2830 let decide_result = match parse_gating_decide_params(&request.params) {
2831 Ok(decide_request) => runtime
2832 .decide_gating_action(decide_request)
2833 .await
2834 .map_err(GatingParamsError::Decision),
2835 Err(e) => Err(e),
2836 };
2837 match decide_result {
2838 Ok(result) => JsonRpcResponse {
2839 jsonrpc: JSONRPC_VERSION.to_string(),
2840 id: response_id,
2841 result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
2842 error: None,
2843 },
2844 Err(err) => JsonRpcResponse {
2845 jsonrpc: JSONRPC_VERSION.to_string(),
2846 id: response_id,
2847 result: None,
2848 error: Some(JsonRpcError {
2849 code: -32602,
2850 message: format!("Invalid params: {}", err.message()),
2851 data: None,
2852 }),
2853 },
2854 }
2855 }
2856 "mobkit/gating/audit" => match parse_gating_audit_params(&request.params) {
2857 Ok(limit) => {
2858 let entries = runtime.gating_audit_entries(limit).await;
2859 JsonRpcResponse {
2860 jsonrpc: JSONRPC_VERSION.to_string(),
2861 id: response_id,
2862 result: Some(serde_json::json!({
2863 "entries": entries,
2864 })),
2865 error: None,
2866 }
2867 }
2868 Err(err) => JsonRpcResponse {
2869 jsonrpc: JSONRPC_VERSION.to_string(),
2870 id: response_id,
2871 result: None,
2872 error: Some(JsonRpcError {
2873 code: -32602,
2874 message: format!("Invalid params: {}", err.message()),
2875 data: None,
2876 }),
2877 },
2878 },
2879 "mobkit/call_tool" => {
2880 let module_id = request.params.get("module_id").and_then(Value::as_str);
2881 let tool = request.params.get("tool").and_then(Value::as_str);
2882 let arguments = request
2883 .params
2884 .get("arguments")
2885 .cloned()
2886 .unwrap_or(serde_json::json!({}));
2887
2888 match (module_id, tool) {
2889 (Some(module_id), Some(tool)) if !module_id.is_empty() && !tool.is_empty() => {
2890 let route = runtime
2891 .route_module_call(
2892 &ModuleRouteRequest {
2893 module_id: module_id.to_string(),
2894 method: tool.to_string(),
2895 params: arguments,
2896 },
2897 timeout,
2898 )
2899 .await;
2900 match route {
2901 Ok(response) => JsonRpcResponse {
2902 jsonrpc: JSONRPC_VERSION.to_string(),
2903 id: response_id,
2904 result: Some(serde_json::json!({
2905 "module_id": response.module_id,
2906 "tool": response.method,
2907 "result": response.payload
2908 })),
2909 error: None,
2910 },
2911 Err(ModuleRouteError::UnloadedModule(mid)) => JsonRpcResponse {
2912 jsonrpc: JSONRPC_VERSION.to_string(),
2913 id: response_id,
2914 result: None,
2915 error: Some(JsonRpcError {
2916 code: -32601,
2917 message: format!("Module '{mid}' not loaded"),
2918 data: None,
2919 }),
2920 },
2921 Err(err) => JsonRpcResponse {
2922 jsonrpc: JSONRPC_VERSION.to_string(),
2923 id: response_id,
2924 result: None,
2925 error: Some(JsonRpcError {
2926 code: -32000,
2927 message: format!("Tool call failed: {err:?}"),
2928 data: None,
2929 }),
2930 },
2931 }
2932 }
2933 _ => JsonRpcResponse {
2934 jsonrpc: JSONRPC_VERSION.to_string(),
2935 id: response_id,
2936 result: None,
2937 error: Some(JsonRpcError {
2938 code: -32602,
2939 message: "Invalid params: module_id and tool required".to_string(),
2940 data: None,
2941 }),
2942 },
2943 }
2944 }
2945 "mobkit/models/catalog" => JsonRpcResponse {
2946 jsonrpc: JSONRPC_VERSION.to_string(),
2947 id: response_id,
2948 result: Some(build_models_catalog_result()),
2949 error: None,
2950 },
2951 storage_methods::STORAGE_DOCTOR_METHOD => {
2955 match storage_methods::parse_storage_doctor_params(&request.params) {
2956 Ok(Some(params)) => {
2957 let result =
2958 storage_methods::run_storage_doctor(¶ms, runtime.resolved_storage())
2959 .await;
2960 JsonRpcResponse {
2961 jsonrpc: JSONRPC_VERSION.to_string(),
2962 id: response_id,
2963 result: Some(result),
2964 error: None,
2965 }
2966 }
2967 Ok(None) => JsonRpcResponse {
2968 jsonrpc: JSONRPC_VERSION.to_string(),
2969 id: response_id,
2970 result: None,
2971 error: Some(storage_methods::storage_doctor_state_dir_unavailable_error()),
2972 },
2973 Err(reason) => JsonRpcResponse {
2974 jsonrpc: JSONRPC_VERSION.to_string(),
2975 id: response_id,
2976 result: None,
2977 error: Some(JsonRpcError {
2978 code: -32602,
2979 message: format!("Invalid params: {reason}"),
2980 data: None,
2981 }),
2982 },
2983 }
2984 }
2985 method if MOBPACK_AUTHORING_METHODS.contains(&method) => {
2986 handle_unified_mobpack_authoring_rpc(runtime, method, &request.params, response_id)
2987 .await
2988 }
2989 "mobkit/blob/get" => {
2990 mob_methods::handle_blob_get(runtime, response_id, &request.params).await
2991 }
2992 "mobkit/send_message" => {
2993 Box::pin(mob_methods::handle_send_message(
2997 runtime,
2998 identity_ctx.map(|ctx| &ctx.runtime),
2999 response_id,
3000 &request.params,
3001 ))
3002 .await
3003 }
3004 "mobkit/find_members" => {
3005 mob_methods::handle_find_members(
3006 runtime,
3007 identity_ctx.map(|ctx| &ctx.runtime),
3008 response_id,
3009 &request.params,
3010 )
3011 .await
3012 }
3013 "mobkit/ensure_member" => {
3014 Box::pin(mob_methods::handle_ensure_member(
3015 runtime,
3016 identity_ctx.map(|ctx| &ctx.runtime),
3017 response_id,
3018 &request.params,
3019 ))
3020 .await
3021 }
3022 "mobkit/list_members" => {
3023 mob_methods::handle_list_members(
3024 runtime,
3025 identity_ctx.map(|ctx| &ctx.runtime),
3026 response_id,
3027 )
3028 .await
3029 }
3030 "mobkit/get_member" => {
3031 mob_methods::handle_get_member(
3032 runtime,
3033 identity_ctx.map(|ctx| &ctx.runtime),
3034 response_id,
3035 &request.params,
3036 )
3037 .await
3038 }
3039 "mobkit/retire_member" => {
3040 mob_methods::handle_retire_member(
3041 runtime,
3042 identity_ctx.map(|ctx| &ctx.runtime),
3043 response_id,
3044 &request.params,
3045 )
3046 .await
3047 }
3048 "mobkit/respawn_member" => {
3049 Box::pin(mob_methods::handle_respawn_member(
3050 runtime,
3051 identity_ctx.map(|ctx| &ctx.runtime),
3052 response_id,
3053 &request.params,
3054 ))
3055 .await
3056 }
3057 "mobkit/reconcile_edges" => mob_methods::handle_reconcile_edges(runtime, response_id).await,
3058 "mobkit/rediscover" => mob_methods::handle_rediscover(runtime, response_id).await,
3059 "mobkit/mob_events/query" => {
3060 mob_methods::handle_mob_events_query(runtime, response_id, request.params).await
3061 }
3062 "mobkit/mob_events/subscribe" => {
3063 mob_methods::handle_mob_events_subscribe(runtime, response_id, request.params).await
3064 }
3065 "mobkit/cross_mob/wire" => {
3066 Box::pin(mob_methods::handle_cross_mob_wire(
3067 runtime,
3068 runtime_owner.cloned(),
3069 identity_ctx.map(|ctx| &ctx.runtime),
3070 response_id,
3071 &request.params,
3072 ))
3073 .await
3074 }
3075 "mobkit/cross_mob/unwire" => {
3076 Box::pin(mob_methods::handle_cross_mob_unwire(
3077 runtime,
3078 runtime_owner.cloned(),
3079 identity_ctx.map(|ctx| &ctx.runtime),
3080 response_id,
3081 &request.params,
3082 ))
3083 .await
3084 }
3085 "mobkit/cross_mob/send" => {
3086 mob_methods::handle_cross_mob_send(
3087 runtime,
3088 runtime_owner.cloned(),
3089 identity_ctx.map(|ctx| &ctx.runtime),
3090 response_id,
3091 &request.params,
3092 )
3093 .await
3094 }
3095 "mobkit/cross_mob/directory" => {
3096 mob_methods::handle_cross_mob_directory(runtime, response_id).await
3097 }
3098 "mobkit/cross_mob/peer_info" => {
3099 mob_methods::handle_cross_mob_peer_info(runtime, response_id, &request.params).await
3100 }
3101 "mobkit/cross_mob/wire_local" => {
3102 mob_methods::handle_cross_mob_wire_local(
3103 runtime,
3104 runtime_owner.cloned(),
3105 identity_ctx.map(|ctx| &ctx.runtime),
3106 response_id,
3107 &request.params,
3108 )
3109 .await
3110 }
3111 "mobkit/cross_mob/unwire_local" => {
3112 mob_methods::handle_cross_mob_unwire_local(
3113 runtime,
3114 runtime_owner.cloned(),
3115 identity_ctx.map(|ctx| &ctx.runtime),
3116 response_id,
3117 &request.params,
3118 )
3119 .await
3120 }
3121 "mobkit/peer_pubkey" => mob_methods::handle_peer_pubkey(runtime, response_id).await,
3122 "mobkit/member_status" => {
3123 mob_methods::handle_member_status(
3124 runtime,
3125 identity_ctx.map(|ctx| &ctx.runtime),
3126 response_id,
3127 &request.params,
3128 )
3129 .await
3130 }
3131 "mobkit/identity/resolved_tools" => {
3132 mob_methods::handle_identity_resolved_tools(
3133 runtime,
3134 identity_ctx.map(|ctx| &ctx.runtime),
3135 response_id,
3136 &request.params,
3137 )
3138 .await
3139 }
3140 "mobkit/force_cancel_member" => {
3141 mob_methods::handle_force_cancel_member(
3142 runtime,
3143 identity_ctx.map(|ctx| &ctx.runtime),
3144 response_id,
3145 &request.params,
3146 )
3147 .await
3148 }
3149 "mobkit/spawn_helper" => {
3150 Box::pin(mob_methods::handle_spawn_helper(
3151 runtime,
3152 identity_ctx.map(|ctx| &ctx.runtime),
3153 response_id,
3154 &request.params,
3155 ))
3156 .await
3157 }
3158 "mobkit/fork_helper" => {
3159 Box::pin(mob_methods::handle_fork_helper(
3160 runtime,
3161 identity_ctx.map(|ctx| &ctx.runtime),
3162 response_id,
3163 &request.params,
3164 ))
3165 .await
3166 }
3167 "mobkit/attach_existing_session" => {
3168 Box::pin(mob_methods::handle_attach_existing_session(
3169 runtime,
3170 identity_ctx.map(|ctx| &ctx.runtime),
3171 response_id,
3172 &request.params,
3173 ))
3174 .await
3175 }
3176 "mobkit/cancel_flow" => {
3177 mob_methods::handle_cancel_flow(runtime, response_id, &request.params).await
3178 }
3179 "mobkit/flow_status" => {
3180 mob_methods::handle_flow_status(runtime, response_id, &request.params).await
3181 }
3182 "mobkit/list_flows" => mob_methods::handle_list_flows(runtime, response_id).await,
3183 "mobkit/list_runs" => {
3184 mob_methods::handle_list_runs(runtime, response_id, &request.params).await
3185 }
3186 "mobkit/run_flow" => {
3187 Box::pin(mob_methods::handle_run_flow(
3188 runtime,
3189 response_id,
3190 &request.params,
3191 ))
3192 .await
3193 }
3194 "mobkit/collect_completed" => {
3195 mob_methods::handle_collect_completed(runtime, response_id).await
3196 }
3197 "mobkit/wait_ready" => {
3198 mob_methods::handle_wait_ready(runtime, response_id, &request.params).await
3199 }
3200 "mobkit/mob_labels/set" => {
3201 mob_methods::handle_mob_labels_set(runtime, response_id, &request.params).await
3202 }
3203 "mobkit/mob_labels/get" => mob_methods::handle_mob_labels_get(runtime, response_id).await,
3204 "mobkit/mob_labels/delete" => {
3205 mob_methods::handle_mob_labels_delete(runtime, response_id).await
3206 }
3207 "mobkit/run_labels/set" => {
3208 mob_methods::handle_run_labels_set(runtime, response_id, &request.params).await
3209 }
3210 "mobkit/run_labels/get" => {
3211 mob_methods::handle_run_labels_get(runtime, response_id, &request.params).await
3212 }
3213 "mobkit/run_labels/delete" => {
3214 mob_methods::handle_run_labels_delete(runtime, response_id, &request.params).await
3215 }
3216 "mobkit/send" => {
3218 let identity_rt = match identity_ctx {
3219 Some(ctx) => &ctx.runtime,
3220 None => return maybe_identity_not_configured(is_notification, response_id),
3221 };
3222 let identity_str = request
3223 .params
3224 .get("identity")
3225 .and_then(|v| v.as_str())
3226 .unwrap_or("");
3227 let target =
3228 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3229 {
3230 Ok(target) => target,
3231 Err(e) => {
3232 return maybe_error_response(
3233 is_notification,
3234 response_id,
3235 -32602,
3236 format!("invalid identity: {e}"),
3237 );
3238 }
3239 };
3240 let identity = target.identity.clone();
3241 if let Some(response) =
3242 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3243 {
3244 return if is_notification {
3245 String::new()
3246 } else {
3247 serialize_response(&response)
3248 };
3249 }
3250 let content_val = request
3251 .params
3252 .get("content")
3253 .cloned()
3254 .unwrap_or(Value::Null);
3255 let content = match serde_json::from_value::<meerkat_core::ContentInput>(content_val) {
3256 Ok(content) => content,
3257 Err(err) => {
3258 return maybe_error_response(
3259 is_notification,
3260 response_id,
3261 -32602,
3262 format!("invalid content: {err}"),
3263 );
3264 }
3265 };
3266 let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3267 .then_some(identity_str);
3268 let send_result = identity_rt
3269 .send_admission_tracked(
3270 &identity,
3271 expected_alias,
3272 &content,
3273 meerkat_core::types::HandlingMode::Queue,
3274 None,
3275 )
3276 .await;
3277 match send_result {
3278 Ok(admission) => JsonRpcResponse {
3279 jsonrpc: JSONRPC_VERSION.to_string(),
3280 id: response_id,
3281 result: Some(serde_json::json!({
3282 "fencing_token": admission.fencing_token.get(),
3283 "completion_baseline": completion_cursor_json(admission.completion_baseline),
3288 })),
3289 error: None,
3290 },
3291 Err(e) => identity_error_response(response_id, &e),
3292 }
3293 }
3294 "mobkit/interact" => {
3295 let identity_rt = match identity_ctx {
3296 Some(ctx) => &ctx.runtime,
3297 None => return maybe_identity_not_configured(is_notification, response_id),
3298 };
3299 let identity_str = request
3300 .params
3301 .get("identity")
3302 .and_then(|v| v.as_str())
3303 .unwrap_or("");
3304 let target =
3305 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3306 {
3307 Ok(target) => target,
3308 Err(e) => {
3309 return maybe_error_response(
3310 is_notification,
3311 response_id,
3312 -32602,
3313 format!("invalid identity: {e}"),
3314 );
3315 }
3316 };
3317 let identity = target.identity.clone();
3318 if let Some(response) =
3319 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3320 {
3321 return if is_notification {
3322 String::new()
3323 } else {
3324 serialize_response(&response)
3325 };
3326 }
3327 let content_val = request
3328 .params
3329 .get("content")
3330 .cloned()
3331 .unwrap_or(Value::Null);
3332 let content =
3333 match serde_json::from_value::<meerkat_core::ContentInput>(content_val.clone()) {
3334 Ok(content) => content,
3335 Err(err) => {
3336 return maybe_error_response(
3337 is_notification,
3338 response_id,
3339 -32602,
3340 format!("invalid content: {err}"),
3341 );
3342 }
3343 };
3344 let origin = request
3345 .params
3346 .get("origin")
3347 .and_then(|v| v.as_str())
3348 .unwrap_or("console");
3349 let interaction_id = request
3350 .params
3351 .get("interaction_id")
3352 .and_then(|v| v.as_str())
3353 .map(ToString::to_string)
3354 .unwrap_or_else(|| meerkat_core::types::SessionId::new().to_string());
3355 let runtime_member_id = identity_rt
3356 .status(&identity)
3357 .await
3358 .ok()
3359 .and_then(|status| status.agent_runtime_id.map(|id| id.as_str().to_string()));
3360
3361 if let Err(err) = runtime
3362 .reserve_identity_interaction(
3363 identity.as_str(),
3364 runtime_member_id.as_deref(),
3365 &interaction_id,
3366 origin,
3367 content_val,
3368 )
3369 .await
3370 {
3371 return maybe_error_response(
3372 is_notification,
3373 response_id,
3374 -32003,
3375 format!("failed to reserve interaction: {err}"),
3376 );
3377 }
3378
3379 let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3380 .then_some(identity_str);
3381 let send_result = identity_rt
3382 .send_admission_tracked(
3383 &identity,
3384 expected_alias,
3385 &content,
3386 meerkat_core::types::HandlingMode::Queue,
3387 None,
3388 )
3389 .await;
3390 match send_result {
3391 Ok(admission) => JsonRpcResponse {
3392 jsonrpc: JSONRPC_VERSION.to_string(),
3393 id: response_id,
3394 result: Some(serde_json::json!({
3395 "interaction_id": interaction_id,
3396 "fencing_token": admission.fencing_token.get(),
3397 "completion_baseline": completion_cursor_json(admission.completion_baseline),
3398 "stream": {
3399 "route": format!("/console/identity/{}/stream", identity.as_str()),
3400 "identity": identity.as_str(),
3401 }
3402 })),
3403 error: None,
3404 },
3405 Err(e) => {
3406 runtime
3407 .record_console_lifecycle(
3408 identity.as_str(),
3409 "interaction_failed",
3410 serde_json::json!({
3411 "interaction_id": interaction_id,
3412 "origin": origin,
3413 "error": e.to_string(),
3414 }),
3415 )
3416 .await;
3417 identity_error_response(response_id, &e)
3418 }
3419 }
3420 }
3421 "mobkit/dispatch" => {
3422 let identity_rt = match identity_ctx {
3423 Some(ctx) => &ctx.runtime,
3424 None => return maybe_identity_not_configured(is_notification, response_id),
3425 };
3426 let identity_str = request
3427 .params
3428 .get("identity")
3429 .and_then(|v| v.as_str())
3430 .unwrap_or("");
3431 let target =
3432 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3433 {
3434 Ok(target) => target,
3435 Err(e) => {
3436 return maybe_error_response(
3437 is_notification,
3438 response_id,
3439 -32602,
3440 format!("invalid identity: {e}"),
3441 );
3442 }
3443 };
3444 let identity = target.identity.clone();
3445 if let Some(response) =
3446 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3447 {
3448 return if is_notification {
3449 String::new()
3450 } else {
3451 serialize_response(&response)
3452 };
3453 }
3454 let di_val = request
3455 .params
3456 .get("dispatch_input")
3457 .cloned()
3458 .unwrap_or(Value::Null);
3459 let content_val = di_val
3460 .get("content")
3461 .cloned()
3462 .unwrap_or_else(|| Value::String(String::new()));
3463 let content = match serde_json::from_value::<meerkat_core::ContentInput>(content_val) {
3464 Ok(content) => content,
3465 Err(err) => {
3466 return maybe_error_response(
3467 is_notification,
3468 response_id,
3469 -32602,
3470 format!("invalid dispatch_input.content: {err}"),
3471 );
3472 }
3473 };
3474 let origin_str = di_val
3475 .get("origin")
3476 .and_then(|v| v.as_str())
3477 .unwrap_or("system");
3478 let origin = match origin_str {
3479 "connector" => crate::identity_first::DispatchOrigin::Connector,
3480 "scheduler" => crate::identity_first::DispatchOrigin::Scheduler,
3481 "policy" => crate::identity_first::DispatchOrigin::Policy,
3482 "flow" => crate::identity_first::DispatchOrigin::Flow,
3483 _ => crate::identity_first::DispatchOrigin::System,
3484 };
3485 let correlation_id = di_val
3486 .get("correlation_id")
3487 .and_then(|v| v.as_str())
3488 .map(crate::identity_first::CorrelationId::new);
3489 let idempotency_key = di_val
3490 .get("idempotency_key")
3491 .and_then(|v| v.as_str())
3492 .map(crate::identity_first::DispatchIdempotencyKey::new);
3493 let dispatch_input = crate::identity_first::DispatchInput {
3494 content,
3495 origin,
3496 correlation_id,
3497 idempotency_key,
3498 };
3499 let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3500 .then_some(identity_str);
3501 let dispatch_result = identity_rt
3502 .dispatch_admission_tracked(&identity, expected_alias, &dispatch_input)
3503 .await;
3504 match dispatch_result {
3505 Ok(admission) => JsonRpcResponse {
3506 jsonrpc: JSONRPC_VERSION.to_string(),
3507 id: response_id,
3508 result: Some(serde_json::json!({
3509 "fencing_token": admission.fencing_token.get(),
3510 "durable": admission.durable,
3511 "completion_baseline": completion_cursor_json(admission.completion_baseline),
3514 })),
3515 error: None,
3516 },
3517 Err(e) => identity_error_response(response_id, &e),
3518 }
3519 }
3520 "mobkit/subscribe" => {
3521 let identity_rt = match identity_ctx {
3522 Some(ctx) => &ctx.runtime,
3523 None => return maybe_identity_not_configured(is_notification, response_id),
3524 };
3525 let identity_str = request
3526 .params
3527 .get("identity")
3528 .and_then(|v| v.as_str())
3529 .unwrap_or("");
3530 let target =
3531 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3532 {
3533 Ok(target) => target,
3534 Err(e) => {
3535 return maybe_error_response(
3536 is_notification,
3537 response_id,
3538 -32602,
3539 format!("invalid identity: {e}"),
3540 );
3541 }
3542 };
3543 let identity = target.identity.clone();
3544 if let Some(response) =
3545 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3546 {
3547 return if is_notification {
3548 String::new()
3549 } else {
3550 serialize_response(&response)
3551 };
3552 }
3553 match identity_rt.subscribe(&identity).await {
3554 Ok(_receiver) => JsonRpcResponse {
3555 jsonrpc: JSONRPC_VERSION.to_string(),
3556 id: response_id,
3557 result: Some(serde_json::json!({
3558 "identity": identity.as_str(),
3559 "stream_id": identity.as_str(),
3560 "subscribed": true,
3561 })),
3562 error: None,
3563 },
3564 Err(e) => identity_error_response(response_id, &e),
3565 }
3566 }
3567 "mobkit/status_identity_bootstrap" => {
3568 let identity_rt = match identity_ctx {
3569 Some(ctx) => &ctx.runtime,
3570 None => return maybe_identity_not_configured(is_notification, response_id),
3571 };
3572 JsonRpcResponse {
3573 jsonrpc: JSONRPC_VERSION.to_string(),
3574 id: response_id,
3575 result: Some(
3576 serde_json::to_value(identity_rt.identity_bootstrap_status())
3577 .unwrap_or(Value::Null),
3578 ),
3579 error: None,
3580 }
3581 }
3582 "mobkit/wait_identity_bootstrap" => {
3583 let identity_rt = match identity_ctx {
3584 Some(ctx) => &ctx.runtime,
3585 None => return maybe_identity_not_configured(is_notification, response_id),
3586 };
3587 let params = match request.params.as_object() {
3588 Some(params) => params,
3589 None => {
3590 return maybe_error_response(
3591 is_notification,
3592 response_id,
3593 -32602,
3594 "params must be an object".to_string(),
3595 );
3596 }
3597 };
3598 if let Some(field) = params
3599 .keys()
3600 .find(|field| !matches!(field.as_str(), "target" | "timeout_ms"))
3601 {
3602 return maybe_error_response(
3603 is_notification,
3604 response_id,
3605 -32602,
3606 format!("unsupported parameter: {field}"),
3607 );
3608 }
3609 let target = match params.get("target") {
3610 None => "materialized".to_string(),
3611 Some(Value::String(target)) => target.clone(),
3612 Some(_) => {
3613 return maybe_error_response(
3614 is_notification,
3615 response_id,
3616 -32602,
3617 "target must be a string".to_string(),
3618 );
3619 }
3620 };
3621 if !matches!(target.as_str(), "materialized" | "startup_ready") {
3622 return maybe_error_response(
3623 is_notification,
3624 response_id,
3625 -32602,
3626 "target must be 'materialized' or 'startup_ready'".to_string(),
3627 );
3628 }
3629 let wait_timeout = match params.get("timeout_ms") {
3630 None => timeout,
3631 Some(value) => match value.as_u64() {
3632 Some(value) => Duration::from_millis(value),
3633 None => {
3634 return maybe_error_response(
3635 is_notification,
3636 response_id,
3637 -32602,
3638 "timeout_ms must be a non-negative integer".to_string(),
3639 );
3640 }
3641 },
3642 };
3643 let (status, timed_out, startup_ready) = if target == "startup_ready" {
3644 let mob_handle = runtime.mob_handle();
3645 let wait = wait_identity_startup_ready(
3646 identity_rt,
3647 wait_timeout,
3648 move |member_ids, remaining| {
3649 let mob_handle = mob_handle.clone();
3650 async move {
3651 match mob_handle
3652 .wait_for_members_ready(&member_ids, Some(remaining))
3653 .await
3654 {
3655 Ok(_) => IdentityMemberReadiness::Ready,
3656 Err(error)
3657 if crate::unified_runtime::mob_ops::is_ready_wait_timeout(
3658 &error,
3659 ) =>
3660 {
3661 IdentityMemberReadiness::TimedOut
3662 }
3663 Err(error) => IdentityMemberReadiness::Failed(error.to_string()),
3664 }
3665 }
3666 },
3667 )
3668 .await;
3669 match wait {
3670 Ok(wait) => (wait.status, wait.timed_out, Some(wait.startup_ready)),
3671 Err(error) => {
3672 return maybe_error_response(is_notification, response_id, -32000, error);
3673 }
3674 }
3675 } else {
3676 let (status, timed_out) = identity_rt
3677 .wait_identity_bootstrap_terminal(wait_timeout)
3678 .await;
3679 (status, timed_out, None)
3680 };
3681 let mut result = serde_json::to_value(status).unwrap_or_else(|_| serde_json::json!({}));
3682 if let Some(object) = result.as_object_mut() {
3683 object.insert("timed_out".to_string(), Value::Bool(timed_out));
3684 object.insert("target".to_string(), Value::String(target));
3685 if let Some(startup_ready) = startup_ready {
3686 object.insert("startup_ready".to_string(), Value::Bool(startup_ready));
3687 }
3688 }
3689 JsonRpcResponse {
3690 jsonrpc: JSONRPC_VERSION.to_string(),
3691 id: response_id,
3692 result: Some(result),
3693 error: None,
3694 }
3695 }
3696 "mobkit/status_identity" => {
3697 let identity_rt = match identity_ctx {
3698 Some(ctx) => &ctx.runtime,
3699 None => return maybe_identity_not_configured(is_notification, response_id),
3700 };
3701 let identity_str = request
3702 .params
3703 .get("identity")
3704 .and_then(|v| v.as_str())
3705 .unwrap_or("");
3706 let target =
3707 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3708 {
3709 Ok(target) => target,
3710 Err(e) => {
3711 return maybe_error_response(
3712 is_notification,
3713 response_id,
3714 -32602,
3715 format!("invalid identity: {e}"),
3716 );
3717 }
3718 };
3719 let identity = target.identity.clone();
3720 if let Some(response) =
3721 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3722 {
3723 return if is_notification {
3724 String::new()
3725 } else {
3726 serialize_response(&response)
3727 };
3728 }
3729 match identity_rt.status(&identity).await {
3730 Ok(status) => {
3731 let continuity_health =
3732 serde_json::to_value(&status.continuity_health).unwrap_or(Value::Null);
3733 let result = serde_json::json!({
3734 "state": identity_lifecycle_state_json(status.state),
3735 "identity": status.identity.as_str(),
3736 "agent_runtime_id": status.agent_runtime_id.as_ref().map(super::identity_first::AgentRuntimeId::as_str),
3737 "session_id": status.session_id.as_ref().map(ToString::to_string),
3738 "profile": status.profile.as_ref().map(meerkat_mob::ProfileName::as_str),
3739 "addressability": addressability_json(status.addressability),
3740 "display_name": status.display_name.as_ref().map(super::identity_first::DisplayName::as_str),
3741 "labels": status.labels,
3742 "generation": status.generation.map(super::identity_first::ContinuityGeneration::get),
3743 "checkpoint_version": status.checkpoint_version.map(super::identity_first::CheckpointVersion::get),
3744 "continuity_health": continuity_health,
3745 "lease_healthy": status.lease.as_ref().map(|lease| lease.healthy),
3746 "lease": status.lease.as_ref().map(|lease| serde_json::json!({
3747 "fencing_token": lease.fencing_token.get(),
3748 "ttl_remaining_ms": lease.ttl_remaining.as_millis() as u64,
3749 "healthy": lease.healthy,
3750 })),
3751 });
3752 JsonRpcResponse {
3753 jsonrpc: JSONRPC_VERSION.to_string(),
3754 id: response_id,
3755 result: Some(result),
3756 error: None,
3757 }
3758 }
3759 Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
3760 if rpc_live_only_fallback_allowed(&target, identity_str)
3761 && let Some(live) = target.live.as_ref()
3762 {
3763 JsonRpcResponse {
3764 jsonrpc: JSONRPC_VERSION.to_string(),
3765 id: response_id,
3766 result: Some(rpc_live_identity_status_json(live)),
3767 error: None,
3768 }
3769 } else {
3770 identity_error_response(response_id, &e)
3771 }
3772 }
3773 Err(e) => identity_error_response(response_id, &e),
3774 }
3775 }
3776 "mobkit/respawn" => {
3777 let identity_rt = match identity_ctx {
3778 Some(ctx) => &ctx.runtime,
3779 None => return maybe_identity_not_configured(is_notification, response_id),
3780 };
3781 let identity_str = request
3782 .params
3783 .get("identity")
3784 .and_then(|v| v.as_str())
3785 .unwrap_or("");
3786 let target =
3787 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3788 {
3789 Ok(target) => target,
3790 Err(e) => {
3791 return maybe_error_response(
3792 is_notification,
3793 response_id,
3794 -32602,
3795 format!("invalid identity: {e}"),
3796 );
3797 }
3798 };
3799 let identity = target.identity.clone();
3800 if let Some(response) =
3801 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3802 {
3803 return if is_notification {
3804 String::new()
3805 } else {
3806 serialize_response(&response)
3807 };
3808 }
3809 let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3810 .then_some(identity_str);
3811 let respawn_result = identity_rt
3812 .respawn_identity_in_place_tracked(&identity, expected_alias)
3813 .await;
3814 match respawn_result {
3815 Ok(record) => {
3816 let live_respawn_warning: Option<Value> = None;
3822 let cleanup_warning: Option<Value> = None;
3823 runtime
3824 .record_console_lifecycle(
3825 identity.as_str(),
3826 "identity_respawned",
3827 serde_json::json!({
3828 "generation": record.generation.get(),
3829 "checkpoint_version": record.checkpoint_version.get(),
3830 "live_respawn_warning": live_respawn_warning.clone(),
3831 "cleanup_warning": cleanup_warning.clone(),
3832 }),
3833 )
3834 .await;
3835 JsonRpcResponse {
3836 jsonrpc: JSONRPC_VERSION.to_string(),
3837 id: response_id,
3838 result: Some(serde_json::json!({
3839 "identity": record.identity.as_str(),
3840 "agent_runtime_id": record.agent_runtime_id.as_str(),
3841 "session_id": record.session_id.to_string(),
3842 "generation": record.generation.get(),
3843 "checkpoint_version": record.checkpoint_version.get(),
3844 "live_respawn_warning": live_respawn_warning,
3845 "cleanup_warning": cleanup_warning,
3846 })),
3847 error: None,
3848 }
3849 }
3850 Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
3851 if rpc_live_only_fallback_allowed(&target, identity_str)
3852 && let Some(live) = target.live.as_ref()
3853 {
3854 match Box::pin(respawn_rpc_live_identity(runtime, live)).await {
3855 Ok(result) => {
3856 runtime
3857 .record_console_lifecycle(
3858 live.identity.as_str(),
3859 "identity_respawned",
3860 serde_json::json!({}),
3861 )
3862 .await;
3863 JsonRpcResponse {
3864 jsonrpc: JSONRPC_VERSION.to_string(),
3865 id: response_id,
3866 result: Some(result),
3867 error: None,
3868 }
3869 }
3870 Err(err) => JsonRpcResponse {
3871 jsonrpc: JSONRPC_VERSION.to_string(),
3872 id: response_id,
3873 result: None,
3874 error: Some(JsonRpcError {
3875 code: -32000,
3876 message: format!("respawn failed: {err}"),
3877 data: None,
3878 }),
3879 },
3880 }
3881 } else {
3882 identity_error_response(response_id, &e)
3883 }
3884 }
3885 Err(e) => identity_error_response(response_id, &e),
3886 }
3887 }
3888 "mobkit/retire" => {
3889 let identity_rt = match identity_ctx {
3890 Some(ctx) => &ctx.runtime,
3891 None => return maybe_identity_not_configured(is_notification, response_id),
3892 };
3893 let identity_str = request
3894 .params
3895 .get("identity")
3896 .and_then(|v| v.as_str())
3897 .unwrap_or("");
3898 let target =
3899 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3900 {
3901 Ok(target) => target,
3902 Err(e) => {
3903 return maybe_error_response(
3904 is_notification,
3905 response_id,
3906 -32602,
3907 format!("invalid identity: {e}"),
3908 );
3909 }
3910 };
3911 let identity = target.identity.clone();
3912 if let Some(response) =
3913 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3914 {
3915 return if is_notification {
3916 String::new()
3917 } else {
3918 serialize_response(&response)
3919 };
3920 }
3921 let cleanup_handle = runtime.mob_handle();
3922 let cleanup_identity = identity.clone();
3923 let include_current = !identity_rt.has_session_bridge();
3924 let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3925 .then_some(identity_str);
3926 let retire_result = identity_rt
3927 .retire_and_cleanup_live_members_tracked(
3928 &identity,
3929 expected_alias,
3930 move |retired_alias| async move {
3931 let stale_member_ids = stale_rpc_member_ids_for_identity_with_handle(
3932 &cleanup_handle,
3933 cleanup_identity.as_str(),
3934 retired_alias
3935 .as_ref()
3936 .map(crate::identity_first::AgentRuntimeId::as_str),
3937 include_current,
3938 )
3939 .await;
3940 retire_rpc_member_ids_with_handle(&cleanup_handle, stale_member_ids)
3941 .await
3942 .err()
3943 .map(|error| {
3944 serde_json::json!({
3945 "kind": "stale_member_cleanup_failed_after_identity_retire",
3946 "message": error,
3947 "identity": cleanup_identity.as_str(),
3948 })
3949 })
3950 },
3951 )
3952 .await;
3953 match retire_result {
3954 Ok((token, cleanup_warning)) => {
3955 runtime
3956 .record_console_lifecycle(
3957 identity.as_str(),
3958 "identity_retired",
3959 serde_json::json!({
3960 "fencing_token": token.get(),
3961 "cleanup_warning": cleanup_warning.clone(),
3962 }),
3963 )
3964 .await;
3965 JsonRpcResponse {
3966 jsonrpc: JSONRPC_VERSION.to_string(),
3967 id: response_id,
3968 result: Some(serde_json::json!({
3969 "fencing_token": token.get(),
3970 "cleanup_warning": cleanup_warning,
3971 })),
3972 error: None,
3973 }
3974 }
3975 Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
3976 if rpc_live_only_fallback_allowed(&target, identity_str)
3977 && let Some(live) = target.live.as_ref()
3978 {
3979 match retire_rpc_live_identity(runtime, live).await {
3980 Ok(()) => {
3981 runtime
3982 .record_console_lifecycle(
3983 live.identity.as_str(),
3984 "identity_retired",
3985 serde_json::json!({}),
3986 )
3987 .await;
3988 JsonRpcResponse {
3989 jsonrpc: JSONRPC_VERSION.to_string(),
3990 id: response_id,
3991 result: Some(
3992 serde_json::json!({ "identity": live.identity.as_str() }),
3993 ),
3994 error: None,
3995 }
3996 }
3997 Err(err) => JsonRpcResponse {
3998 jsonrpc: JSONRPC_VERSION.to_string(),
3999 id: response_id,
4000 result: None,
4001 error: Some(JsonRpcError {
4002 code: -32000,
4003 message: format!("retire failed: {err}"),
4004 data: None,
4005 }),
4006 },
4007 }
4008 } else {
4009 identity_error_response(response_id, &e)
4010 }
4011 }
4012 Err(e) => identity_error_response(response_id, &e),
4013 }
4014 }
4015 "mobkit/reset" => {
4016 let identity_reset_ctx = match identity_ctx {
4017 Some(ctx) => ctx,
4018 None => return maybe_identity_not_configured(is_notification, response_id),
4019 };
4020 let identity_rt = &identity_reset_ctx.runtime;
4021 let identity_str = request
4022 .params
4023 .get("identity")
4024 .and_then(|v| v.as_str())
4025 .unwrap_or("");
4026 let target =
4027 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
4028 {
4029 Ok(target) => target,
4030 Err(e) => {
4031 return maybe_error_response(
4032 is_notification,
4033 response_id,
4034 -32602,
4035 format!("invalid identity: {e}"),
4036 );
4037 }
4038 };
4039 let identity = target.identity.clone();
4040 if let Some(response) =
4041 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
4042 {
4043 return if is_notification {
4044 String::new()
4045 } else {
4046 serialize_response(&response)
4047 };
4048 }
4049 let _registered_status = match identity_rt.status(&identity).await {
4050 Ok(status) => {
4051 if !identity_rt.has_session_bridge() {
4052 let response = rpc_reset_requires_session_bridge_response(response_id);
4053 return if is_notification {
4054 String::new()
4055 } else {
4056 serialize_response(&response)
4057 };
4058 }
4059 status
4060 }
4061 Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
4062 if rpc_live_only_fallback_allowed(&target, identity_str)
4063 && let Some(live) = target.live.as_ref()
4064 {
4065 let response =
4066 match Box::pin(respawn_rpc_live_identity(runtime, live)).await {
4067 Ok(result) => {
4068 runtime
4069 .record_console_lifecycle(
4070 live.identity.as_str(),
4071 "identity_reset",
4072 serde_json::json!({}),
4073 )
4074 .await;
4075 JsonRpcResponse {
4076 jsonrpc: JSONRPC_VERSION.to_string(),
4077 id: response_id,
4078 result: Some(result),
4079 error: None,
4080 }
4081 }
4082 Err(err) => JsonRpcResponse {
4083 jsonrpc: JSONRPC_VERSION.to_string(),
4084 id: response_id,
4085 result: None,
4086 error: Some(JsonRpcError {
4087 code: -32000,
4088 message: format!("reset failed: {err}"),
4089 data: None,
4090 }),
4091 },
4092 };
4093 return if is_notification {
4094 String::new()
4095 } else {
4096 serialize_response(&response)
4097 };
4098 }
4099 let response = identity_error_response(response_id, &e);
4100 return if is_notification {
4101 String::new()
4102 } else {
4103 serialize_response(&response)
4104 };
4105 }
4106 Err(e) => {
4107 let response = identity_error_response(response_id, &e);
4108 return if is_notification {
4109 String::new()
4110 } else {
4111 serialize_response(&response)
4112 };
4113 }
4114 };
4115 identity_rt.set_reset_roster_provider_context(
4116 Some(identity_reset_ctx.roster_provider.clone()),
4117 identity_reset_ctx.mob_definition.clone(),
4118 );
4119 let reset_result = if crate::member_comms_id::is_reserved_generated_alias(identity_str)
4120 {
4121 identity_rt
4122 .reset_member_alias_tracked(&identity, identity_str)
4123 .await
4124 } else {
4125 identity_rt.reset_tracked(&identity).await
4126 };
4127 match reset_result {
4128 Ok(record) => {
4129 let cleanup_warning = Some(serde_json::json!({
4130 "kind": "stale_member_cleanup_skipped_after_identity_reset",
4131 "message": "reset published the new generation without retiring stale live mob members; identity control calls reject stale runtime ids",
4132 "identity": identity.as_str(),
4133 "agent_runtime_id": record.agent_runtime_id.as_str(),
4134 }));
4135 runtime
4136 .record_console_lifecycle(
4137 identity.as_str(),
4138 "identity_reset",
4139 serde_json::json!({
4140 "generation": record.generation.get(),
4141 "checkpoint_version": record.checkpoint_version.get(),
4142 "cleanup_warning": cleanup_warning.clone(),
4143 }),
4144 )
4145 .await;
4146 JsonRpcResponse {
4147 jsonrpc: JSONRPC_VERSION.to_string(),
4148 id: response_id,
4149 result: Some(serde_json::json!({
4150 "identity": record.identity.as_str(),
4151 "agent_runtime_id": record.agent_runtime_id.as_str(),
4152 "session_id": record.session_id.to_string(),
4153 "generation": record.generation.get(),
4154 "checkpoint_version": record.checkpoint_version.get(),
4155 "cleanup_warning": cleanup_warning,
4156 })),
4157 error: None,
4158 }
4159 }
4160 Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
4161 if rpc_live_only_fallback_allowed(&target, identity_str)
4162 && let Some(live) = target.live.as_ref()
4163 {
4164 match Box::pin(respawn_rpc_live_identity(runtime, live)).await {
4165 Ok(result) => {
4166 runtime
4167 .record_console_lifecycle(
4168 live.identity.as_str(),
4169 "identity_reset",
4170 serde_json::json!({}),
4171 )
4172 .await;
4173 JsonRpcResponse {
4174 jsonrpc: JSONRPC_VERSION.to_string(),
4175 id: response_id,
4176 result: Some(result),
4177 error: None,
4178 }
4179 }
4180 Err(err) => JsonRpcResponse {
4181 jsonrpc: JSONRPC_VERSION.to_string(),
4182 id: response_id,
4183 result: None,
4184 error: Some(JsonRpcError {
4185 code: -32000,
4186 message: format!("reset failed: {err}"),
4187 data: None,
4188 }),
4189 },
4190 }
4191 } else {
4192 identity_error_response(response_id, &e)
4193 }
4194 }
4195 Err(e) => identity_error_response(response_id, &e),
4196 }
4197 }
4198 "mobkit/delete_identity" => {
4199 let identity_rt = match identity_ctx {
4200 Some(ctx) => &ctx.runtime,
4201 None => return maybe_identity_not_configured(is_notification, response_id),
4202 };
4203 let identity_str = request
4204 .params
4205 .get("identity")
4206 .and_then(|v| v.as_str())
4207 .unwrap_or("");
4208 let target =
4209 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
4210 {
4211 Ok(target) => target,
4212 Err(e) => {
4213 return maybe_error_response(
4214 is_notification,
4215 response_id,
4216 -32602,
4217 format!("invalid identity: {e}"),
4218 );
4219 }
4220 };
4221 let identity = target.identity.clone();
4222 if let Some(response) =
4223 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
4224 {
4225 return if is_notification {
4226 String::new()
4227 } else {
4228 serialize_response(&response)
4229 };
4230 }
4231 let _registered_status = match identity_rt.status(&identity).await {
4232 Ok(status) => status,
4233 Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
4234 if target.live.is_some() {
4235 let response = JsonRpcResponse {
4236 jsonrpc: JSONRPC_VERSION.to_string(),
4237 id: response_id,
4238 result: None,
4239 error: Some(JsonRpcError {
4240 code: -32602,
4241 message: format!(
4242 "delete_identity requires durable identity: {} is live-only",
4243 identity.as_str()
4244 ),
4245 data: Some(serde_json::json!({
4246 "kind": "live_only_identity_delete_unsupported",
4247 "identity": identity.as_str(),
4248 })),
4249 }),
4250 };
4251 return if is_notification {
4252 String::new()
4253 } else {
4254 serialize_response(&response)
4255 };
4256 }
4257 let response = identity_error_response(response_id, &e);
4258 return if is_notification {
4259 String::new()
4260 } else {
4261 serialize_response(&response)
4262 };
4263 }
4264 Err(e) => {
4265 let response = identity_error_response(response_id, &e);
4266 return if is_notification {
4267 String::new()
4268 } else {
4269 serialize_response(&response)
4270 };
4271 }
4272 };
4273 let cleanup_handle = runtime.mob_handle();
4274 let cleanup_identity = identity.clone();
4275 let include_current = !identity_rt.has_session_bridge();
4276 let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
4277 .then_some(identity_str);
4278 let delete_result = identity_rt
4279 .delete_identity_and_cleanup_live_members_tracked(
4280 &identity,
4281 expected_alias,
4282 move |deleted_alias| async move {
4283 let stale_member_ids = stale_rpc_member_ids_for_identity_with_handle(
4284 &cleanup_handle,
4285 cleanup_identity.as_str(),
4286 deleted_alias
4287 .as_ref()
4288 .map(crate::identity_first::AgentRuntimeId::as_str),
4289 include_current,
4290 )
4291 .await;
4292 retire_rpc_member_ids_with_handle(&cleanup_handle, stale_member_ids)
4293 .await
4294 .err()
4295 .map(|error| {
4296 serde_json::json!({
4297 "kind": "stale_member_cleanup_failed_after_identity_delete",
4298 "identity": cleanup_identity.as_str(),
4299 "message": error,
4300 })
4301 })
4302 },
4303 )
4304 .await;
4305 match delete_result {
4306 Ok(cleanup_warning) => {
4307 runtime
4308 .record_console_lifecycle(
4309 identity.as_str(),
4310 "identity_deleted",
4311 serde_json::json!({
4312 "cleanup_warning": cleanup_warning,
4313 }),
4314 )
4315 .await;
4316 JsonRpcResponse {
4317 jsonrpc: JSONRPC_VERSION.to_string(),
4318 id: response_id,
4319 result: Some(serde_json::json!({
4320 "identity": identity.as_str(),
4321 "cleanup_warning": cleanup_warning,
4322 })),
4323 error: None,
4324 }
4325 }
4326 Err(e) => identity_error_response(response_id, &e),
4327 }
4328 }
4329 "mobkit/inspect_identity" => {
4330 let identity_rt = match identity_ctx {
4331 Some(ctx) => &ctx.runtime,
4332 None => return maybe_identity_not_configured(is_notification, response_id),
4333 };
4334 let identity_str = request
4335 .params
4336 .get("identity")
4337 .and_then(|v| v.as_str())
4338 .unwrap_or("");
4339 let target =
4340 match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
4341 {
4342 Ok(target) => target,
4343 Err(e) => {
4344 return maybe_error_response(
4345 is_notification,
4346 response_id,
4347 -32602,
4348 format!("invalid identity: {e}"),
4349 );
4350 }
4351 };
4352 let identity = target.identity.clone();
4353 let status = identity_rt.status(&identity).await;
4354 let completion_cursor = identity_rt.completion_cursor(&identity).await;
4355 if let Some(response) =
4356 rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
4357 {
4358 return if is_notification {
4359 String::new()
4360 } else {
4361 serialize_response(&response)
4362 };
4363 }
4364 match identity_rt.inspect(&identity).await {
4365 Ok(inspection) => {
4366 let status = status.ok();
4367 JsonRpcResponse {
4368 jsonrpc: JSONRPC_VERSION.to_string(),
4369 id: response_id,
4370 result: Some(serde_json::json!({
4371 "identity": identity.as_str(),
4372 "state": status.as_ref().map(|status| identity_lifecycle_state_json(status.state)),
4373 "profile": status.as_ref().and_then(|status| status.profile.as_ref().map(meerkat_mob::ProfileName::as_str)),
4374 "addressability": status.as_ref().map(|status| addressability_json(status.addressability)),
4375 "display_name": status.as_ref().and_then(|status| status.display_name.as_ref().map(super::identity_first::DisplayName::as_str)),
4376 "labels": status.as_ref().map(|status| status.labels.clone()).unwrap_or_default(),
4377 "generation": status.as_ref().and_then(|status| status.generation.map(super::identity_first::ContinuityGeneration::get)),
4378 "checkpoint_version": status.as_ref().and_then(|status| status.checkpoint_version.map(super::identity_first::CheckpointVersion::get)),
4379 "continuity_health": status.as_ref().and_then(|status| serde_json::to_value(&status.continuity_health).ok()).unwrap_or(Value::Null),
4380 "lease_healthy": status.as_ref().and_then(|status| status.lease.as_ref().map(|lease| lease.healthy)),
4381 "continuity": status.as_ref().map(|status| serde_json::json!({
4382 "generation": status.generation.map(super::identity_first::ContinuityGeneration::get),
4383 "checkpoint_version": status.checkpoint_version.map(super::identity_first::CheckpointVersion::get),
4384 "session_id": status.session_id.as_ref().map(ToString::to_string),
4385 "agent_runtime_id": status.agent_runtime_id.as_ref().map(super::identity_first::AgentRuntimeId::as_str),
4386 })).unwrap_or_else(|| serde_json::json!({})),
4387 "lease": status.as_ref().and_then(|status| status.lease.as_ref().map(|lease| serde_json::json!({
4388 "fencing_token": lease.fencing_token.get(),
4389 "ttl_remaining_ms": lease.ttl_remaining.as_millis() as u64,
4390 "healthy": lease.healthy,
4391 }))),
4392 "output_preview": inspection.output_preview,
4393 "is_final": inspection.is_final,
4394 "peer_reachable_count": inspection.peer_reachable_count,
4395 "completion_cursor": completion_cursor_json(completion_cursor),
4401 })),
4402 error: None,
4403 }
4404 }
4405 Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
4406 if let Some(live) = target.live.as_ref() {
4407 JsonRpcResponse {
4408 jsonrpc: JSONRPC_VERSION.to_string(),
4409 id: response_id,
4410 result: Some(rpc_live_identity_inspect_json(runtime, live).await),
4411 error: None,
4412 }
4413 } else {
4414 identity_error_response(response_id, &e)
4415 }
4416 }
4417 Err(e) => identity_error_response(response_id, &e),
4418 }
4419 }
4420 "mobkit/reconcile_identity" => {
4421 let ctx = match identity_ctx {
4422 Some(ctx) => ctx,
4423 None => return maybe_identity_not_configured(is_notification, response_id),
4424 };
4425 let reconciled = match runtime.refresh_desired_topology().await {
4430 Ok(Some(result)) => Ok(result),
4431 Ok(None) => {
4432 let roster_specs = match ctx
4433 .roster_provider
4434 .roster(&crate::identity_first::RosterContext {
4435 mob_definition: ctx.mob_definition.clone(),
4436 previous_identities: Vec::new(),
4437 })
4438 .await
4439 {
4440 Ok(specs) => specs,
4441 Err(e) => {
4442 return maybe_error_response(
4443 is_notification,
4444 response_id,
4445 -32603,
4446 format!("roster provider failed: {e}"),
4447 );
4448 }
4449 };
4450 ctx.runtime
4451 .restore_flow_tracked(
4452 roster_specs,
4453 ctx.topology_provider.clone(),
4454 ctx.customizer.clone(),
4455 )
4456 .await
4457 }
4458 Err(error) => Err(error),
4459 };
4460 match reconciled {
4461 Ok(result) => {
4462 let outcomes: serde_json::Map<String, Value> = result
4463 .outcomes
4464 .iter()
4465 .map(|(id, outcome)| {
4466 let val = match outcome {
4467 crate::identity_first::RestoreOutcome::Created {
4468 record, ..
4469 } => {
4470 serde_json::json!({
4471 "outcome": "created",
4472 "identity": record.identity.as_str(),
4473 "agent_runtime_id": record.agent_runtime_id.as_str(),
4474 "session_id": record.session_id.to_string(),
4475 "generation": record.generation.get(),
4476 })
4477 }
4478 crate::identity_first::RestoreOutcome::Dormant {
4479 record, ..
4480 } => {
4481 serde_json::json!({
4482 "outcome": "dormant",
4483 "identity": id.as_str(),
4484 "agent_runtime_id": record.as_ref().map(|record| record.agent_runtime_id.as_str()),
4485 "session_id": record.as_ref().map(|record| record.session_id.to_string()),
4486 "generation": record.as_ref().map(|record| record.generation.get()),
4487 })
4488 }
4489 crate::identity_first::RestoreOutcome::Resumed {
4490 record, ..
4491 } => {
4492 serde_json::json!({
4493 "outcome": "resumed",
4494 "identity": record.identity.as_str(),
4495 "agent_runtime_id": record.agent_runtime_id.as_str(),
4496 "session_id": record.session_id.to_string(),
4497 "generation": record.generation.get(),
4498 })
4499 }
4500 crate::identity_first::RestoreOutcome::Broken(failure) => {
4501 serde_json::json!({
4502 "outcome": "broken",
4503 "identity": failure.identity.as_str(),
4504 "detail": failure.detail,
4505 })
4506 }
4507 };
4508 (id.to_string(), val)
4509 })
4510 .collect();
4511 JsonRpcResponse {
4512 jsonrpc: JSONRPC_VERSION.to_string(),
4513 id: response_id,
4514 result: Some(serde_json::json!({
4515 "outcomes": outcomes,
4516 "managed_edges": result.managed_edges.len(),
4517 })),
4518 error: None,
4519 }
4520 }
4521 Err(e) => identity_error_response(response_id, &e),
4522 }
4523 }
4524 method if method.contains('/') && !method.starts_with("mobkit/") => {
4525 let module_id = method
4526 .split('/')
4527 .next()
4528 .map(ToString::to_string)
4529 .unwrap_or_default();
4530 let route = runtime
4531 .route_module_call(
4532 &ModuleRouteRequest {
4533 module_id: module_id.clone(),
4534 method: method.to_string(),
4535 params: request.params,
4536 },
4537 timeout,
4538 )
4539 .await;
4540 match route {
4541 Ok(response) => JsonRpcResponse {
4542 jsonrpc: JSONRPC_VERSION.to_string(),
4543 id: response_id,
4544 result: Some(serde_json::json!({
4545 "module_id": response.module_id,
4546 "method": response.method,
4547 "payload": response.payload
4548 })),
4549 error: None,
4550 },
4551 Err(ModuleRouteError::UnloadedModule(module_id)) => JsonRpcResponse {
4552 jsonrpc: JSONRPC_VERSION.to_string(),
4553 id: response_id,
4554 result: None,
4555 error: Some(JsonRpcError {
4556 code: -32601,
4557 message: format!("Module '{module_id}' not loaded"),
4558 data: None,
4559 }),
4560 },
4561 Err(err) => JsonRpcResponse {
4562 jsonrpc: JSONRPC_VERSION.to_string(),
4563 id: response_id,
4564 result: None,
4565 error: Some(JsonRpcError {
4566 code: -32000,
4567 message: format!("Module route failed: {err:?}"),
4568 data: None,
4569 }),
4570 },
4571 }
4572 }
4573 method if method.starts_with("mobkit/live/") => match live {
4574 None => crate::live_wiring::live_unavailable_response(response_id),
4575 Some(live) => {
4576 let params = request.params.clone();
4577 let member_alias = live_member_alias(¶ms);
4578 let identity_runtime = identity_ctx
4579 .map(|context| &context.runtime)
4580 .or_else(|| runtime.identity_runtime());
4581 let authority_target = if let Some(alias) = member_alias.as_deref()
4582 && let Some(identity_runtime) = identity_runtime
4583 {
4584 identity_runtime.member_alias_lifecycle_target(alias).await
4585 } else {
4586 Ok(None)
4587 };
4588 match authority_target {
4589 Err(error) => identity_error_response(response_id, &error),
4590 Ok(None)
4591 if member_alias
4592 .as_deref()
4593 .is_some_and(crate::member_comms_id::is_reserved_generated_alias) =>
4594 {
4595 JsonRpcResponse {
4596 jsonrpc: JSONRPC_VERSION.to_string(),
4597 id: response_id,
4598 result: None,
4599 error: Some(JsonRpcError {
4600 code: -32000,
4601 message: format!(
4602 "generated live target requires current identity authority: {}",
4603 member_alias.as_deref().unwrap_or_default()
4604 ),
4605 data: None,
4606 }),
4607 }
4608 }
4609 Ok(Some(target)) => {
4610 let handle = runtime.mob_handle();
4611 let identity_runtime = identity_runtime.cloned();
4612 let live = Arc::clone(live);
4613 let method = method.to_string();
4614 let operation_response_id = response_id.clone();
4615 match crate::identity_first::IdentityRuntime::run_member_alias_targets_operation_tracked(
4616 vec![target],
4617 move || async move {
4618 let session = resolve_live_target(
4619 &handle,
4620 identity_runtime.as_ref(),
4621 true,
4622 ¶ms,
4623 )
4624 .await?;
4625 Ok(live(session, method, params, operation_response_id).await)
4626 },
4627 )
4628 .await
4629 {
4630 Ok(response) => response,
4631 Err(error) => identity_error_response(response_id, &error),
4632 }
4633 }
4634 Ok(None) => {
4635 match resolve_live_target(&runtime.mob_handle(), None, false, ¶ms).await
4636 {
4637 Ok(session) => {
4638 live(session, method.to_string(), params, response_id).await
4639 }
4640 Err(error) => JsonRpcResponse {
4641 jsonrpc: JSONRPC_VERSION.to_string(),
4642 id: response_id,
4643 result: None,
4644 error: Some(JsonRpcError {
4645 code: -32000,
4646 message: format!("live target resolution failed: {error}"),
4647 data: None,
4648 }),
4649 },
4650 }
4651 }
4652 }
4653 }
4654 },
4655 method if workgraph_methods::is_workgraph_method(method) => {
4656 let service = runtime.workgraph_service();
4657 let admission = runtime.workgraph_admission();
4658 match workgraph_methods::handle_workgraph_method(
4661 service.as_ref(),
4662 &admission,
4663 workgraph_methods::WorkgraphSurface::HostStdin,
4664 method,
4665 &request.params,
4666 )
4667 .await
4668 {
4669 Ok(result) => JsonRpcResponse {
4670 jsonrpc: JSONRPC_VERSION.to_string(),
4671 id: response_id,
4672 result: Some(result),
4673 error: None,
4674 },
4675 Err(error) => JsonRpcResponse {
4676 jsonrpc: JSONRPC_VERSION.to_string(),
4677 id: response_id,
4678 result: None,
4679 error: Some(error),
4680 },
4681 }
4682 }
4683 _ => JsonRpcResponse {
4684 jsonrpc: JSONRPC_VERSION.to_string(),
4685 id: response_id,
4686 result: None,
4687 error: Some(JsonRpcError {
4688 code: -32601,
4689 message: "Method not found".to_string(),
4690 data: None,
4691 }),
4692 },
4693 };
4694 if is_notification {
4695 String::new()
4696 } else {
4697 serialize_response(&response)
4698 }
4699}
4700
4701fn build_models_catalog_result() -> Value {
4702 let entries: Vec<Value> = meerkat_models::catalog()
4703 .iter()
4704 .filter_map(|e| {
4705 let mut val = serde_json::to_value(e).ok()?;
4706 if let Some(provider) = meerkat_core::Provider::parse_strict(e.provider)
4707 && let Some(profile) = meerkat_models::profile_for(provider, e.id)
4708 && let Ok(p) = serde_json::to_value(&profile)
4709 {
4710 val["profile"] = p;
4711 }
4712 Some(val)
4713 })
4714 .collect();
4715 let defaults: Vec<Value> = meerkat_models::provider_defaults()
4716 .iter()
4717 .filter_map(|d| serde_json::to_value(d).ok())
4718 .collect();
4719 serde_json::json!({
4720 "models": entries,
4721 "provider_defaults": defaults,
4722 })
4723}
4724
4725#[derive(Debug, Clone)]
4726struct RpcLiveIdentityAlias {
4727 identity: crate::identity_first::AgentIdentity,
4728 runtime_member_id: String,
4729 member: meerkat_mob::runtime::MobMemberListEntry,
4730 session_id: Option<String>,
4731}
4732
4733#[derive(Debug, Clone)]
4734struct RpcIdentityControlTarget {
4735 identity: crate::identity_first::AgentIdentity,
4736 live: Option<RpcLiveIdentityAlias>,
4737 was_registered: bool,
4741}
4742
4743fn rpc_live_only_fallback_allowed(
4748 target: &RpcIdentityControlTarget,
4749 requested_identity: &str,
4750) -> bool {
4751 !target.was_registered
4752 && !crate::member_comms_id::is_reserved_generated_alias(requested_identity)
4753 && target.live.as_ref().is_some_and(|live| {
4754 !crate::member_comms_id::is_reserved_generated_alias(&live.runtime_member_id)
4759 })
4760}
4761
4762fn rpc_member_durable_identity(member: &meerkat_mob::runtime::MobMemberListEntry) -> String {
4763 crate::member_comms_id::durable_identity_label(&member.labels)
4764 .map(str::to_owned)
4765 .unwrap_or_else(|| {
4768 crate::member_comms_id::runtime_alias_str(member.agent_identity.as_str()).into_owned()
4769 })
4770}
4771
4772async fn resolve_rpc_live_identity_alias(
4773 runtime: &UnifiedRuntime,
4774 requested_identity: &str,
4775) -> Result<Option<RpcLiveIdentityAlias>, String> {
4776 let matches = resolve_rpc_live_identity_alias_candidates(runtime, requested_identity).await?;
4777 if matches.len() > 1 {
4778 return Err(format!(
4779 "ambiguous live identity alias {requested_identity}: candidates [{}]",
4780 matches
4781 .iter()
4782 .map(|entry| entry.runtime_member_id.clone())
4783 .collect::<Vec<_>>()
4784 .join(", ")
4785 ));
4786 }
4787 Ok(matches.into_iter().next())
4788}
4789
4790async fn resolve_rpc_live_runtime_member_alias(
4791 runtime: &UnifiedRuntime,
4792 runtime_member_id: &str,
4793) -> Result<Option<RpcLiveIdentityAlias>, String> {
4794 let requested_member_id = crate::member_comms_id::mob_member_id(runtime_member_id);
4795 let handle = runtime.mob_handle();
4796 let Some(member) = handle
4797 .list_members_including_retiring()
4798 .await
4799 .into_iter()
4800 .find(|entry| entry.agent_identity == requested_member_id)
4801 else {
4802 return Ok(None);
4803 };
4804 if !rpc_live_identity_alias_member_visible(&member) {
4805 return Ok(None);
4806 }
4807 let durable_identity = rpc_member_durable_identity(&member);
4808 let identity = crate::identity_first::AgentIdentity::parse(&durable_identity)
4809 .map_err(|err| format!("invalid projected identity {durable_identity}: {err}"))?;
4810 let session_id = handle
4811 .resolve_bridge_session_id_observation(&member.agent_identity)
4812 .await
4813 .map(|session_id| session_id.to_string());
4814 Ok(Some(RpcLiveIdentityAlias {
4815 identity,
4816 runtime_member_id: crate::member_comms_id::runtime_alias_str(
4817 member.agent_identity.as_str(),
4818 )
4819 .into_owned(),
4820 member,
4821 session_id,
4822 }))
4823}
4824
4825async fn rpc_runtime_member_alias_exists_hidden(
4826 runtime: &UnifiedRuntime,
4827 runtime_member_id: &str,
4828) -> bool {
4829 let requested_member_id = crate::member_comms_id::mob_member_id(runtime_member_id);
4830 runtime
4831 .mob_handle()
4832 .list_members_including_retiring()
4833 .await
4834 .into_iter()
4835 .find(|entry| entry.agent_identity == requested_member_id)
4836 .is_some_and(|member| !rpc_live_identity_alias_member_visible(&member))
4837}
4838
4839async fn rpc_live_identity_alias_exists_hidden(
4840 runtime: &UnifiedRuntime,
4841 requested_identity: &str,
4842) -> bool {
4843 let requested_member_id = crate::member_comms_id::mob_member_id(requested_identity);
4844 runtime
4845 .mob_handle()
4846 .list_members_including_retiring()
4847 .await
4848 .into_iter()
4849 .any(|member| {
4850 (member.agent_identity == requested_member_id
4851 || crate::member_comms_id::durable_identity_label(&member.labels)
4852 .is_some_and(|identity| identity == requested_identity))
4853 && !rpc_live_identity_alias_member_visible(&member)
4854 })
4855}
4856
4857async fn resolve_rpc_live_identity_alias_candidates(
4858 runtime: &UnifiedRuntime,
4859 requested_identity: &str,
4860) -> Result<Vec<RpcLiveIdentityAlias>, String> {
4861 let requested_member_id = crate::member_comms_id::mob_member_id(requested_identity);
4862 let handle = runtime.mob_handle();
4863 let members = handle.list_members_including_retiring().await;
4864 let exact_matches = members
4865 .iter()
4866 .filter(|entry| entry.agent_identity == requested_member_id)
4867 .cloned()
4868 .collect::<Vec<_>>();
4869 let label_matches = members
4870 .iter()
4871 .filter(|entry| {
4872 crate::member_comms_id::durable_identity_label(&entry.labels)
4873 .is_some_and(|identity| identity == requested_identity)
4874 })
4875 .cloned()
4876 .collect::<Vec<_>>();
4877 let mut matches = exact_matches;
4878 matches.extend(label_matches);
4879 let mut seen_member_ids = BTreeSet::new();
4880 matches.retain(|entry| seen_member_ids.insert(entry.agent_identity.to_string()));
4881 let mut aliases = Vec::with_capacity(matches.len());
4882 for member in matches {
4883 if !rpc_live_identity_alias_member_visible(&member) {
4884 continue;
4885 }
4886 let durable_identity = rpc_member_durable_identity(&member);
4887 let identity = crate::identity_first::AgentIdentity::parse(&durable_identity)
4888 .map_err(|err| format!("invalid projected identity {durable_identity}: {err}"))?;
4889 let session_id = handle
4890 .resolve_bridge_session_id_observation(&member.agent_identity)
4891 .await
4892 .map(|session_id| session_id.to_string());
4893 aliases.push(RpcLiveIdentityAlias {
4894 identity,
4895 runtime_member_id: crate::member_comms_id::runtime_alias_str(
4896 member.agent_identity.as_str(),
4897 )
4898 .into_owned(),
4899 member,
4900 session_id,
4901 });
4902 }
4903 Ok(aliases)
4904}
4905
4906fn rpc_live_identity_alias_member_visible(
4907 member: &meerkat_mob::runtime::MobMemberListEntry,
4908) -> bool {
4909 rpc_live_identity_alias_visible(member.role.as_str(), &member.labels)
4910}
4911
4912fn rpc_live_identity_alias_visible(
4913 member_role: &str,
4914 labels: &std::collections::BTreeMap<String, String>,
4915) -> bool {
4916 let projected_role = labels
4917 .get("role")
4918 .map(String::as_str)
4919 .unwrap_or(member_role);
4920 !is_implicit_delegate_member(member_role, labels)
4921 && !is_implicit_delegate_member(projected_role, labels)
4922}
4923
4924async fn resolve_rpc_identity_control_target(
4925 runtime: &UnifiedRuntime,
4926 identity_rt: &crate::identity_first::IdentityRuntime,
4927 requested_identity: &str,
4928) -> Result<RpcIdentityControlTarget, String> {
4929 let requested_identity = crate::member_comms_id::runtime_alias_str(requested_identity);
4930 let requested_identity = requested_identity.as_ref();
4931 if crate::member_comms_id::is_reserved_generated_alias(requested_identity) {
4932 for status in identity_rt.statuses().await {
4933 if status
4934 .agent_runtime_id
4935 .as_ref()
4936 .is_some_and(|runtime_id| runtime_id.as_str() == requested_identity)
4937 {
4938 let identity = status.identity;
4939 let registered_live =
4940 resolve_rpc_live_runtime_member_alias(runtime, requested_identity).await?;
4941 if let Some(registered) = registered_live {
4942 return Ok(RpcIdentityControlTarget {
4943 identity,
4944 live: Some(registered),
4945 was_registered: true,
4946 });
4947 }
4948 if rpc_runtime_member_alias_exists_hidden(runtime, requested_identity).await {
4949 return Err(format!("identity hidden by policy: {requested_identity}"));
4950 }
4951 let durable_live_candidates =
4952 resolve_rpc_live_identity_alias_candidates(runtime, identity.as_str()).await?;
4953 let durable_live = if durable_live_candidates.len() > 1 {
4954 return Err(format!(
4955 "ambiguous live identity alias {}: candidates [{}]",
4956 identity.as_str(),
4957 durable_live_candidates
4958 .iter()
4959 .map(|alias| alias.runtime_member_id.clone())
4960 .collect::<Vec<_>>()
4961 .join(", ")
4962 ));
4963 } else {
4964 durable_live_candidates.into_iter().next()
4965 };
4966 return Ok(RpcIdentityControlTarget {
4967 identity,
4968 live: durable_live,
4969 was_registered: true,
4970 });
4971 }
4972 }
4973 let live = resolve_rpc_live_identity_alias(runtime, requested_identity).await?;
4974 if let Some(live_alias) = live {
4975 if let Ok(status) = identity_rt.status(&live_alias.identity).await
4976 && !rpc_live_alias_matches_status_runtime(Some(&live_alias), &status)
4977 {
4978 return Ok(RpcIdentityControlTarget {
4979 identity: live_alias.identity.clone(),
4980 live: Some(live_alias),
4981 was_registered: true,
4982 });
4983 }
4984 let live_identity_candidates =
4985 resolve_rpc_live_identity_alias_candidates(runtime, live_alias.identity.as_str())
4986 .await?;
4987 if live_identity_candidates.len() > 1 {
4988 return Err(format!(
4989 "ambiguous live identity alias {}: candidates [{}]",
4990 live_alias.identity.as_str(),
4991 live_identity_candidates
4992 .iter()
4993 .map(|alias| alias.runtime_member_id.clone())
4994 .collect::<Vec<_>>()
4995 .join(", ")
4996 ));
4997 }
4998 return Ok(RpcIdentityControlTarget {
4999 identity: live_alias.identity.clone(),
5000 live: Some(live_alias),
5001 was_registered: false,
5002 });
5003 }
5004 if rpc_runtime_member_alias_exists_hidden(runtime, requested_identity).await {
5005 return Err(format!("identity hidden by policy: {requested_identity}"));
5006 }
5007 return Err(format!("runtime identity not found: {requested_identity}"));
5008 }
5009 if let Ok(identity) = crate::identity_first::AgentIdentity::parse(requested_identity) {
5010 match identity_rt.status(&identity).await {
5011 Ok(status) => {
5012 let registered_live = match status.agent_runtime_id.as_ref() {
5013 Some(runtime_id) => {
5014 resolve_rpc_live_runtime_member_alias(runtime, runtime_id.as_str()).await?
5015 }
5016 None => None,
5017 };
5018 if let Some(registered) = registered_live {
5019 return Ok(RpcIdentityControlTarget {
5020 identity,
5021 live: Some(registered),
5022 was_registered: true,
5023 });
5024 }
5025 if let Some(runtime_id) = status.agent_runtime_id.as_ref()
5026 && rpc_runtime_member_alias_exists_hidden(runtime, runtime_id.as_str()).await
5027 {
5028 return Err(format!("identity hidden by policy: {requested_identity}"));
5029 }
5030 let requested_live_candidates =
5031 resolve_rpc_live_identity_alias_candidates(runtime, requested_identity).await?;
5032 let requested_live = if requested_live_candidates.len() > 1 {
5033 return Err(format!(
5034 "ambiguous live identity alias {requested_identity}: candidates [{}]",
5035 requested_live_candidates
5036 .iter()
5037 .map(|alias| alias.runtime_member_id.clone())
5038 .collect::<Vec<_>>()
5039 .join(", ")
5040 ));
5041 } else {
5042 requested_live_candidates.into_iter().next()
5043 };
5044 return Ok(RpcIdentityControlTarget {
5045 identity,
5046 live: requested_live,
5047 was_registered: true,
5048 });
5049 }
5050 Err(crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {}
5051 Err(err) => return Err(err.to_string()),
5052 }
5053 }
5054 for status in identity_rt.statuses().await {
5055 if status
5056 .agent_runtime_id
5057 .as_ref()
5058 .is_some_and(|runtime_id| runtime_id.as_str() == requested_identity)
5059 {
5060 let identity = status.identity;
5061 let registered_live =
5062 resolve_rpc_live_runtime_member_alias(runtime, requested_identity).await?;
5063 let durable_live_candidates =
5064 resolve_rpc_live_identity_alias_candidates(runtime, identity.as_str()).await?;
5065 let durable_live = if durable_live_candidates.len() > 1 {
5066 return Err(format!(
5067 "ambiguous live identity alias {}: candidates [{}]",
5068 identity.as_str(),
5069 durable_live_candidates
5070 .iter()
5071 .map(|alias| alias.runtime_member_id.clone())
5072 .collect::<Vec<_>>()
5073 .join(", ")
5074 ));
5075 } else {
5076 durable_live_candidates.into_iter().next()
5077 };
5078 let live = match (registered_live, durable_live) {
5079 (Some(registered), Some(durable))
5080 if registered.runtime_member_id == durable.runtime_member_id =>
5081 {
5082 Some(registered)
5083 }
5084 (Some(registered), None) => Some(registered),
5085 (Some(_registered), Some(durable)) => Some(durable),
5086 (None, durable) => durable,
5087 };
5088 return Ok(RpcIdentityControlTarget {
5089 identity,
5090 live,
5091 was_registered: true,
5092 });
5093 }
5094 }
5095 let live = resolve_rpc_live_identity_alias(runtime, requested_identity).await?;
5096 if let Some(live_alias) = live {
5097 if let Some(bound_status) = identity_rt.statuses().await.into_iter().find(|status| {
5098 status
5099 .agent_runtime_id
5100 .as_ref()
5101 .is_some_and(|runtime_id| runtime_id.as_str() == live_alias.runtime_member_id)
5102 }) && bound_status.identity != live_alias.identity
5103 {
5104 return Err(format!(
5105 "stale live identity alias: live console alias {} resolves to {}, but identity runtime binding belongs to {}",
5106 live_alias.identity.as_str(),
5107 live_alias.runtime_member_id,
5108 bound_status.identity.as_str(),
5109 ));
5110 }
5111 let live_identity_candidates =
5112 resolve_rpc_live_identity_alias_candidates(runtime, live_alias.identity.as_str())
5113 .await?;
5114 if live_identity_candidates.len() > 1 {
5115 return Err(format!(
5116 "ambiguous live identity alias {}: candidates [{}]",
5117 live_alias.identity.as_str(),
5118 live_identity_candidates
5119 .iter()
5120 .map(|alias| alias.runtime_member_id.clone())
5121 .collect::<Vec<_>>()
5122 .join(", ")
5123 ));
5124 }
5125 return Ok(RpcIdentityControlTarget {
5126 identity: live_alias.identity.clone(),
5127 live: Some(live_alias),
5128 was_registered: false,
5129 });
5130 }
5131 if rpc_live_identity_alias_exists_hidden(runtime, requested_identity).await {
5132 return Err(format!("identity hidden by policy: {requested_identity}"));
5133 }
5134 let identity = crate::identity_first::AgentIdentity::parse(requested_identity)
5135 .map_err(|err| err.to_string())?;
5136 Ok(RpcIdentityControlTarget {
5137 identity,
5138 live: None,
5139 was_registered: false,
5140 })
5141}
5142
5143fn rpc_reset_requires_session_bridge_response(response_id: Value) -> JsonRpcResponse {
5144 JsonRpcResponse {
5145 jsonrpc: JSONRPC_VERSION.to_string(),
5146 id: response_id,
5147 result: None,
5148 error: Some(JsonRpcError {
5149 code: -32602,
5150 message: "reset requires an identity runtime with a session bridge".to_string(),
5151 data: Some(serde_json::json!({
5152 "kind": "identity_reset_requires_session_bridge",
5153 })),
5154 }),
5155 }
5156}
5157
5158fn rpc_live_alias_matches_status_runtime(
5159 alias: Option<&RpcLiveIdentityAlias>,
5160 status: &crate::identity_first::IdentityStatus,
5161) -> bool {
5162 let Some(alias) = alias else {
5163 return true;
5164 };
5165 let session_matches = match (
5166 status.session_id.as_ref().map(ToString::to_string),
5167 alias.session_id.as_deref(),
5168 ) {
5169 (Some(status_session), Some(live_session)) => status_session == live_session,
5170 _ => true,
5171 };
5172 status
5173 .agent_runtime_id
5174 .as_ref()
5175 .is_some_and(|runtime_id| runtime_id.as_str() == alias.runtime_member_id)
5176 && alias.identity == status.identity
5177 && session_matches
5178}
5179
5180async fn rpc_stale_live_alias_error_response(
5181 identity_rt: &crate::identity_first::IdentityRuntime,
5182 target: &RpcIdentityControlTarget,
5183 response_id: Value,
5184) -> Option<JsonRpcResponse> {
5185 let live = target.live.as_ref()?;
5186 let Ok(status) = identity_rt.status(&target.identity).await else {
5187 return None;
5188 };
5189 if rpc_live_alias_matches_status_runtime(Some(live), &status) {
5190 return None;
5191 }
5192 Some(JsonRpcResponse {
5193 jsonrpc: JSONRPC_VERSION.to_string(),
5194 id: response_id,
5195 result: None,
5196 error: Some(JsonRpcError {
5197 code: -32000,
5198 message: format!(
5199 "identity runtime binding for {} points at {}, but requested live member is {}",
5200 target.identity.as_str(),
5201 status
5202 .agent_runtime_id
5203 .as_ref()
5204 .map(crate::identity_first::AgentRuntimeId::as_str)
5205 .unwrap_or("<none>"),
5206 live.runtime_member_id
5207 ),
5208 data: Some(serde_json::json!({
5209 "kind": "stale_identity_runtime_binding",
5210 "identity": target.identity.as_str(),
5211 "registered_runtime_member_id": status.agent_runtime_id.as_ref().map(crate::identity_first::AgentRuntimeId::as_str),
5212 "live_runtime_member_id": live.runtime_member_id,
5213 "registered_session_id": status.session_id.as_ref().map(ToString::to_string),
5214 "live_session_id": live.session_id,
5215 })),
5216 }),
5217 })
5218}
5219
5220fn rpc_member_is_addressable(member: &meerkat_mob::runtime::MobMemberListEntry) -> bool {
5221 member
5222 .labels
5223 .get("addressable")
5224 .map(|value| !value.eq_ignore_ascii_case("false"))
5225 .unwrap_or(true)
5226}
5227
5228fn rpc_live_identity_status_json(alias: &RpcLiveIdentityAlias) -> Value {
5229 serde_json::json!({
5230 "state": crate::mob_handle_runtime::member_status_state_string(alias.member.status),
5231 "identity": alias.identity.as_str(),
5232 "agent_runtime_id": alias.runtime_member_id,
5233 "session_id": alias.session_id,
5234 "profile": alias.member.role.to_string(),
5235 "addressability": if rpc_member_is_addressable(&alias.member) { "addressable" } else { "internal_only" },
5236 "display_name": alias.member.labels.get("display_name"),
5237 "labels": alias.member.labels,
5238 "generation": Value::Null,
5239 "checkpoint_version": Value::Null,
5240 "continuity_health": Value::Null,
5241 "lease_healthy": Value::Null,
5242 "lease": Value::Null,
5243 })
5244}
5245
5246async fn rpc_live_identity_inspect_json(
5247 runtime: &UnifiedRuntime,
5248 alias: &RpcLiveIdentityAlias,
5249) -> Value {
5250 let snapshot = runtime
5251 .mob_handle()
5252 .member_status(&crate::member_comms_id::mob_member_id(
5253 alias.runtime_member_id.as_str(),
5254 ))
5255 .await
5256 .ok();
5257 serde_json::json!({
5258 "identity": alias.identity.as_str(),
5259 "state": crate::mob_handle_runtime::member_status_state_string(alias.member.status),
5260 "profile": alias.member.role.to_string(),
5261 "addressability": if rpc_member_is_addressable(&alias.member) { "addressable" } else { "internal_only" },
5262 "display_name": alias.member.labels.get("display_name"),
5263 "labels": alias.member.labels,
5264 "generation": Value::Null,
5265 "checkpoint_version": Value::Null,
5266 "continuity_health": Value::Null,
5267 "lease_healthy": Value::Null,
5268 "continuity": {
5269 "generation": Value::Null,
5270 "checkpoint_version": Value::Null,
5271 "session_id": alias.session_id,
5272 "agent_runtime_id": alias.runtime_member_id,
5273 },
5274 "lease": Value::Null,
5275 "output_preview": snapshot.as_ref().and_then(|snapshot| snapshot.output_preview.clone()),
5276 "is_final": snapshot.as_ref().map(|snapshot| snapshot.is_final).unwrap_or(false),
5277 "peer_reachable_count": alias.member.wired_to.len(),
5278 "completion_cursor": Value::Null,
5282 "progress": snapshot.as_ref().and_then(|snapshot| snapshot.progress.clone()),
5285 })
5286}
5287
5288async fn retire_rpc_live_identity(
5289 runtime: &UnifiedRuntime,
5290 alias: &RpcLiveIdentityAlias,
5291) -> Result<(), String> {
5292 retire_rpc_runtime_member_id(runtime, alias.runtime_member_id.as_str()).await
5293}
5294
5295async fn retire_rpc_runtime_member_id(
5296 runtime: &UnifiedRuntime,
5297 runtime_member_id: &str,
5298) -> Result<(), String> {
5299 retire_rpc_runtime_member_id_with_handle(&runtime.mob_handle(), runtime_member_id).await
5300}
5301
5302async fn retire_rpc_runtime_member_id_with_handle(
5303 handle: &meerkat_mob::MobHandle,
5304 runtime_member_id: &str,
5305) -> Result<(), String> {
5306 match handle
5307 .retire(crate::member_comms_id::mob_member_id(runtime_member_id))
5308 .await
5309 {
5310 Ok(()) => Ok(()),
5311 Err(err) if mob_methods::lifecycle_archive_cleanup_completed(&err.to_string()) => Ok(()),
5312 Err(err) => Err(err.to_string()),
5313 }
5314}
5315
5316fn rpc_member_id_matches_durable_identity(member_id: &str, durable_identity: &str) -> bool {
5317 crate::member_comms_id::runtime_alias_str(member_id) == durable_identity
5320}
5321
5322fn rpc_runtime_alias_generation(alias: &str, durable_identity: &str) -> Option<u64> {
5323 let alias = crate::member_comms_id::runtime_alias_str(alias);
5324 let rest = alias.strip_prefix("rt:")?;
5325 let (identity, generation) = rest.rsplit_once(':')?;
5326 if identity != durable_identity {
5327 return None;
5328 }
5329 generation.parse().ok()
5330}
5331
5332async fn stale_rpc_member_ids_for_identity_with_handle(
5333 handle: &meerkat_mob::MobHandle,
5334 durable_identity: &str,
5335 current_runtime_member_id: Option<&str>,
5336 include_current: bool,
5337) -> Vec<String> {
5338 let Some(current_generation) = current_runtime_member_id
5339 .and_then(|alias| rpc_runtime_alias_generation(alias, durable_identity))
5340 else {
5341 return Vec::new();
5342 };
5343 handle
5344 .list_members_including_retiring()
5345 .await
5346 .into_iter()
5347 .filter(|member| {
5348 if !rpc_live_identity_alias_member_visible(member) {
5349 return false;
5350 }
5351 let matches_identity =
5352 rpc_member_id_matches_durable_identity(
5353 member.agent_identity.as_str(),
5354 durable_identity,
5355 ) || crate::member_comms_id::durable_identity_label(&member.labels)
5356 .is_some_and(|identity| identity == durable_identity);
5357 let public_alias =
5358 crate::member_comms_id::runtime_alias_str(member.agent_identity.as_str());
5359 matches_identity
5360 && rpc_runtime_alias_generation(public_alias.as_ref(), durable_identity)
5361 .is_some_and(|generation| {
5362 generation < current_generation
5363 || (include_current && generation == current_generation)
5364 })
5365 })
5366 .map(|member| {
5368 crate::member_comms_id::runtime_alias_str(member.agent_identity.as_str()).into_owned()
5369 })
5370 .collect()
5371}
5372
5373async fn retire_rpc_member_ids_with_handle(
5374 handle: &meerkat_mob::MobHandle,
5375 member_ids: Vec<String>,
5376) -> Result<(), String> {
5377 for member_id in member_ids {
5378 retire_rpc_runtime_member_id_with_handle(handle, &member_id).await?;
5379 }
5380 Ok(())
5381}
5382
5383async fn respawn_rpc_live_identity(
5384 runtime: &UnifiedRuntime,
5385 alias: &RpcLiveIdentityAlias,
5386) -> Result<Value, String> {
5387 let mut result = Box::pin(respawn_rpc_runtime_member_id(
5388 runtime,
5389 alias.runtime_member_id.as_str(),
5390 ))
5391 .await?;
5392 result["identity"] = serde_json::json!(alias.identity.as_str());
5393 Ok(result)
5394}
5395
5396async fn respawn_rpc_runtime_member_id(
5397 runtime: &UnifiedRuntime,
5398 runtime_member_id: &str,
5399) -> Result<Value, String> {
5400 respawn_rpc_runtime_member_id_with_handle(&runtime.mob_handle(), runtime_member_id).await
5401}
5402
5403async fn respawn_rpc_runtime_member_id_with_handle(
5404 handle: &meerkat_mob::MobHandle,
5405 runtime_member_id: &str,
5406) -> Result<Value, String> {
5407 let member_id = crate::member_comms_id::mob_member_id(runtime_member_id);
5408 let entry_before_respawn = handle.get_member(&member_id).await.ok().flatten();
5411 let mut topology_restore_warning = None;
5412 match handle.respawn(member_id.clone(), None).await {
5413 Ok(_receipt) => {}
5414 Err(err) => {
5415 if let Some(failed_peer_ids) = topology_restore_failed_peer_ids(&err) {
5416 tracing::warn!(
5417 member_id = %member_id,
5418 failed_peer_count = failed_peer_ids.len(),
5419 failed_peer_ids = ?failed_peer_ids,
5420 "rpc member respawn restored member with isolated peer edges; continuing degraded respawn"
5421 );
5422 topology_restore_warning = Some(topology_restore_warning_json(&failed_peer_ids));
5423 } else if mob_methods::lifecycle_archive_cleanup_completed(&err.to_string()) {
5424 if handle
5427 .get_member(&member_id)
5428 .await
5429 .map_err(|lookup_err| lookup_err.to_string())?
5430 .is_none()
5431 && let Some(entry) = entry_before_respawn
5432 {
5433 let mut spec =
5434 meerkat_mob::SpawnMemberSpec::new(entry.role.clone(), member_id.clone());
5435 if !entry.labels.is_empty() {
5436 spec = spec.with_labels(entry.labels.clone());
5437 }
5438 handle
5439 .ensure_member(spec)
5440 .await
5441 .map_err(|ensure_err| ensure_err.to_string())?;
5442 }
5443 } else {
5444 return Err(err.to_string());
5445 }
5446 }
5447 }
5448 let session_id = handle
5449 .resolve_bridge_session_id_observation(&member_id)
5450 .await
5451 .map(|session_id| session_id.to_string());
5452 Ok(serde_json::json!({
5453 "agent_runtime_id": runtime_member_id,
5454 "session_id": session_id,
5455 "generation": Value::Null,
5456 "checkpoint_version": Value::Null,
5457 "topology_restore_warning": topology_restore_warning,
5458 }))
5459}
5460
5461fn identity_not_configured(response_id: Value) -> String {
5462 error_response(response_id, -32601, "identity-first runtime not configured")
5463}
5464
5465fn maybe_identity_not_configured(is_notification: bool, response_id: Value) -> String {
5466 if is_notification {
5467 String::new()
5468 } else {
5469 identity_not_configured(response_id)
5470 }
5471}
5472
5473fn completion_cursor_json(cursor: crate::identity_first::CompletionCursor) -> Value {
5478 serde_json::json!({
5479 "epoch": cursor.epoch.get(),
5480 "turns": cursor.turns,
5481 })
5482}
5483
5484fn addressability_json(addressability: crate::identity_first::AgentAddressability) -> &'static str {
5485 match addressability {
5486 crate::identity_first::AgentAddressability::Addressable => "addressable",
5487 crate::identity_first::AgentAddressability::InternalOnly => "internal_only",
5488 }
5489}
5490
5491fn identity_lifecycle_state_json(
5494 state: crate::identity_first::IdentityLifecycleState,
5495) -> &'static str {
5496 state.wire_str()
5497}
5498
5499fn identity_error_response(
5500 response_id: Value,
5501 err: &crate::identity_first::IdentityRuntimeError,
5502) -> JsonRpcResponse {
5503 use crate::identity_first::IdentityRuntimeError;
5504 let (code, message) = match err {
5505 IdentityRuntimeError::UnknownIdentity(id) => (-32001, format!("unknown identity: {id}")),
5506 IdentityRuntimeError::NotAddressable(na) => {
5507 (-32002, format!("not addressable: {}", na.identity))
5508 }
5509 IdentityRuntimeError::NoActiveLease(id) => (-32003, format!("no active lease: {id}")),
5510 IdentityRuntimeError::LeaseLost(id) => (-32005, format!("lease lost: {id}")),
5516 _ => (-32603, format!("{err}")),
5517 };
5518 JsonRpcResponse {
5519 jsonrpc: JSONRPC_VERSION.to_string(),
5520 id: response_id,
5521 result: None,
5522 error: Some(JsonRpcError {
5523 code,
5524 message,
5525 data: None,
5526 }),
5527 }
5528}
5529
5530fn error_response(response_id: Value, code: i64, message: impl Into<String>) -> String {
5531 let message = message.into();
5532 let ambiguous_alias_rest = message
5533 .strip_prefix("ambiguous live identity alias ")
5534 .or_else(|| message.strip_prefix("invalid identity: ambiguous live identity alias "));
5535 let stale_live_alias_rest = message
5536 .strip_prefix("stale live identity alias: live console alias ")
5537 .or_else(|| {
5538 message.strip_prefix("invalid identity: stale live identity alias: live console alias ")
5539 });
5540 let hidden_policy_identity = message
5541 .strip_prefix("identity hidden by policy: ")
5542 .or_else(|| message.strip_prefix("invalid identity: identity hidden by policy: "));
5543 let data = if let Some(rest) = ambiguous_alias_rest {
5544 let (identity, candidates) = rest
5545 .split_once(": candidates [")
5546 .map(|(identity, candidates)| {
5547 (
5548 identity.to_string(),
5549 candidates
5550 .trim_end_matches(']')
5551 .split(',')
5552 .map(str::trim)
5553 .filter(|value| !value.is_empty())
5554 .map(str::to_string)
5555 .collect::<Vec<_>>(),
5556 )
5557 })
5558 .unwrap_or_else(|| (rest.to_string(), Vec::new()));
5559 Some(serde_json::json!({
5560 "kind": "ambiguous_live_identity_alias",
5561 "identity": identity,
5562 "candidates": candidates,
5563 }))
5564 } else if let Some(rest) = stale_live_alias_rest {
5565 let (identity, rest) = rest.split_once(" resolves to ").unwrap_or((rest, ""));
5566 let (runtime_member_id, bound_identity) = rest
5567 .split_once(", but identity runtime binding belongs to ")
5568 .unwrap_or((rest, ""));
5569 Some(serde_json::json!({
5570 "kind": "stale_live_identity_alias",
5571 "identity": identity,
5572 "live_runtime_member_id": runtime_member_id,
5573 "bound_identity": bound_identity,
5574 }))
5575 } else {
5576 hidden_policy_identity.map(|identity| {
5577 serde_json::json!({
5578 "kind": "identity_hidden_by_policy",
5579 "identity": identity,
5580 })
5581 })
5582 };
5583 serialize_response(&JsonRpcResponse {
5584 jsonrpc: JSONRPC_VERSION.to_string(),
5585 id: response_id,
5586 result: None,
5587 error: Some(JsonRpcError {
5588 code,
5589 message,
5590 data,
5591 }),
5592 })
5593}
5594
5595fn live_member_alias(params: &Value) -> Option<String> {
5601 if params.get("session_id").and_then(Value::as_str).is_some() {
5602 return None;
5603 }
5604 params
5605 .get("identity")
5606 .and_then(Value::as_str)
5607 .or_else(|| params.get("member_id").and_then(Value::as_str))
5608 .map(|raw| crate::member_comms_id::runtime_alias_str(raw).into_owned())
5609}
5610
5611async fn resolve_live_target(
5612 handle: &meerkat_mob::MobHandle,
5613 identity_runtime: Option<&Arc<crate::identity_first::IdentityRuntime>>,
5614 identity_authoritative: bool,
5615 params: &Value,
5616) -> Result<Option<meerkat_core::types::SessionId>, String> {
5617 if let Some(raw) = params.get("session_id").and_then(Value::as_str) {
5618 return Ok(meerkat_core::types::SessionId::parse(raw).ok());
5619 }
5620 let Some(raw) = live_member_alias(params) else {
5621 return Ok(None);
5622 };
5623 let current_alias = if identity_authoritative {
5624 let identity_runtime = identity_runtime
5625 .ok_or_else(|| "durable live target lost its IdentityRuntime authority".to_string())?;
5626 let identity =
5627 crate::identity_first::IdentityRuntime::identity_for_generated_member_alias(&raw)
5628 .or_else(|| crate::identity_first::AgentIdentity::parse(&raw).ok())
5629 .ok_or_else(|| format!("invalid durable live target {raw:?}"))?;
5630 identity_runtime
5631 .status(&identity)
5632 .await
5633 .map_err(|error| error.to_string())?
5634 .agent_runtime_id
5635 .map(|runtime_id| runtime_id.to_string())
5636 .ok_or_else(|| format!("identity {identity} has no current runtime member"))?
5637 } else {
5638 let direct = crate::member_comms_id::mob_member_id(&raw);
5639 if handle
5640 .get_member(&direct)
5641 .await
5642 .map_err(|error| error.to_string())?
5643 .is_some()
5644 {
5645 raw.clone()
5646 } else {
5647 let candidates = handle
5648 .list_members_including_retiring()
5649 .await
5650 .into_iter()
5651 .filter(|entry| {
5652 crate::member_comms_id::durable_identity_label(&entry.labels)
5653 .is_some_and(|identity| identity == raw)
5654 })
5655 .map(|entry| {
5656 crate::member_comms_id::runtime_alias_str(entry.agent_identity.as_str())
5657 .into_owned()
5658 })
5659 .collect::<BTreeSet<_>>();
5660 match candidates.len() {
5661 0 => raw.clone(),
5662 1 => candidates
5663 .into_iter()
5664 .next()
5665 .ok_or_else(|| "live member alias candidate disappeared".to_string())?,
5666 _ => {
5667 return Err(format!(
5668 "ambiguous live member alias {raw}: candidates [{}]",
5669 candidates.into_iter().collect::<Vec<_>>().join(", ")
5670 ));
5671 }
5672 }
5673 }
5674 };
5675 let member_id = crate::member_comms_id::mob_member_id(¤t_alias);
5676 Ok(handle.resolve_bridge_session_id(&member_id).await)
5677}
5678
5679fn maybe_error_response(
5680 is_notification: bool,
5681 response_id: Value,
5682 code: i64,
5683 message: impl Into<String>,
5684) -> String {
5685 if is_notification {
5686 String::new()
5687 } else {
5688 error_response(response_id, code, message)
5689 }
5690}
5691
5692pub(crate) fn agent_memory_rpc_error(
5693 operation: &str,
5694 err: crate::identity_first::AgentMemoryError,
5695) -> JsonRpcError {
5696 let code = match &err {
5697 crate::identity_first::AgentMemoryError::InvalidConfig(_)
5698 | crate::identity_first::AgentMemoryError::InvalidRecord(_) => -32602,
5699 crate::identity_first::AgentMemoryError::Unsupported(_) => -32601,
5700 crate::identity_first::AgentMemoryError::Io(_)
5701 | crate::identity_first::AgentMemoryError::Parse(_)
5702 | crate::identity_first::AgentMemoryError::Timeout(_) => -32603,
5703 };
5704 JsonRpcError {
5705 code,
5706 message: format!("agent memory {operation} failed: {err}"),
5707 data: None,
5708 }
5709}
5710
5711fn serialize_response(response: &JsonRpcResponse) -> String {
5712 serde_json::to_string(response).unwrap_or_else(|_| {
5713 r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Internal error"}}"#
5714 .to_string()
5715 })
5716}
5717
5718#[cfg(test)]
5719#[allow(clippy::expect_used)]
5720mod tests {
5721 use super::{
5722 IdentityMemberReadiness, error_response, handle_unified_rpc_json, identity_error_response,
5723 resolve_live_target, resolve_rpc_identity_control_target, rpc_live_identity_alias_visible,
5724 rpc_member_id_matches_durable_identity, rpc_runtime_alias_generation,
5725 wait_identity_startup_ready,
5726 };
5727 use crate::identity_first::contracts::{ContinuityStore, LeaseProvider, RosterProvider};
5728 use crate::identity_first::{
5729 AgentAddressability, AgentBuildDraft, AgentIdentity, AgentRuntimeId, BridgeError,
5730 CheckpointVersion, ContinuityGeneration, ContinuityRecord, DurabilityPolicy,
5731 DurableAgentSpec, FencingToken, IdentityLifecycleState, IdentityRuntime,
5732 IdentityRuntimeConfig, LeaseAcquireResult, LeaseGrant, LocalContinuityStore,
5733 LocalLeaseProvider, MarkdownAgentMemoryStore, ResumeSessionOutcome, RosterContext,
5734 RosterError, SessionBridge, SessionSnapshot,
5735 };
5736 use crate::{
5737 DiscoverySpec, IdentityFirstContext, MobBootstrapOptions, MobBootstrapSpec, MobKitConfig,
5738 UnifiedRuntime,
5739 };
5740 use async_trait::async_trait;
5741 use meerkat::{AgentFactory, Config, build_ephemeral_service};
5742 use meerkat_client::TestClient;
5743 use meerkat_mob::{MobDefinition, MobStorage, SpawnMemberSpec};
5744 use serde_json::{Value, json};
5745 use std::collections::BTreeMap;
5746 use std::sync::Arc;
5747 use std::time::Duration;
5748
5749 #[derive(Debug, Default)]
5750 struct EmptyRosterProvider;
5751
5752 #[async_trait]
5753 impl RosterProvider for EmptyRosterProvider {
5754 async fn roster(
5755 &self,
5756 _context: &RosterContext,
5757 ) -> Result<Vec<DurableAgentSpec>, RosterError> {
5758 Ok(Vec::new())
5759 }
5760 }
5761
5762 #[derive(Debug, Default)]
5763 struct ContextRequiredEmptyRosterProvider {
5764 missing_definition_calls: std::sync::atomic::AtomicUsize,
5765 }
5766
5767 impl ContextRequiredEmptyRosterProvider {
5768 fn missing_definition_calls(&self) -> usize {
5769 self.missing_definition_calls
5770 .load(std::sync::atomic::Ordering::SeqCst)
5771 }
5772 }
5773
5774 #[async_trait]
5775 impl RosterProvider for ContextRequiredEmptyRosterProvider {
5776 async fn roster(
5777 &self,
5778 context: &RosterContext,
5779 ) -> Result<Vec<DurableAgentSpec>, RosterError> {
5780 if context.mob_definition.is_none() {
5781 self.missing_definition_calls
5782 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5783 return Err(RosterError::ProviderUnavailable(
5784 "mob definition required".to_string(),
5785 ));
5786 }
5787 Ok(Vec::new())
5788 }
5789 }
5790
5791 #[derive(Debug)]
5792 struct ContextRequiredStaticRosterProvider {
5793 specs: Vec<DurableAgentSpec>,
5794 missing_definition_calls: std::sync::atomic::AtomicUsize,
5795 }
5796
5797 impl ContextRequiredStaticRosterProvider {
5798 fn new(specs: Vec<DurableAgentSpec>) -> Self {
5799 Self {
5800 specs,
5801 missing_definition_calls: std::sync::atomic::AtomicUsize::new(0),
5802 }
5803 }
5804
5805 fn missing_definition_calls(&self) -> usize {
5806 self.missing_definition_calls
5807 .load(std::sync::atomic::Ordering::SeqCst)
5808 }
5809 }
5810
5811 #[async_trait]
5812 impl RosterProvider for ContextRequiredStaticRosterProvider {
5813 async fn roster(
5814 &self,
5815 context: &RosterContext,
5816 ) -> Result<Vec<DurableAgentSpec>, RosterError> {
5817 if context.mob_definition.is_none() {
5818 self.missing_definition_calls
5819 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5820 return Err(RosterError::ProviderUnavailable(
5821 "mob definition required".to_string(),
5822 ));
5823 }
5824 Ok(self.specs.clone())
5825 }
5826 }
5827
5828 #[derive(Debug, Default)]
5829 struct RpcResetTestBridge {
5830 create_calls: std::sync::atomic::AtomicUsize,
5831 last_create_spec: tokio::sync::Mutex<Option<DurableAgentSpec>>,
5832 }
5833
5834 impl RpcResetTestBridge {
5835 async fn last_create_spec(&self) -> Option<DurableAgentSpec> {
5836 self.last_create_spec.lock().await.clone()
5837 }
5838 }
5839
5840 #[async_trait]
5841 impl SessionBridge for RpcResetTestBridge {
5842 async fn create_session(
5843 &self,
5844 _identity: &AgentIdentity,
5845 _runtime_id: &AgentRuntimeId,
5846 spec: &DurableAgentSpec,
5847 _draft: &AgentBuildDraft,
5848 session_id: &meerkat_core::types::SessionId,
5849 ) -> Result<meerkat_core::types::SessionId, BridgeError> {
5850 self.create_calls
5851 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5852 *self.last_create_spec.lock().await = Some(spec.clone());
5853 Ok(session_id.clone())
5854 }
5855
5856 async fn resume_session(
5857 &self,
5858 _identity: &AgentIdentity,
5859 _runtime_id: &AgentRuntimeId,
5860 _spec: &DurableAgentSpec,
5861 _draft: &AgentBuildDraft,
5862 session_id: &meerkat_core::types::SessionId,
5863 _snapshot: &SessionSnapshot,
5864 ) -> Result<ResumeSessionOutcome, BridgeError> {
5865 Ok(ResumeSessionOutcome::Resumed {
5866 session_id: session_id.clone(),
5867 })
5868 }
5869
5870 async fn deliver_admitted(
5871 &self,
5872 _runtime_id: &AgentRuntimeId,
5873 _delivery: crate::identity_first::BridgeDelivery,
5874 ) -> Result<meerkat_core::types::SessionId, BridgeError> {
5875 Ok(meerkat_core::types::SessionId::new())
5876 }
5877
5878 async fn checkpoint_session(
5879 &self,
5880 _runtime_id: &AgentRuntimeId,
5881 _session_id: &meerkat_core::types::SessionId,
5882 ) -> Result<SessionSnapshot, BridgeError> {
5883 Ok(SessionSnapshot { data: Vec::new() })
5884 }
5885
5886 async fn retire_member(&self, _runtime_id: &AgentRuntimeId) -> Result<(), BridgeError> {
5887 Ok(())
5888 }
5889 }
5890
5891 struct ReadOnlyAgentMemoryProvider;
5892
5893 #[async_trait]
5894 impl crate::identity_first::AgentMemoryProvider for ReadOnlyAgentMemoryProvider {
5895 async fn recall(
5896 &self,
5897 _request: crate::identity_first::AgentMemoryRecallRequest,
5898 ) -> Result<
5899 Vec<crate::identity_first::AgentMemoryRecord>,
5900 crate::identity_first::AgentMemoryError,
5901 > {
5902 Ok(Vec::new())
5903 }
5904 }
5905
5906 fn rpc_test_mob_spec(
5907 temp_dir: &tempfile::TempDir,
5908 ) -> Result<MobBootstrapSpec, Box<dyn std::error::Error + Send + Sync>> {
5909 let session_path = temp_dir.path().join("sessions");
5910 std::fs::create_dir_all(&session_path)?;
5911 let factory = AgentFactory::new(&session_path).comms(true);
5912 let session_service = Arc::new(build_ephemeral_service(factory, Config::default(), 16));
5913 let definition = MobDefinition::from_toml(
5914 r#"
5915[mob]
5916id = "rpc-identity-alias-test"
5917
5918[profiles.worker]
5919model = "gpt-5.5"
5920external_addressable = true
5921
5922[profiles.worker.tools]
5923comms = true
5924"#,
5925 )?;
5926 Ok(
5927 MobBootstrapSpec::new(definition, MobStorage::in_memory(), session_service)
5928 .with_options(MobBootstrapOptions {
5929 allow_ephemeral_sessions: true,
5930 notify_orchestrator_on_resume: true,
5931 default_llm_client: Some(Arc::new(TestClient::default())),
5932 }),
5933 )
5934 }
5935
5936 async fn spawn_identity_projection_fixture(
5941 runtime: &UnifiedRuntime,
5942 mut spec: SpawnMemberSpec,
5943 ) -> Result<(), meerkat_mob::MobError> {
5944 let runtime_alias = spec.identity.to_string();
5945 spec.identity = crate::member_comms_id::mob_member_id(&runtime_alias);
5946 Box::pin(runtime.mob_handle().spawn_spec(spec)).await?;
5947 Ok(())
5948 }
5949
5950 fn rpc_reprofile_mob_spec(
5951 temp_dir: &tempfile::TempDir,
5952 ) -> Result<MobBootstrapSpec, Box<dyn std::error::Error + Send + Sync>> {
5953 let session_path = temp_dir.path().join("sessions");
5954 std::fs::create_dir_all(&session_path)?;
5955 let factory = AgentFactory::new(&session_path).comms(true);
5956 let session_service = Arc::new(build_ephemeral_service(factory, Config::default(), 16));
5957 let definition = MobDefinition::from_toml(
5958 r#"
5959[mob]
5960id = "rpc-reset-reprofile-test"
5961
5962[profiles.domain]
5963model = "gpt-5.5"
5964external_addressable = true
5965
5966[profiles.domain.tools]
5967comms = true
5968
5969[profiles.security]
5970model = "gpt-5.5"
5971external_addressable = true
5972
5973[profiles.security.tools]
5974comms = true
5975shell = true
5976"#,
5977 )?;
5978 Ok(
5979 MobBootstrapSpec::new(definition, MobStorage::in_memory(), session_service)
5980 .with_options(MobBootstrapOptions {
5981 allow_ephemeral_sessions: true,
5982 notify_orchestrator_on_resume: true,
5983 default_llm_client: Some(Arc::new(TestClient::default())),
5984 }),
5985 )
5986 }
5987
5988 fn rpc_durable_spec(identity: &str, profile: &str) -> DurableAgentSpec {
5989 DurableAgentSpec {
5990 identity: AgentIdentity::parse(identity).expect("valid identity"),
5991 profile: meerkat_mob::ProfileName::from(profile),
5992 addressability: AgentAddressability::Addressable,
5993 display_name: None,
5994 labels: BTreeMap::new(),
5995 context: None,
5996 additional_instructions: Vec::new(),
5997 initial_message: None,
5998 runtime_mode_override: None,
5999 backend: None,
6000 binding: None,
6001 }
6002 }
6003
6004 #[tokio::test]
6011 async fn unified_rpc_spawn_rejects_reserved_system_identity()
6012 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6013 let temp_dir = tempfile::tempdir()?;
6014 let runtime = Box::pin(
6015 UnifiedRuntime::builder()
6016 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6017 .module_config(MobKitConfig {
6018 modules: Vec::new(),
6019 discovery: DiscoverySpec {
6020 namespace: "rpc-reserved-identity-test".to_string(),
6021 modules: Vec::new(),
6022 },
6023 pre_spawn: Vec::new(),
6024 })
6025 .timeout(Duration::from_secs(1))
6026 .build(),
6027 )
6028 .await?;
6029
6030 let response: Value = serde_json::from_str(
6031 &handle_unified_rpc_json(
6032 &runtime,
6033 &json!({
6034 "jsonrpc": "2.0",
6035 "id": 1,
6036 "method": "mobkit/spawn_member",
6037 "params": {
6038 "profile": "worker",
6039 "meerkat_id": crate::console_contracts::SYSTEM_EVENT_IDENTITY,
6040 },
6041 })
6042 .to_string(),
6043 Duration::from_secs(1),
6044 None,
6045 None,
6046 )
6047 .await,
6048 )?;
6049
6050 assert_eq!(response["error"]["code"], json!(-32602), "{response:#?}");
6051 assert!(
6052 response["error"]["message"]
6053 .as_str()
6054 .is_some_and(|message| message.contains("reserved")),
6055 "rejection must name the reservation: {response:#?}"
6056 );
6057 let _ = runtime.mob_handle().stop().await;
6058 Ok(())
6059 }
6060
6061 #[tokio::test]
6062 async fn unified_rpc_spawn_member_rejects_generated_runtime_alias_namespace()
6063 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6064 let temp_dir = tempfile::tempdir()?;
6065 let runtime = Box::pin(
6066 UnifiedRuntime::builder()
6067 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6068 .module_config(MobKitConfig {
6069 modules: Vec::new(),
6070 discovery: DiscoverySpec {
6071 namespace: "rpc-generated-alias-reservation-test".to_string(),
6072 modules: Vec::new(),
6073 },
6074 pre_spawn: Vec::new(),
6075 })
6076 .timeout(Duration::from_secs(1))
6077 .build(),
6078 )
6079 .await?;
6080
6081 let response: Value = serde_json::from_str(
6082 &handle_unified_rpc_json(
6083 &runtime,
6084 &json!({
6085 "jsonrpc": "2.0",
6086 "id": 1,
6087 "method": "mobkit/spawn_member",
6088 "params": {
6089 "profile": "worker",
6090 "meerkat_id": "rt:user:forged:0",
6091 },
6092 })
6093 .to_string(),
6094 Duration::from_secs(1),
6095 None,
6096 None,
6097 )
6098 .await,
6099 )?;
6100
6101 assert_eq!(response["error"]["code"], json!(-32602), "{response:#?}");
6102 assert!(
6103 response["error"]["message"]
6104 .as_str()
6105 .is_some_and(|message| message.contains("reserved")),
6106 "generated aliases must be rejected at admission: {response:#?}"
6107 );
6108 assert!(
6109 runtime.mob_handle().list_members().await.is_empty(),
6110 "reserved namespace rejection must happen before raw member spawn"
6111 );
6112 let _ = runtime.mob_handle().stop().await;
6113 Ok(())
6114 }
6115
6116 #[tokio::test]
6117 async fn unified_rpc_spawn_holds_explicit_identity_context_alias_reservation()
6118 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6119 let temp_dir = tempfile::tempdir()?;
6120 let runtime = Box::pin(
6121 UnifiedRuntime::builder()
6122 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6123 .module_config(MobKitConfig {
6124 modules: Vec::new(),
6125 discovery: DiscoverySpec {
6126 namespace: "rpc-explicit-identity-context-reservation-test".to_string(),
6127 modules: Vec::new(),
6128 },
6129 pre_spawn: Vec::new(),
6130 })
6131 .timeout(Duration::from_secs(1))
6132 .build(),
6133 )
6134 .await?;
6135 assert!(runtime.identity_runtime().is_none());
6136
6137 let identity_runtime = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6138 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6139 lease_provider: Arc::new(LocalLeaseProvider::new()),
6140 runtime_instance_id: "rpc-explicit-identity-context-reservation-test".to_string(),
6141 has_runtime_store: true,
6142 durability_policy: DurabilityPolicy::SyncWriteThrough,
6143 bridge: None,
6144 default_timeout: None,
6145 }));
6146 let identity_ctx = IdentityFirstContext {
6147 runtime: identity_runtime.clone(),
6148 roster_provider: Arc::new(EmptyRosterProvider),
6149 topology_provider: None,
6150 customizer: None,
6151 agent_memory_provider: None,
6152 mob_definition: Some(runtime.mob_handle().definition().clone()),
6153 };
6154 let held_lock = identity_runtime
6155 .raw_member_alias_lock("compat-worker")
6156 .await
6157 .lock_owned()
6158 .await;
6159 let request = json!({
6160 "jsonrpc": "2.0",
6161 "id": 1,
6162 "method": "mobkit/spawn_member",
6163 "params": {
6164 "profile": "worker",
6165 "meerkat_id": " compat-worker ",
6166 },
6167 })
6168 .to_string();
6169
6170 assert!(
6171 tokio::time::timeout(
6172 Duration::from_millis(25),
6173 handle_unified_rpc_json(
6174 &runtime,
6175 &request,
6176 Duration::from_secs(1),
6177 None,
6178 Some(&identity_ctx),
6179 ),
6180 )
6181 .await
6182 .is_err(),
6183 "explicit identity authority must block the raw spawn on its canonical alias lock"
6184 );
6185 drop(held_lock);
6186
6187 let response: Value = serde_json::from_str(
6188 &tokio::time::timeout(
6189 Duration::from_secs(2),
6190 handle_unified_rpc_json(
6191 &runtime,
6192 &request,
6193 Duration::from_secs(1),
6194 None,
6195 Some(&identity_ctx),
6196 ),
6197 )
6198 .await?,
6199 )?;
6200 assert!(response["error"].is_null(), "{response:#?}");
6201 assert_eq!(response["result"]["meerkat_id"], json!("compat-worker"));
6202
6203 let _ = runtime.mob_handle().stop().await;
6204 Ok(())
6205 }
6206
6207 #[tokio::test]
6208 async fn unified_rpc_rejects_encoded_roster_ingress_and_authoritative_identity_labels()
6209 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6210 let temp_dir = tempfile::tempdir()?;
6211 let runtime = Box::pin(
6212 UnifiedRuntime::builder()
6213 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6214 .module_config(MobKitConfig {
6215 modules: Vec::new(),
6216 discovery: DiscoverySpec {
6217 namespace: "rpc-public-alias-ingress-test".to_string(),
6218 modules: Vec::new(),
6219 },
6220 pre_spawn: Vec::new(),
6221 })
6222 .timeout(Duration::from_secs(1))
6223 .build(),
6224 )
6225 .await?;
6226
6227 let encoded = crate::member_comms_id::mob_member_id_str("rt:secret:0");
6228 let encoded_response: Value = serde_json::from_str(
6229 &handle_unified_rpc_json(
6230 &runtime,
6231 &json!({
6232 "jsonrpc": "2.0",
6233 "id": 1,
6234 "method": "mobkit/get_member",
6235 "params": { "member_id": encoded.as_ref() },
6236 })
6237 .to_string(),
6238 Duration::from_secs(1),
6239 None,
6240 None,
6241 )
6242 .await,
6243 )?;
6244 assert_eq!(
6245 encoded_response["error"]["code"],
6246 json!(-32602),
6247 "encoded roster spelling must be rejected before resolution: {encoded_response:#?}"
6248 );
6249
6250 let forged_label_response: Value = serde_json::from_str(
6251 &handle_unified_rpc_json(
6252 &runtime,
6253 &json!({
6254 "jsonrpc": "2.0",
6255 "id": 2,
6256 "method": "mobkit/ensure_member",
6257 "params": {
6258 "role": "worker",
6259 "agent_identity": "raw-worker",
6260 "labels": { "agent_identity": "secret" },
6261 },
6262 })
6263 .to_string(),
6264 Duration::from_secs(1),
6265 None,
6266 None,
6267 )
6268 .await,
6269 )?;
6270 assert_eq!(
6271 forged_label_response["error"]["code"],
6272 json!(-32602),
6273 "raw creation must not mint runtime-authoritative identity labels: {forged_label_response:#?}"
6274 );
6275 assert!(runtime.mob_handle().list_members().await.is_empty());
6276
6277 let _ = runtime.mob_handle().stop().await;
6278 Ok(())
6279 }
6280
6281 #[tokio::test]
6282 async fn unified_capabilities_separate_mobpack_authoring_from_runtime_controls()
6283 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6284 let temp_dir = tempfile::tempdir()?;
6285 let runtime = Box::pin(
6286 UnifiedRuntime::builder()
6287 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6288 .module_config(MobKitConfig {
6289 modules: Vec::new(),
6290 discovery: DiscoverySpec {
6291 namespace: "rpc-authoring-capabilities-test".to_string(),
6292 modules: Vec::new(),
6293 },
6294 pre_spawn: Vec::new(),
6295 })
6296 .timeout(Duration::from_secs(1))
6297 .build(),
6298 )
6299 .await?;
6300
6301 let response: Value = serde_json::from_str(
6302 &handle_unified_rpc_json(
6303 &runtime,
6304 &json!({
6305 "jsonrpc": "2.0",
6306 "id": 1,
6307 "method": "mobkit/capabilities",
6308 })
6309 .to_string(),
6310 Duration::from_secs(1),
6311 None,
6312 None,
6313 )
6314 .await,
6315 )?;
6316
6317 assert!(response["error"].is_null(), "{response:#?}");
6318 let methods = response["result"]["methods"]
6319 .as_array()
6320 .expect("methods array")
6321 .iter()
6322 .filter_map(Value::as_str)
6323 .collect::<Vec<_>>();
6324 for method in super::MOBPACK_AUTHORING_METHODS {
6325 assert!(
6326 methods.contains(method),
6327 "missing authoring method {method}"
6328 );
6329 }
6330 assert_eq!(
6331 response["result"]["authoring_capabilities"]["domain"],
6332 json!("mobpack_authoring")
6333 );
6334 assert_eq!(
6335 response["result"]["authoring_capabilities"]["runtime_mutation"],
6336 json!(false)
6337 );
6338 assert_eq!(
6339 response["result"]["authoring_capabilities"]["host_mutation_methods"]["mobkit/mobpacks/deploy"],
6340 json!("when execute=true, writes a mobpack archive and runs rkat mob run on the host")
6341 );
6342 assert_eq!(
6343 response["result"]["authoring_capabilities"]["methods"]
6344 .as_array()
6345 .expect("authoring methods")
6346 .iter()
6347 .filter_map(Value::as_str)
6348 .collect::<Vec<_>>(),
6349 super::MOBPACK_AUTHORING_METHODS
6350 );
6351 assert_eq!(
6352 response["result"]["authoring_capabilities"]["deploy_command"],
6353 json!("rkat mob run")
6354 );
6355
6356 Ok(())
6357 }
6358
6359 #[tokio::test]
6360 async fn unified_rpc_dispatches_mobpack_authoring_methods()
6361 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6362 let temp_dir = tempfile::tempdir()?;
6363 let runtime = Box::pin(
6364 UnifiedRuntime::builder()
6365 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6366 .module_config(MobKitConfig {
6367 modules: Vec::new(),
6368 discovery: DiscoverySpec {
6369 namespace: "rpc-authoring-dispatch-test".to_string(),
6370 modules: Vec::new(),
6371 },
6372 pre_spawn: Vec::new(),
6373 })
6374 .timeout(Duration::from_secs(1))
6375 .build(),
6376 )
6377 .await?;
6378
6379 let response: Value = serde_json::from_str(
6380 &handle_unified_rpc_json(
6381 &runtime,
6382 &json!({
6383 "jsonrpc": "2.0",
6384 "id": 1,
6385 "method": "mobkit/mobpacks/schema",
6386 })
6387 .to_string(),
6388 Duration::from_secs(1),
6389 None,
6390 None,
6391 )
6392 .await,
6393 )?;
6394
6395 assert!(response["error"].is_null(), "{response:#?}");
6396 assert_eq!(
6397 response["result"]["media_type"],
6398 json!("application/vnd.meerkat.mobpack")
6399 );
6400 assert_eq!(
6401 response["result"]["commands"]["deploy_rpc"],
6402 json!("mobkit/mobpacks/deploy")
6403 );
6404 assert_eq!(
6405 response["result"]["deploy_settings"]["runtime_backed"],
6406 json!(true)
6407 );
6408 assert_eq!(
6409 response["result"]["deploy_settings"]["authoring_provider"]["runtime_binding"],
6410 json!("bound")
6411 );
6412 assert_eq!(
6413 response["result"]["deploy_settings"]["provenance"]["source"],
6414 json!("UnifiedRuntime.authoring_provider.deploy_target")
6415 );
6416 assert!(response["result"]["sample_mobpacks"].is_null());
6417 assert!(response["result"]["agent_definitions"].is_null());
6418
6419 let catalogs: Value = serde_json::from_str(
6420 &handle_unified_rpc_json(
6421 &runtime,
6422 &json!({
6423 "jsonrpc": "2.0",
6424 "id": 2,
6425 "method": "mobkit/mobpacks/catalogs",
6426 })
6427 .to_string(),
6428 Duration::from_secs(1),
6429 None,
6430 None,
6431 )
6432 .await,
6433 )?;
6434 assert!(catalogs["error"].is_null(), "{catalogs:#?}");
6435 assert_eq!(catalogs["result"]["runtime_backed"], json!(true));
6436 assert_eq!(
6437 catalogs["result"]["authoring_provider"]["id"],
6438 json!("unified_runtime")
6439 );
6440 assert_eq!(
6441 catalogs["result"]["authoring_provider"]["runtime_binding"],
6442 json!("bound")
6443 );
6444 assert_eq!(
6445 catalogs["result"]["sources"]["runtime"],
6446 json!("unified_runtime")
6447 );
6448 assert_eq!(
6449 catalogs["result"]["sources"]["runtime_binding"],
6450 json!("bound")
6451 );
6452 assert!(
6453 catalogs["result"]["runtime_unavailable_reason"].is_null(),
6454 "{catalogs:#?}"
6455 );
6456 assert_eq!(
6457 catalogs["result"]["catalog_snapshot"]["runtime_backed"],
6458 json!(true)
6459 );
6460 assert_eq!(
6461 catalogs["result"]["authoring_provider"]["deploy_target"]["command"],
6462 json!("rkat mob run")
6463 );
6464 assert!(
6465 catalogs["result"]["authoring_provider"]["runtime_methods"]
6466 .as_array()
6467 .is_some_and(|methods| methods.contains(&json!("mobkit/mobpacks/deploy"))),
6468 "{catalogs:#?}"
6469 );
6470
6471 Ok(())
6472 }
6473
6474 #[tokio::test]
6475 async fn reconcile_identity_passes_mob_definition_to_roster_provider()
6476 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6477 let temp_dir = tempfile::tempdir()?;
6478 let runtime = Box::pin(
6479 UnifiedRuntime::builder()
6480 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6481 .module_config(MobKitConfig {
6482 modules: Vec::new(),
6483 discovery: DiscoverySpec {
6484 namespace: "rpc-reconcile-roster-context-test".to_string(),
6485 modules: Vec::new(),
6486 },
6487 pre_spawn: Vec::new(),
6488 })
6489 .timeout(Duration::from_secs(1))
6490 .build(),
6491 )
6492 .await?;
6493 let identity_rt = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6494 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6495 lease_provider: Arc::new(LocalLeaseProvider::new()),
6496 runtime_instance_id: "rpc-reconcile-roster-context-test".to_string(),
6497 has_runtime_store: true,
6498 durability_policy: DurabilityPolicy::SyncWriteThrough,
6499 bridge: None,
6500 default_timeout: None,
6501 }));
6502 let roster_provider = Arc::new(ContextRequiredEmptyRosterProvider::default());
6503 let identity_ctx = IdentityFirstContext {
6504 runtime: identity_rt,
6505 roster_provider: roster_provider.clone(),
6506 topology_provider: None,
6507 customizer: None,
6508 agent_memory_provider: None,
6509 mob_definition: Some(runtime.mob_handle().definition().clone()),
6510 };
6511
6512 let response: Value = serde_json::from_str(
6513 &handle_unified_rpc_json(
6514 &runtime,
6515 &json!({
6516 "jsonrpc": "2.0",
6517 "id": 1,
6518 "method": "mobkit/reconcile_identity",
6519 "params": {},
6520 })
6521 .to_string(),
6522 Duration::from_secs(1),
6523 None,
6524 Some(&identity_ctx),
6525 )
6526 .await,
6527 )?;
6528
6529 assert!(response["error"].is_null(), "{response:#?}");
6530 assert_eq!(
6531 roster_provider.missing_definition_calls(),
6532 0,
6533 "reconcile_identity must preserve mob_definition in roster context"
6534 );
6535 Ok(())
6536 }
6537
6538 #[tokio::test]
6539 async fn startup_ready_wait_retries_stale_member_success_and_error_after_generation_change()
6540 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6541 for stale_result in [
6542 IdentityMemberReadiness::Ready,
6543 IdentityMemberReadiness::Failed("stale member failure".to_string()),
6544 ] {
6545 let identity_runtime = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6546 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6547 lease_provider: Arc::new(LocalLeaseProvider::new()),
6548 runtime_instance_id: "rpc-startup-ready-generation-test".to_string(),
6549 has_runtime_store: true,
6550 durability_policy: DurabilityPolicy::SyncWriteThrough,
6551 bridge: None,
6552 default_timeout: None,
6553 }));
6554 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
6555 let callback_runtime = identity_runtime.clone();
6556 let callback_calls = calls.clone();
6557 let wait = wait_identity_startup_ready(
6558 &identity_runtime,
6559 Duration::from_secs(1),
6560 move |_member_ids, _remaining| {
6561 let call = callback_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6562 let callback_runtime = callback_runtime.clone();
6563 let stale_result = stale_result.clone();
6564 async move {
6565 if call == 0 {
6566 callback_runtime.test_supersede_identity_bootstrap_ready();
6570 stale_result
6571 } else {
6572 IdentityMemberReadiness::Ready
6573 }
6574 }
6575 },
6576 )
6577 .await?;
6578
6579 assert!(wait.startup_ready);
6580 assert!(!wait.timed_out);
6581 assert!(wait.status.ready);
6582 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 2);
6583 }
6584 Ok(())
6585 }
6586
6587 #[tokio::test]
6588 async fn startup_ready_wait_reports_timeout_and_terminal_broken_status()
6589 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6590 let timeout_runtime = IdentityRuntime::new(IdentityRuntimeConfig {
6591 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6592 lease_provider: Arc::new(LocalLeaseProvider::new()),
6593 runtime_instance_id: "rpc-startup-ready-timeout-test".to_string(),
6594 has_runtime_store: true,
6595 durability_policy: DurabilityPolicy::SyncWriteThrough,
6596 bridge: None,
6597 default_timeout: None,
6598 });
6599 let timed_out = wait_identity_startup_ready(
6600 &timeout_runtime,
6601 Duration::from_secs(1),
6602 |_member_ids, _remaining| async { IdentityMemberReadiness::TimedOut },
6603 )
6604 .await?;
6605 assert!(timed_out.timed_out);
6606 assert!(!timed_out.startup_ready);
6607
6608 let broken_runtime = IdentityRuntime::new(IdentityRuntimeConfig {
6609 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6610 lease_provider: Arc::new(LocalLeaseProvider::new()),
6611 runtime_instance_id: "rpc-startup-ready-broken-test".to_string(),
6612 has_runtime_store: true,
6613 durability_policy: DurabilityPolicy::SyncWriteThrough,
6614 bridge: None,
6615 default_timeout: None,
6616 });
6617 broken_runtime.test_fail_identity_bootstrap("injected terminal failure");
6618 let readiness_calls = std::sync::atomic::AtomicUsize::new(0);
6619 let broken = wait_identity_startup_ready(
6620 &broken_runtime,
6621 Duration::from_secs(1),
6622 |_member_ids, _remaining| {
6623 readiness_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6624 async { IdentityMemberReadiness::Ready }
6625 },
6626 )
6627 .await?;
6628 assert!(!broken.timed_out);
6629 assert!(!broken.startup_ready);
6630 assert!(!broken.status.ready);
6631 assert_eq!(broken.status.counts.broken, 1);
6632 assert!(
6633 broken
6634 .status
6635 .error
6636 .as_deref()
6637 .is_some_and(|error| error.contains("injected terminal failure"))
6638 );
6639 assert_eq!(
6640 readiness_calls.load(std::sync::atomic::Ordering::SeqCst),
6641 0,
6642 "terminal bootstrap failure must not enter member readiness"
6643 );
6644 Ok(())
6645 }
6646
6647 #[tokio::test]
6648 async fn identity_bootstrap_status_and_wait_rpc_are_typed_and_advertised()
6649 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6650 let temp_dir = tempfile::tempdir()?;
6651 let runtime = Box::pin(
6652 UnifiedRuntime::builder()
6653 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6654 .module_config(MobKitConfig {
6655 modules: Vec::new(),
6656 discovery: DiscoverySpec {
6657 namespace: "rpc-identity-bootstrap-status-test".to_string(),
6658 modules: Vec::new(),
6659 },
6660 pre_spawn: Vec::new(),
6661 })
6662 .timeout(Duration::from_secs(1))
6663 .build(),
6664 )
6665 .await?;
6666 let identity_ctx = IdentityFirstContext {
6667 runtime: Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6668 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6669 lease_provider: Arc::new(LocalLeaseProvider::new()),
6670 runtime_instance_id: "rpc-identity-bootstrap-status-test".to_string(),
6671 has_runtime_store: true,
6672 durability_policy: DurabilityPolicy::SyncWriteThrough,
6673 bridge: None,
6674 default_timeout: None,
6675 })),
6676 roster_provider: Arc::new(ContextRequiredEmptyRosterProvider::default()),
6677 topology_provider: None,
6678 customizer: None,
6679 agent_memory_provider: None,
6680 mob_definition: Some(runtime.mob_handle().definition().clone()),
6681 };
6682
6683 let runtime_ref = &runtime;
6684 let identity_ctx_ref = &identity_ctx;
6685 let call = move |method: &'static str, params: Value| {
6686 let request = serde_json::json!({
6687 "jsonrpc": "2.0",
6688 "id": method,
6689 "method": method,
6690 "params": params,
6691 })
6692 .to_string();
6693 async move {
6694 handle_unified_rpc_json(
6695 runtime_ref,
6696 &request,
6697 Duration::from_secs(1),
6698 None,
6699 Some(identity_ctx_ref),
6700 )
6701 .await
6702 }
6703 };
6704
6705 let status: Value = serde_json::from_str(
6706 &call("mobkit/status_identity_bootstrap", serde_json::json!({})).await,
6707 )?;
6708 assert_eq!(status["result"]["mode"]["mode"], json!("eager_materialize"));
6709 assert_eq!(status["result"]["complete"], json!(true));
6710 assert_eq!(status["result"]["ready"], json!(true));
6711
6712 let waited: Value = serde_json::from_str(
6713 &call(
6714 "mobkit/wait_identity_bootstrap",
6715 serde_json::json!({"target": "materialized", "timeout_ms": 0}),
6716 )
6717 .await,
6718 )?;
6719 assert_eq!(waited["result"]["timed_out"], json!(false));
6720 assert_eq!(waited["result"]["target"], json!("materialized"));
6721
6722 let default_waited: Value = serde_json::from_str(
6723 &call("mobkit/wait_identity_bootstrap", serde_json::json!({})).await,
6724 )?;
6725 assert_eq!(default_waited["result"]["target"], json!("materialized"));
6726
6727 let startup_ready: Value = serde_json::from_str(
6728 &call(
6729 "mobkit/wait_identity_bootstrap",
6730 serde_json::json!({"target": "startup_ready", "timeout_ms": 0}),
6731 )
6732 .await,
6733 )?;
6734 assert_eq!(startup_ready["result"]["timed_out"], json!(false));
6735 assert_eq!(startup_ready["result"]["startup_ready"], json!(true));
6736
6737 for (params, expected_message) in [
6738 (serde_json::json!(null), "params must be an object"),
6739 (serde_json::json!([]), "params must be an object"),
6740 (
6741 serde_json::json!({"target": null}),
6742 "target must be a string",
6743 ),
6744 (serde_json::json!({"target": 42}), "target must be a string"),
6745 (
6746 serde_json::json!({"target": "not_ready"}),
6747 "target must be 'materialized' or 'startup_ready'",
6748 ),
6749 (
6750 serde_json::json!({"timeout_ms": null}),
6751 "timeout_ms must be a non-negative integer",
6752 ),
6753 (
6754 serde_json::json!({"timeout_ms": -1}),
6755 "timeout_ms must be a non-negative integer",
6756 ),
6757 (
6758 serde_json::json!({"timeout_ms": 1.5}),
6759 "timeout_ms must be a non-negative integer",
6760 ),
6761 (
6762 serde_json::json!({"timeout_ms": true}),
6763 "timeout_ms must be a non-negative integer",
6764 ),
6765 (
6766 serde_json::json!({"unexpected": true}),
6767 "unsupported parameter: unexpected",
6768 ),
6769 ] {
6770 let response: Value =
6771 serde_json::from_str(&call("mobkit/wait_identity_bootstrap", params).await)?;
6772 assert_eq!(response["error"]["code"], json!(-32602));
6773 assert_eq!(response["error"]["message"], json!(expected_message));
6774 }
6775
6776 let capabilities: Value =
6777 serde_json::from_str(&call("mobkit/capabilities", serde_json::json!({})).await)?;
6778 let methods = capabilities["result"]["methods"]
6779 .as_array()
6780 .expect("methods array");
6781 assert!(methods.contains(&json!("mobkit/status_identity_bootstrap")));
6782 assert!(methods.contains(&json!("mobkit/wait_identity_bootstrap")));
6783 Ok(())
6784 }
6785
6786 #[tokio::test]
6787 async fn reset_identity_preserves_mob_definition_for_roster_reprofile()
6788 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6789 let temp_dir = tempfile::tempdir()?;
6790 let runtime = Box::pin(
6791 UnifiedRuntime::builder()
6792 .mob_spec(rpc_reprofile_mob_spec(&temp_dir)?)
6793 .module_config(MobKitConfig {
6794 modules: Vec::new(),
6795 discovery: DiscoverySpec {
6796 namespace: "rpc-reset-roster-context-test".to_string(),
6797 modules: Vec::new(),
6798 },
6799 pre_spawn: Vec::new(),
6800 })
6801 .timeout(Duration::from_secs(1))
6802 .build(),
6803 )
6804 .await?;
6805
6806 let continuity_store = Arc::new(LocalContinuityStore::in_memory()?);
6807 let lease_provider = Arc::new(LocalLeaseProvider::new());
6808 let bridge = Arc::new(RpcResetTestBridge::default());
6809 let identity_rt = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6810 continuity_store: continuity_store.clone(),
6811 lease_provider: lease_provider.clone(),
6812 runtime_instance_id: "rpc-reset-roster-context-test".to_string(),
6813 has_runtime_store: true,
6814 durability_policy: DurabilityPolicy::SyncWriteThrough,
6815 bridge: Some(bridge.clone()),
6816 default_timeout: None,
6817 }));
6818
6819 let identity = AgentIdentity::parse("domain:security")?;
6820 let initial_grants = lease_provider
6821 .acquire_leases(
6822 std::slice::from_ref(&identity),
6823 "rpc-reset-roster-context-test",
6824 )
6825 .await?;
6826 let initial_grant = match initial_grants.get(&identity) {
6827 Some(LeaseAcquireResult::Acquired(grant)) => grant.clone(),
6828 other => return Err(format!("expected acquired lease, got {other:?}").into()),
6829 };
6830 let initial_record = ContinuityRecord {
6831 identity: identity.clone(),
6832 agent_runtime_id: AgentRuntimeId::parse("rt:domain:security:0")?,
6833 session_id: meerkat_core::types::SessionId::new(),
6834 generation: ContinuityGeneration::new(0),
6835 checkpoint_version: CheckpointVersion::new(0),
6836 };
6837 continuity_store
6838 .upsert_continuity_record(&initial_record, initial_grant.fencing_token)
6839 .await?;
6840 identity_rt
6841 .register(
6842 rpc_durable_spec(identity.as_str(), "domain"),
6843 IdentityLifecycleState::Active,
6844 Some(initial_record),
6845 Some(initial_grant),
6846 )
6847 .await;
6848
6849 let roster_provider = Arc::new(ContextRequiredStaticRosterProvider::new(vec![
6850 rpc_durable_spec(identity.as_str(), "security"),
6851 ]));
6852 let identity_ctx = IdentityFirstContext {
6853 runtime: identity_rt.clone(),
6854 roster_provider: roster_provider.clone(),
6855 topology_provider: None,
6856 customizer: None,
6857 agent_memory_provider: None,
6858 mob_definition: Some(runtime.mob_handle().definition().clone()),
6859 };
6860
6861 let response: Value = serde_json::from_str(
6862 &handle_unified_rpc_json(
6863 &runtime,
6864 &json!({
6865 "jsonrpc": "2.0",
6866 "id": 1,
6867 "method": "mobkit/reset",
6868 "params": { "identity": identity.as_str() },
6869 })
6870 .to_string(),
6871 Duration::from_secs(1),
6872 None,
6873 Some(&identity_ctx),
6874 )
6875 .await,
6876 )?;
6877
6878 assert!(response["error"].is_null(), "{response:#?}");
6879 assert_eq!(
6880 roster_provider.missing_definition_calls(),
6881 0,
6882 "mobkit/reset must preserve mob_definition when installing the reset roster provider"
6883 );
6884 assert_eq!(
6885 bridge
6886 .create_calls
6887 .load(std::sync::atomic::Ordering::SeqCst),
6888 1,
6889 "reset should rebuild through the bridge"
6890 );
6891 let created = bridge
6892 .last_create_spec()
6893 .await
6894 .expect("reset should record created spec");
6895 assert_eq!(created.profile.as_str(), "security");
6896 assert_eq!(
6897 identity_rt
6898 .status(&identity)
6899 .await?
6900 .profile
6901 .expect("identity should keep profile")
6902 .as_str(),
6903 "security"
6904 );
6905 Ok(())
6906 }
6907
6908 #[tokio::test]
6909 async fn unified_rpc_agent_memory_remember_writes_identity_scoped_record()
6910 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6911 let temp_dir = tempfile::tempdir()?;
6912 let runtime = Box::pin(
6913 UnifiedRuntime::builder()
6914 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6915 .module_config(MobKitConfig {
6916 modules: Vec::new(),
6917 discovery: DiscoverySpec {
6918 namespace: "rpc-agent-memory-remember-test".to_string(),
6919 modules: Vec::new(),
6920 },
6921 pre_spawn: Vec::new(),
6922 })
6923 .timeout(Duration::from_secs(1))
6924 .build(),
6925 )
6926 .await?;
6927 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
6928 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6929 lease_provider: Arc::new(LocalLeaseProvider::new()),
6930 runtime_instance_id: "rpc-agent-memory-remember-test".to_string(),
6931 has_runtime_store: true,
6932 durability_policy: DurabilityPolicy::SyncWriteThrough,
6933 bridge: None,
6934 default_timeout: None,
6935 });
6936 let store = Arc::new(MarkdownAgentMemoryStore::open(
6937 temp_dir.path().join("agent-memory"),
6938 )?);
6939 let provider: Arc<dyn crate::identity_first::AgentMemoryProvider> = store.clone();
6940 identity_rt
6941 .set_agent_memory(Some(
6942 crate::identity_first::AgentMemoryRuntimeInjector::new(
6943 provider.clone(),
6944 crate::identity_first::AgentMemoryConfig::default(),
6945 ),
6946 ))
6947 .await;
6948 let memory_identity = AgentIdentity::parse("identity:luka")?;
6949 identity_rt
6950 .register(
6951 DurableAgentSpec {
6952 identity: memory_identity.clone(),
6953 profile: meerkat_mob::ProfileName::from("worker"),
6954 addressability: AgentAddressability::Addressable,
6955 display_name: None,
6956 labels: Default::default(),
6957 context: None,
6958 additional_instructions: Vec::new(),
6959 initial_message: None,
6960 runtime_mode_override: None,
6961 backend: None,
6962 binding: None,
6963 },
6964 IdentityLifecycleState::Active,
6965 None,
6966 None,
6967 )
6968 .await;
6969 let identity_ctx = IdentityFirstContext {
6970 runtime: Arc::new(identity_rt),
6971 roster_provider: Arc::new(EmptyRosterProvider),
6972 topology_provider: None,
6973 customizer: None,
6974 agent_memory_provider: Some(provider),
6975 mob_definition: None,
6976 };
6977
6978 let capabilities: Value = serde_json::from_str(
6979 &handle_unified_rpc_json(
6980 &runtime,
6981 &json!({
6982 "jsonrpc": "2.0",
6983 "id": 1,
6984 "method": "mobkit/capabilities",
6985 })
6986 .to_string(),
6987 Duration::from_secs(1),
6988 None,
6989 Some(&identity_ctx),
6990 )
6991 .await,
6992 )?;
6993 assert!(
6994 capabilities["result"]["methods"]
6995 .as_array()
6996 .is_some_and(|methods| methods.contains(&json!("mobkit/agent_memory/remember"))),
6997 "{capabilities:#?}"
6998 );
6999 assert!(
7000 capabilities["result"]["methods"]
7001 .as_array()
7002 .is_some_and(|methods| methods.contains(&json!("mobkit/agent_memory/recall"))),
7003 "{capabilities:#?}"
7004 );
7005 assert!(
7006 capabilities["result"]["methods"]
7007 .as_array()
7008 .is_some_and(|methods| methods.contains(&json!("mobkit/agent_memory/forget"))),
7009 "{capabilities:#?}"
7010 );
7011 assert!(
7014 capabilities["result"]["methods"]
7015 .as_array()
7016 .is_some_and(|methods| !methods.contains(&json!("mobkit/agent_memory/update"))),
7017 "{capabilities:#?}"
7018 );
7019 assert!(
7020 capabilities["result"]["methods"]
7021 .as_array()
7022 .is_some_and(|methods| !methods.contains(&json!("mobkit/agent_memory/manifest"))),
7023 "{capabilities:#?}"
7024 );
7025
7026 let response: Value = serde_json::from_str(
7027 &handle_unified_rpc_json(
7028 &runtime,
7029 &json!({
7030 "jsonrpc": "2.0",
7031 "id": 2,
7032 "method": "mobkit/agent_memory/remember",
7033 "params": {
7034 "identity": "identity:luka",
7035 "realm": "family",
7036 "title": "School pickup",
7037 "body": "Pickup is before calendar planning.",
7038 "tags": ["family", "calendar"]
7039 },
7040 })
7041 .to_string(),
7042 Duration::from_secs(1),
7043 None,
7044 Some(&identity_ctx),
7045 )
7046 .await,
7047 )?;
7048
7049 assert!(response["error"].is_null(), "{response:#?}");
7050 let memory_id = response["result"]["memory_id"]
7051 .as_str()
7052 .ok_or("memory_id should be present")?
7053 .to_string();
7054 assert_eq!(response["result"]["title"], json!("School pickup"));
7055 assert_eq!(
7056 response["result"]["body"],
7057 json!("Pickup is before calendar planning.")
7058 );
7059 assert_eq!(response["result"]["tags"], json!(["calendar", "family"]));
7060
7061 let records = store.read_records("family", &memory_identity)?;
7062 assert_eq!(records.len(), 1);
7063 assert_eq!(records[0].title, "School pickup");
7064 assert_eq!(records[0].tags, vec!["calendar", "family"]);
7065
7066 let recall_response: Value = serde_json::from_str(
7067 &handle_unified_rpc_json(
7068 &runtime,
7069 &json!({
7070 "jsonrpc": "2.0",
7071 "id": 3,
7072 "method": "mobkit/agent_memory/recall",
7073 "params": {
7074 "identity": "identity:luka",
7075 "realm": "family",
7076 "selection": "contextual",
7077 "query_terms": ["pickup"],
7078 "max_entries": 4
7079 },
7080 })
7081 .to_string(),
7082 Duration::from_secs(1),
7083 None,
7084 Some(&identity_ctx),
7085 )
7086 .await,
7087 )?;
7088
7089 assert!(recall_response["error"].is_null(), "{recall_response:#?}");
7090 assert_eq!(
7091 recall_response["result"]["records"]
7092 .as_array()
7093 .map(Vec::len),
7094 Some(1)
7095 );
7096 assert_eq!(
7097 recall_response["result"]["records"][0]["body"],
7098 json!("Pickup is before calendar planning.")
7099 );
7100
7101 let forget_response: Value = serde_json::from_str(
7102 &handle_unified_rpc_json(
7103 &runtime,
7104 &json!({
7105 "jsonrpc": "2.0",
7106 "id": 4,
7107 "method": "mobkit/agent_memory/forget",
7108 "params": {
7109 "identity": "identity:luka",
7110 "realm": "family",
7111 "memory_id": memory_id.clone()
7112 },
7113 })
7114 .to_string(),
7115 Duration::from_secs(1),
7116 None,
7117 Some(&identity_ctx),
7118 )
7119 .await,
7120 )?;
7121 assert!(forget_response["error"].is_null(), "{forget_response:#?}");
7122 assert_eq!(forget_response["result"]["memory_id"], json!(memory_id));
7123 assert_eq!(forget_response["result"]["deleted"], json!(true));
7124 assert!(store.read_records("family", &memory_identity)?.is_empty());
7125
7126 let recall_after_forget_response: Value = serde_json::from_str(
7127 &handle_unified_rpc_json(
7128 &runtime,
7129 &json!({
7130 "jsonrpc": "2.0",
7131 "id": 5,
7132 "method": "mobkit/agent_memory/recall",
7133 "params": {
7134 "identity": "identity:luka",
7135 "realm": "family",
7136 "selection": "always"
7137 },
7138 })
7139 .to_string(),
7140 Duration::from_secs(1),
7141 None,
7142 Some(&identity_ctx),
7143 )
7144 .await,
7145 )?;
7146 assert!(
7147 recall_after_forget_response["error"].is_null(),
7148 "{recall_after_forget_response:#?}"
7149 );
7150 assert_eq!(
7151 recall_after_forget_response["result"]["records"]
7152 .as_array()
7153 .map(Vec::len),
7154 Some(0)
7155 );
7156
7157 let unknown_response: Value = serde_json::from_str(
7158 &handle_unified_rpc_json(
7159 &runtime,
7160 &json!({
7161 "jsonrpc": "2.0",
7162 "id": 6,
7163 "method": "mobkit/agent_memory/remember",
7164 "params": {
7165 "identity": "identity:unknown",
7166 "title": "Orphan",
7167 "body": "This should not be written."
7168 },
7169 })
7170 .to_string(),
7171 Duration::from_secs(1),
7172 None,
7173 Some(&identity_ctx),
7174 )
7175 .await,
7176 )?;
7177 assert_eq!(unknown_response["error"]["code"], json!(-32602));
7178 assert!(
7179 unknown_response["error"]["message"]
7180 .as_str()
7181 .is_some_and(|message| message.contains("unknown identity")),
7182 "{unknown_response:#?}"
7183 );
7184
7185 let unknown_forget_response: Value = serde_json::from_str(
7186 &handle_unified_rpc_json(
7187 &runtime,
7188 &json!({
7189 "jsonrpc": "2.0",
7190 "id": 7,
7191 "method": "mobkit/agent_memory/forget",
7192 "params": {
7193 "identity": "identity:unknown",
7194 "memory_id": "mem-missing"
7195 },
7196 })
7197 .to_string(),
7198 Duration::from_secs(1),
7199 None,
7200 Some(&identity_ctx),
7201 )
7202 .await,
7203 )?;
7204 assert_eq!(unknown_forget_response["error"]["code"], json!(-32602));
7205 assert!(
7206 unknown_forget_response["error"]["message"]
7207 .as_str()
7208 .is_some_and(|message| message.contains("unknown identity")),
7209 "{unknown_forget_response:#?}"
7210 );
7211
7212 Ok(())
7213 }
7214
7215 #[tokio::test]
7216 async fn unified_rpc_agent_memory_update_and_manifest_over_sqlite_store()
7217 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7218 let temp_dir = tempfile::tempdir()?;
7219 let runtime = Box::pin(
7220 UnifiedRuntime::builder()
7221 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
7222 .module_config(MobKitConfig {
7223 modules: Vec::new(),
7224 discovery: DiscoverySpec {
7225 namespace: "rpc-agent-memory-sqlite-test".to_string(),
7226 modules: Vec::new(),
7227 },
7228 pre_spawn: Vec::new(),
7229 })
7230 .timeout(Duration::from_secs(1))
7231 .build(),
7232 )
7233 .await?;
7234 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
7235 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
7236 lease_provider: Arc::new(LocalLeaseProvider::new()),
7237 runtime_instance_id: "rpc-agent-memory-sqlite-test".to_string(),
7238 has_runtime_store: true,
7239 durability_policy: DurabilityPolicy::SyncWriteThrough,
7240 bridge: None,
7241 default_timeout: None,
7242 });
7243 let store = Arc::new(crate::memory::SqliteAgentMemoryStore::open(
7244 temp_dir.path().join("agent-memory"),
7245 )?);
7246 let provider: Arc<dyn crate::identity_first::AgentMemoryProvider> = store.clone();
7247 identity_rt
7248 .set_agent_memory(Some(
7249 crate::identity_first::AgentMemoryRuntimeInjector::new(
7250 provider.clone(),
7251 crate::identity_first::AgentMemoryConfig::default(),
7252 ),
7253 ))
7254 .await;
7255 let memory_identity = AgentIdentity::parse("identity:luka")?;
7256 identity_rt
7257 .register(
7258 DurableAgentSpec {
7259 identity: memory_identity.clone(),
7260 profile: meerkat_mob::ProfileName::from("worker"),
7261 addressability: AgentAddressability::Addressable,
7262 display_name: None,
7263 labels: Default::default(),
7264 context: None,
7265 additional_instructions: Vec::new(),
7266 initial_message: None,
7267 runtime_mode_override: None,
7268 backend: None,
7269 binding: None,
7270 },
7271 IdentityLifecycleState::Active,
7272 None,
7273 None,
7274 )
7275 .await;
7276 let identity_ctx = IdentityFirstContext {
7277 runtime: Arc::new(identity_rt),
7278 roster_provider: Arc::new(EmptyRosterProvider),
7279 topology_provider: None,
7280 customizer: None,
7281 agent_memory_provider: Some(provider),
7282 mob_definition: None,
7283 };
7284
7285 let capabilities: Value = serde_json::from_str(
7288 &handle_unified_rpc_json(
7289 &runtime,
7290 &json!({
7291 "jsonrpc": "2.0",
7292 "id": 1,
7293 "method": "mobkit/capabilities",
7294 })
7295 .to_string(),
7296 Duration::from_secs(1),
7297 None,
7298 Some(&identity_ctx),
7299 )
7300 .await,
7301 )?;
7302 for method in [
7303 "mobkit/agent_memory/recall",
7304 "mobkit/agent_memory/remember",
7305 "mobkit/agent_memory/forget",
7306 "mobkit/agent_memory/update",
7307 "mobkit/agent_memory/manifest",
7308 ] {
7309 assert!(
7310 capabilities["result"]["methods"]
7311 .as_array()
7312 .is_some_and(|methods| methods.contains(&json!(method))),
7313 "missing {method}: {capabilities:#?}"
7314 );
7315 }
7316
7317 let remember_response: Value = serde_json::from_str(
7318 &handle_unified_rpc_json(
7319 &runtime,
7320 &json!({
7321 "jsonrpc": "2.0",
7322 "id": 2,
7323 "method": "mobkit/agent_memory/remember",
7324 "params": {
7325 "identity": "identity:luka",
7326 "realm": "family",
7327 "title": "School pickup",
7328 "body": "Pickup is before calendar planning.",
7329 },
7330 })
7331 .to_string(),
7332 Duration::from_secs(1),
7333 None,
7334 Some(&identity_ctx),
7335 )
7336 .await,
7337 )?;
7338 assert!(
7339 remember_response["error"].is_null(),
7340 "{remember_response:#?}"
7341 );
7342 let memory_id = remember_response["result"]["memory_id"]
7343 .as_str()
7344 .ok_or("memory_id should be present")?
7345 .to_string();
7346
7347 let update_response: Value = serde_json::from_str(
7348 &handle_unified_rpc_json(
7349 &runtime,
7350 &json!({
7351 "jsonrpc": "2.0",
7352 "id": 3,
7353 "method": "mobkit/agent_memory/update",
7354 "params": {
7355 "identity": "identity:luka",
7356 "realm": "family",
7357 "memory_id": memory_id.clone(),
7358 "title": "School pickup",
7359 "body": "Pickup moved to after calendar planning.",
7360 "tags": ["family"]
7361 },
7362 })
7363 .to_string(),
7364 Duration::from_secs(1),
7365 None,
7366 Some(&identity_ctx),
7367 )
7368 .await,
7369 )?;
7370 assert!(update_response["error"].is_null(), "{update_response:#?}");
7371 let new_id = update_response["result"]["memory_id"]
7372 .as_str()
7373 .ok_or("updated memory_id should be present")?
7374 .to_string();
7375 assert_ne!(new_id, memory_id);
7376 assert_eq!(update_response["result"]["supersedes"], json!(memory_id));
7377
7378 let recall_response: Value = serde_json::from_str(
7380 &handle_unified_rpc_json(
7381 &runtime,
7382 &json!({
7383 "jsonrpc": "2.0",
7384 "id": 4,
7385 "method": "mobkit/agent_memory/recall",
7386 "params": {
7387 "identity": "identity:luka",
7388 "realm": "family",
7389 "selection": "always"
7390 },
7391 })
7392 .to_string(),
7393 Duration::from_secs(1),
7394 None,
7395 Some(&identity_ctx),
7396 )
7397 .await,
7398 )?;
7399 assert!(recall_response["error"].is_null(), "{recall_response:#?}");
7400 assert_eq!(
7401 recall_response["result"]["records"]
7402 .as_array()
7403 .map(Vec::len),
7404 Some(1)
7405 );
7406 assert_eq!(
7407 recall_response["result"]["records"][0]["memory_id"],
7408 json!(new_id)
7409 );
7410
7411 let manifest_response: Value = serde_json::from_str(
7412 &handle_unified_rpc_json(
7413 &runtime,
7414 &json!({
7415 "jsonrpc": "2.0",
7416 "id": 5,
7417 "method": "mobkit/agent_memory/manifest",
7418 "params": {
7419 "identity": "identity:luka",
7420 "realm": "family",
7421 "tier": "working_set",
7422 "k": 4
7423 },
7424 })
7425 .to_string(),
7426 Duration::from_secs(1),
7427 None,
7428 Some(&identity_ctx),
7429 )
7430 .await,
7431 )?;
7432 assert!(
7433 manifest_response["error"].is_null(),
7434 "{manifest_response:#?}"
7435 );
7436 let records = manifest_response["result"]["records"]
7437 .as_array()
7438 .ok_or("manifest records array")?;
7439 assert_eq!(records.len(), 1, "{manifest_response:#?}");
7440 assert_eq!(records[0]["id"], json!(new_id));
7441 assert_eq!(records[0]["kind"], json!("fact"));
7442 assert_eq!(records[0]["age_days"], json!(0));
7443 assert!(
7444 records[0].get("body").is_none(),
7445 "manifest is an index, never a dump: {manifest_response:#?}"
7446 );
7447
7448 let bad_tier_response: Value = serde_json::from_str(
7449 &handle_unified_rpc_json(
7450 &runtime,
7451 &json!({
7452 "jsonrpc": "2.0",
7453 "id": 6,
7454 "method": "mobkit/agent_memory/manifest",
7455 "params": {
7456 "identity": "identity:luka",
7457 "tier": "everything"
7458 },
7459 })
7460 .to_string(),
7461 Duration::from_secs(1),
7462 None,
7463 Some(&identity_ctx),
7464 )
7465 .await,
7466 )?;
7467 assert_eq!(bad_tier_response["error"]["code"], json!(-32602));
7468
7469 Ok(())
7470 }
7471
7472 #[tokio::test]
7473 async fn unified_rpc_agent_memory_capabilities_do_not_advertise_read_only_writes()
7474 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7475 let temp_dir = tempfile::tempdir()?;
7476 let runtime = Box::pin(
7477 UnifiedRuntime::builder()
7478 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
7479 .module_config(MobKitConfig {
7480 modules: Vec::new(),
7481 discovery: DiscoverySpec {
7482 namespace: "rpc-agent-memory-read-only-test".to_string(),
7483 modules: Vec::new(),
7484 },
7485 pre_spawn: Vec::new(),
7486 })
7487 .timeout(Duration::from_secs(1))
7488 .build(),
7489 )
7490 .await?;
7491 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
7492 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
7493 lease_provider: Arc::new(LocalLeaseProvider::new()),
7494 runtime_instance_id: "rpc-agent-memory-read-only-test".to_string(),
7495 has_runtime_store: true,
7496 durability_policy: DurabilityPolicy::SyncWriteThrough,
7497 bridge: None,
7498 default_timeout: None,
7499 });
7500 let provider: Arc<dyn crate::identity_first::AgentMemoryProvider> =
7501 Arc::new(ReadOnlyAgentMemoryProvider);
7502 let identity_ctx = IdentityFirstContext {
7503 runtime: Arc::new(identity_rt),
7504 roster_provider: Arc::new(EmptyRosterProvider),
7505 topology_provider: None,
7506 customizer: None,
7507 agent_memory_provider: Some(provider),
7508 mob_definition: None,
7509 };
7510
7511 let capabilities: Value = serde_json::from_str(
7512 &handle_unified_rpc_json(
7513 &runtime,
7514 &json!({
7515 "jsonrpc": "2.0",
7516 "id": 1,
7517 "method": "mobkit/capabilities",
7518 })
7519 .to_string(),
7520 Duration::from_secs(1),
7521 None,
7522 Some(&identity_ctx),
7523 )
7524 .await,
7525 )?;
7526 let methods = capabilities["result"]["methods"]
7527 .as_array()
7528 .ok_or("methods should be an array")?;
7529
7530 assert!(methods.contains(&json!("mobkit/agent_memory/recall")));
7531 assert!(!methods.contains(&json!("mobkit/agent_memory/remember")));
7532 assert!(!methods.contains(&json!("mobkit/agent_memory/forget")));
7533 Ok(())
7534 }
7535
7536 #[test]
7537 fn identity_lease_lost_maps_off_capability_unavailable_code() {
7538 let identity = AgentIdentity::parse("review:singleton").expect("valid identity");
7544 let err = crate::identity_first::IdentityRuntimeError::LeaseLost(identity);
7545 let response = identity_error_response(json!("req-1"), &err);
7546 let error = response.error.expect("lease-lost must surface an error");
7547 assert_ne!(
7548 error.code, -32004,
7549 "LeaseLost must not use the capability code"
7550 );
7551 assert_eq!(
7552 error.code, -32005,
7553 "LeaseLost has its own identity-plane code"
7554 );
7555 assert!(error.message.contains("lease lost"));
7556 }
7557
7558 #[test]
7559 fn mobpack_authoring_rpc_helper_preserves_runtime_catalog_binding()
7560 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7561 let runtime = crate::mobpack::MobpackRuntimeCatalogState {
7562 loaded_modules: vec!["editor-host".to_string()],
7563 runtime_methods: vec![
7564 "mobkit/mobpacks/catalogs".to_string(),
7565 "mobkit/mobpacks/apply_operation".to_string(),
7566 "mobkit/mobpacks/deploy".to_string(),
7567 ],
7568 has_contact_directory: true,
7569 has_peer_mob_handles: false,
7570 has_inproc_contacts: false,
7571 runtime_flow_rows: vec![json!({
7572 "id": "runtime_rpc_main",
7573 "source": "mobkit/runtime/flow_projection",
7574 "document": {
7575 "mob_id": "runtime_rpc",
7576 "flow": { "name": "main", "steps": [] },
7577 "members": []
7578 },
7579 "validation": { "ok": true }
7580 })],
7581 runtime_agent_definition_sources: vec![json!({
7582 "id": "runtime_profiles_rpc",
7583 "name": "Runtime RPC profiles",
7584 "source": "mobkit/runtime/agent-definitions",
7585 "document": {
7586 "mob_id": "runtime_rpc",
7587 "members": [{
7588 "id": "m_runtime_reviewer",
7589 "name": "Runtime reviewer",
7590 "role": "runtime_reviewer",
7591 "profileBinding": "inline",
7592 "model": "gpt-5.5",
7593 "runtimeMode": "turn_driven",
7594 "tools": ["builtins"],
7595 "skills": ["mob.runtime.review"],
7596 "schema": ""
7597 }],
7598 "schemas": []
7599 }
7600 })],
7601 runtime_skill_realms: vec![json!({
7602 "id": "runtime_rpc",
7603 "label": "Runtime RPC",
7604 "source": "mobkit/runtime/skills",
7605 "skills": [{
7606 "id": "mob.runtime.review",
7607 "label": "Runtime review",
7608 "source": "inline",
7609 "content": "Review runtime work."
7610 }]
7611 })],
7612 };
7613
7614 let catalogs = super::handle_mobpack_authoring_rpc_with_runtime(
7615 "mobkit/mobpacks/catalogs",
7616 &json!({}),
7617 json!(1),
7618 Some(&runtime),
7619 )
7620 .expect("catalogs method");
7621 let catalogs: Value = serde_json::to_value(catalogs)?;
7622 assert_eq!(catalogs["result"]["runtime_backed"], json!(true));
7623 assert_eq!(
7624 catalogs["result"]["authoring_provider"]["runtime_binding"],
7625 json!("bound")
7626 );
7627 assert_eq!(
7628 catalogs["result"]["runtime_flows"][0]["id"],
7629 json!("runtime_rpc_main")
7630 );
7631 let listed = super::handle_mobpack_authoring_rpc_with_runtime(
7632 "mobkit/mobpacks/list",
7633 &json!({}),
7634 json!(2),
7635 Some(&runtime),
7636 )
7637 .expect("list method");
7638 let listed: Value = serde_json::to_value(listed)?;
7639 assert_eq!(listed["result"]["runtime_backed"], json!(true));
7640 assert_eq!(listed["result"]["rows"][0]["id"], json!("runtime_rpc_main"));
7641
7642 let fetched = super::handle_mobpack_authoring_rpc_with_runtime(
7643 "mobkit/mobpacks/get",
7644 &json!({ "id": "runtime_rpc_main" }),
7645 json!(3),
7646 Some(&runtime),
7647 )
7648 .expect("get method");
7649 let fetched: Value = serde_json::to_value(fetched)?;
7650 assert_eq!(fetched["result"]["runtime_backed"], json!(true));
7651 assert_eq!(fetched["result"]["row"]["id"], json!("runtime_rpc_main"));
7652
7653 let definitions = super::handle_mobpack_authoring_rpc_with_runtime(
7654 "mobkit/agent_definitions/list",
7655 &json!({}),
7656 json!(4),
7657 Some(&runtime),
7658 )
7659 .expect("agent definitions method");
7660 let definitions: Value = serde_json::to_value(definitions)?;
7661 let runtime_definition = definitions["result"]["agent_definitions"]
7662 .as_array()
7663 .and_then(|rows| {
7664 rows.iter()
7665 .find(|row| row["sourceOrigin"] == "mobkit/runtime/agent-definitions")
7666 })
7667 .expect("runtime profile definition");
7668 assert_eq!(runtime_definition["role"], json!("runtime_reviewer"));
7669 assert_eq!(
7670 runtime_definition["toolDefinitions"][0]["id"],
7671 json!("builtins")
7672 );
7673 assert_eq!(
7674 runtime_definition["skillDefinitions"][0]["id"],
7675 json!("mob.runtime.review")
7676 );
7677 assert_eq!(
7678 definitions["result"]["catalog_snapshot"]["runtime_backed"],
7679 json!(true)
7680 );
7681
7682 let tools = super::handle_mobpack_authoring_rpc_with_runtime(
7683 "mobkit/tools/catalog",
7684 &json!({}),
7685 json!(5),
7686 Some(&runtime),
7687 )
7688 .expect("tools catalog method");
7689 let tools: Value = serde_json::to_value(tools)?;
7690 let mob_tool = tools["result"]["tool_catalog"]
7691 .as_array()
7692 .expect("tools")
7693 .iter()
7694 .find(|tool| tool["id"] == "mob")
7695 .expect("mob tool");
7696 assert_eq!(mob_tool["runtime_availability"]["available"], json!(false));
7697
7698 let agents = super::handle_mobpack_authoring_rpc_with_runtime(
7699 "mobkit/agent_definitions/list",
7700 &json!({}),
7701 json!(3),
7702 Some(&runtime),
7703 )
7704 .expect("agent definitions method");
7705 let agents: Value = serde_json::to_value(agents)?;
7706 assert_eq!(agents["result"]["runtime_backed"], json!(true));
7707 let planner = agents["result"]["agent_definitions"]
7708 .as_array()
7709 .expect("agent definitions")
7710 .iter()
7711 .find(|definition| definition["role"] == "planner")
7712 .expect("planner definition");
7713 let planner_mob_tool = planner["toolDefinitions"]
7714 .as_array()
7715 .expect("planner tools")
7716 .iter()
7717 .find(|tool| tool["id"] == "mob")
7718 .expect("planner mob tool");
7719 assert_eq!(
7720 planner_mob_tool["runtimeAvailability"]["state"],
7721 json!("unavailable")
7722 );
7723
7724 Ok(())
7725 }
7726
7727 #[test]
7728 fn module_rpc_dispatches_mobpack_authoring_methods()
7729 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7730 let config = MobKitConfig {
7731 modules: Vec::new(),
7732 discovery: DiscoverySpec {
7733 namespace: "module-rpc-authoring-dispatch-test".to_string(),
7734 modules: Vec::new(),
7735 },
7736 pre_spawn: Vec::new(),
7737 };
7738 let mut runtime = crate::start_mobkit_runtime(config, Vec::new(), Duration::from_secs(1))?;
7739
7740 let capabilities: Value = serde_json::from_str(&super::handle_mobkit_rpc_json(
7741 &mut runtime,
7742 &json!({
7743 "jsonrpc": "2.0",
7744 "id": 1,
7745 "method": "mobkit/capabilities",
7746 })
7747 .to_string(),
7748 Duration::from_secs(1),
7749 ))?;
7750 let methods = capabilities["result"]["methods"]
7751 .as_array()
7752 .expect("methods array")
7753 .iter()
7754 .filter_map(Value::as_str)
7755 .collect::<Vec<_>>();
7756 for method in super::MOBPACK_AUTHORING_METHODS {
7757 assert!(
7758 methods.contains(method),
7759 "missing authoring method {method}"
7760 );
7761 }
7762 assert_eq!(
7763 capabilities["result"]["authoring_capabilities"]["runtime_mutation"],
7764 json!(false)
7765 );
7766 assert_eq!(
7767 capabilities["result"]["authoring_capabilities"]["host_mutation_methods"]["mobkit/mobpacks/deploy"],
7768 json!("when execute=true, writes a mobpack archive and runs rkat mob run on the host")
7769 );
7770
7771 let schema: Value = serde_json::from_str(&super::handle_mobkit_rpc_json(
7772 &mut runtime,
7773 &json!({
7774 "jsonrpc": "2.0",
7775 "id": 2,
7776 "method": "mobkit/mobpacks/schema",
7777 })
7778 .to_string(),
7779 Duration::from_secs(1),
7780 ))?;
7781 assert!(schema["error"].is_null(), "{schema:#?}");
7782 assert_eq!(
7783 schema["result"]["commands"]["deploy_rpc"],
7784 json!("mobkit/mobpacks/deploy")
7785 );
7786 assert!(schema["result"]["agent_definitions"].is_null());
7787
7788 let _ = runtime.shutdown();
7789 Ok(())
7790 }
7791
7792 #[test]
7793 fn generated_runtime_ids_match_their_durable_identity_prefix() {
7794 assert!(!rpc_member_id_matches_durable_identity(
7795 "rt:review:singleton:0",
7796 "review:singleton",
7797 ));
7798 assert!(!rpc_member_id_matches_durable_identity(
7799 "review:singleton:gen1",
7800 "review:singleton",
7801 ));
7802 assert!(!rpc_member_id_matches_durable_identity(
7803 "review:singleton:1",
7804 "review:singleton",
7805 ));
7806 assert!(!rpc_member_id_matches_durable_identity(
7807 "rt:reviewer:singleton:0",
7808 "review:singleton",
7809 ));
7810 assert!(!rpc_member_id_matches_durable_identity(
7811 "rt:review:singleton:qa:0",
7812 "review:singleton",
7813 ));
7814 assert!(!rpc_member_id_matches_durable_identity(
7815 "review:singleton:qa",
7816 "review:singleton",
7817 ));
7818 assert_eq!(
7819 rpc_runtime_alias_generation("rt:review:singleton:7", "review:singleton"),
7820 Some(7)
7821 );
7822 assert_eq!(
7823 rpc_runtime_alias_generation("rt:review:singleton:8", "review:other"),
7824 None
7825 );
7826 }
7827
7828 #[test]
7829 fn rpc_live_identity_visibility_matches_delegate_projection_labels() {
7830 assert!(rpc_live_identity_alias_visible("worker", &BTreeMap::new()));
7831
7832 let mut labels = BTreeMap::new();
7833 labels.insert("role".to_string(), "delegate".to_string());
7834 labels.insert("source_mob_id".to_string(), "mob-a".to_string());
7835 labels.insert("agent_identity".to_string(), "review:singleton".to_string());
7836 assert!(!rpc_live_identity_alias_visible("worker", &labels));
7837 assert!(!rpc_live_identity_alias_visible("delegate", &labels));
7838 }
7839
7840 #[test]
7841 fn ambiguous_live_alias_errors_include_structured_data() -> Result<(), serde_json::Error> {
7842 let response: Value = serde_json::from_str(&error_response(
7843 json!(1),
7844 -32602,
7845 "ambiguous live identity alias review:singleton: candidates [rt:review:singleton:0, rt:review:singleton:1]",
7846 ))?;
7847
7848 assert_eq!(
7849 response["error"]["data"]["kind"],
7850 json!("ambiguous_live_identity_alias")
7851 );
7852 assert_eq!(
7853 response["error"]["data"]["identity"],
7854 json!("review:singleton")
7855 );
7856 assert_eq!(
7857 response["error"]["data"]["candidates"],
7858 json!(["rt:review:singleton:0", "rt:review:singleton:1"])
7859 );
7860 Ok(())
7861 }
7862
7863 #[test]
7864 fn wrapped_ambiguous_live_alias_errors_include_structured_data() -> Result<(), serde_json::Error>
7865 {
7866 let response: Value = serde_json::from_str(&error_response(
7867 json!(1),
7868 -32602,
7869 "invalid identity: ambiguous live identity alias review:singleton: candidates [rt:review:singleton:0, rt:review:singleton:1]",
7870 ))?;
7871
7872 assert_eq!(
7873 response["error"]["data"]["kind"],
7874 json!("ambiguous_live_identity_alias")
7875 );
7876 assert_eq!(
7877 response["error"]["data"]["identity"],
7878 json!("review:singleton")
7879 );
7880 assert_eq!(
7881 response["error"]["data"]["candidates"],
7882 json!(["rt:review:singleton:0", "rt:review:singleton:1"])
7883 );
7884 Ok(())
7885 }
7886
7887 #[tokio::test]
7888 async fn runtime_id_live_only_resolution_rejects_duplicate_projected_identity()
7889 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7890 let temp_dir = tempfile::tempdir()?;
7891 let runtime = Box::pin(
7892 UnifiedRuntime::builder()
7893 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
7894 .module_config(MobKitConfig {
7895 modules: Vec::new(),
7896 discovery: DiscoverySpec {
7897 namespace: "rpc-identity-alias-test".to_string(),
7898 modules: Vec::new(),
7899 },
7900 pre_spawn: Vec::new(),
7901 })
7902 .timeout(Duration::from_secs(1))
7903 .build(),
7904 )
7905 .await?;
7906 for runtime_id in ["rt:review:singleton:0", "rt:review:singleton:1"] {
7907 let mut labels = BTreeMap::new();
7908 labels.insert("agent_identity".to_string(), "review:singleton".to_string());
7909 spawn_identity_projection_fixture(
7910 &runtime,
7911 SpawnMemberSpec::from_wire(
7912 "worker".to_string(),
7913 runtime_id.to_string(),
7914 Some("You are a duplicate Review Agent.".into()),
7915 None,
7916 None,
7917 )
7918 .with_labels(labels),
7919 )
7920 .await?;
7921 }
7922 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
7923 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
7924 lease_provider: Arc::new(LocalLeaseProvider::new()),
7925 runtime_instance_id: "rpc-identity-alias-test".to_string(),
7926 has_runtime_store: true,
7927 durability_policy: DurabilityPolicy::SyncWriteThrough,
7928 bridge: None,
7929 default_timeout: None,
7930 });
7931
7932 let err =
7933 resolve_rpc_identity_control_target(&runtime, &identity_rt, "rt:review:singleton:0")
7934 .await
7935 .expect_err("runtime-id live-only fallback should reject duplicate durable alias");
7936 assert!(
7937 err.contains("ambiguous live identity alias review:singleton"),
7938 "unexpected error: {err}"
7939 );
7940
7941 Ok(())
7942 }
7943
7944 #[tokio::test]
7945 async fn durable_resolution_prefers_registered_live_binding_over_stale_duplicates()
7946 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7947 let temp_dir = tempfile::tempdir()?;
7948 let runtime = Box::pin(
7949 UnifiedRuntime::builder()
7950 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
7951 .module_config(MobKitConfig {
7952 modules: Vec::new(),
7953 discovery: DiscoverySpec {
7954 namespace: "rpc-identity-alias-test".to_string(),
7955 modules: Vec::new(),
7956 },
7957 pre_spawn: Vec::new(),
7958 })
7959 .timeout(Duration::from_secs(1))
7960 .build(),
7961 )
7962 .await?;
7963 for runtime_id in ["rt:review:singleton:0", "rt:review:singleton:1"] {
7964 let mut labels = BTreeMap::new();
7965 labels.insert("agent_identity".to_string(), "review:singleton".to_string());
7966 spawn_identity_projection_fixture(
7967 &runtime,
7968 SpawnMemberSpec::from_wire(
7969 "worker".to_string(),
7970 runtime_id.to_string(),
7971 Some("You are a duplicate Review Agent.".into()),
7972 None,
7973 None,
7974 )
7975 .with_labels(labels),
7976 )
7977 .await?;
7978 }
7979 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
7980 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
7981 lease_provider: Arc::new(LocalLeaseProvider::new()),
7982 runtime_instance_id: "rpc-identity-alias-test".to_string(),
7983 has_runtime_store: true,
7984 durability_policy: DurabilityPolicy::SyncWriteThrough,
7985 bridge: None,
7986 default_timeout: None,
7987 });
7988 let identity = AgentIdentity::parse("review:singleton")?;
7989 let record = ContinuityRecord {
7990 identity: identity.clone(),
7991 agent_runtime_id: AgentRuntimeId::parse("rt:review:singleton:1")?,
7992 session_id: meerkat_core::types::SessionId::new(),
7993 generation: ContinuityGeneration::new(1),
7994 checkpoint_version: CheckpointVersion::new(0),
7995 };
7996 identity_rt
7997 .register(
7998 DurableAgentSpec {
7999 identity,
8000 profile: meerkat_mob::ProfileName::from("worker"),
8001 addressability: AgentAddressability::Addressable,
8002 display_name: None,
8003 labels: BTreeMap::new(),
8004 context: None,
8005 additional_instructions: Vec::new(),
8006 initial_message: None,
8007 runtime_mode_override: None,
8008 backend: None,
8009 binding: None,
8010 },
8011 IdentityLifecycleState::Active,
8012 Some(record),
8013 None,
8014 )
8015 .await;
8016
8017 let target =
8018 resolve_rpc_identity_control_target(&runtime, &identity_rt, "review:singleton").await?;
8019 assert_eq!(target.identity.as_str(), "review:singleton");
8020 assert_eq!(
8021 target
8022 .live
8023 .as_ref()
8024 .map(|alias| alias.runtime_member_id.as_str()),
8025 Some("rt:review:singleton:1")
8026 );
8027
8028 let target =
8029 resolve_rpc_identity_control_target(&runtime, &identity_rt, "rt:review:singleton:1")
8030 .await?;
8031 assert_eq!(
8032 target
8033 .live
8034 .as_ref()
8035 .map(|alias| alias.runtime_member_id.as_str()),
8036 Some("rt:review:singleton:1")
8037 );
8038
8039 let stale_target =
8040 resolve_rpc_identity_control_target(&runtime, &identity_rt, "rt:review:singleton:0")
8041 .await?;
8042 assert_eq!(
8043 stale_target
8044 .live
8045 .as_ref()
8046 .map(|alias| alias.runtime_member_id.as_str()),
8047 Some("rt:review:singleton:0")
8048 );
8049 let stale_response =
8050 super::rpc_stale_live_alias_error_response(&identity_rt, &stale_target, json!(99))
8051 .await
8052 .expect("old reset generation should be rejected as stale");
8053 assert_eq!(
8054 stale_response
8055 .error
8056 .as_ref()
8057 .and_then(|error| error.data.as_ref())
8058 .and_then(|data| data.get("kind")),
8059 Some(&json!("stale_identity_runtime_binding"))
8060 );
8061 assert_eq!(
8062 stale_response
8063 .error
8064 .as_ref()
8065 .and_then(|error| error.data.as_ref())
8066 .and_then(|data| data.get("registered_runtime_member_id")),
8067 Some(&json!("rt:review:singleton:1"))
8068 );
8069 assert_eq!(
8070 stale_response
8071 .error
8072 .as_ref()
8073 .and_then(|error| error.data.as_ref())
8074 .and_then(|data| data.get("live_runtime_member_id")),
8075 Some(&json!("rt:review:singleton:0"))
8076 );
8077
8078 Ok(())
8079 }
8080
8081 #[tokio::test]
8082 async fn current_generation_resolvers_ignore_stale_reset_member_rows()
8083 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8084 let temp_dir = tempfile::tempdir()?;
8085 let mut runtime = Box::pin(
8086 UnifiedRuntime::builder()
8087 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8088 .module_config(MobKitConfig {
8089 modules: Vec::new(),
8090 discovery: DiscoverySpec {
8091 namespace: "rpc-current-generation-target-test".to_string(),
8092 modules: Vec::new(),
8093 },
8094 pre_spawn: Vec::new(),
8095 })
8096 .timeout(Duration::from_secs(1))
8097 .build(),
8098 )
8099 .await?;
8100 let durable_identity = "review:current-generation";
8101 let stale_alias = "rt:review:current-generation:0";
8102 let current_alias = "rt:review:current-generation:1";
8103 for runtime_id in [stale_alias, current_alias] {
8104 spawn_identity_projection_fixture(
8105 &runtime,
8106 SpawnMemberSpec::from_wire(
8107 "worker".to_string(),
8108 runtime_id.to_string(),
8109 None,
8110 None,
8111 None,
8112 )
8113 .with_labels(BTreeMap::from([(
8114 "agent_identity".to_string(),
8115 durable_identity.to_string(),
8116 )])),
8117 )
8118 .await?;
8119 }
8120
8121 let identity_runtime = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
8122 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8123 lease_provider: Arc::new(LocalLeaseProvider::new()),
8124 runtime_instance_id: "rpc-current-generation-target-test".to_string(),
8125 has_runtime_store: true,
8126 durability_policy: DurabilityPolicy::SyncWriteThrough,
8127 bridge: None,
8128 default_timeout: None,
8129 }));
8130 let identity = AgentIdentity::parse(durable_identity)?;
8131 identity_runtime
8132 .register(
8133 rpc_durable_spec(durable_identity, "worker"),
8134 IdentityLifecycleState::Active,
8135 Some(ContinuityRecord {
8136 identity: identity.clone(),
8137 agent_runtime_id: AgentRuntimeId::parse(current_alias)?,
8138 session_id: meerkat_core::types::SessionId::new(),
8139 generation: ContinuityGeneration::new(1),
8140 checkpoint_version: CheckpointVersion::new(0),
8141 }),
8142 None,
8143 )
8144 .await;
8145
8146 let expected_session = runtime
8147 .mob_handle()
8148 .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id(current_alias))
8149 .await
8150 .ok_or("current member must have a bridge session")?;
8151 let params = json!({"identity": durable_identity});
8152 let target = identity_runtime
8153 .member_alias_lifecycle_target(durable_identity)
8154 .await?
8155 .ok_or("durable identity must resolve to lifecycle authority")?;
8156 let operation_runtime = Arc::clone(&identity_runtime);
8157 let handle = runtime.mob_handle();
8158 let resolved_session = IdentityRuntime::run_member_alias_targets_operation_tracked(
8159 vec![target],
8160 move || async move {
8161 resolve_live_target(&handle, Some(&operation_runtime), true, ¶ms).await
8162 },
8163 )
8164 .await?;
8165 assert_eq!(resolved_session, Some(expected_session));
8166
8167 let fallback_error = resolve_live_target(
8168 &runtime.mob_handle(),
8169 None,
8170 false,
8171 &json!({"identity": durable_identity}),
8172 )
8173 .await
8174 .expect_err("two live generations without identity authority must be ambiguous");
8175 assert!(
8176 fallback_error.contains("ambiguous live member alias"),
8177 "unexpected fallback error: {fallback_error}"
8178 );
8179 let cross_mob_fallback = runtime
8180 .local_member_peer_info(durable_identity)
8181 .await
8182 .expect_err("cross-mob fallback without identity authority must be ambiguous");
8183 assert!(
8184 cross_mob_fallback
8185 .to_string()
8186 .contains("ambiguous durable member alias"),
8187 "unexpected cross-mob fallback error: {cross_mob_fallback}"
8188 );
8189
8190 runtime.attach_identity_first_context(Arc::new(
8191 crate::identity_first::IdentityFirstRuntimeContext::new(
8192 Arc::clone(&identity_runtime),
8193 Arc::new(EmptyRosterProvider),
8194 None,
8195 None,
8196 None,
8197 ),
8198 ));
8199 let (_, comms_name, _) = runtime.local_member_peer_info(durable_identity).await?;
8200 assert!(
8201 comms_name.ends_with(crate::member_comms_id::mob_member_id_str(current_alias).as_ref()),
8202 "peer info must use the current generation: {comms_name}"
8203 );
8204
8205 let remote_comms_name = "remote-mob/worker/peer";
8206 let remote_pubkey = [42_u8; 32];
8207 let remote_peer_id =
8208 meerkat_core::comms::PeerId::from_ed25519_pubkey(&remote_pubkey).to_string();
8209 runtime
8210 .wire_local(
8211 durable_identity,
8212 remote_comms_name,
8213 &remote_peer_id,
8214 &format!("inproc://{remote_comms_name}"),
8215 Some(remote_pubkey),
8216 )
8217 .await?;
8218 let current = runtime
8219 .mob_handle()
8220 .get_member(&crate::member_comms_id::mob_member_id(current_alias))
8221 .await?
8222 .ok_or("current member missing")?;
8223 let stale = runtime
8224 .mob_handle()
8225 .get_member(&crate::member_comms_id::mob_member_id(stale_alias))
8226 .await?
8227 .ok_or("stale member missing")?;
8228 assert!(
8229 current
8230 .wired_to
8231 .iter()
8232 .any(|peer| peer.as_str() == remote_comms_name),
8233 "current generation must receive the wire"
8234 );
8235 assert!(
8236 stale
8237 .wired_to
8238 .iter()
8239 .all(|peer| peer.as_str() != remote_comms_name),
8240 "stale generation must not receive the wire"
8241 );
8242
8243 runtime.shutdown().await;
8244 Ok(())
8245 }
8246
8247 #[tokio::test]
8248 async fn durable_resolution_rejects_hidden_registered_live_binding()
8249 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8250 let temp_dir = tempfile::tempdir()?;
8251 let runtime = Box::pin(
8252 UnifiedRuntime::builder()
8253 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8254 .module_config(MobKitConfig {
8255 modules: Vec::new(),
8256 discovery: DiscoverySpec {
8257 namespace: "rpc-hidden-bound-test".to_string(),
8258 modules: Vec::new(),
8259 },
8260 pre_spawn: Vec::new(),
8261 })
8262 .timeout(Duration::from_secs(1))
8263 .build(),
8264 )
8265 .await?;
8266 spawn_identity_projection_fixture(
8267 &runtime,
8268 SpawnMemberSpec::from_wire(
8269 "worker".to_string(),
8270 "rt:review:singleton:0".to_string(),
8271 Some("You are a hidden Review Agent.".into()),
8272 None,
8273 None,
8274 )
8275 .with_labels(BTreeMap::from([
8276 ("agent_identity".to_string(), "review:singleton".to_string()),
8277 ("role".to_string(), "delegate".to_string()),
8278 ("source_mob_id".to_string(), "upstream".to_string()),
8279 ])),
8280 )
8281 .await?;
8282 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8283 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8284 lease_provider: Arc::new(LocalLeaseProvider::new()),
8285 runtime_instance_id: "rpc-hidden-bound-test".to_string(),
8286 has_runtime_store: true,
8287 durability_policy: DurabilityPolicy::SyncWriteThrough,
8288 bridge: None,
8289 default_timeout: None,
8290 });
8291 let identity = AgentIdentity::parse("review:singleton")?;
8292 identity_rt
8293 .register(
8294 DurableAgentSpec {
8295 identity: identity.clone(),
8296 profile: meerkat_mob::ProfileName::from("worker"),
8297 addressability: AgentAddressability::Addressable,
8298 display_name: None,
8299 labels: BTreeMap::new(),
8300 context: None,
8301 additional_instructions: Vec::new(),
8302 initial_message: None,
8303 runtime_mode_override: None,
8304 backend: None,
8305 binding: None,
8306 },
8307 IdentityLifecycleState::Active,
8308 Some(ContinuityRecord {
8309 identity,
8310 agent_runtime_id: AgentRuntimeId::parse("rt:review:singleton:0")?,
8311 session_id: meerkat_core::types::SessionId::new(),
8312 generation: ContinuityGeneration::new(0),
8313 checkpoint_version: CheckpointVersion::new(0),
8314 }),
8315 None,
8316 )
8317 .await;
8318
8319 for requested_identity in ["review:singleton", "rt:review:singleton:0"] {
8320 let err =
8321 resolve_rpc_identity_control_target(&runtime, &identity_rt, requested_identity)
8322 .await
8323 .expect_err("hidden registered live binding must not resolve");
8324 assert!(
8325 err.contains("identity hidden by policy"),
8326 "unexpected error for {requested_identity}: {err}"
8327 );
8328 }
8329
8330 Ok(())
8331 }
8332
8333 #[tokio::test]
8334 async fn live_only_hidden_alias_reports_policy_error()
8335 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8336 let temp_dir = tempfile::tempdir()?;
8337 let runtime = Box::pin(
8338 UnifiedRuntime::builder()
8339 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8340 .module_config(MobKitConfig {
8341 modules: Vec::new(),
8342 discovery: DiscoverySpec {
8343 namespace: "rpc-hidden-live-only-test".to_string(),
8344 modules: Vec::new(),
8345 },
8346 pre_spawn: Vec::new(),
8347 })
8348 .timeout(Duration::from_secs(1))
8349 .build(),
8350 )
8351 .await?;
8352 spawn_identity_projection_fixture(
8353 &runtime,
8354 SpawnMemberSpec::from_wire(
8355 "worker".to_string(),
8356 "rt:review:singleton:0".to_string(),
8357 Some("You are a hidden Review Agent.".into()),
8358 None,
8359 None,
8360 )
8361 .with_labels(BTreeMap::from([
8362 ("agent_identity".to_string(), "review:singleton".to_string()),
8363 ("role".to_string(), "delegate".to_string()),
8364 ("source_mob_id".to_string(), "upstream".to_string()),
8365 ])),
8366 )
8367 .await?;
8368 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8369 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8370 lease_provider: Arc::new(LocalLeaseProvider::new()),
8371 runtime_instance_id: "rpc-hidden-live-only-test".to_string(),
8372 has_runtime_store: true,
8373 durability_policy: DurabilityPolicy::SyncWriteThrough,
8374 bridge: None,
8375 default_timeout: None,
8376 });
8377
8378 for requested_identity in ["review:singleton", "rt:review:singleton:0"] {
8379 let err =
8380 resolve_rpc_identity_control_target(&runtime, &identity_rt, requested_identity)
8381 .await
8382 .expect_err("hidden live-only alias must not collapse into unknown identity");
8383 assert!(
8384 err.contains("identity hidden by policy"),
8385 "unexpected error for {requested_identity}: {err}"
8386 );
8387 }
8388
8389 let identity_ctx = IdentityFirstContext {
8390 runtime: Arc::new(identity_rt),
8391 roster_provider: Arc::new(EmptyRosterProvider),
8392 topology_provider: None,
8393 customizer: None,
8394 agent_memory_provider: None,
8395 mob_definition: None,
8396 };
8397 for requested_identity in ["review:singleton", "rt:review:singleton:0"] {
8398 let response: Value = serde_json::from_str(
8399 &handle_unified_rpc_json(
8400 &runtime,
8401 &json!({
8402 "jsonrpc": "2.0",
8403 "id": 1,
8404 "method": "mobkit/status_identity",
8405 "params": { "identity": requested_identity },
8406 })
8407 .to_string(),
8408 Duration::from_secs(1),
8409 None,
8410 Some(&identity_ctx),
8411 )
8412 .await,
8413 )?;
8414 assert_eq!(
8415 response["error"]["data"]["kind"],
8416 json!("identity_hidden_by_policy"),
8417 "unexpected hidden response for {requested_identity}: {response:#?}"
8418 );
8419 }
8420
8421 Ok(())
8422 }
8423
8424 #[tokio::test]
8425 async fn live_only_resolution_rejects_runtime_member_bound_to_other_durable_identity()
8426 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8427 let temp_dir = tempfile::tempdir()?;
8428 let runtime = Box::pin(
8429 UnifiedRuntime::builder()
8430 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8431 .module_config(MobKitConfig {
8432 modules: Vec::new(),
8433 discovery: DiscoverySpec {
8434 namespace: "rpc-identity-alias-test".to_string(),
8435 modules: Vec::new(),
8436 },
8437 pre_spawn: Vec::new(),
8438 })
8439 .timeout(Duration::from_secs(1))
8440 .build(),
8441 )
8442 .await?;
8443 let mut labels = BTreeMap::new();
8444 labels.insert("agent_identity".to_string(), "other:singleton".to_string());
8445 spawn_identity_projection_fixture(
8446 &runtime,
8447 SpawnMemberSpec::from_wire(
8448 "worker".to_string(),
8449 "rt:review:singleton:0".to_string(),
8450 Some("You are a wrong-projected Review Agent.".into()),
8451 None,
8452 None,
8453 )
8454 .with_labels(labels),
8455 )
8456 .await?;
8457
8458 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8459 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8460 lease_provider: Arc::new(LocalLeaseProvider::new()),
8461 runtime_instance_id: "rpc-identity-alias-test".to_string(),
8462 has_runtime_store: true,
8463 durability_policy: DurabilityPolicy::SyncWriteThrough,
8464 bridge: None,
8465 default_timeout: None,
8466 });
8467 let identity = AgentIdentity::parse("review:singleton")?;
8468 let record = ContinuityRecord {
8469 identity: identity.clone(),
8470 agent_runtime_id: AgentRuntimeId::parse("rt:review:singleton:0")?,
8471 session_id: meerkat_core::types::SessionId::new(),
8472 generation: ContinuityGeneration::new(0),
8473 checkpoint_version: CheckpointVersion::new(0),
8474 };
8475 identity_rt
8476 .register(
8477 DurableAgentSpec {
8478 identity: identity.clone(),
8479 profile: meerkat_mob::ProfileName::from("worker"),
8480 addressability: AgentAddressability::Addressable,
8481 display_name: None,
8482 labels: BTreeMap::new(),
8483 context: None,
8484 additional_instructions: Vec::new(),
8485 initial_message: None,
8486 runtime_mode_override: None,
8487 backend: None,
8488 binding: None,
8489 },
8490 IdentityLifecycleState::Active,
8491 Some(record),
8492 Some(LeaseGrant {
8493 identity,
8494 fencing_token: FencingToken::new(1),
8495 ttl: Duration::from_mins(1),
8496 }),
8497 )
8498 .await;
8499
8500 let err = resolve_rpc_identity_control_target(&runtime, &identity_rt, "other:singleton")
8501 .await
8502 .expect_err("wrong-projected live alias must not resolve as live-only");
8503 assert!(
8504 err.contains("identity runtime binding belongs to review:singleton"),
8505 "unexpected error: {err}"
8506 );
8507
8508 Ok(())
8509 }
8510
8511 #[tokio::test]
8520 async fn send_message_resolves_bare_durable_identity_through_identity_bridge()
8521 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8522 let temp_dir = tempfile::tempdir()?;
8523 let runtime = Box::pin(
8524 UnifiedRuntime::builder()
8525 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8526 .module_config(MobKitConfig {
8527 modules: Vec::new(),
8528 discovery: DiscoverySpec {
8529 namespace: "rpc-send-message-identity-bridge-test".to_string(),
8530 modules: Vec::new(),
8531 },
8532 pre_spawn: Vec::new(),
8533 })
8534 .timeout(Duration::from_secs(5))
8535 .build(),
8536 )
8537 .await?;
8538
8539 for (runtime_id, durable) in [
8542 ("rt:atlas-base-001:0", "atlas-base-001"),
8543 ("rt:draco-base-001:0", "draco-base-001"),
8544 ] {
8545 spawn_identity_projection_fixture(
8546 &runtime,
8547 SpawnMemberSpec::from_wire(
8548 "worker".to_string(),
8549 runtime_id.to_string(),
8550 Some("You are a swarm base agent.".into()),
8551 None,
8552 None,
8553 )
8554 .with_labels(BTreeMap::from([(
8555 "agent_identity".to_string(),
8556 durable.to_string(),
8557 )])),
8558 )
8559 .await?;
8560 }
8561 runtime
8564 .spawn(SpawnMemberSpec::from_wire(
8565 "worker".to_string(),
8566 "draco-base-001".to_string(),
8567 Some("You are the raw roster member.".into()),
8568 None,
8569 None,
8570 ))
8571 .await?;
8572
8573 let session_service = runtime
8574 .mob_runtime()
8575 .session_service()
8576 .cloned()
8577 .expect("test mob spec has a session service");
8578 let bridge: Arc<dyn crate::identity_first::SessionBridge> = Arc::new(
8579 crate::identity_first::MobSessionBridge::with_session_service(
8580 runtime.mob_handle(),
8581 session_service,
8582 ),
8583 );
8584 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8585 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8586 lease_provider: Arc::new(LocalLeaseProvider::new()),
8587 runtime_instance_id: "rpc-send-message-identity-bridge-test".to_string(),
8588 has_runtime_store: true,
8589 durability_policy: DurabilityPolicy::SyncWriteThrough,
8590 bridge: Some(bridge),
8591 default_timeout: None,
8592 })
8593 .with_runtime_services(crate::identity_first::AgentRuntimeServices::new(
8594 runtime.mob_handle(),
8595 ));
8596 for (durable, runtime_id) in [
8597 ("atlas-base-001", "rt:atlas-base-001:0"),
8598 ("draco-base-001", "rt:draco-base-001:0"),
8599 ] {
8600 let identity = AgentIdentity::parse(durable)?;
8601 let session_id = runtime
8602 .mob_handle()
8603 .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id(runtime_id))
8604 .await
8605 .unwrap_or_else(meerkat_core::types::SessionId::new);
8606 identity_rt
8607 .register(
8608 DurableAgentSpec {
8609 identity: identity.clone(),
8610 profile: meerkat_mob::ProfileName::from("worker"),
8611 addressability: AgentAddressability::Addressable,
8612 display_name: None,
8613 labels: BTreeMap::new(),
8614 context: None,
8615 additional_instructions: Vec::new(),
8616 initial_message: None,
8617 runtime_mode_override: None,
8618 backend: None,
8619 binding: None,
8620 },
8621 IdentityLifecycleState::Active,
8622 Some(ContinuityRecord {
8623 identity: identity.clone(),
8624 agent_runtime_id: AgentRuntimeId::parse(runtime_id)?,
8625 session_id,
8626 generation: ContinuityGeneration::new(0),
8627 checkpoint_version: CheckpointVersion::new(0),
8628 }),
8629 Some(LeaseGrant {
8630 identity,
8631 fencing_token: FencingToken::new(1),
8632 ttl: Duration::from_mins(1),
8633 }),
8634 )
8635 .await;
8636 }
8637 let identity_ctx = IdentityFirstContext {
8638 runtime: Arc::new(identity_rt),
8639 roster_provider: Arc::new(EmptyRosterProvider),
8640 topology_provider: None,
8641 customizer: None,
8642 agent_memory_provider: None,
8643 mob_definition: None,
8644 };
8645
8646 let send = |id: u64, params: Value| {
8647 let runtime = &runtime;
8648 let identity_ctx = &identity_ctx;
8649 async move {
8650 let raw = handle_unified_rpc_json(
8651 runtime,
8652 &json!({
8653 "jsonrpc": "2.0",
8654 "id": id,
8655 "method": "mobkit/send_message",
8656 "params": params,
8657 })
8658 .to_string(),
8659 Duration::from_secs(10),
8660 None,
8661 Some(identity_ctx),
8662 )
8663 .await;
8664 serde_json::from_str::<Value>(&raw)
8665 }
8666 };
8667
8668 let response = send(
8671 1,
8672 json!({ "member_id": "atlas-base-001", "message": "status check" }),
8673 )
8674 .await?;
8675 assert!(
8676 response["error"].is_null(),
8677 "bare identity send must bridge-resolve: {response:#?}"
8678 );
8679 assert_eq!(response["result"]["accepted"], json!(true));
8680 assert_eq!(response["result"]["member_id"], json!("atlas-base-001"));
8681 let atlas_session = runtime
8682 .mob_handle()
8683 .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id(
8684 "rt:atlas-base-001:0",
8685 ))
8686 .await
8687 .expect("atlas runtime member has a bridge session after send")
8688 .to_string();
8689 assert_eq!(response["result"]["session_id"], json!(atlas_session));
8690
8691 let response = send(
8693 2,
8694 json!({
8695 "member_id": "atlas-base-001",
8696 "message": "steer: stand down",
8697 "handling_mode": "steer",
8698 }),
8699 )
8700 .await?;
8701 assert!(
8702 response["error"].is_null(),
8703 "bare identity steer must bridge-resolve: {response:#?}"
8704 );
8705 assert_eq!(response["result"]["accepted"], json!(true));
8706
8707 let response = send(
8711 3,
8712 json!({ "member_id": "draco-base-001", "message": "raw roster delivery" }),
8713 )
8714 .await?;
8715 assert!(
8716 response["error"].is_null(),
8717 "exact roster member send must keep raw semantics: {response:#?}"
8718 );
8719 assert_eq!(response["result"]["accepted"], json!(true));
8720 let draco_raw_session = runtime
8721 .mob_handle()
8722 .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id("draco-base-001"))
8723 .await
8724 .expect("bare draco member has a bridge session after send")
8725 .to_string();
8726 assert_eq!(response["result"]["session_id"], json!(draco_raw_session));
8727 if let Some(draco_rt_session) = runtime
8728 .mob_handle()
8729 .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id(
8730 "rt:draco-base-001:0",
8731 ))
8732 .await
8733 {
8734 assert_ne!(
8735 response["result"]["session_id"],
8736 json!(draco_rt_session.to_string()),
8737 "exact member id match must not be shadowed by identity resolution"
8738 );
8739 }
8740
8741 let response = send(
8743 4,
8744 json!({ "member_id": "phantom-base-999", "message": "nobody home" }),
8745 )
8746 .await?;
8747 assert_eq!(response["error"]["code"], json!(-32000), "{response:#?}");
8748 let message = response["error"]["message"]
8749 .as_str()
8750 .expect("error message");
8751 assert!(
8752 message.starts_with("send_message failed:"),
8753 "unexpected error message: {message}"
8754 );
8755
8756 Ok(())
8757 }
8758
8759 #[tokio::test]
8766 async fn member_state_wire_vocabulary_is_lowercase_on_both_surfaces()
8767 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8768 let temp_dir = tempfile::tempdir()?;
8769 let runtime = Box::pin(
8770 UnifiedRuntime::builder()
8771 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8772 .module_config(MobKitConfig {
8773 modules: Vec::new(),
8774 discovery: DiscoverySpec {
8775 namespace: "rpc-state-vocabulary-test".to_string(),
8776 modules: Vec::new(),
8777 },
8778 pre_spawn: Vec::new(),
8779 })
8780 .timeout(Duration::from_secs(5))
8781 .build(),
8782 )
8783 .await?;
8784 runtime
8785 .spawn(SpawnMemberSpec::from_wire(
8786 "worker".to_string(),
8787 "worker-one".to_string(),
8788 None,
8789 None,
8790 None,
8791 ))
8792 .await?;
8793
8794 let raw = handle_unified_rpc_json(
8796 &runtime,
8797 &json!({
8798 "jsonrpc": "2.0",
8799 "id": 1,
8800 "method": "mobkit/get_member",
8801 "params": { "member_id": "worker-one" },
8802 })
8803 .to_string(),
8804 Duration::from_secs(5),
8805 None,
8806 None,
8807 )
8808 .await;
8809 let response: Value = serde_json::from_str(&raw)?;
8810 assert_eq!(
8811 response["result"]["state"],
8812 json!("active"),
8813 "member rows must speak the lowercase SDK vocabulary: {response:#?}"
8814 );
8815
8816 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8818 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8819 lease_provider: Arc::new(LocalLeaseProvider::new()),
8820 runtime_instance_id: "rpc-state-vocabulary-test".to_string(),
8821 has_runtime_store: true,
8822 durability_policy: DurabilityPolicy::SyncWriteThrough,
8823 bridge: None,
8824 default_timeout: None,
8825 });
8826 let identity = AgentIdentity::parse("review:singleton")?;
8827 identity_rt
8828 .register(
8829 DurableAgentSpec {
8830 identity: identity.clone(),
8831 profile: meerkat_mob::ProfileName::from("worker"),
8832 addressability: AgentAddressability::Addressable,
8833 display_name: None,
8834 labels: BTreeMap::new(),
8835 context: None,
8836 additional_instructions: Vec::new(),
8837 initial_message: None,
8838 runtime_mode_override: None,
8839 backend: None,
8840 binding: None,
8841 },
8842 IdentityLifecycleState::Active,
8843 Some(ContinuityRecord {
8844 identity: identity.clone(),
8845 agent_runtime_id: AgentRuntimeId::parse("rt:review:singleton:0")?,
8846 session_id: meerkat_core::types::SessionId::new(),
8847 generation: ContinuityGeneration::new(0),
8848 checkpoint_version: CheckpointVersion::new(0),
8849 }),
8850 Some(LeaseGrant {
8851 identity,
8852 fencing_token: FencingToken::new(1),
8853 ttl: Duration::from_mins(1),
8854 }),
8855 )
8856 .await;
8857 let identity_ctx = IdentityFirstContext {
8858 runtime: Arc::new(identity_rt),
8859 roster_provider: Arc::new(EmptyRosterProvider),
8860 topology_provider: None,
8861 customizer: None,
8862 agent_memory_provider: None,
8863 mob_definition: None,
8864 };
8865 let raw = handle_unified_rpc_json(
8866 &runtime,
8867 &json!({
8868 "jsonrpc": "2.0",
8869 "id": 2,
8870 "method": "mobkit/status_identity",
8871 "params": { "identity": "review:singleton" },
8872 })
8873 .to_string(),
8874 Duration::from_secs(5),
8875 None,
8876 Some(&identity_ctx),
8877 )
8878 .await;
8879 let response: Value = serde_json::from_str(&raw)?;
8880 assert_eq!(
8881 response["result"]["state"],
8882 json!("active"),
8883 "identity status must speak the same lowercase vocabulary: {response:#?}"
8884 );
8885
8886 Ok(())
8887 }
8888
8889 #[tokio::test]
8898 async fn send_message_pins_baseline_member_over_identity_fallback()
8899 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8900 let temp_dir = tempfile::tempdir()?;
8901 let runtime = Box::pin(
8902 UnifiedRuntime::builder()
8903 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8904 .module_config(MobKitConfig {
8905 modules: Vec::new(),
8906 discovery: DiscoverySpec {
8907 namespace: "rpc-send-message-baseline-pin-test".to_string(),
8908 modules: Vec::new(),
8909 },
8910 pre_spawn: Vec::new(),
8911 })
8912 .timeout(Duration::from_secs(5))
8913 .build(),
8914 )
8915 .await?;
8916
8917 spawn_identity_projection_fixture(
8920 &runtime,
8921 SpawnMemberSpec::from_wire(
8922 "worker".to_string(),
8923 "rt:draco-base-001:0".to_string(),
8924 None,
8925 None,
8926 None,
8927 ),
8928 )
8929 .await?;
8930 runtime
8931 .spawn(
8932 SpawnMemberSpec::from_wire(
8933 "worker".to_string(),
8934 "draco-base-001".to_string(),
8935 None,
8936 None,
8937 None,
8938 )
8939 .with_runtime_mode(meerkat_mob::MobRuntimeMode::TurnDriven),
8944 )
8945 .await?;
8946
8947 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8948 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8949 lease_provider: Arc::new(LocalLeaseProvider::new()),
8950 runtime_instance_id: "rpc-send-message-baseline-pin-test".to_string(),
8951 has_runtime_store: true,
8952 durability_policy: DurabilityPolicy::SyncWriteThrough,
8953 bridge: None,
8954 default_timeout: None,
8955 });
8956 let identity = AgentIdentity::parse("draco-base-001")?;
8957 identity_rt
8958 .register(
8959 DurableAgentSpec {
8960 identity: identity.clone(),
8961 profile: meerkat_mob::ProfileName::from("worker"),
8962 addressability: AgentAddressability::Addressable,
8963 display_name: None,
8964 labels: BTreeMap::new(),
8965 context: None,
8966 additional_instructions: Vec::new(),
8967 initial_message: None,
8968 runtime_mode_override: None,
8969 backend: None,
8970 binding: None,
8971 },
8972 IdentityLifecycleState::Active,
8973 Some(ContinuityRecord {
8974 identity: identity.clone(),
8975 agent_runtime_id: AgentRuntimeId::parse("rt:draco-base-001:0")?,
8976 session_id: meerkat_core::types::SessionId::new(),
8977 generation: ContinuityGeneration::new(0),
8978 checkpoint_version: CheckpointVersion::new(0),
8979 }),
8980 Some(LeaseGrant {
8981 identity,
8982 fencing_token: FencingToken::new(1),
8983 ttl: Duration::from_mins(1),
8984 }),
8985 )
8986 .await;
8987 let identity_ctx = IdentityFirstContext {
8988 runtime: Arc::new(identity_rt),
8989 roster_provider: Arc::new(EmptyRosterProvider),
8990 topology_provider: None,
8991 customizer: None,
8992 agent_memory_provider: None,
8993 mob_definition: None,
8994 };
8995
8996 runtime
8998 .mob_runtime()
8999 .set_baseline_member_specs(vec![SpawnMemberSpec::new(
9000 meerkat_mob::ProfileName::from("worker"),
9001 meerkat_mob::AgentIdentity::from("draco-base-001"),
9002 )])
9003 .await;
9004 runtime
9007 .mob_handle()
9008 .retire(meerkat_mob::AgentIdentity::from("draco-base-001"))
9009 .await?;
9010
9011 let raw = handle_unified_rpc_json(
9012 &runtime,
9013 &json!({
9014 "jsonrpc": "2.0",
9015 "id": 1,
9016 "method": "mobkit/send_message",
9017 "params": { "member_id": "draco-base-001", "message": "mid-reconcile send" },
9018 })
9019 .to_string(),
9020 Duration::from_secs(10),
9021 None,
9022 Some(&identity_ctx),
9023 )
9024 .await;
9025 let response: Value = serde_json::from_str(&raw)?;
9026 assert_eq!(
9027 response["error"]["code"],
9028 json!(-32000),
9029 "transiently-absent baseline member must keep raw member-id semantics \
9030 instead of silently delivering through the identity bridge: {response:#?}"
9031 );
9032 let message = response["error"]["message"]
9033 .as_str()
9034 .expect("error message");
9035 assert!(
9036 message.starts_with("send_message failed:"),
9037 "unexpected error message: {message}"
9038 );
9039
9040 Ok(())
9041 }
9042
9043 #[tokio::test]
9047 async fn separate_identity_context_pins_generated_alias_for_cross_mob_and_fork()
9048 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9049 let temp_dir = tempfile::tempdir()?;
9050 let runtime = Arc::new(
9051 Box::pin(
9052 UnifiedRuntime::builder()
9053 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9054 .module_config(MobKitConfig {
9055 modules: Vec::new(),
9056 discovery: DiscoverySpec {
9057 namespace: "rpc-separate-identity-context-test".to_string(),
9058 modules: Vec::new(),
9059 },
9060 pre_spawn: Vec::new(),
9061 })
9062 .timeout(Duration::from_secs(5))
9063 .build(),
9064 )
9065 .await?,
9066 );
9067 let stale_alias = "rt:review:separate:0";
9068 let current_alias = "rt:review:separate:1";
9069 spawn_identity_projection_fixture(
9070 &runtime,
9071 SpawnMemberSpec::from_wire(
9072 "worker".to_string(),
9073 stale_alias.to_string(),
9074 None,
9075 None,
9076 None,
9077 )
9078 .with_labels(BTreeMap::from([(
9079 "agent_identity".to_string(),
9080 "review:separate".to_string(),
9081 )])),
9082 )
9083 .await?;
9084
9085 let identity_rt = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
9086 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
9087 lease_provider: Arc::new(LocalLeaseProvider::new()),
9088 runtime_instance_id: "rpc-separate-identity-context-test".to_string(),
9089 has_runtime_store: true,
9090 durability_policy: DurabilityPolicy::SyncWriteThrough,
9091 bridge: None,
9092 default_timeout: None,
9093 }));
9094 let identity = AgentIdentity::parse("review:separate")?;
9095 identity_rt
9096 .register(
9097 rpc_durable_spec(identity.as_str(), "worker"),
9098 IdentityLifecycleState::Active,
9099 Some(ContinuityRecord {
9100 identity: identity.clone(),
9101 agent_runtime_id: AgentRuntimeId::parse(current_alias)?,
9102 session_id: meerkat_core::types::SessionId::new(),
9103 generation: ContinuityGeneration::new(1),
9104 checkpoint_version: CheckpointVersion::new(0),
9105 }),
9106 Some(LeaseGrant {
9107 identity,
9108 fencing_token: FencingToken::new(1),
9109 ttl: Duration::from_mins(1),
9110 }),
9111 )
9112 .await;
9113 let identity_ctx = IdentityFirstContext {
9114 runtime: identity_rt,
9115 roster_provider: Arc::new(EmptyRosterProvider),
9116 topology_provider: None,
9117 customizer: None,
9118 agent_memory_provider: None,
9119 mob_definition: None,
9120 };
9121 assert!(
9122 runtime.identity_runtime().is_none(),
9123 "the regression requires a context supplied only at dispatch"
9124 );
9125
9126 for (method, params) in [
9127 (
9128 "mobkit/cross_mob/wire",
9129 json!({
9130 "local_member_id": stale_alias,
9131 "remote_member_id": "remote-worker",
9132 "remote_mob_id": "remote-mob",
9133 }),
9134 ),
9135 (
9136 "mobkit/fork_helper",
9137 json!({
9138 "source_member_id": stale_alias,
9139 "agent_identity": "fork-probe",
9140 "task": "prove stale source rejection",
9141 }),
9142 ),
9143 ] {
9144 let response: Value = serde_json::from_str(
9145 &super::handle_unified_rpc_json_arc(
9146 &runtime,
9147 &json!({
9148 "jsonrpc": "2.0",
9149 "id": method,
9150 "method": method,
9151 "params": params,
9152 })
9153 .to_string(),
9154 Duration::from_secs(5),
9155 None,
9156 Some(&identity_ctx),
9157 )
9158 .await,
9159 )?;
9160 assert_eq!(response["error"]["code"], json!(-32000), "{response:#?}");
9161 assert!(
9162 response["error"]["message"]
9163 .as_str()
9164 .is_some_and(|message| message.contains("stale runtime alias")),
9165 "{method} must reject through identity authority before raw fallthrough: {response:#?}"
9166 );
9167 }
9168
9169 assert!(
9170 runtime
9171 .mob_handle()
9172 .list_members_including_retiring()
9173 .await
9174 .iter()
9175 .any(|member| {
9176 crate::member_comms_id::runtime_alias_str(member.agent_identity.as_str())
9177 == stale_alias
9178 }),
9179 "the raw stale member remains present, proving rejection was not member-not-found"
9180 );
9181 let _ = runtime.mob_handle().stop().await;
9182 Ok(())
9183 }
9184
9185 #[tokio::test]
9186 async fn embedded_rpc_uses_runtime_owned_identity_context_when_argument_is_absent()
9187 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9188 let temp_dir = tempfile::tempdir()?;
9189 let mut runtime = Box::pin(
9190 UnifiedRuntime::builder()
9191 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9192 .module_config(MobKitConfig {
9193 modules: Vec::new(),
9194 discovery: DiscoverySpec {
9195 namespace: "rpc-owned-identity-context-test".to_string(),
9196 modules: Vec::new(),
9197 },
9198 pre_spawn: Vec::new(),
9199 })
9200 .timeout(Duration::from_secs(5))
9201 .build(),
9202 )
9203 .await?;
9204 let stale_alias = "rt:review:owned-context:0";
9205 let current_alias = "rt:review:owned-context:1";
9206 spawn_identity_projection_fixture(
9207 &runtime,
9208 SpawnMemberSpec::from_wire(
9209 "worker".to_string(),
9210 stale_alias.to_string(),
9211 None,
9212 None,
9213 None,
9214 )
9215 .with_labels(BTreeMap::from([(
9216 "agent_identity".to_string(),
9217 "review:owned-context".to_string(),
9218 )])),
9219 )
9220 .await?;
9221
9222 let identity_runtime = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
9223 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
9224 lease_provider: Arc::new(LocalLeaseProvider::new()),
9225 runtime_instance_id: "rpc-owned-identity-context-test".to_string(),
9226 has_runtime_store: true,
9227 durability_policy: DurabilityPolicy::SyncWriteThrough,
9228 bridge: None,
9229 default_timeout: None,
9230 }));
9231 let identity = AgentIdentity::parse("review:owned-context")?;
9232 identity_runtime
9233 .register(
9234 rpc_durable_spec(identity.as_str(), "worker"),
9235 IdentityLifecycleState::Active,
9236 Some(ContinuityRecord {
9237 identity: identity.clone(),
9238 agent_runtime_id: AgentRuntimeId::parse(current_alias)?,
9239 session_id: meerkat_core::types::SessionId::new(),
9240 generation: ContinuityGeneration::new(1),
9241 checkpoint_version: CheckpointVersion::new(0),
9242 }),
9243 None,
9244 )
9245 .await;
9246 runtime.attach_identity_first_context(Arc::new(
9247 crate::identity_first::IdentityFirstRuntimeContext::new(
9248 identity_runtime,
9249 Arc::new(EmptyRosterProvider),
9250 None,
9251 None,
9252 None,
9253 ),
9254 ));
9255 let runtime = Arc::new(runtime);
9256
9257 let list: Value = serde_json::from_str(
9258 &super::handle_unified_rpc_json_arc(
9259 &runtime,
9260 &json!({
9261 "jsonrpc": "2.0",
9262 "id": 1,
9263 "method": "mobkit/list_members",
9264 "params": {},
9265 })
9266 .to_string(),
9267 Duration::from_secs(5),
9268 None,
9269 None,
9270 )
9271 .await,
9272 )?;
9273 assert_eq!(list["result"], json!([]), "{list:#?}");
9274
9275 let get: Value = serde_json::from_str(
9276 &super::handle_unified_rpc_json_arc(
9277 &runtime,
9278 &json!({
9279 "jsonrpc": "2.0",
9280 "id": 2,
9281 "method": "mobkit/get_member",
9282 "params": {"member_id": stale_alias},
9283 })
9284 .to_string(),
9285 Duration::from_secs(5),
9286 None,
9287 None,
9288 )
9289 .await,
9290 )?;
9291 assert_eq!(
9292 get["error"]["data"]["kind"],
9293 json!("stale_identity_runtime_binding"),
9294 "{get:#?}"
9295 );
9296
9297 runtime.shutdown().await;
9298 Ok(())
9299 }
9300
9301 #[tokio::test]
9305 async fn durable_ownership_observed_before_unknown_identity_never_live_fallbacks()
9306 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9307 let temp_dir = tempfile::tempdir()?;
9308 let runtime = Box::pin(
9309 UnifiedRuntime::builder()
9310 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9311 .module_config(MobKitConfig {
9312 modules: Vec::new(),
9313 discovery: DiscoverySpec {
9314 namespace: "rpc-durable-delete-race-test".to_string(),
9315 modules: Vec::new(),
9316 },
9317 pre_spawn: Vec::new(),
9318 })
9319 .timeout(Duration::from_secs(1))
9320 .build(),
9321 )
9322 .await?;
9323 let durable_identity = "review:delete-race";
9324 let runtime_alias = "rt:review:delete-race:0";
9325 spawn_identity_projection_fixture(
9326 &runtime,
9327 SpawnMemberSpec::from_wire(
9328 "worker".to_string(),
9329 runtime_alias.to_string(),
9330 None,
9331 None,
9332 None,
9333 )
9334 .with_labels(BTreeMap::from([(
9335 "agent_identity".to_string(),
9336 durable_identity.to_string(),
9337 )])),
9338 )
9339 .await?;
9340
9341 let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
9342 continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
9343 lease_provider: Arc::new(LocalLeaseProvider::new()),
9344 runtime_instance_id: "rpc-durable-delete-race-test".to_string(),
9345 has_runtime_store: true,
9346 durability_policy: DurabilityPolicy::SyncWriteThrough,
9347 bridge: None,
9348 default_timeout: None,
9349 });
9350 let identity = AgentIdentity::parse(durable_identity)?;
9351 identity_rt
9352 .register(
9353 rpc_durable_spec(durable_identity, "worker"),
9354 IdentityLifecycleState::Active,
9355 Some(ContinuityRecord {
9356 identity: identity.clone(),
9357 agent_runtime_id: AgentRuntimeId::parse(runtime_alias)?,
9358 session_id: meerkat_core::types::SessionId::new(),
9359 generation: ContinuityGeneration::new(0),
9360 checkpoint_version: CheckpointVersion::new(0),
9361 }),
9362 Some(LeaseGrant {
9363 identity: identity.clone(),
9364 fencing_token: FencingToken::new(1),
9365 ttl: Duration::from_mins(1),
9366 }),
9367 )
9368 .await;
9369
9370 let target =
9371 resolve_rpc_identity_control_target(&runtime, &identity_rt, durable_identity).await?;
9372 assert!(
9373 target.was_registered,
9374 "resolution must capture durable ownership"
9375 );
9376 assert!(target.live.is_some(), "the race requires a live projection");
9377
9378 identity_rt
9379 .remove(&identity)
9380 .await
9381 .expect("simulate a concurrent durable delete after resolution");
9382 assert!(matches!(
9383 identity_rt.status(&identity).await,
9384 Err(crate::identity_first::IdentityRuntimeError::UnknownIdentity(_))
9385 ));
9386 assert!(
9387 !super::rpc_live_only_fallback_allowed(&target, durable_identity),
9388 "a later UnknownIdentity must preserve the observed durable delete"
9389 );
9390
9391 let mut live_only = target.clone();
9392 live_only.was_registered = false;
9393 assert!(
9394 !super::rpc_live_only_fallback_allowed(&live_only, durable_identity),
9395 "a resolved generated alias must remain identity-owned even when registration vanished"
9396 );
9397 assert!(
9398 !super::rpc_live_only_fallback_allowed(&live_only, runtime_alias),
9399 "generated aliases never enter raw/live-only fallback"
9400 );
9401 live_only
9402 .live
9403 .as_mut()
9404 .expect("live projection")
9405 .runtime_member_id = "legacy-review-member".to_string();
9406 assert!(
9407 super::rpc_live_only_fallback_allowed(&live_only, durable_identity),
9408 "genuine bare live-only identities retain compatibility fallback"
9409 );
9410
9411 let _ = runtime.mob_handle().stop().await;
9412 Ok(())
9413 }
9414
9415 #[tokio::test]
9419 async fn retire_member_current_runtime_alias_uses_identity_authority()
9420 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9421 let temp_dir = tempfile::tempdir()?;
9422 let runtime = Box::pin(
9423 UnifiedRuntime::builder()
9424 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9425 .module_config(MobKitConfig {
9426 modules: Vec::new(),
9427 discovery: DiscoverySpec {
9428 namespace: "rpc-owned-retire-test".to_string(),
9429 modules: Vec::new(),
9430 },
9431 pre_spawn: Vec::new(),
9432 })
9433 .timeout(Duration::from_secs(5))
9434 .build(),
9435 )
9436 .await?;
9437 let runtime_alias = "rt:review:owned:0";
9438 spawn_identity_projection_fixture(
9439 &runtime,
9440 SpawnMemberSpec::from_wire(
9441 "worker".to_string(),
9442 runtime_alias.to_string(),
9443 None,
9444 None,
9445 None,
9446 ),
9447 )
9448 .await?;
9449
9450 let continuity_store = Arc::new(LocalContinuityStore::in_memory()?);
9451 let identity_rt = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
9452 continuity_store,
9453 lease_provider: Arc::new(LocalLeaseProvider::new()),
9454 runtime_instance_id: "rpc-owned-retire-test".to_string(),
9455 has_runtime_store: true,
9456 durability_policy: DurabilityPolicy::SyncWriteThrough,
9457 bridge: None,
9458 default_timeout: None,
9459 }));
9460 let identity = AgentIdentity::parse("review:owned")?;
9461 identity_rt
9462 .register(
9463 DurableAgentSpec {
9464 identity: identity.clone(),
9465 profile: meerkat_mob::ProfileName::from("worker"),
9466 addressability: AgentAddressability::Addressable,
9467 display_name: None,
9468 labels: BTreeMap::new(),
9469 context: None,
9470 additional_instructions: Vec::new(),
9471 initial_message: None,
9472 runtime_mode_override: None,
9473 backend: None,
9474 binding: None,
9475 },
9476 IdentityLifecycleState::Active,
9477 Some(ContinuityRecord {
9478 identity: identity.clone(),
9479 agent_runtime_id: AgentRuntimeId::parse(runtime_alias)?,
9480 session_id: meerkat_core::types::SessionId::new(),
9481 generation: ContinuityGeneration::new(0),
9482 checkpoint_version: CheckpointVersion::new(0),
9483 }),
9484 Some(LeaseGrant {
9485 identity: identity.clone(),
9486 fencing_token: FencingToken::new(1),
9487 ttl: Duration::from_mins(1),
9488 }),
9489 )
9490 .await;
9491 let identity_ctx = IdentityFirstContext {
9492 runtime: Arc::clone(&identity_rt),
9493 roster_provider: Arc::new(EmptyRosterProvider),
9494 topology_provider: None,
9495 customizer: None,
9496 agent_memory_provider: None,
9497 mob_definition: None,
9498 };
9499
9500 let raw = handle_unified_rpc_json(
9501 &runtime,
9502 &json!({
9503 "jsonrpc": "2.0",
9504 "id": 1,
9505 "method": "mobkit/retire_member",
9506 "params": { "member_id": runtime_alias },
9507 })
9508 .to_string(),
9509 Duration::from_secs(5),
9510 None,
9511 Some(&identity_ctx),
9512 )
9513 .await;
9514 let response: Value = serde_json::from_str(&raw)?;
9515 assert!(
9516 response["error"].is_null(),
9517 "identity-owned retire_member failed: {response:#?}"
9518 );
9519 assert_eq!(response["result"]["identity_first"], json!(true));
9520 assert_eq!(
9521 identity_rt.status(&identity).await?.state,
9522 IdentityLifecycleState::Retiring,
9523 "the durable authority, not only the Mob projection, must observe retirement"
9524 );
9525 assert!(
9526 runtime
9527 .mob_handle()
9528 .list_members_including_retiring()
9529 .await
9530 .iter()
9531 .any(|entry| {
9532 crate::member_comms_id::runtime_alias_str(entry.agent_identity.as_str())
9533 == runtime_alias
9534 }),
9535 "a bridge-less identity test proves the raw Mob fallback was not invoked"
9536 );
9537
9538 Ok(())
9539 }
9540
9541 #[tokio::test]
9550 async fn retire_and_respawn_rpcs_succeed_for_idle_member()
9551 -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9552 let temp_dir = tempfile::tempdir()?;
9553 let runtime = Box::pin(
9554 UnifiedRuntime::builder()
9555 .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9556 .module_config(MobKitConfig {
9557 modules: Vec::new(),
9558 discovery: DiscoverySpec {
9559 namespace: "rpc-idle-lifecycle-test".to_string(),
9560 modules: Vec::new(),
9561 },
9562 pre_spawn: Vec::new(),
9563 })
9564 .timeout(Duration::from_secs(5))
9565 .build(),
9566 )
9567 .await?;
9568
9569 for member in ["worker-one", "worker-two"] {
9570 runtime
9571 .spawn(SpawnMemberSpec::from_wire(
9572 "worker".to_string(),
9573 member.to_string(),
9574 None,
9575 None,
9576 None,
9577 ))
9578 .await?;
9579 }
9580
9581 let send = |id: u64, method: &'static str, params: Value| {
9582 let runtime = &runtime;
9583 async move {
9584 let raw = handle_unified_rpc_json(
9585 runtime,
9586 &json!({
9587 "jsonrpc": "2.0",
9588 "id": id,
9589 "method": method,
9590 "params": params,
9591 })
9592 .to_string(),
9593 Duration::from_secs(10),
9594 None,
9595 None,
9596 )
9597 .await;
9598 serde_json::from_str::<Value>(&raw)
9599 }
9600 };
9601
9602 let response = send(
9604 1,
9605 "mobkit/retire_member",
9606 json!({"member_id": "worker-one"}),
9607 )
9608 .await?;
9609 assert!(
9610 response["error"].is_null(),
9611 "retire_member must succeed for an idle member: {response:#?}"
9612 );
9613 assert_eq!(response["result"]["accepted"], json!(true));
9614 assert!(
9615 !runtime
9616 .mob_handle()
9617 .list_members_including_retiring()
9618 .await
9619 .iter()
9620 .any(|entry| entry.agent_identity.as_str() == "worker-one"),
9621 "retired member must leave the roster"
9622 );
9623
9624 let response = send(
9627 2,
9628 "mobkit/respawn_member",
9629 json!({"member_id": "worker-two"}),
9630 )
9631 .await?;
9632 assert!(
9633 response["error"].is_null(),
9634 "respawn_member must succeed for an idle member: {response:#?}"
9635 );
9636 assert_eq!(response["result"]["accepted"], json!(true));
9637 let members = runtime.mob_handle().list_members_including_retiring().await;
9638 let worker_two = members
9639 .iter()
9640 .find(|entry| entry.agent_identity.as_str() == "worker-two")
9641 .expect("respawned member must remain in the roster");
9642 assert_eq!(
9643 worker_two.status,
9644 meerkat_mob::MobMemberStatus::Active,
9645 "respawned member must be active, not wedged in retiring"
9646 );
9647
9648 Ok(())
9649 }
9650}