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
@@ -49,7 +49,7 @@ Example Usage:
49
49
  # Initialize publisher with event bus
50
50
  publisher = TransitionNotificationPublisher(
51
51
  event_bus=kafka_event_bus,
52
- topic="onex.fsm.state.transitions.v1",
52
+ topic=SUFFIX_FSM_STATE_TRANSITIONS,
53
53
  )
54
54
 
55
55
  # Publish single notification
@@ -110,6 +110,7 @@ from omnibase_infra.models.resilience import ModelCircuitBreakerConfig
110
110
  from omnibase_infra.runtime.models.model_transition_notification_publisher_metrics import (
111
111
  ModelTransitionNotificationPublisherMetrics,
112
112
  )
113
+ from omnibase_infra.topics import SUFFIX_FSM_STATE_TRANSITIONS
113
114
  from omnibase_infra.utils.util_error_sanitization import sanitize_error_string
114
115
 
115
116
  if TYPE_CHECKING:
@@ -754,7 +755,7 @@ def _verify_protocol_compliance() -> None: # pragma: no cover
754
755
  publisher: ProtocolTransitionNotificationPublisher = (
755
756
  TransitionNotificationPublisher(
756
757
  event_bus=bus,
757
- topic="onex.fsm.state.transitions.v1",
758
+ topic=SUFFIX_FSM_STATE_TRANSITIONS,
758
759
  )
759
760
  )
760
761
  # Use the variable to silence unused warnings
@@ -7,11 +7,20 @@ with the RegistryProtocolBinding and RegistryEventBusBinding. It serves as
7
7
  the bridge between handler implementations and the registry system.
8
8
 
9
9
  The wiring module is responsible for:
10
- - Registering default handlers for standard protocol types
10
+ - Registering default handlers from contract.yaml files
11
11
  - Registering handlers based on contract configuration
12
12
  - Validating that requested handler types are known and supported
13
13
  - Providing a summary of registered handlers for debugging
14
14
 
15
+ Contract-Driven Handler Loading:
16
+ Handler classes are discovered and loaded from contract.yaml files located
17
+ in nodes/handlers/<handler_type>/contract.yaml. Each contract specifies:
18
+ - handler.module: The Python module path
19
+ - handler.name: The class name to load
20
+
21
+ This replaces the old hardcoded _KNOWN_HANDLERS dict with dynamic,
22
+ contract-based discovery.
23
+
15
24
  Event Bus Support:
16
25
  This module registers EventBusInmemory as the default event bus. For production
17
26
  deployments requiring EventBusKafka, the event bus is selected at kernel bootstrap
@@ -22,10 +31,13 @@ Event Bus Support:
22
31
  See kernel.py for event bus selection logic during runtime bootstrap.
23
32
 
24
33
  Design Principles:
34
+ - Contract-driven: Handler configurations live in contract.yaml, not Python code
25
35
  - Explicit wiring: All handler registrations are explicit, not auto-discovered
26
- - Contract-driven: Supports wiring from contract configuration dicts
27
36
  - Validation: Unknown handler types raise clear errors
37
+ - Fail-fast: Missing contracts raise FileNotFoundError immediately
28
38
  - Idempotent: Re-wiring the same handler is safe (overwrites previous)
39
+ - Security: Namespace allowlisting is recommended for production deployments
40
+ (see docs/patterns/handler_plugin_loader.md#optional-security-controls)
29
41
 
30
42
  Adding New Handlers:
31
43
  To add a new handler to the system, follow these steps:
@@ -48,16 +60,24 @@ Adding New Handlers:
48
60
  return {"success": True, "data": ...}
49
61
  ```
50
62
 
51
- 2. Add the handler to _KNOWN_HANDLERS dict with a type constant:
52
-
53
- In handler_registry.py, add a constant:
54
- HANDLER_TYPE_CUSTOM = "custom"
63
+ 2. Create a contract.yaml in nodes/handlers/<type>/contract.yaml:
64
+
65
+ ```yaml
66
+ name: "handler_custom"
67
+ node_type: "EFFECT_GENERIC"
68
+ description: "Custom protocol handler"
69
+ handler_routing:
70
+ routing_strategy: "operation_match"
71
+ handlers:
72
+ - handler_type: "custom"
73
+ handler:
74
+ name: "MyCustomHandler"
75
+ module: "mypackage.handlers.handler_custom"
76
+ ```
55
77
 
56
- In this module, add to _KNOWN_HANDLERS:
57
- HANDLER_TYPE_CUSTOM: (MyCustomHandler, "Custom protocol handler"),
78
+ 3. Add the contract path to _HANDLER_CONTRACT_PATHS in this module.
58
79
 
59
- 3. For runtime registration without modifying _KNOWN_HANDLERS, use
60
- wire_custom_handler():
80
+ 4. For runtime registration without contracts, use wire_custom_handler():
61
81
 
62
82
  ```python
63
83
  from omnibase_infra.runtime.util_wiring import wire_custom_handler
@@ -94,7 +114,7 @@ Example Usage:
94
114
  wire_handlers_from_contract,
95
115
  )
