Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/omnibase_infra/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ModelInfraErrorContext: Configuration model for bundled error context
RuntimeHostError: Base infrastructure error class
ProtocolConfigurationError: Protocol configuration validation errors
ProtocolDependencyResolutionError: Protocol dependency resolution errors
SecretResolutionError: Secret/credential resolution errors
InfraConnectionError: Infrastructure connection errors
InfraTimeoutError: Infrastructure timeout errors
Expand Down Expand Up @@ -115,6 +116,7 @@
InfraTimeoutError,
InfraUnavailableError,
ProtocolConfigurationError,
ProtocolDependencyResolutionError,
RuntimeHostError,
SecretResolutionError,
UnknownHandlerTypeError,
Expand Down Expand Up @@ -162,6 +164,8 @@
"ModelTimeoutErrorContext",
"PolicyRegistryError",
"ProtocolConfigurationError",
# Protocol dependency resolution errors
"ProtocolDependencyResolutionError",
# Repository errors
"RepositoryContractError",
"RepositoryError",
Expand Down
60 changes: 60 additions & 0 deletions src/omnibase_infra/errors/error_infra.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,13 +597,73 @@ def __init__(
)


class ProtocolDependencyResolutionError(RuntimeHostError):
"""Raised when protocol dependencies cannot be resolved from container.

Used when a node's contract.yaml declares protocol dependencies that
are not registered in the container's service_registry. This is a
fail-fast error that prevents node creation with missing dependencies.

Example:
>>> context = ModelInfraErrorContext(
... transport_type=EnumInfraTransportType.RUNTIME,
... operation="resolve_dependencies",
... target_name="NodeContractPersistenceEffect",
... )
>>> raise ProtocolDependencyResolutionError(
... "Missing required protocols for node",
... context=context,
... missing_protocols=["ProtocolPostgresAdapter"],
... node_name="node_contract_persistence_effect",
... )

.. versionadded:: 0.x.x
Part of OMN-1732 runtime dependency injection.
"""

def __init__(
self,
message: str,
context: ModelInfraErrorContext | None = None,
*,
missing_protocols: list[str] | None = None,
node_name: str | None = None,
contract_path: str | None = None,
**extra_context: object,
) -> None:
"""Initialize ProtocolDependencyResolutionError.

Args:
message: Human-readable error message
context: Bundled infrastructure context
missing_protocols: List of protocol class names that could not be resolved
node_name: Name of the node requiring the protocols
contract_path: Path to the contract.yaml file (for debugging)
**extra_context: Additional context information
"""
if missing_protocols is not None:
extra_context["missing_protocols"] = missing_protocols
if node_name is not None:
extra_context["node_name"] = node_name
if contract_path is not None:
extra_context["contract_path"] = contract_path

super().__init__(
message=message,
error_code=EnumCoreErrorCode.DEPENDENCY_ERROR,
context=context,
**extra_context,
)


__all__: list[str] = [
"EnvelopeValidationError",
"InfraAuthenticationError",
"InfraConnectionError",
"InfraTimeoutError",
"InfraUnavailableError",
"ProtocolConfigurationError",
"ProtocolDependencyResolutionError",
"RuntimeHostError",
"SecretResolutionError",
"UnknownHandlerTypeError",
Expand Down
4 changes: 4 additions & 0 deletions src/omnibase_infra/models/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
from omnibase_infra.models.runtime.model_plugin_load_summary import (
ModelPluginLoadSummary,
)
from omnibase_infra.models.runtime.model_resolved_dependencies import (
ModelResolvedDependencies,
)

# ModelContractLoadResult and ModelRuntimeContractConfig are exported from
# omnibase_infra.runtime.models (canonical location for runtime loader models)
Expand All @@ -45,5 +48,6 @@
"ModelLoadedHandler",
"ModelPluginLoadContext",
"ModelPluginLoadSummary",
"ModelResolvedDependencies",
"ModelRuntimeContractConfig",
]
116 changes: 116 additions & 0 deletions src/omnibase_infra/models/runtime/model_resolved_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 OmniNode Team
"""Model for resolved protocol dependencies.

This module provides ModelResolvedDependencies, a container for protocol
instances resolved from the container service_registry at node creation time.

Part of OMN-1732: Runtime dependency injection for zero-code nodes.
"""

from __future__ import annotations

from typing import Any

from pydantic import BaseModel, ConfigDict, Field


class ModelResolvedDependencies(BaseModel):
"""Container for resolved protocol dependencies.

Holds protocol instances resolved from ModelONEXContainer.service_registry
for injection into node constructors. This model is immutable after creation.

The protocols dict maps protocol class names to their resolved instances:
- Key: Protocol class name (e.g., "ProtocolPostgresAdapter")
- Value: Resolved instance from container

Example:
>>> resolved = ModelResolvedDependencies(
... protocols={
... "ProtocolPostgresAdapter": postgres_adapter,
... "ProtocolCircuitBreakerAware": circuit_breaker,
... }
... )
>>> adapter = resolved.get("ProtocolPostgresAdapter")

.. versionadded:: 0.x.x
Part of OMN-1732 runtime dependency injection.
"""

