omnibase_infra 0.3.1__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 (117) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +9 -0
  4. omnibase_infra/enums/enum_postgres_error_code.py +188 -0
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_infra.py +60 -0
  7. omnibase_infra/handlers/__init__.py +3 -0
  8. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  9. omnibase_infra/handlers/models/__init__.py +14 -0
  10. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  11. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  12. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  13. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  14. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +29 -20
  15. omnibase_infra/mixins/__init__.py +14 -0
  16. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  17. omnibase_infra/mixins/mixin_postgres_error_response.py +314 -0
  18. omnibase_infra/mixins/mixin_postgres_op_executor.py +298 -0
  19. omnibase_infra/models/__init__.py +3 -0
  20. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  21. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  22. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  23. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  24. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  25. omnibase_infra/{nodes/effects/models → models}/model_backend_result.py +22 -6
  26. omnibase_infra/models/projection/__init__.py +11 -0
  27. omnibase_infra/models/projection/model_contract_projection.py +170 -0
  28. omnibase_infra/models/projection/model_topic_projection.py +148 -0
  29. omnibase_infra/models/runtime/__init__.py +4 -0
  30. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  31. omnibase_infra/nodes/contract_registry_reducer/__init__.py +5 -0
  32. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  33. omnibase_infra/nodes/contract_registry_reducer/contract_registration_event_router.py +689 -0
  34. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  35. omnibase_infra/nodes/effects/__init__.py +1 -1
  36. omnibase_infra/nodes/effects/models/__init__.py +6 -4
  37. omnibase_infra/nodes/effects/models/model_registry_response.py +1 -1
  38. omnibase_infra/nodes/effects/protocol_consul_client.py +1 -1
  39. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +1 -1
  40. omnibase_infra/nodes/effects/registry_effect.py +1 -1
  41. omnibase_infra/nodes/node_contract_persistence_effect/__init__.py +101 -0
  42. omnibase_infra/nodes/node_contract_persistence_effect/contract.yaml +490 -0
  43. omnibase_infra/nodes/node_contract_persistence_effect/handlers/__init__.py +74 -0
  44. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_cleanup_topics.py +217 -0
  45. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_contract_upsert.py +242 -0
  46. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_deactivate.py +194 -0
  47. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_heartbeat.py +243 -0
  48. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_mark_stale.py +208 -0
  49. omnibase_infra/nodes/node_contract_persistence_effect/handlers/handler_postgres_topic_update.py +298 -0
  50. omnibase_infra/nodes/node_contract_persistence_effect/models/__init__.py +15 -0
  51. omnibase_infra/nodes/node_contract_persistence_effect/models/model_persistence_result.py +52 -0
  52. omnibase_infra/nodes/node_contract_persistence_effect/node.py +131 -0
  53. omnibase_infra/nodes/node_contract_persistence_effect/registry/__init__.py +27 -0
  54. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +251 -0
  55. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  56. omnibase_infra/nodes/node_registry_effect/models/__init__.py +2 -2
  57. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  58. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  59. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  60. omnibase_infra/projectors/__init__.py +6 -0
  61. omnibase_infra/projectors/projection_reader_contract.py +1301 -0
  62. omnibase_infra/runtime/__init__.py +12 -0
  63. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  64. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  65. omnibase_infra/runtime/contract_registration_event_router.py +500 -0
  66. omnibase_infra/runtime/db/__init__.py +4 -0
  67. omnibase_infra/runtime/db/models/__init__.py +15 -10
  68. omnibase_infra/runtime/db/models/model_db_operation.py +40 -0
  69. omnibase_infra/runtime/db/models/model_db_param.py +24 -0
  70. omnibase_infra/runtime/db/models/model_db_repository_contract.py +40 -0
  71. omnibase_infra/runtime/db/models/model_db_return.py +26 -0
  72. omnibase_infra/runtime/db/models/model_db_safety_policy.py +32 -0
  73. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  74. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  75. omnibase_infra/runtime/intent_execution_router.py +430 -0
  76. omnibase_infra/runtime/models/__init__.py +6 -0
  77. omnibase_infra/runtime/models/model_contract_registry_config.py +41 -0
  78. omnibase_infra/runtime/models/model_intent_execution_summary.py +79 -0
  79. omnibase_infra/runtime/models/model_runtime_config.py +8 -0
  80. omnibase_infra/runtime/protocols/__init__.py +16 -0
  81. omnibase_infra/runtime/protocols/protocol_intent_executor.py +107 -0
  82. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  83. omnibase_infra/runtime/registry_policy.py +29 -15
  84. omnibase_infra/runtime/request_response_wiring.py +793 -0
  85. omnibase_infra/runtime/service_kernel.py +295 -8
  86. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  87. omnibase_infra/runtime/util_version.py +5 -1
  88. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  89. omnibase_infra/services/contract_publisher/config.py +4 -4
  90. omnibase_infra/services/contract_publisher/service.py +8 -5
  91. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  92. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  93. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  94. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  95. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  96. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  97. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  98. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  99. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  100. omnibase_infra/services/registry_api/models/__init__.py +25 -0
  101. omnibase_infra/services/registry_api/models/model_contract_ref.py +44 -0
  102. omnibase_infra/services/registry_api/models/model_contract_view.py +81 -0
  103. omnibase_infra/services/registry_api/models/model_response_contracts.py +50 -0
  104. omnibase_infra/services/registry_api/models/model_response_topics.py +50 -0
  105. omnibase_infra/services/registry_api/models/model_topic_summary.py +57 -0
  106. omnibase_infra/services/registry_api/models/model_topic_view.py +63 -0
  107. omnibase_infra/services/registry_api/routes.py +205 -6
  108. omnibase_infra/services/registry_api/service.py +528 -1
  109. omnibase_infra/utils/__init__.py +7 -0
  110. omnibase_infra/utils/util_db_error_context.py +292 -0
  111. omnibase_infra/validation/infra_validators.py +3 -1
  112. omnibase_infra/validation/validation_exemptions.yaml +65 -0
  113. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +3 -3
  114. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +117 -58
  115. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  116. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  117. {omnibase_infra-0.3.1.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -55,6 +55,7 @@ import signal
55
55
  import sys
56
56
  import time
57
57
  from collections.abc import Awaitable, Callable
58
+ from functools import partial
58
59
  from importlib.metadata import version as get_package_version
59
60
  from pathlib import Path
60
61
  from typing import cast
@@ -75,7 +76,15 @@ from omnibase_infra.errors import (
75
76
  from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
76
77
  from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
77
78
  from omnibase_infra.event_bus.models.config import ModelKafkaEventBusConfig
79
+ from omnibase_infra.event_bus.models.model_event_message import ModelEventMessage
78
80
  from omnibase_infra.models import ModelNodeIdentity
81
+ from omnibase_infra.nodes.contract_registry_reducer.contract_registration_event_router import (
82
+ ContractRegistrationEventRouter,
83
+ ProtocolIntentEffect,
84
+ )
85
+ from omnibase_infra.nodes.contract_registry_reducer.reducer import (
86
+ ContractRegistryReducer,
87
+ )
79
88
  from omnibase_infra.nodes.node_registration_orchestrator.dispatchers import (
80
89
  DispatcherNodeIntrospected,
81
90
  )
@@ -423,6 +432,11 @@ async def bootstrap() -> int:
423
432
  health_server: ServiceHealth | None = None
424
433
  postgres_pool: asyncpg.Pool | None = None
425
434
  introspection_unsubscribe: Callable[[], Awaitable[None]] | None = None
435
+ # Contract registry unsubscribe functions and router
436
+ contract_router: ContractRegistrationEventRouter | None = None
437
+ contract_unsub_registered: Callable[[], Awaitable[None]] | None = None
438
+ contract_unsub_deregistered: Callable[[], Awaitable[None]] | None = None
439
+ contract_unsub_heartbeat: Callable[[], Awaitable[None]] | None = None
426
440
  correlation_id = generate_correlation_id()
427
441
  bootstrap_start_time = time.time()
428
442
 
@@ -458,6 +472,7 @@ async def bootstrap() -> int:
458
472
 
459
473
  # 3. Create event bus
460
474
  # Dispatch based on configuration or environment variable:
475
+ # - ONEX_EVENT_BUS_TYPE env var overrides config.event_bus.type
461
476
  # - If KAFKA_BOOTSTRAP_SERVERS env var is set, use EventBusKafka
462
477
  # - If config.event_bus.type == "kafka", use EventBusKafka
463
478
  # - Otherwise, use EventBusInmemory for local development/testing
@@ -465,12 +480,54 @@ async def bootstrap() -> int:
465
480
  environment = os.getenv("ONEX_ENVIRONMENT") or config.event_bus.environment
466
481
  kafka_bootstrap_servers = os.getenv("KAFKA_BOOTSTRAP_SERVERS")
467
482
 
468
- # Explicit bool evaluation (not truthy string) for kafka usage.
469
- # KAFKA_BOOTSTRAP_SERVERS env var takes precedence over config.event_bus.type.
470
- # This prevents implicit "kafka but localhost" fallback scenarios.
471
- use_kafka: bool = (
472
- bool(kafka_bootstrap_servers) or config.event_bus.type == "kafka"
473
- )
483
+ # Check for ONEX_EVENT_BUS_TYPE environment variable override
484
+ # This allows CI/testing environments to force inmemory event bus
485
+ # even when the config file defaults to kafka.
486
+ event_bus_type_override = os.getenv("ONEX_EVENT_BUS_TYPE", "").lower()
487
+ if event_bus_type_override:
488
+ logger.debug(
489
+ "Event bus type override from ONEX_EVENT_BUS_TYPE=%s (correlation_id=%s)",
490
+ event_bus_type_override,
491
+ correlation_id,
492
+ )
493
+
494
+ # Determine effective event bus type with override precedence:
495
+ # 1. ONEX_EVENT_BUS_TYPE env var (highest priority)
496
+ # 2. KAFKA_BOOTSTRAP_SERVERS env var (if set, implies kafka)
497
+ # 3. config.event_bus.type (from runtime_config.yaml)
498
+ if event_bus_type_override == "inmemory":
499
+ # Explicit inmemory override - use inmemory regardless of other config
500
+ use_kafka = False
501
+ logger.info(
502
+ "Using inmemory event bus (ONEX_EVENT_BUS_TYPE override) (correlation_id=%s)",
503
+ correlation_id,
504
+ )
505
+ elif event_bus_type_override == "kafka":
506
+ # Explicit kafka override - validate that bootstrap_servers is available
507
+ use_kafka = True
508
+ elif event_bus_type_override and event_bus_type_override not in (
509
+ "inmemory",
510
+ "kafka",
511
+ ):
512
+ # Invalid override value - warn and fall back to config
513
+ logger.warning(
514
+ "Invalid ONEX_EVENT_BUS_TYPE value '%s', expected 'inmemory' or 'kafka'. "
515
+ "Falling back to config.event_bus.type='%s' (correlation_id=%s)",
516
+ event_bus_type_override,
517
+ config.event_bus.type,
518
+ correlation_id,
519
+ )
520
+ use_kafka = (
521
+ bool(kafka_bootstrap_servers) or config.event_bus.type == "kafka"
522
+ )
523
+ else:
524
+ # No override - use original logic
525
+ # Explicit bool evaluation (not truthy string) for kafka usage.
526
+ # KAFKA_BOOTSTRAP_SERVERS env var takes precedence over config.event_bus.type.
527
+ # This prevents implicit "kafka but localhost" fallback scenarios.
528
+ use_kafka = (
529
+ bool(kafka_bootstrap_servers) or config.event_bus.type == "kafka"
530
+ )
474
531
 
475
532
  # Validate bootstrap_servers is provided when kafka is requested via config
476
533
  # This prevents confusing implicit localhost:9092 fallback
@@ -931,6 +988,81 @@ async def bootstrap() -> int:
931
988
  },
932
989
  )
