omnibase_infra 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (675) hide show
  1. omnibase_infra/__init__.py +101 -0
  2. omnibase_infra/cli/__init__.py +1 -0
  3. omnibase_infra/cli/commands.py +216 -0
  4. omnibase_infra/clients/__init__.py +0 -0
  5. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +261 -0
  6. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +138 -0
  7. omnibase_infra/decorators/__init__.py +29 -0
  8. omnibase_infra/decorators/allow_any.py +109 -0
  9. omnibase_infra/dlq/__init__.py +90 -0
  10. omnibase_infra/dlq/constants_dlq.py +57 -0
  11. omnibase_infra/dlq/models/__init__.py +26 -0
  12. omnibase_infra/dlq/models/enum_replay_status.py +37 -0
  13. omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
  14. omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
  15. omnibase_infra/dlq/service_dlq_tracking.py +611 -0
  16. omnibase_infra/enums/__init__.py +123 -0
  17. omnibase_infra/enums/enum_any_type_violation.py +104 -0
  18. omnibase_infra/enums/enum_backend_type.py +27 -0
  19. omnibase_infra/enums/enum_capture_outcome.py +42 -0
  20. omnibase_infra/enums/enum_capture_state.py +88 -0
  21. omnibase_infra/enums/enum_chain_violation_type.py +119 -0
  22. omnibase_infra/enums/enum_circuit_state.py +51 -0
  23. omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
  24. omnibase_infra/enums/enum_contract_type.py +84 -0
  25. omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
  26. omnibase_infra/enums/enum_dispatch_status.py +191 -0
  27. omnibase_infra/enums/enum_environment.py +46 -0
  28. omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
  29. omnibase_infra/enums/enum_handler_error_type.py +101 -0
  30. omnibase_infra/enums/enum_handler_loader_error.py +178 -0
  31. omnibase_infra/enums/enum_handler_source_type.py +87 -0
  32. omnibase_infra/enums/enum_handler_type.py +77 -0
  33. omnibase_infra/enums/enum_handler_type_category.py +61 -0
  34. omnibase_infra/enums/enum_infra_transport_type.py +73 -0
  35. omnibase_infra/enums/enum_introspection_reason.py +154 -0
  36. omnibase_infra/enums/enum_message_category.py +213 -0
  37. omnibase_infra/enums/enum_node_archetype.py +74 -0
  38. omnibase_infra/enums/enum_node_output_type.py +185 -0
  39. omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
  40. omnibase_infra/enums/enum_policy_type.py +32 -0
  41. omnibase_infra/enums/enum_registration_state.py +261 -0
  42. omnibase_infra/enums/enum_registration_status.py +33 -0
  43. omnibase_infra/enums/enum_registry_response_status.py +28 -0
  44. omnibase_infra/enums/enum_response_status.py +26 -0
  45. omnibase_infra/enums/enum_retry_error_category.py +98 -0
  46. omnibase_infra/enums/enum_security_rule_id.py +103 -0
  47. omnibase_infra/enums/enum_selection_strategy.py +91 -0
  48. omnibase_infra/enums/enum_topic_standard.py +42 -0
  49. omnibase_infra/enums/enum_validation_severity.py +78 -0
  50. omnibase_infra/errors/__init__.py +156 -0
  51. omnibase_infra/errors/error_architecture_violation.py +152 -0
  52. omnibase_infra/errors/error_chain_propagation.py +188 -0
  53. omnibase_infra/errors/error_compute_registry.py +92 -0
  54. omnibase_infra/errors/error_consul.py +132 -0
  55. omnibase_infra/errors/error_container_wiring.py +243 -0
  56. omnibase_infra/errors/error_event_bus_registry.py +102 -0
  57. omnibase_infra/errors/error_infra.py +608 -0
  58. omnibase_infra/errors/error_message_type_registry.py +101 -0
  59. omnibase_infra/errors/error_policy_registry.py +112 -0
  60. omnibase_infra/errors/error_vault.py +123 -0
  61. omnibase_infra/event_bus/__init__.py +72 -0
  62. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +86 -0
  63. omnibase_infra/event_bus/event_bus_inmemory.py +743 -0
  64. omnibase_infra/event_bus/event_bus_kafka.py +1658 -0
  65. omnibase_infra/event_bus/mixin_kafka_broadcast.py +184 -0
  66. omnibase_infra/event_bus/mixin_kafka_dlq.py +765 -0
  67. omnibase_infra/event_bus/models/__init__.py +29 -0
  68. omnibase_infra/event_bus/models/config/__init__.py +20 -0
  69. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +725 -0
  70. omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
  71. omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
  72. omnibase_infra/event_bus/models/model_event_headers.py +115 -0
  73. omnibase_infra/event_bus/models/model_event_message.py +60 -0
  74. omnibase_infra/event_bus/topic_constants.py +376 -0
  75. omnibase_infra/handlers/__init__.py +75 -0
  76. omnibase_infra/handlers/filesystem/__init__.py +48 -0
  77. omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
  78. omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
  79. omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
  80. omnibase_infra/handlers/handler_consul.py +787 -0
  81. omnibase_infra/handlers/handler_db.py +1039 -0
  82. omnibase_infra/handlers/handler_filesystem.py +1478 -0
  83. omnibase_infra/handlers/handler_graph.py +1154 -0
  84. omnibase_infra/handlers/handler_http.py +920 -0
  85. omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
  86. omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
  87. omnibase_infra/handlers/handler_mcp.py +748 -0
  88. omnibase_infra/handlers/handler_qdrant.py +1076 -0
  89. omnibase_infra/handlers/handler_vault.py +422 -0
  90. omnibase_infra/handlers/mcp/__init__.py +19 -0
  91. omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
  92. omnibase_infra/handlers/mcp/protocols.py +178 -0
  93. omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
  94. omnibase_infra/handlers/mixins/__init__.py +42 -0
  95. omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
  96. omnibase_infra/handlers/mixins/mixin_consul_kv.py +337 -0
  97. omnibase_infra/handlers/mixins/mixin_consul_service.py +277 -0
  98. omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
  99. omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
  100. omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
  101. omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
  102. omnibase_infra/handlers/models/__init__.py +286 -0
  103. omnibase_infra/handlers/models/consul/__init__.py +81 -0
  104. omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
  105. omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
  106. omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
  107. omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
  108. omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
  109. omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
  110. omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
  111. omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
  112. omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
  113. omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
  114. omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
  115. omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
  116. omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
  117. omnibase_infra/handlers/models/graph/__init__.py +35 -0
  118. omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
  119. omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
  120. omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
  121. omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
  122. omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
  123. omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
  124. omnibase_infra/handlers/models/http/__init__.py +50 -0
  125. omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
  126. omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
  127. omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
  128. omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
  129. omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
  130. omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
  131. omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
  132. omnibase_infra/handlers/models/mcp/__init__.py +23 -0
  133. omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
  134. omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
  135. omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
  136. omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
  137. omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
  138. omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
  139. omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
  140. omnibase_infra/handlers/models/model_db_query_response.py +60 -0
  141. omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
  142. omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
  143. omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
  144. omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
  145. omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
  146. omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
  147. omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
  148. omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
  149. omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
  150. omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
  151. omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
  152. omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
  153. omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
  154. omnibase_infra/handlers/models/model_handler_response.py +103 -0
  155. omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
  156. omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
  157. omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
  158. omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
  159. omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
  160. omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
  161. omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
  162. omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
  163. omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
  164. omnibase_infra/handlers/models/model_operation_context.py +187 -0
  165. omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
  166. omnibase_infra/handlers/models/model_retry_state.py +162 -0
  167. omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
  168. omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
  169. omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
  170. omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
  171. omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
  172. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
  173. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
  174. omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
  175. omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
  176. omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
  177. omnibase_infra/handlers/models/vault/__init__.py +69 -0
  178. omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
  179. omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
  180. omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
  181. omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
  182. omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
  183. omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
  184. omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
  185. omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
  186. omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
  187. omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
  188. omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
  189. omnibase_infra/handlers/registration_storage/__init__.py +43 -0
  190. omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
  191. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +915 -0
  192. omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
  193. omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
  194. omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
  195. omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
  196. omnibase_infra/handlers/service_discovery/__init__.py +43 -0
  197. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +747 -0
  198. omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
  199. omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
  200. omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
  201. omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
  202. omnibase_infra/handlers/service_discovery/models/model_service_info.py +99 -0
  203. omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
  204. omnibase_infra/idempotency/__init__.py +94 -0
  205. omnibase_infra/idempotency/models/__init__.py +43 -0
  206. omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
  207. omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
  208. omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
  209. omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
  210. omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
  211. omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
  212. omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
  213. omnibase_infra/idempotency/store_inmemory.py +265 -0
  214. omnibase_infra/idempotency/store_postgres.py +923 -0
  215. omnibase_infra/infrastructure/__init__.py +0 -0
  216. omnibase_infra/mixins/__init__.py +71 -0
  217. omnibase_infra/mixins/mixin_async_circuit_breaker.py +655 -0
  218. omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
  219. omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
  220. omnibase_infra/mixins/mixin_node_introspection.py +2465 -0
  221. omnibase_infra/mixins/mixin_retry_execution.py +386 -0
  222. omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
  223. omnibase_infra/models/__init__.py +136 -0
  224. omnibase_infra/models/corpus/__init__.py +17 -0
  225. omnibase_infra/models/corpus/model_capture_config.py +133 -0
  226. omnibase_infra/models/corpus/model_capture_result.py +86 -0
  227. omnibase_infra/models/discovery/__init__.py +42 -0
  228. omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
  229. omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
  230. omnibase_infra/models/discovery/model_introspection_config.py +311 -0
  231. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
  232. omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
  233. omnibase_infra/models/dispatch/__init__.py +147 -0
  234. omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
  235. omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
  236. omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
  237. omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
  238. omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
  239. omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
  240. omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
  241. omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
  242. omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
  243. omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
  244. omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
  245. omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
  246. omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
  247. omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
  248. omnibase_infra/models/errors/__init__.py +45 -0
  249. omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
  250. omnibase_infra/models/errors/model_infra_error_context.py +99 -0
  251. omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
  252. omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
  253. omnibase_infra/models/handlers/__init__.py +37 -0
  254. omnibase_infra/models/handlers/model_contract_discovery_result.py +80 -0
  255. omnibase_infra/models/handlers/model_handler_descriptor.py +185 -0
  256. omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
  257. omnibase_infra/models/health/__init__.py +9 -0
  258. omnibase_infra/models/health/model_health_check_result.py +40 -0
  259. omnibase_infra/models/lifecycle/__init__.py +39 -0
  260. omnibase_infra/models/logging/__init__.py +51 -0
  261. omnibase_infra/models/logging/model_log_context.py +756 -0
  262. omnibase_infra/models/model_retry_error_classification.py +78 -0
  263. omnibase_infra/models/projection/__init__.py +43 -0
  264. omnibase_infra/models/projection/model_capability_fields.py +112 -0
  265. omnibase_infra/models/projection/model_registration_projection.py +434 -0
  266. omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
  267. omnibase_infra/models/projection/model_sequence_info.py +182 -0
  268. omnibase_infra/models/projection/model_snapshot_topic_config.py +590 -0
  269. omnibase_infra/models/projectors/__init__.py +41 -0
  270. omnibase_infra/models/projectors/model_projector_column.py +289 -0
  271. omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
  272. omnibase_infra/models/projectors/model_projector_index.py +270 -0
  273. omnibase_infra/models/projectors/model_projector_schema.py +415 -0
  274. omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
  275. omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
  276. omnibase_infra/models/registration/__init__.py +59 -0
  277. omnibase_infra/models/registration/commands/__init__.py +15 -0
  278. omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
  279. omnibase_infra/models/registration/events/__init__.py +56 -0
  280. omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
  281. omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
  282. omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
  283. omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
  284. omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
  285. omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
  286. omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
  287. omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
  288. omnibase_infra/models/registration/model_node_capabilities.py +179 -0
  289. omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
  290. omnibase_infra/models/registration/model_node_introspection_event.py +175 -0
  291. omnibase_infra/models/registration/model_node_metadata.py +79 -0
  292. omnibase_infra/models/registration/model_node_registration.py +162 -0
  293. omnibase_infra/models/registration/model_node_registration_record.py +162 -0
  294. omnibase_infra/models/registry/__init__.py +29 -0
  295. omnibase_infra/models/registry/model_domain_constraint.py +202 -0
  296. omnibase_infra/models/registry/model_message_type_entry.py +271 -0
  297. omnibase_infra/models/resilience/__init__.py +9 -0
  298. omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
  299. omnibase_infra/models/routing/__init__.py +25 -0
  300. omnibase_infra/models/routing/model_routing_entry.py +52 -0
  301. omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
  302. omnibase_infra/models/runtime/__init__.py +40 -0
  303. omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
  304. omnibase_infra/models/runtime/model_discovery_error.py +81 -0
  305. omnibase_infra/models/runtime/model_discovery_result.py +162 -0
  306. omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
  307. omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
  308. omnibase_infra/models/runtime/model_handler_contract.py +280 -0
  309. omnibase_infra/models/runtime/model_loaded_handler.py +120 -0
  310. omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
  311. omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
  312. omnibase_infra/models/security/__init__.py +50 -0
  313. omnibase_infra/models/security/classification_levels.py +99 -0
  314. omnibase_infra/models/security/model_environment_policy.py +145 -0
  315. omnibase_infra/models/security/model_handler_security_policy.py +107 -0
  316. omnibase_infra/models/security/model_security_error.py +81 -0
  317. omnibase_infra/models/security/model_security_validation_result.py +328 -0
  318. omnibase_infra/models/security/model_security_warning.py +67 -0
  319. omnibase_infra/models/snapshot/__init__.py +27 -0
  320. omnibase_infra/models/snapshot/model_field_change.py +65 -0
  321. omnibase_infra/models/snapshot/model_snapshot.py +270 -0
  322. omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
  323. omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
  324. omnibase_infra/models/types/__init__.py +71 -0
  325. omnibase_infra/models/validation/__init__.py +89 -0
  326. omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
  327. omnibase_infra/models/validation/model_any_type_violation.py +141 -0
  328. omnibase_infra/models/validation/model_category_match_result.py +345 -0
  329. omnibase_infra/models/validation/model_chain_violation.py +166 -0
  330. omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
  331. omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
  332. omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
  333. omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
  334. omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
  335. omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
  336. omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
  337. omnibase_infra/models/validation/model_output_validation_params.py +74 -0
  338. omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
  339. omnibase_infra/models/validation/model_validation_error_params.py +84 -0
  340. omnibase_infra/models/validation/model_validation_outcome.py +287 -0
  341. omnibase_infra/nodes/__init__.py +48 -0
  342. omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
  343. omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
  344. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +208 -0
  345. omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
  346. omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
  347. omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
  348. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
  349. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
  350. omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
  351. omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
  352. omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
  353. omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
  354. omnibase_infra/nodes/architecture_validator/node.py +262 -0
  355. omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
  356. omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
  357. omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
  358. omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
  359. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +99 -0
  360. omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
  361. omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
  362. omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
  363. omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
  364. omnibase_infra/nodes/effects/README.md +358 -0
  365. omnibase_infra/nodes/effects/__init__.py +26 -0
  366. omnibase_infra/nodes/effects/contract.yaml +172 -0
  367. omnibase_infra/nodes/effects/models/__init__.py +32 -0
  368. omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
  369. omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
  370. omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
  371. omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
  372. omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
  373. omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
  374. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
  375. omnibase_infra/nodes/effects/registry_effect.py +525 -0
  376. omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
  377. omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
  378. omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
  379. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +475 -0
  380. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
  381. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
  382. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
  383. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
  384. omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
  385. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
  386. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +609 -0
  387. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
  388. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
  389. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
  390. omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
  391. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
  392. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
  393. omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
  394. omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
  395. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
  396. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
  397. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
  398. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
  399. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
  400. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
  401. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
  402. omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
  403. omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
  404. omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
  405. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
  406. omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
  407. omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
  408. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +525 -0
  409. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +392 -0
  410. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +742 -0
  411. omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
  412. omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
  413. omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
  414. omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
  415. omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
  416. omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
  417. omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
  418. omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
  419. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +225 -0
  420. omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
  421. omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
  422. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
  423. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
  424. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
  425. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
  426. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
  427. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
  428. omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
  429. omnibase_infra/nodes/node_registration_storage_effect/node.py +109 -0
  430. omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
  431. omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
  432. omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
  433. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +194 -0
  434. omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
  435. omnibase_infra/nodes/node_registry_effect/contract.yaml +682 -0
  436. omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
  437. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
  438. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
  439. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +416 -0
  440. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
  441. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
  442. omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
  443. omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
  444. omnibase_infra/nodes/node_registry_effect/node.py +165 -0
  445. omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
  446. omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
  447. omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
  448. omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
  449. omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
  450. omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
  451. omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
  452. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
  453. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
  454. omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
  455. omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
  456. omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
  457. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
  458. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
  459. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
  460. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
  461. omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
  462. omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
  463. omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
  464. omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
  465. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +214 -0
  466. omnibase_infra/nodes/reducers/__init__.py +30 -0
  467. omnibase_infra/nodes/reducers/models/__init__.py +32 -0
  468. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +76 -0
  469. omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
  470. omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
  471. omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
  472. omnibase_infra/nodes/reducers/registration_reducer.py +1137 -0
  473. omnibase_infra/observability/__init__.py +143 -0
  474. omnibase_infra/observability/constants_metrics.py +91 -0
  475. omnibase_infra/observability/factory_observability_sink.py +525 -0
  476. omnibase_infra/observability/handlers/__init__.py +118 -0
  477. omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
  478. omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
  479. omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
  480. omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
  481. omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
  482. omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
  483. omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
  484. omnibase_infra/observability/hooks/__init__.py +74 -0
  485. omnibase_infra/observability/hooks/hook_observability.py +1223 -0
  486. omnibase_infra/observability/models/__init__.py +30 -0
  487. omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
  488. omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
  489. omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
  490. omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
  491. omnibase_infra/observability/sinks/__init__.py +69 -0
  492. omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
  493. omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
  494. omnibase_infra/plugins/__init__.py +27 -0
  495. omnibase_infra/plugins/examples/__init__.py +28 -0
  496. omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
  497. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
  498. omnibase_infra/plugins/models/__init__.py +21 -0
  499. omnibase_infra/plugins/models/model_plugin_context.py +76 -0
  500. omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
  501. omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
  502. omnibase_infra/plugins/plugin_compute_base.py +435 -0
  503. omnibase_infra/projectors/__init__.py +30 -0
  504. omnibase_infra/projectors/contracts/__init__.py +63 -0
  505. omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
  506. omnibase_infra/projectors/projection_reader_registration.py +1559 -0
  507. omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
  508. omnibase_infra/protocols/__init__.py +99 -0
  509. omnibase_infra/protocols/protocol_capability_projection.py +253 -0
  510. omnibase_infra/protocols/protocol_capability_query.py +251 -0
  511. omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
  512. omnibase_infra/protocols/protocol_event_projector.py +96 -0
  513. omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
  514. omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
  515. omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
  516. omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
  517. omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
  518. omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
  519. omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
  520. omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
  521. omnibase_infra/runtime/__init__.py +296 -0
  522. omnibase_infra/runtime/binding_config_resolver.py +2706 -0
  523. omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
  524. omnibase_infra/runtime/contract_handler_discovery.py +582 -0
  525. omnibase_infra/runtime/contract_loaders/__init__.py +42 -0
  526. omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
  527. omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
  528. omnibase_infra/runtime/enums/__init__.py +18 -0
  529. omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
  530. omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
  531. omnibase_infra/runtime/envelope_validator.py +179 -0
  532. omnibase_infra/runtime/handler_contract_source.py +669 -0
  533. omnibase_infra/runtime/handler_plugin_loader.py +2029 -0
  534. omnibase_infra/runtime/handler_registry.py +321 -0
  535. omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
  536. omnibase_infra/runtime/kernel.py +40 -0
  537. omnibase_infra/runtime/mixin_policy_validation.py +522 -0
  538. omnibase_infra/runtime/mixin_semver_cache.py +378 -0
  539. omnibase_infra/runtime/mixins/__init__.py +17 -0
  540. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +757 -0
  541. omnibase_infra/runtime/models/__init__.py +192 -0
  542. omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
  543. omnibase_infra/runtime/models/model_binding_config.py +168 -0
  544. omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
  545. omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
  546. omnibase_infra/runtime/models/model_cached_secret.py +138 -0
  547. omnibase_infra/runtime/models/model_compute_key.py +138 -0
  548. omnibase_infra/runtime/models/model_compute_registration.py +97 -0
  549. omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
  550. omnibase_infra/runtime/models/model_config_ref.py +331 -0
  551. omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
  552. omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
  553. omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
  554. omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
  555. omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
  556. omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
  557. omnibase_infra/runtime/models/model_failed_component.py +55 -0
  558. omnibase_infra/runtime/models/model_health_check_response.py +168 -0
  559. omnibase_infra/runtime/models/model_health_check_result.py +228 -0
  560. omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
  561. omnibase_infra/runtime/models/model_logging_config.py +42 -0
  562. omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
  563. omnibase_infra/runtime/models/model_optional_string.py +94 -0
  564. omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
  565. omnibase_infra/runtime/models/model_policy_context.py +100 -0
  566. omnibase_infra/runtime/models/model_policy_key.py +138 -0
  567. omnibase_infra/runtime/models/model_policy_registration.py +139 -0
  568. omnibase_infra/runtime/models/model_policy_result.py +103 -0
  569. omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
  570. omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
  571. omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
  572. omnibase_infra/runtime/models/model_retry_policy.py +105 -0
  573. omnibase_infra/runtime/models/model_runtime_config.py +150 -0
  574. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +624 -0
  575. omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
  576. omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
  577. omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
  578. omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
  579. omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
  580. omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
  581. omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
  582. omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
  583. omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
  584. omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
  585. omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
  586. omnibase_infra/runtime/projector_schema_manager.py +565 -0
  587. omnibase_infra/runtime/projector_shell.py +1102 -0
  588. omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
  589. omnibase_infra/runtime/protocol_contract_source.py +92 -0
  590. omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
  591. omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
  592. omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
  593. omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
  594. omnibase_infra/runtime/protocol_policy.py +366 -0
  595. omnibase_infra/runtime/protocols/__init__.py +27 -0
  596. omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
  597. omnibase_infra/runtime/registry/__init__.py +93 -0
  598. omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
  599. omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
  600. omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
  601. omnibase_infra/runtime/registry/registry_message_type.py +542 -0
  602. omnibase_infra/runtime/registry/registry_protocol_binding.py +444 -0
  603. omnibase_infra/runtime/registry_compute.py +1143 -0
  604. omnibase_infra/runtime/registry_dispatcher.py +678 -0
  605. omnibase_infra/runtime/registry_policy.py +1502 -0
  606. omnibase_infra/runtime/runtime_scheduler.py +1070 -0
  607. omnibase_infra/runtime/secret_resolver.py +2110 -0
  608. omnibase_infra/runtime/security_metadata_validator.py +776 -0
  609. omnibase_infra/runtime/service_kernel.py +1573 -0
  610. omnibase_infra/runtime/service_message_dispatch_engine.py +1805 -0
  611. omnibase_infra/runtime/service_runtime_host_process.py +2260 -0
  612. omnibase_infra/runtime/util_container_wiring.py +1123 -0
  613. omnibase_infra/runtime/util_validation.py +314 -0
  614. omnibase_infra/runtime/util_version.py +98 -0
  615. omnibase_infra/runtime/util_wiring.py +566 -0
  616. omnibase_infra/schemas/schema_registration_projection.sql +320 -0
  617. omnibase_infra/services/__init__.py +68 -0
  618. omnibase_infra/services/corpus_capture.py +678 -0
  619. omnibase_infra/services/service_capability_query.py +945 -0
  620. omnibase_infra/services/service_health.py +897 -0
  621. omnibase_infra/services/service_node_selector.py +530 -0
  622. omnibase_infra/services/service_timeout_emitter.py +682 -0
  623. omnibase_infra/services/service_timeout_scanner.py +390 -0
  624. omnibase_infra/services/snapshot/__init__.py +31 -0
  625. omnibase_infra/services/snapshot/service_snapshot.py +647 -0
  626. omnibase_infra/services/snapshot/store_inmemory.py +637 -0
  627. omnibase_infra/services/snapshot/store_postgres.py +1279 -0
  628. omnibase_infra/shared/__init__.py +8 -0
  629. omnibase_infra/testing/__init__.py +10 -0
  630. omnibase_infra/testing/utils.py +23 -0
  631. omnibase_infra/types/__init__.py +48 -0
  632. omnibase_infra/types/type_cache_info.py +49 -0
  633. omnibase_infra/types/type_dsn.py +173 -0
  634. omnibase_infra/types/type_infra_aliases.py +60 -0
  635. omnibase_infra/types/typed_dict/__init__.py +21 -0
  636. omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
  637. omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
  638. omnibase_infra/types/typed_dict_capabilities.py +64 -0
  639. omnibase_infra/utils/__init__.py +89 -0
  640. omnibase_infra/utils/correlation.py +208 -0
  641. omnibase_infra/utils/util_datetime.py +372 -0
  642. omnibase_infra/utils/util_dsn_validation.py +333 -0
  643. omnibase_infra/utils/util_env_parsing.py +264 -0
  644. omnibase_infra/utils/util_error_sanitization.py +457 -0
  645. omnibase_infra/utils/util_pydantic_validators.py +477 -0
  646. omnibase_infra/utils/util_semver.py +233 -0
  647. omnibase_infra/validation/__init__.py +307 -0
  648. omnibase_infra/validation/enums/__init__.py +11 -0
  649. omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
  650. omnibase_infra/validation/infra_validators.py +1486 -0
  651. omnibase_infra/validation/linter_contract.py +907 -0
  652. omnibase_infra/validation/mixin_any_type_classification.py +120 -0
  653. omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
  654. omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
  655. omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
  656. omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
  657. omnibase_infra/validation/models/__init__.py +15 -0
  658. omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
  659. omnibase_infra/validation/models/model_contract_violation.py +41 -0
  660. omnibase_infra/validation/service_validation_aggregator.py +395 -0
  661. omnibase_infra/validation/validation_exemptions.yaml +1710 -0
  662. omnibase_infra/validation/validator_any_type.py +715 -0
  663. omnibase_infra/validation/validator_chain_propagation.py +839 -0
  664. omnibase_infra/validation/validator_execution_shape.py +465 -0
  665. omnibase_infra/validation/validator_localhandler.py +261 -0
  666. omnibase_infra/validation/validator_registration_security.py +410 -0
  667. omnibase_infra/validation/validator_routing_coverage.py +1020 -0
  668. omnibase_infra/validation/validator_runtime_shape.py +915 -0
  669. omnibase_infra/validation/validator_security.py +410 -0
  670. omnibase_infra/validation/validator_topic_category.py +1152 -0
  671. omnibase_infra-0.2.1.dist-info/METADATA +197 -0
  672. omnibase_infra-0.2.1.dist-info/RECORD +675 -0
  673. omnibase_infra-0.2.1.dist-info/WHEEL +4 -0
  674. omnibase_infra-0.2.1.dist-info/entry_points.txt +4 -0
  675. omnibase_infra-0.2.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2465 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ # ruff: noqa: G201
