Skip to content

feat(OMN-1732): Runtime dependency injection for zero-code nodes - #238

Merged
jonahgabriel merged 2 commits into
mainfrom
jonah/omn-1732-feat-runtime-dependency-injection-for-zero-code-nodes
Feb 4, 2026
Merged

jonahgabriel merged 2 commits into
mainfrom
jonah/omn-1732-feat-runtime-dependency-injection-for-zero-code-nodes

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Feb 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Implement ContractDependencyResolver that reads protocol dependencies from node contract.yaml files and resolves them from container.service_registry.

Architecture (DEC-003):

  • Container: service provider (owns protocol instances)
  • Runtime (resolver): wiring authority (resolves + validates)
  • Node: consumer (receives resolved deps via constructor)

Changes

Components Added

  • ContractDependencyResolver: Extracts deps from contracts, resolves from container
  • ModelResolvedDependencies: Pydantic model for resolved protocol instances
  • ProtocolDependencyResolutionError: Fail-fast error for missing protocols

Node Changes

  • NodeContractPersistenceEffect accepts optional dependencies parameter
  • RegistryInfra*.get_required_protocols() deprecated (contract is source of truth)

Files Changed

File Change
runtime/contract_dependency_resolver.py New - Core resolver implementation
models/runtime/model_resolved_dependencies.py New - Resolved dependencies model
errors/error_infra.py Added ProtocolDependencyResolutionError
nodes/node_contract_persistence_effect/node.py Constructor injection support
nodes/node_contract_persistence_effect/registry/... Deprecated get_required_protocols()

Test Plan

  • 31 unit tests for ContractDependencyResolver
  • Tests cover: empty deps, successful resolution, fail-fast, allow_missing, None registry
  • Tests for ModelResolvedDependencies model methods
  • All pre-commit hooks pass
  • mypy type checking passes

Linear Ticket

OMN-1732

Summary by CodeRabbit

  • New Features

    • Added dependency resolver that resolves protocol-type dependencies and a new immutable resolved-dependencies model; nodes can accept pre-resolved dependencies.
    • New error type for dependency resolution failures, providing richer context when resolution fails.
  • Deprecations

    • Manual protocol-discovery API marked deprecated; deprecation warning emitted, prefer contract-based declarations.
  • Tests

    • Comprehensive tests for resolution logic and the resolved-dependencies model.
  • Bug Fixes

    • Updated emitted topic for contract-registered events.

@coderabbitai

coderabbitai Bot commented Feb 4, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds protocol dependency resolution: new ProtocolDependencyResolutionError, an immutable ModelResolvedDependencies container, a ContractDependencyResolver that imports and resolves protocol instances from a container and raises on missing required dependencies, wiring to accept pre-resolved dependencies in node persistence effects, and tests for resolver and model.

Changes