933
990
 
991
+ # 4.9. Wire ContractRegistrationEventRouter if contract_registry.enabled
992
+ # This router subscribes to contract lifecycle events (registration,
993
+ # deregistration, heartbeat) and routes them to the ContractRegistryReducer.
994
+ # The router also runs an internal tick timer for staleness computation.
995
+ if config.contract_registry.enabled and postgres_pool is not None:
996
+ # Import postgres handlers for contract persistence
997
+ # Deferred import to avoid loading heavy dependencies when not needed
998
+ from omnibase_infra.nodes.node_contract_persistence_effect.handlers import (
999
+ HandlerPostgresCleanupTopics,
1000
+ HandlerPostgresContractUpsert,
1001
+ HandlerPostgresDeactivate,
1002
+ HandlerPostgresHeartbeat,
1003
+ HandlerPostgresMarkStale,
1004
+ HandlerPostgresTopicUpdate,
1005
+ )
1006
+
1007
+ # Create effect handlers keyed by intent_type
1008
+ # These handlers execute PostgreSQL operations for intents from the reducer
1009
+ # Note: Handlers implement ProtocolIntentEffect duck-typing style with
1010
+ # more specific payload types. Cast tells mypy they satisfy the protocol.
1011
+ contract_effect_handlers: dict[str, ProtocolIntentEffect] = {
1012
+ "postgres.upsert_contract": cast(
1013
+ "ProtocolIntentEffect",
1014
+ HandlerPostgresContractUpsert(postgres_pool),
1015
+ ),
1016
+ "postgres.update_topic": cast(
1017
+ "ProtocolIntentEffect",
1018
+ HandlerPostgresTopicUpdate(postgres_pool),
1019
+ ),
1020
+ "postgres.mark_stale": cast(
1021
+ "ProtocolIntentEffect",
1022
+ HandlerPostgresMarkStale(postgres_pool),
1023
+ ),
1024
+ "postgres.update_heartbeat": cast(
1025
+ "ProtocolIntentEffect",
1026
+ HandlerPostgresHeartbeat(postgres_pool),
1027
+ ),
1028
+ "postgres.deactivate_contract": cast(
1029
+ "ProtocolIntentEffect",
1030
+ HandlerPostgresDeactivate(postgres_pool),
1031
+ ),
1032
+ "postgres.cleanup_topic_references": cast(
1033
+ "ProtocolIntentEffect",
1034
+ HandlerPostgresCleanupTopics(postgres_pool),
1035
+ ),
1036
+ }
1037
+
1038
+ # Create reducer and router
1039
+ contract_reducer = ContractRegistryReducer()
1040
+ contract_router = ContractRegistrationEventRouter(
1041
+ container=container,
1042
+ reducer=contract_reducer,
1043
+ effect_handlers=contract_effect_handlers,
1044
+ event_bus=event_bus,
1045
+ tick_interval_seconds=config.contract_registry.tick_interval_seconds,
1046
+ )
1047
+
1048
+ logger.info(
1049
+ "ContractRegistrationEventRouter created (correlation_id=%s)",
1050
+ correlation_id,
1051
+ extra={
1052
+ "tick_interval_seconds": config.contract_registry.tick_interval_seconds,
1053
+ "handler_count": len(contract_effect_handlers),
1054
+ },
1055
+ )
1056
+ else:
1057
+ logger.debug(
1058
+ "Contract registry disabled or no postgres_pool (correlation_id=%s)",
1059
+ correlation_id,
1060
+ extra={
1061
+ "contract_registry_enabled": config.contract_registry.enabled,
1062
+ "postgres_pool_available": postgres_pool is not None,
1063
+ },
1064
+ )
1065
+
934
1066
  except Exception as pool_error:
935
1067
  # Log warning but continue without registration support
936
1068
  # Use sanitize_error_message to prevent credential leakage in logs
@@ -1298,6 +1430,84 @@ async def bootstrap() -> int:
1298
1430
  },
1299
1431
  )
1300
1432
 
1433
+ # 9.6. Start contract registry event consumer if router is available
1434
+ # This consumer subscribes to 3 Kafka topics for contract lifecycle events
1435
+ # and routes them to the ContractRegistryReducer for projection.
1436
+ if contract_router is not None and has_subscribe:
1437
+ # Create typed node identity for contract registry subscriptions
1438
+ contract_node_identity = ModelNodeIdentity(
1439
+ env=environment,
1440
+ service=config.name or "onex-kernel",
1441
+ node_name="contract-registry",
1442
+ version=config.contract_version or "v1",
1443
+ )
1444
+
1445
+ # Subscribe to 3 contract lifecycle topics with same identity
1446
+ contract_subscribe_start_time = time.time()
1447
+
1448
+ # Derive environment-aware topic names (avoid hardcoded "dev." prefix)
1449
+ contract_registered_topic = (
1450
+ f"{environment}.onex.evt.platform.contract-registered.v1"
1451
+ )
1452
+ contract_deregistered_topic = (
1453
+ f"{environment}.onex.evt.platform.contract-deregistered.v1"
1454
+ )
1455
+ node_heartbeat_topic = f"{environment}.onex.evt.platform.node-heartbeat.v1"
1456
+
1457
+ logger.info(
1458
+ "Subscribing to contract registry events on event bus (correlation_id=%s)",
1459
+ correlation_id,
1460
+ extra={
1461
+ "topics": [
1462
+ contract_registered_topic,
1463
+ contract_deregistered_topic,
1464
+ node_heartbeat_topic,
1465
+ ],
1466
+ "node_identity": {
1467
+ "env": contract_node_identity.env,
1468
+ "service": contract_node_identity.service,
1469
+ "node_name": contract_node_identity.node_name,
1470
+ "version": contract_node_identity.version,
1471
+ },
1472
+ "purpose": EnumConsumerGroupPurpose.CONTRACT_REGISTRY.value,
1473
+ },
1474
+ )
1475
+
1476
+ contract_unsub_registered = await event_bus.subscribe(
1477
+ topic=contract_registered_topic,
1478
+ node_identity=contract_node_identity,
1479
+ on_message=contract_router.handle_message,
1480
+ purpose=EnumConsumerGroupPurpose.CONTRACT_REGISTRY,
1481
+ )
1482
+ contract_unsub_deregistered = await event_bus.subscribe(
1483
+ topic=contract_deregistered_topic,
1484
+ node_identity=contract_node_identity,
1485
+ on_message=contract_router.handle_message,
1486
+ purpose=EnumConsumerGroupPurpose.CONTRACT_REGISTRY,
1487
+ )
1488
+ contract_unsub_heartbeat = await event_bus.subscribe(
1489
+ topic=node_heartbeat_topic,
1490
+ node_identity=contract_node_identity,
1491
+ on_message=contract_router.handle_message,
1492
+ purpose=EnumConsumerGroupPurpose.CONTRACT_REGISTRY,
1493
+ )
1494
+
1495
+ # Start the router's tick timer
1496
+ await contract_router.start()
1497
+
1498
+ contract_subscribe_duration = time.time() - contract_subscribe_start_time
1499
+ logger.info(
1500
+ "Contract registry event consumers started successfully in %.3fs (correlation_id=%s)",
1501
+ contract_subscribe_duration,
1502
+ correlation_id,
1503
+ extra={
1504
+ "topics_count": 3,
1505
+ "tick_interval_seconds": contract_router.tick_interval_seconds,
1506
+ "subscribe_duration_seconds": contract_subscribe_duration,
1507
+ "event_bus_type": event_bus_type,
1508
+ },
1509
+ )
1510
+
1301
1511
  # Calculate total bootstrap time
