omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__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 (139) hide show
  1. omnibase_infra/constants_topic_patterns.py +26 -0
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  4. omnibase_infra/enums/enum_handler_source_mode.py +16 -2
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_binding_resolution.py +128 -0
  7. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
  8. omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
  9. omnibase_infra/event_bus/event_bus_kafka.py +105 -47
  10. omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
  11. omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
  12. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
  13. omnibase_infra/event_bus/testing/__init__.py +26 -0
  14. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  15. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  16. omnibase_infra/handlers/handler_consul.py +2 -0
  17. omnibase_infra/handlers/mixins/__init__.py +5 -0
  18. omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
  19. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  20. omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
  21. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  22. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  23. omnibase_infra/mixins/mixin_node_introspection.py +189 -19
  24. omnibase_infra/models/__init__.py +8 -0
  25. omnibase_infra/models/bindings/__init__.py +59 -0
  26. omnibase_infra/models/bindings/constants.py +144 -0
  27. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  28. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  29. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  30. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  31. omnibase_infra/models/discovery/model_introspection_config.py +25 -17
  32. omnibase_infra/models/dispatch/__init__.py +8 -0
  33. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  34. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  35. omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
  36. omnibase_infra/models/model_node_identity.py +126 -0
  37. omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
  38. omnibase_infra/models/registration/__init__.py +9 -0
  39. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  40. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  41. omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
  42. omnibase_infra/models/runtime/__init__.py +9 -0
  43. omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
  44. omnibase_infra/nodes/__init__.py +9 -0
  45. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  46. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  47. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  48. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  49. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  50. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  51. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  52. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  53. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  54. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  55. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  56. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  57. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  58. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  59. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  60. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  61. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  62. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  63. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  64. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  65. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  66. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  67. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  68. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  69. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  70. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  71. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  72. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  73. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  74. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  75. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  76. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  77. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  78. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  79. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  80. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  81. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  82. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  83. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  84. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  85. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  86. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
  87. omnibase_infra/nodes/reducers/models/__init__.py +7 -2
  88. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
  89. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  90. omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
  91. omnibase_infra/protocols/__init__.py +3 -0
  92. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  93. omnibase_infra/runtime/__init__.py +60 -0
  94. omnibase_infra/runtime/binding_resolver.py +753 -0
  95. omnibase_infra/runtime/constants_security.py +70 -0
  96. omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
  97. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  98. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  99. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  100. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  101. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  102. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  103. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  104. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  105. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  106. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  107. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  108. omnibase_infra/runtime/handler_source_resolver.py +43 -2
  109. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  110. omnibase_infra/runtime/models/__init__.py +13 -0
  111. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  112. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  113. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
  114. omnibase_infra/runtime/models/model_security_config.py +109 -0
  115. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  116. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  117. omnibase_infra/runtime/service_kernel.py +76 -6
  118. omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
  119. omnibase_infra/runtime/service_runtime_host_process.py +770 -20
  120. omnibase_infra/runtime/transition_notification_publisher.py +3 -2
  121. omnibase_infra/runtime/util_wiring.py +206 -62
  122. omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
  123. omnibase_infra/services/session/config_consumer.py +25 -8
  124. omnibase_infra/services/session/config_store.py +2 -2
  125. omnibase_infra/services/session/consumer.py +1 -1
  126. omnibase_infra/topics/__init__.py +45 -0
  127. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  128. omnibase_infra/topics/util_topic_composition.py +95 -0
  129. omnibase_infra/types/typed_dict/__init__.py +9 -1
  130. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  131. omnibase_infra/utils/__init__.py +9 -0
  132. omnibase_infra/utils/util_consumer_group.py +232 -0
  133. omnibase_infra/validation/infra_validators.py +18 -1
  134. omnibase_infra/validation/validation_exemptions.yaml +192 -0
  135. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
  136. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
  137. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
  138. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
  139. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,466 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Event bus subcontract wiring for contract-driven Kafka subscriptions.