model_config = ConfigDict(
frozen=True,
extra="forbid",
arbitrary_types_allowed=True, # Required for protocol instances
)

# ONEX_EXCLUDE: any_type - dict[str, Any] required for heterogeneous protocol instances
# resolved from container.service_registry. Type varies by protocol (ProtocolPostgresAdapter,
# ProtocolCircuitBreakerAware, etc.). Cannot use Union as protocols are open-ended.
protocols: dict[str, Any] = Field(
default_factory=dict,
description="Map of protocol class names to resolved instances",
)

# ONEX_EXCLUDE: any_type - returns heterogeneous protocol instance from protocols dict
def get(self, protocol_name: str) -> Any:
"""Get a resolved protocol by name.

Args:
protocol_name: The protocol class name (e.g., "ProtocolPostgresAdapter")

Returns:
The resolved protocol instance.

Raises:
KeyError: If protocol_name is not in the resolved protocols.

Example:
>>> adapter = resolved.get("ProtocolPostgresAdapter")
"""
if protocol_name not in self.protocols:
raise KeyError(
f"Protocol '{protocol_name}' not found in resolved dependencies. "
f"Available: {list(self.protocols.keys())}"
)
return self.protocols[protocol_name]

# ONEX_EXCLUDE: any_type - returns heterogeneous protocol instance, default can be any type
def get_optional(self, protocol_name: str, default: Any = None) -> Any:
"""Get a resolved protocol by name, returning default if not found.

Args:
protocol_name: The protocol class name
default: Value to return if protocol not found

Returns:
The resolved protocol instance or default.
"""
return self.protocols.get(protocol_name, default)

def has(self, protocol_name: str) -> bool:
"""Check if a protocol is available.

Args:
protocol_name: The protocol class name

Returns:
True if protocol is resolved, False otherwise.
"""
return protocol_name in self.protocols

def __len__(self) -> int:
"""Return number of resolved protocols."""
return len(self.protocols)

def __bool__(self) -> bool:
"""Return True if any protocols are resolved.

Warning:
**Non-standard __bool__ behavior**: Returns True only when
at least one protocol is resolved.
"""
return len(self.protocols) > 0


__all__ = ["ModelResolvedDependencies"]
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,14 @@

if TYPE_CHECKING:
from omnibase_core.models.container.model_onex_container import ModelONEXContainer
from omnibase_infra.models.runtime.model_resolved_dependencies import (
ModelResolvedDependencies,
)


# ONEX_EXCLUDE: declarative_node - OMN-1732 DEC-003 requires constructor injection
# for protocol dependencies. The _resolved_dependencies instance variable stores
# pre-resolved protocols from ContractDependencyResolver.
class NodeContractPersistenceEffect(NodeEffect):
"""Declarative effect node for contract registry persistence.

Expand All @@ -76,6 +82,9 @@ class NodeContractPersistenceEffect(NodeEffect):

Args:
container: ONEX dependency injection container.
dependencies: Optional pre-resolved protocol dependencies. If provided,
the node will use these instead of resolving from container.
Part of OMN-1732 runtime dependency injection.

Dependency Injection:
Backend adapters (PostgreSQL) are resolved via container.
Expand All @@ -102,13 +111,21 @@ class NodeContractPersistenceEffect(NodeEffect):
```
"""

def __init__(self, container: ModelONEXContainer) -> None:
def __init__(
self,
container: ModelONEXContainer,
dependencies: ModelResolvedDependencies | None = None,
) -> None:
"""Initialize effect node with container dependency injection.

Args:
container: ONEX dependency injection container.
dependencies: Optional pre-resolved protocol dependencies from
ContractDependencyResolver. If provided, the node uses these
instead of resolving from container. Part of OMN-1732.
"""
super().__init__(container)
self._resolved_dependencies = dependencies


__all__ = ["NodeContractPersistenceEffect"]
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@

from __future__ import annotations

import warnings
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from omnibase_core.models.container.model_onex_container import ModelONEXContainer
from omnibase_infra.models.runtime.model_resolved_dependencies import (
ModelResolvedDependencies,
)
from omnibase_infra.nodes.node_contract_persistence_effect.node import (
NodeContractPersistenceEffect,
)
Expand Down Expand Up @@ -62,7 +66,10 @@ class RegistryInfraContractPersistenceEffect:
"""

@staticmethod
def create(container: ModelONEXContainer) -> NodeContractPersistenceEffect:
def create(
container: ModelONEXContainer,
dependencies: ModelResolvedDependencies | None = None,
) -> NodeContractPersistenceEffect:
"""Create a NodeContractPersistenceEffect instance with resolved dependencies.

Factory method that creates a fully configured NodeContractPersistenceEffect
Expand All @@ -73,6 +80,10 @@ def create(container: ModelONEXContainer) -> NodeContractPersistenceEffect:
following protocols registered:
- ProtocolPostgresAdapter: PostgreSQL database operations
- ProtocolCircuitBreakerAware: Backend circuit breaker protection
dependencies: Optional pre-resolved protocol dependencies from
ContractDependencyResolver. If provided, the node uses these
instead of resolving from container. Part of OMN-1732 runtime
dependency injection.