Cohort / File(s) Summary
Error Infrastructure
src/omnibase_infra/errors/__init__.py, src/omnibase_infra/errors/error_infra.py
Introduce ProtocolDependencyResolutionError (subclass of RuntimeHostError), add to module exports and re-export from package __init__.
Dependency Models
src/omnibase_infra/models/runtime/model_resolved_dependencies.py, src/omnibase_infra/models/runtime/__init__.py
Add immutable ModelResolvedDependencies Pydantic model (protocol name → instance map) with accessors (get, get_optional, has, __len__, __bool__) and export it publicly.
Contract Dependency Resolution
src/omnibase_infra/runtime/contract_dependency_resolver.py, src/omnibase_infra/runtime/__init__.py
Add ContractDependencyResolver to extract protocol dependencies from a contract, import protocol classes, resolve instances from container.service_registry, return ModelResolvedDependencies, and raise ProtocolDependencyResolutionError when required protocols are missing.
Node Integration
src/omnibase_infra/nodes/node_contract_persistence_effect/node.py, src/omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py
Allow injecting pre-resolved `dependencies: ModelResolvedDependencies
Import Path Update
src/omnibase_infra/services/contract_publisher/service.py
Adjusted import source for TOPIC_SUFFIX_CONTRACT_REGISTERED to omnibase_infra.runtime.contract_registration_event_router.
Tests
tests/unit/runtime/test_contract_dependency_resolver.py, tests/unit/services/contract_publisher/test_service.py
Add comprehensive unit tests for resolver and model (many resolution, import, and error scenarios) and update expected topic strings in publisher tests.

Sequence Diagram

sequenceDiagram
    participant Node as NodeContractPersistenceEffect
    participant Resolver as ContractDependencyResolver
    participant Container as ModelONEXContainer
    participant Registry as ServiceRegistry
    participant Model as ModelResolvedDependencies

    Node->>Resolver: resolve(contract, allow_missing=false)
    activate Resolver
    Resolver->>Resolver: _extract_protocol_dependencies(contract)
    loop each protocol dependency
        Resolver->>Resolver: _import_protocol_class(class_name, module)
        Resolver->>Container: access service_registry
        Container->>Registry: lookup(protocol_class)
        Registry-->>Resolver: instance or None
        alt resolved
            Resolver->>Resolver: add to protocols map
        else missing
            Resolver->>Resolver: record missing_protocols
        end
    end
    alt missing_protocols empty or allow_missing=true
        Resolver->>Model: construct ModelResolvedDependencies(protocols)
        Model-->>Resolver: ModelResolvedDependencies
        Resolver-->>Node: return ModelResolvedDependencies
    else required missing and allow_missing=false
        Resolver-->>Node: raise ProtocolDependencyResolutionError(with missing_protocols context)
    end
    deactivate Resolver
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through contracts, chased each name,
Sniffed out protocols, and brought them tame.
Bundled the instances, tidy and neat,
If one went missing, I sounded the beat.
Hooray — resolved, now the node's complete!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately and concisely summarizes the main objective: implementing runtime dependency injection for zero-code nodes via OMN-1732.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jonah/omn-1732-feat-runtime-dependency-injection-for-zero-code-nodes

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@src/omnibase_infra/nodes/node_contract_persistence_effect/registry/registry_infra_contract_persistence_effect.py`:
- Around line 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.
🧹 Nitpick comments (4)
tests/unit/runtime/test_contract_dependency_resolver.py (4)

1-22: Missing @pytest.mark.unit markers on test classes.

Per coding guidelines, test files should use pytest markers for classification. This unit test file uses @pytest.mark.asyncio on async tests but lacks the required @pytest.mark.unit marker to classify tests appropriately.

♻️ Proposed fix to add unit marker

Add the marker to each test class or apply it at module level:

 import pytest
 
+pytestmark = pytest.mark.unit
+
 from omnibase_infra.errors import ProtocolDependencyResolutionError

As per coding guidelines: tests/**/*.py: Use pytest markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.slow, @pytest.mark.chaos, @pytest.mark.serial for test classification.


114-157: Unnecessary @pytest.mark.asyncio on synchronous tests.

Tests test_resolve_extracts_protocol_dependencies_is_protocol_method, test_resolve_extracts_protocol_dependencies_type_field, and test_resolve_extracts_protocol_dependencies_enum_type call the synchronous method _extract_protocol_dependencies but are marked as async. While this works, it adds unnecessary overhead.

