omnibase_infra 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/errors/__init__.py +4 -0
  3. omnibase_infra/errors/error_infra.py +60 -0
  4. omnibase_infra/handlers/__init__.py +3 -0
  5. omnibase_infra/handlers/handler_slack_webhook.py +426 -0
  6. omnibase_infra/handlers/models/__init__.py +14 -0
  7. omnibase_infra/handlers/models/enum_alert_severity.py +36 -0
  8. omnibase_infra/handlers/models/model_slack_alert.py +24 -0
  9. omnibase_infra/handlers/models/model_slack_alert_payload.py +77 -0
  10. omnibase_infra/handlers/models/model_slack_alert_result.py +73 -0
  11. omnibase_infra/mixins/mixin_node_introspection.py +42 -20
  12. omnibase_infra/models/discovery/model_dependency_spec.py +1 -0
  13. omnibase_infra/models/discovery/model_discovered_capabilities.py +1 -1
  14. omnibase_infra/models/discovery/model_introspection_config.py +28 -1
  15. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +1 -0
  16. omnibase_infra/models/discovery/model_introspection_task_config.py +1 -0
  17. omnibase_infra/models/runtime/__init__.py +4 -0
  18. omnibase_infra/models/runtime/model_resolved_dependencies.py +116 -0
  19. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +6 -5
  20. omnibase_infra/nodes/contract_registry_reducer/reducer.py +9 -26
  21. omnibase_infra/nodes/node_contract_persistence_effect/node.py +18 -1
  22. omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py +33 -2
  23. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +8 -12
  24. omnibase_infra/nodes/node_slack_alerter_effect/__init__.py +33 -0
  25. omnibase_infra/nodes/node_slack_alerter_effect/contract.yaml +291 -0
  26. omnibase_infra/nodes/node_slack_alerter_effect/node.py +106 -0
  27. omnibase_infra/runtime/__init__.py +7 -0
  28. omnibase_infra/runtime/baseline_subscriptions.py +13 -6
  29. omnibase_infra/runtime/contract_dependency_resolver.py +455 -0
  30. omnibase_infra/runtime/contract_registration_event_router.py +5 -5
  31. omnibase_infra/runtime/emit_daemon/event_registry.py +34 -22
  32. omnibase_infra/runtime/event_bus_subcontract_wiring.py +63 -23
  33. omnibase_infra/runtime/publisher_topic_scoped.py +16 -11
  34. omnibase_infra/runtime/registry_policy.py +29 -15
  35. omnibase_infra/runtime/request_response_wiring.py +15 -7
  36. omnibase_infra/runtime/service_runtime_host_process.py +149 -5
  37. omnibase_infra/runtime/util_version.py +5 -1
  38. omnibase_infra/schemas/schema_latency_baseline.sql +135 -0
  39. omnibase_infra/services/contract_publisher/config.py +4 -4
  40. omnibase_infra/services/contract_publisher/service.py +8 -5
  41. omnibase_infra/services/observability/injection_effectiveness/__init__.py +67 -0
  42. omnibase_infra/services/observability/injection_effectiveness/config.py +295 -0
  43. omnibase_infra/services/observability/injection_effectiveness/consumer.py +1461 -0
  44. omnibase_infra/services/observability/injection_effectiveness/models/__init__.py +32 -0
  45. omnibase_infra/services/observability/injection_effectiveness/models/model_agent_match.py +79 -0
  46. omnibase_infra/services/observability/injection_effectiveness/models/model_context_utilization.py +118 -0
  47. omnibase_infra/services/observability/injection_effectiveness/models/model_latency_breakdown.py +107 -0
  48. omnibase_infra/services/observability/injection_effectiveness/models/model_pattern_utilization.py +46 -0
  49. omnibase_infra/services/observability/injection_effectiveness/writer_postgres.py +596 -0
  50. omnibase_infra/utils/__init__.py +7 -0
  51. omnibase_infra/utils/util_db_error_context.py +292 -0
  52. omnibase_infra/validation/validation_exemptions.yaml +11 -0
  53. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/METADATA +2 -2
  54. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/RECORD +57 -36
  55. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/WHEEL +0 -0
  56. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/entry_points.txt +0 -0
  57. {omnibase_infra-0.3.2.dist-info → omnibase_infra-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,77 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Slack Alert Input Payload Model.
4
+
5
+ Provides the input payload model for Slack alert operations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from uuid import UUID, uuid4
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+ from omnibase_core.types import JsonType
15
+ from omnibase_infra.handlers.models.enum_alert_severity import EnumAlertSeverity
16
+
17
+
18
+ class ModelSlackAlert(BaseModel):
19
+ """Input payload for Slack alert operations.
20
+
21
+ This model defines the structure of alert payloads sent to
22
+ the HandlerSlackWebhook. The handler transforms this into
23
+ Slack Block Kit formatted messages.
24
+
25
+ Attributes:
26
+ severity: Alert severity level for visual formatting
27
+ message: Main alert message (required)
28
+ title: Optional alert title (defaults to severity-based title)
29
+ details: Optional key-value details to include in the alert
30
+ channel: Optional channel override (webhook default if not specified)
31
+ correlation_id: UUID for distributed tracing
32
+
33
+ Example:
34
+ >>> alert = ModelSlackAlert(
35
+ ... severity=EnumAlertSeverity.WARNING,
36
+ ... message="High memory usage detected",
37
+ ... title="Resource Alert",
38
+ ... details={"node": "registry-effect", "memory_pct": "85"},
39
+ ... )
40
+ """
41
+
42
+ model_config = ConfigDict(
43
+ frozen=True,
44
+ extra="forbid",
45
+ from_attributes=True, # Support pytest-xdist compatibility
46
+ )
47
+
48
+ severity: EnumAlertSeverity = Field(
49
+ default=EnumAlertSeverity.INFO,
50
+ description="Alert severity level for visual formatting",
51
+ )
52
+ message: str = Field(
53
+ ...,
54
+ min_length=1,
55
+ max_length=3000,
56
+ description="Main alert message content",
57
+ )
58
+ title: str | None = Field(
59
+ default=None,
60
+ max_length=150,
61
+ description="Optional alert title (defaults to severity-based title)",
62
+ )
63
+ details: dict[str, JsonType] = Field(
64
+ default_factory=dict,
65
+ description="Additional key-value details to include in the alert",
66
+ )
67
+ channel: str | None = Field(
68
+ default=None,
69
+ description="Optional channel override (uses webhook default if not specified)",
70
+ )
71
+ correlation_id: UUID = Field(
72
+ default_factory=uuid4,
73
+ description="UUID for distributed tracing",
74
+ )
75
+
76
+
77
+ __all__ = ["ModelSlackAlert"]
@@ -0,0 +1,73 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Slack Alert Result Model.
4
+
5
+ Provides the response model for Slack webhook operations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from uuid import UUID
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field
13
+
14
+
15
+ class ModelSlackAlertResult(BaseModel):
16
+ """Response from Slack webhook operations.
17
+
18
+ This model captures the result of a Slack alert send operation,
19
+ including success/failure status, timing, and retry information.
20
+
21
+ Attributes:
22
+ success: Whether the alert was delivered successfully
23
+ duration_ms: Time taken for the operation in milliseconds
24
+ correlation_id: UUID from the original request for tracing
25
+ error: Sanitized error message if success is False
26
+ error_code: Error code for programmatic handling
27
+ retry_count: Number of retry attempts made
28
+
29
+ Example:
30
+ >>> result = ModelSlackAlertResult(
31
+ ... success=True,
32
+ ... duration_ms=123.45,
33
+ ... correlation_id=uuid4(),
34
+ ... )
35
+ >>> print(result.success)
36
+ True
37
+ """
38
+
39
+ model_config = ConfigDict(
40
+ frozen=True,
41
+ extra="forbid",
42
+ from_attributes=True,
43
+ )
44
+
45
+ success: bool = Field(
46
+ ...,
47
+ description="Whether the alert was delivered successfully",
48
+ )
49
+ duration_ms: float = Field(
50
+ default=0.0,
51
+ ge=0.0,
52
+ description="Time taken for the operation in milliseconds",
53
+ )
54
+ correlation_id: UUID = Field(
55
+ ...,
56
+ description="UUID from the original request for tracing",
57
+ )
58
+ error: str | None = Field(
59
+ default=None,
60
+ description="Sanitized error message if success is False",
61
+ )
62
+ error_code: str | None = Field(
63
+ default=None,
64
+ description="Error code for programmatic handling",
65
+ )
66
+ retry_count: int = Field(
67
+ default=0,
68
+ ge=0,
69
+ description="Number of retry attempts made",
70
+ )
71
+
72
+
73
+ __all__ = ["ModelSlackAlertResult"]
@@ -154,6 +154,7 @@ Usage:
154
154
  config = ModelIntrospectionConfig(
155
155
  node_id=node_config.node_id,
156
156
  node_type=EnumNodeKind.EFFECT,
157
+ node_name="my_node",
157
158
  event_bus=event_bus,
158
159
  )
159
160
  self.initialize_introspection(config)
@@ -214,6 +215,7 @@ from omnibase_infra.capabilities import ContractCapabilityExtractor
214
215
  from omnibase_infra.constants_topic_patterns import TOPIC_NAME_PATTERN
215
216
  from omnibase_infra.enums import EnumInfraTransportType, EnumIntrospectionReason
216
217
  from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
218
+ from omnibase_infra.models import ModelNodeIdentity
217
219
  from omnibase_infra.models.discovery import (
218
220
  ModelDiscoveredCapabilities,
219
221
  ModelIntrospectionConfig,
@@ -422,6 +424,7 @@ class MixinNodeIntrospection:
422
424
  config = ModelIntrospectionConfig(
423
425
  node_id=node_id,
424
426
  node_type=EnumNodeKind.EFFECT,
427
+ node_name="postgres_adapter",
425
428
  event_bus=adapter_config.event_bus,
426
429
  )
427
430
  self.initialize_introspection(config)
@@ -463,6 +466,9 @@ class MixinNodeIntrospection:
463
466
  _introspection_node_type: EnumNodeKind | None
464
467
  _introspection_event_bus: ProtocolEventBus | None
465
468
  _introspection_version: str
469
+ _introspection_node_name: str
470
+ _introspection_env: str
471
+ _introspection_service: str
466
472
  _introspection_start_time: float | None
467
473
  _introspection_contract: ModelContractBase | None
468
474
 
@@ -590,6 +596,7 @@ class MixinNodeIntrospection:
590
596
  config = ModelIntrospectionConfig(
591
597
  node_id=node_config.node_id,
592
598
  node_type=EnumNodeKind.EFFECT,
599
+ node_name="my_node",
593
600
  event_bus=node_config.event_bus,
594
601
  version="1.2.0",
595
602
  )
@@ -601,6 +608,7 @@ class MixinNodeIntrospection:
601
608
  config = ModelIntrospectionConfig(
602
609
  node_id=node_config.node_id,
603
610
  node_type=EnumNodeKind.EFFECT,
611
+ node_name="my_effect_node",
604
612
  event_bus=node_config.event_bus,
605
613
  operation_keywords=frozenset({"fetch", "upload", "download"}),
606
614
  )
@@ -638,6 +646,9 @@ class MixinNodeIntrospection:
638
646
  )
639
647
  self._introspection_event_bus = config.event_bus
640
648
  self._introspection_version = config.version
649
+ self._introspection_node_name = config.node_name
650
+ self._introspection_env = config.env
651
+ self._introspection_service = config.service
641
652
  self._introspection_cache_ttl = config.cache_ttl
642
653
 
643
654
  # Capability discovery configuration - frozensets are immutable, no copy needed
@@ -984,38 +995,40 @@ class MixinNodeIntrospection:
984
995
  ) -> ModelNodeEventBusConfig | None:
985
996
  """Extract and resolve event_bus config from contract.
986
997
 
987
- Extracts topic suffixes from the contract's event_bus subcontract and
988
- resolves them to full environment-qualified topic strings.
998
+ Extracts topic names from the contract's event_bus subcontract.
999
+ Topics are realm-agnostic in ONEX - environment/realm is enforced via
1000
+ envelope identity, not topic naming.
989
1001
 
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").
1002
+ Topic Format:
1003
+ Topics follow ONEX naming convention:
1004
+ onex.{kind}.{producer}.{event-name}.v{n}
1005
+
1006
+ Example: "onex.evt.intent-classified.v1"
994
1007
 
995
1008
  Args:
996
- env_prefix: Environment prefix (e.g., "dev", "prod", "staging").
997
- Must be a valid identifier without dots or special characters.
1009
+ env_prefix: Environment prefix (retained for compatibility, but no longer
1010
+ used for topic resolution as topics are realm-agnostic).
998
1011
 
999
1012
  Returns:
1000
- Resolved event bus config with full topic strings, or None if:
1013
+ Resolved event bus config with topic strings, or None if:
1001
1014
  - No contract is configured (_introspection_contract is None)
1002
1015
  - Contract has no event_bus subcontract
1003
1016
  - event_bus subcontract has no publish_topics or subscribe_topics
1004
1017
 
1005
1018
  Raises:
1006
- ValueError: If topic resolution fails due to unresolved placeholders
1007
- (e.g., "{env}" or "{namespace}" remaining in the resolved topic).
1019
+ ValueError: If topic validation fails due to unresolved placeholders
1020
+ (e.g., "{env}" or "{namespace}" remaining in the topic).
1008
1021
  This is a fail-fast mechanism to prevent misconfigured topics
1009
1022
  from being published to the registry.
1010
1023
 
1011
1024
  Example:
1012
1025
  >>> config = self._extract_event_bus_config("dev")
1013
1026
  >>> config.publish_topic_strings
1014
- ['dev.onex.evt.node-registered.v1']
1027
+ ['onex.evt.node-registered.v1']
1015
1028
 
1016
1029
  See Also:
1017
- - ModelEventBusSubcontract: Contract model with topic suffixes
1018
- - ModelNodeEventBusConfig: Registry storage model with full topics
1030
+ - ModelEventBusSubcontract: Contract model with topics
1031
+ - ModelNodeEventBusConfig: Registry storage model
1019
1032
  """