Returns:
Configured NodeContractPersistenceEffect instance ready for operation.
Expand All @@ -84,14 +95,22 @@ def create(container: ModelONEXContainer) -> NodeContractPersistenceEffect:
>>> container = ModelONEXContainer()
>>> container.register(ProtocolPostgresAdapter, postgres_adapter)
>>> effect = RegistryInfraContractPersistenceEffect.create(container)
>>>
>>> # With pre-resolved dependencies (OMN-1732)
>>> resolved = resolver.resolve(contract)
>>> effect = RegistryInfraContractPersistenceEffect.create(
... container, dependencies=resolved
... )
Comment on lines +98 to +103

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine lines 98-103
cat -n src/omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py | sed -n '90,110p'

Repository: OmniNode-ai/omnibase_infra

Length of output: 1106


🏁 Script executed:

# Search for ContractDependencyResolver definition to check the resolve method
rg -n "class ContractDependencyResolver" --type py -A 20

Repository: OmniNode-ai/omnibase_infra

Length of output: 2299


🏁 Script executed:

# Also search for any async resolve methods
rg -n "async def resolve" --type py | head -20

Repository: OmniNode-ai/omnibase_infra

Length of output: 756


🏁 Script executed:

# Check the create method signature and full docstring context
cat -n src/omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py | sed -n '80,110p'

Repository: OmniNode-ai/omnibase_infra

Length of output: 1749


🏁 Script executed:

# Also check the class definition and method signatures
ast-grep --pattern 'def create($_) {
  $$$
}'

Repository: OmniNode-ai/omnibase_infra

Length of output: 52


🏁 Script executed:

# Look for async create method
rg -n "def create|async def create" src/omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py -A 5

Repository: OmniNode-ai/omnibase_infra

Length of output: 333


Async example should await resolver.

ContractDependencyResolver.resolve() is async, so the example at line 100 must use await to correctly resolve the dependencies. Without it, the code would assign a coroutine object instead of the resolved dependencies.

✏️ Suggested doc fix
-            >>> resolved = resolver.resolve(contract)
+            >>> resolved = await resolver.resolve(contract)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
>>>
>>> # With pre-resolved dependencies (OMN-1732)
>>> resolved = resolver.resolve(contract)
>>> effect = RegistryInfraContractPersistenceEffect.create(
... container, dependencies=resolved
... )
>>>
>>> # With pre-resolved dependencies (OMN-1732)
>>> resolved = await resolver.resolve(contract)
>>> effect = RegistryInfraContractPersistenceEffect.create(
... container, dependencies=resolved
... )
🤖 Prompt for AI Agents
In
`@src/omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py`
around lines 98 - 103, The example uses resolver.resolve(contract) but
ContractDependencyResolver.resolve() is async; change the example to await the
call so resolved holds the actual dependencies (i.e., use resolved = await
resolver.resolve(contract)) before passing them into
RegistryInfraContractPersistenceEffect.create(..., dependencies=resolved) to
avoid assigning a coroutine.


.. versionadded:: 0.5.0
.. versionchanged:: 0.6.0
Added optional ``dependencies`` parameter for constructor injection (OMN-1732).
"""
from omnibase_infra.nodes.node_contract_persistence_effect.node import (
NodeContractPersistenceEffect,
)

return NodeContractPersistenceEffect(container)
return NodeContractPersistenceEffect(container, dependencies=dependencies)

@staticmethod
def get_required_protocols() -> list[str]:
Expand All @@ -100,6 +119,11 @@ def get_required_protocols() -> list[str]:
Returns the protocol class names that must be registered in the
container before creating a NodeContractPersistenceEffect instance.

.. deprecated:: 0.6.0
Use contract.yaml dependencies field instead. This method will be
removed in a future version. The contract is now the single source
of truth for protocol requirements (OMN-1732).

Returns:
List of protocol class names required for node operation.

Expand All @@ -111,6 +135,13 @@ def get_required_protocols() -> list[str]:

.. versionadded:: 0.5.0
"""
warnings.warn(
"get_required_protocols() is deprecated. Use contract.yaml dependencies "
"field instead. The contract is the single source of truth for protocol "
"requirements (OMN-1732).",
DeprecationWarning,
stacklevel=2,
)
return [
"ProtocolPostgresAdapter",
"ProtocolCircuitBreakerAware",
Expand Down
7 changes: 7 additions & 0 deletions src/omnibase_infra/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@
get_baseline_topics,
)

# Contract dependency resolver (OMN-1732)
from omnibase_infra.runtime.contract_dependency_resolver import (
ContractDependencyResolver,
)

# Chain-aware dispatch (OMN-951) - must be imported LAST to avoid circular import
from omnibase_infra.runtime.chain_aware_dispatch import (
ChainAwareDispatcher,
Expand Down Expand Up @@ -458,4 +463,6 @@
"BASELINE_CONTRACT_TOPICS",
"BASELINE_PLATFORM_TOPICS",
"get_baseline_topics",
# Contract dependency resolver (OMN-1732)
"ContractDependencyResolver",
]
Loading