96
116
 
97
- # Wire all default handlers
117
+ # Wire all default handlers from contracts
98
118
  summary = wire_default_handlers()
99
119
  print(f"Registered handlers: {summary['handlers']}")
100
120
  print(f"Registered event buses: {summary['event_buses']}")
@@ -113,19 +133,16 @@ Example Usage:
113
133
 
114
134
  from __future__ import annotations
115
135
 
136
+ import importlib
116
137
  import logging
138
+ from pathlib import Path
117
139
  from typing import TYPE_CHECKING
118
140
 
141
+ import yaml
142
+
119
143
  from omnibase_core.types import JsonType
120
144
  from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
121
145
  from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
122
- from omnibase_infra.handlers.handler_consul import HandlerConsul
123
- from omnibase_infra.handlers.handler_db import HandlerDb
124
- from omnibase_infra.handlers.handler_graph import HandlerGraph
125
- from omnibase_infra.handlers.handler_http import HandlerHttpRest
126
- from omnibase_infra.handlers.handler_intent import HandlerIntent
127
- from omnibase_infra.handlers.handler_mcp import HandlerMCP
128
- from omnibase_infra.handlers.handler_vault import HandlerVault
129
146
  from omnibase_infra.runtime.handler_registry import (
130
147
  EVENT_BUS_INMEMORY,
131
148
  HANDLER_TYPE_CONSUL,
@@ -147,39 +164,20 @@ if TYPE_CHECKING:
147
164
 
148
165
  logger = logging.getLogger(__name__)
149
166
 
150
- # Known handler types that can be wired.
151
- #
152
- # Pattern: handler_type_constant -> (handler_class, description)
153
- #
154
- # - handler_type_constant: String constant defined in handler_registry.py
155
- # (e.g., HANDLER_TYPE_HTTP = "http"). This is the key used to look up
156
- # and route envelopes to the correct handler.
157
- #
158
- # - handler_class: The class implementing ProtocolHandler protocol.
159
- # Must have async initialize(config) and async execute(envelope) methods.
160
- # The wiring module registers the CLASS; RuntimeHostProcess instantiates it.
161
- #
162
- # - description: Human-readable description for logging and debugging.
163
- # Appears in log messages when handlers are registered.
164
- #
165
- # To add a new handler:
166
- # 1. Define HANDLER_TYPE_XXX constant in handler_registry.py
167
- # 2. Import the handler class at the top of this module
168
- # 3. Add entry below: HANDLER_TYPE_XXX: (XxxHandler, "Description"),
169
- #
170
- # NOTE: HandlerHttpRest and HandlerDb use legacy execute(envelope: dict) signature.
171
- # They will be migrated to ProtocolHandler.execute(request, operation_config) in future.
172
- # Type ignore comments suppress MyPy errors during MVP phase.
173
- _KNOWN_HANDLERS: dict[str, tuple[type[ProtocolContainerAware], str]] = {
174
- # NOTE: Handlers implement ProtocolHandler structurally but concrete types differ from protocol.
175
- HANDLER_TYPE_CONSUL: (HandlerConsul, "HashiCorp Consul service discovery handler"), # type: ignore[dict-item] # NOTE: structural subtyping
176
- HANDLER_TYPE_DATABASE: (HandlerDb, "PostgreSQL database handler"), # type: ignore[dict-item] # NOTE: structural subtyping
177
- HANDLER_TYPE_GRAPH: (HandlerGraph, "Graph database (Memgraph/Neo4j) handler"), # type: ignore[dict-item] # NOTE: structural subtyping
178
- HANDLER_TYPE_HTTP: (HandlerHttpRest, "HTTP REST protocol handler"), # type: ignore[dict-item] # NOTE: structural subtyping
179
- # DEMO: Temporary registration - remove when contract-driven (OMN-1515)
180
- HANDLER_TYPE_INTENT: (HandlerIntent, "Intent storage and query handler for demo"), # type: ignore[dict-item] # NOTE: structural subtyping
181
- HANDLER_TYPE_MCP: (HandlerMCP, "Model Context Protocol handler for AI agents"), # type: ignore[dict-item] # NOTE: structural subtyping
182
- HANDLER_TYPE_VAULT: (HandlerVault, "HashiCorp Vault secret management handler"), # type: ignore[dict-item] # NOTE: structural subtyping
167
+ # Handler contract directory path.
168
+ # Handler configurations are loaded from contract.yaml files in this directory.
169
+ _HANDLERS_BASE = Path(__file__).parent.parent / "nodes" / "handlers"
170
+
171
+ # Mapping of handler types to their contract paths.
172
+ # Each entry maps a handler type constant to the path of its contract.yaml file.
173
+ _HANDLER_CONTRACT_PATHS: dict[str, Path] = {
174
+ HANDLER_TYPE_CONSUL: _HANDLERS_BASE / "consul" / "contract.yaml",
175
+ HANDLER_TYPE_DATABASE: _HANDLERS_BASE / "db" / "contract.yaml",
176
+ HANDLER_TYPE_GRAPH: _HANDLERS_BASE / "graph" / "contract.yaml",
177
+ HANDLER_TYPE_HTTP: _HANDLERS_BASE / "http" / "contract.yaml",
178
+ HANDLER_TYPE_INTENT: _HANDLERS_BASE / "intent" / "contract.yaml",
179
+ HANDLER_TYPE_MCP: _HANDLERS_BASE / "mcp" / "contract.yaml",
180
+ HANDLER_TYPE_VAULT: _HANDLERS_BASE / "vault" / "contract.yaml",
183
181
  }
184
182
 
185
183
  # Known event bus kinds that can be wired via this module.
@@ -194,6 +192,142 @@ _KNOWN_EVENT_BUSES: dict[str, tuple[type[ProtocolEventBus], str]] = {
194
192
  }
195
193
 
196
194
 
195
+ def _load_handler_from_contract(
196
+ handler_type: str, contract_path: Path
197
+ ) -> tuple[type[ProtocolContainerAware], str]:
198
+ """Load handler class from a contract.yaml file.
199
+
200
+ Args:
201
+ handler_type: The handler type identifier (e.g., "consul", "db").
202
+ contract_path: Path to the contract.yaml file.
203
+
204
+ Returns:
205
+ Tuple of (handler_class, description).
206
+
207
+ Raises:
208
+ FileNotFoundError: If contract file does not exist.
209
+ ProtocolConfigurationError: If contract is malformed or handler cannot be loaded.
210
+
211
+ Security Note:
212
+ This function uses ``importlib.import_module()`` to dynamically load handler
213
+ modules specified in contracts. This means contract files are effectively
214
+ executable code - a compromised contract pointing to a malicious module
215
+ will execute that module's code during import.
216
+
217
+ **Production Security Recommendations:**
218
+
219
+ 1. **Namespace Allowlisting**: For dynamic handler discovery scenarios,
220
+ use ``HandlerPluginLoader`` with the ``allowed_namespaces`` parameter
221
+ to restrict which module namespaces can be loaded::
222
+
223
+ from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
224
+
225
+ loader = HandlerPluginLoader(
226
+ allowed_namespaces=["omnibase_infra.", "omnibase_core.", "myapp.handlers."]
227
+ )
228
+
229
+ This prevents loading handlers from untrusted namespaces even if a
230
+ contract is compromised.
231
+
232
+ 2. **Write Protection**: Contract directories should be read-only at runtime.
233
+ Mount contract directories as read-only volumes in containerized deployments.
234
+
235
+ 3. **Source Validation**: Contracts in ``_HANDLER_CONTRACT_PATHS`` come from
236
+ the omnibase_infra package. Ensure these are from trusted, version-controlled
237
+ sources with code review.
238
+
239
+ See Also:
240
+ - ``docs/patterns/handler_plugin_loader.md#optional-security-controls``
241
+ - ``docs/patterns/security_patterns.md``
242
+ - ``docs/decisions/adr-handler-plugin-loader-security.md``
243
+ """
244
+ if not contract_path.exists():
245
+ raise FileNotFoundError(
246
+ f"Handler contract not found: {contract_path}. "
247
+ f"All handlers must have contract.yaml files."
248
+ )
249
+
250
+ with contract_path.open("r") as f:
251
+ contract = yaml.safe_load(f)
252
+
253
+ if contract is None:
254
+ context = ModelInfraErrorContext.with_correlation(
255
+ operation="load_handler_contract",
256
+ target_name=str(contract_path),
257
+ )
258
+ raise ProtocolConfigurationError(
259
+ f"Empty contract file: {contract_path}",
260
+ context=context,
261
+ )
262
+
263
+ handler_routing = contract.get("handler_routing", {})
264
+ handlers = handler_routing.get("handlers", [])
265
+
266
+ if not handlers:
267
+ context = ModelInfraErrorContext.with_correlation(
268
+ operation="load_handler_contract",
269
+ target_name=str(contract_path),
270
+ )
271
+ raise ProtocolConfigurationError(
272
+ f"No handlers defined in contract: {contract_path}",
273
+ context=context,
274
+ )
275
+
276
+ handler_def = handlers[0]
277
+ handler_info = handler_def.get("handler", {})
278
+ handler_module = handler_info.get("module")
279
+ handler_class_name = handler_info.get("name")
280
+
281
+ if not handler_module or not handler_class_name:
282
+ context = ModelInfraErrorContext.with_correlation(
283
+ operation="load_handler_contract",
284
+ target_name=str(contract_path),
285
+ )
286
+ raise ProtocolConfigurationError(
287
+ f"Missing handler module or name in contract: {contract_path}. "
288
+ f"Expected handler.module and handler.name fields.",
289
+ context=context,
290
+ )
291
+
292
+ try:
293
+ module = importlib.import_module(handler_module)
294
+ except ImportError as e:
295
+ context = ModelInfraErrorContext.with_correlation(
296
+ operation="import_handler_module",
297
+ target_name=handler_module,
298
+ )
299
+ raise ProtocolConfigurationError(
300
+ f"Failed to import handler module '{handler_module}': {e}",
301
+ context=context,
302
+ ) from e
303
+
304
+ try:
305
+ handler_class = getattr(module, handler_class_name)
306
+ except AttributeError as e:
307
+ context = ModelInfraErrorContext.with_correlation(
308
+ operation="load_handler_class",
309
+ target_name=f"{handler_module}.{handler_class_name}",
310
+ )
311
+ raise ProtocolConfigurationError(
312
+ f"Handler class '{handler_class_name}' not found in module '{handler_module}'",
313
+ context=context,
314
+ ) from e
315
+
316
+ description = contract.get("description", f"{handler_type} handler")
317
+
318
+ logger.debug(
319
+ "Loaded handler from contract",
320
+ extra={
321
+ "handler_type": handler_type,
322
+ "handler_class": handler_class_name,
323
+ "handler_module": handler_module,
324
+ "contract_path": str(contract_path),
325
+ },
326
+ )
327
+
328
+ return handler_class, description
329
+
330
+
197
331
  def wire_default_handlers() -> dict[str, list[str]]:
198
332
  """Register all default handlers and event buses with singleton registries.
199
333
 
@@ -240,17 +374,21 @@ def wire_default_handlers() -> dict[str, list[str]]:
240
374
  handler_registry = get_handler_registry()
241
375
  event_bus_registry = get_event_bus_registry()
242
376
 
243
- # Register all known handlers
244
- for handler_type, (handler_cls, description) in _KNOWN_HANDLERS.items():
377
+ # Register all handlers from contracts
378
+ for handler_type, contract_path in _HANDLER_CONTRACT_PATHS.items():
379
+ handler_cls, description = _load_handler_from_contract(
380
+ handler_type, contract_path
381
+ )
245
382
  # NOTE: Handlers implement ProtocolHandler structurally but don't inherit from it.
246
383
  # Mypy cannot verify structural subtyping for registration argument.
247
384
  handler_registry.register(handler_type, handler_cls) # type: ignore[arg-type] # NOTE: structural subtyping
248
385
  logger.debug(
249
- "Registered handler",
386
+ "Registered handler from contract",
250
387
  extra={
251
388
  "handler_type": handler_type,
252
389
  "handler_class": handler_cls.__name__,
253
390
  "description": description,
391
+ "contract_path": str(contract_path),
254
392
  },
255
393
  )
256
394
 
@@ -379,28 +517,32 @@ def wire_handlers_from_contract(
379
517
  )
380
518
  continue
381
519
 
382
- # Validate handler type is known
383
- if handler_type not in _KNOWN_HANDLERS:
384
- known_types = sorted(_KNOWN_HANDLERS.keys())
520
+ # Validate handler type is known (has a contract)
521
+ if handler_type not in _HANDLER_CONTRACT_PATHS:
522
+ known_types = sorted(_HANDLER_CONTRACT_PATHS.keys())
385
523
  raise ProtocolConfigurationError(
386
524
  f"Unknown handler type: {handler_type!r}. "
387
525
  f"Known types: {known_types}",
388
526
  context=_make_error_context("validate_handler_type", handler_type),
389
527
  )
390
528
 
391
- # Register the handler
392
- handler_cls, description = _KNOWN_HANDLERS[handler_type]
529
+ # Load and register the handler from contract
530
+ contract_path = _HANDLER_CONTRACT_PATHS[handler_type]
531
+ handler_cls, description = _load_handler_from_contract(
532
+ handler_type, contract_path
533
+ )
393
534
  # NOTE: Handlers implement ProtocolHandler structurally but don't inherit from it.
394
535
  # Mypy cannot verify structural subtyping for registration argument.
395
536
  handler_registry.register(handler_type, handler_cls) # type: ignore[arg-type] # NOTE: structural subtyping
396
537
  registered_handlers.append(handler_type)
397
538
 
398
539
  logger.debug(
399
- "Registered handler from contract",
540
+ "Registered handler from contract config",
400
541
  extra={
401
542
  "handler_type": handler_type,
402
543
  "handler_class": handler_cls.__name__,
403
544
  "description": description,
545
+ "contract_path": str(contract_path),
404
546
  },
405
547
  )
406
548
 
@@ -471,14 +613,16 @@ def wire_handlers_from_contract(
471
613
  def get_known_handler_types() -> list[str]:
472
614
  """Get list of known handler types that can be wired.
473
615
 
616
+ Handler types are discovered from contract.yaml files in nodes/handlers/.
617
+
474
618
  Returns:
475
619
  Sorted list of handler type strings.
476
620
 
477
621
  Example:
478
622
  >>> get_known_handler_types()
479
- ['db', 'http']
623
+ ['consul', 'db', 'graph', 'http', 'intent', 'mcp', 'vault']
480
624
  """
481
- return sorted(_KNOWN_HANDLERS.keys())
625
+ return sorted(_HANDLER_CONTRACT_PATHS.keys())
482
626
 
483
627
 
484
628
  def get_known_event_bus_kinds() -> list[str]:
@@ -8,7 +8,7 @@ the MCP tool registry in real-time. It supports:
8
8
  - Deregistration: Removed orchestrators are removed from tool registry
9
9
  - Idempotency: Duplicate/out-of-order events are handled correctly
10
10
 
11
- Event Topic: node.registration.v1
11
+ Event Topic: Uses SUFFIX_NODE_REGISTRATION (onex.evt.platform.node-registration.v1)
12
12
  Event Types:
13
13
  - registered: New node registered → upsert tool
14
14
  - updated: Node updated → upsert tool
@@ -26,9 +26,11 @@ from uuid import uuid4
26
26
 
27
27
  from omnibase_core.container import ModelONEXContainer
28
28
  from omnibase_core.types import JsonType
29
+ from omnibase_infra.models import ModelNodeIdentity
29
30
  from omnibase_infra.models.mcp.model_mcp_tool_definition import (
30
31
  ModelMCPToolDefinition,
31
32
  )
33
+ from omnibase_infra.topics import SUFFIX_NODE_REGISTRATION
32
34
 
33
35
  if TYPE_CHECKING:
34
36
  from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
@@ -76,9 +78,8 @@ class ServiceMCPToolSync:
76
78
  >>> await sync.stop()
77
79
  """
78
80
 
79
- # Topic for node registration events
80
- TOPIC = "node.registration.v1"
81
- GROUP_ID = "mcp-tool-sync"
81
+ # Topic for node registration events (uses platform suffix constant)
82
+ TOPIC = SUFFIX_NODE_REGISTRATION
82
83
 
83
84
  # MCP tag constants
84
85
  TAG_MCP_ENABLED = "mcp-enabled"
@@ -150,7 +151,6 @@ class ServiceMCPToolSync:
150
151
  "ServiceMCPToolSync initialized",
151
152
  extra={
152
153
  "topic": self.TOPIC,
153
- "group_id": self.GROUP_ID,
154
154
  },
155
155
  )
156
156
 
@@ -172,19 +172,37 @@ class ServiceMCPToolSync:
172
172
 
173
173
  correlation_id = uuid4()
174
174
 
175
+ # OMN-1602: Typed node identity for consumer group derivation.
176
+ #
177
+ # Identity Field Rationale:
178
+ # - env: From bus.environment for deployment-specific consumer groups
179
+ # - service="mcp": MCP is a singleton per environment
180
+ # - node_name="tool_sync": Unique consumer within the MCP service
181
+ # - version="v1": Consumer protocol version (increment on breaking changes)
182
+ sync_identity = ModelNodeIdentity(
183
+ env=self._bus.environment,
184
+ service="mcp",
185
+ node_name="tool_sync",
186
+ version="v1",
187
+ )
188
+
175
189
  logger.info(
176
190
  "Starting MCP tool sync",
177
191
  extra={
178
192
  "topic": self.TOPIC,
179
- "group_id": self.GROUP_ID,
193
+ "node_identity": {
194
+ "env": sync_identity.env,
195
+ "service": sync_identity.service,
196
+ "node_name": sync_identity.node_name,
197
+ "version": sync_identity.version,
198
+ },
180
199
  "correlation_id": str(correlation_id),
181
200
  },
182
201
  )
183
202
 
184
- # Subscribe to registration events
185
203
  self._unsubscribe = await self._bus.subscribe(
186
204
  topic=self.TOPIC,
187
- group_id=self.GROUP_ID,
205
+ node_identity=sync_identity,
188
206
  on_message=self._on_message,
189
207
  )
190
208
 
@@ -539,7 +557,7 @@ class ServiceMCPToolSync:
539
557
  return {
540
558
  "service_name": "ServiceMCPToolSync",
541
559
  "topic": self.TOPIC,
542
- "group_id": self.GROUP_ID,
560
+ "group_id_derived": True, # Group ID derived from ModelNodeIdentity
543
561
  "is_running": self._started,
544
562
  }
545
563
 
@@ -8,6 +8,7 @@ Moved from omniclaude as part of OMN-1526 architectural cleanup.
8
8
  from __future__ import annotations
9
9
 
10
10
  import logging
11
+ from typing import Self
11
12
 
12
13
  from pydantic import Field, model_validator
13
14
  from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -42,13 +43,8 @@ class ConfigSessionConsumer(BaseSettings):
42
43
 
43
44
  # Topics to subscribe
44
45
  topics: list[str] = Field(
45
- default=[
46
- "dev.omniclaude.session.started.v1",
47
- "dev.omniclaude.session.ended.v1",
48
- "dev.omniclaude.prompt.submitted.v1",
49
- "dev.omniclaude.tool.executed.v1",
50
- ],
51
- description="Kafka topics to consume",
46
+ default_factory=list,
47
+ description="Kafka topics to consume. Must be explicitly configured via environment or discovery.",
52
48
  )
53
49
 
54
50
  # Consumer behavior
@@ -96,7 +92,28 @@ class ConfigSessionConsumer(BaseSettings):
96
92
  )
97
93
 
98
94
  @model_validator(mode="after")
99
- def validate_timing_relationships(self) -> ConfigSessionConsumer:
95
+ def validate_topic_configuration(self) -> Self:
96
+ """Ensure topics are explicitly configured.
97
+
98
+ Fails fast if no topics provided, preventing silent misconfiguration.
99
+
100
+ Returns:
101
+ Self if validation passes.
102
+
103
+ Raises:
104
+ ProtocolConfigurationError: If no topics are configured.
105
+ """
106
+ if not self.topics:
107
+ from omnibase_infra.errors import ProtocolConfigurationError
108
+
109
+ raise ProtocolConfigurationError(
110
+ "No topics configured for session consumer. "
111
+ "Provide explicit 'topics' via configuration or environment variable."
112
+ )
113
+ return self
114
+
115
+ @model_validator(mode="after")
116
+ def validate_timing_relationships(self) -> Self:
100
117
  """Validate timing relationships between configuration values.
101
118
 
102
119
  Warns if circuit breaker timeout is very short relative to batch processing,
@@ -15,7 +15,7 @@ class ConfigSessionStorage(BaseSettings):
15
15
  """Configuration for session snapshot PostgreSQL storage.
16
16
 
17
17
  Environment variables use the OMNIBASE_INFRA_SESSION_STORAGE_ prefix.
18
- Example: OMNIBASE_INFRA_SESSION_STORAGE_POSTGRES_HOST=192.168.86.200
18
+ Example: OMNIBASE_INFRA_SESSION_STORAGE_POSTGRES_HOST=db.example.com
19
19
  """
20
20
 
21
21
  model_config = SettingsConfigDict(
@@ -28,7 +28,7 @@ class ConfigSessionStorage(BaseSettings):
28
28
 
29
29
  # PostgreSQL connection
30
30
  postgres_host: str = Field(
31
- default="192.168.86.200",
31
+ default="localhost",
32
32
  description="PostgreSQL host",
33
33
  )
34
34
  postgres_port: int = Field(
@@ -257,7 +257,7 @@ class SessionEventConsumer:
257
257
 
258
258
  Example:
259
259
  >>> config = ConfigSessionConsumer(
260
- ... bootstrap_servers="192.168.86.200:29092",
260
+ ... bootstrap_servers="localhost:9092",
261
261
  ... group_id="my-consumer-group",
262
262
  ... )
263
263
  >>> aggregator = InMemorySessionAggregator()
@@ -0,0 +1,45 @@
1
+ """ONEX Infrastructure Topic Constants.
2
+
3
+ This module provides platform-reserved topic suffix constants for ONEX infrastructure
4
+ components. Domain services should NOT import from this module - domain topics should
5
+ be defined in domain contracts.
6
+
7
+ Exports:
8
+ Platform topic suffix constants (e.g., SUFFIX_NODE_REGISTRATION)
9
+ ALL_PLATFORM_SUFFIXES: Complete tuple of all platform-reserved suffixes
10
+ build_full_topic: Compose full topic from env, namespace, and suffix
11
+ TopicCompositionError: Error raised when topic composition fails
12
+ """
13
+
14
+ from omnibase_infra.topics.platform_topic_suffixes import (
15
+ ALL_PLATFORM_SUFFIXES,
16
+ SUFFIX_FSM_STATE_TRANSITIONS,
17
+ SUFFIX_NODE_HEARTBEAT,
18
+ SUFFIX_NODE_INTROSPECTION,
19
+ SUFFIX_NODE_REGISTRATION,
20
+ SUFFIX_REGISTRATION_SNAPSHOTS,
21
+ SUFFIX_REQUEST_INTROSPECTION,
22
+ SUFFIX_RUNTIME_TICK,
23
+ )
24
+ from omnibase_infra.topics.util_topic_composition import (
25
+ MAX_NAMESPACE_LENGTH,
26
+ TopicCompositionError,
27
+ build_full_topic,
28
+ )
29
+
30
+ __all__: list[str] = [
31
+ # Individual suffix constants
32
+ "SUFFIX_NODE_REGISTRATION",
33
+ "SUFFIX_NODE_INTROSPECTION",
34
+ "SUFFIX_NODE_HEARTBEAT",
35
+ "SUFFIX_REQUEST_INTROSPECTION",
36
+ "SUFFIX_FSM_STATE_TRANSITIONS",
37
+ "SUFFIX_RUNTIME_TICK",
38
+ "SUFFIX_REGISTRATION_SNAPSHOTS",
39
+ # Aggregate tuple
40
+ "ALL_PLATFORM_SUFFIXES",
41
+ # Topic composition utilities
42
+ "build_full_topic",
43
+ "TopicCompositionError",
44
+ "MAX_NAMESPACE_LENGTH",
45
+ ]