1302
1512
  bootstrap_duration = time.time() - bootstrap_start_time
1303
1513
 
@@ -1309,14 +1519,24 @@ async def bootstrap() -> int:
1309
1519
  registration_status = "enabled (PostgreSQL only)"
1310
1520
  else:
1311
1521
  registration_status = "disabled"
1522
+
1523
+ # Contract registry status for banner
1524
+ if contract_router is not None:
1525
+ contract_registry_status = (
1526
+ f"enabled (tick: {config.contract_registry.tick_interval_seconds}s)"
1527
+ )
1528
+ else:
1529
+ contract_registry_status = "disabled"
1530
+
1312
1531
  banner_lines = [
1313
1532
  "=" * 60,
1314
1533
  f"ONEX Runtime Kernel v{KERNEL_VERSION}",
1315
1534
  f"Environment: {environment}",
1316
1535
  f"Contracts: {contracts_dir}",
1317
1536
  f"Event Bus: {event_bus_type} (group: {config.consumer_group})",
1318
- f"Topics: {config.input_topic} {config.output_topic}",
1537
+ f"Topics: {config.input_topic} -> {config.output_topic}",
1319
1538
  f"Registration: {registration_status}",
1539
+ f"Contract Registry: {contract_registry_status}",
1320
1540
  f"Health endpoint: http://0.0.0.0:{http_port}/health",
1321
1541
  f"Bootstrap time: {bootstrap_duration:.3f}s",
1322
1542
  f"Correlation ID: {correlation_id}",
@@ -1367,6 +1587,47 @@ async def bootstrap() -> int:
1367
1587
  )
