omnibase_infra 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/errors/__init__.py +4 -0
- omnibase_infra/errors/error_infra.py +60 -0
- omnibase_infra/handlers/__init__.py +3 -0
- omnibase_infra/handlers/handler_slack_webhook.py +426 -0
- omnibase_infra/handlers/models/__init__.py +14 -0
- omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
- omnibase_infra/handlers/models/model_slack_alert.py +24 -0
- omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
- omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
- omnibase_infra/mixins/mixin_node_introspection.py +42 -20
- omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
- omnibase_infra/models/discovery/model_introspection_config.py +28 -1
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
- omnibase_infra/models/runtime/__init__.py +4 -0
- omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
- omnibase_infra/nodes/node_contract_persistence_effect/node.py +18 -1
- omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +33 -2
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
- omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
- omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
- omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
- omnibase_infra/runtime/__init__.py +7 -0
- omnibase_infra/runtime/baseline_subscriptions.py +13 -6
- omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
- omnibase_infra/runtime/contract_registration_event_router.py +5 -5
- omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
- omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
- omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
- omnibase_infra/runtime/registry_policy.py +29 -15
- omnibase_infra/runtime/request_response_wiring.py +15 -7
- omnibase_infra/runtime/service_runtime_host_process.py +149 -5
- omnibase_infra/runtime/util_version.py +5 -1
- omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
- omnibase_infra/services/contract_publisher/config.py +4 -4
- omnibase_infra/services/contract_publisher/service.py +8 -5
- omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
- omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
- omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
- omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
- omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
- omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
- omnibase_infra/utils/__init__.py +7 -0
- omnibase_infra/utils/util_db_error_context.py +292 -0
- omnibase_infra/validation/validation_exemptions.yaml +11 -0
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +2 -2
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +57 -36
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,7 +9,7 @@ all Kafka plumbing - nodes/handlers never create consumers or producers directly
|
|
|
9
9
|
Architecture:
|
|
10
10
|
The EventBusSubcontractWiring class is responsible for:
|
|
11
11
|
1. Reading `subscribe_topics` from ModelEventBusSubcontract
|
|
12
|
-
2.
|
|
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
|
|
57
|
-
|
|
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
|
|
62
|
-
|
|
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
|
-
-
|
|
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
|
|
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,
|
|
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
|
|
236
|
-
Used
|
|
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
|
|
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
|
|
283
|
-
|
|
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
|
-
|
|
291
|
-
(e.g., '
|
|
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
|
-
'
|
|
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
|
|
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
|
-
|
|
352
|
-
#
|
|
353
|
-
|
|
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
|
-
|
|
403
|
+
node_identity=node_identity,
|
|
364
404
|
on_message=callback,
|
|
365
405
|
)
|
|
366
406
|
self._unsubscribe_callables.append(unsubscribe)
|
|
@@ -9,7 +9,7 @@ event emission and maintaining clean architectural boundaries.
|
|
|
9
9
|
|
|
10
10
|
Design Principles:
|
|
11
11
|
- **Contract-Driven Access Control**: Topics must be declared in contract
|
|
12
|
-
- **
|
|
12
|
+
- **Realm-Agnostic Topics**: Topics passed through unchanged (no env prefix)
|
|
13
13
|
- **Fail-Fast Validation**: Invalid topics raise immediately, not at delivery
|
|
14
14
|
- **Duck-Typed Protocol**: Implements publisher protocol without explicit inheritance
|
|
15
15
|
|
|
@@ -95,7 +95,7 @@ class PublisherTopicScoped:
|
|
|
95
95
|
|
|
96
96
|
Features:
|
|
97
97
|
- Contract-driven topic access control
|
|
98
|
-
-
|
|
98
|
+
- Realm-agnostic topics (no environment prefix)
|
|
99
99
|
- Fail-fast validation on disallowed topics
|
|
100
100
|
- JSON serialization for payloads
|
|
101
101
|
- Correlation ID propagation for distributed tracing
|
|
@@ -103,7 +103,7 @@ class PublisherTopicScoped:
|
|
|
103
103
|
Attributes:
|
|
104
104
|
_event_bus: The underlying event bus for publishing
|
|
105
105
|
_allowed_topics: Set of topic suffixes allowed by contract
|
|
106
|
-
_environment: Environment
|
|
106
|
+
_environment: Environment identifier (retained for future use)
|
|
107
107
|
|
|
108
108
|
Example:
|
|
109
109
|
>>> publisher = PublisherTopicScoped(
|
|
@@ -132,8 +132,8 @@ class PublisherTopicScoped:
|
|
|
132
132
|
Must implement publish(topic, key, value) method. Duck typed per ONEX.
|
|
133
133
|
allowed_topics: Set of topic suffixes from contract's publish_topics.
|
|
134
134
|
These are the ONLY topics this publisher can publish to.
|
|
135
|
-
environment: Environment
|
|
136
|
-
|
|
135
|
+
environment: Environment identifier (e.g., 'dev', 'staging', 'prod').
|
|
136
|
+
Retained for future use; topics are realm-agnostic (no prefix).
|
|
137
137
|
|
|
138
138
|
Example:
|
|
139
139
|
>>> publisher = PublisherTopicScoped(
|
|
@@ -174,22 +174,27 @@ class PublisherTopicScoped:
|
|
|
174
174
|
return str(correlation_id).encode("utf-8")
|
|
175
175
|
|
|
176
176
|
def resolve_topic(self, topic_suffix: str) -> str:
|
|
177
|
-
"""Resolve topic suffix to
|
|
177
|
+
"""Resolve topic suffix to topic name (realm-agnostic, no environment prefix).
|
|
178
178
|
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
Topics are realm-agnostic in ONEX. The environment/realm is enforced via
|
|
180
|
+
envelope identity, not topic naming. This enables cross-environment event
|
|
181
|
+
routing when needed while maintaining proper isolation through identity.
|
|
181
182
|
|
|
182
183
|
Args:
|
|
183
184
|
topic_suffix: ONEX format topic suffix (e.g., 'onex.events.v1')
|
|
184
185
|
|
|
185
186
|
Returns:
|
|
186
|
-
|
|
187
|
+
Topic name (same as suffix, no environment prefix)
|
|
187
188
|
|
|
188
189
|
Example:
|
|
189
190
|
>>> publisher.resolve_topic("onex.events.v1")
|
|
190
|
-
'
|
|
191
|
+
'onex.events.v1'
|
|
192
|
+
|
|
193
|
+
Note:
|
|
194
|
+
The environment is still stored for potential consumer group derivation
|
|
195
|
+
in related components. Topics themselves are realm-agnostic.
|
|
191
196
|
"""
|
|
192
|
-
return
|
|
197
|
+
return topic_suffix
|
|
193
198
|
|
|
194
199
|
async def publish(
|
|
195
200
|
self,
|
|
@@ -86,7 +86,6 @@ from typing import TYPE_CHECKING
|
|
|
86
86
|
|
|
87
87
|
from pydantic import ValidationError
|
|
88
88
|
|
|
89
|
-
from omnibase_core.models.primitives import ModelSemVer
|
|
90
89
|
from omnibase_infra.enums import EnumInfraTransportType, EnumPolicyType
|
|
91
90
|
from omnibase_infra.errors import PolicyRegistryError, ProtocolConfigurationError
|
|
92
91
|
from omnibase_infra.models.errors.model_infra_error_context import (
|
|
@@ -97,6 +96,7 @@ from omnibase_infra.runtime.mixin_semver_cache import MixinSemverCache
|
|
|
97
96
|
from omnibase_infra.runtime.models import ModelPolicyKey, ModelPolicyRegistration
|
|
98
97
|
from omnibase_infra.runtime.util_version import normalize_version
|
|
99
98
|
from omnibase_infra.types import PolicyTypeInput
|
|
99
|
+
from omnibase_infra.utils import validate_policy_type_value
|
|
100
100
|
|
|
101
101
|
if TYPE_CHECKING:
|
|
102
102
|
from omnibase_infra.runtime.protocol_policy import ProtocolPolicy
|
|
@@ -463,11 +463,13 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
|
|
|
463
463
|
and string literals, normalizing them to their string representation while
|
|
464
464
|
ensuring they match valid EnumPolicyType values.
|
|
465
465
|
|
|
466
|
+
Delegates validation to the shared ``validate_policy_type_value()`` utility,
|
|
467
|
+
which is the SINGLE SOURCE OF TRUTH for policy type validation in omnibase_infra.
|
|
468
|
+
|
|
466
469
|
Validation Process:
|
|
467
|
-
1.
|
|
468
|
-
2.
|
|
469
|
-
3.
|
|
470
|
-
4. Return normalized string value
|
|
470
|
+
1. Delegate to validate_policy_type_value() for validation and coercion
|
|
471
|
+
2. Extract .value from the validated EnumPolicyType
|
|
472
|
+
3. Return normalized string value
|
|
471
473
|
|
|
472
474
|
This centralized validation ensures consistent policy type handling across
|
|
473
475
|
all registry operations (register, get, list_keys, is_registered, unregister).
|
|
@@ -498,12 +500,14 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
|
|
|
498
500
|
PolicyRegistryError: Invalid policy_type: 'invalid'.
|
|
499
501
|
Must be one of: ['orchestrator', 'reducer']
|
|
500
502
|
"""
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
503
|
+
try:
|
|
504
|
+
# Use shared validation utility (SINGLE SOURCE OF TRUTH)
|
|
505
|
+
validated_enum = validate_policy_type_value(policy_type)
|
|
506
|
+
return validated_enum.value
|
|
507
|
+
except ValueError as exc:
|
|
508
|
+
# Convert ValueError from shared utility to PolicyRegistryError
|
|
509
|
+
# for consistent error handling in the registry
|
|
510
|
+
valid_types = {pt.value for pt in EnumPolicyType}
|
|
507
511
|
context = ModelInfraErrorContext.with_correlation(
|
|
508
512
|
transport_type=EnumInfraTransportType.RUNTIME,
|
|
509
513
|
operation="normalize_policy_type",
|
|
@@ -512,11 +516,9 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
|
|
|
512
516
|
f"Invalid policy_type: {policy_type!r}. "
|
|
513
517
|
f"Must be one of: {sorted(valid_types)}",
|
|
514
518
|
policy_id=None,
|
|
515
|
-
policy_type=policy_type,
|
|
519
|
+
policy_type=str(policy_type),
|
|
516
520
|
context=context,
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
return policy_type
|
|
521
|
+
) from exc
|
|
520
522
|
|
|
521
523
|
@staticmethod
|
|
522
524
|
def _normalize_version(version: str) -> str:
|
|
@@ -656,6 +658,10 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
|
|
|
656
658
|
Partial version strings (e.g., "1", "1.0") are auto-normalized to
|
|
657
659
|
"x.y.z" format by ModelPolicyRegistration.
|
|
658
660
|
|
|
661
|
+
.. deprecated::
|
|
662
|
+
This method is deprecated. Use ``register(ModelPolicyRegistration(...))``
|
|
663
|
+
directly for new code. This method will be removed in a future version.
|
|
664
|
+
|
|
659
665
|
Note:
|
|
660
666
|
For new code, prefer using register(ModelPolicyRegistration(...))
|
|
661
667
|
directly. This is a convenience method for simple registrations.
|
|
@@ -684,6 +690,14 @@ class RegistryPolicy(MixinPolicyValidation, MixinSemverCache):
|
|
|
684
690
|
... version="1.0.0",
|
|
685
691
|
... )
|
|
686
692
|
"""
|
|
693
|
+
# Emit deprecation warning at runtime
|
|
694
|
+
warnings.warn(
|
|
695
|
+
"register_policy() is deprecated. Use register(ModelPolicyRegistration(...)) "
|
|
696
|
+
"instead. This method will be removed in a future version.",
|
|
697
|
+
DeprecationWarning,
|
|
698
|
+
stacklevel=2,
|
|
699
|
+
)
|
|
700
|
+
|
|
687
701
|
# Version normalization is handled by ModelPolicyRegistration validator
|
|
688
702
|
# which normalizes partial versions and v-prefixed versions automatically
|
|
689
703
|
try:
|
|
@@ -191,7 +191,7 @@ class RequestResponseWiring(MixinAsyncCircuitBreaker):
|
|
|
191
191
|
|
|
192
192
|
Attributes:
|
|
193
193
|
_event_bus: Event bus for publishing requests
|
|
194
|
-
_environment: Environment
|
|
194
|
+
_environment: Environment identifier for consumer groups (e.g., 'dev', 'prod')
|
|
195
195
|
_app_name: Application name for consumer group identification
|
|
196
196
|
_instances: Dict mapping instance names to their state
|
|
197
197
|
_bootstrap_servers: Kafka bootstrap servers from event bus
|
|
@@ -211,8 +211,9 @@ class RequestResponseWiring(MixinAsyncCircuitBreaker):
|
|
|
211
211
|
Args:
|
|
212
212
|
event_bus: Event bus for publishing requests. Must implement
|
|
213
213
|
ProtocolEventBusPublisher interface.
|
|
214
|
-
environment: Environment
|
|
215
|
-
Used
|
|
214
|
+
environment: Environment identifier (e.g., 'dev', 'prod').
|
|
215
|
+
Used for consumer group naming. Topics are realm-agnostic and
|
|
216
|
+
do not include environment prefixes.
|
|
216
217
|
app_name: Application name for logging and consumer group naming.
|
|
217
218
|
bootstrap_servers: Kafka bootstrap servers. If not provided, attempts
|
|
218
219
|
to read from event_bus._bootstrap_servers or environment variable.
|
|
@@ -263,17 +264,24 @@ class RequestResponseWiring(MixinAsyncCircuitBreaker):
|
|
|
263
264
|
)
|
|
264
265
|
|
|
265
266
|
def resolve_topic(self, topic_suffix: str) -> str:
|
|
266
|
-
"""Resolve topic suffix to
|
|
267
|
+
"""Resolve topic suffix to topic name (realm-agnostic, no environment prefix).
|
|
268
|
+
|
|
269
|
+
Topics are realm-agnostic in ONEX. The environment/realm is enforced via
|
|
270
|
+
envelope identity, not topic naming. This enables cross-environment event
|
|
271
|
+
routing when needed while maintaining proper isolation through identity.
|
|
267
272
|
|
|
268
273
|
Args:
|
|
269
274
|
topic_suffix: ONEX format topic suffix
|
|
270
275
|
(e.g., 'onex.cmd.intelligence.analyze-code.v1')
|
|
271
276
|
|
|
272
277
|
Returns:
|
|
273
|
-
|
|
274
|
-
(e.g., '
|
|
278
|
+
Topic name (same as suffix, no environment prefix)
|
|
279
|
+
(e.g., 'onex.cmd.intelligence.analyze-code.v1')
|
|
280
|
+
|
|
281
|
+
Note:
|
|
282
|
+
Consumer groups still include environment for proper isolation.
|
|
275
283
|
"""
|
|
276
|
-
return
|
|
284
|
+
return topic_suffix
|
|
277
285
|
|
|
278
286
|
async def wire_request_response(
|
|
279
287
|
self,
|
|
@@ -73,6 +73,12 @@ from omnibase_infra.errors import (
|
|
|
73
73
|
from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
|
|
74
74
|
from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
|
|
75
75
|
from omnibase_infra.models import ModelNodeIdentity
|
|
76
|
+
from omnibase_infra.models.runtime.model_resolved_dependencies import (
|
|
77
|
+
ModelResolvedDependencies,
|
|
78
|
+
)
|
|
79
|
+
from omnibase_infra.runtime.contract_dependency_resolver import (
|
|
80
|
+
ContractDependencyResolver,
|
|
81
|
+
)
|
|
76
82
|
from omnibase_infra.runtime.envelope_validator import (
|
|
77
83
|
normalize_correlation_id,
|
|
78
84
|
validate_envelope,
|
|
@@ -764,6 +770,10 @@ class RuntimeHostProcess:
|
|
|
764
770
|
# Enables contract config to be passed to handlers via initialize()
|
|
765
771
|
self._handler_descriptors: dict[str, ModelHandlerDescriptor] = {}
|
|
766
772
|
|
|
773
|
+
# Contract dependency resolver for protocol auto-injection (OMN-1903)
|
|
774
|
+
# Lazy-created when first needed during handler population
|
|
775
|
+
self._dependency_resolver: ContractDependencyResolver | None = None
|
|
776
|
+
|
|
767
777
|
# Pending message tracking for graceful shutdown (OMN-756)
|
|
768
778
|
# Tracks count of in-flight messages currently being processed
|
|
769
779
|
self._pending_message_count: int = 0
|
|
@@ -1798,11 +1808,44 @@ class RuntimeHostProcess:
|
|
|
1798
1808
|
handler_type
|
|
1799
1809
|
)
|
|
1800
1810
|
|
|
1801
|
-
#
|
|
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
|
-
|
|
1804
|
-
|
|
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
|
|
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()
|