♻️ Example fix for one test
-    `@pytest.mark.asyncio`
-    async def test_resolve_extracts_protocol_dependencies_is_protocol_method(
+    def test_resolve_extracts_protocol_dependencies_is_protocol_method(
         self,
         mock_container: MagicMock,
         mock_contract_with_protocols: MagicMock,
     ) -> None:

325-397: Unnecessary @pytest.mark.asyncio on synchronous helper method tests.

Tests for _import_protocol_class, _is_protocol_dependency, and _extract_protocol_dependencies are all testing synchronous methods but are marked with @pytest.mark.asyncio. Consider removing the async markers and async keyword for clarity.


533-538: Consider using a more specific exception type for immutability test.

The test catches a broad Exception when testing frozen model immutability. Pydantic raises ValidationError when attempting to mutate a frozen model.

♻️ Proposed improvement
     def test_model_is_immutable(self) -> None:
         """Test that the model is frozen (immutable)."""
+        from pydantic import ValidationError
+
         resolved = ModelResolvedDependencies(protocols={"Test": MagicMock()})
 
-        with pytest.raises(Exception):  # ValidationError or AttributeError
+        with pytest.raises(ValidationError):
             resolved.protocols = {}  # type: ignore[misc]

Comment on lines +98 to +103
>>>
>>> # With pre-resolved dependencies (OMN-1732)
>>> resolved = resolver.resolve(contract)
>>> effect = RegistryInfraContractPersistenceEffect.create(
... container, dependencies=resolved
... )

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.

Implement ContractDependencyResolver that reads protocol dependencies from
node contract.yaml files and resolves them from container.service_registry.

Architecture (DEC-003):
- Container: service provider (owns protocol instances)
- Runtime (resolver): wiring authority (resolves + validates)
- Node: consumer (receives resolved deps via constructor)

Components added:
- ContractDependencyResolver: Extracts deps from contracts, resolves from container
- ModelResolvedDependencies: Pydantic model for resolved protocol instances
- ProtocolDependencyResolutionError: Fail-fast error for missing protocols

Changes:
- NodeContractPersistenceEffect accepts optional dependencies parameter
- RegistryInfra*.get_required_protocols() deprecated (contract is source of truth)

Includes 31 unit tests covering resolution, fail-fast behavior, and mock injection.
@jonahgabriel
jonahgabriel force-pushed the jonah/omn-1732-feat-runtime-dependency-injection-for-zero-code-nodes branch from 9c7dc3e to 45110ed Compare February 4, 2026 18:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@tests/unit/services/contract_publisher/test_service.py`:
- Line 136: Three tests use the outdated topic string missing the "platform."
segment; update the inputs passed to ServiceContractPublisher.resolve_topic in
the tests named test_resolve_topic_* so they use
"onex.evt.platform.contract-registered.v1" instead of
"onex.evt.contract-registered.v1" (apply this change to the three occurrences
referenced near lines 488–489, 505–506, and 524–525) so the tests align with
TOPIC_SUFFIX_CONTRACT_REGISTERED and the platform namespace.
🧹 Nitpick comments (2)
tests/unit/runtime/test_contract_dependency_resolver.py (2)

24-26: Consider adding @pytest.mark.unit class decorator.

Per coding guidelines, test files should use pytest markers for test classification. Consider adding @pytest.mark.unit to both test classes.

♻️ Suggested change
+@pytest.mark.unit
 class TestContractDependencyResolver:
     """Tests for ContractDependencyResolver."""

And similarly for TestModelResolvedDependencies at line 421.


129-157: Remove unnecessary @pytest.mark.asyncio from synchronous tests.

Tests for _extract_protocol_dependencies (lines 129-157) and _import_protocol_class (lines 325-365) don't contain async code but have @pytest.mark.asyncio. While not a bug, this adds unnecessary overhead.

♻️ Suggested change for lines 129-141
-    `@pytest.mark.asyncio`
-    async def test_resolve_extracts_protocol_dependencies_type_field(
+    def test_resolve_extracts_protocol_dependencies_type_field(
         self,
         mock_container: MagicMock,
         mock_contract_with_type_field: MagicMock,
     ) -> None:

Similar changes apply to other synchronous test methods.

Comment thread tests/unit/services/contract_publisher/test_service.py
… platform segment

Update test_resolve_topic_* tests to use "onex.evt.platform.contract-registered.v1"
instead of "onex.evt.contract-registered.v1" to match TOPIC_SUFFIX_CONTRACT_REGISTERED
constant and platform-wide namespace evolution.

Addresses CodeRabbit review feedback.
@jonahgabriel
jonahgabriel merged commit 40431df into main Feb 4, 2026
19 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-1732-feat-runtime-dependency-injection-for-zero-code-nodes branch April 4, 2026 02:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant