omnibase_infra 0.2.5__py3-none-any.whl → 0.2.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. omnibase_infra/constants_topic_patterns.py +26 -0
  2. omnibase_infra/enums/__init__.py +3 -0
  3. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  4. omnibase_infra/enums/enum_handler_source_mode.py +16 -2
  5. omnibase_infra/errors/__init__.py +4 -0
  6. omnibase_infra/errors/error_binding_resolution.py +128 -0
  7. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +0 -2
  8. omnibase_infra/event_bus/event_bus_inmemory.py +64 -10
  9. omnibase_infra/event_bus/event_bus_kafka.py +105 -47
  10. omnibase_infra/event_bus/mixin_kafka_broadcast.py +3 -7
  11. omnibase_infra/event_bus/mixin_kafka_dlq.py +12 -6
  12. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +0 -81
  13. omnibase_infra/event_bus/testing/__init__.py +26 -0
  14. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  15. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  16. omnibase_infra/handlers/handler_consul.py +2 -0
  17. omnibase_infra/handlers/mixins/__init__.py +5 -0
  18. omnibase_infra/handlers/mixins/mixin_consul_service.py +274 -10
  19. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  20. omnibase_infra/handlers/models/model_filesystem_config.py +4 -4
  21. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  22. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  23. omnibase_infra/mixins/mixin_node_introspection.py +189 -19
  24. omnibase_infra/models/__init__.py +8 -0
  25. omnibase_infra/models/bindings/__init__.py +59 -0
  26. omnibase_infra/models/bindings/constants.py +144 -0
  27. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  28. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  29. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  30. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  31. omnibase_infra/models/discovery/model_introspection_config.py +25 -17
  32. omnibase_infra/models/dispatch/__init__.py +8 -0
  33. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  34. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  35. omnibase_infra/models/handlers/model_handler_source_config.py +1 -1
  36. omnibase_infra/models/model_node_identity.py +126 -0
  37. omnibase_infra/models/projection/model_snapshot_topic_config.py +3 -2
  38. omnibase_infra/models/registration/__init__.py +9 -0
  39. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  40. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  41. omnibase_infra/models/registration/model_node_introspection_event.py +11 -0
  42. omnibase_infra/models/runtime/__init__.py +9 -0
  43. omnibase_infra/models/validation/model_coverage_metrics.py +2 -2
  44. omnibase_infra/nodes/__init__.py +9 -0
  45. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  46. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  47. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  48. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  49. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  50. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  51. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  52. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  53. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  54. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  55. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  56. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  57. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  58. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  59. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  60. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  61. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  62. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  63. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  64. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  65. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  66. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  67. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  68. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  69. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  70. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  71. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  72. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  73. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  74. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  75. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  76. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  77. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  78. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  79. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  80. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  81. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  82. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  83. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  84. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  85. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  86. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -5
  87. omnibase_infra/nodes/reducers/models/__init__.py +7 -2
  88. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +11 -0
  89. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  90. omnibase_infra/nodes/reducers/registration_reducer.py +1 -0
  91. omnibase_infra/protocols/__init__.py +3 -0
  92. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  93. omnibase_infra/runtime/__init__.py +60 -0
  94. omnibase_infra/runtime/binding_resolver.py +753 -0
  95. omnibase_infra/runtime/constants_security.py +70 -0
  96. omnibase_infra/runtime/contract_loaders/__init__.py +9 -0
  97. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  98. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  99. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  100. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  101. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  102. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  103. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  104. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  105. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  106. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  107. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  108. omnibase_infra/runtime/handler_source_resolver.py +43 -2
  109. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  110. omnibase_infra/runtime/models/__init__.py +13 -0
  111. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  112. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  113. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +4 -3
  114. omnibase_infra/runtime/models/model_security_config.py +109 -0
  115. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  116. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  117. omnibase_infra/runtime/service_kernel.py +76 -6
  118. omnibase_infra/runtime/service_message_dispatch_engine.py +558 -15
  119. omnibase_infra/runtime/service_runtime_host_process.py +770 -20
  120. omnibase_infra/runtime/transition_notification_publisher.py +3 -2
  121. omnibase_infra/runtime/util_wiring.py +206 -62
  122. omnibase_infra/services/mcp/service_mcp_tool_sync.py +27 -9
  123. omnibase_infra/services/session/config_consumer.py +25 -8
  124. omnibase_infra/services/session/config_store.py +2 -2
  125. omnibase_infra/services/session/consumer.py +1 -1
  126. omnibase_infra/topics/__init__.py +45 -0
  127. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  128. omnibase_infra/topics/util_topic_composition.py +95 -0
  129. omnibase_infra/types/typed_dict/__init__.py +9 -1
  130. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  131. omnibase_infra/utils/__init__.py +9 -0
  132. omnibase_infra/utils/util_consumer_group.py +232 -0
  133. omnibase_infra/validation/infra_validators.py +18 -1
  134. omnibase_infra/validation/validation_exemptions.yaml +192 -0
  135. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/METADATA +3 -3
  136. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/RECORD +139 -52
  137. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/entry_points.txt +1 -0
  138. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/WHEEL +0 -0
  139. {omnibase_infra-0.2.5.dist-info → omnibase_infra-0.2.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,166 @@
1
+ -- Migration: 001_create_event_ledger.sql
2
+ -- Purpose: Create the event_ledger table for durable event capture and replay
3
+ -- Author: ONEX Infrastructure Team
4
+ -- Date: 2026-01-29
5
+ --
6
+ -- Design Decisions:
7
+ -- 1. TEXT vs VARCHAR(255) for topic: Kafka topic names can exceed 255 characters
8
+ -- with namespaced conventions (e.g., "dev.archon-intelligence.intelligence.code-analysis-requested.v1")
9
+ --
10
+ -- 2. BYTEA for event_key/event_value: Raw binary preservation without encoding assumptions.
11
+ -- Events may contain non-UTF8 data (Protobuf, Avro, MessagePack). TEXT would require
12
+ -- encoding validation and could corrupt binary payloads.
13
+ --
14
+ -- 3. All metadata fields NULLABLE: The audit ledger must NEVER drop events due to
15
+ -- malformed metadata. Missing envelope_id, correlation_id, event_type, or source
16
+ -- must not block event capture. Schema enforcement happens downstream.
17
+ --
18
+ -- 4. JSONB for onex_headers: Structured storage of ONEX-specific headers with indexing
19
+ -- capability. Avoids column explosion as header schema evolves.
20
+ --
21
+ -- 5. Dual timestamps: event_timestamp (from event) may be NULL if source doesn't provide it.
22
+ -- ledger_written_at provides guaranteed ordering for replay scenarios.
23
+ --
24
+ -- 6. Idempotency via (topic, partition, kafka_offset): Ensures exactly-once semantics
25
+ -- for event capture even with consumer restarts or rebalancing.
26
+
27
+ -- =============================================================================
28
+ -- TABLE: event_ledger
29
+ -- =============================================================================
30
+ -- The event_ledger provides durable, append-only storage of all events consumed
31
+ -- from Kafka. It serves as:
32
+ -- - Audit trail for compliance and debugging
33
+ -- - Source for event replay and reprocessing
34
+ -- - Idempotency guard against duplicate processing
35
+ -- =============================================================================
36
+
37
+ CREATE TABLE IF NOT EXISTS event_ledger (
38
+ -- Primary key: Auto-generated UUID for ledger entry identification
39
+ ledger_entry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
40
+
41
+ -- =========================================================================
42
+ -- Kafka Position (Idempotency Key)
43
+ -- =========================================================================
44
+ -- These three fields together form the unique idempotency key.
45
+ -- Any consumer restart will attempt to re-insert, but the constraint prevents duplicates.
46
+ topic TEXT NOT NULL, -- Kafka topic name (TEXT for long namespaced topics)
47
+ partition INTEGER NOT NULL, -- Kafka partition number
48
+ kafka_offset BIGINT NOT NULL, -- Kafka offset within partition
49
+
50
+ -- =========================================================================
51
+ -- Raw Event Data
52
+ -- =========================================================================
53
+ -- Preserved exactly as received from Kafka, no transformation applied.
54
+ event_key BYTEA, -- Kafka message key (nullable - not all events have keys)
55
+ event_value BYTEA NOT NULL, -- Kafka message value (required - the actual event payload)
56
+ onex_headers JSONB NOT NULL DEFAULT '{}', -- ONEX-specific headers extracted from Kafka headers
57
+
58
+ -- =========================================================================
59
+ -- Extracted Metadata (ALL NULLABLE)
60
+ -- =========================================================================
61
+ -- These fields are extracted from the event for query optimization.
62
+ -- ALL are nullable because:
63
+ -- 1. Malformed events must still be captured (audit requirement)
64
+ -- 2. Legacy events may not have all metadata fields
65
+ -- 3. Schema evolution may introduce new optional fields
66
+ -- Missing metadata must NEVER block event capture.
67
+ envelope_id UUID, -- Event envelope identifier (if present)
68
+ correlation_id UUID, -- Request correlation ID for distributed tracing
69
+ event_type TEXT, -- Event type discriminator (e.g., "NodeRegistered")
70
+ source TEXT, -- Event source identifier (e.g., "node-registration-orchestrator")
71
+
72
+ -- =========================================================================
73
+ -- Timestamps
74
+ -- =========================================================================
75
+ event_timestamp TIMESTAMPTZ, -- Timestamp from event payload (nullable - not all events have timestamps)
76
+ ledger_written_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When this ledger entry was written (guaranteed)
77
+
78
+ -- =========================================================================
79
+ -- Constraints
80
+ -- =========================================================================
81
+ -- Idempotency constraint: Ensures each Kafka message is recorded exactly once.
82
+ -- On consumer restart, duplicate inserts will fail gracefully (ON CONFLICT DO NOTHING).
83
+ CONSTRAINT uk_event_ledger_kafka_position UNIQUE (topic, partition, kafka_offset)
84
+ );
85
+
86
+ -- =============================================================================
87
+ -- INDEXES
88
+ -- =============================================================================
89
+ -- Optimized for common query patterns: correlation lookups, event type filtering,
90
+ -- and time-range scans.
91
+
92
+ -- Index 1: Correlation ID lookups (partial - only indexed when NOT NULL)
93
+ -- Use case: Distributed tracing, finding all events for a request
94
+ -- Partial index reduces storage overhead for events without correlation_id
95
+ CREATE INDEX IF NOT EXISTS idx_event_ledger_correlation_id
96
+ ON event_ledger (correlation_id)
97
+ WHERE correlation_id IS NOT NULL;
98
+
99
+ -- Index 2: Event type filtering (partial - only indexed when NOT NULL)
100
+ -- Use case: Finding all events of a specific type for replay or analysis
101
+ -- Partial index excludes malformed events without event_type
102
+ CREATE INDEX IF NOT EXISTS idx_event_ledger_event_type
103
+ ON event_ledger (event_type)
104
+ WHERE event_type IS NOT NULL;
105
+
106
+ -- Index 3: Timestamp ordering with fallback
107
+ -- Use case: Time-range queries for replay, audit, and debugging
108
+ -- COALESCE ensures consistent ordering even when event_timestamp is NULL
109
+ -- Falls back to ledger_written_at which is always populated
110
+ CREATE INDEX IF NOT EXISTS idx_event_ledger_event_timestamp
111
+ ON event_ledger (COALESCE(event_timestamp, ledger_written_at));
112
+
113
+ -- Index 4: Topic + Timestamp composite (for topic-scoped time-range queries)
114
+ -- Use case: Replay events from a specific topic within a time window
115
+ -- Common pattern: "replay all registration events from the last hour"
116
+ CREATE INDEX IF NOT EXISTS idx_event_ledger_topic_timestamp
117
+ ON event_ledger (topic, COALESCE(event_timestamp, ledger_written_at));
118
+
119
+ -- =============================================================================
120
+ -- COMMENTS
121
+ -- =============================================================================
122
+
123
+ COMMENT ON TABLE event_ledger IS
124
+ 'Durable, append-only ledger of all events consumed from Kafka. Provides audit trail, replay capability, and idempotency guarantees.';
125
+
126
+ COMMENT ON COLUMN event_ledger.ledger_entry_id IS
127
+ 'Auto-generated UUID primary key for this ledger entry';
128
+
129
+ COMMENT ON COLUMN event_ledger.topic IS
130
+ 'Kafka topic name (TEXT to support long namespaced topics)';
131
+
132
+ COMMENT ON COLUMN event_ledger.partition IS
133
+ 'Kafka partition number';
134
+
135
+ COMMENT ON COLUMN event_ledger.kafka_offset IS
136
+ 'Kafka offset within the partition (idempotency key component)';
137
+
138
+ COMMENT ON COLUMN event_ledger.event_key IS
139
+ 'Raw Kafka message key as BYTEA (nullable - not all events have keys)';
140
+
141
+ COMMENT ON COLUMN event_ledger.event_value IS
142
+ 'Raw Kafka message value as BYTEA (required - the actual event payload)';
143
+
144
+ COMMENT ON COLUMN event_ledger.onex_headers IS
145
+ 'ONEX-specific headers extracted from Kafka headers as JSONB';
146
+
147
+ COMMENT ON COLUMN event_ledger.envelope_id IS
148
+ 'Event envelope identifier extracted from payload (nullable for malformed events)';
149
+
150
+ COMMENT ON COLUMN event_ledger.correlation_id IS
151
+ 'Request correlation ID for distributed tracing (nullable for legacy events)';
152
+
153
+ COMMENT ON COLUMN event_ledger.event_type IS
154
+ 'Event type discriminator extracted from payload (nullable for malformed events)';
155
+
156
+ COMMENT ON COLUMN event_ledger.source IS
157
+ 'Event source identifier (nullable for events without source metadata)';
158
+
159
+ COMMENT ON COLUMN event_ledger.event_timestamp IS
160
+ 'Timestamp from event payload (nullable - falls back to ledger_written_at)';
161
+
162
+ COMMENT ON COLUMN event_ledger.ledger_written_at IS
163
+ 'Timestamp when this entry was written to the ledger (guaranteed, used for ordering fallback)';
164
+
165
+ COMMENT ON CONSTRAINT uk_event_ledger_kafka_position ON event_ledger IS
166
+ 'Idempotency constraint: ensures each Kafka message is recorded exactly once';
@@ -0,0 +1,18 @@
1
+ -- Migration: 001_drop_event_ledger.sql
2
+ -- Purpose: Rollback for 001_create_event_ledger.sql
3
+ -- Author: ONEX Infrastructure Team
4
+ -- Date: 2026-01-29
5
+ --
6
+ -- WARNING: This migration is DESTRUCTIVE. All data in event_ledger will be lost.
7
+ -- Use only in development or when intentionally resetting the ledger.
8
+ --
9
+ -- This rollback drops:
10
+ -- - Table: event_ledger
11
+ -- - Constraint: uk_event_ledger_kafka_position (dropped with table)
12
+ -- - Index: idx_event_ledger_correlation_id (dropped with table)
13
+ -- - Index: idx_event_ledger_event_type (dropped with table)
14
+ -- - Index: idx_event_ledger_event_timestamp (dropped with table)
15
+ -- - Index: idx_event_ledger_topic_timestamp (dropped with table)
16
+ -- - All table and column comments (dropped with table)
17
+
18
+ DROP TABLE IF EXISTS event_ledger CASCADE;
@@ -199,6 +199,7 @@ import asyncio
199
199
  import inspect
200
200
  import json
201
201
  import logging
202
+ import os
202
203
  import time
203
204
  from collections.abc import AsyncIterator, Awaitable, Callable
204
205
  from contextlib import asynccontextmanager
@@ -210,6 +211,7 @@ from omnibase_core.enums import EnumNodeKind
210
211
  from omnibase_core.models.events.model_event_envelope import ModelEventEnvelope
211
212
  from omnibase_core.models.primitives.model_semver import ModelSemVer
212
213
  from omnibase_infra.capabilities import ContractCapabilityExtractor
214
+ from omnibase_infra.constants_topic_patterns import TOPIC_NAME_PATTERN
213
215
  from omnibase_infra.enums import EnumInfraTransportType, EnumIntrospectionReason
214
216
  from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
215
217
  from omnibase_infra.models.discovery import (
@@ -224,6 +226,10 @@ from omnibase_infra.models.registration import (
224
226
  ModelNodeCapabilities,
225
227
  ModelNodeHeartbeatEvent,
226
228
  )
229
+ from omnibase_infra.models.registration.model_node_event_bus_config import (
230
+ ModelEventBusTopicEntry,
231
+ ModelNodeEventBusConfig,
232
+ )
227
233
  from omnibase_infra.models.registration.model_node_introspection_event import (
228
234
  ModelNodeIntrospectionEvent,
229
235
  )
@@ -237,11 +243,6 @@ if TYPE_CHECKING:
237
243
 
238
244
  logger = logging.getLogger(__name__)
239
245
 
240
- # Event topic constants
241
- INTROSPECTION_TOPIC = "node.introspection"
242
- HEARTBEAT_TOPIC = "node.heartbeat"
243
- REQUEST_INTROSPECTION_TOPIC = "node.request_introspection"
244
-
245
246
  # Performance threshold constants (in milliseconds)
246
247
  PERF_THRESHOLD_GET_CAPABILITIES_MS = 50.0
247
248
  PERF_THRESHOLD_DISCOVER_CAPABILITIES_MS = 30.0
@@ -977,6 +978,109 @@ class MixinNodeIntrospection:
977
978
  )
978
979
  return None
979
980
 
981
+ def _extract_event_bus_config(
982
+ self,
983
+ env_prefix: str,
984
+ ) -> ModelNodeEventBusConfig | None:
985
+ """Extract and resolve event_bus config from contract.
986
+
987
+ Extracts topic suffixes from the contract's event_bus subcontract and
988
+ resolves them to full environment-qualified topic strings.
989
+
990
+ Topic Resolution:
991
+ Contract topics are suffixes (e.g., "onex.evt.intent-classified.v1").
992
+ This method prepends the environment prefix to create full topics
993
+ (e.g., "dev.onex.evt.intent-classified.v1").
994
+
995
+ Args:
996
+ env_prefix: Environment prefix (e.g., "dev", "prod", "staging").
997
+ Must be a valid identifier without dots or special characters.
998
+
999
+ Returns:
1000
+ Resolved event bus config with full topic strings, or None if:
1001
+ - No contract is configured (_introspection_contract is None)
1002
+ - Contract has no event_bus subcontract
1003
+ - event_bus subcontract has no publish_topics or subscribe_topics
1004
+
1005
+ Raises:
1006
+ ValueError: If topic resolution fails due to unresolved placeholders
1007
+ (e.g., "{env}" or "{namespace}" remaining in the resolved topic).
1008
+ This is a fail-fast mechanism to prevent misconfigured topics
1009
+ from being published to the registry.
1010
+
1011
+ Example:
1012
+ >>> config = self._extract_event_bus_config("dev")
1013
+ >>> config.publish_topic_strings
1014
+ ['dev.onex.evt.node-registered.v1']
1015
+
1016
+ See Also:
1017
+ - ModelEventBusSubcontract: Contract model with topic suffixes
1018
+ - ModelNodeEventBusConfig: Registry storage model with full topics
1019
+ """
1020
+ if self._introspection_contract is None:
1021
+ return None
1022
+
1023
+ # Get event_bus subcontract if present
1024
+ event_bus_sub = getattr(self._introspection_contract, "event_bus", None)
1025
+ if event_bus_sub is None:
1026
+ return None
1027
+
1028
+ # Get topic suffix lists from the subcontract
1029
+ publish_suffixes: list[str] = (
1030
+ getattr(event_bus_sub, "publish_topics", None) or []
1031
+ )
1032
+ subscribe_suffixes: list[str] = (
1033
+ getattr(event_bus_sub, "subscribe_topics", None) or []
1034
+ )
1035
+
1036
+ if not publish_suffixes and not subscribe_suffixes:
1037
+ return None
1038
+
1039
+ def resolve_topic(suffix: str) -> str:
1040
+ """Resolve topic suffix to full topic with env prefix."""
1041
+ # Full topic format: {env}.{suffix}
1042
+ # Strip whitespace from suffix to handle YAML formatting artifacts
1043
+ suffix = suffix.strip()
1044
+
1045
+ # Fail-fast: check for unresolved placeholders BEFORE format validation
1046
+ # This provides more helpful error messages when placeholders aren't resolved
1047
+ if "{" in suffix or "}" in suffix:
1048
+ raise ValueError(
1049
+ f"Unresolved placeholder in topic: '{suffix}'. "
1050
+ "Ensure all placeholders like {env} or {namespace} are resolved "
1051
+ "before topic validation."
1052
+ )
1053
+
1054
+ # Validate suffix format (alphanumeric, dots, hyphens, underscores)
1055
+ if not TOPIC_NAME_PATTERN.match(suffix):
1056
+ context = ModelInfraErrorContext.with_correlation(
1057
+ transport_type=EnumInfraTransportType.RUNTIME,
1058
+ operation="_extract_event_bus_config",
1059
+ target_name=suffix,
1060
+ )
1061
+ raise ProtocolConfigurationError(
1062
+ f"Invalid topic suffix format: '{suffix}'. "
1063
+ "Topics must contain only alphanumeric characters, dots, hyphens, and underscores.",
1064
+ context=context,
1065
+ parameter="topic_suffix",
1066
+ )
1067
+
1068
+ full_topic = f"{env_prefix}.{suffix}"
1069
+
1070
+ return full_topic
1071
+
1072
+ def build_entry(suffix: str) -> ModelEventBusTopicEntry:
1073
+ """Build topic entry from suffix."""
1074
+ return ModelEventBusTopicEntry(
1075
+ topic=resolve_topic(suffix),
1076
+ # Metadata fields left as defaults (tooling-only)
1077
+ )
1078
+
1079
+ return ModelNodeEventBusConfig(
1080
+ publish_topics=[build_entry(s) for s in publish_suffixes],
1081
+ subscribe_topics=[build_entry(s) for s in subscribe_suffixes],
1082
+ )
1083
+
980
1084
  async def get_capabilities(self) -> ModelDiscoveredCapabilities:
981
1085
  """Extract node capabilities via reflection.
982
1086
 
@@ -1355,6 +1459,28 @@ class MixinNodeIntrospection:
1355
1459
  self._introspection_contract
1356
1460
  )
1357
1461
 
1462
+ # Extract event_bus config from contract (OMN-1613)
1463
+ # Resolves topic suffixes to full environment-qualified topics
1464
+ # ValueError from _extract_event_bus_config (unresolved placeholders) is
1465
+ # wrapped in ProtocolConfigurationError for consistent error handling.
1466
+ # ProtocolConfigurationError (invalid format) propagates directly.
1467
+ event_bus_config: ModelNodeEventBusConfig | None = None
1468
+ env_prefix = os.getenv("ONEX_ENV", "dev")
1469
+ try:
1470
+ event_bus_config = self._extract_event_bus_config(env_prefix)
1471
+ except ValueError as e:
1472
+ # Wrap ValueError in ProtocolConfigurationError for fail-fast behavior
1473
+ context = ModelInfraErrorContext.with_correlation(
1474
+ transport_type=EnumInfraTransportType.RUNTIME,
1475
+ operation="get_introspection_data",
1476
+ target_name="event_bus",
1477
+ )
1478
+ raise ProtocolConfigurationError(
1479
+ f"Event bus extraction failed: {e}",
1480
+ context=context,
1481
+ parameter="event_bus",
1482
+ ) from e
1483
+
1358
1484
  # Create event with performance metrics (metrics is already Pydantic model)
1359
1485
  event = ModelNodeIntrospectionEvent(
1360
1486
  node_id=node_id_uuid,
@@ -1369,6 +1495,7 @@ class MixinNodeIntrospection:
1369
1495
  correlation_id=uuid4(),
1370
1496
  timestamp=datetime.now(UTC),
1371
1497
  performance_metrics=metrics,
1498
+ event_bus=event_bus_config,
1372
1499
  )
1373
1500
 
1374
1501
  # Update cache - cast the model_dump output to our typed dict since we know
@@ -1549,6 +1676,7 @@ class MixinNodeIntrospection:
1549
1676
  extra={
1550
1677
  "node_id": self._introspection_node_id,
1551
1678
  "reason": reason_enum.value,
1679
+ "correlation_id": str(final_correlation_id),
1552
1680
  "error_type": type(e).__name__,
1553
1681
  "error_message": str(e),
1554
1682
  },
@@ -1580,6 +1708,9 @@ class MixinNodeIntrospection:
1580
1708
  if self._introspection_event_bus is None:
1581
1709
  return False
1582
1710
 
1711
+ # Generate correlation_id early for reliable exception logging
1712
+ heartbeat_correlation_id = uuid4()
1713
+
1583
1714
  try:
1584
1715
  # Calculate uptime
1585
1716
  uptime_seconds = 0.0
@@ -1593,7 +1724,10 @@ class MixinNodeIntrospection:
1593
1724
  logger.warning(
1594
1725
  "Node ID not initialized, using nil UUID in heartbeat - "
1595
1726
  "ensure initialize_introspection() was called correctly",
1596
- extra={"operation": "_publish_heartbeat"},
1727
+ extra={
1728
+ "operation": "_publish_heartbeat",
1729
+ "correlation_id": str(heartbeat_correlation_id),
1730
+ },
1597
1731
  )
1598
1732
  # Use nil UUID (all zeros) as sentinel for uninitialized node
1599
1733
  node_id = UUID("00000000-0000-0000-0000-000000000000")
@@ -1605,7 +1739,11 @@ class MixinNodeIntrospection:
1605
1739
  logger.warning(
1606
1740
  "Node type not initialized, using EFFECT in heartbeat - "
1607
1741
  "ensure initialize_introspection() was called correctly",
1608
- extra={"node_id": str(node_id), "operation": "_publish_heartbeat"},
1742
+ extra={
1743
+ "node_id": str(node_id),
1744
+ "operation": "_publish_heartbeat",
1745
+ "correlation_id": str(heartbeat_correlation_id),
1746
+ },
1609
1747
  )
1610
1748
  node_type = EnumNodeKind.EFFECT
1611
1749
 
@@ -1620,7 +1758,7 @@ class MixinNodeIntrospection:
1620
1758
  node_type=node_type,
1621
1759
  uptime_seconds=uptime_seconds,
1622
1760
  active_operations_count=active_ops_count,
1623
- correlation_id=uuid4(),
1761
+ correlation_id=heartbeat_correlation_id,
1624
1762
  timestamp=now, # Required: time injection pattern
1625
1763
  )
1626
1764
 
@@ -1653,6 +1791,7 @@ class MixinNodeIntrospection:
1653
1791
  f"Published heartbeat for {self._introspection_node_id}",
1654
1792
  extra={
1655
1793
  "node_id": self._introspection_node_id,
1794
+ "correlation_id": str(heartbeat_correlation_id),
1656
1795
  "uptime_seconds": uptime_seconds,
1657
1796
  "active_operations": active_ops_count,
1658
1797
  "topic": topic,
@@ -1667,6 +1806,7 @@ class MixinNodeIntrospection:
1667
1806
  f"Failed to publish heartbeat for {self._introspection_node_id}",
1668
1807
  extra={
1669
1808
  "node_id": self._introspection_node_id,
1809
+ "correlation_id": str(heartbeat_correlation_id),
1670
1810
  "error_type": type(e).__name__,
1671
1811
  "error_message": str(e),
1672
1812
  },
@@ -1696,12 +1836,17 @@ class MixinNodeIntrospection:
1696
1836
  )
1697
1837
 
1698
1838
  while not self._introspection_stop_event.is_set():
1839
+ # Generate correlation_id for this loop iteration for traceability
1840
+ loop_correlation_id = uuid4()
1699
1841
  try:
1700
1842
  await self._publish_heartbeat()
1701
1843
  except asyncio.CancelledError:
1702
1844
  logger.debug(
1703
1845
  f"Heartbeat loop cancelled for {self._introspection_node_id}",
1704
- extra={"node_id": self._introspection_node_id},
1846
+ extra={
1847
+ "node_id": self._introspection_node_id,
1848
+ "correlation_id": str(loop_correlation_id),
1849
+ },
1705
1850
  )
1706
1851
  break
1707
1852
  except Exception as e:
@@ -1711,6 +1856,7 @@ class MixinNodeIntrospection:
1711
1856
  f"Error in heartbeat loop for {self._introspection_node_id}",
1712
1857
  extra={
1713
1858
  "node_id": self._introspection_node_id,
1859
+ "correlation_id": str(loop_correlation_id),
1714
1860
  "error_type": type(e).__name__,
1715
1861
  "error_message": str(e),
1716
1862
  },
@@ -1783,9 +1929,17 @@ class MixinNodeIntrospection:
1783
1929
  """
1784
1930
  return consecutive_failures == 1 or consecutive_failures % threshold == 0
1785
1931
 
1786
- async def _cleanup_registry_subscription(self) -> None:
1787
- """Clean up the current registry subscription."""
1932
+ async def _cleanup_registry_subscription(
1933
+ self, correlation_id: UUID | None = None
1934
+ ) -> None:
1935
+ """Clean up the current registry subscription.
1936
+
1937
+ Args:
1938
+ correlation_id: Optional correlation ID for traceability in logs.
1939
+ If not provided, a new one will be generated.
1940
+ """
1788
1941
  if self._registry_unsubscribe is not None:
1942
+ cleanup_correlation_id = correlation_id or uuid4()
1789
1943
  try:
1790
1944
  result = self._registry_unsubscribe()
1791
1945
  if asyncio.iscoroutine(result):
@@ -1796,6 +1950,7 @@ class MixinNodeIntrospection:
1796
1950
  f"{self._introspection_node_id}",
1797
1951
  extra={
1798
1952
  "node_id": self._introspection_node_id,
1953
+ "correlation_id": str(cleanup_correlation_id),
1799
1954
  "error_type": type(cleanup_error).__name__,
1800
1955
  "error_message": str(cleanup_error),
1801
1956
  },
@@ -1814,12 +1969,14 @@ class MixinNodeIntrospection:
1814
1969
  Args:
1815
1970
  message: The incoming event message (implements ProtocolEventMessage protocol)
1816
1971
  """
1972
+ # Generate correlation_id for this request for traceability
1973
+ request_correlation_id = uuid4()
1817
1974
  try:
1818
1975
  await self._process_introspection_request(message)
1819
1976
  # Reset failure counter on success
1820
1977
  self._registry_callback_consecutive_failures = 0
1821
1978
  except Exception as e:
1822
- self._handle_request_error(e)
1979
+ self._handle_request_error(e, request_correlation_id)
1823
1980
 
1824
1981
  async def _process_introspection_request(
1825
1982
  self, message: ProtocolEventMessage
@@ -1859,13 +2016,14 @@ class MixinNodeIntrospection:
1859
2016
  correlation_id=correlation_id,
1860
2017
  )
1861
2018
 
1862
- def _handle_request_error(self, error: Exception) -> None:
2019
+ def _handle_request_error(self, error: Exception, correlation_id: UUID) -> None:
1863
2020
  """Handle error during introspection request processing.
1864
2021
 
1865
2022
  Tracks consecutive failures and rate-limits error logging.
1866
2023
 
1867
2024
  Args:
1868
2025
  error: The exception that occurred
2026
+ correlation_id: Correlation ID for traceability in logs
1869
2027
  """
1870
2028
  # Track consecutive failures for rate-limited logging
1871
2029
  self._registry_callback_consecutive_failures += 1
@@ -1880,6 +2038,7 @@ class MixinNodeIntrospection:
1880
2038
  f"Error handling introspection request for {self._introspection_node_id}",
1881
2039
  extra={
1882
2040
  "node_id": self._introspection_node_id,
2041
+ "correlation_id": str(correlation_id),
1883
2042
  "error_type": type(error).__name__,
1884
2043
  "error_message": str(error),
1885
2044
  "consecutive_failures": self._registry_callback_consecutive_failures,
@@ -1895,6 +2054,7 @@ class MixinNodeIntrospection:
1895
2054
  f"(failure {self._registry_callback_consecutive_failures})",
1896
2055
  extra={
1897
2056
  "node_id": self._introspection_node_id,
2057
+ "correlation_id": str(correlation_id),
1898
2058
  "error_type": type(error).__name__,
1899
2059
  "consecutive_failures": self._registry_callback_consecutive_failures,
1900
2060
  },
@@ -2028,6 +2188,8 @@ class MixinNodeIntrospection:
2028
2188
  # Retry loop with exponential backoff for subscription failures
2029
2189
  retry_count = 0
2030
2190
  while not self._introspection_stop_event.is_set():
2191
+ # Generate correlation_id for this subscription attempt for traceability
2192
+ subscription_correlation_id = uuid4()
2031
2193
  try:
2032
2194
  if await self._attempt_subscription():
2033
2195
  # Wait for stop signal
@@ -2038,13 +2200,20 @@ class MixinNodeIntrospection:
2038
2200
  except asyncio.CancelledError:
2039
2201
  logger.debug(
2040
2202
  f"Registry listener cancelled for {self._introspection_node_id}",
2041
- extra={"node_id": self._introspection_node_id},
2203
+ extra={
2204
+ "node_id": self._introspection_node_id,
2205
+ "correlation_id": str(subscription_correlation_id),
2206
+ },
2042
2207
  )
2043
2208
  break
2044
2209
  except Exception as e:
2045
2210
  retry_count += 1
2046
2211
  if not await self._handle_subscription_error(
2047
- e, retry_count, max_retries, base_backoff_seconds
2212
+ e,
2213
+ retry_count,
2214
+ max_retries,
2215
+ base_backoff_seconds,
2216
+ subscription_correlation_id,
2048
2217
  ):
2049
2218
  break
2050
2219
 
@@ -2062,6 +2231,7 @@ class MixinNodeIntrospection:
2062
2231
  retry_count: int,
2063
2232
  max_retries: int,
2064
2233
  base_backoff_seconds: float,
2234
+ correlation_id: UUID,
2065
2235
  ) -> bool:
2066
2236
  """Handle subscription error with retry logic.
2067
2237
 
@@ -2070,6 +2240,7 @@ class MixinNodeIntrospection:
2070
2240
  retry_count: Current retry attempt number
2071
2241
  max_retries: Maximum retry attempts
2072
2242
  base_backoff_seconds: Base backoff time for exponential retry
2243
+ correlation_id: Correlation ID for traceability in logs
2073
2244
 
2074
2245
  Returns:
2075
2246
  True if should continue retrying, False if should stop
@@ -2078,6 +2249,7 @@ class MixinNodeIntrospection:
2078
2249
  f"Error in registry listener for {self._introspection_node_id}",
2079
2250
  extra={
2080
2251
  "node_id": self._introspection_node_id,
2252
+ "correlation_id": str(correlation_id),
2081
2253
  "error_type": type(error).__name__,
2082
2254
  "error_message": str(error),
2083
2255
  "retry_count": retry_count,
@@ -2087,7 +2259,7 @@ class MixinNodeIntrospection:
2087
2259
  )
2088
2260
 
2089
2261
  # Clean up any partial subscription before retry
2090
- await self._cleanup_registry_subscription()
2262
+ await self._cleanup_registry_subscription(correlation_id)
2091
2263
 
2092
2264
  # Check if we should retry
2093
2265
  if retry_count >= max_retries:
@@ -2095,6 +2267,7 @@ class MixinNodeIntrospection:
2095
2267
  "Registry listener exhausted retries",
2096
2268
  extra={
2097
2269
  "node_id": self._introspection_node_id,
2270
+ "correlation_id": str(correlation_id),
2098
2271
  "retry_count": retry_count,
2099
2272
  "max_retries": max_retries,
2100
2273
  "error_type": type(error).__name__,
@@ -2485,13 +2658,10 @@ class MixinNodeIntrospection:
2485
2658
 
2486
2659
 
2487
2660
  __all__ = [
2488
- "HEARTBEAT_TOPIC",
2489
- "INTROSPECTION_TOPIC",
2490
2661
  "PERF_THRESHOLD_CACHE_HIT_MS",
2491
2662
  "PERF_THRESHOLD_DISCOVER_CAPABILITIES_MS",
2492
2663
  "PERF_THRESHOLD_GET_CAPABILITIES_MS",
2493
2664
  "PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS",
2494
- "REQUEST_INTROSPECTION_TOPIC",
2495
2665
  "DiscoveredCapabilitiesCacheDict", # TypedDict for cached discovered capabilities
2496
2666
  "IntrospectionCacheDict",
2497
2667
  "MixinNodeIntrospection",
@@ -5,6 +5,9 @@
5
5
  This module exports all infrastructure-specific Pydantic models.
6
6
  """
7
7
 
8
+ from omnibase_infra.models.bindings import (
9
+ ModelParsedBinding,
10
+ )
8
11
  from omnibase_infra.models.dispatch import (
9
12
  EnumDispatchStatus,
10
13
  EnumTopicStandard,
@@ -22,6 +25,7 @@ from omnibase_infra.models.errors import ModelHandlerValidationError
22
25
  from omnibase_infra.models.handlers import ModelHandlerIdentifier
23
26
  from omnibase_infra.models.health import ModelHealthCheckResult
24
27
  from omnibase_infra.models.logging import ModelLogContext
28
+ from omnibase_infra.models.model_node_identity import ModelNodeIdentity
25
29
  from omnibase_infra.models.model_retry_error_classification import (
26
30
  ModelRetryErrorClassification,
27
31
  )
@@ -75,6 +79,8 @@ from omnibase_infra.models.validation import (
75
79
  )
76
80
 
77
81
  __all__: list[str] = [
82
+ # Binding models
83
+ "ModelParsedBinding",
78
84
  # Dispatch models
79
85
  "EnumDispatchStatus",
80
86
  "EnumTopicStandard",
@@ -108,6 +114,8 @@ __all__: list[str] = [
108
114
  "ModelLogContext",
109
115
  "ModelNodeCapabilities",
110
116
  "ModelNodeHeartbeatEvent",
117
+ # Node identity model
118
+ "ModelNodeIdentity",
111
119
  "ModelNodeIntrospectionEvent",
112
120
  "ModelNodeMetadata",
113
121
  "ModelNodeRegistration",