omnibase_infra 0.3.2__py3-none-any.whl → 0.4.0__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 (57) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/errors/__init__.py +4 -0
  3. omnibase_infra/errors/error_infra.py +60 -0
  4. omnibase_infra/handlers/__init__.py +3 -0
  5. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  6. omnibase_infra/handlers/models/__init__.py +14 -0
  7. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  8. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  9. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  10. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  11. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  12. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  13. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  14. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  15. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  16. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  17. omnibase_infra/models/runtime/__init__.py +4 -0
  18. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  19. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  20. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  21. omnibase_infra/nodes/node_contract_persistence_effect/node.py +18 -1
  22. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +33 -2
  23. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  24. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  25. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  26. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  27. omnibase_infra/runtime/__init__.py +7 -0
  28. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  29. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  30. omnibase_infra/runtime/contract_registration_event_router.py +5 -5
  31. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  32. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  33. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  34. omnibase_infra/runtime/registry_policy.py +29 -15
  35. omnibase_infra/runtime/request_response_wiring.py +15 -7
  36. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  37. omnibase_infra/runtime/util_version.py +5 -1
  38. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  39. omnibase_infra/services/contract_publisher/config.py +4 -4
  40. omnibase_infra/services/contract_publisher/service.py +8 -5
  41. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  42. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  43. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  44. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  45. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  46. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  47. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  48. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  49. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  50. omnibase_infra/utils/__init__.py +7 -0
  51. omnibase_infra/utils/util_db_error_context.py +292 -0
  52. omnibase_infra/validation/validation_exemptions.yaml +11 -0
  53. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +2 -2
  54. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +57 -36
  55. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  56. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  57. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -9,7 +9,7 @@ all Kafka plumbing - nodes/handlers never create consumers or producers directly
9
9
  Architecture:
10
10
  The EventBusSubcontractWiring class is responsible for:
11
11
  1. Reading `subscribe_topics` from ModelEventBusSubcontract
12
- 2. Resolving topic suffixes to full topic names with environment prefix
12
+ 2. Passing topic suffixes through unchanged (topics are realm-agnostic)
13
13
  3. Creating Kafka subscriptions with appropriate consumer groups
14
14
  4. Bridging received messages to the MessageDispatchEngine
15
15
  5. Managing subscription lifecycle (creation and cleanup)
@@ -50,16 +50,21 @@ DLQ Consumer Group Alignment:
50
50
  3. Using it in all _publish_to_dlq calls within the callback closure
51
51
 
52
52
  Topic Resolution:
53
+ Topics are realm-agnostic and do NOT include environment prefixes.
54
+ The environment/realm is a routing boundary enforced via envelope identity,
55
+ not a topic prefix. This enables cross-environment event routing when needed.
56
+
53
57
  Topic suffixes from contracts follow the ONEX naming convention:
54
58
  onex.{kind}.{producer}.{event-name}.v{n}
55
59
 
56
- The wiring resolves these to full topics by prepending the environment:
57
- {environment}.onex.{kind}.{producer}.{event-name}.v{n}
60
+ The wiring passes these topic suffixes through unchanged:
61
+ onex.{kind}.{producer}.{event-name}.v{n}
58
62
 
59
63
  Example:
60
64
  - Contract declares: "onex.evt.omniintelligence.intent-classified.v1"
61
- - Resolved (dev): "dev.onex.evt.omniintelligence.intent-classified.v1"
62
- - Resolved (prod): "prod.onex.evt.omniintelligence.intent-classified.v1"
65
+ - Resolved: "onex.evt.omniintelligence.intent-classified.v1"
66
+
67
+ Note: Consumer groups still include environment for isolation.
63
68
 
64
69
  Related:
65
70
  - OMN-1621: Runtime consumes event_bus subcontract for contract-driven wiring
