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
@@ -0,0 +1,26 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Database return type model for SQL operations."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class ModelDbReturn(BaseModel):
11
+ """Return type specification for a database operation.
12
+
13
+ Attributes:
14
+ model_ref: Reference to the model type for results (e.g., "User")
15
+ many: If True, operation returns multiple rows; if False, single row
16
+ """
17
+
18
+ model_config = ConfigDict(frozen=True, extra="forbid")
19
+
20
+ model_ref: str = Field(default="", description="Model reference for results")
21
+ many: bool = Field(
22
+ default=False, description="Whether operation returns multiple rows"
23
+ )
24
+
25
+
26
+ __all__ = ["ModelDbReturn"]
@@ -0,0 +1,32 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Database safety policy model for SQL operations."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+
10
+ class ModelDbSafetyPolicy(BaseModel):
11
+ """Safety policy constraints for database operations.
12
+
13
+ Attributes:
14
+ require_where_clause: If True, require WHERE clause for updates/deletes
15
+ max_affected_rows: Maximum number of rows that can be affected
16
+ allow_full_table_scan: If True, allow queries without index usage
17
+ """
18
+
19
+ model_config = ConfigDict(frozen=True, extra="forbid")
20
+
21
+ require_where_clause: bool = Field(
22
+ default=True, description="Require WHERE clause for updates/deletes"
23
+ )
24
+ max_affected_rows: int | None = Field(
25
+ default=None, description="Maximum affected rows (None = unlimited)"
26
+ )
27
+ allow_full_table_scan: bool = Field(
28
+ default=True, description="Allow queries without index usage"
29
+ )
30
+
31
+
32
+ __all__ = ["ModelDbSafetyPolicy"]
@@ -25,15 +25,15 @@ Example Usage:
25
25
  registry.register(
26
26
  ModelEventRegistration(
27
27
  event_type="custom.event",
28
- topic_template="{env}.onex.evt.custom.event.v1",
28
+ topic_template="onex.evt.custom.event.v1",
29
29
  partition_key_field="session_id",
30
30
  required_fields=["session_id", "user_id"],
31
31
  )
32
32
  )
33
33
 
34
- # Resolve topic for event type
34
+ # Resolve topic for event type (realm-agnostic, no env prefix)
35
35
  topic = registry.resolve_topic("prompt.submitted")
36
- # Returns: "dev.onex.evt.omniclaude.prompt-submitted.v1"
36
+ # Returns: "onex.evt.omniclaude.prompt-submitted.v1"
37
37
 
38
38
  # Inject metadata into payload