4
+
5
+ This module provides the bridge between contract-declared topics (from the
6
+ `event_bus` subcontract) and actual Kafka subscriptions. The runtime owns
7
+ all Kafka plumbing - nodes/handlers never create consumers or producers directly.
8
+
9
+ Architecture:
10
+ The EventBusSubcontractWiring class is responsible for:
11
+ 1. Reading `subscribe_topics` from ModelEventBusSubcontract
12
+ 2. Resolving topic suffixes to full topic names with environment prefix
13
+ 3. Creating Kafka subscriptions with appropriate consumer groups
14
+ 4. Bridging received messages to the MessageDispatchEngine
15
+ 5. Managing subscription lifecycle (creation and cleanup)
16
+
17
+ This follows the ARCH-002 principle: "Runtime owns all Kafka plumbing."
18
+ Nodes and handlers declare their topic requirements in contracts, but
19
+ never directly interact with Kafka consumers or producers.
20
+
21
+ Topic Resolution:
22
+ Topic suffixes from contracts follow the ONEX naming convention:
23
+ onex.{kind}.{producer}.{event-name}.v{n}
24
+
25
+ The wiring resolves these to full topics by prepending the environment:
26
+ {environment}.onex.{kind}.{producer}.{event-name}.v{n}
27
+
28
+ Example:
29
+ - Contract declares: "onex.evt.omniintelligence.intent-classified.v1"
30
+ - Resolved (dev): "dev.onex.evt.omniintelligence.intent-classified.v1"
31
+ - Resolved (prod): "prod.onex.evt.omniintelligence.intent-classified.v1"
32
+
33
+ Related:
34
+ - OMN-1621: Runtime consumes event_bus subcontract for contract-driven wiring
35
+ - ModelEventBusSubcontract: Contract model defining subscribe/publish topics
36
+ - MessageDispatchEngine: Dispatch engine that processes received messages
37
+ - EventBusKafka: Kafka event bus implementation
38
+
39
+ .. versionadded:: 0.2.5
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import json
45
+ import logging
46
+ from collections.abc import Awaitable, Callable
47
+ from pathlib import Path
48
+ from typing import TYPE_CHECKING
49
+
50
+ import yaml
51
+ from pydantic import ValidationError
52
+
53
+ from omnibase_core.models.contracts.subcontracts import ModelEventBusSubcontract
54
+ from omnibase_core.models.events.model_event_envelope import ModelEventEnvelope
55
+ from omnibase_core.protocols.event_bus.protocol_event_bus_subscriber import (
56
+ ProtocolEventBusSubscriber,
57
+ )
58
+ from omnibase_core.protocols.event_bus.protocol_event_message import (
59
+ ProtocolEventMessage,
60
+ )
61
+ from omnibase_infra.enums import EnumInfraTransportType
62
+ from omnibase_infra.errors import ModelInfraErrorContext, RuntimeHostError
63
+ from omnibase_infra.protocols import ProtocolDispatchEngine
64
+
65
+ if TYPE_CHECKING:
66
+ from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
67
+ from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
68
+ from omnibase_infra.runtime.service_message_dispatch_engine import (
69
+ MessageDispatchEngine,
70
+ )
71
+
72
+
73
+ class EventBusSubcontractWiring:
74
+ """Wires event_bus subcontracts to Kafka subscriptions and publishers.
75
+
76
+ This class bridges contract-declared topics to actual Kafka subscriptions,
77
+ ensuring that nodes/handlers never directly interact with Kafka infrastructure.
78
+ The runtime owns all Kafka plumbing per ARCH-002.
79
+
80
+ Responsibilities:
81
+ - Parse subscribe_topics from ModelEventBusSubcontract
82
+ - Resolve topic suffixes to full topic names with environment prefix
83
+ - Create Kafka subscriptions with appropriate consumer groups
84
+ - Deserialize incoming messages to ModelEventEnvelope
85
+ - Dispatch envelopes to MessageDispatchEngine
86
+ - Manage subscription lifecycle (cleanup on shutdown)
87
+
88
+ Thread Safety:
89
+ This class is designed for single-threaded async use. All subscription
90
+ operations should be performed from a single async context. The underlying
91
+ event bus implementations (EventBusKafka, EventBusInmemory) handle their
92
+ own thread safety for message delivery.
93
+
94
+ Example:
95
+ ```python
96
+ from omnibase_infra.runtime import EventBusSubcontractWiring
97
+ from omnibase_core.models.contracts.subcontracts import ModelEventBusSubcontract
98
+
99
+ # Create wiring with event bus and dispatch engine
100
+ wiring = EventBusSubcontractWiring(
101
+ event_bus=event_bus,
102
+ dispatch_engine=dispatch_engine,
103
+ environment="dev",
104
+ )
105
+
106
+ # Wire subscriptions from subcontract
107
+ subcontract = ModelEventBusSubcontract(
108
+ version=ModelSemVer(major=1, minor=0, patch=0),
109
+ subscribe_topics=["onex.evt.omniintelligence.intent-classified.v1"],
110
+ )
111
+ await wiring.wire_subscriptions(subcontract, node_name="my-handler")
112
+
113
+ # Cleanup on shutdown
114
+ await wiring.cleanup()
115
+ ```
116
+
117
+ Attributes:
118
+ _event_bus: The event bus implementation (Kafka or in-memory)
119
+ _dispatch_engine: Engine to dispatch received messages to handlers
120
+ _environment: Environment prefix for topics (e.g., 'dev', 'prod')
121
+ _unsubscribe_callables: List of callables to unsubscribe from topics
122
+ _logger: Logger for debug and error messages
123
+
124
+ .. versionadded:: 0.2.5
125
+ """
126
+
127
+ def __init__(
128
+ self,
129
+ event_bus: ProtocolEventBusSubscriber,
130
+ dispatch_engine: ProtocolDispatchEngine,
131
+ environment: str,
132
+ ) -> None:
133
+ """Initialize event bus wiring.
134
+
135
+ Args:
136
+ event_bus: The event bus implementation (EventBusKafka or EventBusInmemory).
137
+ Must implement subscribe(topic, group_id, on_message) -> unsubscribe callable.
138
+ Duck typed per ONEX patterns.
139
+ dispatch_engine: Engine to dispatch received messages to handlers.
140
+ Must implement ProtocolDispatchEngine interface.
141
+ Must be frozen (registrations complete) before wiring subscriptions.
142
+ environment: Environment prefix for topics (e.g., 'dev', 'prod').
143
+ Used to resolve topic suffixes to full topic names.
144
+
145
+ Note:
146
+ The dispatch_engine should be frozen before wiring subscriptions.
147
+ Attempting to dispatch to an unfrozen engine will raise an error.
148
+
149
+ Raises:
150
+ ValueError: If environment is empty or whitespace-only.
151
+ """
152
+ if not environment or not environment.strip():
153
+ raise ValueError("environment must be a non-empty string")
154
+
155
+ self._event_bus = event_bus
156
+ self._dispatch_engine = dispatch_engine
157
+ self._environment = environment
158
+ self._unsubscribe_callables: list[Callable[[], Awaitable[None]]] = []
159
+ self._logger = logging.getLogger(__name__)
160
+
161
+ def resolve_topic(self, topic_suffix: str) -> str:
162
+ """Resolve topic suffix to full topic name with environment prefix.
163
+
164
+ Topic suffixes from contracts follow the ONEX naming convention:
165
+ onex.{kind}.{producer}.{event-name}.v{n}
166
+
167
+ This method prepends the environment to create the full topic name:
168
+ {environment}.onex.{kind}.{producer}.{event-name}.v{n}
169
+
170
+ Args:
171
+ topic_suffix: ONEX format topic suffix
172
+ (e.g., 'onex.evt.omniintelligence.intent-classified.v1')
173
+
174
+ Returns:
175
+ Full topic name with environment prefix
176
+ (e.g., 'dev.onex.evt.omniintelligence.intent-classified.v1')
177
+
178
+ Example:
179
+ >>> wiring = EventBusSubcontractWiring(bus, engine, "dev")
180
+ >>> wiring.resolve_topic("onex.evt.user.created.v1")
181
+ 'dev.onex.evt.user.created.v1'
182
+ """
183
+ return f"{self._environment}.{topic_suffix}"
184
+
185
+ async def wire_subscriptions(
186
+ self,
187
+ subcontract: ModelEventBusSubcontract,
188
+ node_name: str,
189
+ ) -> None:
190
+ """Wire Kafka subscriptions from subcontract.subscribe_topics.
191
+
192
+ Creates Kafka subscriptions for each topic declared in the subcontract's
193
+ subscribe_topics list. Each subscription uses a consumer group ID based
194
+ on the environment and node name for proper load balancing.
195
+
196
+ Consumer Group Naming:
197
+ Consumer groups are named as: {environment}.{node_name}
198
+ Example: "dev.registration-handler"
199
+
200
+ This ensures:
201
+ - Each node instance in an environment shares the same consumer group
202
+ - Multiple instances of the same node load-balance message processing
203
+ - Different environments are completely isolated
204
+
205
+ Args:
206
+ subcontract: The event_bus subcontract from a handler's contract.
207
+ Contains subscribe_topics list with topic suffixes.
208
+ node_name: Name of the node/handler for consumer group identification.
209
+ Should be unique per handler type (e.g., "registration-handler").
210
+
211
+ Raises:
212
+ InfraConnectionError: If Kafka connection fails during subscription.
213
+ InfraTimeoutError: If subscription times out.
214
+
215
+ Example:
216
+ >>> subcontract = ModelEventBusSubcontract(
217
+ ... version=ModelSemVer(major=1, minor=0, patch=0),
218
+ ... subscribe_topics=["onex.evt.node.introspected.v1"],
219
+ ... )
220
+ >>> await wiring.wire_subscriptions(subcontract, "registration-handler")
221
+ """
222
+ if not subcontract.subscribe_topics:
223
+ self._logger.debug(
224
+ "No subscribe_topics in subcontract for node '%s'",
225
+ node_name,
226
+ )
227
+ return
228
+
229
+ for topic_suffix in subcontract.subscribe_topics:
230
+ full_topic = self.resolve_topic(topic_suffix)
231
+ group_id = f"{self._environment}.{node_name}"
232
+
233
+ # Create dispatch callback for this topic
234
+ callback = self._create_dispatch_callback(full_topic)
235
+
236
+ # Subscribe and store unsubscribe callable
237
+ unsubscribe = await self._event_bus.subscribe(
238
+ topic=full_topic,
239
+ group_id=group_id,
240
+ on_message=callback,
241
+ )
242
+ self._unsubscribe_callables.append(unsubscribe)
243
+
244
+ self._logger.info(
245
+ "Wired subscription: topic=%s, group_id=%s, node=%s",
246
+ full_topic,
247
+ group_id,
248
+ node_name,
249
+ )
250
+
251
+ def _create_dispatch_callback(
252
+ self,
253
+ topic: str,
254
+ ) -> Callable[[ProtocolEventMessage], Awaitable[None]]:
255
+ """Create callback that bridges Kafka consumer to dispatch engine.
256
+
257
+ Creates an async callback function that:
258
+ 1. Receives ProtocolEventMessage from the Kafka consumer
259
+ 2. Deserializes the message value to ModelEventEnvelope
260
+ 3. Dispatches the envelope to the MessageDispatchEngine
261
+
262
+ Error Handling:
263
+ - Deserialization errors are logged and the message is skipped
264
+ - Dispatch errors are propagated (handled by the event bus DLQ logic)
265
+
266
+ Args:
267
+ topic: The full topic name for routing context in logs.
268
+
269
+ Returns:
270
+ Async callback function compatible with event bus subscribe().
271
+ """
272
+
273
+ async def callback(message: ProtocolEventMessage) -> None:
274
+ """Process incoming Kafka message and dispatch to engine."""
275
+ try:
276
+ envelope = self._deserialize_to_envelope(message)
277
+ # Dispatch via ProtocolDispatchEngine interface
278
+ await self._dispatch_engine.dispatch(topic, envelope)
279
+ except json.JSONDecodeError as e:
280
+ self._logger.exception(
281
+ "Failed to deserialize message from topic '%s': %s",
282
+ topic,
283
+ e,
284
+ )
285
+ # Wrap in OnexError per CLAUDE.md: "OnexError Only"
286
+ raise RuntimeHostError(
287
+ f"Failed to deserialize message from topic '{topic}'",
288
+ context=ModelInfraErrorContext.with_correlation(
289
+ transport_type=EnumInfraTransportType.KAFKA,
290
+ operation="event_bus_deserialize",
291
+ ),
292
+ ) from e
293
+ except Exception as e:
294
+ self._logger.exception(
295
+ "Failed to dispatch message from topic '%s': %s",
296
+ topic,
297
+ e,
298
+ )
299
+ # Wrap in OnexError per CLAUDE.md: "OnexError Only"
300
+ raise RuntimeHostError(
301
+ f"Failed to dispatch message from topic '{topic}'",
302
+ context=ModelInfraErrorContext.with_correlation(
303
+ transport_type=EnumInfraTransportType.KAFKA,
304
+ operation="event_bus_dispatch",
305
+ ),
306
+ ) from e
307
+
308
+ return callback
309
+
310
+ def _deserialize_to_envelope(
311
+ self,
312
+ message: ProtocolEventMessage,
313
+ ) -> ModelEventEnvelope[object]:
314
+ """Deserialize Kafka message to event envelope.
315
+
316
+ Converts the raw bytes in ProtocolEventMessage.value to a ModelEventEnvelope
317
+ that can be processed by the dispatch engine.
318
+
319
+ Deserialization Strategy:
320
+ 1. Decode message.value from UTF-8 bytes to string
321
+ 2. Parse JSON string to dict
322
+ 3. Validate and construct ModelEventEnvelope
323
+
324
+ Args:
325
+ message: ProtocolEventMessage from Kafka consumer containing raw bytes.
326
+
327
+ Returns:
328
+ Deserialized ModelEventEnvelope for dispatch.
329
+
330
+ Raises:
331
+ json.JSONDecodeError: If message value is not valid JSON.
332
+ ValidationError: If JSON does not match ModelEventEnvelope schema.
333
+ """
334
+ # Decode bytes to string
335
+ json_str = message.value.decode("utf-8")
336
+
337
+ # Parse JSON to dict
338
+ data = json.loads(json_str)
339
+
340
+ # Validate and construct envelope
341
+ return ModelEventEnvelope[object].model_validate(data)
342
+
343
+ async def cleanup(self) -> None:
344
+ """Unsubscribe from all topics.
345
+
346
+ Should be called during runtime shutdown to properly clean up
347
+ Kafka consumer subscriptions. This ensures:
348
+ - Consumer group offsets are committed
349
+ - Connections are properly closed
350
+ - Resources are released
351
+
352
+ This method is safe to call multiple times - subsequent calls
353
+ are no-ops after the first successful cleanup.
354
+
355
+ Example:
356
+ >>> # During shutdown
357
+ >>> await wiring.cleanup()
358
+ """
359
+ cleanup_count = len(self._unsubscribe_callables)
360
+
361
+ for unsubscribe in self._unsubscribe_callables:
362
+ try:
363
+ await unsubscribe()
364
+ except Exception as e:
365
+ self._logger.warning(
366
+ "Error during unsubscribe: %s",
367
+ e,
368
+ )
369
+
370
+ self._unsubscribe_callables.clear()
371
+
372
+ if cleanup_count > 0:
373
+ self._logger.info(
374
+ "Cleaned up %d event bus subscription(s)",
375
+ cleanup_count,
376
+ )
377
+
378
+
379
+ def load_event_bus_subcontract(
380
+ contract_path: Path,
381
+ logger: logging.Logger | None = None,
382
+ ) -> ModelEventBusSubcontract | None:
383
+ """Load event_bus subcontract from contract YAML file.
384
+
385
+ Reads a contract YAML file and extracts the event_bus section,
386
+ returning a validated ModelEventBusSubcontract if present.
387
+
388
+ File Format:
389
+ The contract YAML should have an `event_bus` section:
390
+
391
+ ```yaml
392
+ event_bus:
393
+ version:
394
+ major: 1
395
+ minor: 0
396
+ patch: 0
397
+ subscribe_topics:
398
+ - onex.evt.node.introspected.v1
399
+ - onex.evt.node.registered.v1
400
+ publish_topics:
401
+ - onex.cmd.node.register.v1
402
+ ```
403
+
404
+ Args:
405
+ contract_path: Path to the contract YAML file.
406
+ logger: Optional logger for warnings. If not provided, uses module logger.
407
+
408
+ Returns:
409
+ ModelEventBusSubcontract if event_bus section exists and is valid,
410
+ None otherwise.
411
+
412
+ Example:
413
+ >>> subcontract = load_event_bus_subcontract(Path("contract.yaml"))
414
+ >>> if subcontract:
415
+ ... print(f"Subscribe topics: {subcontract.subscribe_topics}")
416
+ """
417
+ _logger = logger or logging.getLogger(__name__)
418
+
419
+ if not contract_path.exists():
420
+ _logger.warning(
421
+ "Contract file not found: %s",
422
+ contract_path,
423
+ )
424
+ return None
425
+
426
+ try:
427
+ with contract_path.open() as f:
428
+ contract_data = yaml.safe_load(f)
429
+
430
+ if contract_data is None:
431
+ _logger.warning(
432
+ "Empty contract file: %s",
433
+ contract_path,
434
+ )
435
+ return None
436
+
437
+ event_bus_data = contract_data.get("event_bus")
438
+ if not event_bus_data:
439
+ _logger.debug(
440
+ "No event_bus section in contract: %s",
441
+ contract_path,
442
+ )
443
+ return None
444
+
445
+ return ModelEventBusSubcontract.model_validate(event_bus_data)
446
+
447
+ except yaml.YAMLError as e:
448
+ _logger.warning(
449
+ "Failed to parse YAML in contract %s: %s",
450
+ contract_path,
451
+ e,
452
+ )
453
+ return None
454
+ except ValidationError as e:
455
+ _logger.warning(
456
+ "Invalid event_bus subcontract in %s: %s",
457
+ contract_path,
458
+ e,
459
+ )
460
+ return None
461
+
462
+
463
+ __all__: list[str] = [
464
+ "EventBusSubcontractWiring",
465
+ "load_event_bus_subcontract",
466
+ ]
@@ -3,7 +3,7 @@
3
3
  """Handler Source Resolver for Multi-Source Handler Discovery.
4
4
 
5
5
  This module provides the HandlerSourceResolver class, which resolves handlers
6
- from multiple sources (bootstrap, contract) based on the configured mode.
6
+ from multiple sources (bootstrap, contract, Kafka events) based on the configured mode.
7
7
 
8
8
  Part of OMN-1095: Handler Source Mode Hybrid Resolution.
9
9
 
@@ -11,6 +11,7 @@ Resolution Modes:
11
11
  - BOOTSTRAP: Only use hardcoded bootstrap handlers.
12
12
  - CONTRACT: Only use YAML contract-discovered handlers.
13
13
  - HYBRID: Per-handler resolution with configurable precedence.
14
+ - KAFKA_EVENTS: Use Kafka-based contract source for cache-based discovery.
14
15
 
15
16
  In HYBRID mode, the resolver performs per-handler identity resolution:
16
17
  1. Discovers handlers from both bootstrap and contract sources
@@ -20,10 +21,16 @@ In HYBRID mode, the resolver performs per-handler identity resolution:
20
21
  - True: Bootstrap handlers override contract handlers
21
22
  4. Non-conflicting handlers are included from both sources
22
23
 
24
+ In KAFKA_EVENTS mode, the resolver delegates to a KafkaContractSource instance
25
+ that returns cached descriptors from contract registration events. This is a
26
+ beta cache-only implementation where discovered contracts take effect on the
27
+ next runtime restart.
28
+
23
29
  See Also:
24
30
  - EnumHandlerSourceMode: Defines the resolution modes
25
31
  - HandlerBootstrapSource: Provides bootstrap handlers
26
32
  - HandlerContractSource: Provides contract-discovered handlers
33
+ - KafkaContractSource: Provides Kafka cache-based handler discovery
27
34
  - ProtocolContractSource: Protocol for handler sources
28
35
 
29
36
  .. versionadded:: 0.7.0
@@ -60,7 +67,7 @@ class HandlerSourceResolver:
60
67
  """Resolver for multi-source handler discovery with configurable modes.