4
+ # G201 disabled: Logging extra dict is intentional for structured logging with correlation IDs
5
+ """Node introspection mixin providing automatic capability discovery.
6
+
7
+ This module provides a reusable mixin for ONEX nodes to implement automatic
8
+ capability discovery, endpoint reporting, and periodic heartbeat broadcasting.
9
+ It uses reflection to discover node capabilities and integrates with the event
10
+ bus for distributed service discovery.
11
+
12
+ Features:
13
+ - Automatic capability discovery via reflection
14
+ - Endpoint URL discovery (health, api, metrics)
15
+ - FSM state reporting if applicable
16
+ - Cached introspection data with configurable TTL
17
+ - Background heartbeat task for periodic health broadcasts
18
+ - Registry listener for REQUEST_INTROSPECTION events
19
+ - Graceful degradation when event bus is unavailable
20
+
21
+ Note:
22
+ - active_operations_count in heartbeats is tracked via ``track_operation()``
23
+ context manager. Nodes should wrap their operations with this context
24
+ manager to accurately report concurrent operation counts.
25
+
26
+ - **track_operation() Usage Guidelines**:
27
+
28
+ Within MixinNodeIntrospection itself, only ``publish_introspection()`` uses
29
+ ``track_operation()``. This is intentional for the following reasons:
30
+
31
+ 1. **_publish_heartbeat()**: Explicitly excluded because it's an internal
32
+ background task. Tracking it would cause self-referential counting
33
+ (heartbeat counting itself as active) and would report infrastructure
34
+ overhead rather than business load.
35
+
36
+ 2. **get_introspection_data()**: Called by ``publish_introspection()``, which
37
+ already wraps the entire operation. Adding tracking here would cause
38
+ double-counting. Additionally, this is metadata gathering, not a business
39
+ operation that represents node load.
40
+
41
+ 3. **start/stop_introspection_tasks()**: One-time lifecycle operations that
42
+ complete quickly. They spawn/cancel background tasks but don't represent
43
+ ongoing load. The counter would increment and immediately decrement.
44
+
45
+ 4. **get_capabilities(), get_endpoints(), get_current_state()**: Internal
46
+ metadata operations that are part of introspection data gathering, not
47
+ independent business operations.
48
+
49
+ **For consuming nodes**: Use ``track_operation()`` in your business methods
50
+ (e.g., ``execute_query()``, ``process_request()``, ``handle_event()``) to
51
+ accurately report concurrent operation counts in heartbeats. See the
52
+ ``track_operation()`` docstring for usage examples.
53
+
54
+ Security Considerations:
55
+ This mixin uses Python reflection (via the ``inspect`` module) to automatically
56
+ discover node capabilities. While this enables powerful service discovery, it
57
+ has security implications that developers must understand.
58
+
59
+ **Threat Model**:
60
+
61
+ Introspection data could be valuable to an attacker for:
62
+
63
+ - **Reconnaissance**: Learning what operations a node supports to identify
64
+ attack vectors (e.g., discovering ``decrypt_*``, ``admin_*`` methods).
65
+ - **Architecture mapping**: Understanding system topology through protocol
66
+ and mixin discovery (e.g., which nodes implement ``ProtocolDatabaseAdapter``).
67
+ - **Version fingerprinting**: Identifying outdated versions with known
68
+ vulnerabilities via the ``version`` field.
69
+ - **State inference**: Deducing system state or health from FSM state values.
70
+
71
+ **What Gets Exposed via Introspection**:
72
+
73
+ - **Public method names**: Method names that may reveal operations
74
+ (e.g., ``execute_query``, ``process_payment``).
75
+ - **Method signatures**: Full signatures including parameter names and type
76
+ annotations. Parameter names like ``api_key``, ``user_password``, or
77
+ ``decrypt_key`` reveal sensitive parameter purposes.
78
+ - **Protocol implementations**: Class names from inheritance hierarchy that
79
+ start with ``Protocol`` or ``Mixin`` (e.g., ``ProtocolDatabaseAdapter``,
80
+ ``MixinAsyncCircuitBreaker``).
81
+ - **FSM state information**: Current state value if FSM attributes exist
82
+ (e.g., ``connected``, ``authenticated``, ``processing``).
83
+ - **Endpoint URLs**: Health, API, and metrics endpoint paths.
84
+ - **Node metadata**: Node ID (UUID), type via ``EnumNodeKind`` (EFFECT/COMPUTE/REDUCER/ORCHESTRATOR), and version.
85
+
86
+ **What is NOT Exposed**:
87
+
88
+ - Private methods (prefixed with ``_``) - completely excluded from discovery.
89
+ - Method implementations or source code - only signatures, not logic.
90
+ - Internal state variables - only FSM state if present.
91
+ - Configuration values - secrets, connection strings, etc. are not exposed.
92
+ - Environment variables or runtime parameters.
93
+ - Request/response payloads or historical data.
94
+
95
+ **Built-in Protections**:
96
+
97
+ The mixin includes filtering mechanisms to limit exposure:
98
+
99
+ - **Private method exclusion**: Methods prefixed with ``_`` are excluded from
100
+ capability discovery.
101
+ - **Utility method filtering**: Common utility prefixes (``get_*``, ``set_*``,
102
+ ``initialize*``, ``start_*``, ``stop_*``) are filtered out by default.
103
+ - **Operation keyword matching**: Only methods containing operation keywords
104
+ (``execute``, ``handle``, ``process``, ``run``, ``invoke``, ``call``) are
105
+ reported as capabilities in the operations list.
106
+ - **Configurable exclusions**: The ``exclude_prefixes`` parameter in
107
+ ``initialize_introspection()`` allows additional filtering.
108
+ - **Caching with TTL**: Introspection data is cached to reduce reflection
109
+ frequency, with configurable TTL for freshness.
110
+
111
+ **Best Practices for Node Developers**:
112
+
113
+ - Prefix internal/sensitive methods with ``_`` to exclude them from introspection.
114
+ - Avoid exposing sensitive business logic in public method names (e.g., use
115
+ ``process_request`` instead of ``decrypt_and_forward_to_payment_gateway``).
116
+ - Use generic parameter names for public methods (e.g., ``data`` instead of
117
+ ``user_credentials``, ``payload`` instead of ``encrypted_secret``).
118
+ - Review exposed capabilities before deploying to production environments.
119
+ - Consider network segmentation for introspection event topics in multi-tenant
120
+ environments.
121
+ - Use the ``exclude_prefixes`` parameter to filter additional method patterns
122
+ if needed.
123
+
124
+ **Network Security Considerations**:
125
+
126
+ - Introspection data is published to Kafka topics (``node.introspection``,
127
+ ``node.heartbeat``, ``node.request_introspection``).
128
+ - In multi-tenant environments, ensure proper topic ACLs are configured.
129
+ - Consider whether introspection topics should be accessible outside the cluster.
130
+ - Monitor introspection topic consumers for unauthorized access.
131
+ - The registry listener responds to ANY request on the request topic without
132
+ authentication - secure the topic with Kafka ACLs.
133
+
134
+ **Production Deployment Checklist**:
135
+
136
+ 1. Review ``get_capabilities()`` output for each node before deployment.
137
+ 2. Verify no sensitive method names or parameter names are exposed.
138
+ 3. Configure Kafka topic ACLs to restrict introspection topic access.
139
+ 4. Consider disabling ``enable_registry_listener`` if not needed.
140
+ 5. Monitor introspection topic consumer groups for unexpected consumers.
141
+ 6. Use network segmentation to isolate introspection traffic if required.
142
+
143
+ For more details, see the "Node Introspection Security Considerations" section
144
+ in ``CLAUDE.md``.
145
+
146
+ Usage:
147
+ ```python
148
+ from omnibase_core.enums import EnumNodeKind
149
+ from omnibase_infra.mixins import MixinNodeIntrospection
150
+ from omnibase_infra.models.discovery import ModelIntrospectionConfig
151
+
152
+ class MyNode(MixinNodeIntrospection):
153
+ def __init__(self, node_config, event_bus=None):
154
+ config = ModelIntrospectionConfig(
155
+ node_id=node_config.node_id,
156
+ node_type=EnumNodeKind.EFFECT,
157
+ event_bus=event_bus,
158
+ )
159
+ self.initialize_introspection(config)
160
+
161
+ async def startup(self):
162
+ # Publish initial introspection on startup
163
+ await self.publish_introspection(reason="startup")
164
+
165
+ # Start background tasks
166
+ await self.start_introspection_tasks(
167
+ enable_heartbeat=True,
168
+ heartbeat_interval_seconds=30.0,
169
+ enable_registry_listener=True,
170
+ )
171
+
172
+ async def shutdown(self):
173
+ # Publish shutdown introspection
174
+ await self.publish_introspection(reason="shutdown")
175
+
176
+ # Stop background tasks
177
+ await self.stop_introspection_tasks()
178
+ ```
179
+
180
+ Integration Requirements:
181
+ Classes using this mixin must:
182
+ 1. Call `initialize_introspection(config)` during initialization with a
183
+ ModelIntrospectionConfig instance
184
+ 2. Optionally call `start_introspection_tasks()` for background operations
185
+ 3. Call `stop_introspection_tasks()` during shutdown
186
+ 4. Ensure event_bus has `publish_envelope()` method if provided
187
+
188
+ See Also:
189
+ - ModelIntrospectionConfig for configuration options
190
+ - MixinAsyncCircuitBreaker for circuit breaker pattern
191
+ - ModelNodeIntrospectionEvent for event model
192
+ - ModelNodeHeartbeatEvent for heartbeat model
193
+ - CLAUDE.md "Node Introspection Security Considerations" section
194
+ """
195
+
196
+ from __future__ import annotations
197
+
198
+ import asyncio
199
+ import inspect
200
+ import json
201
+ import logging
202
+ import time
203
+ from collections.abc import AsyncIterator, Awaitable, Callable
204
+ from contextlib import asynccontextmanager
205
+ from datetime import UTC, datetime
206
+ from typing import TYPE_CHECKING, ClassVar, TypedDict, cast
207
+ from uuid import UUID, uuid4
208
+
209
+ from omnibase_core.enums import EnumNodeKind
210
+ from omnibase_core.models.primitives.model_semver import ModelSemVer
211
+ from omnibase_infra.enums import EnumInfraTransportType, EnumIntrospectionReason
212
+ from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
213
+ from omnibase_infra.models.discovery import (
214
+ ModelDiscoveredCapabilities,
215
+ ModelIntrospectionConfig,
216
+ ModelIntrospectionTaskConfig,
217
+ )
218
+ from omnibase_infra.models.discovery.model_introspection_performance_metrics import (
219
+ ModelIntrospectionPerformanceMetrics,
220
+ )
221
+ from omnibase_infra.models.registration import (
222
+ ModelNodeCapabilities,
223
+ ModelNodeHeartbeatEvent,
224
+ )
225
+ from omnibase_infra.models.registration.model_node_introspection_event import (
226
+ ModelNodeIntrospectionEvent,
227
+ )
228
+
229
+ if TYPE_CHECKING:
230
+ from omnibase_core.protocols.event_bus.protocol_event_bus import ProtocolEventBus
231
+ from omnibase_infra.event_bus.models import ModelEventMessage
232
+
233
+ logger = logging.getLogger(__name__)
234
+
235
+ # Event topic constants
236
+ INTROSPECTION_TOPIC = "node.introspection"
237
+ HEARTBEAT_TOPIC = "node.heartbeat"
238
+ REQUEST_INTROSPECTION_TOPIC = "node.request_introspection"
239
+
240
+ # Performance threshold constants (in milliseconds)
241
+ PERF_THRESHOLD_GET_CAPABILITIES_MS = 50.0
242
+ PERF_THRESHOLD_DISCOVER_CAPABILITIES_MS = 30.0
243
+ PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS = 50.0
244
+ PERF_THRESHOLD_CACHE_HIT_MS = 1.0
245
+
246
+
247
+ class PerformanceMetricsCacheDict(TypedDict, total=False):
248
+ """TypedDict for JSON-serialized ModelIntrospectionPerformanceMetrics.
249
+
250
+ This type matches the output of ModelIntrospectionPerformanceMetrics.model_dump(mode="json"),
251
+ enabling proper type checking for cached performance metrics.
252
+
253
+ Attributes:
254
+ get_capabilities_ms: Time taken by get_capabilities() in milliseconds.
255
+ discover_capabilities_ms: Time taken by _discover_capabilities() in ms.
256
+ get_endpoints_ms: Time taken by get_endpoints() in milliseconds.
257
+ get_current_state_ms: Time taken by get_current_state() in milliseconds.
258
+ total_introspection_ms: Total time for get_introspection_data() in ms.
259
+ cache_hit: Whether the result was served from cache.
260
+ method_count: Number of methods discovered during reflection.
261
+ threshold_exceeded: Whether any operation exceeded performance thresholds.
262
+ slow_operations: List of operation names that exceeded their thresholds.
263
+ captured_at: UTC timestamp when metrics were captured (ISO string).
264
+ """
265
+
266
+ get_capabilities_ms: float
267
+ discover_capabilities_ms: float
268
+ get_endpoints_ms: float
269
+ get_current_state_ms: float
270
+ total_introspection_ms: float
271
+ cache_hit: bool
272
+ method_count: int
273
+ threshold_exceeded: bool
274
+ slow_operations: list[str]
275
+ captured_at: str # datetime serializes to ISO string in JSON mode
276
+
277
+
278
+ class DiscoveredCapabilitiesCacheDict(TypedDict, total=False):
279
+ """TypedDict for JSON-serialized ModelDiscoveredCapabilities.
280
+
281
+ Attributes:
282
+ operations: List of method names matching operation keywords.
283
+ has_fsm: Whether the node has FSM state management.
284
+ method_signatures: Mapping of method names to signature strings.
285
+ attributes: Additional discovered attributes.
286
+ """
287
+
288
+ operations: list[str]
289
+ has_fsm: bool
290
+ method_signatures: dict[str, str]
291
+ attributes: dict[str, object]
292
+
293
+
294
+ class IntrospectionCacheDict(TypedDict):
295
+ """TypedDict representing the JSON-serialized ModelNodeIntrospectionEvent.
296
+
297
+ This type matches the output of ModelNodeIntrospectionEvent.model_dump(mode="json"),
298
+ enabling proper type checking for cache operations without requiring type: ignore comments.
299
+
300
+ Note:
301
+ The capabilities are split into declared_capabilities (from contract) and
302
+ discovered_capabilities (from reflection). This reflects the fundamental
303
+ difference between what a node declares and what introspection discovers.
304
+ """
305
+
306
+ node_id: str
307
+ node_type: str
308
+ node_version: dict[str, int] # ModelSemVer serializes to {major, minor, patch}
309
+ declared_capabilities: dict[str, object] # ModelNodeCapabilities (flexible schema)
310
+ discovered_capabilities: DiscoveredCapabilitiesCacheDict
311
+ endpoints: dict[str, str]
312
+ current_state: str | None
313
+ reason: str # EnumIntrospectionReason serializes to string
314
+ correlation_id: str # UUID serializes to string in JSON mode (required field)
315
+ timestamp: str # datetime serializes to ISO string in JSON mode
316
+ # Optional fields
317
+ node_role: str | None
318
+ metadata: dict[str, object] # ModelNodeMetadata serializes to dict
319
+ network_id: str | None
320
+ deployment_id: str | None
321
+ epoch: int | None
322
+ # Performance metrics from introspection operation (may be None)
323
+ performance_metrics: PerformanceMetricsCacheDict | None
324
+
325
+
326
+ class MixinNodeIntrospection:
327
+ """Mixin providing node introspection capabilities.
328
+
329
+ Provides automatic capability discovery using reflection, endpoint
330
+ reporting, and periodic heartbeat broadcasting for ONEX nodes.
331
+
332
+ State Variables:
333
+ _introspection_cache: Cached introspection data
334
+ _introspection_cache_ttl: Cache time-to-live in seconds
335
+ _introspection_cached_at: Timestamp when cache was populated
336
+
337
+ Background Task Variables:
338
+ _heartbeat_task: Background heartbeat task
339
+ _registry_listener_task: Background registry listener task
340
+ _introspection_stop_event: Event to signal task shutdown
341
+
342
+ Configuration Variables:
343
+ _introspection_node_id: Node identifier
344
+ _introspection_node_type: Node type classification
345
+ _introspection_event_bus: Optional event bus for publishing
346
+ _introspection_version: Node version string
347
+ _introspection_start_time: Node startup timestamp
348
+
349
+ Security Considerations:
350
+ This mixin uses Python reflection (via the ``inspect`` module) to
351
+ automatically discover node capabilities. While this enables powerful
352
+ service discovery, it has security implications:
353
+
354
+ **Threat Model**:
355
+
356
+ - **Reconnaissance**: Method names may reveal attack vectors
357
+ - **Architecture mapping**: Protocol discovery exposes topology
358
+ - **Version fingerprinting**: Version field enables vulnerability scanning
359
+ - **State inference**: FSM state reveals system status
360
+
361
+ **Exposed Information**:
362
+
363
+ - Public method names (potential operations a node can perform)
364
+ - Method signatures (parameter names and type annotations)
365
+ - Protocol and mixin implementations (discovered capabilities)
366
+ - FSM state information (if state attributes are present)
367
+ - Endpoint URLs (health, API, metrics paths)
368
+ - Node metadata (name, version, type)
369
+
370
+ **What is NOT Exposed**:
371
+
372
+ - Private methods (``_`` prefix) - excluded from discovery
373
+ - Method implementations or source code
374
+ - Configuration values, secrets, or connection strings
375
+ - Environment variables or runtime parameters
376
+ - Request/response payloads or historical data
377
+
378
+ **Built-in Protections**:
379
+
380
+ - Private methods (prefixed with ``_``) are excluded by default
381
+ - Utility method prefixes (``get_*``, ``set_*``, etc.) are filtered
382
+ - Only methods containing operation keywords are reported as operations
383
+ - Configure ``exclude_prefixes`` in ``initialize_introspection()`` for
384
+ additional filtering
385
+ - Caching with TTL reduces reflection frequency
386
+
387
+ **Recommendations for Production**:
388
+
389
+ - Prefix internal/sensitive methods with ``_`` to exclude them
390
+ - Use generic operation names that don't reveal implementation details
391
+ - Use generic parameter names (``data`` instead of ``user_credentials``)
392
+ - Review ``get_capabilities()`` output before production deployment
393
+ - In multi-tenant environments, configure Kafka topic ACLs for
394
+ introspection events (``node.introspection``, ``node.heartbeat``,
395
+ ``node.request_introspection``)
396
+ - Monitor introspection topic consumers for unauthorized access
397
+ - Consider network segmentation for introspection event topics
398
+ - Consider disabling ``enable_registry_listener`` if not needed
399
+
400
+ See Also:
401
+ - Module docstring for detailed security documentation and threat model
402
+ - CLAUDE.md "Node Introspection Security Considerations" section
403
+ - ``get_capabilities()`` for filtering logic details
404
+
405
+ Example:
406
+ ```python
407
+ from uuid import UUID
408
+ from omnibase_core.enums import EnumNodeKind
409
+ from omnibase_infra.models.discovery import ModelIntrospectionConfig
410
+
411
+ class PostgresAdapter(MixinNodeIntrospection):
412
+ def __init__(self, node_id: UUID, adapter_config):
413
+ config = ModelIntrospectionConfig(
414
+ node_id=node_id,
415
+ node_type=EnumNodeKind.EFFECT,
416
+ event_bus=adapter_config.event_bus,
417
+ )
418
+ self.initialize_introspection(config)
419
+
420
+ async def execute(self, query: str) -> list[dict]:
421
+ # Node operation - WILL be exposed via introspection
422
+ ...
423
+
424
+ def _internal_helper(self, data: dict) -> dict:
425
+ # Private method - will NOT be exposed
426
+ ...
427
+ ```
428
+ """
429
+
430
+ # Class-level cache for method signatures (populated once per class)
431
+ # Maps class -> {method_name: signature_string}
432
+ # This avoids expensive reflection on each introspection call since
433
+ # method signatures don't change after class definition.
434
+ # NOTE: ClassVar is intentionally shared across all instances - this is correct
435
+ # behavior for a per-class cache of immutable method signatures.
436
+ _class_method_cache: ClassVar[dict[type, dict[str, str]]] = {}
437
+
438
+ # Type annotations for instance attributes (no default values to avoid shared state)
439
+ # All of these are initialized in initialize_introspection()
440
+ #
441
+ # Caching attributes
442
+ _introspection_cache: IntrospectionCacheDict | None
443
+ _introspection_cache_ttl: float
444
+ _introspection_cached_at: float | None
445
+
446
+ # Background task attributes
447
+ _heartbeat_task: asyncio.Task[None] | None
448
+ _registry_listener_task: asyncio.Task[None] | None
449
+ _introspection_stop_event: asyncio.Event | None
450
+ _registry_unsubscribe: Callable[[], None] | Callable[[], Awaitable[None]] | None
451
+
452
+ # Configuration attributes
453
+ _introspection_node_id: UUID | None
454
+ _introspection_node_type: EnumNodeKind | None
455
+ _introspection_event_bus: ProtocolEventBus | None
456
+ _introspection_version: str
457
+ _introspection_start_time: float | None
458
+
459
+ # Capability discovery configuration
460
+ _introspection_operation_keywords: frozenset[str]
461
+ _introspection_exclude_prefixes: frozenset[str]
462
+
463
+ # Registry listener callback error tracking (instance-level)
464
+ # Used for rate-limiting error logging to prevent log spam during
465
+ # sustained failures. These are initialized in initialize_introspection().
466
+ _registry_callback_consecutive_failures: int
467
+ _registry_callback_last_failure_time: float
468
+ _registry_callback_failure_log_threshold: int
469
+
470
+ # Performance metrics tracking (instance-level)
471
+ # Stores the most recent performance metrics from introspection operations
472
+ _introspection_last_metrics: ModelIntrospectionPerformanceMetrics | None
473
+
474
+ # Active operations tracking (instance-level)
475
+ # Thread-safe counter for tracking concurrent operations
476
+ # Used by heartbeat to report active_operations_count
477
+ _active_operations: int
478
+ _operations_lock: asyncio.Lock
479
+
480
+ # Default operation keywords for capability discovery
481
+ DEFAULT_OPERATION_KEYWORDS: ClassVar[frozenset[str]] = frozenset(
482
+ {
483
+ "execute",
484
+ "handle",
485
+ "process",
486
+ "run",
487
+ "invoke",
488
+ "call",
489
+ }
490
+ )
491
+
492
+ # Default prefixes to exclude from capability discovery
493
+ DEFAULT_EXCLUDE_PREFIXES: ClassVar[frozenset[str]] = frozenset(
494
+ {
495
+ "_",
496
+ "get_",
497
+ "set_",
498
+ "initialize",
499
+ "start_",
500
+ "stop_",
501
+ }
502
+ )
503
+
504
+ # Node-type-specific operation keyword suggestions
505
+ # Uses EnumNodeKind as keys to ensure type safety when accessing with node_type.
506
+ # Example: keywords = NODE_TYPE_OPERATION_KEYWORDS.get(node_type, set())
507
+ NODE_TYPE_OPERATION_KEYWORDS: ClassVar[dict[EnumNodeKind, set[str]]] = {
508
+ EnumNodeKind.EFFECT: {
509
+ "execute",
510
+ "handle",
511
+ "process",
512
+ "run",
513
+ "invoke",
514
+ "call",
515
+ "fetch",
516
+ "send",
517
+ "query",
518
+ "connect",
519
+ },
520
+ EnumNodeKind.COMPUTE: {
521
+ "execute",
522
+ "handle",
523
+ "process",
524
+ "run",
525
+ "compute",
526
+ "transform",
527
+ "calculate",
528
+ "convert",
529
+ "parse",
530
+ },
531
+ EnumNodeKind.REDUCER: {
532
+ "execute",
533
+ "handle",
534
+ "process",
535
+ "run",
536
+ "aggregate",
537
+ "reduce",
538
+ "merge",
539
+ "combine",
540
+ "accumulate",
541
+ },
542
+ EnumNodeKind.ORCHESTRATOR: {
543
+ "execute",
544
+ "handle",
545
+ "process",
546
+ "run",
547
+ "orchestrate",
548
+ "coordinate",
549
+ "schedule",
550
+ "dispatch",
551
+ },
552
+ }
553
+
554
+ def initialize_introspection(
555
+ self,
556
+ config: ModelIntrospectionConfig,
557
+ ) -> None:
558
+ """Initialize introspection from a configuration model.
559
+
560
+ This method accepts a typed configuration model for all introspection
561
+ settings. Must be called during class initialization before any
562
+ introspection operations are performed.
563
+
564
+ Args:
565
+ config: Configuration model containing all introspection settings.
566
+ See ModelIntrospectionConfig for available options.
567
+
568
+ Raises:
569
+ ValueError: If config.node_id is not a valid UUID or config.node_type
570
+ is not a valid EnumNodeKind member.
571
+ TypeError: If node_type is neither EnumNodeKind nor str.
572
+
573
+ Example:
574
+ ```python
575
+ from omnibase_core.enums import EnumNodeKind
576
+ from omnibase_infra.models.discovery import ModelIntrospectionConfig
577
+
578
+ class MyNode(MixinNodeIntrospection):
579
+ def __init__(self, node_config):
580
+ config = ModelIntrospectionConfig(
581
+ node_id=node_config.node_id,
582
+ node_type=EnumNodeKind.EFFECT,
583
+ event_bus=node_config.event_bus,
584
+ version="1.2.0",
585
+ )
586
+ self.initialize_introspection(config)
587
+
588
+ # With custom operation keywords
589
+ class MyEffectNode(MixinNodeIntrospection):
590
+ def __init__(self, node_config):
591
+ config = ModelIntrospectionConfig(
592
+ node_id=node_config.node_id,
593
+ node_type=EnumNodeKind.EFFECT,
594
+ event_bus=node_config.event_bus,
595
+ operation_keywords=frozenset({"fetch", "upload", "download"}),
596
+ )
597
+ self.initialize_introspection(config)
598
+ ```
599
+
600
+ See Also:
601
+ ModelIntrospectionConfig: Configuration model with all available options.
602
+ """
603
+ # Note: Pydantic validates node_id is a valid UUID and node_type is EnumNodeKind
604
+
605
+ # Configuration - extract from config model
606
+ self._introspection_node_id = config.node_id
607
+
608
+ # Defensive type handling for node_type: accept both EnumNodeKind and string.
609
+ # While ModelIntrospectionConfig's validator ensures EnumNodeKind, this defensive
610
+ # check handles edge cases like mocked configs or direct attribute access patterns.
611
+ if isinstance(config.node_type, EnumNodeKind):
612
+ self._introspection_node_type = config.node_type
613
+ elif isinstance(config.node_type, str):
614
+ # Coerce string to EnumNodeKind (handles both "effect" and "EFFECT")
615
+ self._introspection_node_type = EnumNodeKind(config.node_type.lower())
616
+ else:
617
+ # Should never happen with proper ModelIntrospectionConfig, but handle gracefully
618
+ context = ModelInfraErrorContext.with_correlation(
619
+ transport_type=EnumInfraTransportType.RUNTIME,
620
+ operation="initialize_introspection",
621
+ target_name=str(config.node_id),
622
+ )
623
+ raise ProtocolConfigurationError(
624
+ f"node_type must be EnumNodeKind or str, got {type(config.node_type).__name__}",
625
+ context=context,
626
+ parameter="node_type",
627
+ actual_type=type(config.node_type).__name__,
628
+ )
629
+ self._introspection_event_bus = config.event_bus
630
+ self._introspection_version = config.version
631
+ self._introspection_cache_ttl = config.cache_ttl
632
+
633
+ # Capability discovery configuration - frozensets are immutable, no copy needed
634
+ self._introspection_operation_keywords = (
635
+ config.operation_keywords
636
+ if config.operation_keywords is not None
637
+ else self.DEFAULT_OPERATION_KEYWORDS
638
+ )
639
+ self._introspection_exclude_prefixes = (
640
+ config.exclude_prefixes
641
+ if config.exclude_prefixes is not None
642
+ else self.DEFAULT_EXCLUDE_PREFIXES
643
+ )
644
+
645
+ # Topic configuration - extract from config model
646
+ self._introspection_topic = config.introspection_topic
647
+ self._heartbeat_topic = config.heartbeat_topic
648
+ self._request_introspection_topic = config.request_introspection_topic
649
+
650
+ # State
651
+ self._introspection_cache = None
652
+ self._introspection_cached_at = None
653
+ self._introspection_start_time = time.time()
654
+
655
+ # Background tasks
656
+ self._heartbeat_task = None
657
+ self._registry_listener_task = None
658
+ self._introspection_stop_event = asyncio.Event()
659
+ self._registry_unsubscribe = None
660
+
661
+ # Registry listener callback error tracking
662
+ # Used for rate-limiting error logging to prevent log spam
663
+ self._registry_callback_consecutive_failures = 0
664
+ self._registry_callback_last_failure_time = 0.0
665
+ # Only log every Nth consecutive failure to prevent log spam
666
+ self._registry_callback_failure_log_threshold = 5
667
+
668
+ # Performance metrics tracking
669
+ self._introspection_last_metrics = None
670
+
671
+ # Active operations tracking
672
+ # Thread-safe counter for tracking concurrent operations
673
+ self._active_operations = 0
674
+ self._operations_lock = asyncio.Lock()
675
+
676
+ if config.event_bus is None:
677
+ logger.warning(
678
+ f"Introspection initialized without event bus for {config.node_id}",
679
+ extra={
680
+ "node_id": config.node_id,
681
+ "node_type": config.node_type.value
682
+ if hasattr(config.node_type, "value")
683
+ else str(config.node_type),
684
+ },
685
+ )
686
+
687
+ logger.debug(
688
+ f"Introspection initialized for {config.node_id}",
689
+ extra={
690
+ "node_id": config.node_id,
691
+ "node_type": config.node_type.value
692
+ if hasattr(config.node_type, "value")
693
+ else str(config.node_type),
694
+ "version": config.version,
695
+ "cache_ttl": config.cache_ttl,
696
+ "has_event_bus": config.event_bus is not None,
697
+ "operation_keywords_count": len(self._introspection_operation_keywords),
698
+ "exclude_prefixes_count": len(self._introspection_exclude_prefixes),
699
+ "introspection_topic": self._introspection_topic,
700
+ "heartbeat_topic": self._heartbeat_topic,
701
+ "request_introspection_topic": self._request_introspection_topic,
702
+ },
703
+ )
704
+
705
+ def _ensure_initialized(self) -> None:
706
+ """Ensure introspection has been initialized.
707
+
708
+ This method validates that `initialize_introspection()` was called
709
+ before using introspection methods. It should be called at the start
710
+ of public entry point methods.
711
+
712
+ Raises:
713
+ ProtocolConfigurationError: If initialize_introspection() was not called.
714
+
715
+ Example:
716
+ ```python
717
+ async def get_introspection_data(self) -> ModelNodeIntrospectionEvent:
718
+ self._ensure_initialized()
719
+ # ... rest of method
720
+ ```
721
+ """
722
+ # Use getattr with sentinel to avoid AttributeError if initialize_introspection()
723
+ # was never called. This ensures we always raise structured error, not AttributeError.
724
+ _not_set = object()
725
+ node_id = getattr(self, "_introspection_node_id", _not_set)
726
+ if node_id is _not_set or node_id is None:
727
+ ctx = ModelInfraErrorContext(
728
+ transport_type=EnumInfraTransportType.KAFKA,
729
+ operation="_ensure_initialized",
730
+ target_name="node_introspection_mixin",
731
+ )
732
+ raise ProtocolConfigurationError(
733
+ "MixinNodeIntrospection not initialized. "
734
+ "Call initialize_introspection() before using introspection methods.",
735
+ context=ctx,
736
+ )
737
+
738
+ def _get_class_method_signatures(self) -> dict[str, str]:
739
+ """Get method signatures from class-level cache.
740
+
741
+ This method returns cached method signatures for the current class,
742
+ populating the cache on first access. The cache is shared across all
743
+ instances of the same class, avoiding expensive reflection operations
744
+ on each introspection call.
745
+
746
+ Security Note:
747
+ This method uses Python's ``inspect`` module to extract method
748
+ signatures, which exposes detailed type information:
749
+
750
+ - Parameter names may reveal business logic (e.g., ``user_id``,
751
+ ``payment_token``, ``decrypt_key``)
752
+ - Type annotations expose internal data structures
753
+ - Return types reveal output formats
754
+
755
+ **Filtering Applied**:
756
+
757
+ - Only public methods (not starting with ``_``) are included
758
+ - Methods without inspectable signatures get ``(...)`` placeholder
759
+
760
+ **Mitigation**:
761
+
762
+ - Use generic parameter names for public methods
763
+ - Prefix sensitive helper methods with ``_``
764
+
765
+ Returns:
766
+ Dictionary mapping public method names to signature strings.
767
+
768
+ Note:
769
+ The cache is populated lazily on first access and persists for
770
+ the lifetime of the class. Use `_invalidate_class_method_cache()`
771
+ if methods are added dynamically at runtime.
772
+
773
+ Example:
774
+ ```python
775
+ # First call populates cache
776
+ signatures = self._get_class_method_signatures()
777
+ # {"execute": "(query: str) -> list[dict]", ...}
778
+
779
+ # Subsequent calls return cached data
780
+ signatures = self._get_class_method_signatures()
781
+ ```
782
+ """
783
+ cls = type(self)
784
+ if cls not in MixinNodeIntrospection._class_method_cache:
785
+ # Populate cache for this class
786
+ signatures: dict[str, str] = {}
787
+ for name in dir(self):
788
+ if name.startswith("_"):
789
+ continue
790
+ attr = getattr(self, name, None)
791
+ if callable(attr) and inspect.ismethod(attr):
792
+ try:
793
+ sig = inspect.signature(attr)
794
+ signatures[name] = str(sig)
795
+ except (ValueError, TypeError):
796
+ # Some methods don't have inspectable signatures
797
+ signatures[name] = "(...)"
798
+ MixinNodeIntrospection._class_method_cache[cls] = signatures
799
+ return MixinNodeIntrospection._class_method_cache[cls]
800
+
801
+ @classmethod
802
+ def _invalidate_class_method_cache(cls, target_class: type | None = None) -> None:
803
+ """Invalidate the class-level method signature cache.
804
+
805
+ Call this method when methods are dynamically added or removed from
806
+ a class at runtime. For most use cases, this is not necessary as
807
+ class methods are defined at class creation time.
808
+
809
+ Args:
810
+ target_class: Specific class to invalidate cache for.
811
+ If None, clears cache for all classes.
812
+
813
+ Example:
814
+ ```python
815
+ # Invalidate cache for a specific class
816
+ MixinNodeIntrospection._invalidate_class_method_cache(MyNodeClass)
817
+
818
+ # Invalidate cache for all classes
819
+ MixinNodeIntrospection._invalidate_class_method_cache()
820
+ ```
821
+
822
+ Note:
823
+ This is typically only needed in testing scenarios or when
824
+ using dynamic method registration patterns.
825
+ """
826
+ if target_class is not None:
827
+ cls._class_method_cache.pop(target_class, None)
828
+ else:
829
+ cls._class_method_cache.clear()
830
+
831
+ def _should_skip_method(self, method_name: str) -> bool:
832
+ """Check if method should be excluded from capability discovery.
833
+
834
+ Uses the configured exclude_prefixes set for efficient prefix matching.
835
+
836
+ Order-Dependent Matching:
837
+ This method uses ``any()`` with a generator expression, which
838
+ short-circuits on the first matching prefix. This means:
839
+
840
+ - **Performance**: Prefixes earlier in the set that match common
841
+ patterns will provide faster filtering. However, since frozenset
842
+ has no guaranteed iteration order, this is not controllable.
843
+ - **Correctness**: The result is deterministic regardless of order.
844
+ A method is skipped if ANY prefix matches, so iteration order
845
+ does not affect the outcome.
846
+
847
+ The default exclude prefixes are: ``_``, ``get_``, ``set_``,
848
+ ``initialize``, ``start_``, ``stop_``.
849
+
850
+ Args:
851
+ method_name: Name of the method to check
852
+
853
+ Returns:
854
+ True if method should be skipped, False otherwise
855
+ """
856
+ return any(
857
+ method_name.startswith(prefix)
858
+ for prefix in self._introspection_exclude_prefixes
859
+ )
860
+
861
+ def _is_operation_method(self, method_name: str) -> bool:
862
+ """Check if method name indicates an operation.
863
+
864
+ Uses the configured operation_keywords set to identify methods
865
+ that represent node operations.
866
+
867
+ Order-Dependent Matching:
868
+ This method uses ``any()`` with a generator expression, which
869
+ short-circuits on the first matching keyword. This means:
870
+
871
+ - **Performance**: Keywords earlier in the set that appear more
872
+ frequently in method names will provide faster matching. However,
873
+ since frozenset has no guaranteed iteration order, this is not
874
+ directly controllable.
875
+ - **Correctness**: The result is deterministic regardless of order.
876
+ A method is classified as an operation if ANY keyword is found
877
+ in its lowercase name, so iteration order does not affect the
878
+ classification outcome.
879
+
880
+ The default operation keywords are: ``execute``, ``handle``,
881
+ ``process``, ``run``, ``invoke``, ``call``.
882
+
883
+ Node-type-specific keywords are available via
884
+ ``NODE_TYPE_OPERATION_KEYWORDS`` for specialized filtering.
885
+
886
+ Args:
887
+ method_name: Name of the method to check
888
+
889
+ Returns:
890
+ True if method appears to be an operation, False otherwise
891
+ """
892
+ name_lower = method_name.lower()
893
+ return any(
894
+ keyword in name_lower for keyword in self._introspection_operation_keywords
895
+ )
896
+
897
+ def _has_fsm_state(self) -> bool:
898
+ """Check if this class has FSM state management.
899
+
900
+ Looks for common FSM state attribute patterns.
901
+
902
+ Returns:
903
+ True if FSM state attributes are found, False otherwise
904
+ """
905
+ fsm_indicators = {"_state", "current_state", "_current_state", "state"}
906
+ return any(hasattr(self, indicator) for indicator in fsm_indicators)
907
+
908
+ def _extract_state_value(self, state: object) -> str:
909
+ """Extract string value from a state object.
910
+
911
+ Handles both enum states (with .value attribute) and plain values.
912
+
913
+ Args:
914
+ state: The state object to extract value from
915
+
916
+ Returns:
917
+ String representation of the state value
918
+ """
919
+ if hasattr(state, "value"):
920
+ return str(state.value)
921
+ return str(state)
922
+
923
+ def _get_state_from_attribute(self, attr_name: str) -> str | None:
924
+ """Try to get state value from a named attribute.
925
+
926
+ Args:
927
+ attr_name: Name of the attribute to check
928
+
929
+ Returns:
930
+ State value as string if found and not None, None otherwise
931
+ """
932
+ if not hasattr(self, attr_name):
933
+ return None
934
+ state = getattr(self, attr_name)
935
+ if state is None:
936
+ return None
937
+ return self._extract_state_value(state)
938
+
939
+ async def _get_state_from_method(self) -> str | None:
940
+ """Try to get state value from get_state method.
941
+
942
+ Handles both sync and async get_state methods.
943
+
944
+ Returns:
945
+ State value as string if method exists and returns non-None, None otherwise
946
+ """
947
+ if not hasattr(self, "get_state"):
948
+ return None
949
+
950
+ method = self.get_state
951
+ if not callable(method):
952
+ return None
953
+
954
+ try:
955
+ result = method()
956
+ if asyncio.iscoroutine(result):
957
+ result = await result
958
+ if result is None:
959
+ return None
960
+ return self._extract_state_value(result)
961
+ except Exception as e:
962
+ logger.debug(
963
+ f"Failed to get state from get_state method: {e}",
964
+ extra={"error": str(e)},
965
+ )
966
+ return None
967
+
968
+ async def get_capabilities(self) -> ModelDiscoveredCapabilities:
969
+ """Extract node capabilities via reflection.
970
+
971
+ Uses the inspect module to discover:
972
+ - Public methods (potential operations)
973
+ - FSM state attributes
974
+
975
+ Method signatures are cached at the class level for performance
976
+ optimization, as they don't change after class definition.
977
+
978
+ Security Note:
979
+ This method exposes information about the node's public interface.
980
+ The returned data includes method names, parameter signatures, and
981
+ type annotations which may reveal implementation details.
982
+
983
+ **What Gets Exposed**:
984
+
985
+ - Method names matching operation keywords (execute, handle, etc.)
986
+ - Full method signatures including parameter names and types
987
+ - Whether FSM state management is present
988
+
989
+ **Filtering Applied**:
990
+
991
+ - Private methods (``_`` prefix) are excluded
992
+ - Utility methods (``get_*``, ``set_*``, ``initialize*``, etc.) are
993
+ filtered based on ``exclude_prefixes`` configuration
994
+ - Only methods containing configured ``operation_keywords`` are
995
+ listed in the ``operations`` field
996
+
997
+ **Best Practices**:
998
+
999
+ - Review this output before production deployment
1000
+ - Use generic operation names (e.g., ``process_request`` instead of
1001
+ ``decrypt_and_forward_to_payment_gateway``)
1002
+ - Prefix sensitive internal methods with ``_``
1003
+ - Configure additional ``exclude_prefixes`` if needed
1004
+
1005
+ Returns:
1006
+ ModelDiscoveredCapabilities containing:
1007
+ - operations: Tuple of public method names that may be operations
1008
+ - has_fsm: Boolean indicating if node has FSM state management
1009
+ - method_signatures: Dict of method names to signature strings
1010
+
1011
+ Raises:
1012
+ ProtocolConfigurationError: If initialize_introspection() was not called.
1013
+
1014
+ Example:
1015
+ ```python
1016
+ capabilities = await node.get_capabilities()
1017
+ # ModelDiscoveredCapabilities(
1018
+ # operations=("execute", "query", "batch_execute"),
1019
+ # has_fsm=True,
1020
+ # method_signatures={
1021
+ # "execute": "(query: str) -> list[dict]",
1022
+ # ...
1023
+ # }
1024
+ # )
1025
+
1026
+ # Review exposed capabilities before production
1027
+ for op in capabilities.operations:
1028
+ print(f"Exposed operation: {op}")
1029
+ ```
1030
+ """
1031
+ self._ensure_initialized()
1032
+ start_time = time.perf_counter()
1033
+
1034
+ # Get cached method signatures (class-level, computed once per class)
1035
+ # Track discovery time separately for performance analysis
1036
+ discover_start = time.perf_counter()
1037
+ cached_signatures = self._get_class_method_signatures()
1038
+ discover_elapsed_ms = (time.perf_counter() - discover_start) * 1000
1039
+
1040
+ # Filter signatures and identify operations
1041
+ operations: list[str] = []
1042
+ method_signatures: dict[str, str] = {}
1043
+
1044
+ for name, sig in cached_signatures.items():
1045
+ # Skip utility methods based on configured prefixes
1046
+ if self._should_skip_method(name):
1047
+ continue
1048
+
1049
+ # Add method signature to filtered results
1050
+ method_signatures[name] = sig
1051
+
1052
+ # Add methods that look like operations
1053
+ if self._is_operation_method(name):
1054
+ operations.append(name)
1055
+
1056
+ # Build capabilities model
1057
+ capabilities = ModelDiscoveredCapabilities(
1058
+ operations=tuple(operations),
1059
+ has_fsm=self._has_fsm_state(),
1060
+ method_signatures=method_signatures,
1061
+ )
1062
+
1063
+ # Performance instrumentation
1064
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
1065
+ if elapsed_ms > PERF_THRESHOLD_GET_CAPABILITIES_MS:
1066
+ logger.warning(
1067
+ "Capability discovery exceeded 50ms target",
1068
+ extra={
1069
+ "node_id": self._introspection_node_id,
1070
+ "elapsed_ms": round(elapsed_ms, 2),
1071
+ "discover_elapsed_ms": round(discover_elapsed_ms, 2),
1072
+ "method_count": len(cached_signatures),
1073
+ "operation_count": len(operations),
1074
+ "threshold_ms": PERF_THRESHOLD_GET_CAPABILITIES_MS,
1075
+ },
1076
+ )
1077
+
1078
+ return capabilities
1079
+
1080
+ async def get_endpoints(self) -> dict[str, str]:
1081
+ """Discover endpoint URLs for this node.
1082
+
1083
+ Looks for common endpoint attributes and methods to build
1084
+ a dictionary of available endpoints.
1085
+
1086
+ Returns:
1087
+ Dictionary mapping endpoint names to URLs.
1088
+ Common keys: health, api, metrics, readiness, liveness
1089
+
1090
+ Raises:
1091
+ ProtocolConfigurationError: If initialize_introspection() was not called.
1092
+
1093
+ Example:
1094
+ ```python
1095
+ endpoints = await node.get_endpoints()
1096
+ # {
1097
+ # "health": "http://localhost:8080/health",
1098
+ # "metrics": "http://localhost:8080/metrics",
1099
+ # }
1100
+ ```
1101
+ """
1102
+ self._ensure_initialized()
1103
+ endpoints: dict[str, str] = {}
1104
+
1105
+ # Check for endpoint attributes
1106
+ endpoint_attrs = [
1107
+ ("health_url", "health"),
1108
+ ("health_endpoint", "health"),
1109
+ ("api_url", "api"),
1110
+ ("api_endpoint", "api"),
1111
+ ("metrics_url", "metrics"),
1112
+ ("metrics_endpoint", "metrics"),
1113
+ ("readiness_url", "readiness"),
1114
+ ("readiness_endpoint", "readiness"),
1115
+ ("liveness_url", "liveness"),
1116
+ ("liveness_endpoint", "liveness"),
1117
+ ]
1118
+
1119
+ for attr_name, endpoint_name in endpoint_attrs:
1120
+ if hasattr(self, attr_name):
1121
+ value = getattr(self, attr_name)
1122
+ if value and isinstance(value, str):
1123
+ endpoints[endpoint_name] = value
1124
+
1125
+ # Check for endpoint methods
1126
+ endpoint_methods = [
1127
+ ("get_health_url", "health"),
1128
+ ("get_api_url", "api"),
1129
+ ("get_metrics_url", "metrics"),
1130
+ ]
1131
+
1132
+ for method_name, endpoint_name in endpoint_methods:
1133
+ if hasattr(self, method_name) and endpoint_name not in endpoints:
1134
+ method = getattr(self, method_name)
1135
+ if callable(method):
1136
+ try:
1137
+ # Handle both sync and async methods
1138
+ result = method()
1139
+ if asyncio.iscoroutine(result):
1140
+ result = await result
1141
+ if result and isinstance(result, str):
1142
+ endpoints[endpoint_name] = result
1143
+ except Exception as e:
1144
+ logger.debug(
1145
+ f"Failed to get endpoint from {method_name}: {e}",
1146
+ extra={"method": method_name, "error": str(e)},
1147
+ )
1148
+
1149
+ return endpoints
1150
+
1151
+ async def get_current_state(self) -> str | None:
1152
+ """Get the current FSM state if applicable.
1153
+
1154
+ Checks common FSM state attribute patterns and returns
1155
+ the current state value if found.
1156
+
1157
+ Returns:
1158
+ Current state string if FSM state is found, None otherwise.
1159
+
1160
+ Raises:
1161
+ ProtocolConfigurationError: If initialize_introspection() was not called.
1162
+
1163
+ Example:
1164
+ ```python
1165
+ state = await node.get_current_state()
1166
+ # "connected" or None
1167
+ ```
1168
+ """
1169
+ self._ensure_initialized()
1170
+
1171
+ # Check for state attributes in order of preference
1172
+ state_attrs = ["_state", "current_state", "_current_state", "state"]
1173
+ for attr_name in state_attrs:
1174
+ state_value = self._get_state_from_attribute(attr_name)
1175
+ if state_value is not None:
1176
+ return state_value
1177
+
1178
+ # Fall back to get_state method
1179
+ return await self._get_state_from_method()
1180
+
1181
+ async def get_introspection_data(self) -> ModelNodeIntrospectionEvent:
1182
+ """Get introspection data with caching support.
1183
+
1184
+ Returns cached data if available and not expired, otherwise
1185
+ builds fresh introspection data and caches it.
1186
+
1187
+ Performance metrics are captured for each call and stored in
1188
+ ``_introspection_last_metrics``. Use ``get_performance_metrics()``
1189
+ to retrieve the most recent metrics.
1190
+
1191
+ Returns:
1192
+ ModelNodeIntrospectionEvent containing full introspection data.
1193
+
1194
+ Raises:
1195
+ ProtocolConfigurationError: If initialize_introspection() was not called.
1196
+
1197
+ Example:
1198
+ ```python
1199
+ data = await node.get_introspection_data()
1200
+ print(f"Node {data.node_id} has capabilities: {data.discovered_capabilities}")
1201
+ ```
1202
+ """
1203
+ self._ensure_initialized()
1204
+ total_start = time.perf_counter()
1205
+ current_time = time.time()
1206
+
1207
+ # Collect metrics values in local variables (model is frozen)
1208
+ get_capabilities_ms = 0.0
1209
+ get_endpoints_ms = 0.0
1210
+ get_current_state_ms = 0.0
1211
+ method_count = 0
1212
+ slow_operations: list[str] = []
1213
+
1214
+ # Check cache validity
1215
+ if (
1216
+ self._introspection_cache is not None
1217
+ and self._introspection_cached_at is not None
1218
+ and current_time - self._introspection_cached_at
1219
+ < self._introspection_cache_ttl
1220
+ ):
1221
+ # Return cached data (timestamp reflects when cache was populated, not current time)
1222
+ cached_event = ModelNodeIntrospectionEvent(**self._introspection_cache)
1223
+
1224
+ # Record cache hit metrics
1225
+ elapsed_ms = (time.perf_counter() - total_start) * 1000
1226
+ threshold_exceeded = elapsed_ms > PERF_THRESHOLD_CACHE_HIT_MS
1227
+ if threshold_exceeded:
1228
+ slow_operations.append("cache_hit")
1229
+
1230
+ # Create frozen metrics object with final values
1231
+ metrics = ModelIntrospectionPerformanceMetrics(
1232
+ total_introspection_ms=elapsed_ms,
1233
+ cache_hit=True,
1234
+ threshold_exceeded=threshold_exceeded,
1235
+ slow_operations=slow_operations,
1236
+ )
1237
+ self._introspection_last_metrics = metrics
1238
+ return cached_event
1239
+
1240
+ # Build fresh introspection data with timing for each component
1241
+ # First, measure the class method signature discovery time separately.
1242
+ # This is cached at the class level, so subsequent calls are instant.
1243
+ discover_start = time.perf_counter()
1244
+ self._get_class_method_signatures() # Force cache population if not already done
1245
+ discover_capabilities_ms = (time.perf_counter() - discover_start) * 1000
1246
+
1247
+ cap_start = time.perf_counter()
1248
+ discovered_capabilities = await self.get_capabilities()
1249
+ get_capabilities_ms = (time.perf_counter() - cap_start) * 1000
1250
+
1251
+ # Extract method count from capabilities (now a Pydantic model)
1252
+ method_count = len(discovered_capabilities.method_signatures)
1253
+
1254
+ endpoints_start = time.perf_counter()
1255
+ endpoints = await self.get_endpoints()
1256
+ get_endpoints_ms = (time.perf_counter() - endpoints_start) * 1000
1257
+
1258
+ state_start = time.perf_counter()
1259
+ current_state = await self.get_current_state()
1260
+ get_current_state_ms = (time.perf_counter() - state_start) * 1000
1261
+
1262
+ # Get node_id and node_type with fallback logging
1263
+ # The nil UUID fallback indicates a potential initialization issue
1264
+ node_id_uuid = self._introspection_node_id
1265
+ if node_id_uuid is None:
1266
+ logger.warning(
1267
+ "Node ID not initialized, using nil UUID - "
1268
+ "ensure initialize_introspection() was called correctly",
1269
+ extra={"operation": "get_introspection_data"},
1270
+ )
1271
+ # Use nil UUID (all zeros) as sentinel for uninitialized node
1272
+ node_id_uuid = UUID("00000000-0000-0000-0000-000000000000")
1273
+
1274
+ node_type = self._introspection_node_type
1275
+ if node_type is None:
1276
+ # Design Note: EnumNodeKind.EFFECT is the intended sentinel/default value
1277
+ # when node_type is uninitialized. EFFECT is chosen because:
1278
+ # 1. It's the most common node type in the ONEX ecosystem
1279
+ # 2. Effect nodes have the broadest capability expectations
1280
+ # 3. Fallback to EFFECT is safer than ORCHESTRATOR (avoids privilege escalation)
1281
+ logger.warning(
1282
+ "Node type not initialized, using EFFECT as fallback - "
1283
+ "ensure initialize_introspection() was called correctly",
1284
+ extra={
1285
+ "node_id": str(node_id_uuid),
1286
+ "operation": "get_introspection_data",
1287
+ },
1288
+ )
1289
+ node_type = EnumNodeKind.EFFECT
1290
+
1291
+ # Extract operations count from discovered capabilities
1292
+ operations_count = len(discovered_capabilities.operations)
1293
+
1294
+ # Finalize metrics calculations
1295
+ total_introspection_ms = (time.perf_counter() - total_start) * 1000
1296
+ threshold_exceeded = False
1297
+
1298
+ # Check thresholds and identify slow operations
1299
+ if get_capabilities_ms > PERF_THRESHOLD_GET_CAPABILITIES_MS:
1300
+ threshold_exceeded = True
1301
+ slow_operations.append("get_capabilities")
1302
+
1303
+ if total_introspection_ms > PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS:
1304
+ threshold_exceeded = True
1305
+ if "total_introspection" not in slow_operations:
1306
+ slow_operations.append("total_introspection")
1307
+
1308
+ # Create frozen metrics object with final values
1309
+ metrics = ModelIntrospectionPerformanceMetrics(
1310
+ get_capabilities_ms=get_capabilities_ms,
1311
+ discover_capabilities_ms=discover_capabilities_ms,
1312
+ get_endpoints_ms=get_endpoints_ms,
1313
+ get_current_state_ms=get_current_state_ms,
1314
+ total_introspection_ms=total_introspection_ms,
1315
+ cache_hit=False,
1316
+ method_count=method_count,
1317
+ threshold_exceeded=threshold_exceeded,
1318
+ slow_operations=slow_operations,
1319
+ )
1320
+
1321
+ # Store metrics for later retrieval
1322
+ self._introspection_last_metrics = metrics
1323
+
1324
+ # Parse version string into ModelSemVer
1325
+ try:
1326
+ version_parts = self._introspection_version.split(".")
1327
+ node_version = ModelSemVer(
1328
+ major=int(version_parts[0]) if len(version_parts) > 0 else 1,
1329
+ minor=int(version_parts[1]) if len(version_parts) > 1 else 0,
1330
+ patch=int(version_parts[2].split("-")[0])
1331
+ if len(version_parts) > 2
1332
+ else 0,
1333
+ )
1334
+ except (ValueError, IndexError):
1335
+ # Fallback to 1.0.0 if version parsing fails
1336
+ node_version = ModelSemVer(major=1, minor=0, patch=0)
1337
+
1338
+ # Create event with performance metrics (metrics is already Pydantic model)
1339
+ event = ModelNodeIntrospectionEvent(
1340
+ node_id=node_id_uuid,
1341
+ node_type=node_type,
1342
+ node_version=node_version,
1343
+ declared_capabilities=ModelNodeCapabilities(),
1344
+ discovered_capabilities=discovered_capabilities,
1345
+ endpoints=endpoints,
1346
+ current_state=current_state,
1347
+ reason=EnumIntrospectionReason.HEARTBEAT, # cache_refresh maps to heartbeat
1348
+ correlation_id=uuid4(),
1349
+ timestamp=datetime.now(UTC),
1350
+ performance_metrics=metrics,
1351
+ )
1352
+
1353
+ # Update cache - cast the model_dump output to our typed dict since we know
1354
+ # the structure matches (model_dump returns dict[str, Any] by default)
1355
+ self._introspection_cache = cast(
1356
+ IntrospectionCacheDict, event.model_dump(mode="json")
1357
+ )
1358
+ self._introspection_cached_at = current_time
1359
+
1360
+ # Log if any threshold was exceeded
1361
+ if metrics.threshold_exceeded:
1362
+ logger.warning(
1363
+ "Introspection exceeded performance threshold",
1364
+ extra={
1365
+ "node_id": self._introspection_node_id,
1366
+ "total_ms": round(metrics.total_introspection_ms, 2),
1367
+ "get_capabilities_ms": round(metrics.get_capabilities_ms, 2),
1368
+ "get_endpoints_ms": round(metrics.get_endpoints_ms, 2),
1369
+ "get_current_state_ms": round(metrics.get_current_state_ms, 2),
1370
+ "method_count": metrics.method_count,
1371
+ "slow_operations": metrics.slow_operations,
1372
+ "threshold_ms": PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS,
1373
+ },
1374
+ )
1375
+
1376
+ logger.debug(
1377
+ f"Introspection data refreshed for {self._introspection_node_id}",
1378
+ extra={
1379
+ "node_id": self._introspection_node_id,
1380
+ "capabilities_count": operations_count,
1381
+ "endpoints_count": len(endpoints),
1382
+ "total_ms": round(metrics.total_introspection_ms, 2),
1383
+ },
1384
+ )
1385
+
1386
+ return event
1387
+
1388
+ async def publish_introspection(
1389
+ self,
1390
+ reason: str | EnumIntrospectionReason = EnumIntrospectionReason.STARTUP,
1391
+ correlation_id: UUID | None = None,
1392
+ ) -> bool:
1393
+ """Publish introspection event to the event bus.
1394
+
1395
+ Gracefully degrades if event bus is unavailable - logs warning
1396
+ and returns False instead of raising an exception.
1397
+
1398
+ This method uses ``track_operation()`` to track active operations
1399
+ for heartbeat reporting, demonstrating the recommended pattern
1400
+ for integrating operation tracking into node operations.
1401
+
1402
+ Args:
1403
+ reason: Reason for the introspection event. Can be an
1404
+ EnumIntrospectionReason or a string matching enum values
1405
+ (startup, shutdown, request, heartbeat, health_change,
1406
+ capability_change). Invalid strings default to HEARTBEAT.
1407
+ correlation_id: Optional correlation ID for tracing
1408
+
1409
+ Returns:
1410
+ True if published successfully, False otherwise
1411
+
1412
+ Raises:
1413
+ ProtocolConfigurationError: If initialize_introspection() was not called.
1414
+
1415
+ Example:
1416
+ ```python
1417
+ # On startup (using enum - preferred)
1418
+ success = await node.publish_introspection(
1419
+ reason=EnumIntrospectionReason.STARTUP
1420
+ )
1421
+
1422
+ # On shutdown (using string - backwards compatible)
1423
+ success = await node.publish_introspection(reason="shutdown")
1424
+ ```
1425
+ """
1426
+ self._ensure_initialized()
1427
+
1428
+ # Convert reason to enum - check Enum first since EnumIntrospectionReason
1429
+ # inherits from str, so isinstance(..., str) would match both types.
1430
+ # Normalize string inputs with strip().lower() for robust matching.
1431
+ reason_enum: EnumIntrospectionReason
1432
+ if isinstance(reason, EnumIntrospectionReason):
1433
+ reason_enum = reason
1434
+ elif isinstance(reason, str):
1435
+ try:
1436
+ # Normalize: strip whitespace, lowercase for case-insensitive match
1437
+ reason_enum = EnumIntrospectionReason(reason.strip().lower())
1438
+ except ValueError:
1439
+ logger.warning(
1440
+ f"Unknown introspection reason '{reason}', defaulting to HEARTBEAT",
1441
+ extra={
1442
+ "node_id": self._introspection_node_id,
1443
+ "provided_reason": reason,
1444
+ },
1445
+ )
1446
+ reason_enum = EnumIntrospectionReason.HEARTBEAT
1447
+ else:
1448
+ context = ModelInfraErrorContext.with_correlation(
1449
+ transport_type=EnumInfraTransportType.RUNTIME,
1450
+ operation="publish_introspection",
1451
+ target_name=str(self._introspection_node_id),
1452
+ )
1453
+ raise ProtocolConfigurationError(
1454
+ f"reason must be str or EnumIntrospectionReason, got {type(reason).__name__}",
1455
+ context=context,
1456
+ parameter="reason",
1457
+ actual_type=type(reason).__name__,
1458
+ )
1459
+
1460
+ if self._introspection_event_bus is None:
1461
+ logger.warning(
1462
+ f"Cannot publish introspection - no event bus configured for {self._introspection_node_id}",
1463
+ extra={
1464
+ "node_id": self._introspection_node_id,
1465
+ "reason": reason_enum.value,
1466
+ },
1467
+ )
1468
+ return False
1469
+
1470
+ # Track this operation for heartbeat reporting
1471
+ async with self.track_operation("publish_introspection"):
1472
+ try:
1473
+ # Get introspection data
1474
+ event = await self.get_introspection_data()
1475
+
1476
+ # Create publish event with updated reason and correlation_id
1477
+ # Use model_copy for clean field updates (Pydantic v2)
1478
+ final_correlation_id = correlation_id or uuid4()
1479
+ publish_event = event.model_copy(
1480
+ update={
1481
+ "reason": reason_enum,
1482
+ "correlation_id": final_correlation_id,
1483
+ }
1484
+ )
1485
+
1486
+ # Publish to event bus using configured topic
1487
+ # Type narrowing: we've already checked _introspection_event_bus is not None above
1488
+ event_bus = self._introspection_event_bus
1489
+ assert event_bus is not None # Redundant but helps mypy
1490
+ topic = self._introspection_topic
1491
+ if hasattr(event_bus, "publish_envelope"):
1492
+ await event_bus.publish_envelope(
1493
+ envelope=publish_event,
1494
+ topic=topic,
1495
+ )
1496
+ else:
1497
+ # Fallback to publish method with raw bytes
1498
+ event_data = publish_event.model_dump(mode="json")
1499
+ value = json.dumps(event_data).encode("utf-8")
1500
+ await event_bus.publish(
1501
+ topic=topic,
1502
+ key=str(self._introspection_node_id).encode("utf-8")
1503
+ if self._introspection_node_id is not None
1504
+ else None,
1505
+ value=value,
1506
+ )
1507
+
1508
+ logger.info(
1509
+ f"Published introspection event for {self._introspection_node_id}",
1510
+ extra={
1511
+ "node_id": self._introspection_node_id,
1512
+ "reason": reason_enum.value,
1513
+ "correlation_id": str(final_correlation_id),
1514
+ },
1515
+ )
1516
+ return True
1517
+
1518
+ except Exception as e:
1519
+ # Use error() with exc_info=True instead of exception() to include
1520
+ # structured error_type and error_message fields for log aggregation
1521
+ logger.error(
1522
+ f"Failed to publish introspection for {self._introspection_node_id}",
1523
+ extra={
1524
+ "node_id": self._introspection_node_id,
1525
+ "reason": reason_enum.value,
1526
+ "error_type": type(e).__name__,
1527
+ "error_message": str(e),
1528
+ },
1529
+ exc_info=True,
1530
+ )
1531
+ return False
1532
+
1533
+ async def _publish_heartbeat(self) -> bool:
1534
+ """Publish heartbeat event to the event bus.
1535
+
1536
+ Internal method for heartbeat broadcasting. Calculates uptime
1537
+ and publishes heartbeat event.
1538
+
1539
+ Note:
1540
+ This method intentionally does NOT use ``track_operation()``
1541
+ because:
1542
+ 1. It's an internal background task, not a business operation
1543
+ 2. Tracking it would cause self-referential counting (the
1544
+ heartbeat would count itself as an active operation)
1545
+ 3. The purpose of operation tracking is to report business
1546
+ load, not infrastructure overhead
1547
+
1548
+ For business operations, use ``track_operation()`` as
1549
+ demonstrated in ``publish_introspection()``.
1550
+
1551
+ Returns:
1552
+ True if published successfully, False otherwise
1553
+ """
1554
+ if self._introspection_event_bus is None:
1555
+ return False
1556
+
1557
+ try:
1558
+ # Calculate uptime
1559
+ uptime_seconds = 0.0
1560
+ if self._introspection_start_time is not None:
1561
+ uptime_seconds = time.time() - self._introspection_start_time
1562
+
1563
+ # Get node_id and node_type with fallback logging
1564
+ # The nil UUID fallback indicates a potential initialization issue
1565
+ node_id = self._introspection_node_id
1566
+ if node_id is None:
1567
+ logger.warning(
1568
+ "Node ID not initialized, using nil UUID in heartbeat - "
1569
+ "ensure initialize_introspection() was called correctly",
1570
+ extra={"operation": "_publish_heartbeat"},
1571
+ )
1572
+ # Use nil UUID (all zeros) as sentinel for uninitialized node
1573
+ node_id = UUID("00000000-0000-0000-0000-000000000000")
1574
+
1575
+ node_type = self._introspection_node_type
1576
+ if node_type is None:
1577
+ # Design Note: EnumNodeKind.EFFECT is the intended sentinel/default value.
1578
+ # See get_introspection_data() for detailed rationale.
1579
+ logger.warning(
1580
+ "Node type not initialized, using EFFECT in heartbeat - "
1581
+ "ensure initialize_introspection() was called correctly",
1582
+ extra={"node_id": str(node_id), "operation": "_publish_heartbeat"},
1583
+ )
1584
+ node_type = EnumNodeKind.EFFECT
1585
+
1586
+ # Get current active operations count (coroutine-safe)
1587
+ async with self._operations_lock:
1588
+ active_ops_count = self._active_operations
1589
+
1590
+ # Create heartbeat event
1591
+ now = datetime.now(UTC)
1592
+ heartbeat = ModelNodeHeartbeatEvent(
1593
+ node_id=node_id,
1594
+ node_type=node_type,
1595
+ uptime_seconds=uptime_seconds,
1596
+ active_operations_count=active_ops_count,
1597
+ correlation_id=uuid4(),
1598
+ timestamp=now, # Required: time injection pattern
1599
+ )
1600
+
1601
+ # Publish to event bus using configured topic
1602
+ # Type narrowing: we've already checked _introspection_event_bus is not None above
1603
+ event_bus = self._introspection_event_bus
1604
+ assert event_bus is not None # Redundant but helps mypy
1605
+ topic = self._heartbeat_topic
1606
+ if hasattr(event_bus, "publish_envelope"):
1607
+ await event_bus.publish_envelope(
1608
+ envelope=heartbeat,
1609
+ topic=topic,
1610
+ )
1611
+ else:
1612
+ value = json.dumps(heartbeat.model_dump(mode="json")).encode("utf-8")
1613
+ await event_bus.publish(
1614
+ topic=topic,
1615
+ key=str(self._introspection_node_id).encode("utf-8")
1616
+ if self._introspection_node_id is not None
1617
+ else None,
1618
+ value=value,
1619
+ )
1620
+
1621
+ logger.debug(
1622
+ f"Published heartbeat for {self._introspection_node_id}",
1623
+ extra={
1624
+ "node_id": self._introspection_node_id,
1625
+ "uptime_seconds": uptime_seconds,
1626
+ "active_operations": active_ops_count,
1627
+ "topic": topic,
1628
+ },
1629
+ )
1630
+ return True
1631
+
1632
+ except Exception as e:
1633
+ # Use error() with exc_info=True instead of exception() to include
1634
+ # structured error_type and error_message fields for log aggregation
1635
+ logger.error(
1636
+ f"Failed to publish heartbeat for {self._introspection_node_id}",
1637
+ extra={
1638
+ "node_id": self._introspection_node_id,
1639
+ "error_type": type(e).__name__,
1640
+ "error_message": str(e),
1641
+ },
1642
+ exc_info=True,
1643
+ )
1644
+ return False
1645
+
1646
+ async def _heartbeat_loop(self, interval: float) -> None:
1647
+ """Background loop for periodic heartbeat publishing.
1648
+
1649
+ Runs until stop event is set, publishing heartbeats at the
1650
+ specified interval.
1651
+
1652
+ Args:
1653
+ interval: Time between heartbeats in seconds
1654
+ """
1655
+ # Ensure stop event is initialized
1656
+ if self._introspection_stop_event is None:
1657
+ self._introspection_stop_event = asyncio.Event()
1658
+
1659
+ logger.info(
1660
+ f"Starting heartbeat loop for {self._introspection_node_id}",
1661
+ extra={
1662
+ "node_id": self._introspection_node_id,
1663
+ "interval_seconds": interval,
1664
+ },
1665
+ )
1666
+
1667
+ while not self._introspection_stop_event.is_set():
1668
+ try:
1669
+ await self._publish_heartbeat()
1670
+ except asyncio.CancelledError:
1671
+ logger.debug(
1672
+ f"Heartbeat loop cancelled for {self._introspection_node_id}",
1673
+ extra={"node_id": self._introspection_node_id},
1674
+ )
1675
+ break
1676
+ except Exception as e:
1677
+ # Use error() with exc_info=True instead of exception() to include
1678
+ # structured error_type and error_message fields for log aggregation
1679
+ logger.error(
1680
+ f"Error in heartbeat loop for {self._introspection_node_id}",
1681
+ extra={
1682
+ "node_id": self._introspection_node_id,
1683
+ "error_type": type(e).__name__,
1684
+ "error_message": str(e),
1685
+ },
1686
+ exc_info=True,
1687
+ )
1688
+
1689
+ # Wait for next interval or stop event
1690
+ try:
1691
+ await asyncio.wait_for(
1692
+ self._introspection_stop_event.wait(),
1693
+ timeout=interval,
1694
+ )
1695
+ # Stop event was set
1696
+ break
1697
+ except TimeoutError:
1698
+ # Normal timeout, continue loop
1699
+ pass
1700
+
1701
+ logger.info(
1702
+ f"Heartbeat loop stopped for {self._introspection_node_id}",
1703
+ extra={"node_id": self._introspection_node_id},
1704
+ )
1705
+
1706
+ def _parse_correlation_id(self, raw_value: str | None) -> UUID | None:
1707
+ """Parse correlation ID from request data with graceful fallback.
1708
+
1709
+ Args:
1710
+ raw_value: Raw correlation_id value from request JSON
1711
+
1712
+ Returns:
1713
+ Parsed UUID or None if parsing fails or value is empty
1714
+ """
1715
+ if not raw_value:
1716
+ return None
1717
+
1718
+ try:
1719
+ # UUID() raises ValueError for malformed strings,
1720
+ # TypeError for non-string inputs (e.g., int, list).
1721
+ # Convert to string first for safer handling of unexpected types.
1722
+ return UUID(str(raw_value))
1723
+ except (ValueError, TypeError) as e:
1724
+ # Log warning with structured fields for monitoring.
1725
+ # Truncate received value preview to avoid log bloat
1726
+ # from potentially malicious oversized input.
1727
+ logger.warning(
1728
+ "Invalid correlation_id format in introspection "
1729
+ "request, generating new correlation_id",
1730
+ extra={
1731
+ "node_id": self._introspection_node_id,
1732
+ "error_type": type(e).__name__,
1733
+ "error_message": str(e),
1734
+ "received_value_type": type(raw_value).__name__,
1735
+ "received_value_preview": str(raw_value)[:50],
1736
+ },
1737
+ )
1738
+ return None
1739
+
1740
+ @staticmethod
1741
+ def _should_log_failure(consecutive_failures: int, threshold: int) -> bool:
1742
+ """Determine if failure should be logged based on rate limiting.
1743
+
1744
+ Logs first failure and every Nth consecutive failure to prevent log spam.
1745
+
1746
+ Args:
1747
+ consecutive_failures: Current consecutive failure count
1748
+ threshold: Log every Nth failure
1749
+
1750
+ Returns:
1751
+ True if this failure should be logged at error level
1752
+ """
1753
+ return consecutive_failures == 1 or consecutive_failures % threshold == 0
1754
+
1755
+ async def _cleanup_registry_subscription(self) -> None:
1756
+ """Clean up the current registry subscription."""
1757
+ if self._registry_unsubscribe is not None:
1758
+ try:
1759
+ result = self._registry_unsubscribe()
1760
+ if asyncio.iscoroutine(result):
1761
+ await result
1762
+ except Exception as cleanup_error:
1763
+ logger.debug(
1764
+ "Error unsubscribing registry listener for "
1765
+ f"{self._introspection_node_id}",
1766
+ extra={
1767
+ "node_id": self._introspection_node_id,
1768
+ "error_type": type(cleanup_error).__name__,
1769
+ "error_message": str(cleanup_error),
1770
+ },
1771
+ )
1772
+ self._registry_unsubscribe = None
1773
+
1774
+ async def _handle_introspection_request(self, message: ModelEventMessage) -> None:
1775
+ """Handle incoming introspection request.
1776
+
1777
+ Includes error recovery with rate-limited logging to prevent
1778
+ log spam during sustained failures. Continues processing on
1779
+ non-fatal errors to maintain graceful degradation.
1780
+
1781
+ Args:
1782
+ message: The incoming event message
1783
+ """
1784
+ try:
1785
+ await self._process_introspection_request(message)
1786
+ # Reset failure counter on success
1787
+ self._registry_callback_consecutive_failures = 0
1788
+ except Exception as e:
1789
+ self._handle_request_error(e)
1790
+
1791
+ async def _process_introspection_request(self, message: ModelEventMessage) -> None:
1792
+ """Process the introspection request message.
1793
+
1794
+ Args:
1795
+ message: The incoming event message
1796
+
1797
+ Raises:
1798
+ Exception: If processing fails (will be caught by caller)
1799
+ """
1800
+ # Early exit if message has no parseable value
1801
+ if not hasattr(message, "value") or not message.value:
1802
+ await self.publish_introspection(
1803
+ reason="request",
1804
+ correlation_id=uuid4(),
1805
+ )
1806
+ return
1807
+
1808
+ # Parse request data
1809
+ request_data = json.loads(message.value.decode("utf-8"))
1810
+
1811
+ # Check if request targets a specific node (early exit if not us)
1812
+ # Note: Compare as strings since target_node_id from JSON is a string
1813
+ # while _introspection_node_id is a UUID object
1814
+ target_node_id = request_data.get("target_node_id")
1815
+ if target_node_id and str(target_node_id) != str(self._introspection_node_id):
1816
+ return
1817
+
1818
+ # Parse correlation ID with graceful fallback
1819
+ correlation_id = self._parse_correlation_id(request_data.get("correlation_id"))
1820
+
1821
+ # Respond with introspection data
1822
+ await self.publish_introspection(
1823
+ reason="request",
1824
+ correlation_id=correlation_id,
1825
+ )
1826
+
1827
+ def _handle_request_error(self, error: Exception) -> None:
1828
+ """Handle error during introspection request processing.
1829
+
1830
+ Tracks consecutive failures and rate-limits error logging.
1831
+
1832
+ Args:
1833
+ error: The exception that occurred
1834
+ """
1835
+ # Track consecutive failures for rate-limited logging
1836
+ self._registry_callback_consecutive_failures += 1
1837
+ self._registry_callback_last_failure_time = time.time()
1838
+
1839
+ # Rate-limit error logging to prevent log spam during sustained failures
1840
+ if self._should_log_failure(
1841
+ self._registry_callback_consecutive_failures,
1842
+ self._registry_callback_failure_log_threshold,
1843
+ ):
1844
+ logger.error(
1845
+ f"Error handling introspection request for {self._introspection_node_id}",
1846
+ extra={
1847
+ "node_id": self._introspection_node_id,
1848
+ "error_type": type(error).__name__,
1849
+ "error_message": str(error),
1850
+ "consecutive_failures": self._registry_callback_consecutive_failures,
1851
+ "log_rate_limited": self._registry_callback_consecutive_failures
1852
+ > 1,
1853
+ },
1854
+ exc_info=True,
1855
+ )
1856
+ else:
1857
+ # Log at debug level for rate-limited failures
1858
+ logger.debug(
1859
+ f"Suppressed error log for introspection request "
1860
+ f"(failure {self._registry_callback_consecutive_failures})",
1861
+ extra={
1862
+ "node_id": self._introspection_node_id,
1863
+ "error_type": type(error).__name__,
1864
+ "consecutive_failures": self._registry_callback_consecutive_failures,
1865
+ },
1866
+ )
1867
+
1868
+ async def _attempt_subscription(self) -> bool:
1869
+ """Attempt to subscribe to the request introspection topic.
1870
+
1871
+ Returns:
1872
+ True if subscribed successfully and should wait for stop signal,
1873
+ False if subscription not supported or failed
1874
+
1875
+ Note:
1876
+ This method should only be called when event bus is verified to exist.
1877
+ The caller (_registry_listener_loop) checks for None before calling.
1878
+ """
1879
+ event_bus = self._introspection_event_bus
1880
+ if event_bus is None or not hasattr(event_bus, "subscribe"):
1881
+ logger.warning(
1882
+ "Event bus does not support subscribe for "
1883
+ f"{self._introspection_node_id}",
1884
+ extra={"node_id": self._introspection_node_id},
1885
+ )
1886
+ return False
1887
+
1888
+ request_topic = self._request_introspection_topic
1889
+ unsubscribe = await event_bus.subscribe(
1890
+ topic=request_topic,
1891
+ group_id=f"introspection-{self._introspection_node_id}",
1892
+ on_message=self._handle_introspection_request,
1893
+ )
1894
+ self._registry_unsubscribe = unsubscribe
1895
+
1896
+ logger.info(
1897
+ f"Registry listener subscribed for {self._introspection_node_id}",
1898
+ extra={
1899
+ "node_id": self._introspection_node_id,
1900
+ "topic": request_topic,
1901
+ },
1902
+ )
1903
+ return True
1904
+
1905
+ async def _wait_for_backoff_or_stop(self, backoff_seconds: float) -> bool:
1906
+ """Wait for backoff period or stop signal.
1907
+
1908
+ Args:
1909
+ backoff_seconds: Time to wait in seconds
1910
+
1911
+ Returns:
1912
+ True if stop signal received, False if timeout (should retry)
1913
+
1914
+ Note:
1915
+ This method should only be called when stop_event is verified to exist.
1916
+ The caller (_registry_listener_loop) initializes the event before calling.
1917
+ """
1918
+ stop_event = self._introspection_stop_event
1919
+ if stop_event is None:
1920
+ # Should not happen if called correctly, but handle gracefully
1921
+ return False
1922
+
1923
+ try:
1924
+ await asyncio.wait_for(
1925
+ stop_event.wait(),
1926
+ timeout=backoff_seconds,
1927
+ )
1928
+ # Stop signal received during backoff
1929
+ return True
1930
+ except TimeoutError:
1931
+ # Normal timeout, continue to retry
1932
+ return False
1933
+
1934
+ async def _registry_listener_loop(
1935
+ self,
1936
+ max_retries: int = 3,
1937
+ base_backoff_seconds: float = 1.0,
1938
+ ) -> None:
1939
+ """Background loop listening for REQUEST_INTROSPECTION events.
1940
+
1941
+ Subscribes to the request_introspection topic and responds
1942
+ with introspection data when requests are received. Includes
1943
+ retry logic with exponential backoff for subscription failures.
1944
+
1945
+ Security Note:
1946
+ This method subscribes to the ``node.request_introspection`` Kafka
1947
+ topic and responds with full introspection data to any request.
1948
+ This creates a network-accessible endpoint for capability discovery.
1949
+
1950
+ **Network Exposure**:
1951
+
1952
+ - Any consumer on the Kafka cluster can request introspection data
1953
+ - Responses are published to ``node.introspection`` topic
1954
+ - No authentication is performed on incoming requests
1955
+
1956
+ **Multi-tenant Considerations**:
1957
+
1958
+ - Configure Kafka topic ACLs to restrict access to introspection
1959
+ topics in multi-tenant environments
1960
+ - Consider whether introspection topics should be accessible
1961
+ outside the cluster boundary
1962
+ - Monitor topic consumers for unauthorized access patterns
1963
+ - Use separate Kafka clusters for different security domains
1964
+
1965
+ **Request Validation**:
1966
+
1967
+ - The ``target_node_id`` field allows filtering requests to
1968
+ specific nodes - only matching requests are processed
1969
+ - Malformed requests are handled gracefully without crashing
1970
+ - Correlation IDs are validated but invalid IDs don't block
1971
+ processing
1972
+
1973
+ Args:
1974
+ max_retries: Maximum subscription retry attempts (default: 3)
1975
+ base_backoff_seconds: Base backoff time for exponential retry
1976
+ """
1977
+ if self._introspection_event_bus is None:
1978
+ logger.warning(
1979
+ f"Cannot start registry listener - no event bus for {self._introspection_node_id}",
1980
+ extra={"node_id": self._introspection_node_id},
1981
+ )
1982
+ return
1983
+
1984
+ # Ensure stop event is initialized
1985
+ if self._introspection_stop_event is None:
1986
+ self._introspection_stop_event = asyncio.Event()
1987
+
1988
+ logger.info(
1989
+ f"Starting registry listener for {self._introspection_node_id}",
1990
+ extra={"node_id": self._introspection_node_id},
1991
+ )
1992
+
1993
+ # Retry loop with exponential backoff for subscription failures
1994
+ retry_count = 0
1995
+ while not self._introspection_stop_event.is_set():
1996
+ try:
1997
+ if await self._attempt_subscription():
1998
+ # Wait for stop signal
1999
+ await self._introspection_stop_event.wait()
2000
+ # Exit loop after subscription ends or not supported
2001
+ break
2002
+
2003
+ except asyncio.CancelledError:
2004
+ logger.debug(
2005
+ f"Registry listener cancelled for {self._introspection_node_id}",
2006
+ extra={"node_id": self._introspection_node_id},
2007
+ )
2008
+ break
2009
+ except Exception as e:
2010
+ retry_count += 1
2011
+ if not await self._handle_subscription_error(
2012
+ e, retry_count, max_retries, base_backoff_seconds
2013
+ ):
2014
+ break
2015
+
2016
+ # Final cleanup
2017
+ await self._cleanup_registry_subscription()
2018
+
2019
+ logger.info(
2020
+ f"Registry listener stopped for {self._introspection_node_id}",
2021
+ extra={"node_id": self._introspection_node_id},
2022
+ )
2023
+
2024
+ async def _handle_subscription_error(
2025
+ self,
2026
+ error: Exception,
2027
+ retry_count: int,
2028
+ max_retries: int,
2029
+ base_backoff_seconds: float,
2030
+ ) -> bool:
2031
+ """Handle subscription error with retry logic.
2032
+
2033
+ Args:
2034
+ error: The exception that occurred
2035
+ retry_count: Current retry attempt number
2036
+ max_retries: Maximum retry attempts
2037
+ base_backoff_seconds: Base backoff time for exponential retry
2038
+
2039
+ Returns:
2040
+ True if should continue retrying, False if should stop
2041
+ """
2042
+ logger.error(
2043
+ f"Error in registry listener for {self._introspection_node_id}",
2044
+ extra={
2045
+ "node_id": self._introspection_node_id,
2046
+ "error_type": type(error).__name__,
2047
+ "error_message": str(error),
2048
+ "retry_count": retry_count,
2049
+ "max_retries": max_retries,
2050
+ },
2051
+ exc_info=True,
2052
+ )
2053
+
2054
+ # Clean up any partial subscription before retry
2055
+ await self._cleanup_registry_subscription()
2056
+
2057
+ # Check if we should retry
2058
+ if retry_count >= max_retries:
2059
+ logger.error(
2060
+ "Registry listener exhausted retries",
2061
+ extra={
2062
+ "node_id": self._introspection_node_id,
2063
+ "retry_count": retry_count,
2064
+ "max_retries": max_retries,
2065
+ "error_type": type(error).__name__,
2066
+ "error_message": str(error),
2067
+ },
2068
+ exc_info=True,
2069
+ )
2070
+ return False
2071
+
2072
+ # Exponential backoff before retry
2073
+ backoff = base_backoff_seconds * (2 ** (retry_count - 1))
2074
+ logger.info(
2075
+ f"Registry listener retrying in {backoff}s for "
2076
+ f"{self._introspection_node_id}",
2077
+ extra={
2078
+ "node_id": self._introspection_node_id,
2079
+ "backoff_seconds": backoff,
2080
+ "retry_count": retry_count,
2081
+ },
2082
+ )
2083
+
2084
+ # Wait for backoff period or stop signal
2085
+ if await self._wait_for_backoff_or_stop(backoff):
2086
+ return False # Stop signal received
2087
+
2088
+ return True # Continue retrying
2089
+
2090
+ async def start_introspection_tasks(
2091
+ self,
2092
+ enable_heartbeat: bool = True,
2093
+ heartbeat_interval_seconds: float = 30.0,
2094
+ enable_registry_listener: bool = True,
2095
+ ) -> None:
2096
+ """Start background introspection tasks.
2097
+
2098
+ Starts the heartbeat loop and/or registry listener as background
2099
+ tasks. Safe to call multiple times - won't start duplicate tasks.
2100
+
2101
+ Args:
2102
+ enable_heartbeat: Whether to start the heartbeat loop
2103
+ heartbeat_interval_seconds: Interval between heartbeats in seconds
2104
+ enable_registry_listener: Whether to start the registry listener
2105
+
2106
+ Raises:
2107
+ ProtocolConfigurationError: If initialize_introspection() was not called.
2108
+
2109
+ Example:
2110
+ ```python
2111
+ await node.start_introspection_tasks(
2112
+ enable_heartbeat=True,
2113
+ heartbeat_interval_seconds=30.0,
2114
+ enable_registry_listener=True,
2115
+ )
2116
+ ```
2117
+ """
2118
+ self._ensure_initialized()
2119
+ # Reset stop event if previously set
2120
+ if self._introspection_stop_event is None:
2121
+ self._introspection_stop_event = asyncio.Event()
2122
+ elif self._introspection_stop_event.is_set():
2123
+ self._introspection_stop_event.clear()
2124
+
2125
+ # Start heartbeat task if enabled and not running
2126
+ if enable_heartbeat and self._heartbeat_task is None:
2127
+ self._heartbeat_task = asyncio.create_task(
2128
+ self._heartbeat_loop(heartbeat_interval_seconds),
2129
+ name=f"heartbeat-{self._introspection_node_id}",
2130
+ )
2131
+ logger.debug(
2132
+ f"Started heartbeat task for {self._introspection_node_id}",
2133
+ extra={
2134
+ "node_id": self._introspection_node_id,
2135
+ "interval": heartbeat_interval_seconds,
2136
+ },
2137
+ )
2138
+
2139
+ # Start registry listener if enabled and not running
2140
+ if enable_registry_listener and self._registry_listener_task is None:
2141
+ self._registry_listener_task = asyncio.create_task(
2142
+ self._registry_listener_loop(),
2143
+ name=f"registry-listener-{self._introspection_node_id}",
2144
+ )
2145
+ logger.debug(
2146
+ f"Started registry listener task for {self._introspection_node_id}",
2147
+ extra={"node_id": self._introspection_node_id},
2148
+ )
2149
+
2150
+ async def start_introspection_tasks_from_config(
2151
+ self,
2152
+ config: ModelIntrospectionTaskConfig,
2153
+ ) -> None:
2154
+ """Start background introspection tasks from a configuration model.
2155
+
2156
+ This method provides an alternative to ``start_introspection_tasks()``
2157
+ using a configuration model instead of individual parameters. This
2158
+ reduces union types in calling code and follows ONEX patterns.
2159
+
2160
+ Args:
2161
+ config: Configuration model containing task settings.
2162
+ See ModelIntrospectionTaskConfig for available options.
2163
+
2164
+ Raises:
2165
+ ProtocolConfigurationError: If initialize_introspection() was not called.
2166
+
2167
+ Example:
2168
+ ```python
2169
+ from omnibase_infra.models.discovery import ModelIntrospectionTaskConfig
2170
+
2171
+ class MyNode(MixinNodeIntrospection):
2172
+ async def startup(self):
2173
+ config = ModelIntrospectionTaskConfig(
2174
+ enable_heartbeat=True,
2175
+ heartbeat_interval_seconds=15.0,
2176
+ enable_registry_listener=True,
2177
+ )
2178
+ await self.start_introspection_tasks_from_config(config)
2179
+
2180
+ # Using defaults
2181
+ class SimpleNode(MixinNodeIntrospection):
2182
+ async def startup(self):
2183
+ config = ModelIntrospectionTaskConfig()
2184
+ await self.start_introspection_tasks_from_config(config)
2185
+ ```
2186
+
2187
+ See Also:
2188
+ start_introspection_tasks: Original method with parameters.
2189
+ ModelIntrospectionTaskConfig: Configuration model with all options.
2190
+ """
2191
+ await self.start_introspection_tasks(
2192
+ enable_heartbeat=config.enable_heartbeat,
2193
+ heartbeat_interval_seconds=config.heartbeat_interval_seconds,
2194
+ enable_registry_listener=config.enable_registry_listener,
2195
+ )
2196
+
2197
+ async def stop_introspection_tasks(self) -> None:
2198
+ """Stop all background introspection tasks.
2199
+
2200
+ Signals tasks to stop and waits for clean shutdown.
2201
+ Safe to call multiple times.
2202
+
2203
+ Example:
2204
+ ```python
2205
+ await node.stop_introspection_tasks()
2206
+ ```
2207
+ """
2208
+ logger.info(
2209
+ f"Stopping introspection tasks for {self._introspection_node_id}",
2210
+ extra={"node_id": self._introspection_node_id},
2211
+ )
2212
+
2213
+ # Signal tasks to stop
2214
+ if self._introspection_stop_event is not None:
2215
+ self._introspection_stop_event.set()
2216
+
2217
+ # Cancel and wait for heartbeat task
2218
+ if self._heartbeat_task is not None:
2219
+ self._heartbeat_task.cancel()
2220
+ try:
2221
+ await self._heartbeat_task
2222
+ except asyncio.CancelledError:
2223
+ pass
2224
+ self._heartbeat_task = None
2225
+
2226
+ # Cancel and wait for registry listener task
2227
+ if self._registry_listener_task is not None:
2228
+ self._registry_listener_task.cancel()
2229
+ try:
2230
+ await self._registry_listener_task
2231
+ except asyncio.CancelledError:
2232
+ pass
2233
+ self._registry_listener_task = None
2234
+
2235
+ logger.info(
2236
+ f"Introspection tasks stopped for {self._introspection_node_id}",
2237
+ extra={"node_id": self._introspection_node_id},
2238
+ )
2239
+
2240
+ def invalidate_introspection_cache(self) -> None:
2241
+ """Invalidate the introspection cache.
2242
+
2243
+ Call this when node capabilities change to ensure fresh
2244
+ data is reported on next introspection request.
2245
+
2246
+ Example:
2247
+ ```python
2248
+ node.register_new_handler(handler)
2249
+ node.invalidate_introspection_cache()
2250
+ ```
2251
+ """
2252
+ self._introspection_cache = None
2253
+ self._introspection_cached_at = None
2254
+ logger.debug(
2255
+ f"Introspection cache invalidated for {self._introspection_node_id}",
2256
+ extra={"node_id": self._introspection_node_id},
2257
+ )
2258
+
2259
+ def get_performance_metrics(self) -> ModelIntrospectionPerformanceMetrics | None:
2260
+ """Get the most recent performance metrics from introspection operations.
2261
+
2262
+ Returns the performance metrics captured during the last call to
2263
+ ``get_introspection_data()``. Use this to monitor introspection
2264
+ performance and detect when operations exceed the <50ms threshold.
2265
+
2266
+ Returns:
2267
+ ModelIntrospectionPerformanceMetrics if introspection has been called,
2268
+ None if no introspection has been performed yet.
2269
+
2270
+ Example:
2271
+ ```python
2272
+ # After calling introspection
2273
+ await node.get_introspection_data()
2274
+
2275
+ # Check performance metrics
2276
+ metrics = node.get_performance_metrics()
2277
+ if metrics and metrics.threshold_exceeded:
2278
+ logger.warning(
2279
+ "Slow introspection detected",
2280
+ extra={
2281
+ "slow_operations": metrics.slow_operations,
2282
+ "total_ms": metrics.total_introspection_ms,
2283
+ }
2284
+ )
2285
+
2286
+ # Access individual timings
2287
+ if metrics:
2288
+ print(f"Total time: {metrics.total_introspection_ms:.2f}ms")
2289
+ print(f"Cache hit: {metrics.cache_hit}")
2290
+ print(f"Methods discovered: {metrics.method_count}")
2291
+ ```
2292
+ """
2293
+ return self._introspection_last_metrics
2294
+
2295
+ @asynccontextmanager
2296
+ async def track_operation(
2297
+ self,
2298
+ operation_name: str | None = None,
2299
+ ) -> AsyncIterator[None]:
2300
+ """Context manager for tracking active operations.
2301
+
2302
+ Provides coroutine-safe tracking of concurrent operations for
2303
+ heartbeat reporting. Increments the active operations counter
2304
+ on entry and decrements it on exit (whether successful or not).
2305
+
2306
+ Concurrency Safety:
2307
+ Uses asyncio.Lock for coroutine-safe counter updates.
2308
+ The lock is held only during counter updates, not during
2309
+ the operation itself. Logging occurs AFTER lock release
2310
+ to prevent blocking during I/O.
2311
+
2312
+ Error Handling:
2313
+ Counter updates are protected with try/except to ensure
2314
+ operation tracking failures don't affect the main operation.
2315
+ The counter will never go negative due to atomic operations.
2316
+
2317
+ Args:
2318
+ operation_name: Optional name for logging/debugging.
2319
+ Not used for counter logic but useful for diagnostics.
2320
+
2321
+ Yields:
2322
+ None. The context manager is used purely for side effects.
2323
+
2324
+ Example:
2325
+ ```python
2326
+ class MyNode(MixinNodeIntrospection):
2327
+ async def execute_query(self, query: str) -> Result:
2328
+ async with self.track_operation("execute_query"):
2329
+ # This operation is now tracked in heartbeats
2330
+ return await self._database.execute(query)
2331
+
2332
+ async def process_batch(self, items: list[Item]) -> None:
2333
+ # Track multiple concurrent operations
2334
+ async with asyncio.TaskGroup() as tg:
2335
+ for item in items:
2336
+ tg.create_task(self._process_with_tracking(item))
2337
+
2338
+ async def _process_with_tracking(self, item: Item) -> None:
2339
+ async with self.track_operation("process_item"):
2340
+ await self._process_single(item)
2341
+ ```
2342
+
2343
+ Note:
2344
+ The counter is read by ``_publish_heartbeat()`` to report
2345
+ the current number of active operations. This provides
2346
+ visibility into node load for monitoring and scaling.
2347
+ """
2348
+ # Increment counter on entry - capture count inside lock, log outside
2349
+ count_after_increment = 0
2350
+ increment_succeeded = False
2351
+ try:
2352
+ async with self._operations_lock:
2353
+ self._active_operations += 1
2354
+ count_after_increment = self._active_operations
2355
+ increment_succeeded = True
2356
+ except Exception as e:
2357
+ # Log but don't fail the operation
2358
+ logger.warning(
2359
+ f"Failed to increment operation counter: {e}",
2360
+ extra={
2361
+ "node_id": self._introspection_node_id,
2362
+ "operation": operation_name,
2363
+ "error_type": type(e).__name__,
2364
+ },
2365
+ )
2366
+
2367
+ # Log AFTER releasing lock to prevent blocking during I/O
2368
+ if increment_succeeded and operation_name:
2369
+ logger.debug(
2370
+ f"Operation started: {operation_name}",
2371
+ extra={
2372
+ "node_id": self._introspection_node_id,
2373
+ "operation": operation_name,
2374
+ "active_operations": count_after_increment,
2375
+ },
2376
+ )
2377
+
2378
+ try:
2379
+ yield
2380
+ finally:
2381
+ # Decrement counter on exit - capture state inside lock, log outside
2382
+ count_after_decrement = 0
2383
+ decrement_succeeded = False
2384
+ counter_was_zero = False
2385
+ try:
2386
+ async with self._operations_lock:
2387
+ # Prevent negative counter (defensive check)
2388
+ if self._active_operations > 0:
2389
+ self._active_operations -= 1
2390
+ else:
2391
+ counter_was_zero = True
2392
+ count_after_decrement = self._active_operations
2393
+ decrement_succeeded = True
2394
+ except Exception as e:
2395
+ # Log but don't fail the operation
2396
+ logger.warning(
2397
+ f"Failed to decrement operation counter: {e}",
2398
+ extra={
2399
+ "node_id": self._introspection_node_id,
2400
+ "operation": operation_name,
2401
+ "error_type": type(e).__name__,
2402
+ },
2403
+ )
2404
+
2405
+ # Log AFTER releasing lock to prevent blocking during I/O
2406
+ if decrement_succeeded:
2407
+ if counter_was_zero:
2408
+ # This should never happen, but log if it does
2409
+ logger.warning(
2410
+ "Active operations counter already at zero during decrement",
2411
+ extra={
2412
+ "node_id": self._introspection_node_id,
2413
+ "operation": operation_name,
2414
+ },
2415
+ )
2416
+ elif operation_name:
2417
+ logger.debug(
2418
+ f"Operation completed: {operation_name}",
2419
+ extra={
2420
+ "node_id": self._introspection_node_id,
2421
+ "operation": operation_name,
2422
+ "active_operations": count_after_decrement,
2423
+ },
2424
+ )
2425
+
2426
+ async def get_active_operations_count(self) -> int:
2427
+ """Get the current count of active operations.
2428
+
2429
+ Returns the number of operations currently being tracked via
2430
+ ``track_operation()``. This is the same value reported in
2431
+ heartbeat events.
2432
+
2433
+ Concurrency Safety:
2434
+ Uses asyncio.Lock for coroutine-safe counter access.
2435
+ The returned value is a snapshot; concurrent operations
2436
+ may change the count immediately after reading.
2437
+
2438
+ Returns:
2439
+ Current number of active operations (>= 0).
2440
+
2441
+ Example:
2442
+ ```python
2443
+ count = await node.get_active_operations_count()
2444
+ if count > threshold:
2445
+ logger.warning(f"High operation load: {count} active")
2446
+ ```
2447
+ """
2448
+ async with self._operations_lock:
2449
+ return self._active_operations
2450
+
2451
+
2452
+ __all__ = [
2453
+ "HEARTBEAT_TOPIC",
2454
+ "INTROSPECTION_TOPIC",
2455
+ "PERF_THRESHOLD_CACHE_HIT_MS",
2456
+ "PERF_THRESHOLD_DISCOVER_CAPABILITIES_MS",
2457
+ "PERF_THRESHOLD_GET_CAPABILITIES_MS",
2458
+ "PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS",
2459
+ "REQUEST_INTROSPECTION_TOPIC",
2460
+ "DiscoveredCapabilitiesCacheDict", # TypedDict for cached discovered capabilities
2461
+ "IntrospectionCacheDict",
2462
+ "MixinNodeIntrospection",
2463
+ "ModelIntrospectionPerformanceMetrics",
2464
+ "PerformanceMetricsCacheDict", # TypedDict for cached performance metrics
2465
+ ]