1368
1588
  introspection_unsubscribe = None
1369
1589
 
1590
+ # Stop contract registry router and consumers
1591
+ if contract_router is not None:
1592
+ try:
1593
+ await contract_router.stop()
1594
+ logger.debug(
1595
+ "Contract registry router stopped (correlation_id=%s)",
1596
+ correlation_id,
1597
+ )
1598
+ except Exception as router_stop_error:
1599
+ logger.warning(
1600
+ "Failed to stop contract registry router: %s (correlation_id=%s)",
1601
+ sanitize_error_message(router_stop_error),
1602
+ correlation_id,
1603
+ )
1604
+ contract_router = None
1605
+
1606
+ # Unsubscribe from contract registry topics
1607
+ for unsub_name, unsub_func in [
1608
+ ("contract-registered", contract_unsub_registered),
1609
+ ("contract-deregistered", contract_unsub_deregistered),
1610
+ ("node-heartbeat", contract_unsub_heartbeat),
1611
+ ]:
1612
+ if unsub_func is not None:
1613
+ try:
1614
+ await unsub_func()
1615
+ logger.debug(
1616
+ "Contract registry consumer %s stopped (correlation_id=%s)",
1617
+ unsub_name,
1618
+ correlation_id,
1619
+ )
1620
+ except Exception as unsub_error:
1621
+ logger.warning(
1622
+ "Failed to stop contract registry consumer %s: %s (correlation_id=%s)",
1623
+ unsub_name,
1624
+ sanitize_error_message(unsub_error),
1625
+ correlation_id,
1626
+ )
1627
+ contract_unsub_registered = None
1628
+ contract_unsub_deregistered = None
1629
+ contract_unsub_heartbeat = None
1630
+
1370
1631
  # Stop health server (fast, non-blocking)