@@ -93,12 +98,13 @@ from omnibase_core.protocols.event_bus.protocol_event_bus_subscriber import (
93
98
  from omnibase_core.protocols.event_bus.protocol_event_message import (
94
99
  ProtocolEventMessage,
95
100
  )
96
- from omnibase_infra.enums import EnumInfraTransportType
101
+ from omnibase_infra.enums import EnumConsumerGroupPurpose, EnumInfraTransportType
97
102
  from omnibase_infra.errors import (
98
103
  ModelInfraErrorContext,
99
104
  ProtocolConfigurationError,
100
105
  RuntimeHostError,
101
106
  )
107
+ from omnibase_infra.models import ModelNodeIdentity
102
108
  from omnibase_infra.models.event_bus import (
103
109
  ModelConsumerRetryConfig,
104
110
  ModelDlqConfig,
@@ -106,6 +112,7 @@ from omnibase_infra.models.event_bus import (
106
112
  ModelOffsetPolicyConfig,
107
113
  )
108
114
  from omnibase_infra.protocols import ProtocolDispatchEngine, ProtocolIdempotencyStore
115
+ from omnibase_infra.utils import compute_consumer_group_id
109
116
 
110
117
  if TYPE_CHECKING:
111
118
  from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
@@ -124,7 +131,7 @@ class EventBusSubcontractWiring:
124
131
 
125
132
  Responsibilities:
126
133
  - Parse subscribe_topics from ModelEventBusSubcontract
127
- - Resolve topic suffixes to full topic names with environment prefix
134
+ - Pass topic suffixes through unchanged (topics are realm-agnostic)
128
135
  - Create Kafka subscriptions with appropriate consumer groups
129
136
  - Deserialize incoming messages to ModelEventEnvelope
130
137
  - Check idempotency and skip duplicate messages (if enabled)
@@ -194,7 +201,7 @@ class EventBusSubcontractWiring:
194
201
  Attributes:
195
202
  _event_bus: The event bus implementation (Kafka or in-memory)
196
203
  _dispatch_engine: Engine to dispatch received messages to handlers
197
- _environment: Environment prefix for topics (e.g., 'dev', 'prod')
204
+ _environment: Environment identifier for consumer groups (e.g., 'dev', 'prod')
198
205
  _node_name: Name of the node/handler for consumer group and logging
199
206
  _idempotency_store: Optional store for tracking processed messages
200
207
  _idempotency_config: Configuration for idempotency behavior
@@ -217,6 +224,8 @@ class EventBusSubcontractWiring:
217
224
  dispatch_engine: ProtocolDispatchEngine,
218
225
  environment: str,
219
226
  node_name: str,
227
+ service: str,
228
+ version: str,
220
229
  idempotency_store: ProtocolIdempotencyStore | None = None,
221
230
  idempotency_config: ModelIdempotencyConfig | None = None,
222
231
  dlq_config: ModelDlqConfig | None = None,
@@ -227,14 +236,19 @@ class EventBusSubcontractWiring:
227
236
 
228
237
  Args:
229
238
  event_bus: The event bus implementation (EventBusKafka or EventBusInmemory).
230
- Must implement subscribe(topic, group_id, on_message) -> unsubscribe callable.
239
+ Must implement subscribe(topic, node_identity, on_message) -> unsubscribe callable.
231
240
  Duck typed per ONEX patterns.
232
241
  dispatch_engine: Engine to dispatch received messages to handlers.
233
242
  Must implement ProtocolDispatchEngine interface.
234
243
  Must be frozen (registrations complete) before wiring subscriptions.
235
- environment: Environment prefix for topics (e.g., 'dev', 'prod').
236
- Used to resolve topic suffixes to full topic names.
244
+ environment: Environment identifier (e.g., 'dev', 'prod').
245
+ Used for consumer group naming and node identity. Topics are
246
+ realm-agnostic and do not include environment prefixes.
237
247
  node_name: Name of the node/handler for consumer group identification and logging.
248
+ service: Service name for node identity (e.g., 'omniintelligence', 'omnibridge').
249
+ Used to derive consumer group ID.
250
+ version: Version string for node identity (e.g., 'v1', 'v1.0.0').
251
+ Used to derive consumer group ID.
238
252
  idempotency_store: Optional idempotency store for message deduplication.
239
253
  If provided with enabled config, messages are deduplicated by envelope_id.
240
254
  idempotency_config: Optional configuration for idempotency behavior.
@@ -254,15 +268,21 @@ class EventBusSubcontractWiring:
254
268
  Attempting to dispatch to an unfrozen engine will raise an error.
255
269
 
256
270
  Raises:
257
- ValueError: If environment is empty or whitespace-only.
271
+ ValueError: If environment, service, or version is empty or whitespace-only.
258
272
  """
259
273
  if not environment or not environment.strip():
260
274
  raise ValueError("environment must be a non-empty string")
275
+ if not service or not service.strip():
276
+ raise ValueError("service must be a non-empty string")
277
+ if not version or not version.strip():
278
+ raise ValueError("version must be a non-empty string")
261
279
 
262
280
  self._event_bus = event_bus
263
281
  self._dispatch_engine = dispatch_engine
264
282
  self._environment = environment
265
283
  self._node_name = node_name
284
+ self._service = service
285
+ self._version = version
266
286
  self._idempotency_store = idempotency_store
267
287
  self._idempotency_config = idempotency_config or ModelIdempotencyConfig()
268
288
  self._dlq_config = dlq_config or ModelDlqConfig()
@@ -274,28 +294,36 @@ class EventBusSubcontractWiring:
274
294
  self._retry_counts: dict[UUID, int] = {}
275
295
 
276
296
  def resolve_topic(self, topic_suffix: str) -> str:
277
- """Resolve topic suffix to full topic name with environment prefix.
297
+ """Resolve topic suffix to topic name (realm-agnostic, no environment prefix).
298
+
299
+ Topics are realm-agnostic in ONEX. The environment/realm is enforced via
300
+ envelope identity, not topic naming. This enables cross-environment event
301
+ routing when needed while maintaining proper isolation through identity.
278
302
 
279
303
  Topic suffixes from contracts follow the ONEX naming convention:
280
304
  onex.{kind}.{producer}.{event-name}.v{n}
281
305
 
282
- This method prepends the environment to create the full topic name:
283
- {environment}.onex.{kind}.{producer}.{event-name}.v{n}
306
+ This method returns the topic suffix unchanged:
307
+ onex.{kind}.{producer}.{event-name}.v{n}
284
308
 
285
309
  Args:
286
310
  topic_suffix: ONEX format topic suffix
287
311
  (e.g., 'onex.evt.omniintelligence.intent-classified.v1')
288
312
 
289
313
  Returns:
290
- Full topic name with environment prefix
291
- (e.g., 'dev.onex.evt.omniintelligence.intent-classified.v1')
314
+ Topic name (same as suffix, no environment prefix)
315
+ (e.g., 'onex.evt.omniintelligence.intent-classified.v1')
292
316
 
293
317
  Example:
294
318
  >>> wiring = EventBusSubcontractWiring(bus, engine, "dev")
295
319
  >>> wiring.resolve_topic("onex.evt.user.created.v1")
296
- 'dev.onex.evt.user.created.v1'
320
+ 'onex.evt.user.created.v1'
321
+
322
+ Note:
323
+ Consumer groups still include environment for proper isolation.
324
+ See wire_subscriptions() for consumer group naming.
297
325
  """
298
- return f"{self._environment}.{topic_suffix}"
326
+ return topic_suffix
299
327
 
300
328
  async def wire_subscriptions(
301
329
  self,
@@ -348,9 +376,21 @@ class EventBusSubcontractWiring:
348
376
 
349
377
  for topic_suffix in subcontract.subscribe_topics:
350
378
  full_topic = self.resolve_topic(topic_suffix)
351
- # Consumer group ID derived from environment and node_name
352
- # This same group_id is passed to DLQ publishing for traceability
353
- consumer_group = f"{self._environment}.{node_name}"
379
+
380
+ # Create typed node identity for consumer group derivation
381
+ # The event bus derives consumer group as: {env}.{service}.{node_name}.{purpose}.{version}
382
+ node_identity = ModelNodeIdentity(
383
+ env=self._environment,
384
+ service=self._service,
385
+ node_name=node_name,
386
+ version=self._version,
387
+ )
388
+
389
+ # Consumer group for logging and DLQ traceability
390
+ # Use shared helper for consistent derivation across codebase
391
+ consumer_group = compute_consumer_group_id(
392
+ node_identity, EnumConsumerGroupPurpose.CONSUME
393
+ )
354
394
 
355
395
  # Create dispatch callback for this topic, capturing the consumer_group
356
396
  # used for this subscription to ensure DLQ messages have consistent
@@ -360,7 +400,7 @@ class EventBusSubcontractWiring:
360
400
  # Subscribe and store unsubscribe callable
361
401
  unsubscribe = await self._event_bus.subscribe(
362
402
  topic=full_topic,
363
- group_id=consumer_group,
403
+ node_identity=node_identity,
364
404
  on_message=callback,
365
405
  )
366
406
  self._unsubscribe_callables.append(unsubscribe)
@@ -9,7 +9,7 @@ event emission and maintaining clean architectural boundaries.
9
9
 
10
10
  Design Principles:
11
11
  - **Contract-Driven Access Control**: Topics must be declared in contract
12
- - **Environment-Aware Routing**: Topic suffixes are prefixed with environment
12
+ - **Realm-Agnostic Topics**: Topics passed through unchanged (no env prefix)
13
13
  - **Fail-Fast Validation**: Invalid topics raise immediately, not at delivery
14
14
  - **Duck-Typed Protocol**: Implements publisher protocol without explicit inheritance
15
15
 
@@ -95,7 +95,7 @@ class PublisherTopicScoped:
95
95
 
96
96
  Features:
97
97
  - Contract-driven topic access control
98
- - Environment-aware topic resolution
98
+ - Realm-agnostic topics (no environment prefix)
99
99
  - Fail-fast validation on disallowed topics
100
100
  - JSON serialization for payloads
101
101
  - Correlation ID propagation for distributed tracing
@@ -103,7 +103,7 @@ class PublisherTopicScoped:
103
103
  Attributes:
104
104
  _event_bus: The underlying event bus for publishing
105
105
  _allowed_topics: Set of topic suffixes allowed by contract
106
- _environment: Environment prefix for topic resolution
106
+ _environment: Environment identifier (retained for future use)
107
107
 
108
108
  Example:
109
109
  >>> publisher = PublisherTopicScoped(
@@ -132,8 +132,8 @@ class PublisherTopicScoped:
132
132
  Must implement publish(topic, key, value) method. Duck typed per ONEX.
133
133
  allowed_topics: Set of topic suffixes from contract's publish_topics.
134
134
  These are the ONLY topics this publisher can publish to.
135
- environment: Environment prefix (e.g., 'dev', 'staging', 'prod').
136
- Used to construct full topic names.
135
+ environment: Environment identifier (e.g., 'dev', 'staging', 'prod').
136
+ Retained for future use; topics are realm-agnostic (no prefix).
137
137
 
138
138
  Example:
139
139
  >>> publisher = PublisherTopicScoped(
@@ -174,22 +174,27 @@ class PublisherTopicScoped:
174
174
  return str(correlation_id).encode("utf-8")
175
175
 
176
176
  def resolve_topic(self, topic_suffix: str) -> str:
177
- """Resolve topic suffix to full topic name with environment prefix.
177
+ """Resolve topic suffix to topic name (realm-agnostic, no environment prefix).
178
178
 
179
- The full topic name follows the ONEX convention:
180
- `{environment}.{topic_suffix}`
179
+ Topics are realm-agnostic in ONEX. The environment/realm is enforced via
180
+ envelope identity, not topic naming. This enables cross-environment event
181
+ routing when needed while maintaining proper isolation through identity.
181
182
 
182
183
  Args:
183
184
  topic_suffix: ONEX format topic suffix (e.g., 'onex.events.v1')
184
185
 
185
186
  Returns:
186
- Full topic name with environment prefix (e.g., 'dev.onex.events.v1')
187
+ Topic name (same as suffix, no environment prefix)
187
188
 
188
189
  Example:
189
190
  >>> publisher.resolve_topic("onex.events.v1")
190
- 'dev.onex.events.v1'
191
+ 'onex.events.v1'
192
+
193
+ Note:
194
+ The environment is still stored for potential consumer group derivation
195
+ in related components. Topics themselves are realm-agnostic.
191
196
  """
192
- return f"{self._environment}.{topic_suffix}"
197
+ return topic_suffix
193
198
 
194
199
  async def publish(
195
200
  self,
@@ -86,7 +86,6 @@ from typing import TYPE_CHECKING
86
86
 
87
87
  from pydantic import ValidationError
88
88
 
89
- from omnibase_core.models.primitives import ModelSemVer
90
89
  from omnibase_infra.enums import EnumInfraTransportType, EnumPolicyType
91
90
  from omnibase_infra.errors import PolicyRegistryError, ProtocolConfigurationError
92
91
  from omnibase_infra.models.errors.model_infra_error_context import (
@@ -97,6 +96,7 @@ from omnibase_infra.runtime.mixin_semver_cache import MixinSemverCache
97
96
  from omnibase_infra.runtime.models import ModelPolicyKey, ModelPolicyRegistration
98
97
  from omnibase_infra.runtime.util_version import normalize_version
99
98
  from omnibase_infra.types import PolicyTypeInput
99
+ from omnibase_infra.utils import validate_policy_type_value
100
100
 
101
101
  if TYPE_CHECKING:
102
102
  from omnibase_infra.runtime.protocol_policy import ProtocolPolicy
@@ -463,11 +463,13 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
463
463
  and string literals, normalizing them to their string representation while
464
464
  ensuring they match valid EnumPolicyType values.
465
465
 
466
+ Delegates validation to the shared ``validate_policy_type_value()`` utility,
467
+ which is the SINGLE SOURCE OF TRUTH for policy type validation in omnibase_infra.
468
+
466
469
  Validation Process:
467
- 1. If policy_type is EnumPolicyType instance, extract .value
468
- 2. If policy_type is string, validate against EnumPolicyType values
469
- 3. Raise PolicyRegistryError if string doesn't match any enum value
470
- 4. Return normalized string value
470
+ 1. Delegate to validate_policy_type_value() for validation and coercion
471
+ 2. Extract .value from the validated EnumPolicyType
472
+ 3. Return normalized string value
471
473
 
472
474
  This centralized validation ensures consistent policy type handling across
473
475
  all registry operations (register, get, list_keys, is_registered, unregister).
@@ -498,12 +500,14 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
498
500
  PolicyRegistryError: Invalid policy_type: 'invalid'.
499
501
  Must be one of: ['orchestrator', 'reducer']
500
502
  """
501
- if isinstance(policy_type, EnumPolicyType):
502
- return policy_type.value
503
-
504
- # Validate string against enum values
505
- valid_types = {e.value for e in EnumPolicyType}
506
- if policy_type not in valid_types:
503
+ try:
504
+ # Use shared validation utility (SINGLE SOURCE OF TRUTH)
505
+ validated_enum = validate_policy_type_value(policy_type)
506
+ return validated_enum.value
507
+ except ValueError as exc:
508
+ # Convert ValueError from shared utility to PolicyRegistryError
509
+ # for consistent error handling in the registry
510
+ valid_types = {pt.value for pt in EnumPolicyType}
507
511
  context = ModelInfraErrorContext.with_correlation(
508
512
  transport_type=EnumInfraTransportType.RUNTIME,
509
513
  operation="normalize_policy_type",
@@ -512,11 +516,9 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
512
516
  f"Invalid policy_type: {policy_type!r}. "
513
517
  f"Must be one of: {sorted(valid_types)}",
514
518
  policy_id=None,
515
- policy_type=policy_type,
519
+ policy_type=str(policy_type),
516
520
  context=context,
517
- )
518
-
519
- return policy_type
521
+ ) from exc
520
522
 
521
523
  @staticmethod
522
524
  def _normalize_version(version: str) -> str:
@@ -656,6 +658,10 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
656
658
  Partial version strings (e.g., "1", "1.0") are auto-normalized to
657
659
  "x.y.z" format by ModelPolicyRegistration.
658
660
 
661
+ .. deprecated::
662
+ This method is deprecated. Use ``register(ModelPolicyRegistration(...))``
663
+ directly for new code. This method will be removed in a future version.
664
+
659
665
  Note:
660
666
  For new code, prefer using register(ModelPolicyRegistration(...))
661
667
  directly. This is a convenience method for simple registrations.
@@ -684,6 +690,14 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
684
690
  ... version="1.0.0",
685
691
  ... )
686
692
  """
693
+ # Emit deprecation warning at runtime
694
+ warnings.warn(
695
+ "register_policy() is deprecated. Use register(ModelPolicyRegistration(...)) "
696
+ "instead. This method will be removed in a future version.",
697
+ DeprecationWarning,
698
+ stacklevel=2,
699
+ )
700
+
687
701
  # Version normalization is handled by ModelPolicyRegistration validator
688
702
  # which normalizes partial versions and v-prefixed versions automatically
689
703
  try:
@@ -191,7 +191,7 @@ class RequestResponseWiring(MixinAsyncCircuitBreaker):
191
191
 
192
192
  Attributes:
193
193
  _event_bus: Event bus for publishing requests
194
- _environment: Environment prefix for topics (e.g., 'dev', 'prod')
194
+ _environment: Environment identifier for consumer groups (e.g., 'dev', 'prod')
195
195
  _app_name: Application name for consumer group identification
196
196
  _instances: Dict mapping instance names to their state
197
197
  _bootstrap_servers: Kafka bootstrap servers from event bus
@@ -211,8 +211,9 @@ class RequestResponseWiring(MixinAsyncCircuitBreaker):
211
211
  Args:
212
212
  event_bus: Event bus for publishing requests. Must implement
213
213
  ProtocolEventBusPublisher interface.
214
- environment: Environment prefix for topics (e.g., 'dev', 'prod').
215
- Used to resolve topic suffixes to full topic names.
214
+ environment: Environment identifier (e.g., 'dev', 'prod').
215
+ Used for consumer group naming. Topics are realm-agnostic and
216
+ do not include environment prefixes.
216
217
  app_name: Application name for logging and consumer group naming.
217
218
  bootstrap_servers: Kafka bootstrap servers. If not provided, attempts
218
219
  to read from event_bus._bootstrap_servers or environment variable.
@@ -263,17 +264,24 @@ class RequestResponseWiring(MixinAsyncCircuitBreaker):
263
264
  )
264
265
 
265
266
  def resolve_topic(self, topic_suffix: str) -> str:
266
- """Resolve topic suffix to full topic name with environment prefix.
267
+ """Resolve topic suffix to topic name (realm-agnostic, no environment prefix).
268
+
269
+ Topics are realm-agnostic in ONEX. The environment/realm is enforced via
270
+ envelope identity, not topic naming. This enables cross-environment event
271
+ routing when needed while maintaining proper isolation through identity.
267
272
 
268
273
  Args:
269
274
  topic_suffix: ONEX format topic suffix
270
275
  (e.g., 'onex.cmd.intelligence.analyze-code.v1')
271
276
 
272
277
  Returns:
273
- Full topic name with environment prefix
274
- (e.g., 'dev.onex.cmd.intelligence.analyze-code.v1')
278
+ Topic name (same as suffix, no environment prefix)
279
+ (e.g., 'onex.cmd.intelligence.analyze-code.v1')
280
+
281
+ Note:
282
+ Consumer groups still include environment for proper isolation.
275
283
  """
276
- return f"{self._environment}.{topic_suffix}"
284
+ return topic_suffix
277
285
 
278
286
  async def wire_request_response(
279
287
  self,
@@ -73,6 +73,12 @@ from omnibase_infra.errors import (
73
73
  from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
74
74
  from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
75
75
  from omnibase_infra.models import ModelNodeIdentity
76
+ from omnibase_infra.models.runtime.model_resolved_dependencies import (
77
+ ModelResolvedDependencies,
78
+ )
79
+ from omnibase_infra.runtime.contract_dependency_resolver import (
80
+ ContractDependencyResolver,
81
+ )
76
82
  from omnibase_infra.runtime.envelope_validator import (
77
83
  normalize_correlation_id,
78
84
  validate_envelope,
@@ -764,6 +770,10 @@ class RuntimeHostProcess:
764
770
  # Enables contract config to be passed to handlers via initialize()
765
771
  self._handler_descriptors: dict[str, ModelHandlerDescriptor] = {}
766
772
 
773
+ # Contract dependency resolver for protocol auto-injection (OMN-1903)
774
+ # Lazy-created when first needed during handler population
775
+ self._dependency_resolver: ContractDependencyResolver | None = None
776
+
767
777
  # Pending message tracking for graceful shutdown (OMN-756)
768
778
  # Tracks count of in-flight messages currently being processed
769
779
  self._pending_message_count: int = 0
@@ -1798,11 +1808,44 @@ class RuntimeHostProcess:
1798
1808
  handler_type
1799
1809
  )
1800
1810
 
1801
- # Instantiate the handler with container for dependency injection
1811
+ # Get descriptor early for dependency resolution (OMN-1903)
1812
+ descriptor = self._handler_descriptors.get(handler_type)
1813
+
1814
+ # R1/R3: Resolve dependencies if contract has them (OMN-1903)
1815
+ # Returns None if descriptor has no contract_path or no dependencies
1816
+ resolved_dependencies: ModelResolvedDependencies | None = None
1817
+ if descriptor:
1818
+ # This may raise ProtocolDependencyResolutionError (R2: fail-fast)
1819
+ resolved_dependencies = await self._resolve_handler_dependencies(
1820
+ descriptor
1821
+ )
1822
+
1823
+ # Instantiate the handler with container (and dependencies if supported)
1802
1824
  # ProtocolContainerAware defines __init__(container: ModelONEXContainer)
1803
- handler_instance: ProtocolContainerAware = handler_cls(
1804
- container=container
1805
- )
1825
+ # Handlers that support OMN-1732 can accept optional dependencies parameter
1826
+ handler_instance: ProtocolContainerAware
1827
+ if resolved_dependencies and self._accepts_dependencies_param(
1828
+ handler_cls
1829
+ ):
1830
+ # New-style handler with dependency injection
1831
+ # Type ignore: handler_cls is typed as ProtocolContainerAware which doesn't
1832
+ # have dependencies param, but runtime introspection confirmed it exists
1833
+ handler_instance = handler_cls( # type: ignore[call-arg]
1834
+ container=container,
1835
+ dependencies=resolved_dependencies,
1836
+ )
1837
+ logger.debug(
1838
+ "Instantiated handler with resolved dependencies",
1839
+ extra={
1840
+ "handler_type": handler_type,
1841
+ "resolved_protocols": list(
1842
+ resolved_dependencies.protocols.keys()
1843
+ ),
1844
+ },
1845
+ )
1846
+ else:
1847
+ # Legacy handler without dependency parameter
1848
+ handler_instance = handler_cls(container=container)
1806
1849
 
1807
1850
  # Call initialize() if the handler has this method
1808
1851
  # Handlers may require async initialization with config
@@ -1814,7 +1857,6 @@ class RuntimeHostProcess:
1814
1857
  config_source = "runtime_only"
1815
1858
 
1816
1859
  # Layer 1: Contract config as baseline (if descriptor exists with config)
1817
- descriptor = self._handler_descriptors.get(handler_type)
1818
1860
  if descriptor and descriptor.contract_config:
1819
1861
  effective_config.update(descriptor.contract_config)
1820
1862
  config_source = "contract_only"
@@ -1890,6 +1932,106 @@ class RuntimeHostProcess:
1890
1932
  },
1891
1933
  )
1892
1934
 
1935
+ async def _resolve_handler_dependencies(
1936
+ self,
1937
+ descriptor: ModelHandlerDescriptor,
1938
+ ) -> ModelResolvedDependencies | None:
1939
+ """Resolve protocol dependencies for a handler from its contract.
1940
+
1941
+ Part of OMN-1903: Runtime dependency injection integration.
1942
+
1943
+ If the handler's contract declares protocol dependencies, this method
1944
+ resolves them from the container's service_registry. Returns None if:
1945
+ - No contract_path in descriptor (opt-in behavior, R3)
1946
+ - Contract has no dependencies section
1947
+
1948
+ Args:
1949
+ descriptor: Handler descriptor containing contract_path.
1950
+
1951
+ Returns:
1952
+ ModelResolvedDependencies with resolved protocols, or None if no
1953
+ dependencies to resolve.
1954
+
1955
+ Raises:
1956
+ ProtocolDependencyResolutionError: If any required protocol cannot
1957
+ be resolved (fail-fast behavior, R2).
1958
+ ProtocolConfigurationError: If contract file cannot be loaded.
1959
+ """
1960
+ # R3: Opt-in behavior - skip if no contract_path
1961
+ if not descriptor.contract_path:
1962
+ logger.debug(
1963
+ "Handler has no contract_path, skipping dependency resolution",
1964
+ extra={"handler_id": descriptor.handler_id},
1965
+ )
1966
+ return None
1967
+
1968
+ # Lazy-create resolver on first use
1969
+ if self._dependency_resolver is None:
1970
+ container = self._get_or_create_container()
1971
+ self._dependency_resolver = ContractDependencyResolver(container)
1972
+
1973
+ # R1: Call resolver with contract path
1974
+ contract_path = Path(descriptor.contract_path)
1975
+ logger.debug(
1976
+ "Resolving dependencies for handler",
1977
+ extra={
1978
+ "handler_id": descriptor.handler_id,
1979
+ "contract_path": str(contract_path),
1980
+ },
1981
+ )
1982
+
1983
+ # R2: Fail-fast on missing protocols (allow_missing=False)
1984
+ resolved = await self._dependency_resolver.resolve_from_path(
1985
+ contract_path,
1986
+ allow_missing=False,
1987
+ )
1988
+
1989
+ if resolved:
1990
+ logger.debug(
1991
+ "Resolved dependencies for handler",
1992
+ extra={
1993
+ "handler_id": descriptor.handler_id,
1994
+ "resolved_protocols": list(resolved.protocols.keys()),
1995
+ },
1996
+ )
1997
+ else:
1998
+ logger.debug(
1999
+ "No protocol dependencies in contract",
2000
+ extra={
2001
+ "handler_id": descriptor.handler_id,
2002
+ "contract_path": str(contract_path),
2003
+ },
2004
+ )
2005
+
2006
+ return resolved if resolved else None
2007
+
2008
+ def _accepts_dependencies_param(self, handler_cls: type) -> bool:
2009
+ """Check if a handler class accepts 'dependencies' in its constructor.
2010
+
2011
+ Part of OMN-1903: Runtime dependency injection integration.
2012
+
2013
+ Uses introspection to check if the handler's __init__ accepts a
2014
+ 'dependencies' keyword argument. This enables gradual migration:
2015
+ - Legacy handlers: __init__(container) - no dependencies param
2016
+ - New handlers: __init__(container, dependencies=...) - receives deps
2017
+
2018
+ Args:
2019
+ handler_cls: The handler class to check.
2020
+
2021
+ Returns:
2022
+ True if the handler accepts 'dependencies' parameter, False otherwise.
2023
+ """
2024
+ import inspect
2025
+
2026
+ try:
2027
+ # Use inspect.signature on the class itself, not __init__
2028
+ # This avoids the "unsound instance access" mypy warning
2029
+ sig = inspect.signature(handler_cls)
2030
+ return "dependencies" in sig.parameters
2031
+ except (ValueError, TypeError):
2032
+ # Cannot inspect signature (e.g., builtin class)
2033
+ return False
2034
+
1893
2035
  async def _load_contract_configs(self, correlation_id: UUID) -> None:
1894
2036
  """Load contract configurations from all discovered contracts.
1895
2037
 
@@ -2919,6 +3061,8 @@ class RuntimeHostProcess:
2919
3061
  dispatch_engine=self._dispatch_engine,
2920
3062
  environment=environment,
2921
3063
  node_name="runtime-host",
3064
+ service=self._node_identity.service,
3065
+ version=self._node_identity.version,
2922
3066
  )
2923
3067
 
2924
3068
  # Wire subscriptions for each handler with a contract
@@ -14,6 +14,7 @@ ModelPolicyKey, and ModelPolicyRegistration.
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ from omnibase_core.models.errors import ModelOnexError
17
18
  from omnibase_core.models.primitives import ModelSemVer
18
19
 
19
20
 
@@ -81,9 +82,12 @@ def normalize_version(version: str) -> str:
81
82
  expanded_version = ".".join(version_nums)
82
83
 
83
84
  # Parse with ModelSemVer for validation
85
+ # Note: ModelSemVer.parse() raises ModelOnexError for invalid versions,
86
+ # but we also catch ValueError for defensive programming against
87
+ # potential upstream changes.
84
88
  try:
85
89
  semver = ModelSemVer.parse(expanded_version)
86
- except Exception as e:
90
+ except (ModelOnexError, ValueError) as e:
87
91
  raise ValueError(f"Invalid version format: {e}") from e
88
92
 
89
93
  result: str = semver.to_string()