61
68
 
62
69
  This class resolves handlers from bootstrap and contract sources based on
63
- the configured mode. It supports three resolution strategies:
70
+ the configured mode. It supports four resolution strategies:
64
71
 
65
72
  - BOOTSTRAP: Use only bootstrap handlers, ignore contracts.
66
73
  - CONTRACT: Use only contract handlers, ignore bootstrap.
@@ -70,6 +77,9 @@ class HandlerSourceResolver:
70
77
  - allow_bootstrap_override=True: Bootstrap handlers take precedence
71
78
  over contract handlers with the same handler_id.
72
79
  In both cases, handlers without conflicts are included from both sources.
80
+ - KAFKA_EVENTS: Use Kafka-based contract source for cache-based discovery.
81
+ Delegates to a KafkaContractSource that returns cached descriptors from
82
+ contract registration events. This is a beta cache-only implementation.
73
83
 
74
84
  Attributes:
75
85
  mode: The configured resolution mode.
@@ -142,6 +152,7 @@ class HandlerSourceResolver:
142
152
  - BOOTSTRAP: Only queries bootstrap source
143
153
  - CONTRACT: Only queries contract source
144
154
  - HYBRID: Queries both sources and merges with contract precedence
155
+ - KAFKA_EVENTS: Queries Kafka-based contract cache
145
156
 
