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
|
@@ -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
|
|
988
|
-
|
|
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
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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 (
|
|
997
|
-
|
|
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
|
|
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
|
|
1007
|
-
(e.g., "{env}" or "{namespace}" remaining in the
|
|
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
|
-
['
|
|
1027
|
+
['onex.evt.node-registered.v1']
|
|
1015
1028
|
|
|
1016
1029
|
See Also:
|
|
1017
|
-
- ModelEventBusSubcontract: Contract model with
|
|
1018
|
-
- ModelNodeEventBusConfig: Registry storage model
|
|
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
|
|
1041
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2108
|
+
node_identity=node_identity,
|
|
2087
2109
|
on_message=self._handle_introspection_request,
|
|
2088
2110
|
)
|
|
2089
2111
|
self._registry_unsubscribe = unsubscribe
|
|
@@ -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
|
-
|
|
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,
|
|
@@ -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
|
|
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: "
|
|
45
|
+
- topic: "onex.evt.platform.contract-registered.v1"
|
|
45
46
|
event_type: "ModelContractRegisteredEvent"
|
|
46
47
|
description: "Contract registration from nodes on startup"
|
|
47
|
-
- topic: "
|
|
48
|
+
- topic: "onex.evt.platform.contract-deregistered.v1"
|
|
48
49
|
event_type: "ModelContractDeregisteredEvent"
|
|
49
50
|
description: "Explicit contract deregistration on graceful shutdown"
|
|
50
|
-
- topic: "
|
|
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: "
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
#
|
|
701
|
-
#
|
|
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)
|