39
39
  enriched = registry.inject_metadata(
@@ -70,8 +70,10 @@ class ModelEventRegistration(BaseModel):
70
70
  Attributes:
71
71
  event_type: Semantic event type identifier (e.g., "prompt.submitted").
72
72
  This is the logical name used by event emitters.
73
- topic_template: Kafka topic name template with {env} placeholder.
74
- Example: "{env}.onex.evt.omniclaude.prompt-submitted.v1"
73
+ topic_template: Kafka topic name (realm-agnostic, no environment prefix).
74
+ Example: "onex.evt.omniclaude.prompt-submitted.v1"
75
+ Note: Topics are realm-agnostic in ONEX. The environment/realm is
76
+ enforced via envelope identity, not topic naming.
75
77
  partition_key_field: Optional field name in payload to use as partition key.
76
78
  When set, ensures events with same key go to same partition for ordering.
77
79
  required_fields: List of field names that must be present in payload.
@@ -82,7 +84,7 @@ class ModelEventRegistration(BaseModel):
82
84
  Example:
83
85
  >>> reg = ModelEventRegistration(
84
86
  ... event_type="prompt.submitted",
85
- ... topic_template="{env}.onex.evt.omniclaude.prompt-submitted.v1",
87
+ ... topic_template="onex.evt.omniclaude.prompt-submitted.v1",
86
88
  ... partition_key_field="session_id",
87
89
  ... required_fields=["prompt", "session_id"],
88
90
  ... schema_version="1.0.0",
@@ -99,7 +101,7 @@ class ModelEventRegistration(BaseModel):
99
101
  description="Semantic event type identifier (e.g., 'prompt.submitted')",
100
102
  )
101
103
  topic_template: str = Field(
102
- description="Kafka topic name template with {env} placeholder",
104
+ description="Kafka topic name (realm-agnostic, no environment prefix)",
103
105
  )
104
106
  partition_key_field: str | None = Field(
105
107
  default=None,
@@ -133,7 +135,7 @@ class EventRegistry:
133
135
  >>> registry = EventRegistry(environment="dev")
134
136
  >>> topic = registry.resolve_topic("prompt.submitted")
135
137
  >>> print(topic)
136
- 'dev.onex.evt.omniclaude.prompt-submitted.v1'
138
+ 'onex.evt.omniclaude.prompt-submitted.v1'
137
139
 
138
140
  >>> registry.validate_payload("prompt.submitted", {"prompt": "Hello"})
139
141
  True
@@ -144,6 +146,11 @@ class EventRegistry:
144
146
  ... )
145
147
  >>> "correlation_id" in enriched
146
148
  True
149
+
150
+ Note:
151
+ Topics are realm-agnostic in ONEX. The environment is stored for
152
+ potential use in consumer group derivation by related components,
153
+ but topics themselves do not include environment prefixes.
147
154
  """
148
155
 
149
156
  def __init__(self, environment: str = "dev") -> None:
@@ -156,7 +163,11 @@ class EventRegistry:
156
163
  Example:
157
164
  >>> registry = EventRegistry(environment="staging")
158
165
  >>> registry.resolve_topic("prompt.submitted")
159
- 'staging.onex.evt.omniclaude.prompt-submitted.v1'
166
+ 'onex.evt.omniclaude.prompt-submitted.v1'
167
+
168
+ Note:
169
+ The environment is stored for potential consumer group derivation
170
+ in related components. Topics themselves are realm-agnostic.
160
171
  """
161
172
  self._environment = environment
162
173
  self._registrations: dict[str, ModelEventRegistration] = {}
@@ -174,25 +185,25 @@ class EventRegistry:
174
185
  defaults = [
175
186
  ModelEventRegistration(
176
187
  event_type="prompt.submitted",
177
- topic_template="{env}.onex.evt.omniclaude.prompt-submitted.v1",
188
+ topic_template="onex.evt.omniclaude.prompt-submitted.v1",
178
189
  partition_key_field="session_id",
179
190
  required_fields=["prompt"],
180
191
  ),
181
192
  ModelEventRegistration(
182
193
  event_type="session.started",
183
- topic_template="{env}.onex.evt.omniclaude.session-started.v1",
194
+ topic_template="onex.evt.omniclaude.session-started.v1",
184
195
  partition_key_field="session_id",
185
196
  required_fields=["session_id"],
186
197
  ),
187
198
  ModelEventRegistration(
188
199
  event_type="session.ended",
189
- topic_template="{env}.onex.evt.omniclaude.session-ended.v1",
200
+ topic_template="onex.evt.omniclaude.session-ended.v1",
190
201
  partition_key_field="session_id",
191
202
  required_fields=["session_id"],
192
203
  ),
193
204
  ModelEventRegistration(
194
205
  event_type="tool.executed",
195
- topic_template="{env}.onex.evt.omniclaude.tool-executed.v1",
206
+ topic_template="onex.evt.omniclaude.tool-executed.v1",
196
207
  partition_key_field="session_id",
197
208
  required_fields=["tool_name"],
198
209
  ),
@@ -214,25 +225,26 @@ class EventRegistry:
214
225
  >>> registry.register(
215
226
  ... ModelEventRegistration(
216
227
  ... event_type="custom.event",
217
- ... topic_template="{env}.onex.evt.custom.event.v1",
228
+ ... topic_template="onex.evt.custom.event.v1",
218
229
  ... )
219
230
  ... )
220
231
  >>> registry.resolve_topic("custom.event")
221
- 'dev.onex.evt.custom.event.v1'
232
+ 'onex.evt.custom.event.v1'
222
233
  """
223
234
  self._registrations[registration.event_type] = registration
224
235
 
225
236
  def resolve_topic(self, event_type: str) -> str:
226
- """Get the Kafka topic for an event type.
237
+ """Get the Kafka topic for an event type (realm-agnostic).
227
238
 
228
- Resolves the topic template by substituting the {env} placeholder
229
- with the configured environment name.
239
+ Topics are realm-agnostic in ONEX. The environment/realm is enforced via
240
+ envelope identity, not topic naming. This enables cross-environment event
241
+ routing when needed while maintaining proper isolation through identity.
230
242
 
231
243
  Args:
232
244
  event_type: Semantic event type identifier.
233
245
 
234
246
  Returns:
235
- Fully resolved Kafka topic name.
247
+ Kafka topic name (no environment prefix).
236
248
 
237
249
  Raises:
238
250
  OnexError: If the event type is not registered.
@@ -240,7 +252,7 @@ class EventRegistry:
240
252
  Example:
241
253
  >>> registry = EventRegistry(environment="prod")
242
254
  >>> registry.resolve_topic("prompt.submitted")
243
- 'prod.onex.evt.omniclaude.prompt-submitted.v1'
255
+ 'onex.evt.omniclaude.prompt-submitted.v1'
244
256
  """
245
257
  registration = self._registrations.get(event_type)
246
258
  if registration is None:
@@ -248,7 +260,7 @@ class EventRegistry:
248
260
  raise OnexError(
249
261
  f"Unknown event type: '{event_type}'. Registered types: {registered}"
250
262
  )
251
- return registration.topic_template.format(env=self._environment)
263
+ return registration.topic_template
252
264
 
253
265
  def get_partition_key(
254
266
  self,
@@ -452,7 +464,7 @@ class EventRegistry:
452
464
  >>> registry = EventRegistry()
453
465
  >>> reg = registry.get_registration("prompt.submitted")
454
466
  >>> reg.topic_template
455
- '{env}.onex.evt.omniclaude.prompt-submitted.v1'
467
+ 'onex.evt.omniclaude.prompt-submitted.v1'
456
468
  """
457
469
  return self._registrations.get(event_type)
458
470
 
@@ -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)