146
157
  Returns:
147
158
  ModelContractDiscoveryResult: Container with discovered descriptors
@@ -151,6 +162,8 @@ class HandlerSourceResolver:
151
162
  return await self._resolve_bootstrap()
152
163
  elif self._mode == EnumHandlerSourceMode.CONTRACT:
153
164
  return await self._resolve_contract()
165
+ elif self._mode == EnumHandlerSourceMode.KAFKA_EVENTS:
166
+ return await self._resolve_kafka_events()
154
167
  else:
155
168
  # HYBRID mode
156
169
  return await self._resolve_hybrid()
@@ -193,6 +206,34 @@ class HandlerSourceResolver:
193
206
 
194
207
  return result
195
208
 
209
+ async def _resolve_kafka_events(self) -> ModelContractDiscoveryResult:
210
+ """Resolve handlers using Kafka-based contract source.
211
+
212
+ For KAFKA_EVENTS mode, the contract_source is expected to be a
213
+ KafkaContractSource instance that returns cached descriptors from
214
+ contract registration events.
215
+
216
+ Note:
217
+ This is a beta cache-only implementation. Discovered contracts
218
+ take effect on the next runtime restart.
219
+
220
+ Returns:
221
+ ModelContractDiscoveryResult: Discovery result from the Kafka
222
+ contract cache.
223
+ """
224
+ result = await self._contract_source.discover_handlers()
225
+
226
+ logger.info(
227
+ "Handler resolution completed (KAFKA_EVENTS mode)",
228
+ extra={
229
+ "mode": self._mode.value,
230
+ "kafka_handler_count": len(result.descriptors),
231
+ "resolved_handler_count": len(result.descriptors),
232
+ },
233
+ )
234
+
235
+ return result
236
+
196
237
  async def _resolve_hybrid(self) -> ModelContractDiscoveryResult:
197
238
  """Resolve handlers using both sources with configurable precedence.
198
239