1020
1033
  if self._introspection_contract is None:
1021
1034
  return None
@@ -1037,8 +1050,12 @@ class MixinNodeIntrospection:
1037
1050
  return None
1038
1051
 
1039
1052
  def resolve_topic(suffix: str) -> str:
1040
- """Resolve topic suffix to full topic with env prefix."""
1041
- # Full topic format: {env}.{suffix}
1053
+ """Resolve topic suffix to topic name (realm-agnostic).
1054
+
1055
+ Topics are realm-agnostic in ONEX. The environment/realm is enforced via
1056
+ envelope identity, not topic naming. This enables cross-environment event
1057
+ routing when needed while maintaining proper isolation through identity.
1058
+ """
1042
1059
  # Strip whitespace from suffix to handle YAML formatting artifacts
1043
1060
  suffix = suffix.strip()
1044
1061
 
@@ -1065,9 +1082,8 @@ class MixinNodeIntrospection:
1065
1082
  parameter="topic_suffix",
1066
1083
  )
1067
1084
 
1068
- full_topic = f"{env_prefix}.{suffix}"
1069
-
1070
- return full_topic
1085
+ # Return topic unchanged - topics are realm-agnostic
1086
+ return suffix
1071
1087
 
1072
1088
  def build_entry(suffix: str) -> ModelEventBusTopicEntry:
1073
1089
  """Build topic entry from suffix."""
@@ -2081,9 +2097,15 @@ class MixinNodeIntrospection:
2081
2097
  return False
2082
2098
 
2083
2099
  request_topic = self._request_introspection_topic
2100
+ node_identity = ModelNodeIdentity(
2101
+ env=self._introspection_env,
2102
+ service=self._introspection_service,
2103
+ node_name=self._introspection_node_name,
2104
+ version=self._introspection_version,
2105
+ )
2084
2106
  unsubscribe = await event_bus.subscribe(
2085
2107
  topic=request_topic,
2086
- group_id=f"introspection-{self._introspection_node_id}",
2108
+ node_identity=node_identity,
2087
2109
  on_message=self._handle_introspection_request,
2088
2110
  )
2089
2111
  self._registry_unsubscribe = unsubscribe
@@ -89,6 +89,7 @@ class ModelDependencySpec(BaseModel):
89
89
  model_config = ConfigDict(
90
90
  frozen=True,
91
91
  extra="forbid",
92
+ from_attributes=True, # ORM/pytest-xdist compatibility
92
93
  )
93
94
 
94
95
  # Identity
@@ -27,7 +27,7 @@ class ModelDiscoveredCapabilities(BaseModel):
27
27
  ... )
28
28
  """
29
29
 
30
- model_config = ConfigDict(extra="forbid", frozen=True)
30
+ model_config = ConfigDict(extra="forbid", frozen=True, from_attributes=True)
31
31
 
32
32
  operations: tuple[str, ...] = Field(
33
33
  default=(),
@@ -108,6 +108,7 @@ class ModelIntrospectionConfig(BaseModel):
108
108
  config = ModelIntrospectionConfig(
109
109
  node_id=node_id,
110
110
  node_type=EnumNodeKind.EFFECT, # Use enum directly (preferred)
111
+ node_name="my_effect_node",
111
112
  event_bus=event_bus,
112
113
  version="1.2.0",
113
114
  )
@@ -119,6 +120,7 @@ class ModelIntrospectionConfig(BaseModel):
119
120
  config = ModelIntrospectionConfig(
120
121
  node_id=node_id or uuid4(),
121
122
  node_type=EnumNodeKind.EFFECT, # Use enum directly (preferred)
123
+ node_name="my_custom_effect_node",
122
124
  event_bus=event_bus,
123
125
  operation_keywords=frozenset({"fetch", "upload", "download"}),
124
126
  )
@@ -141,6 +143,27 @@ class ModelIntrospectionConfig(BaseModel):
141
143
  "Accepts EnumNodeKind directly (preferred) or string (deprecated, will be coerced).",
142
144
  )
143
145
 
146
+ node_name: str = Field( # pattern-ok: canonical identifier, not a foreign key reference
147
+ ...,
148
+ min_length=1,
149
+ description="Node name for consumer group identification (e.g., 'claude_hook_effect'). "
150
+ "Cannot be empty.",
151
+ )
152
+
153
+ env: str = Field(
154
+ default="dev",
155
+ min_length=1,
156
+ description="Environment identifier (e.g., 'dev', 'staging', 'prod'). "
157
+ "Used for node identity in event bus subscriptions. Cannot be empty.",
158
+ )
159
+
160
+ service: str = Field(
161
+ default="onex",
162
+ min_length=1,
163
+ description="Service name (e.g., 'omniintelligence', 'omnibridge'). "
164
+ "Used for node identity in event bus subscriptions. Cannot be empty.",
165
+ )
166
+
144
167
  # Event bus for publishing introspection events.
145
168
  # Uses _EventBusType which provides:
146
169
  # - ProtocolEventBus | None during static analysis (TYPE_CHECKING)
@@ -155,7 +178,8 @@ class ModelIntrospectionConfig(BaseModel):
155
178
 
156
179
  version: str = Field(
157
180
  default="1.0.0",
158
- description="Node version string",
181
+ min_length=1,
182
+ description="Node version string. Cannot be empty.",
159
183
  )
160
184
 
161
185
  cache_ttl: float = Field(
@@ -284,6 +308,7 @@ class ModelIntrospectionConfig(BaseModel):
284
308
  model_config = ConfigDict(
285
309
  frozen=True,
286
310
  extra="forbid",
311
+ from_attributes=True, # ORM/pytest-xdist compatibility
287
312
  arbitrary_types_allowed=True, # Allow arbitrary types for event_bus
288
313
  json_schema_extra={
289
314
  "examples": [
@@ -291,6 +316,7 @@ class ModelIntrospectionConfig(BaseModel):
291
316
  {
292
317
  "node_id": "550e8400-e29b-41d4-a716-446655440000",
293
318
  "node_type": "EFFECT",
319
+ "node_name": "example_effect_node",
294
320
  "event_bus": None,
295
321
  "version": "1.0.0",
296
322
  "cache_ttl": 300.0,
@@ -305,6 +331,7 @@ class ModelIntrospectionConfig(BaseModel):
305
331
  {
306
332
  "node_id": "550e8400-e29b-41d4-a716-446655440001",
307
333
  "node_type": "COMPUTE",
334
+ "node_name": "example_compute_node",
308
335
  "event_bus": None,
309
336
  "version": "2.1.0",
310
337
  "cache_ttl": 120.0,
@@ -131,6 +131,7 @@ class ModelIntrospectionPerformanceMetrics(BaseModel):
131
131
  model_config = ConfigDict(
132
132
  frozen=True,
133
133
  extra="forbid",
134
+ from_attributes=True, # ORM/pytest-xdist compatibility
134
135
  json_schema_extra={
135
136
  "examples": [
136
137
  {
@@ -96,6 +96,7 @@ class ModelIntrospectionTaskConfig(BaseModel):
96
96
  model_config = ConfigDict(
97
97
  frozen=True,
98
98
  extra="forbid",
99
+ from_attributes=True, # ORM/pytest-xdist compatibility
99
100
  json_schema_extra={
100
101
  "examples": [
101
102
  {
@@ -26,6 +26,9 @@ from omnibase_infra.models.runtime.model_plugin_load_context import (
26
26
  from omnibase_infra.models.runtime.model_plugin_load_summary import (
27
27
  ModelPluginLoadSummary,
28
28
  )
29
+ from omnibase_infra.models.runtime.model_resolved_dependencies import (
30
+ ModelResolvedDependencies,
31
+ )
29
32
 
30
33
  # ModelContractLoadResult and ModelRuntimeContractConfig are exported from
31
34
  # omnibase_infra.runtime.models (canonical location for runtime loader models)
@@ -45,5 +48,6 @@ __all__ = [
45
48
  "ModelLoadedHandler",
46
49
  "ModelPluginLoadContext",
47
50
  "ModelPluginLoadSummary",
51
+ "ModelResolvedDependencies",
48
52
  "ModelRuntimeContractConfig",
49
53
  ]
@@ -0,0 +1,116 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Model for resolved protocol dependencies.
4
+
5
+ This module provides ModelResolvedDependencies, a container for protocol
6
+ instances resolved from the container service_registry at node creation time.
7
+
8
+ Part of OMN-1732: Runtime dependency injection for zero-code nodes.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ from pydantic import BaseModel, ConfigDict, Field
16
+
17
+
18
+ class ModelResolvedDependencies(BaseModel):
19
+ """Container for resolved protocol dependencies.
20
+
21
+ Holds protocol instances resolved from ModelONEXContainer.service_registry
22
+ for injection into node constructors. This model is immutable after creation.
23
+
24
+ The protocols dict maps protocol class names to their resolved instances:
25
+ - Key: Protocol class name (e.g., "ProtocolPostgresAdapter")
26
+ - Value: Resolved instance from container
27
+
28
+ Example:
29
+ >>> resolved = ModelResolvedDependencies(
30
+ ... protocols={
31
+ ... "ProtocolPostgresAdapter": postgres_adapter,
32
+ ... "ProtocolCircuitBreakerAware": circuit_breaker,
33
+ ... }
34
+ ... )
35
+ >>> adapter = resolved.get("ProtocolPostgresAdapter")
36
+
37
+ .. versionadded:: 0.x.x
38
+ Part of OMN-1732 runtime dependency injection.
39
+ """
40
+
41
+ model_config = ConfigDict(
42
+ frozen=True,
43
+ extra="forbid",
44
+ arbitrary_types_allowed=True, # Required for protocol instances
45
+ )
46
+
47
+ # ONEX_EXCLUDE: any_type - dict[str, Any] required for heterogeneous protocol instances
48
+ # resolved from container.service_registry. Type varies by protocol (ProtocolPostgresAdapter,
49
+ # ProtocolCircuitBreakerAware, etc.). Cannot use Union as protocols are open-ended.
50
+ protocols: dict[str, Any] = Field(
51
+ default_factory=dict,
52
+ description="Map of protocol class names to resolved instances",
53
+ )
54
+
55
+ # ONEX_EXCLUDE: any_type - returns heterogeneous protocol instance from protocols dict
56
+ def get(self, protocol_name: str) -> Any:
57
+ """Get a resolved protocol by name.
58
+
59
+ Args:
60
+ protocol_name: The protocol class name (e.g., "ProtocolPostgresAdapter")
61
+
62
+ Returns:
63
+ The resolved protocol instance.
64
+
65
+ Raises:
66
+ KeyError: If protocol_name is not in the resolved protocols.
67
+
68
+ Example:
69
+ >>> adapter = resolved.get("ProtocolPostgresAdapter")
70
+ """
71
+ if protocol_name not in self.protocols:
72
+ raise KeyError(
73
+ f"Protocol '{protocol_name}' not found in resolved dependencies. "
74
+ f"Available: {list(self.protocols.keys())}"
75
+ )
76
+ return self.protocols[protocol_name]
77
+
78
+ # ONEX_EXCLUDE: any_type - returns heterogeneous protocol instance, default can be any type
79
+ def get_optional(self, protocol_name: str, default: Any = None) -> Any:
80
+ """Get a resolved protocol by name, returning default if not found.
81
+
82
+ Args:
83
+ protocol_name: The protocol class name
84
+ default: Value to return if protocol not found
85
+
86
+ Returns:
87
+ The resolved protocol instance or default.
88
+ """
89
+ return self.protocols.get(protocol_name, default)
90
+
91
+ def has(self, protocol_name: str) -> bool:
92
+ """Check if a protocol is available.
93
+
94
+ Args:
95
+ protocol_name: The protocol class name
96
+
97
+ Returns:
98
+ True if protocol is resolved, False otherwise.
99
+ """
100
+ return protocol_name in self.protocols
101
+
102
+ def __len__(self) -> int:
103
+ """Return number of resolved protocols."""
104
+ return len(self.protocols)
105
+
106
+ def __bool__(self) -> bool:
107
+ """Return True if any protocols are resolved.
108
+
109
+ Warning:
110
+ **Non-standard __bool__ behavior**: Returns True only when
111
+ at least one protocol is resolved.
112
+ """
113
+ return len(self.protocols) > 0
114
+
115
+
116
+ __all__ = ["ModelResolvedDependencies"]
@@ -38,19 +38,20 @@ description: |
38
38
  are separated.
39
39
  # Event Consumption Configuration
40
40
  # ================================
41
- # Topics follow 5-segment naming: {env}.onex.{category}.{domain}.{event-name}.v{version}
41
+ # Topics are realm-agnostic: onex.{category}.{domain}.{event-name}.v{version}
42
42
  # Categories: evt (external events), int (internal events), cmd (commands)
43
+ # Note: Environment/realm is enforced via envelope identity, not topic naming.
43
44
  consumed_events:
44
- - topic: "{env}.onex.evt.platform.contract-registered.v1"
45
+ - topic: "onex.evt.platform.contract-registered.v1"
45
46
  event_type: "ModelContractRegisteredEvent"
46
47
  description: "Contract registration from nodes on startup"
47
- - topic: "{env}.onex.evt.platform.contract-deregistered.v1"
48
+ - topic: "onex.evt.platform.contract-deregistered.v1"
48
49
  event_type: "ModelContractDeregisteredEvent"
49
50
  description: "Explicit contract deregistration on graceful shutdown"
50
- - topic: "{env}.onex.evt.platform.node-heartbeat.v1"
51
+ - topic: "onex.evt.platform.node-heartbeat.v1"
51
52
  event_type: "ModelNodeHeartbeatEvent"
52
53
  description: "Heartbeat for liveness tracking and last_seen_at updates"
53
- - topic: "{env}.onex.int.platform.runtime-tick.v1"
54
+ - topic: "onex.int.platform.runtime-tick.v1"
54
55
  event_type: "ModelRuntimeTick"
55
56
  internal: true
56
57
  description: "Periodic tick for staleness computation - marks contracts as stale/inactive"
@@ -632,30 +632,14 @@ class ContractRegistryReducer:
632
632
  Parses the contract_yaml for consumed_events and published_events,
633
633
  then creates postgres.update_topic intents for each topic suffix.
634
634
 
635
- Environment Placeholder Handling:
636
- Topic suffixes from contract_yaml may contain ``{env}.`` placeholders
637
- (e.g., ``{env}.onex.evt.platform.contract-registered.v1``). This reducer
638
- stores these values **as-is** without stripping the placeholder.
635
+ Realm-Agnostic Topics:
636
+ Topics in ONEX are realm-agnostic. The environment/realm is enforced via
637
+ envelope identity, not topic naming. Contract topics are stored without
638
+ any environment prefix (e.g., ``onex.evt.platform.contract-registered.v1``).
639
639
 
640
- This is intentional for several reasons:
641
-
642
- 1. **Reducer Purity**: The reducer remains environment-agnostic and
643
- deterministic - it doesn't need to know about deployment environments.
644
-
645
- 2. **Effect Layer Responsibility**: The PostgresAdapter (Effect layer)
646
- is responsible for resolving or stripping the ``{env}.`` placeholder
647
- at write time, when the actual environment context is available.
648
-
649
- 3. **Auditing**: Storing the raw contract value preserves the original
650
- contract specification for debugging and auditing purposes.
651
-
652
- 4. **Query Flexibility**: Downstream consumers can query topics with
653
- or without the placeholder depending on their needs.
654
-
655
- The Effect layer should handle ``{env}.`` resolution via one of:
656
- - Stripping the prefix before storage (simple)
657
- - Replacing with actual environment (e.g., ``dev.``, ``prod.``)
658
- - Storing as-is with environment-aware queries
640
+ The Effect layer (PostgresAdapter) may still encounter legacy topics with
641
+ ``{env}.`` placeholders and will strip them during storage normalization.
642
+ See ``normalize_topic_for_storage()`` in handler_postgres_topic_update.py.
659
643
 
660
644
  Args:
661
645
  event: Contract registered event with contract_yaml.
@@ -697,9 +681,8 @@ class ContractRegistryReducer:
697
681
  if isinstance(consumed_events, list):
698
682
  for consumed in consumed_events:
699
683
  if isinstance(consumed, dict):
700
- # NOTE: topic_suffix may contain {env}. placeholder (e.g.,
701
- # "{env}.onex.evt.platform.contract-registered.v1").
702
- # We store it as-is; the Effect layer handles resolution.
684
+ # Topics are realm-agnostic (e.g., "onex.evt.platform.contract-registered.v1").
685
+ # Legacy topics with {env}. prefix are normalized by the Effect layer.
703
686
  topic_suffix = consumed.get("topic")
704
687
  if topic_suffix and isinstance(topic_suffix, str):
705
688
  # Validate event_type is string (may be missing or wrong type in malformed YAML)