1371
1632
  if health_server is not None:
1372
1633
  try:
@@ -1492,7 +1753,7 @@ async def bootstrap() -> int:
1492
1753
 
1493
1754
  finally:
1494
1755
  # Guard cleanup - stop all resources if not already stopped
1495
- # Order: introspection consumer -> health server -> runtime -> pool
1756
+ # Order: introspection consumer -> contract registry -> health server -> runtime -> pool
1496
1757
 
1497
1758
  if introspection_unsubscribe is not None:
1498
1759
  try:
@@ -1504,6 +1765,32 @@ async def bootstrap() -> int:
1504
1765
  correlation_id,
1505
1766
  )
1506
1767
 
1768
+ # Cleanup contract registry router and consumers
1769
+ if contract_router is not None:
1770
+ try:
1771
+ await contract_router.stop()
1772
+ except Exception as cleanup_error:
1773
+ logger.warning(
1774
+ "Failed to stop contract registry router during cleanup: %s (correlation_id=%s)",
1775
+ sanitize_error_message(cleanup_error),
1776
+ correlation_id,
1777
+ )
1778
+
1779
+ for unsub_func in [
1780
+ contract_unsub_registered,
1781
+ contract_unsub_deregistered,
1782
+ contract_unsub_heartbeat,
1783
+ ]:
1784
+ if unsub_func is not None:
1785
+ try:
1786
+ await unsub_func()
1787
+ except Exception as cleanup_error:
1788
+ logger.warning(
1789
+ "Failed to stop contract registry consumer during cleanup: %s (correlation_id=%s)",
1790
+ sanitize_error_message(cleanup_error),
1791
+ correlation_id,
1792
+ )
1793
+
1507
1794
  if health_server is not None:
1508
1795
  try:
1509
1796
  await health_server.stop()
@@ -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()